diff --git a/modules/purchase_trade/stock.py b/modules/purchase_trade/stock.py index 1e31e45..c875688 100755 --- a/modules/purchase_trade/stock.py +++ b/modules/purchase_trade/stock.py @@ -1622,13 +1622,12 @@ class ShipmentIn(metaclass=PoolMeta): 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()) + laytime_start = s._applied_laytime_start() + if laytime_start: + s.laytime_commenced = laytime_start quantity = float(s.quantity or sh.get_quantity() or 0) s.laytime_allowed = s._applied_laytime_allowed(quantity) - s.laytime_completed = s.hoses_disconnected + s.laytime_completed = s._applied_laytime_end() #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]) @@ -2227,6 +2226,10 @@ class StatementOfFacts(ModelSQL, ModelView): 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 self.laytime_clause_hour: t = self.laytime_clause_hour elif self.laytime_clause_hour_o: @@ -2236,6 +2239,48 @@ 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 _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 + return self.notice_of_readiness_time + + def _applied_laytime_start(self): + base = self._laytime_start_base() + if not base: + return + return base + self._applied_turn_delta() + + 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 + return self.hoses_disconnected or self.end_pumping or self.sailing_time + def _applied_compensation_amount(self): balance = Decimal(str(self.laytime_balance or 0)) if balance < 0: diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 45f57f2..4df537b 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -214,6 +214,33 @@ class PurchaseTradeTestCase(ModuleTestCase): sof._applied_compensation_amount(), Decimal('-12000.00')) 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' + Sof = Pool().get('sof.statement') + sof = Sof() + sof.applied_condition = Mock( + turn_time=None, + laytime_start='NOR + 6 hours') + sof.notice_of_readiness_time = datetime.datetime(2026, 6, 1, 2, 0) + + self.assertEqual( + sof._applied_laytime_start(), + 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' + Sof = Pool().get('sof.statement') + sof = Sof() + sof.applied_condition = Mock(laytime_end='Hoses disconnected') + 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_end(), + datetime.datetime(2026, 6, 1, 13, 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'