Demurrage
This commit is contained in:
@@ -1953,9 +1953,25 @@ import io
|
||||
import re
|
||||
import PyPDF2
|
||||
|
||||
class StatementOfFacts(ModelSQL, ModelView):
|
||||
"Statement of Facts for Vessel Discharge"
|
||||
__name__ = 'sof.statement'
|
||||
class StatementOfFacts(ModelSQL, ModelView):
|
||||
"Statement of Facts for Vessel Discharge"
|
||||
__name__ = 'sof.statement'
|
||||
|
||||
_datetime_parts = {
|
||||
'arrival_time': ('arrival_date', 'arrival_hour'),
|
||||
'notice_of_readiness_time': (
|
||||
'notice_of_readiness_date', 'notice_of_readiness_hour'),
|
||||
'hoses_connected': ('hoses_connected_date', 'hoses_connected_hour'),
|
||||
'start_pumping': ('start_pumping_date', 'start_pumping_hour'),
|
||||
'end_pumping': ('end_pumping_date', 'end_pumping_hour'),
|
||||
'hoses_disconnected': (
|
||||
'hoses_disconnected_date', 'hoses_disconnected_hour'),
|
||||
'sailing_time': ('sailing_date', 'sailing_hour'),
|
||||
'laytime_commenced': (
|
||||
'laytime_commenced_date', 'laytime_commenced_hour'),
|
||||
'laytime_completed': (
|
||||
'laytime_completed_date', 'laytime_completed_hour'),
|
||||
}
|
||||
|
||||
# === A. Informations générales ===
|
||||
shipment = fields.Many2One('stock.shipment.in', 'Shipment', required=True, ondelete='CASCADE')
|
||||
@@ -2008,19 +2024,41 @@ class StatementOfFacts(ModelSQL, ModelView):
|
||||
sof = fields.Many2One('document.incoming',"Statement of facts")
|
||||
|
||||
# === B. Événements du SOF ===
|
||||
sof_events = fields.One2Many('sof.event', 'statement', "SOF Events")
|
||||
|
||||
arrival_time = fields.DateTime('Arrival Time')
|
||||
hoses_connected = fields.DateTime('Hoses Connected')
|
||||
start_pumping = fields.DateTime('Start Pumping')
|
||||
end_pumping = fields.DateTime('End Pumping')
|
||||
hoses_disconnected = fields.DateTime('Hoses Disconnected')
|
||||
sailing_time = fields.DateTime('Sailing Time')
|
||||
|
||||
# === C. Résultats du calcul ===
|
||||
laytime_commenced = fields.DateTime('Laytime Commenced')
|
||||
laytime_completed = fields.DateTime('Laytime Completed')
|
||||
actual_time_used = fields.Float('Actual Time Used (hours)', readonly=True)
|
||||
sof_events = fields.One2Many('sof.event', 'statement', "SOF Events")
|
||||
|
||||
arrival_time = fields.DateTime('Arrival Time')
|
||||
arrival_date = fields.Date('Arrival Date')
|
||||
arrival_hour = fields.Time('Arrival Time')
|
||||
hoses_connected = fields.DateTime('Hoses Connected')
|
||||
hoses_connected_date = fields.Date('Hoses Connected Date')
|
||||
hoses_connected_hour = fields.Time('Hoses Connected Time')
|
||||
start_pumping = fields.DateTime('Start Pumping')
|
||||
start_pumping_date = fields.Date('Start Pumping Date')
|
||||
start_pumping_hour = fields.Time('Start Pumping Time')
|
||||
end_pumping = fields.DateTime('End Pumping')
|
||||
end_pumping_date = fields.Date('End Pumping Date')
|
||||
end_pumping_hour = fields.Time('End Pumping Time')
|
||||
hoses_disconnected = fields.DateTime('Hoses Disconnected')
|
||||
hoses_disconnected_date = fields.Date('Hoses Disconnected Date')
|
||||
hoses_disconnected_hour = fields.Time('Hoses Disconnected Time')
|
||||
sailing_time = fields.DateTime('Sailing Time')
|
||||
sailing_date = fields.Date('Sailing Date')
|
||||
sailing_hour = fields.Time('Sailing Time')
|
||||
notice_of_readiness_date = fields.Date('Notice of Readiness Date')
|
||||
notice_of_readiness_hour = fields.Time('Notice of Readiness Time')
|
||||
|
||||
# === C. Résultats du calcul ===
|
||||
laytime_commenced = fields.DateTime('Laytime Commenced')
|
||||
laytime_commenced_date = fields.Date(
|
||||
'Laytime Commenced Date', readonly=True)
|
||||
laytime_commenced_hour = fields.Time(
|
||||
'Laytime Commenced Time', readonly=True)
|
||||
laytime_completed = fields.DateTime('Laytime Completed')
|
||||
laytime_completed_date = fields.Date(
|
||||
'Laytime Completed Date', readonly=True)
|
||||
laytime_completed_hour = fields.Time(
|
||||
'Laytime Completed Time', readonly=True)
|
||||
actual_time_used = fields.Float('Actual Time Used (hours)', readonly=True)
|
||||
deductions = fields.Float('Deductions (hours)', readonly=True)
|
||||
laytime_used = fields.Float('Laytime Used (hours)', readonly=True)
|
||||
laytime_balance = fields.Float('Laytime Balance (hours)', readonly=True)
|
||||
@@ -2034,6 +2072,76 @@ class StatementOfFacts(ModelSQL, ModelView):
|
||||
|
||||
notes = fields.Text('Remarks / Notes')
|
||||
|
||||
@staticmethod
|
||||
def _combine_date_time(date, time):
|
||||
if date and time:
|
||||
return datetime.datetime.combine(date, time)
|
||||
|
||||
@staticmethod
|
||||
def _split_datetime(value):
|
||||
if value:
|
||||
return value.date(), value.time()
|
||||
return None, None
|
||||
|
||||
@classmethod
|
||||
def _sync_datetime_values(cls, values):
|
||||
for datetime_field, parts in cls._datetime_parts.items():
|
||||
date_field, time_field = parts
|
||||
if date_field in values or time_field in values:
|
||||
values[datetime_field] = cls._combine_date_time(
|
||||
values.get(date_field), values.get(time_field))
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def _sync_datetime_write_values(cls, record, values):
|
||||
values = dict(values)
|
||||
for datetime_field, parts in cls._datetime_parts.items():
|
||||
date_field, time_field = parts
|
||||
if date_field in values or time_field in values:
|
||||
values[datetime_field] = cls._combine_date_time(
|
||||
values.get(date_field, getattr(record, date_field, None)),
|
||||
values.get(time_field, getattr(record, time_field, None)))
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def _sync_datetime_parts(cls, statements):
|
||||
for statement in statements:
|
||||
changed = False
|
||||
for datetime_field, parts in cls._datetime_parts.items():
|
||||
value = getattr(statement, datetime_field, None)
|
||||
date_field, time_field = parts
|
||||
date_value, time_value = cls._split_datetime(value)
|
||||
if getattr(statement, date_field, None) != date_value:
|
||||
setattr(statement, date_field, date_value)
|
||||
changed = True
|
||||
if getattr(statement, time_field, None) != time_value:
|
||||
setattr(statement, time_field, time_value)
|
||||
changed = True
|
||||
if changed:
|
||||
statement.save()
|
||||
|
||||
@classmethod
|
||||
def create(cls, vlist):
|
||||
vlist = [cls._sync_datetime_values(dict(values)) for values in vlist]
|
||||
statements = super().create(vlist)
|
||||
cls._sync_datetime_parts(statements)
|
||||
return statements
|
||||
|
||||
@classmethod
|
||||
def write(cls, *args):
|
||||
new_args = []
|
||||
written_records = []
|
||||
for index in range(0, len(args), 2):
|
||||
records = args[index]
|
||||
values = args[index + 1]
|
||||
for record in records:
|
||||
new_args.extend([
|
||||
[record],
|
||||
cls._sync_datetime_write_values(record, values)])
|
||||
written_records.extend(records)
|
||||
super().write(*new_args)
|
||||
cls._sync_datetime_parts(written_records)
|
||||
|
||||
@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:
|
||||
@@ -2148,7 +2256,7 @@ class StatementOfFacts(ModelSQL, ModelView):
|
||||
if self.shipment:
|
||||
return self.shipment.get_quantity()
|
||||
|
||||
class SoFEvent(ModelSQL, ModelView):
|
||||
class SoFEvent(ModelSQL, ModelView):
|
||||
"Event from Statement of Facts"
|
||||
__name__ = 'sof.event'
|
||||
|
||||
@@ -2163,8 +2271,53 @@ class SoFEvent(ModelSQL, ModelView):
|
||||
end_time = fields.Time("End Time (H)")
|
||||
|
||||
duration = fields.Float("Duration")
|
||||
description = fields.Char("Description")
|
||||
deductible = fields.Boolean("Deduct from Laytime")
|
||||
description = fields.Char("Description")
|
||||
deductible = fields.Boolean("Deduct from Laytime")
|
||||
|
||||
@staticmethod
|
||||
def _combine_date_time(date, time):
|
||||
if date and time:
|
||||
return datetime.datetime.combine(date, time)
|
||||
|
||||
@classmethod
|
||||
def _sync_datetime_values(cls, values):
|
||||
if 'start_date' in values or 'start_time' in values:
|
||||
values['start'] = cls._combine_date_time(
|
||||
values.get('start_date'), values.get('start_time'))
|
||||
if 'end_date' in values or 'end_time' in values:
|
||||
values['end'] = cls._combine_date_time(
|
||||
values.get('end_date'), values.get('end_time'))
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def _sync_datetime_write_values(cls, record, values):
|
||||
values = dict(values)
|
||||
if 'start_date' in values or 'start_time' in values:
|
||||
values['start'] = cls._combine_date_time(
|
||||
values.get('start_date', getattr(record, 'start_date', None)),
|
||||
values.get('start_time', getattr(record, 'start_time', None)))
|
||||
if 'end_date' in values or 'end_time' in values:
|
||||
values['end'] = cls._combine_date_time(
|
||||
values.get('end_date', getattr(record, 'end_date', None)),
|
||||
values.get('end_time', getattr(record, 'end_time', None)))
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def create(cls, vlist):
|
||||
return super().create([
|
||||
cls._sync_datetime_values(dict(values)) for values in vlist])
|
||||
|
||||
@classmethod
|
||||
def write(cls, *args):
|
||||
new_args = []
|
||||
for index in range(0, len(args), 2):
|
||||
records = args[index]
|
||||
values = args[index + 1]
|
||||
for record in records:
|
||||
new_args.extend([
|
||||
[record],
|
||||
cls._sync_datetime_write_values(record, values)])
|
||||
super().write(*new_args)
|
||||
|
||||
class ImportSoFStart(ModelView):
|
||||
"""Wizard Start View"""
|
||||
|
||||
@@ -214,6 +214,35 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
sof._applied_compensation_amount(), Decimal('-12000.00'))
|
||||
self.assertEqual(sof.compensation_type, 'demurrage')
|
||||
|
||||
@with_transaction()
|
||||
def test_sof_header_date_time_values_sync_to_datetime(self):
|
||||
'sof header date and time fields build the technical datetime value'
|
||||
Sof = Pool().get('sof.statement')
|
||||
values = Sof._sync_datetime_values({
|
||||
'notice_of_readiness_date': datetime.date(2026, 7, 6),
|
||||
'notice_of_readiness_hour': datetime.time(8, 0),
|
||||
})
|
||||
|
||||
self.assertEqual(
|
||||
values['notice_of_readiness_time'],
|
||||
datetime.datetime(2026, 7, 6, 8, 0))
|
||||
|
||||
@with_transaction()
|
||||
def test_sof_event_date_time_values_sync_to_datetime(self):
|
||||
'sof event date and time fields build the technical datetime values'
|
||||
Event = Pool().get('sof.event')
|
||||
values = Event._sync_datetime_values({
|
||||
'start_date': datetime.date(2026, 1, 1),
|
||||
'start_time': datetime.time(10, 0),
|
||||
'end_date': datetime.date(2026, 1, 1),
|
||||
'end_time': datetime.time(12, 0),
|
||||
})
|
||||
|
||||
self.assertEqual(
|
||||
values['start'], datetime.datetime(2026, 1, 1, 10, 0))
|
||||
self.assertEqual(
|
||||
values['end'], datetime.datetime(2026, 1, 1, 12, 0))
|
||||
|
||||
def test_contract_clause_variables_and_rendering_use_contract_context(self):
|
||||
'contract clauses expose placeholders and render from purchase context'
|
||||
clause = purchase_module.ContractClause()
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<form>
|
||||
<label name="start"/>
|
||||
<field name="start"/>
|
||||
<label name="end"/>
|
||||
<field name="end"/>
|
||||
<label name="start_date"/>
|
||||
<field name="start_date"/>
|
||||
<field name="start_time"/>
|
||||
<label name="end_date"/>
|
||||
<field name="end_date"/>
|
||||
<field name="end_time"/>
|
||||
<newline/>
|
||||
|
||||
<label name="duration"/>
|
||||
|
||||
@@ -16,39 +16,48 @@
|
||||
<field name="timebar_warn"/>
|
||||
</page>
|
||||
|
||||
<page string="Sof events" id="event">
|
||||
<page string="Sof events" id="event" col="6">
|
||||
<label name="sof"/>
|
||||
<field name="sof"/>
|
||||
<field name="sof" colspan="5"/>
|
||||
<newline/>
|
||||
<label name="arrival_time"/>
|
||||
<field name="arrival_time"/>
|
||||
<field name="arrival_date"/>
|
||||
<field name="arrival_hour"/>
|
||||
<label name="notice_of_readiness_time"/>
|
||||
<field name="notice_of_readiness_time"/>
|
||||
<field name="notice_of_readiness_date"/>
|
||||
<field name="notice_of_readiness_hour"/>
|
||||
<newline/>
|
||||
<label name="hoses_connected"/>
|
||||
<field name="hoses_connected"/>
|
||||
<field name="hoses_connected_date"/>
|
||||
<field name="hoses_connected_hour"/>
|
||||
<label name="start_pumping"/>
|
||||
<field name="start_pumping"/>
|
||||
<field name="start_pumping_date"/>
|
||||
<field name="start_pumping_hour"/>
|
||||
<newline/>
|
||||
<label name="end_pumping"/>
|
||||
<field name="end_pumping"/>
|
||||
<field name="end_pumping_date"/>
|
||||
<field name="end_pumping_hour"/>
|
||||
<label name="hoses_disconnected"/>
|
||||
<field name="hoses_disconnected"/>
|
||||
<field name="hoses_disconnected_date"/>
|
||||
<field name="hoses_disconnected_hour"/>
|
||||
<newline/>
|
||||
<label name="sailing_time"/>
|
||||
<field name="sailing_time"/>
|
||||
<field name="sailing_date"/>
|
||||
<field name="sailing_hour"/>
|
||||
<newline/>
|
||||
<field name="sof_events" colspan="4" mode="tree,form" view_ids="purchase_trade.view_sof_event_tree,purchase_trade.view_sof_event_form"/>
|
||||
<field name="sof_events" colspan="6" mode="tree,form" view_ids="purchase_trade.view_sof_event_tree,purchase_trade.view_sof_event_form"/>
|
||||
</page>
|
||||
|
||||
<page string="Calculation" id="calcul">
|
||||
<page string="Calculation" id="calcul" col="6">
|
||||
<label name="quantity"/>
|
||||
<field name="quantity"/>
|
||||
<field name="quantity" colspan="5"/>
|
||||
<newline/>
|
||||
<label name="laytime_commenced"/>
|
||||
<field name="laytime_commenced"/>
|
||||
<field name="laytime_commenced_date"/>
|
||||
<field name="laytime_commenced_hour"/>
|
||||
<label name="laytime_completed"/>
|
||||
<field name="laytime_completed"/>
|
||||
<field name="laytime_completed_date"/>
|
||||
<field name="laytime_completed_hour"/>
|
||||
<newline/>
|
||||
<label name="actual_time_used"/>
|
||||
<field name="actual_time_used"/>
|
||||
|
||||
Reference in New Issue
Block a user