diff --git a/modules/purchase_trade/stock.py b/modules/purchase_trade/stock.py
index a64b9c8..a34b63a 100755
--- a/modules/purchase_trade/stock.py
+++ b/modules/purchase_trade/stock.py
@@ -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()
diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py
index 7d70239..a8a31c3 100644
--- a/modules/purchase_trade/tests/test_module.py
+++ b/modules/purchase_trade/tests/test_module.py
@@ -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'
diff --git a/modules/purchase_trade/view/sof_form.xml b/modules/purchase_trade/view/sof_form.xml
index 1dfa781..8696a5c 100755
--- a/modules/purchase_trade/view/sof_form.xml
+++ b/modules/purchase_trade/view/sof_form.xml
@@ -4,17 +4,8 @@
-
-
-
-
-
-
-
-
-
-
-
+
+