Add SOF calculation overrides
This commit is contained in:
@@ -39,7 +39,10 @@ CHARTER_LAYTIME_EVENT_SELECTION = [
|
||||
('hoses_connected', 'Hoses Connected'),
|
||||
('start_pumping', 'Start Pumping'),
|
||||
('end_pumping', 'End Pumping'),
|
||||
('completed_loading', 'Completed Loading'),
|
||||
('completed_discharge', 'Completed Discharge'),
|
||||
('hoses_disconnected', 'Hoses Disconnected'),
|
||||
('documents_on_board', 'Documents on Board'),
|
||||
('sailing', 'Sailing'),
|
||||
]
|
||||
|
||||
@@ -2539,7 +2542,8 @@ class ShipmentIn(metaclass=PoolMeta):
|
||||
laytime_start = s._applied_laytime_start()
|
||||
if laytime_start:
|
||||
s.laytime_commenced = laytime_start
|
||||
quantity = float(s.quantity or sh.get_quantity() or 0)
|
||||
quantity = float(s._calculation_quantity(
|
||||
s.quantity or sh.get_quantity() or 0))
|
||||
s.laytime_allowed = s._applied_laytime_allowed(quantity)
|
||||
s.laytime_completed = s._applied_laytime_end()
|
||||
#s.laytime_completed = s.laytime_commenced + datetime.timedelta(hours=s.laytime_allowed)
|
||||
@@ -3146,7 +3150,10 @@ class StatementOfFacts(ModelSQL, ModelView):
|
||||
'hoses_connected': 'Hoses Connected',
|
||||
'start_pumping': 'Start Pumping',
|
||||
'end_pumping': 'End Pumping',
|
||||
'completed_loading': 'Completed Loading',
|
||||
'completed_discharge': 'Completed Discharge',
|
||||
'hoses_disconnected': 'Hoses Disconnected',
|
||||
'documents_on_board': 'Documents on Board',
|
||||
'sailing': 'Sailing',
|
||||
}
|
||||
_party_role_labels = {
|
||||
@@ -3176,8 +3183,14 @@ class StatementOfFacts(ModelSQL, ModelView):
|
||||
'hoses_connected': ('hoses_connected_date', 'hoses_connected_hour'),
|
||||
'start_pumping': ('start_pumping_date', 'start_pumping_hour'),
|
||||
'end_pumping': ('end_pumping_date', 'end_pumping_hour'),
|
||||
'completed_loading': (
|
||||
'completed_loading_date', 'completed_loading_hour'),
|
||||
'completed_discharge': (
|
||||
'completed_discharge_date', 'completed_discharge_hour'),
|
||||
'hoses_disconnected': (
|
||||
'hoses_disconnected_date', 'hoses_disconnected_hour'),
|
||||
'documents_on_board': (
|
||||
'documents_on_board_date', 'documents_on_board_hour'),
|
||||
'sailing_time': ('sailing_date', 'sailing_hour'),
|
||||
'laytime_commenced': (
|
||||
'laytime_commenced_date', 'laytime_commenced_hour'),
|
||||
@@ -3200,8 +3213,15 @@ class StatementOfFacts(ModelSQL, ModelView):
|
||||
laytime_clause_hour = fields.Time("")
|
||||
laytime_start = fields.DateTime('Laytime Start')
|
||||
laytime_end = fields.DateTime('Laytime End')
|
||||
demurrage_rate = fields.Numeric('Demurrage Rate ($/day)', digits=(16, 2))
|
||||
pumping_rate = fields.Float('Pumping Rate (MT/hour)')
|
||||
demurrage_rate = fields.Numeric('Demurrage Rate ($/day)', digits=(16, 2))
|
||||
pumping_rate = fields.Float('Pumping Rate (MT/hour)')
|
||||
demurrage_rate_override = fields.Numeric(
|
||||
'Demurrage Rate Override ($/day)', digits=(16, 2))
|
||||
despatch_rate_override = fields.Numeric(
|
||||
'Despatch Rate Override ($/day)', digits=(16, 2))
|
||||
pumping_rate_override = fields.Numeric(
|
||||
'Pumping Rate Override (MT/hour)', digits=(16, 4))
|
||||
additional_quantity = fields.Numeric('Additional Qt', digits=(16, 4))
|
||||
|
||||
laytime_type_o = fields.Selection([
|
||||
('nor', 'NOR +'),
|
||||
@@ -3253,9 +3273,18 @@ class StatementOfFacts(ModelSQL, ModelView):
|
||||
end_pumping = fields.DateTime('End Pumping')
|
||||
end_pumping_date = fields.Date('End Pumping Date')
|
||||
end_pumping_hour = fields.Time('End Pumping Time')
|
||||
completed_loading = fields.DateTime('Completed Loading')
|
||||
completed_loading_date = fields.Date('Completed Loading Date')
|
||||
completed_loading_hour = fields.Time('Completed Loading Time')
|
||||
completed_discharge = fields.DateTime('Completed Discharge')
|
||||
completed_discharge_date = fields.Date('Completed Discharge Date')
|
||||
completed_discharge_hour = fields.Time('Completed Discharge Time')
|
||||
hoses_disconnected = fields.DateTime('Hoses Disconnected')
|
||||
hoses_disconnected_date = fields.Date('Hoses Disconnected Date')
|
||||
hoses_disconnected_hour = fields.Time('Hoses Disconnected Time')
|
||||
documents_on_board = fields.DateTime('Documents on Board')
|
||||
documents_on_board_date = fields.Date('Documents on Board Date')
|
||||
documents_on_board_hour = fields.Time('Documents on Board Time')
|
||||
sailing_time = fields.DateTime('Sailing Time')
|
||||
sailing_date = fields.Date('Sailing Date')
|
||||
sailing_hour = fields.Time('Sailing Time')
|
||||
@@ -3451,6 +3480,32 @@ class StatementOfFacts(ModelSQL, ModelView):
|
||||
if self._rate_category(rate) == category and rate.rate is not None:
|
||||
return Decimal(str(rate.rate))
|
||||
|
||||
def _rate_override(self, category):
|
||||
override_fields = {
|
||||
'demurrage': 'demurrage_rate_override',
|
||||
'despatch': 'despatch_rate_override',
|
||||
'pumping': 'pumping_rate_override',
|
||||
}
|
||||
value = getattr(self, override_fields.get(category, ''), None)
|
||||
if value is not None:
|
||||
return Decimal(str(value))
|
||||
|
||||
def _applied_rate(self, category):
|
||||
override = self._rate_override(category)
|
||||
if override is not None:
|
||||
return override
|
||||
rate = self._condition_rate(category)
|
||||
if rate is not None:
|
||||
return rate
|
||||
if category == 'demurrage':
|
||||
legacy = self.demurrage_rate_o or self.demurrage_rate
|
||||
if legacy is not None:
|
||||
return Decimal(str(legacy))
|
||||
if category == 'pumping':
|
||||
legacy = self.pumping_rate_o or self.pumping_rate
|
||||
if legacy:
|
||||
return Decimal(str(legacy))
|
||||
|
||||
def _applied_laytime_allowed(self, quantity):
|
||||
condition = getattr(self, 'applied_condition', None)
|
||||
if condition and condition.laytime_allowed is not None:
|
||||
@@ -3460,14 +3515,10 @@ class StatementOfFacts(ModelSQL, ModelView):
|
||||
allowed *= Decimal(24)
|
||||
return float(allowed)
|
||||
|
||||
pumping_rate = self._condition_rate('pumping')
|
||||
pumping_rate = self._applied_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:
|
||||
@@ -3505,7 +3556,10 @@ class StatementOfFacts(ModelSQL, ModelView):
|
||||
'hoses_connected': self.hoses_connected,
|
||||
'start_pumping': self.start_pumping,
|
||||
'end_pumping': self.end_pumping,
|
||||
'completed_loading': self.completed_loading,
|
||||
'completed_discharge': self.completed_discharge,
|
||||
'hoses_disconnected': self.hoses_disconnected,
|
||||
'documents_on_board': self.documents_on_board,
|
||||
'sailing': self.sailing_time,
|
||||
}.get(event)
|
||||
|
||||
@@ -3525,6 +3579,8 @@ class StatementOfFacts(ModelSQL, ModelView):
|
||||
def _laytime_start_base(self):
|
||||
condition = getattr(self, 'applied_condition', None)
|
||||
rule = getattr(condition, 'laytime_start_rule', 'simple')
|
||||
if rule not in {'earliest_of', 'latest_of'}:
|
||||
rule = 'simple'
|
||||
if rule in {'earliest_of', 'latest_of'}:
|
||||
candidates = self._laytime_start_rule_candidates()
|
||||
if candidates:
|
||||
@@ -3540,8 +3596,8 @@ class StatementOfFacts(ModelSQL, ModelView):
|
||||
if not base:
|
||||
return
|
||||
condition = getattr(self, 'applied_condition', None)
|
||||
if getattr(condition, 'laytime_start_rule', 'simple') in {
|
||||
'earliest_of', 'latest_of'}:
|
||||
rule = getattr(condition, 'laytime_start_rule', 'simple')
|
||||
if rule in {'earliest_of', 'latest_of'}:
|
||||
return base
|
||||
return base + self._applied_turn_delta()
|
||||
|
||||
@@ -3557,12 +3613,10 @@ class StatementOfFacts(ModelSQL, ModelView):
|
||||
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))
|
||||
rate = self._applied_rate('demurrage') or Decimal(0)
|
||||
return round(balance * rate / Decimal(24), 2)
|
||||
if balance > 0:
|
||||
rate = self._condition_rate('despatch')
|
||||
rate = self._applied_rate('despatch')
|
||||
if rate is not None:
|
||||
self.compensation_type = 'despatch'
|
||||
return round(balance * rate / Decimal(24), 2)
|
||||
@@ -3584,8 +3638,8 @@ class StatementOfFacts(ModelSQL, ModelView):
|
||||
|
||||
def _calculation_offset_text(self):
|
||||
condition = getattr(self, 'applied_condition', None)
|
||||
if getattr(condition, 'laytime_start_rule', 'simple') in {
|
||||
'earliest_of', 'latest_of'}:
|
||||
rule = getattr(condition, 'laytime_start_rule', 'simple')
|
||||
if rule in {'earliest_of', 'latest_of'}:
|
||||
return self._calculation_start_candidates_text()
|
||||
value = getattr(condition, 'laytime_start_offset', None)
|
||||
unit = getattr(condition, 'laytime_start_offset_unit', None)
|
||||
@@ -3629,22 +3683,15 @@ class StatementOfFacts(ModelSQL, ModelView):
|
||||
return '; '.join(parts)
|
||||
|
||||
def _calculation_rate_text(self, category):
|
||||
rate = self._condition_rate(category)
|
||||
rate = self._applied_rate(category)
|
||||
if rate is not None:
|
||||
return '%s / day' % self._format_calculation_number(rate)
|
||||
if category == 'demurrage':
|
||||
legacy = self.demurrage_rate_o or self.demurrage_rate
|
||||
if legacy is not None:
|
||||
return '%s / day' % self._format_calculation_number(legacy)
|
||||
return ''
|
||||
|
||||
def _calculation_pumping_rate_text(self):
|
||||
rate = self._condition_rate('pumping')
|
||||
rate = self._applied_rate('pumping')
|
||||
if rate is not None:
|
||||
return '%s MT/hour' % self._format_calculation_number(rate)
|
||||
legacy = self.pumping_rate_o or self.pumping_rate
|
||||
if legacy:
|
||||
return '%s MT/hour' % self._format_calculation_number(legacy)
|
||||
return ''
|
||||
|
||||
def _calculation_laytime_allowed_text(self):
|
||||
@@ -3710,7 +3757,13 @@ class StatementOfFacts(ModelSQL, ModelView):
|
||||
def get_qt(self,name):
|
||||
if self.shipment:
|
||||
return self.shipment.get_quantity()
|
||||
|
||||
|
||||
def _calculation_quantity(self, base_quantity=None):
|
||||
if base_quantity is None:
|
||||
base_quantity = self.quantity
|
||||
return Decimal(str(base_quantity or 0)) + Decimal(str(
|
||||
self.additional_quantity or 0))
|
||||
|
||||
class SoFEvent(ModelSQL, ModelView):
|
||||
"Event from Statement of Facts"
|
||||
__name__ = 'sof.event'
|
||||
|
||||
@@ -1457,6 +1457,30 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
sof._applied_compensation_amount(), Decimal('-12000.00'))
|
||||
self.assertEqual(sof.compensation_type, 'demurrage')
|
||||
|
||||
@with_transaction()
|
||||
def test_sof_calculation_rate_overrides_win_over_condition_rates(self):
|
||||
'sof calculation rate overrides are used before condition rates'
|
||||
Sof = Pool().get('sof.statement')
|
||||
sof = Sof()
|
||||
sof.applied_condition = Mock(
|
||||
laytime_allowed=None,
|
||||
rates=[
|
||||
Mock(rate=Decimal('24000'), rate_type=Mock(category='demurrage')),
|
||||
Mock(rate=Decimal('100'), rate_type=Mock(category='pumping')),
|
||||
])
|
||||
sof.demurrage_rate_override = Decimal('36000')
|
||||
sof.pumping_rate_override = Decimal('200')
|
||||
sof.laytime_balance = -Decimal('12')
|
||||
|
||||
self.assertEqual(
|
||||
sof._applied_laytime_allowed(Decimal('1000')), 5.0)
|
||||
self.assertEqual(
|
||||
sof._applied_compensation_amount(), Decimal('-18000.00'))
|
||||
self.assertEqual(sof.compensation_type, 'demurrage')
|
||||
self.assertEqual(
|
||||
sof.get_calculation_info('calculation_demurrage_rate'),
|
||||
'36000 / day')
|
||||
|
||||
@with_transaction()
|
||||
def test_sof_available_conditions_include_shipment_contracts(self):
|
||||
'sof applied condition choices include owner, purchase and sale terms'
|
||||
@@ -1539,13 +1563,25 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
'sof laytime end reads the structured end event'
|
||||
Sof = Pool().get('sof.statement')
|
||||
sof = Sof()
|
||||
sof.applied_condition = Mock(laytime_end_event='hoses_disconnected')
|
||||
sof.applied_condition = Mock(laytime_end_event='documents_on_board')
|
||||
sof.end_pumping = datetime.datetime(2026, 6, 1, 12, 0)
|
||||
sof.hoses_disconnected = datetime.datetime(2026, 6, 1, 13, 0)
|
||||
sof.documents_on_board = datetime.datetime(2026, 6, 1, 14, 0)
|
||||
|
||||
self.assertEqual(
|
||||
sof._applied_laytime_end(),
|
||||
datetime.datetime(2026, 6, 1, 13, 0))
|
||||
datetime.datetime(2026, 6, 1, 14, 0))
|
||||
|
||||
@with_transaction()
|
||||
def test_sof_additional_quantity_is_added_to_calculation_quantity(self):
|
||||
'sof additional quantity is added to the quantity used for calculation'
|
||||
Sof = Pool().get('sof.statement')
|
||||
sof = Sof()
|
||||
sof.additional_quantity = Decimal('25')
|
||||
|
||||
self.assertEqual(
|
||||
sof._calculation_quantity(Decimal('100')),
|
||||
Decimal('125'))
|
||||
|
||||
@with_transaction()
|
||||
def test_sof_laytime_ignores_free_text_clauses(self):
|
||||
@@ -1627,6 +1663,8 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
'notice_of_readiness_hour': datetime.time(8, 0),
|
||||
'all_fast_date': datetime.date(2026, 7, 6),
|
||||
'all_fast_hour': datetime.time(10, 30),
|
||||
'documents_on_board_date': datetime.date(2026, 7, 6),
|
||||
'documents_on_board_hour': datetime.time(12, 0),
|
||||
})
|
||||
|
||||
self.assertEqual(
|
||||
@@ -1635,6 +1673,9 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
self.assertEqual(
|
||||
values['all_fast'],
|
||||
datetime.datetime(2026, 7, 6, 10, 30))
|
||||
self.assertEqual(
|
||||
values['documents_on_board'],
|
||||
datetime.datetime(2026, 7, 6, 12, 0))
|
||||
|
||||
@with_transaction()
|
||||
def test_sof_event_date_time_values_sync_to_datetime(self):
|
||||
|
||||
@@ -46,10 +46,24 @@
|
||||
<field name="end_pumping_date"/>
|
||||
<label name="end_pumping_hour"/>
|
||||
<field name="end_pumping_hour"/>
|
||||
<newline/>
|
||||
<label name="completed_loading_date"/>
|
||||
<field name="completed_loading_date"/>
|
||||
<label name="completed_loading_hour"/>
|
||||
<field name="completed_loading_hour"/>
|
||||
<label name="completed_discharge_date"/>
|
||||
<field name="completed_discharge_date"/>
|
||||
<label name="completed_discharge_hour"/>
|
||||
<field name="completed_discharge_hour"/>
|
||||
<newline/>
|
||||
<label name="hoses_disconnected_date"/>
|
||||
<field name="hoses_disconnected_date"/>
|
||||
<label name="hoses_disconnected_hour"/>
|
||||
<field name="hoses_disconnected_hour"/>
|
||||
<label name="documents_on_board_date"/>
|
||||
<field name="documents_on_board_date"/>
|
||||
<label name="documents_on_board_hour"/>
|
||||
<field name="documents_on_board_hour"/>
|
||||
<newline/>
|
||||
<label name="sailing_date"/>
|
||||
<field name="sailing_date"/>
|
||||
@@ -77,6 +91,13 @@
|
||||
<label name="calculation_despatch_rate"/>
|
||||
<field name="calculation_despatch_rate"/>
|
||||
<newline/>
|
||||
<label name="demurrage_rate_override"/>
|
||||
<field name="demurrage_rate_override"/>
|
||||
<label name="despatch_rate_override"/>
|
||||
<field name="despatch_rate_override"/>
|
||||
<label name="pumping_rate_override"/>
|
||||
<field name="pumping_rate_override"/>
|
||||
<newline/>
|
||||
<separator string="Laytime basis" id="calculation_laytime_section" colspan="8"/>
|
||||
<label name="calculation_start_event"/>
|
||||
<field name="calculation_start_event"/>
|
||||
@@ -94,7 +115,9 @@
|
||||
<newline/>
|
||||
<separator string="Result" id="calculation_result_section" colspan="8"/>
|
||||
<label name="quantity"/>
|
||||
<field name="quantity" colspan="7"/>
|
||||
<field name="quantity"/>
|
||||
<label name="additional_quantity"/>
|
||||
<field name="additional_quantity"/>
|
||||
<newline/>
|
||||
<label name="laytime_commenced_date"/>
|
||||
<field name="laytime_commenced_date"/>
|
||||
|
||||
Reference in New Issue
Block a user