Demurrage
This commit is contained in:
@@ -35,7 +35,7 @@ canonique seulement quand elle est reprise dans une page thematique.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Les shipments et SOF exposent les conditions Owner, Supplier et Customer retrouvees depuis le charter party et les lots/lignes physiques.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">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.
|
||||
<li style="margin:0.38rem 0;">Le calcul SOF privilegie la condition appliquee: laytime allowed, evenement de debut de laytime, offset de debut, evenement de fin, pumping rate, rate demurrage et rate despatch. Les clauses texte laytime/NOR/demurrage/despatch restent contractuelles et ne doivent pas servir de source de calcul.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Un solde de laytime negatif cree une compensation <code>demurrage</code>; un solde positif peut creer une compensation <code>despatch</code> si un rate despatch est defini.
|
||||
</li>
|
||||
|
||||
@@ -32,8 +32,10 @@ canonique seulement quand elle est reprise dans une page thematique.
|
||||
- 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.
|
||||
evenement de debut de laytime, offset de debut, evenement de fin,
|
||||
pumping rate, rate demurrage et rate despatch. Les clauses texte
|
||||
laytime/NOR/demurrage/despatch restent contractuelles et ne doivent pas
|
||||
servir de source de calcul.
|
||||
- Un solde de laytime negatif cree une compensation `demurrage`; un solde
|
||||
positif peut creer une compensation `despatch` si un rate despatch est
|
||||
defini.
|
||||
|
||||
@@ -526,6 +526,33 @@ class CharterCondition(ModelSQL, ModelView):
|
||||
laytime_start = fields.Text("Laytime Start")
|
||||
laytime_end = fields.Text("Laytime End")
|
||||
nor_clause = fields.Text("NOR Clause")
|
||||
laytime_start_event = fields.Selection([
|
||||
(None, ''),
|
||||
('notice_of_readiness', 'Notice of Readiness'),
|
||||
('arrival', 'Arrival'),
|
||||
('hoses_connected', 'Hoses Connected'),
|
||||
('start_pumping', 'Start Pumping'),
|
||||
('end_pumping', 'End Pumping'),
|
||||
('hoses_disconnected', 'Hoses Disconnected'),
|
||||
('sailing', 'Sailing'),
|
||||
], "Laytime Start Event")
|
||||
laytime_start_offset = fields.Numeric(
|
||||
"Laytime Start Offset", digits=(16, 4))
|
||||
laytime_start_offset_unit = fields.Selection([
|
||||
(None, ''),
|
||||
('hours', 'Hours'),
|
||||
('days', 'Days'),
|
||||
], "Laytime Start Offset Unit")
|
||||
laytime_end_event = fields.Selection([
|
||||
(None, ''),
|
||||
('notice_of_readiness', 'Notice of Readiness'),
|
||||
('arrival', 'Arrival'),
|
||||
('hoses_connected', 'Hoses Connected'),
|
||||
('start_pumping', 'Start Pumping'),
|
||||
('end_pumping', 'End Pumping'),
|
||||
('hoses_disconnected', 'Hoses Disconnected'),
|
||||
('sailing', 'Sailing'),
|
||||
], "Laytime End Event")
|
||||
turn_time = fields.Numeric("Turn Time", digits=(16, 4))
|
||||
turn_time_unit = fields.Selection([
|
||||
(None, ''),
|
||||
@@ -549,6 +576,18 @@ class CharterCondition(ModelSQL, ModelView):
|
||||
def default_all_time_saved():
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def default_laytime_start_event():
|
||||
return 'notice_of_readiness'
|
||||
|
||||
@staticmethod
|
||||
def default_laytime_start_offset_unit():
|
||||
return 'hours'
|
||||
|
||||
@staticmethod
|
||||
def default_laytime_end_event():
|
||||
return 'hoses_disconnected'
|
||||
|
||||
@staticmethod
|
||||
def default_party_role():
|
||||
role_context = Transaction().context.get('charter_condition_role')
|
||||
@@ -2220,15 +2259,17 @@ class StatementOfFacts(ModelSQL, ModelView):
|
||||
|
||||
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))
|
||||
|
||||
text_delta = self._laytime_start_text_delta(condition)
|
||||
if text_delta is not None:
|
||||
return text_delta
|
||||
if condition:
|
||||
value = getattr(condition, 'laytime_start_offset', None)
|
||||
unit = getattr(condition, 'laytime_start_offset_unit', None)
|
||||
if value is None:
|
||||
value = getattr(condition, 'turn_time', None)
|
||||
unit = getattr(condition, 'turn_time_unit', None)
|
||||
if value is not None:
|
||||
hours = Decimal(str(value))
|
||||
if unit == 'days':
|
||||
hours *= Decimal(24)
|
||||
return datetime.timedelta(hours=float(hours))
|
||||
|
||||
if self.laytime_clause_hour:
|
||||
t = self.laytime_clause_hour
|
||||
@@ -2239,29 +2280,23 @@ class StatementOfFacts(ModelSQL, ModelView):
|
||||
return datetime.timedelta(
|
||||
hours=t.hour, minutes=t.minute, seconds=t.second)
|
||||
|
||||
@staticmethod
|
||||
def _laytime_start_text_delta(condition):
|
||||
text = (getattr(condition, 'laytime_start', None) or '').lower()
|
||||
match = re.search(
|
||||
r'([+-]?\s*\d+(?:[.,]\d+)?)\s*(hours?|hrs?|h|days?|d)',
|
||||
text)
|
||||
if not match:
|
||||
return
|
||||
quantity = Decimal(match.group(1).replace(' ', '').replace(',', '.'))
|
||||
unit = match.group(2)
|
||||
if unit.startswith('d'):
|
||||
quantity *= Decimal(24)
|
||||
return datetime.timedelta(hours=float(quantity))
|
||||
def _condition_event_datetime(self, event):
|
||||
return {
|
||||
'notice_of_readiness': self.notice_of_readiness_time,
|
||||
'arrival': self.arrival_time,
|
||||
'hoses_connected': self.hoses_connected,
|
||||
'start_pumping': self.start_pumping,
|
||||
'end_pumping': self.end_pumping,
|
||||
'hoses_disconnected': self.hoses_disconnected,
|
||||
'sailing': self.sailing_time,
|
||||
}.get(event)
|
||||
|
||||
def _laytime_start_base(self):
|
||||
condition = getattr(self, 'applied_condition', None)
|
||||
text = (getattr(condition, 'laytime_start', None) or '').lower()
|
||||
if 'arrival' in text:
|
||||
return self.arrival_time
|
||||
if 'hose' in text and 'connect' in text:
|
||||
return self.hoses_connected
|
||||
if 'pump' in text or 'loading' in text:
|
||||
return self.start_pumping
|
||||
event = getattr(condition, 'laytime_start_event', None)
|
||||
base = self._condition_event_datetime(event)
|
||||
if base:
|
||||
return base
|
||||
return self.notice_of_readiness_time
|
||||
|
||||
def _applied_laytime_start(self):
|
||||
@@ -2272,13 +2307,10 @@ class StatementOfFacts(ModelSQL, ModelView):
|
||||
|
||||
def _applied_laytime_end(self):
|
||||
condition = getattr(self, 'applied_condition', None)
|
||||
text = (getattr(condition, 'laytime_end', None) or '').lower()
|
||||
if 'pump' in text or 'loading' in text:
|
||||
return self.end_pumping
|
||||
if 'sailing' in text or 'sailed' in text:
|
||||
return self.sailing_time
|
||||
if 'hose' in text and ('disconnect' in text or 'off' in text):
|
||||
return self.hoses_disconnected
|
||||
event = getattr(condition, 'laytime_end_event', None)
|
||||
end = self._condition_event_datetime(event)
|
||||
if end:
|
||||
return end
|
||||
return self.hoses_disconnected or self.end_pumping or self.sailing_time
|
||||
|
||||
def _applied_compensation_amount(self):
|
||||
|
||||
@@ -197,6 +197,8 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
sof = Sof()
|
||||
sof.applied_condition = Mock(
|
||||
laytime_allowed=None,
|
||||
laytime_start_offset=None,
|
||||
laytime_start_offset_unit=None,
|
||||
turn_time=Decimal('6'),
|
||||
turn_time_unit='hours',
|
||||
rates=[
|
||||
@@ -215,13 +217,15 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
self.assertEqual(sof.compensation_type, 'demurrage')
|
||||
|
||||
@with_transaction()
|
||||
def test_sof_laytime_start_reads_text_clause_turn_time(self):
|
||||
'sof laytime start reads NOR plus hours from the condition text'
|
||||
def test_sof_laytime_start_reads_structured_condition_rule(self):
|
||||
'sof laytime start reads the structured event and offset'
|
||||
Sof = Pool().get('sof.statement')
|
||||
sof = Sof()
|
||||
sof.applied_condition = Mock(
|
||||
turn_time=None,
|
||||
laytime_start='NOR + 6 hours')
|
||||
laytime_start_event='notice_of_readiness',
|
||||
laytime_start_offset=Decimal('6'),
|
||||
laytime_start_offset_unit='hours',
|
||||
turn_time=None)
|
||||
sof.notice_of_readiness_time = datetime.datetime(2026, 6, 1, 2, 0)
|
||||
|
||||
self.assertEqual(
|
||||
@@ -229,11 +233,11 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
datetime.datetime(2026, 6, 1, 8, 0))
|
||||
|
||||
@with_transaction()
|
||||
def test_sof_laytime_end_reads_hoses_disconnected_clause(self):
|
||||
'sof laytime end can use hoses disconnected from the condition text'
|
||||
def test_sof_laytime_end_reads_structured_condition_rule(self):
|
||||
'sof laytime end reads the structured end event'
|
||||
Sof = Pool().get('sof.statement')
|
||||
sof = Sof()
|
||||
sof.applied_condition = Mock(laytime_end='Hoses disconnected')
|
||||
sof.applied_condition = Mock(laytime_end_event='hoses_disconnected')
|
||||
sof.end_pumping = datetime.datetime(2026, 6, 1, 12, 0)
|
||||
sof.hoses_disconnected = datetime.datetime(2026, 6, 1, 13, 0)
|
||||
|
||||
@@ -241,6 +245,31 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
sof._applied_laytime_end(),
|
||||
datetime.datetime(2026, 6, 1, 13, 0))
|
||||
|
||||
@with_transaction()
|
||||
def test_sof_laytime_ignores_free_text_clauses(self):
|
||||
'sof laytime calculation ignores contractual free text fields'
|
||||
Sof = Pool().get('sof.statement')
|
||||
sof = Sof()
|
||||
sof.applied_condition = Mock(
|
||||
laytime_start_event='arrival',
|
||||
laytime_start_offset=Decimal('1'),
|
||||
laytime_start_offset_unit='hours',
|
||||
laytime_end_event='end_pumping',
|
||||
laytime_start='NOR + 6 hours',
|
||||
laytime_end='Hoses disconnected',
|
||||
turn_time=None)
|
||||
sof.arrival_time = datetime.datetime(2026, 6, 1, 3, 0)
|
||||
sof.notice_of_readiness_time = datetime.datetime(2026, 6, 1, 2, 0)
|
||||
sof.end_pumping = datetime.datetime(2026, 6, 1, 12, 0)
|
||||
sof.hoses_disconnected = datetime.datetime(2026, 6, 1, 13, 0)
|
||||
|
||||
self.assertEqual(
|
||||
sof._applied_laytime_start(),
|
||||
datetime.datetime(2026, 6, 1, 4, 0))
|
||||
self.assertEqual(
|
||||
sof._applied_laytime_end(),
|
||||
datetime.datetime(2026, 6, 1, 12, 0))
|
||||
|
||||
@with_transaction()
|
||||
def test_sof_header_date_time_values_sync_to_datetime(self):
|
||||
'sof header date and time fields build the technical datetime value'
|
||||
|
||||
@@ -14,16 +14,20 @@
|
||||
<field name="laytime_allowed"/>
|
||||
<label name="laytime_unit"/>
|
||||
<field name="laytime_unit"/>
|
||||
<label name="turn_time"/>
|
||||
<field name="turn_time"/>
|
||||
<label name="turn_time_unit"/>
|
||||
<field name="turn_time_unit"/>
|
||||
<label name="reversible"/>
|
||||
<field name="reversible"/>
|
||||
<label name="all_time_saved"/>
|
||||
<field name="all_time_saved"/>
|
||||
</page>
|
||||
<page string="Laytime" col="4" id="laytime">
|
||||
<label name="laytime_start_event"/>
|
||||
<field name="laytime_start_event"/>
|
||||
<label name="laytime_end_event"/>
|
||||
<field name="laytime_end_event"/>
|
||||
<label name="laytime_start_offset"/>
|
||||
<field name="laytime_start_offset"/>
|
||||
<label name="laytime_start_offset_unit"/>
|
||||
<field name="laytime_start_offset_unit"/>
|
||||
<label name="laytime_clause"/>
|
||||
<field name="laytime_clause" colspan="3"/>
|
||||
<label name="laytime_start"/>
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
<field name="party"/>
|
||||
<field name="party_role"/>
|
||||
<field name="responsibility"/>
|
||||
<field name="laytime_start_event"/>
|
||||
<field name="laytime_start_offset"/>
|
||||
<field name="laytime_start_offset_unit"/>
|
||||
<field name="laytime_end_event"/>
|
||||
<field name="laytime_allowed"/>
|
||||
<field name="laytime_unit"/>
|
||||
</tree>
|
||||
|
||||
Reference in New Issue
Block a user