new module delivery_carrier_partner_account started

This commit is contained in:
Marc Durepos 2024-11-20 09:15:59 -05:00
parent f542d8fbf9
commit 34ec1a8230
11 changed files with 489 additions and 0 deletions

View file

@ -0,0 +1 @@
from . import models

View file

@ -0,0 +1,36 @@
#
# Bemade Inc.
#
# Copyright (C) 2023-June Bemade Inc. (<https://www.bemade.org>).
# Author: Marc Durepos (Contact : marc@bemade.org)
#
# This program is under the terms of the GNU Lesser General Public License,
# version 3.
#
# For full license details, see https://www.gnu.org/licenses/lgpl-3.0.en.html.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#
{
"name": "Carrier Accounts by Partner",
"version": "17.0.0.1.0",
"summary": "Add one or many carrier accounts per partner",
"category": "Delivery",
"author": "Bemade Inc.",
"website": "http://www.bemade.org",
"license": "LGPL-3",
"depends": [
"incrementing_sequence_mixin",
"stock_delivery",
],
"data": ["security/ir.model.access.csv"],
"assets": {},
"installable": True,
"auto_install": False,
}

View file

@ -0,0 +1,5 @@
from . import carrier_account_mixin
from . import delivery_carrier_account
from . import res_partner
from . import sales_order
from . import stock_picking

View file

@ -0,0 +1,174 @@
from odoo import models, fields, api, _
from odoo.exceptions import UserError
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")
delivery_billing_mode = fields.Selection(
[
("no charge", "No Charge"),
("prepaid", "Prepaid"),
("collect", "Collect"),
("third party", "Third Party"),
],
help=_(
"""
Prepaid: The shipper will pay the carrier (and may bill the client).
Collect: The recipient will be billed (account information needed)
Third Party: A third party will be billed (account information needed)
"""
),
tracking=1,
string="Delivery Billing Mode",
)
carrier_account_id = fields.Many2one(
comodel_name="delivery.carrier.account",
ondelete="restrict",
compute="_compute_carrier_account_id",
inverse="_inverse_carrier_account_id",
store=True,
compute_sudo=True,
string="Carrier Account",
)
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",
)
@api.depends("delivery_billing_mode", "carrier_id", "recipient_id", "sender_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 == "prepaid":
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, 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
)
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."
)
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 == "prepaid":
rec.carrier_account_id = (
self.env["delivery.carrier.account"]
.search([("partner_id", "=", rec.sender_id.id)])
.filtered(
lambda account: account.delivery_carrier_id == rec.carrier_id
)
)
if (
rec.delivery_billing_mode == "no charge"
or not rec.delivery_billing_mode
):
rec.carrier_account_id = False
@api.constrains("carrier_account_id")
def _check_account_id(self):
for rec in self:
if (
not rec.delivery_billing_mode
or rec.delivery_billing_mode == "no charge"
) and rec.carrier_account_id:
raise UserError(
_("No carrier account should be set for no charge delivery.")
)
# 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 not in rec.valid_carrier_account_ids:
raise UserError(_("Invalid carrier account selected."))
def _inverse_carrier_account_id(self):
pass

View file

@ -0,0 +1,27 @@
from odoo import models, fields
class DeliveryCarrierAccount(models.Model):
_name = "delivery.carrier.account"
_description = "Delivery Carrier Account"
_inherit = ["mail.thread", "mail.activity.mixin"]
delivery_carrier_id = fields.Many2one(
comodel_name="delivery.carrier",
string="Delivery Carriers",
required=True,
readonly=True,
ondelete="restrict",
)
account_number = fields.Char(
required=True,
tracking=1,
)
partner_id = fields.Many2one(
comodel_name="res.partner",
required=True,
readonly=True,
ondelete="cascade",
)

View file

@ -0,0 +1,18 @@
from odoo import models, fields
class Partner(models.Model):
_inherit = "res.partner"
carrier_account_ids = fields.One2many(
comodel_name="delivery.carrier.account",
inverse_name="partner_id",
tracking=2,
string="Carrier Accounts",
)
default_carrier_account_id = fields.Many2one(
comodel_name="delivery.carrier.account",
tracking=1,
ondelete="restrict",
)

View file

@ -0,0 +1,30 @@
from odoo import models, fields, api, _
class SalesOrder(models.Model):
_inherit = ["sale.order", "carrier.account.mixin"]
_name = "sale.order"
recipient_id = fields.Many2one(
comodel_name="res.partner",
related="partner_id",
)
sender_id = fields.Many2one(
comodel_name="res.partner",
related="company_id.partner_id",
)
@api.model
def write(self, values):
res = super().write(values)
# If carrier account ID changes for a confirmed order, change it on its
# pending pickings as well.
if "carrier_account_id" in values:
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.carrier_account_id = rec.carrier_account_id
return res

View file

@ -0,0 +1,31 @@
from odoo import models, fields, api
class Picking(models.Model):
_inherit = ["stock.picking", "carrier.account.mixin"]
_name = "stock.picking"
recipient_id = fields.Many2one(
comodel_name="res.partner",
related="partner_id",
)
sender_id = fields.Many2one(
comodel_name="res.partner",
related="company_id.partner_id",
)
# 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

View file

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
delivery_carrier_partner_account.access_delivery_carrier_account,access_delivery_carrier_account,delivery_carrier_partner_account.model_delivery_carrier_account,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 delivery_carrier_partner_account.access_delivery_carrier_account access_delivery_carrier_account delivery_carrier_partner_account.model_delivery_carrier_account base.group_user 1 1 1 1

View file

@ -0,0 +1 @@
from . import test_carrier_account_mixin

View file

@ -0,0 +1,164 @@
from odoo.tests import TransactionCase, tagged, mute_logger
from odoo import Command
from odoo.exceptions import UserError
@tagged("-at_install", "post_install")
class TestCarrierAccountMixin(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.client_partner = cls.env["res.partner"].create(
{
"name": "Test partner",
}
)
cls.random_partner = cls.env["res.partner"].create(
{
"name": "Third Party",
}
)
cls.delivery_carrier_1 = cls.env.ref("delivery.free_delivery_carrier")
cls.delivery_carrier_2 = cls.env.ref("delivery.delivery_local_delivery")
cls.client_account_1 = cls.env["delivery.carrier.account"].create(
{
"partner_id": cls.client_partner.id,
"delivery_carrier_id": cls.delivery_carrier_1.id,
"account_number": "1234567890",
}
)
cls.client_account_2 = cls.env["delivery.carrier.account"].create(
{
"partner_id": cls.client_partner.id,
"delivery_carrier_id": cls.delivery_carrier_2.id,
"account_number": "0987654321",
}
)
cls.sender_account_1 = cls.env["delivery.carrier.account"].create(
{
"partner_id": cls.env.company.partner_id.id,
"delivery_carrier_id": cls.delivery_carrier_1.id,
"account_number": "hijklmn",
}
)
cls.sender_account_2 = cls.env["delivery.carrier.account"].create(
{
"partner_id": cls.env.company.partner_id.id,
"delivery_carrier_id": cls.delivery_carrier_2.id,
"account_number": "abcdefg",
}
)
cls.third_party_account_1 = cls.env["delivery.carrier.account"].create(
{
"partner_id": cls.random_partner.id,
"delivery_carrier_id": cls.delivery_carrier_1.id,
"account_number": "8910111213",
}
)
cls.third_party_account_2 = cls.env["delivery.carrier.account"].create(
{
"partner_id": cls.random_partner.id,
"delivery_carrier_id": cls.delivery_carrier_2.id,
"account_number": "zzzzzzzzz",
}
)
def test_compute_account_collect_order(self):
order = self._create_sale_order(
"collect",
self.delivery_carrier_1,
False,
)
self.assertEqual(order.carrier_account_id, self.client_account_1)
def test_compute_account_prepaid_order(self):
picking = self.env["stock.picking"].create(
{
"partner_id": self.client_partner.id,
"carrier_id": self.delivery_carrier_2.id,
"picking_type_id": self.env.ref("stock.warehouse0").out_type_id.id,
"delivery_billing_mode": "prepaid",
}
)
self.assertEqual(picking.carrier_account_id, self.sender_account_2)
def test_compute_account_third_party_order(self):
picking = self.env["stock.picking"].create(
{
"partner_id": self.client_partner.id,
"carrier_id": self.delivery_carrier_2.id,
"picking_type_id": self.env.ref("stock.warehouse0").out_type_id.id,
"delivery_billing_mode": "prepaid",
}
)
# No need to assert we have an account selected here. Tested elsewhere.
picking.delivery_billing_mode = "third party"
self.assertFalse(picking.carrier_account_id)
def test_changing_account_on_confirmed_sale_changes_picking(self):
new_account = self.env["delivery.carrier.account"].create(
{
"partner_id": self.client_partner.id,
"delivery_carrier_id": self.delivery_carrier_1.id,
"account_number": "1234567891",
}
)
order = self._create_sale_order("collect", self.delivery_carrier_1, False)
order.action_confirm()
order.carrier_account_id = new_account
self.assertEqual(order.picking_ids.carrier_account_id, new_account)
def test_incorrect_collect_account(self):
with self.assertRaises(UserError):
self._create_sale_order(
"collect",
self.delivery_carrier_1,
self.sender_account_1,
)
with self.assertRaises(UserError):
self._create_sale_order(
"collect",
self.delivery_carrier_1,
self.third_party_account_1,
)
def test_incorrect_prepaid_account(self):
with self.assertRaises(UserError):
self._create_sale_order(
"prepaid",
self.delivery_carrier_1,
self.client_account_1,
)
with self.assertRaises(UserError):
self._create_sale_order(
"prepaid",
self.delivery_carrier_1,
self.third_party_account_1,
)
def test_incorrect_third_party_account(self):
with self.assertRaises(UserError):
self._create_sale_order(
"third party", self.delivery_carrier_1, self.client_account_1
)
with self.assertRaises(UserError):
self._create_sale_order(
"third party", self.delivery_carrier_1, self.sender_account_1
)
def _create_sale_order(self, billing_mode, carrier, account):
vals = {
"partner_id": self.client_partner.id,
"carrier_id": carrier.id,
"delivery_billing_mode": billing_mode,
"order_line": [
Command.create(
{
"product_id": self.env.ref("product.product_product_4").id,
}
)
],
}
if account:
vals["carrier_account_id"] = account.id
return self.env["sale.order"].create(vals)