new module delivery_carrier_partner_account started
This commit is contained in:
parent
f542d8fbf9
commit
34ec1a8230
11 changed files with 489 additions and 0 deletions
1
delivery_carrier_partner_account/__init__.py
Normal file
1
delivery_carrier_partner_account/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
36
delivery_carrier_partner_account/__manifest__.py
Normal file
36
delivery_carrier_partner_account/__manifest__.py
Normal 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,
|
||||
}
|
||||
5
delivery_carrier_partner_account/models/__init__.py
Normal file
5
delivery_carrier_partner_account/models/__init__.py
Normal 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
|
||||
174
delivery_carrier_partner_account/models/carrier_account_mixin.py
Normal file
174
delivery_carrier_partner_account/models/carrier_account_mixin.py
Normal 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
|
||||
|
|
@ -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",
|
||||
)
|
||||
18
delivery_carrier_partner_account/models/res_partner.py
Normal file
18
delivery_carrier_partner_account/models/res_partner.py
Normal 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",
|
||||
)
|
||||
30
delivery_carrier_partner_account/models/sales_order.py
Normal file
30
delivery_carrier_partner_account/models/sales_order.py
Normal 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
|
||||
31
delivery_carrier_partner_account/models/stock_picking.py
Normal file
31
delivery_carrier_partner_account/models/stock_picking.py
Normal 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
|
||||
|
|
@ -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
delivery_carrier_partner_account/tests/__init__.py
Normal file
1
delivery_carrier_partner_account/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import test_carrier_account_mixin
|
||||
|
|
@ -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)
|
||||
Loading…
Reference in a new issue