From 671251d23e6e721b4eaaada60b01825d37f5ea7a Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Wed, 3 Jun 2026 08:17:49 +0200 Subject: [PATCH] Demurrage --- .../purchase_trade/docs/business/sessions.md | 2 + .../docs_source/business/sessions.md | 6 ++ modules/purchase_trade/stock.py | 62 ++++++++++++++++--- modules/purchase_trade/tests/test_module.py | 39 ++++++++++++ .../view/charter_party_form.xml | 2 +- modules/purchase_trade/view/purchase_form.xml | 2 +- .../view/purchase_line_form.xml | 2 +- modules/purchase_trade/view/sale_form.xml | 2 +- .../purchase_trade/view/sale_line_form.xml | 2 +- .../purchase_trade/view/shipment_in_form.xml | 2 +- modules/purchase_trade/view/sof_form.xml | 2 +- 11 files changed, 106 insertions(+), 17 deletions(-) diff --git a/modules/purchase_trade/docs/business/sessions.md b/modules/purchase_trade/docs/business/sessions.md index bdfe469..e6f7aab 100644 --- a/modules/purchase_trade/docs/business/sessions.md +++ b/modules/purchase_trade/docs/business/sessions.md @@ -31,6 +31,8 @@ canonique seulement quand elle est reprise dans une page thematique.
  • Les contrats Sale et Purchase ont un onglet Charter Conditions au niveau entete et ligne. Une ligne sans conditions propres herite des conditions de l'entete; des conditions de ligne remplacent l'heritage.
  • +
  • Les conditions creees depuis une Sale ou une Sale Line sont limitees au role Customer; celles creees depuis une Purchase ou Purchase Line sont limitees au role Supplier. Les conditions creees depuis une Charter Party ne peuvent pas utiliser les roles Supplier ou Customer afin d'eviter la confusion entre conditions armateur/charte et conditions contractuelles achat/vente. +
  • Les shipments et SOF exposent les conditions Owner, Supplier et Customer retrouvees depuis le charter party et les lots/lignes physiques.
  • Le calcul SOF privilegie la condition appliquee: laytime allowed, turn time, pumping rate, rate demurrage et rate despatch. A defaut, il garde les anciens champs legacy demurrage/pumping. diff --git a/modules/purchase_trade/docs_source/business/sessions.md b/modules/purchase_trade/docs_source/business/sessions.md index e5882d3..5159e8d 100644 --- a/modules/purchase_trade/docs_source/business/sessions.md +++ b/modules/purchase_trade/docs_source/business/sessions.md @@ -23,6 +23,12 @@ canonique seulement quand elle est reprise dans une page thematique. - Les contrats Sale et Purchase ont un onglet `Charter Conditions` au niveau entete et ligne. Une ligne sans conditions propres herite des conditions de l'entete; des conditions de ligne remplacent l'heritage. + - Les conditions creees depuis une Sale ou une Sale Line sont limitees au + role `Customer`; celles creees depuis une Purchase ou Purchase Line sont + limitees au role `Supplier`. Les conditions creees depuis une + Charter Party ne peuvent pas utiliser les roles `Supplier` ou `Customer` + afin d'eviter la confusion entre conditions armateur/charte et conditions + contractuelles achat/vente. - Les shipments et SOF exposent les conditions Owner, Supplier et Customer retrouvees depuis le charter party et les lots/lignes physiques. - Le calcul SOF privilegie la condition appliquee: laytime allowed, diff --git a/modules/purchase_trade/stock.py b/modules/purchase_trade/stock.py index 1fb60e0..2f6a1d4 100755 --- a/modules/purchase_trade/stock.py +++ b/modules/purchase_trade/stock.py @@ -481,15 +481,7 @@ class CharterCondition(ModelSQL, ModelView): "Charter Condition" __name__ = 'charter.condition' - name = fields.Char("Name") - charter_party = fields.Many2One( - 'stock.charter.party', "Charter Party", ondelete='CASCADE') - purchase = fields.Many2One( - 'purchase.purchase', "Purchase", ondelete='CASCADE') - purchase_line = fields.Many2One( - 'purchase.line', "Purchase Line", ondelete='CASCADE') - party = fields.Many2One('party.party', "Party") - party_role = fields.Selection([ + _party_role_selection = [ (None, ''), ('owner', 'Owner'), ('charterer', 'Charterer'), @@ -499,7 +491,18 @@ class CharterCondition(ModelSQL, ModelView): ('agent', 'Agent'), ('terminal', 'Terminal'), ('other', 'Other'), - ], "Party Role") + ] + + name = fields.Char("Name") + charter_party = fields.Many2One( + 'stock.charter.party', "Charter Party", ondelete='CASCADE') + purchase = fields.Many2One( + 'purchase.purchase', "Purchase", ondelete='CASCADE') + purchase_line = fields.Many2One( + 'purchase.line', "Purchase Line", ondelete='CASCADE') + party = fields.Many2One('party.party', "Party") + party_role = fields.Selection( + 'get_party_roles', "Party Role") responsibility = fields.Selection([ (None, ''), ('ours', 'Ours'), @@ -546,6 +549,45 @@ class CharterCondition(ModelSQL, ModelView): def default_all_time_saved(): return False + @staticmethod + def default_party_role(): + role_context = Transaction().context.get('charter_condition_role') + if role_context in {'purchase', 'purchase_line', 'supplier'}: + return 'supplier' + if role_context in {'sale', 'sale_line', 'customer'}: + return 'customer' + if role_context == 'charter_party': + return 'owner' + + @fields.depends( + 'charter_party', 'purchase', 'purchase_line', 'sale', 'sale_line') + def get_party_roles(self): + role_context = Transaction().context.get('charter_condition_role') + if (role_context in {'purchase', 'purchase_line', 'supplier'} + or getattr(self, 'purchase', None) + or getattr(self, 'purchase_line', None)): + return [(None, ''), ('supplier', 'Supplier')] + if (role_context in {'sale', 'sale_line', 'customer'} + or getattr(self, 'sale', None) + or getattr(self, 'sale_line', None)): + return [(None, ''), ('customer', 'Customer')] + if role_context == 'charter_party' or getattr( + self, 'charter_party', None): + return [ + item for item in self._party_role_selection + if item[0] not in {'supplier', 'customer'}] + return self._party_role_selection + + @classmethod + def validate(cls, conditions): + super().validate(conditions) + for condition in conditions: + allowed_roles = {role for role, _ in condition.get_party_roles()} + if condition.party_role not in allowed_roles: + raise UserError( + "This charter condition party role is not allowed " + "for its source.") + class CharterParty(ModelSQL, ModelView): "Charter Party" diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 06bf9c5..9f0aac8 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -9,6 +9,7 @@ from trytond.pool import Pool from trytond.pyson import Eval from trytond.tests.test_tryton import ModuleTestCase, with_transaction from trytond.exceptions import UserError +from trytond.transaction import Transaction from trytond.modules.purchase_trade import valuation as valuation_module from trytond.modules.purchase_trade import lot as lot_module from trytond.modules.purchase_trade import purchase as purchase_module @@ -150,6 +151,44 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertEqual(rate.basis, 'per_day') + @with_transaction() + def test_charter_condition_role_defaults_from_context(self): + 'charter condition role defaults to the business source context' + Condition = Pool().get('charter.condition') + + with Transaction().set_context(charter_condition_role='sale'): + self.assertEqual(Condition.default_party_role(), 'customer') + condition = Condition() + self.assertEqual( + condition.get_party_roles(), + [(None, ''), ('customer', 'Customer')]) + + with Transaction().set_context(charter_condition_role='purchase'): + self.assertEqual(Condition.default_party_role(), 'supplier') + condition = Condition() + self.assertEqual( + condition.get_party_roles(), + [(None, ''), ('supplier', 'Supplier')]) + + with Transaction().set_context(charter_condition_role='charter_party'): + self.assertEqual(Condition.default_party_role(), 'owner') + condition = Condition() + roles = {role for role, _ in condition.get_party_roles()} + self.assertNotIn('supplier', roles) + self.assertNotIn('customer', roles) + + @with_transaction() + def test_charter_condition_role_rejects_invalid_source_role(self): + 'charter condition role is constrained by its source' + Condition = Pool().get('charter.condition') + condition = Condition() + condition.name = 'Invalid supplier condition on sale' + condition.party_role = 'supplier' + + with Transaction().set_context(charter_condition_role='sale'): + with self.assertRaises(UserError): + Condition.validate([condition]) + @with_transaction() def test_sof_calculation_reads_applied_condition_rates(self): 'sof demurrage calculation uses the selected charter condition rates' diff --git a/modules/purchase_trade/view/charter_party_form.xml b/modules/purchase_trade/view/charter_party_form.xml index 4c5fde6..059c1f1 100644 --- a/modules/purchase_trade/view/charter_party_form.xml +++ b/modules/purchase_trade/view/charter_party_form.xml @@ -44,7 +44,7 @@ - + - +