demurrage
This commit is contained in:
@@ -1995,6 +1995,34 @@ class StatementOfFacts(ModelSQL, ModelView):
|
||||
"Statement of Facts for Vessel Discharge"
|
||||
__name__ = 'sof.statement'
|
||||
|
||||
_event_labels = {
|
||||
'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',
|
||||
}
|
||||
_party_role_labels = {
|
||||
'owner': 'Owner',
|
||||
'charterer': 'Charterer',
|
||||
'supplier': 'Supplier',
|
||||
'customer': 'Customer',
|
||||
'broker': 'Broker',
|
||||
'agent': 'Agent',
|
||||
'terminal': 'Terminal',
|
||||
'other': 'Other',
|
||||
}
|
||||
_responsibility_labels = {
|
||||
'ours': 'Ours',
|
||||
'counterparty': 'Counterparty',
|
||||
'shared': 'Shared',
|
||||
'pass_through': 'Pass-through',
|
||||
'owner': 'Owner',
|
||||
'charterer': 'Charterer',
|
||||
}
|
||||
|
||||
_datetime_parts = {
|
||||
'arrival_time': ('arrival_date', 'arrival_hour'),
|
||||
'notice_of_readiness_time': (
|
||||
@@ -2107,6 +2135,32 @@ class StatementOfFacts(ModelSQL, ModelView):
|
||||
], 'Compensation Type')
|
||||
compensation_amount = fields.Numeric('Compensation Amount ($)', readonly=True, digits=(16, 2))
|
||||
quantity = fields.Function(fields.Float("Quantity loaded"),'get_qt')
|
||||
calculation_condition = fields.Function(
|
||||
fields.Char("Condition Used"), 'get_calculation_info')
|
||||
calculation_party_role = fields.Function(
|
||||
fields.Char("Party Role"), 'get_calculation_info')
|
||||
calculation_responsibility = fields.Function(
|
||||
fields.Char("Responsibility"), 'get_calculation_info')
|
||||
calculation_laytime_allowed = fields.Function(
|
||||
fields.Char("Allowed Laytime Source"), 'get_calculation_info')
|
||||
calculation_pumping_rate = fields.Function(
|
||||
fields.Char("Pumping Rate Source"), 'get_calculation_info')
|
||||
calculation_demurrage_rate = fields.Function(
|
||||
fields.Char("Demurrage Rate Source"), 'get_calculation_info')
|
||||
calculation_despatch_rate = fields.Function(
|
||||
fields.Char("Despatch Rate Source"), 'get_calculation_info')
|
||||
calculation_start_event = fields.Function(
|
||||
fields.Char("Start Event"), 'get_calculation_info')
|
||||
calculation_start_event_time = fields.Function(
|
||||
fields.Char("Start Event Time"), 'get_calculation_info')
|
||||
calculation_start_offset = fields.Function(
|
||||
fields.Char("Start Offset"), 'get_calculation_info')
|
||||
calculation_end_event = fields.Function(
|
||||
fields.Char("End Event"), 'get_calculation_info')
|
||||
calculation_end_event_time = fields.Function(
|
||||
fields.Char("End Event Time"), 'get_calculation_info')
|
||||
calculation_rate_used = fields.Function(
|
||||
fields.Char("Rate Used"), 'get_calculation_info')
|
||||
|
||||
notes = fields.Text('Remarks / Notes')
|
||||
|
||||
@@ -2329,6 +2383,115 @@ class StatementOfFacts(ModelSQL, ModelView):
|
||||
self.compensation_type = None
|
||||
return Decimal(0)
|
||||
|
||||
@staticmethod
|
||||
def _format_calculation_datetime(value):
|
||||
if value:
|
||||
return value.strftime('%d/%m/%Y %H:%M')
|
||||
return ''
|
||||
|
||||
@staticmethod
|
||||
def _format_calculation_number(value):
|
||||
if value is None:
|
||||
return ''
|
||||
decimal = Decimal(str(value)).normalize()
|
||||
return format(decimal, 'f')
|
||||
|
||||
def _calculation_offset_text(self):
|
||||
condition = getattr(self, 'applied_condition', None)
|
||||
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:
|
||||
return '%s %s' % (
|
||||
self._format_calculation_number(value),
|
||||
unit or 'hours')
|
||||
legacy = self.laytime_clause_hour or self.laytime_clause_hour_o
|
||||
if legacy:
|
||||
return legacy.strftime('%H:%M:%S')
|
||||
return ''
|
||||
|
||||
def _calculation_rate_text(self, category):
|
||||
rate = self._condition_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')
|
||||
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):
|
||||
condition = getattr(self, 'applied_condition', None)
|
||||
if condition and condition.laytime_allowed is not None:
|
||||
value = self._format_calculation_number(condition.laytime_allowed)
|
||||
return '%s %s' % (value, condition.laytime_unit or 'hours')
|
||||
pumping_rate = self._calculation_pumping_rate_text()
|
||||
if pumping_rate:
|
||||
return 'Computed from pumping rate'
|
||||
return ''
|
||||
|
||||
def _calculation_rate_used_text(self):
|
||||
balance = Decimal(str(self.laytime_balance or 0))
|
||||
if balance < 0:
|
||||
rate = self._calculation_rate_text('demurrage')
|
||||
if rate:
|
||||
return 'Demurrage: %s' % rate
|
||||
if balance > 0:
|
||||
rate = self._calculation_rate_text('despatch')
|
||||
if rate:
|
||||
return 'Despatch: %s' % rate
|
||||
return ''
|
||||
|
||||
def get_calculation_info(self, name):
|
||||
condition = getattr(self, 'applied_condition', None)
|
||||
start_event = getattr(condition, 'laytime_start_event', None)
|
||||
end_event = getattr(condition, 'laytime_end_event', None)
|
||||
if name == 'calculation_condition':
|
||||
return getattr(condition, 'rec_name', None) or getattr(
|
||||
condition, 'name', None) or ''
|
||||
if name == 'calculation_party_role':
|
||||
return self._party_role_labels.get(
|
||||
getattr(condition, 'party_role', None), '')
|
||||
if name == 'calculation_responsibility':
|
||||
return self._responsibility_labels.get(
|
||||
getattr(condition, 'responsibility', None), '')
|
||||
if name == 'calculation_laytime_allowed':
|
||||
return self._calculation_laytime_allowed_text()
|
||||
if name == 'calculation_pumping_rate':
|
||||
return self._calculation_pumping_rate_text()
|
||||
if name == 'calculation_demurrage_rate':
|
||||
return self._calculation_rate_text('demurrage')
|
||||
if name == 'calculation_despatch_rate':
|
||||
return self._calculation_rate_text('despatch')
|
||||
if name == 'calculation_start_event':
|
||||
return self._event_labels.get(
|
||||
start_event, self._event_labels['notice_of_readiness'])
|
||||
if name == 'calculation_start_event_time':
|
||||
return self._format_calculation_datetime(
|
||||
self._laytime_start_base())
|
||||
if name == 'calculation_start_offset':
|
||||
return self._calculation_offset_text()
|
||||
if name == 'calculation_end_event':
|
||||
return self._event_labels.get(
|
||||
end_event, self._event_labels['hoses_disconnected'])
|
||||
if name == 'calculation_end_event_time':
|
||||
return self._format_calculation_datetime(
|
||||
self._applied_laytime_end())
|
||||
if name == 'calculation_rate_used':
|
||||
return self._calculation_rate_used_text()
|
||||
return ''
|
||||
|
||||
def get_qt(self,name):
|
||||
if self.shipment:
|
||||
return self.shipment.get_quantity()
|
||||
|
||||
@@ -270,6 +270,52 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
sof._applied_laytime_end(),
|
||||
datetime.datetime(2026, 6, 1, 12, 0))
|
||||
|
||||
@with_transaction()
|
||||
def test_sof_calculation_info_explains_structured_inputs(self):
|
||||
'sof calculation info displays the structured calculation inputs'
|
||||
Sof = Pool().get('sof.statement')
|
||||
sof = Sof()
|
||||
sof.applied_condition = Mock(
|
||||
rec_name='Discharge',
|
||||
name='Discharge',
|
||||
party_role='owner',
|
||||
responsibility='ours',
|
||||
laytime_allowed=Decimal('72'),
|
||||
laytime_unit='hours',
|
||||
laytime_start_event='notice_of_readiness',
|
||||
laytime_start_offset=Decimal('6'),
|
||||
laytime_start_offset_unit='hours',
|
||||
laytime_end_event='hoses_disconnected',
|
||||
laytime_start='Arrival + 1 day',
|
||||
laytime_end='End pumping',
|
||||
rates=[
|
||||
Mock(rate=Decimal('24000'), rate_type=Mock(category='demurrage')),
|
||||
Mock(rate=Decimal('12000'), rate_type=Mock(category='despatch')),
|
||||
])
|
||||
sof.notice_of_readiness_time = datetime.datetime(2026, 6, 1, 2, 0)
|
||||
sof.hoses_disconnected = datetime.datetime(2026, 6, 4, 10, 0)
|
||||
sof.laytime_balance = -Decimal('12')
|
||||
|
||||
self.assertEqual(
|
||||
sof.get_calculation_info('calculation_condition'), 'Discharge')
|
||||
self.assertEqual(
|
||||
sof.get_calculation_info('calculation_start_event'),
|
||||
'Notice of Readiness')
|
||||
self.assertEqual(
|
||||
sof.get_calculation_info('calculation_start_event_time'),
|
||||
'01/06/2026 02:00')
|
||||
self.assertEqual(
|
||||
sof.get_calculation_info('calculation_start_offset'), '6 hours')
|
||||
self.assertEqual(
|
||||
sof.get_calculation_info('calculation_end_event'),
|
||||
'Hoses Disconnected')
|
||||
self.assertEqual(
|
||||
sof.get_calculation_info('calculation_end_event_time'),
|
||||
'04/06/2026 10:00')
|
||||
self.assertEqual(
|
||||
sof.get_calculation_info('calculation_rate_used'),
|
||||
'Demurrage: 24000 / day')
|
||||
|
||||
@with_transaction()
|
||||
def test_sof_header_date_time_values_sync_to_datetime(self):
|
||||
'sof header date and time fields build the technical datetime value'
|
||||
|
||||
@@ -56,6 +56,39 @@
|
||||
</page>
|
||||
|
||||
<page string="Calculation" id="calcul" col="8">
|
||||
<separator string="Applied condition" id="calculation_condition_section" colspan="8"/>
|
||||
<label name="calculation_condition"/>
|
||||
<field name="calculation_condition" colspan="3"/>
|
||||
<label name="calculation_party_role"/>
|
||||
<field name="calculation_party_role"/>
|
||||
<label name="calculation_responsibility"/>
|
||||
<field name="calculation_responsibility"/>
|
||||
<newline/>
|
||||
<label name="calculation_laytime_allowed"/>
|
||||
<field name="calculation_laytime_allowed"/>
|
||||
<label name="calculation_pumping_rate"/>
|
||||
<field name="calculation_pumping_rate"/>
|
||||
<label name="calculation_demurrage_rate"/>
|
||||
<field name="calculation_demurrage_rate"/>
|
||||
<label name="calculation_despatch_rate"/>
|
||||
<field name="calculation_despatch_rate"/>
|
||||
<newline/>
|
||||
<separator string="Laytime basis" id="calculation_laytime_section" colspan="8"/>
|
||||
<label name="calculation_start_event"/>
|
||||
<field name="calculation_start_event"/>
|
||||
<label name="calculation_start_event_time"/>
|
||||
<field name="calculation_start_event_time"/>
|
||||
<label name="calculation_start_offset"/>
|
||||
<field name="calculation_start_offset"/>
|
||||
<newline/>
|
||||
<label name="calculation_end_event"/>
|
||||
<field name="calculation_end_event"/>
|
||||
<label name="calculation_end_event_time"/>
|
||||
<field name="calculation_end_event_time"/>
|
||||
<label name="calculation_rate_used"/>
|
||||
<field name="calculation_rate_used"/>
|
||||
<newline/>
|
||||
<separator string="Result" id="calculation_result_section" colspan="8"/>
|
||||
<label name="quantity"/>
|
||||
<field name="quantity" colspan="7"/>
|
||||
<newline/>
|
||||
|
||||
Reference in New Issue
Block a user