Merge branch '17.0' of git.bemade.org:bemade/bemade-addons into 17.0

This commit is contained in:
xtremxpert 2024-11-21 09:57:00 -05:00
commit b6eca04802
34 changed files with 851 additions and 18 deletions

View file

@ -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': """

View file

@ -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):

View file

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

View 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,
}

View 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>

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,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

View file

@ -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

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,4 @@
from . import test_carrier_account_common
from . import test_carrier_account_mixin
from . import test_choose_delivery_carrier
from . import test_sale_order

View file

@ -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)

View file

@ -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
)

View file

@ -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")

View file

@ -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})

View file

@ -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>

View file

@ -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>

View 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>

View 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>

View file

@ -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>

View file

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

View file

@ -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

View file

@ -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>

View file

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

View 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,
}

View file

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

View 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

View 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 }}&amp;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>

View file

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

View 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"

View file

@ -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>

View file

@ -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