From c5682a25484c0d9007b436d46b115f82736f3132 Mon Sep 17 00:00:00 2001 From: Maxime Richez Date: Mon, 14 Oct 2024 18:00:19 +0200 Subject: [PATCH] Warn when incoterm on shipment is different from its origins Closes #13354 --- modules/incoterm/CHANGELOG | 1 + modules/incoterm/exceptions.py | 7 +++ modules/incoterm/message.xml | 3 + modules/incoterm/stock.py | 63 +++++++++++++++++++- modules/incoterm/tests/scenario_incoterm.rst | 45 ++++++++++++++ 5 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 modules/incoterm/exceptions.py diff --git a/modules/incoterm/CHANGELOG b/modules/incoterm/CHANGELOG index d9931de79c..5634831356 100644 --- a/modules/incoterm/CHANGELOG +++ b/modules/incoterm/CHANGELOG @@ -1,3 +1,4 @@ +* Warn when incoterm on shipment is different from its origins Version 7.2.0 - 2024-04-29 -------------------------- diff --git a/modules/incoterm/exceptions.py b/modules/incoterm/exceptions.py new file mode 100644 index 0000000000..4294318a16 --- /dev/null +++ b/modules/incoterm/exceptions.py @@ -0,0 +1,7 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. +from trytond.exceptions import UserWarning + + +class DifferentIncotermWarning(UserWarning): + pass diff --git a/modules/incoterm/message.xml b/modules/incoterm/message.xml index 64e14e0fe6..2c85f1cd40 100644 --- a/modules/incoterm/message.xml +++ b/modules/incoterm/message.xml @@ -18,5 +18,8 @@ this repository contains the full copyright notices and license terms. --> To get a quote for purchase "%(purchase)s" you must enter an incoterm. + + The incoterm "%(shipment_incoterm)s" of shipment "%(shipment)s" is different from the incoterms "%(origin_incoterms)s" of its origin. + diff --git a/modules/incoterm/stock.py b/modules/incoterm/stock.py index 07f8681724..1d2d270b48 100644 --- a/modules/incoterm/stock.py +++ b/modules/incoterm/stock.py @@ -1,10 +1,13 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. -from trytond.pool import PoolMeta +from trytond.i18n import gettext +from trytond.model import ModelView, Workflow +from trytond.pool import Pool, PoolMeta from trytond.pyson import Eval from .common import IncotermMixin +from .exceptions import DifferentIncotermWarning class ShipmentMixin(IncotermMixin): @@ -31,6 +34,35 @@ class ShipmentIn(ShipmentMixin, metaclass=PoolMeta): def _incoterm_related_party(cls): return Eval('supplier'), {'supplier'} + @classmethod + @ModelView.button + @Workflow.transition('received') + def receive(cls, shipments): + pool = Pool() + Warning = pool.get('res.user.warning') + PurchaseLine = pool.get('purchase.line') + for shipment in shipments: + if shipment.incoterm: + incoterms = { + move.origin.purchase.incoterm for move in shipment.moves + if isinstance(move.origin, PurchaseLine) + and move.state != 'cancelled'} + if {shipment.incoterm} != incoterms: + incoterms.discard(shipment.incoterm) + origin_incoterms = ', '.join( + i.rec_name if i else '' for i in incoterms) + warning_key = Warning.format( + 'different_incoterm', [shipment]) + if Warning.check(warning_key): + raise DifferentIncotermWarning( + warning_key, + gettext('incoterm' + '.msg_shipment_different_incoterm', + shipment_incoterm=shipment.incoterm.rec_name, + shipment=shipment.rec_name, + origin_incoterms=origin_incoterms)) + super().receive(shipments) + class ShipmentInReturn(ShipmentMixin, metaclass=PoolMeta): __name__ = 'stock.shipment.in.return' @@ -47,6 +79,35 @@ class ShipmentOut(ShipmentMixin, metaclass=PoolMeta): def _incoterm_related_party(cls): return Eval('customer'), {'customer'} + @classmethod + @ModelView.button + @Workflow.transition('waiting') + def wait(cls, shipments, moves=None): + pool = Pool() + Warning = pool.get('res.user.warning') + SaleLine = pool.get('sale.line') + for shipment in shipments: + if shipment.incoterm: + incoterms = { + move.origin.sale.incoterm for move in shipment.moves + if isinstance(move.origin, SaleLine) + and move.state != 'cancelled'} + if {shipment.incoterm} != incoterms: + incoterms.discard(shipment.incoterm) + origin_incoterms = ', '.join( + i.rec_name if i else '' for i in incoterms) + warning_key = Warning.format( + 'different_incoterm', [shipment]) + if Warning.check(warning_key): + raise DifferentIncotermWarning( + warning_key, + gettext('incoterm' + '.msg_shipment_different_incoterm', + shipment_incoterm=shipment.incoterm.rec_name, + shipment=shipment.rec_name, + origin_incoterms=origin_incoterms)) + super().wait(shipments, moves) + class ShipmentOutReturn(ShipmentMixin, metaclass=PoolMeta): __name__ = 'stock.shipment.out.return' diff --git a/modules/incoterm/tests/scenario_incoterm.rst b/modules/incoterm/tests/scenario_incoterm.rst index 4b4311ff31..fbf1878136 100644 --- a/modules/incoterm/tests/scenario_incoterm.rst +++ b/modules/incoterm/tests/scenario_incoterm.rst @@ -22,12 +22,14 @@ Activate modules:: >>> Country = Model.get('country.country') >>> Incoterm = Model.get('incoterm.incoterm') >>> Location = Model.get('stock.location') + >>> Move = Model.get('stock.move') >>> Party = Model.get('party.party') >>> ProductCategory = Model.get('product.category') >>> ProductTemplate = Model.get('product.template') >>> ProductUom = Model.get('product.uom') >>> Purchase = Model.get('purchase.purchase') >>> Sale = Model.get('sale.sale') + >>> ShipmentIn = Model.get('stock.shipment.in') Create countries:: @@ -110,6 +112,7 @@ Create products:: >>> template.default_uom = unit >>> template.type = 'goods' >>> template.salable = True + >>> template.purchasable = True >>> template.list_price = Decimal('20') >>> template.account_category = account_category >>> template.save() @@ -192,9 +195,51 @@ Test incoterm on shipment:: 'CIF (2020)' >>> assertEqual(shipment.incoterm_location, port.addresses[0]) +Warn if incoterm on shipment is different from its origins:: + + >>> shipment.click('draft') + >>> shipment.incoterm, = Incoterm.find([ + ... ('code', '=', 'EXW'), ('version', '=', '2020')]) + >>> shipment.click('wait') + Traceback (most recent call last): + ... + DifferentIncotermWarning: ... + Test incoterm is set on purchase:: >>> purchase = Purchase() >>> purchase.party = supplier >>> purchase.incoterm.rec_name 'CFR (2020)' + >>> line = purchase.lines.new() + >>> line.product = product + >>> line.quantity = 1 + >>> line.unit_price = Decimal('5.0000') + >>> purchase.click('quote') + >>> purchase.click('confirm') + >>> purchase.state + 'processing' + +Create supplier shipment with different incoterm:: + + >>> shipment = ShipmentIn() + >>> shipment.supplier = supplier + >>> shipment.incoterm, = Incoterm.find([ + ... ('code', '=', 'EXW'), ('version', '=', '2020')]) + >>> for move in purchase.moves: + ... incoming_move = Move(id=move.id) + ... shipment.incoming_moves.append(incoming_move) + >>> shipment.save() + >>> shipment.click('receive') + Traceback (most recent call last): + ... + DifferentIncotermWarning: ... + +Update supplier shipment with the same incoterm as the purchase:: + + >>> shipment.incoterm, = Incoterm.find([ + ... ('code', '=', 'CFR'), ('version', '=', '2020')]) + >>> shipment.save() + >>> shipment.click('receive') + >>> shipment.state + 'received'