Charter party

This commit is contained in:
2026-05-31 22:58:42 +02:00
parent 2dd8360d62
commit d483e7cf69
3 changed files with 135 additions and 34 deletions

View File

@@ -1576,28 +1576,30 @@ class ShipmentIn(metaclass=PoolMeta):
@classmethod
@ModelView.button
def compute(cls, shipments):
Sof = Pool().get('sof.statement')
for sh in shipments:
if sh.sof:
for s in sh.sof:
if s.laytime_clause_hour:
t = s.laytime_clause_hour # instance de datetime.time
delta = datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second)
s.laytime_commenced = s.notice_of_readiness_time + delta
Sof = Pool().get('sof.statement')
for sh in shipments:
if sh.sof:
for s in sh.sof:
if s.notice_of_readiness_time:
s.laytime_commenced = (
s.notice_of_readiness_time
+ s._applied_turn_delta())
quantity = float(s.quantity or sh.get_quantity() or 0)
s.laytime_allowed = round(quantity / s.pumping_rate_o,2)
s.laytime_completed = s.hoses_disconnected
#s.laytime_completed = s.laytime_commenced + datetime.timedelta(hours=s.laytime_allowed)
total_time = (s.laytime_completed - s.laytime_commenced).total_seconds() / 3600.0
logger.info("COMPUTE_DEDUCTION3:%s",total_time)
s.actual_time_used = round(total_time,2)
s.deductions = round(sum([e.duration for e in s.sof_events if e.deductible == True]),2)
effective_time = round(total_time - s.deductions,2)
s.laytime_used = effective_time
s.laytime_balance = round(s.laytime_allowed - s.laytime_used,2)
penalty = Decimal(s.laytime_balance) * (s.demurrage_rate_o / 24)
s.compensation_amount = round(penalty, 2)
Sof.save([s])
s.laytime_allowed = s._applied_laytime_allowed(quantity)
s.laytime_completed = s.hoses_disconnected
#s.laytime_completed = s.laytime_commenced + datetime.timedelta(hours=s.laytime_allowed)
if not s.laytime_completed or s.laytime_allowed is None:
Sof.save([s])
continue
total_time = (s.laytime_completed - s.laytime_commenced).total_seconds() / 3600.0
logger.info("COMPUTE_DEDUCTION3:%s",total_time)
s.actual_time_used = round(total_time,2)
s.deductions = round(sum([e.duration for e in s.sof_events if e.deductible == True]),2)
effective_time = round(total_time - s.deductions,2)
s.laytime_used = effective_time
s.laytime_balance = round(s.laytime_allowed - s.laytime_used,2)
s.compensation_amount = s._applied_compensation_amount()
Sof.save([s])
def get_vessel_url(self):
return f"https://www.vesselfinder.com/en/vessels/VOS-TRAVELLER-IMO-{self.vessel.vessel_imo}"
@@ -1944,6 +1946,8 @@ class StatementOfFacts(ModelSQL, ModelView):
timebar_day = fields.Integer("TimeBar")
timebar_warn = fields.Integer("Warning TimeBar")
charter_party = fields.Many2One('stock.charter.party', "Charter Party")
applied_condition = fields.Many2One(
'charter.condition', "Applied Condition")
owner_charter_conditions = fields.Function(
fields.One2Many('charter.condition', '', "Owner Conditions"),
'get_owner_charter_conditions')
@@ -1978,15 +1982,27 @@ class StatementOfFacts(ModelSQL, ModelView):
('despatch', 'Despatch'),
(None, 'No Compensation'),
], 'Compensation Type')
compensation_amount = fields.Numeric('Compensation Amount ($)', readonly=True, digits=(16, 2))
compensation_amount = fields.Numeric('Compensation Amount ($)', readonly=True, digits=(16, 2))
quantity = fields.Function(fields.Float("Quantity loaded"),'get_qt')
notes = fields.Text('Remarks / Notes')
@fields.depends('shipment', 'charter_party')
@fields.depends('shipment', 'charter_party', 'applied_condition')
def on_change_shipment(self):
if self.shipment and self.shipment.charter_party and not self.charter_party:
self.charter_party = self.shipment.charter_party
self._set_default_applied_condition()
@fields.depends('charter_party', 'applied_condition')
def on_change_charter_party(self):
self._set_default_applied_condition()
def _set_default_applied_condition(self):
if self.applied_condition:
return
conditions = getattr(self.charter_party, 'conditions', None) or []
if len(conditions) == 1:
self.applied_condition = conditions[0]
def get_owner_charter_conditions(self, name=None):
if self.charter_party:
@@ -2005,6 +2021,76 @@ class StatementOfFacts(ModelSQL, ModelView):
return self.shipment.get_sale_charter_conditions(name)
return []
@staticmethod
def _rate_category(rate):
rate_type = getattr(rate, 'rate_type', None)
category = getattr(rate_type, 'category', None)
if category:
return category
label = ' '.join(filter(None, [
getattr(rate_type, 'name', None),
getattr(rate, 'description', None),
])).lower()
for category in ('demurrage', 'despatch', 'pumping'):
if category in label:
return category
def _condition_rate(self, category):
condition = getattr(self, 'applied_condition', None)
for rate in getattr(condition, 'rates', None) or []:
if self._rate_category(rate) == category and rate.rate is not None:
return Decimal(str(rate.rate))
def _applied_laytime_allowed(self, quantity):
condition = getattr(self, 'applied_condition', None)
if condition and condition.laytime_allowed is not None:
allowed = Decimal(str(condition.laytime_allowed))
if condition.laytime_unit in {
'days', 'wwd', 'wwd_shex', 'wwd_shinc'}:
allowed *= Decimal(24)
return float(allowed)
pumping_rate = self._condition_rate('pumping')
if pumping_rate:
return round(float(Decimal(str(quantity or 0)) / pumping_rate), 2)
legacy_pumping = self.pumping_rate_o or self.pumping_rate
if legacy_pumping:
return round(float(quantity or 0) / float(legacy_pumping), 2)
def _applied_turn_delta(self):
condition = getattr(self, 'applied_condition', None)
if condition and condition.turn_time is not None:
hours = Decimal(str(condition.turn_time))
if condition.turn_time_unit == 'days':
hours *= Decimal(24)
return datetime.timedelta(hours=float(hours))
if self.laytime_clause_hour:
t = self.laytime_clause_hour
elif self.laytime_clause_hour_o:
t = self.laytime_clause_hour_o
else:
return datetime.timedelta(0)
return datetime.timedelta(
hours=t.hour, minutes=t.minute, seconds=t.second)
def _applied_compensation_amount(self):
balance = Decimal(str(self.laytime_balance or 0))
if balance < 0:
self.compensation_type = 'demurrage'
rate = self._condition_rate('demurrage')
if rate is None:
rate = Decimal(str(self.demurrage_rate_o or self.demurrage_rate or 0))
return round(balance * rate / Decimal(24), 2)
if balance > 0:
rate = self._condition_rate('despatch')
if rate is not None:
self.compensation_type = 'despatch'
return round(balance * rate / Decimal(24), 2)
self.compensation_type = None
return Decimal(0)
def get_qt(self,name):
if self.shipment:
return self.shipment.get_quantity()

View File

@@ -150,6 +150,30 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual(rate.basis, 'per_day')
@with_transaction()
def test_sof_calculation_reads_applied_condition_rates(self):
'sof demurrage calculation uses the selected charter condition rates'
Sof = Pool().get('sof.statement')
sof = Sof()
sof.applied_condition = Mock(
laytime_allowed=None,
turn_time=Decimal('6'),
turn_time_unit='hours',
rates=[
Mock(rate=Decimal('300'), rate_type=Mock(category='pumping')),
Mock(rate=Decimal('24000'), rate_type=Mock(category='demurrage')),
],
)
sof.laytime_balance = -Decimal('12')
self.assertEqual(
sof._applied_turn_delta(), datetime.timedelta(hours=6))
self.assertEqual(
sof._applied_laytime_allowed(Decimal('600')), 2.0)
self.assertEqual(
sof._applied_compensation_amount(), Decimal('-12000.00'))
self.assertEqual(sof.compensation_type, 'demurrage')
@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'

View File

@@ -4,17 +4,8 @@
<label name="charter_party"/>
<field name="charter_party"/>
<newline/>
<notebook colspan="4">
<page string="Owner" id="owner_conditions">
<field name="owner_charter_conditions" colspan="4" mode="tree,form" view_ids="purchase_trade.charter_condition_view_tree,purchase_trade.charter_condition_view_form"/>
</page>
<page string="Supplier" id="supplier_conditions">
<field name="purchase_charter_conditions" colspan="4" mode="tree,form" view_ids="purchase_trade.charter_condition_view_tree,purchase_trade.charter_condition_view_form"/>
</page>
<page string="Customer" id="customer_conditions">
<field name="sale_charter_conditions" colspan="4" mode="tree,form" view_ids="purchase_trade.charter_condition_view_tree,purchase_trade.charter_condition_view_form"/>
</page>
</notebook>
<label name="applied_condition"/>
<field name="applied_condition"/>
<newline/>
<separator string="TimeBar" id="tb"/>
<newline/>