Significant rework of delivery_carrier_account.

carrier_id, carrier_account_id and delivery_billing_mode fields are all computed and have a single inverse method that can be overriden by subclasses.
This commit is contained in:
Marc Durepos 2025-01-21 16:16:14 -05:00
parent f45cd3a633
commit 4563b8b8b1
7 changed files with 286 additions and 172 deletions

View file

@ -1,5 +1,8 @@
from odoo import models, fields, api
from odoo import models, fields, api
from odoo.exceptions import UserError
import logging
_logger = logging.getLogger(__name__)
class CarrierAccountMixin(models.AbstractModel):
@ -23,7 +26,17 @@ class CarrierAccountMixin(models.AbstractModel):
sender_id = fields.Many2one(comodel_name="res.partner", string="Sender")
recipient_id = fields.Many2one(comodel_name="res.partner", string="Recipient")
carrier_id = fields.Many2one(comodel_name="delivery.carrier", string="Carrier")
carrier_id = fields.Many2one(
comodel_name="delivery.carrier",
string="Carrier",
compute="_compute_carrier_id",
store=True,
inverse="_on_carrier_fields_changed",
compute_sudo=True,
)
_default_carrier_field = "property_delivery_carrier_id"
_default_carrier_account_field = "default_carrier_account_id"
delivery_billing_mode = fields.Selection(
[
@ -42,16 +55,20 @@ class CarrierAccountMixin(models.AbstractModel):
"""
),
string="Delivery Billing Mode",
compute="_compute_delivery_billing_mode",
inverse="_on_carrier_fields_changed",
store=True,
compute_sudo=True,
)
carrier_account_id = fields.Many2one(
comodel_name="delivery.carrier.account",
ondelete="restrict",
string="Carrier Account",
compute="_compute_carrier_account_id",
inverse="_inverse_carrier_account_id",
inverse="_on_carrier_fields_changed",
store=True,
compute_sudo=True,
string="Carrier Account",
)
carrier_account_owner_id = fields.Many2one(
@ -67,120 +84,205 @@ class CarrierAccountMixin(models.AbstractModel):
string="Valid Carrier Accounts",
)
@api.depends("delivery_billing_mode", "carrier_id", "recipient_id", "sender_id")
valid_carrier_ids = fields.One2many(
comodel_name="delivery.carrier",
compute="_compute_valid_carrier_ids",
compute_sudo=True,
)
def _on_carrier_fields_changed(self):
"""Hook for subclasses to perform additional actions when carrier fields change."""
pass
@api.depends(
"valid_carrier_account_ids",
"sender_id",
"recipient_id",
"delivery_billing_mode",
)
def _compute_carrier_id(self):
for rec in self.filtered(
lambda rec: not rec.carrier_id
or rec.carrier_id not in rec.valid_carrier_ids
):
rec.carrier_id = rec._get_default_carrier()
def _get_default_carrier(self):
self.ensure_one()
recipient = self.recipient_id
sender = self.sender_id
def_car = self._default_carrier_field
match self.delivery_billing_mode:
case "collect":
return getattr(recipient, def_car) or getattr(
recipient.commercial_partner_id, def_car
)
case "ppc" | "prepaid" | "no charge":
return getattr(sender, def_car) or getattr(
sender.commercial_partner_id, def_car
)
case _:
return False
@api.depends(
"sender_id",
"recipient_id",
"delivery_billing_mode",
"carrier_id",
"valid_carrier_account_ids",
)
def _compute_carrier_account_id(self):
for rec in self.filtered(
lambda rec: not rec.carrier_account_id
or rec.carrier_account_id not in rec.valid_carrier_account_ids
):
_logger.debug(
f"Setting carrier account. Carrier: {rec.carrier_id},"
f" billing mode: {rec.delivery_billing_mode}"
f" account: {rec.carrier_account_id}"
)
rec.carrier_account_id = rec._get_default_carrier_account()
_logger.debug(f"Set account to {rec.carrier_account_id}")
def _get_default_carrier_account(self):
self.ensure_one()
match self.delivery_billing_mode:
case "collect":
default_acct = getattr(
self.recipient_id, self._default_carrier_account_field
) or getattr(
self.recipient_id.commercial_partner_id,
self._default_carrier_account_field,
)
if default_acct and default_acct.delivery_carrier_id == self.carrier_id:
return default_acct
return self.recipient_id.get_carrier_account(self.carrier_id)
case "ppc" | "prepaid" | "no charge":
default_acct = getattr(
self.sender_id, self._default_carrier_account_field
) or getattr(
self.sender_id.commercial_partner_id,
self._default_carrier_account_field,
)
if default_acct and default_acct.delivery_carrier_id == self.carrier_id:
return default_acct
return self.sender_id.get_carrier_account(self.carrier_id)
case _:
return False
@api.depends("carrier_account_id")
def _compute_delivery_billing_mode(self):
for rec in self.filtered(lambda rec: not rec.delivery_billing_mode):
if not rec.carrier_account_id:
rec.delivery_billing_mode = False
continue
account_partner = rec.carrier_account_id.partner_id
if account_partner in (
rec.recipient_id | rec.recipient_id.commercial_partner_id
):
rec.delivery_billing_mode = "collect"
elif account_partner in (
rec.sender_id | rec.sender_id.commercial_partner_id
):
rec.delivery_billing_mode = "ppc"
else:
rec.delivery_billing_mode = "third party"
@api.depends(
"delivery_billing_mode",
"carrier_id",
"recipient_id",
"sender_id",
"carrier_account_id",
)
def _compute_valid_carrier_account_ids(self):
for rec in self:
if rec.delivery_billing_mode == "collect":
rec.valid_carrier_account_ids = (
(rec.recipient_id | rec.recipient_id.commercial_partner_id)
.mapped("carrier_account_ids")
.filtered(
lambda account: account.delivery_carrier_id == rec.carrier_id
)
)
if rec.delivery_billing_mode == "third party":
rec.valid_carrier_account_ids = self.env[
"delivery.carrier.account"
].search(
[
("delivery_carrier_id", "=", rec.carrier_id.id),
(
"partner_id",
"not in",
[
rec.sender_id.id,
rec.recipient_id.id,
rec.recipient_id.commercial_partner_id.id,
],
),
]
)
if rec.delivery_billing_mode in ["prepaid", "ppc"]:
rec.valid_carrier_account_ids = (
rec.sender_id.carrier_account_ids.filtered(
lambda account: account.delivery_carrier_id == rec.carrier_id
)
)
if rec.delivery_billing_mode == "no charge":
rec.valid_carrier_account_ids = self.env["delivery.carrier.account"]
if not rec.delivery_billing_mode:
rec.valid_carrier_account_ids = self.env["delivery.carrier.account"]
@api.depends("delivery_billing_mode", "carrier_id", "valid_carrier_account_ids")
def _compute_carrier_account_id(self):
"""Compute the carrier account to use for this record if one is not set or if
the current one doesn't match the carrier_id selected.
When delivery_billing_mode is collect, we need to choose a carrier account that
matches both the carrier_id and the partner_id or its commercial partner.
When it is third party, any account matching the carrier_id is fine.
When it is prepaid or ppc, we select the company's account.
"""
for rec in self:
if rec.delivery_billing_mode == "collect":
if rec.carrier_account_id not in rec.valid_carrier_account_ids:
if (
rec.recipient_id.default_carrier_account_id.delivery_carrier_id
== rec.carrier_id
):
rec.carrier_account_id = (
rec.recipient_id.default_carrier_account_id
match rec.delivery_billing_mode:
case "collect":
rec.valid_carrier_account_ids = (
(rec.recipient_id | rec.recipient_id.commercial_partner_id)
.mapped("carrier_account_ids")
.filtered(
lambda account: account.delivery_carrier_id
== rec.carrier_id
)
elif rec.valid_carrier_account_ids:
rec.carrier_account_id = rec.valid_carrier_account_ids[0]
else:
raise UserError(
"The client does not have an account with the selected carrier."
)
case "third party":
rec.valid_carrier_account_ids = self.env[
"delivery.carrier.account"
].search(
[
("delivery_carrier_id", "=", rec.carrier_id.id),
(
"partner_id",
"not in",
[
rec.sender_id.id,
rec.sender_id.commercial_partner_id.id,
rec.recipient_id.id,
rec.recipient_id.commercial_partner_id.id,
],
),
]
)
case "prepaid" | "ppc" | "no charge":
rec.valid_carrier_account_ids = (
(rec.sender_id | rec.sender_id.commercial_partner_id)
.mapped("carrier_account_ids")
.filtered(
lambda account: account.delivery_carrier_id
== rec.carrier_id
)
if rec.delivery_billing_mode == "third party":
if rec.carrier_account_id not in rec.valid_carrier_account_ids:
rec.carrier_account_id = False
if rec.delivery_billing_mode in ["prepaid", "ppc"]:
accounts = (
self.env["delivery.carrier.account"]
.search([("partner_id", "=", rec.sender_id.id)])
.filtered(
lambda account: account.delivery_carrier_id == rec.carrier_id
)
)
if accounts:
rec.carrier_account_id = accounts[0]
if (
rec.delivery_billing_mode == "no charge"
or not rec.delivery_billing_mode
):
rec.carrier_account_id = False
case _:
rec.valid_carrier_account_ids = self.env[
"delivery.carrier.account"
].search([])
@api.constrains("carrier_account_id")
def _check_account_id(self):
@api.depends("valid_carrier_account_ids")
def _compute_valid_carrier_ids(self):
for rec in self:
if (
not rec.delivery_billing_mode
or rec.delivery_billing_mode == "no charge"
):
if rec.carrier_account_id:
raise UserError(
"No carrier account should be set for no charge delivery."
)
continue
# We allow empty carrier account for third party since we can't always
# set it automatically.
if (
rec.delivery_billing_mode == "third party"
and not rec.carrier_account_id
):
continue
if (
rec.carrier_account_id
and rec.carrier_account_id not in rec.valid_carrier_account_ids
):
raise UserError(
f"Invalid carrier account selected. Account: {rec.carrier_account_id} for carrier {rec.carrier_id} from sender {rec.sender_id} to recipient {rec.recipient_id} in mode {rec.delivery_billing_mode}."
f"\nSender accounts: {rec.sender_id.carrier_account_ids}"
f"\nRecipient accounts: {rec.recipient_id.carrier_account_ids}"
)
rec.valid_carrier_ids = rec.valid_carrier_account_ids.mapped(
"delivery_carrier_id"
)
def _inverse_carrier_account_id(self):
def _on_carrier_fields_changed(self):
pass
@api.constrains("delivery_billing_mode", "carrier_id", "carrier_account_id")
def _check_carrier_account(self):
for rec in self:
if rec.carrier_account_id and rec.delivery_billing_mode:
if (
rec.delivery_billing_mode == "collect"
and rec.carrier_account_id
not in (
rec.recipient_id | rec.recipient_id.commercial_partner_id
).carrier_account_ids
):
raise UserError(
"Carrier account is not associated with the recipient, but billing mode is collect."
)
elif (
rec.delivery_billing_mode in ["prepaid", "ppc", "no charge"]
and rec.carrier_account_id
not in (
rec.sender_id | rec.sender_id.commercial_partner_id
).carrier_account_ids
):
raise UserError(
"Carrier account is not associated with the sender, but billing mode is prepaid, ppc or no charge."
)
elif (
rec.delivery_billing_mode == "third party"
and rec.carrier_account_id
in (
rec.sender_id
| rec.sender_id.commercial_partner_id
| rec.recipient_id
| rec.recipient_id.commercial_partner_id
).carrier_account_ids
):
raise UserError(
"Third party carrier account cannot belong to sender or recipient."
)

View file

@ -33,3 +33,17 @@ class Partner(models.Model):
if not rec.default_carrier_account_id and rec.carrier_account_ids:
rec.default_carrier_account_id = rec.carrier_account_ids[0]
return res
def get_carrier_account(self, carrier):
self.ensure_one()
own_accounts = self.carrier_account_ids.filtered(
lambda account: account.delivery_carrier_id == carrier
)
if own_accounts:
return own_accounts[0]
commercial_patner_accounts = self.commercial_partner_id.carrier_account_ids.filtered(
lambda account: account.delivery_carrier_id == carrier
)
if commercial_patner_accounts:
return commercial_patner_accounts[0]
return self.env["delivery.carrier.account"]

View file

@ -1,4 +1,7 @@
from odoo import models, fields, api, _
import logging
_logger = logging.getLogger(__name__)
class SalesOrder(models.Model):
@ -14,41 +17,34 @@ class SalesOrder(models.Model):
related="warehouse_id.partner_id",
)
@api.model
def write(self, vals):
res = super().write(vals)
if (
"carrier_account_id" in vals
or "carrier_id" in vals
or "delivery_billing_mode" in vals
):
for rec in self.filtered(
lambda order: order.state not in ["draft", "sent"]
):
for picking in rec.picking_ids.filtered(
lambda pick: pick.state not in ["done", "cancel"]
):
picking.write(
{
"carrier_id": rec.carrier_id,
"carrier_account_id": rec.carrier_account_id,
"delivery_billing_mode": rec.delivery_billing_mode,
}
)
return res
def _create_delivery_line(self, carrier, price_unit):
line = super()._create_delivery_line(carrier, price_unit)
name = line.name
delivery_billing_mode = self.delivery_billing_mode or self.env.context.get(
"delivery_billing_mode", False
)
carrier_account = self.carrier_account_id or self.env.context.get(
"carrier_account", False
)
delivery_billing_mode = self.delivery_billing_mode
carrier_account = self.carrier_account_id
if delivery_billing_mode:
name = name + f" [{delivery_billing_mode.upper()}]"
mode_display = delivery_billing_mode.upper()
name = name + f" [{mode_display}]"
if delivery_billing_mode in ["collect", "third party"] and carrier_account:
name = name + f" #{carrier_account.account_number}"
line.name = name
return line
def _on_carrier_fields_changed(self):
"""Propagate carrier field changes to pickings."""
super()._on_carrier_fields_changed()
_logger.debug("In sale_order._on_carrier_fields_changed")
for rec in self:
for picking in rec.picking_ids.filtered(
lambda pick: pick.state not in ["done", "cancel"]
):
_logger.debug("Writing to picking")
picking.write(
{
"carrier_id": rec.carrier_id and rec.carrier_id.id,
"delivery_billing_mode": rec.delivery_billing_mode,
"carrier_account_id": (
rec.carrier_account_id and rec.carrier_account_id.id
),
}
)

View file

@ -7,28 +7,27 @@ class Picking(models.Model):
recipient_id = fields.Many2one(
comodel_name="res.partner",
related="partner_id",
compute="_compute_sender_recipient",
)
sender_id = fields.Many2one(
comodel_name="res.partner",
related="company_id.partner_id",
compute="_compute_sender_recipient",
)
# Override to base it on the sale order field initially and when changed
delivery_billing_mode = fields.Selection(
compute="_compute_delivery_billing_mode",
inverse="_inverse_delivery_billing_mode",
store=True,
)
@api.depends("sale_id", "sale_id.delivery_billing_mode")
def _compute_delivery_billing_mode(self):
for rec in self:
rec.delivery_billing_mode = rec.sale_id.delivery_billing_mode
rec.carrier_account_id = rec.sale_id.carrier_account_id
def _inverse_delivery_billing_mode(self):
pass
def _compute_sender_recipient(self):
for picking in self:
dest_usage = picking.location_dest_id.usage
src_usage = picking.location_id.usage
match (src_usage, dest_usage):
case ("internal", "customer") | ("internal", "supplier"):
picking.recipient_id = picking.partner_id
picking.sender_id = picking.picking_type_id.warehouse_id.partner_id or picking.company_id.partner_id
case ("customer", "internal") | ("supplier", "internal"):
picking.recipient_id = picking.picking_type_id.warehouse_id.partner_id or picking.company_id.partner_id
picking.sender_id = picking.partner_id
case _:
picking.recipient_id = picking.location_dest_id.warehouse_id.partner_id or picking.partner_id
picking.sender_id = picking.location_id.warehouse_id.partner_id or picking.partner_id
def _add_delivery_cost_to_so(self):
self.ensure_one()

View file

@ -1,5 +1,10 @@
from .test_carrier_account_common import TestCarrierAccountCommon
from odoo.exceptions import UserError
from odoo.tests import Form
import logging
_logger = logging.getLogger(__name__)
class TestCarrierAccountMixin(TestCarrierAccountCommon):
@ -42,8 +47,9 @@ class TestCarrierAccountMixin(TestCarrierAccountCommon):
"delivery_billing_mode": "prepaid",
}
)
# No need to assert we have an account selected here. Tested elsewhere.
picking.delivery_billing_mode = "third party"
with Form(picking) as form: # Use a form here to trigger recomputation
form.delivery_billing_mode = "third party"
picking = form.record
self.assertFalse(picking.carrier_account_id)
def test_changing_account_on_confirmed_sale_changes_picking(self):

View file

@ -36,6 +36,7 @@ class TestSalesOrder(TestCarrierAccountCommon):
wiz.delivery_billing_mode = "third party"
wiz.carrier_account_id = self.third_party_account_1
wiz.button_confirm()
self.assertEqual(order.carrier_account_id, self.third_party_account_1)
self.assertEqual(
order.order_line[0].name,
f"{self.delivery_carrier_1.name} [THIRD PARTY] #{self.third_party_account_1.account_number}",

View file

@ -11,17 +11,13 @@ class ChooseDeliveryCarrier(models.TransientModel):
recipient_id = fields.Many2one(related="partner_id")
def button_confirm(self):
res = super(
ChooseDeliveryCarrier,
self.with_context(
delivery_billing_mode=self.delivery_billing_mode,
carrier_account=self.carrier_account_id,
),
).button_confirm()
extra_vals = {}
vals = {}
if self.delivery_billing_mode:
extra_vals.update(delivery_billing_mode=self.delivery_billing_mode)
vals.update(delivery_billing_mode=self.delivery_billing_mode)
if self.carrier_account_id:
extra_vals.update(carrier_account_id=self.carrier_account_id)
self.order_id.write(extra_vals)
vals.update(carrier_account_id=self.carrier_account_id)
if self.carrier_id:
vals.update(carrier_id=self.carrier_id.id)
self.order_id.with_context(no_carrier_update=True).write(vals)
res = super().button_confirm()
return res