From f8d7f1e9d918236961748bbb927b94b6950cba3d Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Sun, 31 May 2026 21:34:24 +0200 Subject: [PATCH] Charter party --- modules/purchase_trade/__init__.py | 10 +- modules/purchase_trade/purchase.py | 28 ++- modules/purchase_trade/sale.py | 12 +- modules/purchase_trade/stock.py | 208 +++++++++++++++++- modules/purchase_trade/stock.xml | 143 +++++++++++- modules/purchase_trade/tests/test_module.py | 55 +++++ .../view/charter_condition_form.xml | 50 +++++ .../view/charter_condition_rate_form.xml | 21 ++ .../view/charter_condition_rate_tree.xml | 11 + .../view/charter_condition_tree.xml | 9 + .../view/charter_party_form.xml | 58 +++++ .../view/charter_party_tree.xml | 12 + .../view/charter_rate_type_form.xml | 13 ++ .../view/charter_rate_type_tree.xml | 8 + modules/purchase_trade/view/purchase_form.xml | 3 + .../view/purchase_line_form.xml | 13 +- modules/purchase_trade/view/sale_form.xml | 3 + .../purchase_trade/view/sale_line_form.xml | 13 +- .../purchase_trade/view/shipment_in_form.xml | 3 + 19 files changed, 643 insertions(+), 30 deletions(-) create mode 100644 modules/purchase_trade/view/charter_condition_form.xml create mode 100644 modules/purchase_trade/view/charter_condition_rate_form.xml create mode 100644 modules/purchase_trade/view/charter_condition_rate_tree.xml create mode 100644 modules/purchase_trade/view/charter_condition_tree.xml create mode 100644 modules/purchase_trade/view/charter_party_form.xml create mode 100644 modules/purchase_trade/view/charter_party_tree.xml create mode 100644 modules/purchase_trade/view/charter_rate_type_form.xml create mode 100644 modules/purchase_trade/view/charter_rate_type_tree.xml diff --git a/modules/purchase_trade/__init__.py b/modules/purchase_trade/__init__.py index 8c2f420..e599e09 100755 --- a/modules/purchase_trade/__init__.py +++ b/modules/purchase_trade/__init__.py @@ -90,10 +90,14 @@ def register(): ctrm_reporting.CTRMCashFlowContext, ctrm_reporting.CTRMGLReconciliation, ctrm_reporting.CTRMGLReconciliationContext, + stock.CharterRateType, + stock.CharterConditionRate, + stock.CharterCondition, + stock.CharterParty, configuration.Configuration, - pricing.ImportPricesStart, - pricing.ImportPricesResult, - module='purchase_trade', type_='model') + pricing.ImportPricesStart, + pricing.ImportPricesResult, + module='purchase_trade', type_='model') Pool.register( incoming.ImportSwift, incoming.PrepareDocuments, diff --git a/modules/purchase_trade/purchase.py b/modules/purchase_trade/purchase.py index 783ce76..18ade70 100755 --- a/modules/purchase_trade/purchase.py +++ b/modules/purchase_trade/purchase.py @@ -256,13 +256,15 @@ class PurchaseCrop(ModelSQL,ModelView): __name__ = 'purchase.crop' name = fields.Char("Name") -class Purchase(metaclass=PoolMeta): - __name__ = 'purchase.purchase' - - btb = fields.Many2One('back.to.back',"Back to back") - bank_accounts = fields.Function( - fields.Many2Many('bank.account', None, None, "Bank Accounts"), - 'on_change_with_bank_accounts') +class Purchase(metaclass=PoolMeta): + __name__ = 'purchase.purchase' + + btb = fields.Many2One('back.to.back',"Back to back") + charter_conditions = fields.One2Many( + 'charter.condition', 'purchase', "Charter Conditions") + bank_accounts = fields.Function( + fields.Many2Many('bank.account', None, None, "Bank Accounts"), + 'on_change_with_bank_accounts') bank_account = fields.Many2One( 'bank.account', "Bank Account", domain=[('id', 'in', Eval('bank_accounts', []))], @@ -1409,7 +1411,9 @@ class Line(metaclass=PoolMeta): price_summary = fields.One2Many('purchase.pricing.summary','line',"Summary") premium_decomposition = fields.One2Many( 'premium.composition', 'line', "Premium decomposition") - estimated_date = fields.One2Many('pricing.estimated','line',"Estimated date") + charter_conditions = fields.One2Many( + 'charter.condition', 'purchase_line', "Charter Conditions") + estimated_date = fields.One2Many('pricing.estimated','line',"Estimated date") optional = fields.One2Many('optional.scenario','line',"Optionals Scenarios") lots = fields.One2Many('lot.lot','line',"Lots",readonly=True) purchase_line = fields.Many2One('purchase.line',"Lines") @@ -1459,7 +1463,7 @@ class Line(metaclass=PoolMeta): 'required': Eval('enable_linked_currency'), }, depends=['enable_linked_currency']) premium = fields.Function( - fields.Numeric("Premium/Discount",digits='unit'), + fields.Numeric("Premium/Discount", digits='unit', readonly=True), 'get_premium', setter='set_premium') fee_ = fields.Many2One('fee.fee',"Fee") pricing_rule = fields.Text("Pricing description") @@ -1726,6 +1730,12 @@ class Line(metaclass=PoolMeta): else premium.get_base_amount('base_amount') or 0)) for premium in (self.premium_decomposition or [])), 4) + def get_effective_charter_conditions(self): + if self.charter_conditions: + return list(self.charter_conditions) + purchase = getattr(self, 'purchase', None) + return list(getattr(purchase, 'charter_conditions', None) or []) + @classmethod def set_premium(cls, lines, name, value): Premium = Pool().get('premium.composition') diff --git a/modules/purchase_trade/sale.py b/modules/purchase_trade/sale.py index bb5b64d..11ef108 100755 --- a/modules/purchase_trade/sale.py +++ b/modules/purchase_trade/sale.py @@ -225,6 +225,8 @@ class Sale(metaclass=PoolMeta): __name__ = 'sale.sale' btb = fields.Many2One('back.to.back',"Back to back") + charter_conditions = fields.One2Many( + 'charter.condition', 'sale', "Charter Conditions") bank_accounts = fields.Function( fields.Many2Many('bank.account', None, None, "Bank Accounts"), 'on_change_with_bank_accounts') @@ -1455,6 +1457,8 @@ class SaleLine(metaclass=PoolMeta): price_summary = fields.One2Many('sale.pricing.summary','sale_line',"Summary") premium_decomposition = fields.One2Many( 'premium.composition', 'sale_line', "Premium decomposition") + charter_conditions = fields.One2Many( + 'charter.condition', 'sale_line', "Charter Conditions") estimated_date = fields.One2Many('pricing.estimated','sale_line',"Estimated date") tol_min = fields.Numeric("Tol - in %",states={ 'readonly': (Eval('inherit_tol')), @@ -1499,7 +1503,7 @@ class SaleLine(metaclass=PoolMeta): 'required': Eval('enable_linked_currency'), }, depends=['enable_linked_currency']) premium = fields.Function( - fields.Numeric("Premium/Discount",digits='unit'), + fields.Numeric("Premium/Discount", digits='unit', readonly=True), 'get_premium', setter='set_premium') fee_ = fields.Many2One('fee.fee',"Fee") @@ -1775,6 +1779,12 @@ class SaleLine(metaclass=PoolMeta): else premium.get_base_amount('base_amount') or 0)) for premium in (self.premium_decomposition or [])), 4) + def get_effective_charter_conditions(self): + if self.charter_conditions: + return list(self.charter_conditions) + sale = getattr(self, 'sale', None) + return list(getattr(sale, 'charter_conditions', None) or []) + @classmethod def set_premium(cls, lines, name, value): Premium = Pool().get('premium.composition') diff --git a/modules/purchase_trade/stock.py b/modules/purchase_trade/stock.py index a0dfef0..fb8bb02 100755 --- a/modules/purchase_trade/stock.py +++ b/modules/purchase_trade/stock.py @@ -394,12 +394,203 @@ class ShipmentContainer(ModelSQL, ModelView): seal_no = fields.Char('Seal Number') is_reefer = fields.Boolean('Reefer') -class ShipmentWR(ModelSQL,ModelView): - "Shipment WR" - __name__ = "shipment.wr" - shipment_in = fields.Many2One('stock.shipment.in',"Shipment In") - wr = fields.Many2One('weight.report',"WR") - +class ShipmentWR(ModelSQL,ModelView): + "Shipment WR" + __name__ = "shipment.wr" + shipment_in = fields.Many2One('stock.shipment.in',"Shipment In") + wr = fields.Many2One('weight.report',"WR") + + +RATE_BASIS = [ + (None, ''), + ('per_day', 'Per Day'), + ('per_hour', 'Per Hour'), + ('per_mt', 'Per MT'), + ('per_wmt', 'Per WMT'), + ('per_dmt', 'Per DMT'), + ('per_cbm', 'Per CBM'), + ('per_lot', 'Per Lot'), + ('lumpsum', 'Lumpsum'), + ('percent', 'Percent'), + ('other', 'Other'), +] + + +class CharterRateType(ModelSQL, ModelView): + "Charter Rate Type" + __name__ = 'charter.rate.type' + + name = fields.Char("Name", required=True) + code = fields.Char("Code") + category = fields.Selection([ + (None, ''), + ('demurrage', 'Demurrage'), + ('despatch', 'Despatch'), + ('pumping', 'Pumping'), + ('freight', 'Freight'), + ('detention', 'Detention'), + ('waiting', 'Waiting'), + ('shifting', 'Shifting'), + ('heating', 'Heating'), + ('cleaning', 'Cleaning'), + ('bunker', 'Bunker'), + ('port_cost', 'Port Cost'), + ('other', 'Other'), + ], "Category") + default_basis = fields.Selection(RATE_BASIS, "Default Basis") + active = fields.Boolean("Active") + + @staticmethod + def default_active(): + return True + + +class CharterConditionRate(ModelSQL, ModelView): + "Charter Condition Rate" + __name__ = 'charter.condition.rate' + + condition = fields.Many2One( + 'charter.condition', "Condition", required=True, ondelete='CASCADE') + rate_type = fields.Many2One('charter.rate.type', "Type") + description = fields.Char("Description") + rate = fields.Numeric("Rate", digits=(16, 6)) + currency = fields.Many2One('currency.currency', "Curr.") + basis = fields.Selection(RATE_BASIS, "Basis") + applies_to = fields.Selection([ + (None, ''), + ('load', 'Load'), + ('discharge', 'Discharge'), + ('both', 'Both'), + ('waiting', 'Waiting'), + ('pumping', 'Pumping'), + ('shifting', 'Shifting'), + ('voyage', 'Voyage'), + ('other', 'Other'), + ], "Applies To") + minimum = fields.Numeric("Min", digits=(16, 6)) + maximum = fields.Numeric("Max", digits=(16, 6)) + reference = fields.Text("Reference") + + @fields.depends('rate_type') + def on_change_rate_type(self): + if self.rate_type and self.rate_type.default_basis: + self.basis = self.rate_type.default_basis + + +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') + sale = fields.Many2One( + 'sale.sale', "Sale", ondelete='CASCADE') + sale_line = fields.Many2One( + 'sale.line', "Sale Line", ondelete='CASCADE') + party = fields.Many2One('party.party', "Party") + party_role = fields.Selection([ + (None, ''), + ('owner', 'Owner'), + ('charterer', 'Charterer'), + ('supplier', 'Supplier'), + ('customer', 'Customer'), + ('broker', 'Broker'), + ('agent', 'Agent'), + ('terminal', 'Terminal'), + ('other', 'Other'), + ], "Party Role") + responsibility = fields.Selection([ + (None, ''), + ('ours', 'Ours'), + ('counterparty', 'Counterparty'), + ('shared', 'Shared'), + ('pass_through', 'Pass-through'), + ('owner', 'Owner'), + ('charterer', 'Charterer'), + ], "Responsibility") + laytime_clause = fields.Text("Laytime Clause") + laytime_allowed = fields.Numeric("Laytime Allowed", digits=(16, 4)) + laytime_unit = fields.Selection([ + (None, ''), + ('hours', 'Hours'), + ('days', 'Days'), + ('wwd', 'Weather Working Days'), + ('wwd_shex', 'WWD SHEX'), + ('wwd_shinc', 'WWD SHINC'), + ('running_hours', 'Running Hours'), + ], "Laytime Unit") + laytime_start = fields.Text("Laytime Start") + laytime_end = fields.Text("Laytime End") + nor_clause = fields.Text("NOR Clause") + turn_time = fields.Numeric("Turn Time", digits=(16, 4)) + turn_time_unit = fields.Selection([ + (None, ''), + ('hours', 'Hours'), + ('days', 'Days'), + ], "Turn Time Unit") + reversible = fields.Boolean("Reversible Laytime") + all_time_saved = fields.Boolean("All Time Saved") + demurrage_clause = fields.Text("Demurrage Clause") + despatch_clause = fields.Text("Despatch Clause") + exceptions = fields.Text("Exceptions") + remarks = fields.Text("Remarks") + rates = fields.One2Many( + 'charter.condition.rate', 'condition', "Rates") + + @staticmethod + def default_reversible(): + return False + + @staticmethod + def default_all_time_saved(): + return False + + +class CharterParty(ModelSQL, ModelView): + "Charter Party" + __name__ = 'stock.charter.party' + + name = fields.Char("Name", required=True) + reference = fields.Char("Reference") + active = fields.Boolean("Active") + company = fields.Many2One('company.company', "Company") + charter_type = fields.Selection([ + (None, ''), + ('voyage', 'Voyage Charter'), + ('time', 'Time Charter'), + ('coa', 'Contract of Affreightment'), + ('bareboat', 'Bareboat Charter'), + ], "Charter Type", required=True) + owner = fields.Many2One('party.party', "Owner") + charterer = fields.Many2One('party.party', "Charterer") + broker = fields.Many2One('party.party', "Broker") + vessel = fields.Many2One('trade.vessel', "Vessel") + product = fields.Many2One('product.product', "Product") + load_port = fields.Many2One('stock.location', "Load Port") + discharge_port = fields.Many2One('stock.location', "Discharge Port") + from_location = fields.Many2One('stock.location', "From Location") + to_location = fields.Many2One('stock.location', "To Location") + laycan_from = fields.Date("Laycan From") + laycan_to = fields.Date("Laycan To") + contract_date = fields.Date("Contract Date") + currency = fields.Many2One('currency.currency', "Currency") + governing_law = fields.Char("Governing Law") + jurisdiction = fields.Char("Jurisdiction") + document = fields.Many2One('document.incoming', "Document") + notes = fields.Text("Notes") + conditions = fields.One2Many( + 'charter.condition', 'charter_party', "Conditions") + + @staticmethod + def default_active(): + return True + + class ShipmentIn(metaclass=PoolMeta): __name__ = 'stock.shipment.in' @@ -436,8 +627,9 @@ class ShipmentIn(metaclass=PoolMeta): ref = fields.Char("Our reference") note = fields.Text("Notes") dashboard = fields.Many2One('purchase.dashboard',"Dashboard") - himself = fields.Function(fields.Many2One('stock.shipment.in',"Shipment"),'get_sh') - sof = fields.One2Many('sof.statement', 'shipment',"Demurrage calculations") + himself = fields.Function(fields.Many2One('stock.shipment.in',"Shipment"),'get_sh') + charter_party = fields.Many2One('stock.charter.party', "Charter Party") + sof = fields.One2Many('sof.statement', 'shipment',"Demurrage calculations") del_from = fields.Date("Delivery period from") del_to = fields.Date("to") estimated_date = fields.One2Many('pricing.estimated','shipment_in',"Estimated date") diff --git a/modules/purchase_trade/stock.xml b/modules/purchase_trade/stock.xml index 8a0e02d..78a081f 100755 --- a/modules/purchase_trade/stock.xml +++ b/modules/purchase_trade/stock.xml @@ -51,7 +51,148 @@ this repository contains the full copyright notices and license terms. --> shipment_wr_tree - + + charter.rate.type + tree + charter_rate_type_tree + + + charter.rate.type + form + charter_rate_type_form + + + charter.condition.rate + tree + charter_condition_rate_tree + + + charter.condition.rate + form + charter_condition_rate_form + + + charter.condition + tree + charter_condition_tree + + + charter.condition + form + charter_condition_form + + + stock.charter.party + tree + charter_party_tree + + + stock.charter.party + form + charter_party_form + + + + Charter Party + stock.charter.party + + + + + + + + + + + + + Charter Rate Types + charter.rate.type + + + + + + + + + + + + + + + + stock.charter.party + + + + + + + charter.rate.type + + + + + + + charter.condition + + + + + + + charter.condition.rate + + + + + + + stock.charter.party + + + + + + + + charter.rate.type + + + + + + + + charter.condition + + + + + + + + charter.condition.rate + + + + + + + + 🚢 Find vessel stock.shipment.in.vf stock.shipment.in diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 55c3342..7d70239 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -103,6 +103,53 @@ class PurchaseTradeTestCase(ModuleTestCase): strategy.get_mtm(line, Decimal('3')), Decimal('225.00')) + @with_transaction() + def test_purchase_line_charter_conditions_inherit_header_when_empty(self): + 'purchase line uses header charter conditions when it has no line terms' + Line = Pool().get('purchase.line') + line_condition = Mock(id=2) + header_condition = Mock(id=1) + line = Line() + line.charter_conditions = [] + line.purchase = Mock(charter_conditions=[header_condition]) + + self.assertEqual( + line.get_effective_charter_conditions(), [header_condition]) + + line.charter_conditions = [line_condition] + + self.assertEqual( + line.get_effective_charter_conditions(), [line_condition]) + + @with_transaction() + def test_sale_line_charter_conditions_inherit_header_when_empty(self): + 'sale line uses header charter conditions when it has no line terms' + Line = Pool().get('sale.line') + line_condition = Mock(id=2) + header_condition = Mock(id=1) + line = Line() + line.charter_conditions = [] + line.sale = Mock(charter_conditions=[header_condition]) + + self.assertEqual( + line.get_effective_charter_conditions(), [header_condition]) + + line.charter_conditions = [line_condition] + + self.assertEqual( + line.get_effective_charter_conditions(), [line_condition]) + + @with_transaction() + def test_charter_condition_rate_type_sets_default_basis(self): + 'charter condition rate copies the dynamic type default basis' + Rate = Pool().get('charter.condition.rate') + rate = Rate() + rate.rate_type = Mock(default_basis='per_day') + + rate.on_change_rate_type() + + self.assertEqual(rate.basis, 'per_day') + @with_transaction() def test_add_physical_lot_defaults_hidden_premium_and_chunk_key(self): 'add physical lot works when hidden tree fields are not loaded' @@ -2059,6 +2106,14 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertEqual(line.get_premium('premium'), Decimal('10.2500')) + def test_purchase_and_sale_line_premium_fields_are_readonly_totals(self): + 'line premium fields are readonly totals from premium decomposition' + PurchaseLine = Pool().get('purchase.line') + SaleLine = Pool().get('sale.line') + + self.assertTrue(PurchaseLine.premium.readonly) + self.assertTrue(SaleLine.premium.readonly) + def test_premium_composition_base_amount_converts_on_contract_date(self): 'premium composition base amount converts to contract currency at spot date' Premium = Pool().get('premium.composition') diff --git a/modules/purchase_trade/view/charter_condition_form.xml b/modules/purchase_trade/view/charter_condition_form.xml new file mode 100644 index 0000000..b16f117 --- /dev/null +++ b/modules/purchase_trade/view/charter_condition_form.xml @@ -0,0 +1,50 @@ + +
+ + + + + + + + + + + +
diff --git a/modules/purchase_trade/view/charter_condition_rate_form.xml b/modules/purchase_trade/view/charter_condition_rate_form.xml new file mode 100644 index 0000000..8ac881f --- /dev/null +++ b/modules/purchase_trade/view/charter_condition_rate_form.xml @@ -0,0 +1,21 @@ + +
+