Merge branch '17.0' of git.bemade.org:bemade/bemade-addons into 17.0
This commit is contained in:
commit
b6eca04802
34 changed files with 851 additions and 18 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
'name': 'Automated Partner Association by Email Domain',
|
||||
'version': '17.0.0.0.0',
|
||||
'version': '17.0.0.0.1',
|
||||
'category': 'Extra Tools',
|
||||
'summary': 'Automatically associates partners with companies using matching email domains',
|
||||
'description': """
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from odoo import api, fields, models, Command
|
||||
from odoo.tools.sql import SQL
|
||||
import uuid
|
||||
|
||||
from odoo.addons.website.controllers.main import Website
|
||||
|
|
@ -37,7 +38,6 @@ class Partner(models.Model):
|
|||
)
|
||||
template.with_context(links=links).send_mail(self.id, force_send=True)
|
||||
|
||||
# @api.onchange('email')
|
||||
def _check_parent_from_email_domain(self):
|
||||
for rec in self:
|
||||
if rec.parent_id:
|
||||
|
|
@ -52,21 +52,27 @@ class Partner(models.Model):
|
|||
# If there's no '@' symbol, the email address is invalid
|
||||
return False
|
||||
|
||||
# Loop while there's more than one part in the domain (e.g., subdomain.domain.tld)
|
||||
while "." in email_domain:
|
||||
# If the current email domain matches the main domain, return True
|
||||
company_domain = self.env["res.partner"].search(
|
||||
[("email_domain", "ilike", email_domain)]
|
||||
sql = """
|
||||
SELECT id
|
||||
FROM res_partner
|
||||
WHERE email_domain IS NOT NULL
|
||||
AND email_domain = RIGHT(%s, LENGTH(email_domain))
|
||||
AND company_id = %s
|
||||
"""
|
||||
self.env.cr.execute(
|
||||
SQL(sql, email_domain, self.env.company and self.env.company.id or None)
|
||||
)
|
||||
res = self.env.cr.fetchall()
|
||||
if len(res) > 1:
|
||||
rec._send_selection_email(
|
||||
self.env["res.partner"].search(
|
||||
[("id", "in", [result[0] for result in res])]
|
||||
)
|
||||
)
|
||||
if company_domain:
|
||||
if len(company_domain) > 1:
|
||||
rec._send_selection_email(company_domain)
|
||||
else:
|
||||
rec.parent_id = company_domain.id
|
||||
return
|
||||
|
||||
# If not, drop the part before the first '.' to check the next level
|
||||
email_domain = email_domain.split(".", 1)[1]
|
||||
elif len(res) == 1:
|
||||
rec.write({"parent_id": res[0][0]})
|
||||
else:
|
||||
continue
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
|
|
|
|||
2
delivery_carrier_partner_account/__init__.py
Normal file
2
delivery_carrier_partner_account/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from . import models
|
||||
from . import wizard
|
||||
45
delivery_carrier_partner_account/__manifest__.py
Normal file
45
delivery_carrier_partner_account/__manifest__.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
#
|
||||
# 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",
|
||||
"data/actions.xml",
|
||||
"views/res_partner_views.xml",
|
||||
"views/delivery_carrier_views.xml",
|
||||
"views/delivery_carrier_account_views.xml",
|
||||
"views/sale_order_views.xml",
|
||||
"views/stock_picking_views.xml",
|
||||
"wizard/choose_delivery_carrier_views.xml",
|
||||
],
|
||||
"assets": {},
|
||||
"installable": True,
|
||||
"auto_install": False,
|
||||
}
|
||||
11
delivery_carrier_partner_account/data/actions.xml
Normal file
11
delivery_carrier_partner_account/data/actions.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="action_open_delivery_carrier_accounts" model="ir.actions.act_window">
|
||||
<field name="name">Carrier Accounts</field>
|
||||
<field name="res_model">delivery.carrier.account</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="target">current</field>
|
||||
<field name="domain">[('delivery_carrier_id', '=', context.get("carrier_id"))]</field>
|
||||
<field name="context">{'default_delivery_carrier_id': active_id}</field>
|
||||
</record>
|
||||
</odoo>
|
||||
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
|
||||
175
delivery_carrier_partner_account/models/carrier_account_mixin.py
Normal file
175
delivery_carrier_partner_account/models/carrier_account_mixin.py
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
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)
|
||||
"""
|
||||
),
|
||||
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"
|
||||
):
|
||||
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 not in rec.valid_carrier_account_ids:
|
||||
raise UserError(_("Invalid carrier account selected."))
|
||||
|
||||
def _inverse_carrier_account_id(self):
|
||||
pass
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
|
||||
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,
|
||||
ondelete="restrict",
|
||||
)
|
||||
|
||||
account_number = fields.Char(
|
||||
required=True,
|
||||
tracking=1,
|
||||
)
|
||||
|
||||
partner_id = fields.Many2one(
|
||||
comodel_name="res.partner",
|
||||
required=True,
|
||||
ondelete="cascade",
|
||||
)
|
||||
|
||||
@api.depends("account_number")
|
||||
def _compute_display_name(self):
|
||||
for record in self:
|
||||
record.display_name = record.account_number
|
||||
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
|
||||
|
4
delivery_carrier_partner_account/tests/__init__.py
Normal file
4
delivery_carrier_partner_account/tests/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from . import test_carrier_account_common
|
||||
from . import test_carrier_account_mixin
|
||||
from . import test_choose_delivery_carrier
|
||||
from . import test_sale_order
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
from odoo.tests import TransactionCase, tagged
|
||||
from odoo import Command
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestCarrierAccountCommon(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 _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)
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
from .test_carrier_account_common import TestCarrierAccountCommon
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class TestCarrierAccountMixin(TestCarrierAccountCommon):
|
||||
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
|
||||
)
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
from .test_carrier_account_common import TestCarrierAccountCommon
|
||||
|
||||
|
||||
class TestChooseDeliveryCarrier(TestCarrierAccountCommon):
|
||||
def test_sale_order_add_transport(self):
|
||||
order = self.env["sale.order"].create(
|
||||
{
|
||||
"partner_id": self.client_partner.id,
|
||||
}
|
||||
)
|
||||
wizard_action = order.action_open_delivery_wizard()
|
||||
|
||||
wizard = (
|
||||
self.env[wizard_action["res_model"]]
|
||||
.with_context(wizard_action["context"])
|
||||
.create({})
|
||||
)
|
||||
wizard.carrier_id = self.delivery_carrier_1
|
||||
wizard.delivery_billing_mode = "collect"
|
||||
wizard.button_confirm()
|
||||
self.assertEqual(order.carrier_id, self.delivery_carrier_1)
|
||||
self.assertEqual(order.carrier_account_id, self.client_account_1)
|
||||
self.assertEqual(order.delivery_billing_mode, "collect")
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
from .test_carrier_account_common import TestCarrierAccountCommon
|
||||
|
||||
|
||||
class TestSalesOrder(TestCarrierAccountCommon):
|
||||
def test_sales_order_creation_with_default_account(self):
|
||||
self.client_partner.property_delivery_carrier_id = self.delivery_carrier_1
|
||||
self.client_partner.default_carrier_account_id = self.client_account_1
|
||||
self.env["sale.order"].create({"partner_id": self.client_partner.id})
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="delivery_carrier_account_view_tree" model="ir.ui.view">
|
||||
<field name="name">delivery.carrier.account.view.tree</field>
|
||||
<field name="model">delivery.carrier.account</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="bottom" multi_edit="1">
|
||||
<field name="delivery_carrier_id" column_invisible="context.get('carrier_id')"/>
|
||||
<field name="partner_id" readonly="id"/>
|
||||
<field name="account_number"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="delivery_carrier_view_form" model="ir.ui.view">
|
||||
<field name="inherit_id" ref="delivery.view_delivery_carrier_form"/>
|
||||
<field name="model">delivery.carrier</field>
|
||||
<field name="name">delivery.carrier.view.form</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="delivery_carrier_partner_account.action_open_delivery_carrier_accounts"
|
||||
type="action"
|
||||
string="Accounts"
|
||||
class="oe_highlight"
|
||||
icon="fa-dollar"
|
||||
context="{'carrier_id': id}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
29
delivery_carrier_partner_account/views/res_partner_views.xml
Normal file
29
delivery_carrier_partner_account/views/res_partner_views.xml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="res_partner_view_form" model="ir.ui.view">
|
||||
<field name="name">res.partner.view.form</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="delivery.view_partner_property_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page name="logistics" string="Logistics">
|
||||
<group name="defaults" string="Defaults">
|
||||
<field name="property_delivery_carrier_id" position="move"/>
|
||||
<field invisible="not property_delivery_carrier_id"
|
||||
name="default_carrier_account_id"
|
||||
domain="[('delivery_carrier_id', '=', property_delivery_carrier_id), ('id', 'in', carrier_account_ids)]"
|
||||
/>
|
||||
</group>
|
||||
<group name="carrier_accounts" string="Carrier Accounts">
|
||||
<field name="carrier_account_ids">
|
||||
<tree editable="bottom">
|
||||
<field name="delivery_carrier_id" readonly="id"/>
|
||||
<field name="account_number"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
18
delivery_carrier_partner_account/views/sale_order_views.xml
Normal file
18
delivery_carrier_partner_account/views/sale_order_views.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="sale_order_view_form" model="ir.ui.view">
|
||||
<field name="name">sale.order.view.form</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<group name="sale_shipping" position="inside">
|
||||
<field name="valid_carrier_account_ids" invisible="1"/>
|
||||
<field name="carrier_id"/>
|
||||
<field name="delivery_billing_mode"/>
|
||||
<field name="carrier_account_id"
|
||||
domain="[('id', 'in', valid_carrier_account_ids)]"
|
||||
readonly="not delivery_billing_mode or delivery_billing_mode == 'no charge'"/>
|
||||
</group>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="stock_picking_view_form" model="ir.ui.view">
|
||||
<field name="name">stock.picking.view.form</field>
|
||||
<field name="model">stock.picking</field>
|
||||
<field name="inherit_id" ref="stock_delivery.view_picking_withcarrier_out_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="carrier_id" position="after">
|
||||
<field name="valid_carrier_account_ids" invisible="1"/>
|
||||
<field name="delivery_billing_mode"/>
|
||||
<field name="carrier_account_id"
|
||||
domain="[('id', 'in', valid_carrier_account_ids)]"
|
||||
readonly="not delivery_billing_mode or delivery_billing_mode == 'no charge'"
|
||||
/>
|
||||
</field>
|
||||
</field>
|
||||
|
||||
</record>
|
||||
</odoo>
|
||||
1
delivery_carrier_partner_account/wizard/__init__.py
Normal file
1
delivery_carrier_partner_account/wizard/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import choose_delivery_carrier
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
from odoo import models, fields
|
||||
|
||||
|
||||
class ChooseDeliveryCarrier(models.TransientModel):
|
||||
"""Add options to select the carrier account and billing mode."""
|
||||
|
||||
_inherit = ["choose.delivery.carrier", "carrier.account.mixin"]
|
||||
_name = "choose.delivery.carrier"
|
||||
|
||||
sender_id = fields.Many2one(related="company_id.partner_id")
|
||||
recipient_id = fields.Many2one(related="partner_id")
|
||||
|
||||
def button_confirm(self):
|
||||
res = super().button_confirm()
|
||||
extra_vals = {}
|
||||
if self.delivery_billing_mode:
|
||||
extra_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)
|
||||
return res
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="choose_delivery_carrier_view_form" model="ir.ui.view">
|
||||
<field name="name">choose.delivery.carrier.view.form</field>
|
||||
<field name="model">choose.delivery.carrier</field>
|
||||
<field name="inherit_id" ref="delivery.choose_delivery_carrier_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="carrier_id" position="after">
|
||||
<field name="delivery_billing_mode"/>
|
||||
<field name="valid_carrier_account_ids" invisible="1"/>
|
||||
<field
|
||||
name="carrier_account_id"
|
||||
domain="[('id', 'in', valid_carrier_account_ids)]"
|
||||
/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
2
fsm_visit_confirmation/__init__.py
Normal file
2
fsm_visit_confirmation/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from . import controllers
|
||||
from . import models
|
||||
33
fsm_visit_confirmation/__manifest__.py
Normal file
33
fsm_visit_confirmation/__manifest__.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#
|
||||
# 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": "FSM Visit Confirmation",
|
||||
"version": "17.0.0.1.0",
|
||||
"summary": "Have clients confirm tentatively booked visits",
|
||||
"category": "Services/Field Service",
|
||||
"author": "Bemade Inc.",
|
||||
"website": "http://www.bemade.org",
|
||||
"license": "LGPL-3",
|
||||
"depends": ["industry_fsm"],
|
||||
"data": ["data/mail_templates.xml"],
|
||||
"assets": {},
|
||||
"installable": True,
|
||||
"auto_install": False,
|
||||
}
|
||||
1
fsm_visit_confirmation/controllers/__init__.py
Normal file
1
fsm_visit_confirmation/controllers/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import portal
|
||||
35
fsm_visit_confirmation/controllers/portal.py
Normal file
35
fsm_visit_confirmation/controllers/portal.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
from odoo.addons.portal.controllers.portal import CustomerPortal
|
||||
from odoo.http import request, route
|
||||
from odoo.exceptions import AccessError, MissingError
|
||||
|
||||
|
||||
class FsmCustomerPortal(CustomerPortal):
|
||||
@route(
|
||||
"/my/tasks/approve_booking/<int:task_id>",
|
||||
type="http",
|
||||
auth="public",
|
||||
website=True,
|
||||
)
|
||||
def portal_approve_booking(self, task_id, access_token=None):
|
||||
try:
|
||||
visit_sudo = self._document_check_access(
|
||||
"project.task", task_id, access_token=access_token
|
||||
)
|
||||
except (AccessError, MissingError):
|
||||
return request.redirect("/my")
|
||||
|
||||
visit_sudo.action_approve_booking()
|
||||
request.session["visit_confirmation_accepted"] = True
|
||||
request.redirect(f"/my/tasks/{task_id}")
|
||||
|
||||
def _task_get_page_view_values(self, task, access_token, **kwargs):
|
||||
vals = super()._task_get_page_view_values(task, access_token, **kwargs)
|
||||
if request.session.pop("visit_confirmation_accepted", False):
|
||||
vals.update(visit_confirmation_accepted=True)
|
||||
return vals
|
||||
|
||||
def _prepare_home_portal_values(self, counters):
|
||||
vals = super()._prepare_home_portal_values(counters)
|
||||
if request.session.pop("visit_confirmation_accepted", False):
|
||||
vals.update(visit_confirmation_accepted=True)
|
||||
return vals
|
||||
47
fsm_visit_confirmation/data/mail_templates.xml
Normal file
47
fsm_visit_confirmation/data/mail_templates.xml
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="request_fsm_task_date_confirmation" model="mail.template">
|
||||
<field name="name">Field Service Visit Confirmation Request</field>
|
||||
<field name="model_id" ref="project.model_project_task"/>
|
||||
<field name="subject">Please confirm the date for your upcoming Durpro service visit</field>
|
||||
<field name="email_from">{{ object.company_id.email_formatted }}</field>
|
||||
<field name="body_html">
|
||||
<table class="table table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td scope="row">Summary</td>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td scope="row">Technician Arrival</td>
|
||||
<td>{{ object.planned_date_begin }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td scope="row">Intervention address</td>
|
||||
<td>
|
||||
<span t-esc="object.partner_id.street"/><br/>
|
||||
<span t-if="object.partner_id.street2" t-esc="object.partner_id.street2"/><br/>
|
||||
<span t-esc="object.partner_id.city"/><br/>
|
||||
<span t-esc="object.partner_id.state"/><br/>
|
||||
<span t-esc="object.partner_id.zip"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td scope="row">
|
||||
Assigned Technician(s):
|
||||
</td>
|
||||
<td>
|
||||
<t t-foreach="object.user_ids" t-as="technician">
|
||||
<t t-esc="technician.name"/><br/>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>To confirm this visit, <a href="{{ user.company_id.website }}/my/tasks/approve_booking/?task_id={{ object.id }}&access_token={{ object.access_token}}">click here</a>.</p>
|
||||
<p>If you would like to propose another time for this visit, please reply to this email.</p>
|
||||
<p>Best regards,</p>
|
||||
<p>The {{ object.company_id.name }} service management team.</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
1
fsm_visit_confirmation/models/__init__.py
Normal file
1
fsm_visit_confirmation/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import project_task
|
||||
9
fsm_visit_confirmation/models/project_task.py
Normal file
9
fsm_visit_confirmation/models/project_task.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class Task(models.Model):
|
||||
_inherit = "project.task"
|
||||
|
||||
def action_approve_booking(self):
|
||||
self.ensure_one()
|
||||
self.state = "03_approved"
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<template id="portal_my_task" inherit_id="project.portal_my_task">
|
||||
<xpath expr="//div[@id='task_content']" position="before">
|
||||
<div t-if="visit_confirmation_accepted" class="alert alert-success" role="alert">
|
||||
<strong>Success!</strong> We've received your confirmation. Thank you!
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -21,7 +21,7 @@ class IncrementingSequenceMixin(models.AbstractModel):
|
|||
res = super().create(vals_list)
|
||||
for rec in res:
|
||||
if rec.sequence == 0:
|
||||
group_field = rec._sequence_group
|
||||
group_field = getattr(rec, "_sequence_group")
|
||||
group_field_data = getattr(rec, group_field)
|
||||
if hasattr(group_field_data, "id"):
|
||||
group_field_data = group_field_data.id
|
||||
|
|
@ -35,7 +35,7 @@ class IncrementingSequenceMixin(models.AbstractModel):
|
|||
return res
|
||||
|
||||
def _default_sequence(self):
|
||||
group_field = self._sequence_group
|
||||
group_field = getattr(self, "_sequence_group")
|
||||
group_field_data = getattr(self, group_field)
|
||||
if hasattr(group_field_data, "id"):
|
||||
group_field_data = group_field_data.id
|
||||
|
|
|
|||
Loading…
Reference in a new issue