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'