diff --git a/delivery_carrier_partner_account/models/carrier_account_mixin.py b/delivery_carrier_partner_account/models/carrier_account_mixin.py index 1911e96..d1db19f 100644 --- a/delivery_carrier_partner_account/models/carrier_account_mixin.py +++ b/delivery_carrier_partner_account/models/carrier_account_mixin.py @@ -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." + ) diff --git a/delivery_carrier_partner_account/models/res_partner.py b/delivery_carrier_partner_account/models/res_partner.py index 47a6f66..8168634 100644 --- a/delivery_carrier_partner_account/models/res_partner.py +++ b/delivery_carrier_partner_account/models/res_partner.py @@ -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"] \ No newline at end of file diff --git a/delivery_carrier_partner_account/models/sales_order.py b/delivery_carrier_partner_account/models/sales_order.py index 7d6c8ab..4bd52f6 100644 --- a/delivery_carrier_partner_account/models/sales_order.py +++ b/delivery_carrier_partner_account/models/sales_order.py @@ -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 + ), + } + ) diff --git a/delivery_carrier_partner_account/models/stock_picking.py b/delivery_carrier_partner_account/models/stock_picking.py index d39e949..2fa8c87 100644 --- a/delivery_carrier_partner_account/models/stock_picking.py +++ b/delivery_carrier_partner_account/models/stock_picking.py @@ -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() diff --git a/delivery_carrier_partner_account/tests/test_carrier_account_mixin.py b/delivery_carrier_partner_account/tests/test_carrier_account_mixin.py index 15593ce..6c88794 100644 --- a/delivery_carrier_partner_account/tests/test_carrier_account_mixin.py +++ b/delivery_carrier_partner_account/tests/test_carrier_account_mixin.py @@ -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): diff --git a/delivery_carrier_partner_account/tests/test_sale_order.py b/delivery_carrier_partner_account/tests/test_sale_order.py index 804dc67..3002678 100644 --- a/delivery_carrier_partner_account/tests/test_sale_order.py +++ b/delivery_carrier_partner_account/tests/test_sale_order.py @@ -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}", diff --git a/delivery_carrier_partner_account/wizard/choose_delivery_carrier.py b/delivery_carrier_partner_account/wizard/choose_delivery_carrier.py index 630ef5f..29926b1 100644 --- a/delivery_carrier_partner_account/wizard/choose_delivery_carrier.py +++ b/delivery_carrier_partner_account/wizard/choose_delivery_carrier.py @@ -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