from odoo import models, fields, api from odoo.exceptions import UserError import logging _logger = logging.getLogger(__name__) class CarrierAccountMixin(models.AbstractModel): """ Carrier Account Mixin. This class provides functionality for handling carrier accounts within an order system. It ensures that the correct carrier account is used based on the delivery billing mode (collect, third party, prepaid). It also provides methods to compute and validate carrier accounts according to the selected carrier and partners involved in the order. Most implementations should override the sender_id and recipient_id fields with related fields that simply point to the res.partner record that is appropriate. For example, the sender_id for a sales order would be company_id.partner_id and its recipient_id would be partner_id. """ _name = "carrier.account.mixin" _description = "Carrier Account Mixin" 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", 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( [ ("no charge", "No Charge"), ("ppc", "Prepaid & Charge"), ("prepaid", "Prepaid"), ("collect", "Collect"), ("third party", "Third Party"), ], help=( """ Prepaid: The shipper will pay the carrier and the client pays the estimate. Prepaid & Charge: The shipper will pay the carrier and bill the client based on the actual price paid. Collect: The recipient will be billed (account information needed) Third Party: A third party will be billed (account information needed) """ ), 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="_on_carrier_fields_changed", store=True, compute_sudo=True, ) carrier_account_owner_id = fields.Many2one( comodel_name="res.partner", related="carrier_account_id.partner_id", string="Carrier Account Owner", ) valid_carrier_account_ids = fields.One2many( comodel_name="delivery.carrier.account", compute="_compute_valid_carrier_account_ids", compute_sudo=True, string="Valid Carrier Accounts", ) 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 def _get_valid_carrier_partners(self): """Get partners that can have valid carrier accounts for the current billing mode. Returns: Tuple containing: - partners: recordset of partners that can have valid accounts - is_third_party: boolean indicating if this is a third party billing mode - invalid_partners: for third party mode, partners that cannot own the account """ self.ensure_one() invalid_partners = self.env["res.partner"].browse() match self.delivery_billing_mode: case "collect": return ( self.recipient_id | self.recipient_id.commercial_partner_id, False, invalid_partners, ) case "prepaid" | "ppc" | "no charge": return ( self.sender_id | self.sender_id.commercial_partner_id, False, invalid_partners, ) case "third party": invalid_partners = ( self.recipient_id | self.recipient_id.commercial_partner_id ) | (self.sender_id | self.sender_id.commercial_partner_id) return self.env["res.partner"].browse(), True, invalid_partners case _: return self.env["res.partner"].browse(), False, invalid_partners @api.depends( "sender_id", "recipient_id", "delivery_billing_mode", ) def _compute_carrier_id(self): for rec in self: # Only set carrier if not already set if not rec.carrier_id: rec.carrier_id = rec._get_default_carrier() continue # Don't reset carrier if we're changing billing mode and there's a valid account if rec.delivery_billing_mode: partners, is_third_party, invalid_partners = ( rec._get_valid_carrier_partners() ) if is_third_party: # For third party, check if there's any account for this carrier # that doesn't belong to sender or recipient has_valid_account = bool( self.env["delivery.carrier.account"].search_count( [ ("delivery_carrier_id", "=", rec.carrier_id.id), ("partner_id", "not in", invalid_partners.ids), ] ) ) if has_valid_account: continue else: # Check if there's a valid account for this carrier if any( account.delivery_carrier_id == rec.carrier_id for account in partners.mapped("carrier_account_ids") ): continue # If we get here, there's no valid account for this carrier 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 ): rec.carrier_account_id = rec._get_default_carrier_account() 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: 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 ) ) 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 ) ) case _: rec.valid_carrier_account_ids = self.env[ "delivery.carrier.account" ].search([]) @api.depends("delivery_billing_mode", "sender_id", "recipient_id") def _compute_valid_carrier_ids(self): """Compute all valid carriers for the current billing mode and partners.""" for rec in self: partners, is_third_party, invalid_partners = ( rec._get_valid_carrier_partners() ) if is_third_party: # For third party, all carriers with accounts not belonging to sender/recipient are valid accounts = self.env["delivery.carrier.account"].search( [ ("partner_id", "not in", invalid_partners.ids), ] ) rec.valid_carrier_ids = accounts.mapped("delivery_carrier_id") else: rec.valid_carrier_ids = partners.mapped( "carrier_account_ids.delivery_carrier_id" ) 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 and rec.carrier_account_id not in rec.valid_carrier_account_ids ): if rec.delivery_billing_mode == "collect": 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"]: 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": raise UserError( "Third party carrier account cannot belong to sender or recipient." )