4694 lines
183 KiB
Python
Executable File
4694 lines
183 KiB
Python
Executable File
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||
# this repository contains the full copyright notices and license terms.
|
||
from trytond.model import fields, Workflow
|
||
from trytond.pool import Pool, PoolMeta
|
||
from trytond.pyson import Bool, Eval, Id
|
||
from trytond.model import (ModelSQL, ModelView)
|
||
from trytond.tools import is_full_text, lstrip_wildcard
|
||
from trytond.transaction import Transaction, inactive_records
|
||
from decimal import getcontext, Decimal, ROUND_HALF_UP
|
||
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
|
||
from sql.conditionals import Case
|
||
from sql import Column, Literal
|
||
from sql.functions import CurrentTimestamp, DateTrunc
|
||
from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction
|
||
from itertools import chain, groupby
|
||
from operator import itemgetter
|
||
import datetime
|
||
from collections import defaultdict
|
||
from sql import Table
|
||
from trytond.modules.purchase_trade.service import ContractFactory
|
||
import requests
|
||
import io
|
||
import base64
|
||
import logging
|
||
import json
|
||
import re
|
||
import html
|
||
from trytond.exceptions import UserError
|
||
from trytond.modules.stock.shipment import SupplierShipping as BaseSupplierShipping
|
||
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
CHARTER_LAYTIME_EVENT_SELECTION = [
|
||
(None, ''),
|
||
('notice_of_readiness', 'Notice of Readiness'),
|
||
('arrival', 'Arrival'),
|
||
('all_fast', 'All Fast'),
|
||
('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'),
|
||
]
|
||
|
||
class Location(metaclass=PoolMeta):
|
||
__name__ = 'stock.location'
|
||
|
||
def get_places(self):
|
||
t = Table('places')
|
||
cursor = Transaction().connection.cursor()
|
||
cursor.execute(*t.select(
|
||
t.PLACE_KEY,
|
||
where=t.PLACE_NAME.ilike(f'%{self.name}%')
|
||
))
|
||
rows = cursor.fetchall()
|
||
if rows:
|
||
return int(rows[0][0])
|
||
|
||
@classmethod
|
||
def getLocationByName(cls, location, type):
|
||
location = location.upper()
|
||
loc = cls.search([('name', '=', location),('type', '=', type)], limit=1)
|
||
if loc:
|
||
return loc[0].id
|
||
else:
|
||
loc = cls()
|
||
loc.name = location
|
||
loc.type = type
|
||
cls.save([loc])
|
||
return loc
|
||
|
||
@classmethod
|
||
def get_transit_id(cls):
|
||
return cls.getLocationByName('TRANSIT','storage')
|
||
|
||
def is_transit(self):
|
||
if self.name == 'Transit':
|
||
return True
|
||
|
||
class Move(metaclass=PoolMeta):
|
||
__name__ = 'stock.move'
|
||
|
||
bldate = fields.Date("BL date")
|
||
blnumber = fields.Char("BL number")
|
||
lotqt = fields.One2Many('lot.qt','lot_move',"Lots")
|
||
lot = fields.Many2One('lot.lot',"Lot")
|
||
trade_purchase = fields.Function(
|
||
fields.Many2One('purchase.purchase', "Purchase"), 'get_trade_purchase')
|
||
trade_sale = fields.Function(
|
||
fields.Many2One('sale.sale', "Sale"), 'get_trade_sale')
|
||
trade_purchase_reference = fields.Function(
|
||
fields.Char("Purchase Ref."), 'get_trade_purchase_reference')
|
||
trade_sale_reference = fields.Function(
|
||
fields.Char("Sale Ref."), 'get_trade_sale_reference')
|
||
|
||
def _trade_purchase_line(self):
|
||
return getattr(getattr(self, 'lot', None), 'line', None)
|
||
|
||
def _trade_sale_line(self):
|
||
return getattr(getattr(self, 'lot', None), 'sale_line', None)
|
||
|
||
@staticmethod
|
||
def _contract_reference(contract):
|
||
if not contract:
|
||
return None
|
||
return (
|
||
getattr(contract, 'reference', None)
|
||
or getattr(contract, 'our_reference', None))
|
||
|
||
def get_trade_purchase(self, name):
|
||
line = self._trade_purchase_line()
|
||
purchase = getattr(line, 'purchase', None)
|
||
return purchase.id if purchase else None
|
||
|
||
def get_trade_sale(self, name):
|
||
line = self._trade_sale_line()
|
||
sale = getattr(line, 'sale', None)
|
||
return sale.id if sale else None
|
||
|
||
def get_trade_purchase_reference(self, name):
|
||
line = self._trade_purchase_line()
|
||
return self._contract_reference(getattr(line, 'purchase', None))
|
||
|
||
def get_trade_sale_reference(self, name):
|
||
line = self._trade_sale_line()
|
||
return self._contract_reference(getattr(line, 'sale', None))
|
||
|
||
def get_linked_transit_move(self):
|
||
if self.from_location.is_transit():
|
||
Move = Pool().get('stock.move')
|
||
Location = Pool().get('stock.location')
|
||
moves = Move.search([('lot','=',self.lot),('to_location','=',Location.get_transit_id())],order=[('id', 'DESC')],limit=1)
|
||
return moves[0] if moves else None
|
||
|
||
@classmethod
|
||
def validate(cls, moves):
|
||
super(Move, cls).validate(moves)
|
||
Lot = Pool().get('lot.lot')
|
||
LotMove = Pool().get('lot.move')
|
||
for move in moves:
|
||
if move.lot:
|
||
l = Lot(move.lot.id)
|
||
lm = LotMove.search([('lot','=',l.id),('move','=',move)])
|
||
if len(lm) == 0:
|
||
lm = LotMove.search(['lot','>',0],order=[('sequence','DESC')])
|
||
new_lm = LotMove()
|
||
new_lm.move = move.id
|
||
new_lm.lot = l.id
|
||
new_lm.sequence = 1
|
||
if lm:
|
||
new_lm.sequence = lm[0].sequence + 1
|
||
|
||
LotMove.save([new_lm])
|
||
|
||
class InvoiceLine(metaclass=PoolMeta):
|
||
__name__ = 'account.invoice.line'
|
||
|
||
lot = fields.Many2One('lot.lot',"Lot")
|
||
fee = fields.Many2One('fee.fee',"Fee")
|
||
included_padding = fields.Function(
|
||
fields.Numeric("Inc. padding", digits=(1, 5)),
|
||
'get_included_padding')
|
||
|
||
def get_included_padding(self, name):
|
||
if (self.lot
|
||
and self.invoice_type == 'out'
|
||
and self.description == 'Pro forma'
|
||
and Decimal(str(self.quantity or 0)) > 0):
|
||
return self.lot.sale_invoice_padding or Decimal(0)
|
||
return Decimal(0)
|
||
|
||
@classmethod
|
||
def validate(cls, lines):
|
||
super(InvoiceLine, cls).validate(lines)
|
||
Lot = Pool().get('lot.lot')
|
||
for line in lines:
|
||
if line.lot and line.quantity > 0:
|
||
l = Lot(line.lot.id)
|
||
if line.description == 'Pro forma':
|
||
if line.invoice_type == 'in':
|
||
l.invoice_line_prov = line.id
|
||
l.lot_pur_inv_state = 'prov'
|
||
else:
|
||
l.sale_invoice_line_prov = line.id
|
||
l.lot_sale_inv_state = 'prov'
|
||
elif line.description == 'Final':
|
||
if line.invoice_type == 'in':
|
||
l.invoice_line = line.id
|
||
l.lot_pur_inv_state = 'invoiced'
|
||
else:
|
||
l.sale_invoice_line = line.id
|
||
l.lot_sale_inv_state = 'invoiced'
|
||
Lot.save([l])
|
||
|
||
class ShipmentInternal(metaclass=PoolMeta):
|
||
__name__ = 'stock.shipment.internal'
|
||
fees = fields.One2Many('fee.fee','shipment_internal',"Fees")
|
||
|
||
sof_json = [
|
||
{
|
||
"date": "2025-02-02",
|
||
"time": "19:00",
|
||
"event": "S/B FOR ARRIVAL - PROCEED TO CAK NO.2 ANCHORAGE",
|
||
"end_time": "2025-02-02 21:30",
|
||
"duration": {"iso_8601": "PT26H30M", "text": "2h 30m"},
|
||
},
|
||
{
|
||
"date": "2025-02-02",
|
||
"time": "21:30",
|
||
"event": "DROP ANCHOR AT CAK NO.2 ANCHOR / ARR. CAN NOR TENDERED",
|
||
"end_time": "2025-02-02 21:40",
|
||
"duration": {"iso_8601": "-PT23H50M", "text": "10m"},
|
||
},
|
||
{
|
||
"date": "2025-02-02",
|
||
"time": "21:40",
|
||
"event": "BROUGHT UP ANCHOR/F.W.E",
|
||
"end_time": "2025-02-03 07:20",
|
||
"duration": {"iso_8601": "PT9H40M", "text": "9h 40m"},
|
||
},
|
||
{
|
||
"date": "2025-02-03",
|
||
"time": "07:20",
|
||
"event": "S.B.E. FOR SHIFTING",
|
||
"end_time": "2025-02-03 07:45",
|
||
"duration": {"iso_8601": "PT0H25M", "text": "25m"},
|
||
},
|
||
{
|
||
"date": "2025-02-03",
|
||
"time": "07:45",
|
||
"event": "ANCHOR AWEIGHT",
|
||
"end_time": "2025-02-03 10:45",
|
||
"duration": {"iso_8601": "PT3H0M", "text": "3h"},
|
||
},
|
||
{
|
||
"date": "2025-02-03",
|
||
"time": "10:45",
|
||
"event": "SEA PILOT ON BOARD",
|
||
"end_time": "2025-02-03 14:00",
|
||
"duration": {"iso_8601": "PT3H15M", "text": "3h 15m"},
|
||
},
|
||
{
|
||
"date": "2025-02-03",
|
||
"time": "14:00",
|
||
"event": "EXCHANGED SEA PILOT TO RIVER PILOT",
|
||
"end_time": "2025-02-03 17:45",
|
||
"duration": {"iso_8601": "PT3H45M", "text": "3h 45m"},
|
||
},
|
||
{
|
||
"date": "2025-02-03",
|
||
"time": "17:45",
|
||
"event": "DROP ANCHOR AT NANTONS DANGEROUS ANCHORAGE",
|
||
"end_time": "2025-02-03 18:00",
|
||
"duration": {"iso_8601": "PT0H15M", "text": "15m"},
|
||
},
|
||
{
|
||
"date": "2025-02-03",
|
||
"time": "18:00",
|
||
"event": "BROUGHT UP ANCHOR/F.W.E/PILOT OFF",
|
||
"end_time": "2025-02-04 06:30",
|
||
"duration": {"iso_8601": "PT12H30M", "text": "12h 30m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "06:30",
|
||
"event": "S.B.E. FOR BERTHING",
|
||
"end_time": "2025-02-04 07:10",
|
||
"duration": {"iso_8601": "PT0H40M", "text": "40m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "07:10",
|
||
"event": "PILOT ON BOARD",
|
||
"end_time": "2025-02-04 07:15",
|
||
"duration": {"iso_8601": "PT0H5M", "text": "5m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "07:15",
|
||
"event": "ANCHOR AWEIGHT",
|
||
"end_time": "2025-02-04 09:20",
|
||
"duration": {"iso_8601": "PT2H5M", "text": "2h 5m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "09:20",
|
||
"event": "FWD TUG MADE FAST",
|
||
"end_time": "2025-02-04 09:35",
|
||
"duration": {"iso_8601": "PT0H15M", "text": "15m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "09:35",
|
||
"event": "FIRST LINE ASHORE",
|
||
"end_time": "2025-02-04 08:50",
|
||
"duration": {
|
||
"iso_8601": "-PT0H45M",
|
||
"text": "Incohérence temporelle",
|
||
"alert": "Vérifier l'heure de TUG OFF",
|
||
},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "08:50",
|
||
"event": "TUG OFF",
|
||
"end_time": "2025-02-04 09:55",
|
||
"duration": {"iso_8601": "PT1H5M", "text": "1h 5m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "09:55",
|
||
"event": "ALL LINE MADE FAST/F.W.E / NOR ACCEPTED",
|
||
"end_time": "2025-02-04 10:15",
|
||
"duration": {"iso_8601": "PT0H20M", "text": "20m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "10:15",
|
||
"event": "GANGWAY DOWN/PILOT OFF",
|
||
"end_time": "2025-02-04 10:20",
|
||
"duration": {"iso_8601": "PT0H5M", "text": "5m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "10:20",
|
||
"event": "AGENT / BAMBRIATION ON BOARD",
|
||
"end_time": "2025-02-04 11:10",
|
||
"duration": {"iso_8601": "PT0H50M", "text": "50m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "11:10",
|
||
"event": "FREE-PRACTIQUE QUANTED",
|
||
"end_time": "2025-02-04 11:40",
|
||
"duration": {"iso_8601": "PT0H30M", "text": "30m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "11:40",
|
||
"event": "SURVEYOR & LOADING MASTER ON BOARD",
|
||
"end_time": "2025-02-04 11:40",
|
||
"duration": {
|
||
"iso_8601": "PT0H0M",
|
||
"text": "0m",
|
||
"note": "Début de SAFETY MEETING",
|
||
},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "11:40",
|
||
"event": "SAFETY MEETING",
|
||
"end_time": "2025-02-04 12:20",
|
||
"duration": {"iso_8601": "PT0H40M", "text": "40m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "11:45",
|
||
"event": "TANK INSPECTION",
|
||
"end_time": "2025-02-04 12:30",
|
||
"duration": {"iso_8601": "PT0H45M", "text": "45m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "13:10",
|
||
"event": 'CARGO HOSE CONNECTED (3 x 8")',
|
||
"end_time": "2025-02-04 13:25",
|
||
"duration": {"iso_8601": "PT0H15M", "text": "15m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "13:25",
|
||
"event": "LEAKAGE TESTED",
|
||
"end_time": "2025-02-04 13:45",
|
||
"duration": {"iso_8601": "PT0H20M", "text": "20m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "14:15",
|
||
"event": "COMMENCED LOADING SULPHURIC ACID",
|
||
"end_time": "2025-02-05 08:50",
|
||
"duration": {"iso_8601": "PT18H35M", "text": "18h 35m"},
|
||
},
|
||
{
|
||
"date": "2025-02-05",
|
||
"time": "08:50",
|
||
"event": "COMPLETED LOADING SULPHURIC ACID",
|
||
"end_time": "2025-02-05 09:00",
|
||
"duration": {"iso_8601": "PT0H10M", "text": "10m"},
|
||
},
|
||
{
|
||
"date": "2025-02-05",
|
||
"time": "08:50",
|
||
"event": "AIR BLOWING",
|
||
"end_time": "2025-02-05 09:00",
|
||
"duration": {"iso_8601": "PT0H10M", "text": "10m"},
|
||
},
|
||
{
|
||
"date": "2025-02-05",
|
||
"time": "09:00",
|
||
"event": "VILLAGING, SAMPLING AND CALCULATION",
|
||
"end_time": "2025-02-05 11:00",
|
||
"duration": {"iso_8601": "PT2H0M", "text": "2h"},
|
||
},
|
||
{
|
||
"date": "2025-02-05",
|
||
"time": "10:40",
|
||
"event": 'HOSE OFF (308")',
|
||
"end_time": "2025-02-05 11:40",
|
||
"duration": {"iso_8601": "PT1H0M", "text": "1h"},
|
||
},
|
||
{
|
||
"date": "2025-02-05",
|
||
"time": "11:40",
|
||
"event": "CARGO DOCUMENT COMPLETED",
|
||
"end_time": "2025-02-05 11:40",
|
||
"duration": {"iso_8601": "PT0H0M", "text": "Événement final"},
|
||
},
|
||
]
|
||
|
||
class ContainerType(ModelSQL, ModelView):
|
||
"Container Type"
|
||
__name__ = 'stock.container.type'
|
||
|
||
code = fields.Char('Code', required=True)
|
||
name = fields.Char('Description', required=True)
|
||
|
||
teu = fields.Numeric('TEU Factor', digits=(5, 2), required=True)
|
||
|
||
is_reefer = fields.Boolean('Reefer')
|
||
is_special = fields.Boolean('Special Equipment')
|
||
|
||
class ShipmentContainer(ModelSQL, ModelView):
|
||
"Shipment Container"
|
||
__name__ = 'stock.shipment.container'
|
||
|
||
shipment = fields.Many2One(
|
||
'stock.shipment.in', 'Shipment', required=True
|
||
)
|
||
|
||
container_type = fields.Many2One(
|
||
'stock.container.type', 'Container Type', required=True
|
||
)
|
||
|
||
container_no = fields.Char('Container Number')
|
||
quantity = fields.Integer('Quantity', required=True)
|
||
seal_no = fields.Char('Seal Number')
|
||
is_reefer = fields.Boolean('Reefer')
|
||
|
||
class ShipmentWR(ModelSQL,ModelView):
|
||
"Shipment WR"
|
||
__name__ = "shipment.wr"
|
||
shipment_in = fields.Many2One('stock.shipment.in',"Shipment In")
|
||
wr = fields.Many2One('weight.report',"WR")
|
||
|
||
|
||
RATE_BASIS = [
|
||
(None, ''),
|
||
('per_day', 'Per Day'),
|
||
('per_hour', 'Per Hour'),
|
||
('mt_per_hour', 'MT/hour'),
|
||
('per_mt', 'Per MT'),
|
||
('per_wmt', 'Per WMT'),
|
||
('per_dmt', 'Per DMT'),
|
||
('per_cbm', 'Per CBM'),
|
||
('per_lot', 'Per Lot'),
|
||
('lumpsum', 'Lumpsum'),
|
||
('percent', 'Percent'),
|
||
('other', 'Other'),
|
||
]
|
||
|
||
|
||
class CharterRateType(ModelSQL, ModelView):
|
||
"Charter Rate Type"
|
||
__name__ = 'charter.rate.type'
|
||
|
||
name = fields.Char("Name", required=True)
|
||
code = fields.Char("Code")
|
||
category = fields.Selection([
|
||
(None, ''),
|
||
('demurrage', 'Demurrage'),
|
||
('despatch', 'Despatch'),
|
||
('pumping', 'Pumping'),
|
||
('freight', 'Freight'),
|
||
('detention', 'Detention'),
|
||
('waiting', 'Waiting'),
|
||
('shifting', 'Shifting'),
|
||
('heating', 'Heating'),
|
||
('cleaning', 'Cleaning'),
|
||
('bunker', 'Bunker'),
|
||
('port_cost', 'Port Cost'),
|
||
('other', 'Other'),
|
||
], "Category")
|
||
default_basis = fields.Selection(RATE_BASIS, "Default Basis")
|
||
active = fields.Boolean("Active")
|
||
|
||
@staticmethod
|
||
def default_active():
|
||
return True
|
||
|
||
|
||
class CharterConditionRate(ModelSQL, ModelView):
|
||
"Charter Condition Rate"
|
||
__name__ = 'charter.condition.rate'
|
||
|
||
condition = fields.Many2One(
|
||
'charter.condition', "Condition", required=True, ondelete='CASCADE')
|
||
rate_type = fields.Many2One('charter.rate.type', "Type")
|
||
description = fields.Char("Description")
|
||
rate = fields.Numeric("Rate", digits=(16, 6))
|
||
currency = fields.Many2One('currency.currency', "Curr.")
|
||
basis = fields.Selection(RATE_BASIS, "Basis")
|
||
applies_to = fields.Selection([
|
||
(None, ''),
|
||
('load', 'Load'),
|
||
('discharge', 'Discharge'),
|
||
('both', 'Both'),
|
||
('waiting', 'Waiting'),
|
||
('pumping', 'Pumping'),
|
||
('shifting', 'Shifting'),
|
||
('voyage', 'Voyage'),
|
||
('other', 'Other'),
|
||
], "Applies To")
|
||
minimum = fields.Numeric("Min", digits=(16, 6))
|
||
maximum = fields.Numeric("Max", digits=(16, 6))
|
||
reference = fields.Text("Reference")
|
||
|
||
@fields.depends('rate_type')
|
||
def on_change_rate_type(self):
|
||
if self.rate_type and self.rate_type.default_basis:
|
||
self.basis = self.rate_type.default_basis
|
||
|
||
|
||
class CharterConditionLaytimeStartRule(ModelSQL, ModelView):
|
||
"Charter Condition Laytime Start Rule"
|
||
__name__ = 'charter.condition.laytime.start.rule'
|
||
|
||
condition = fields.Many2One(
|
||
'charter.condition', "Condition", required=True, ondelete='CASCADE')
|
||
sequence = fields.Integer("Sequence")
|
||
event = fields.Selection(
|
||
CHARTER_LAYTIME_EVENT_SELECTION, "Event", required=True)
|
||
offset = fields.Numeric("Offset", digits=(16, 4))
|
||
offset_unit = fields.Selection([
|
||
(None, ''),
|
||
('hours', 'Hours'),
|
||
('days', 'Days'),
|
||
], "Offset Unit")
|
||
|
||
@staticmethod
|
||
def default_sequence():
|
||
return 10
|
||
|
||
@staticmethod
|
||
def default_offset_unit():
|
||
return 'hours'
|
||
|
||
|
||
class CharterCondition(ModelSQL, ModelView):
|
||
"Charter Condition"
|
||
__name__ = 'charter.condition'
|
||
|
||
_party_role_selection = [
|
||
(None, ''),
|
||
('owner', 'Owner'),
|
||
('charterer', 'Charterer'),
|
||
('supplier', 'Supplier'),
|
||
('customer', 'Customer'),
|
||
('broker', 'Broker'),
|
||
('agent', 'Agent'),
|
||
('terminal', 'Terminal'),
|
||
('other', 'Other'),
|
||
]
|
||
|
||
name = fields.Char("Name")
|
||
charter_party = fields.Many2One(
|
||
'stock.charter.party', "Charter Party", ondelete='CASCADE')
|
||
purchase = fields.Many2One(
|
||
'purchase.purchase', "Purchase", ondelete='CASCADE')
|
||
purchase_line = fields.Many2One(
|
||
'purchase.line', "Purchase Line", ondelete='CASCADE')
|
||
party = fields.Many2One('party.party', "Party")
|
||
party_role = fields.Selection(
|
||
_party_role_selection, "Party Role")
|
||
responsibility = fields.Selection([
|
||
(None, ''),
|
||
('ours', 'Ours'),
|
||
('counterparty', 'Counterparty'),
|
||
('shared', 'Shared'),
|
||
('pass_through', 'Pass-through'),
|
||
('owner', 'Owner'),
|
||
('charterer', 'Charterer'),
|
||
], "Responsibility")
|
||
laytime_clause = fields.Text("Laytime Clause")
|
||
laytime_allowed = fields.Numeric("Laytime Allowed", digits=(16, 4))
|
||
laytime_unit = fields.Selection([
|
||
(None, ''),
|
||
('hours', 'Hours'),
|
||
('days', 'Days'),
|
||
('wwd', 'Weather Working Days'),
|
||
('wwd_shex', 'WWD SHEX'),
|
||
('wwd_shinc', 'WWD SHINC'),
|
||
('running_hours', 'Running Hours'),
|
||
], "Laytime Unit")
|
||
laytime_start = fields.Text("Laytime Start")
|
||
laytime_end = fields.Text("Laytime End")
|
||
nor_clause = fields.Text("NOR Clause")
|
||
laytime_start_rule = fields.Selection([
|
||
('simple', 'Single Event + Offset'),
|
||
('earliest_of', 'Earliest Of'),
|
||
('latest_of', 'Latest Of'),
|
||
], "Laytime Start Rule")
|
||
laytime_start_event = fields.Selection(
|
||
CHARTER_LAYTIME_EVENT_SELECTION, "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_start_rules = fields.One2Many(
|
||
'charter.condition.laytime.start.rule', 'condition',
|
||
"Laytime Start Candidates")
|
||
laytime_end_event = fields.Selection(
|
||
CHARTER_LAYTIME_EVENT_SELECTION, "Laytime End Event")
|
||
turn_time = fields.Numeric("Turn Time", digits=(16, 4))
|
||
turn_time_unit = fields.Selection([
|
||
(None, ''),
|
||
('hours', 'Hours'),
|
||
('days', 'Days'),
|
||
], "Turn Time Unit")
|
||
reversible = fields.Boolean("Reversible Laytime")
|
||
all_time_saved = fields.Boolean("All Time Saved")
|
||
demurrage_clause = fields.Text("Demurrage Clause")
|
||
despatch_clause = fields.Text("Despatch Clause")
|
||
exceptions = fields.Text("Exceptions")
|
||
remarks = fields.Text("Remarks")
|
||
rates = fields.One2Many(
|
||
'charter.condition.rate', 'condition', "Rates")
|
||
|
||
@staticmethod
|
||
def default_reversible():
|
||
return False
|
||
|
||
@staticmethod
|
||
def default_all_time_saved():
|
||
return False
|
||
|
||
@staticmethod
|
||
def default_laytime_start_rule():
|
||
return 'simple'
|
||
|
||
@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')
|
||
if role_context in {'purchase', 'purchase_line', 'supplier'}:
|
||
return 'supplier'
|
||
if role_context in {'sale', 'sale_line', 'customer'}:
|
||
return 'customer'
|
||
if role_context == 'charter_party':
|
||
return 'owner'
|
||
|
||
@fields.depends(
|
||
'charter_party', 'purchase', 'purchase_line', 'sale', 'sale_line')
|
||
def get_party_roles(self):
|
||
role_context = Transaction().context.get('charter_condition_role')
|
||
if (role_context in {'purchase', 'purchase_line', 'supplier'}
|
||
or getattr(self, 'purchase', None)
|
||
or getattr(self, 'purchase_line', None)):
|
||
return [(None, ''), ('supplier', 'Supplier')]
|
||
if (role_context in {'sale', 'sale_line', 'customer'}
|
||
or getattr(self, 'sale', None)
|
||
or getattr(self, 'sale_line', None)):
|
||
return [(None, ''), ('customer', 'Customer')]
|
||
if role_context == 'charter_party' or getattr(
|
||
self, 'charter_party', None):
|
||
return [
|
||
item for item in self._party_role_selection
|
||
if item[0] not in {'supplier', 'customer'}]
|
||
return self._party_role_selection
|
||
|
||
@classmethod
|
||
def validate(cls, conditions):
|
||
super().validate(conditions)
|
||
for condition in conditions:
|
||
allowed_roles = {role for role, _ in condition.get_party_roles()}
|
||
if condition.party_role not in allowed_roles:
|
||
raise UserError(
|
||
"This charter condition party role is not allowed "
|
||
"for its source.")
|
||
|
||
|
||
class CharterParty(ModelSQL, ModelView):
|
||
"Charter Party"
|
||
__name__ = 'stock.charter.party'
|
||
|
||
name = fields.Char("Name", required=True)
|
||
reference = fields.Char("Reference")
|
||
active = fields.Boolean("Active")
|
||
company = fields.Many2One('company.company', "Company")
|
||
charter_type = fields.Selection([
|
||
(None, ''),
|
||
('voyage', 'Voyage Charter'),
|
||
('time', 'Time Charter'),
|
||
('coa', 'Contract of Affreightment'),
|
||
('bareboat', 'Bareboat Charter'),
|
||
], "Charter Type", required=True)
|
||
owner = fields.Many2One('party.party', "Owner")
|
||
charterer = fields.Many2One('party.party', "Charterer")
|
||
broker = fields.Many2One('party.party', "Broker")
|
||
vessel = fields.Many2One('trade.vessel', "Vessel")
|
||
product = fields.Many2One('product.product', "Product")
|
||
load_port = fields.Many2One('stock.location', "Load Port")
|
||
discharge_port = fields.Many2One('stock.location', "Discharge Port")
|
||
from_location = fields.Many2One('stock.location', "From Location")
|
||
to_location = fields.Many2One('stock.location', "To Location")
|
||
laycan_from = fields.Date("Laycan From")
|
||
laycan_to = fields.Date("Laycan To")
|
||
contract_date = fields.Date("Contract Date")
|
||
currency = fields.Many2One('currency.currency', "Currency")
|
||
governing_law = fields.Char("Governing Law")
|
||
jurisdiction = fields.Char("Jurisdiction")
|
||
document = fields.Many2One('document.incoming', "Document")
|
||
notes = fields.Text("Notes")
|
||
conditions = fields.One2Many(
|
||
'charter.condition', 'charter_party', "Conditions")
|
||
|
||
@staticmethod
|
||
def default_active():
|
||
return True
|
||
|
||
|
||
class ShipmentIn(metaclass=PoolMeta):
|
||
__name__ = 'stock.shipment.in'
|
||
|
||
from_location = fields.Many2One(
|
||
'stock.location', 'From location',
|
||
domain=[('type', '!=', 'customer')])
|
||
to_location = fields.Many2One(
|
||
'stock.location', 'To location',
|
||
domain=[('type', '!=', 'supplier')])
|
||
shipment_type = fields.Function(fields.Selection([
|
||
(None, ''),
|
||
('dropship', 'Dropship'),
|
||
('inbound', 'Inbound'),
|
||
], "Shipment Type"), 'get_shipment_type')
|
||
transport_type = fields.Selection([
|
||
('vessel', 'Vessel'),
|
||
('truck', 'Truck'),
|
||
('other', 'Other'),
|
||
], 'Transport type')
|
||
vessel = fields.Many2One('trade.vessel', "Vessel",
|
||
states={'invisible': Eval('transport_type') == 'truck'})
|
||
info = fields.Function(fields.Text("Info",states={'invisible': ~Eval('info',False)}),'get_info')
|
||
anim = fields.Function(fields.Text(""),'get_anim')
|
||
carte = fields.Function(fields.Text("",states={'invisible': ~Eval('info',False)}),'get_imo')
|
||
fees = fields.One2Many('fee.fee','shipment_in',"Fees")
|
||
pnl_lines = fields.Function(
|
||
fields.One2Many('valuation.valuation.line', '', "Pnl"),
|
||
'get_pnl_lines')
|
||
lotqt = fields.One2Many('lot.qt','lot_shipment_in',"Lots")
|
||
quantity = fields.Function(fields.Numeric("Quantity"),'get_quantity')
|
||
unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit')
|
||
bl_date = fields.Date("BL date")
|
||
bl_number = fields.Char("BL number")
|
||
etl = fields.Date("Est. Loading")
|
||
eta = fields.Date("🏗️ETA POL")
|
||
etad = fields.Date("⚓ETA")
|
||
atad = fields.Date("Act. Destination")
|
||
etd = fields.Date("ETD")
|
||
unloaded = fields.Date("Act. Discharge")
|
||
booking = fields.Char("Booking Nb")
|
||
booking_date =fields.Date("Booking date")
|
||
ref = fields.Char("Our reference")
|
||
note = fields.Text("Notes")
|
||
dashboard = fields.Many2One('purchase.dashboard',"Dashboard")
|
||
himself = fields.Function(fields.Many2One('stock.shipment.in',"Shipment"),'get_sh')
|
||
charter_party = fields.Many2One('stock.charter.party', "Charter Party")
|
||
owner_charter_conditions = fields.Function(
|
||
fields.One2Many('charter.condition', '', "Owner Conditions"),
|
||
'get_owner_charter_conditions')
|
||
purchase_charter_conditions = fields.Function(
|
||
fields.One2Many('charter.condition', '', "Supplier Conditions"),
|
||
'get_purchase_charter_conditions')
|
||
sale_charter_conditions = fields.Function(
|
||
fields.One2Many('charter.condition', '', "Customer Conditions"),
|
||
'get_sale_charter_conditions')
|
||
sof = fields.One2Many('sof.statement', 'shipment',"Demurrage calculations")
|
||
del_from = fields.Date("Shipment period from")
|
||
del_to = fields.Date("to")
|
||
estimated_date = fields.One2Many('pricing.estimated','shipment_in',"Estimated date")
|
||
carrier_ = fields.Many2One('party.party',"Carrier")
|
||
operator = fields.Many2One(
|
||
'party.party', "Operator",
|
||
domain=[('categories.name', '=', 'OPERATOR')])
|
||
start_date = fields.Date("Start date")
|
||
travel_nb = fields.Char("Travel nb")
|
||
receive_date = fields.Date("Reception date")
|
||
receive_nb = fields.Char("Reception nb")
|
||
cargo_mode = fields.Selection([
|
||
('none', ''),
|
||
('bulk', 'Bulk'),
|
||
('container', 'Container'),
|
||
('tanker', 'Tanker'),
|
||
], 'Cargo Mode', required=True,
|
||
states={'invisible': Eval('transport_type') == 'truck'})
|
||
|
||
vessel_type = fields.Function(
|
||
fields.Many2One('stock.vessel.type', "Vessel type",
|
||
states={'invisible': Eval('transport_type') == 'truck'}),
|
||
'get_vessel_type')
|
||
container = fields.One2Many(
|
||
'stock.shipment.container',
|
||
'shipment',
|
||
'Container'
|
||
)
|
||
shipment_wr = fields.One2Many('shipment.wr','shipment_in',"WR")
|
||
controller = fields.Many2One('party.party',"Controller")
|
||
surveyor = fields.Many2One('party.party', "Surveyor")
|
||
controller_target = fields.Char("Targeted controller")
|
||
send_instruction = fields.Boolean("Send instruction")
|
||
instructions = fields.Text("Instructions")
|
||
add_bl = fields.Boolean("Add BL")
|
||
add_invoice = fields.Boolean("Add invoice")
|
||
returned_id = fields.Char("Returned ID")
|
||
result = fields.Char("Result",readonly=True)
|
||
agent = fields.Many2One('party.party',"Booking Agent")
|
||
service_order_key = fields.Integer("Service Order Key")
|
||
|
||
@classmethod
|
||
def __setup__(cls):
|
||
super().__setup__()
|
||
cls._transitions |= set((
|
||
('started', 'cancelled'),
|
||
))
|
||
cls._buttons.update({
|
||
'compute': {},
|
||
'send': {},
|
||
})
|
||
|
||
@staticmethod
|
||
def _normalize_transport_values(values, record=None):
|
||
values = dict(values)
|
||
transport_type = values.get(
|
||
'transport_type', getattr(record, 'transport_type', None))
|
||
if transport_type == 'truck':
|
||
values['vessel'] = None
|
||
values['cargo_mode'] = 'none'
|
||
elif values.get('cargo_mode') == 'none':
|
||
values['cargo_mode'] = 'bulk'
|
||
return values
|
||
|
||
@classmethod
|
||
def create(cls, vlist):
|
||
return super().create([
|
||
cls._normalize_transport_values(values) for values in vlist])
|
||
|
||
@classmethod
|
||
def write(cls, *args):
|
||
new_args = cls._normalize_transport_write_args(args)
|
||
if not new_args:
|
||
return
|
||
return super().write(*new_args)
|
||
|
||
@classmethod
|
||
def _normalize_transport_write_args(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._normalize_transport_values(values, record)])
|
||
return new_args
|
||
|
||
@classmethod
|
||
def _stock_move_origin(cls, move):
|
||
return 'stock.move,%s' % move.id
|
||
|
||
@classmethod
|
||
def _delete_draft_account_moves_for_stock_moves(cls, moves):
|
||
moves = [move for move in moves if getattr(move, 'id', None)]
|
||
if not moves:
|
||
return
|
||
|
||
AccountMove = Pool().get('account.move')
|
||
account_moves = AccountMove.search([
|
||
('origin', 'in', [cls._stock_move_origin(move) for move in moves]),
|
||
])
|
||
posted = [
|
||
move for move in account_moves
|
||
if getattr(move, 'state', None) == 'posted']
|
||
if posted:
|
||
names = ', '.join(move.rec_name for move in posted[:5])
|
||
if len(posted) > 5:
|
||
names += '...'
|
||
raise UserError(
|
||
"Cannot cancel started shipment because stock accounting "
|
||
"move(s) are already posted: %s" % names)
|
||
|
||
draft_moves = [
|
||
move for move in account_moves
|
||
if getattr(move, 'state', None) != 'posted']
|
||
if draft_moves:
|
||
AccountMove.delete(draft_moves)
|
||
|
||
@classmethod
|
||
@ModelView.button
|
||
@Workflow.transition('cancelled')
|
||
def cancel(cls, shipments):
|
||
moves = [
|
||
move for shipment in shipments
|
||
for move in shipment.incoming_moves + shipment.inventory_moves]
|
||
cls._delete_draft_account_moves_for_stock_moves(moves)
|
||
super().cancel(shipments)
|
||
started_shipments = [
|
||
shipment for shipment in shipments
|
||
if shipment.state == 'started']
|
||
if started_shipments:
|
||
cls.write(started_shipments, {
|
||
'start_date': None,
|
||
'started_by': None,
|
||
})
|
||
|
||
def get_vessel_type(self,name=None):
|
||
if self.vessel:
|
||
return self.vessel.vessel_type
|
||
|
||
@fields.depends('from_location', 'to_location')
|
||
def get_shipment_type(self, name=None):
|
||
if (self.from_location and self.to_location
|
||
and self.from_location.type == 'supplier'
|
||
and self.to_location.type == 'customer'):
|
||
return 'dropship'
|
||
return 'inbound'
|
||
|
||
def _get_report_primary_move(self):
|
||
moves = list(self.incoming_moves or self.moves or [])
|
||
return moves[0] if moves else None
|
||
|
||
def _get_report_primary_lot(self):
|
||
move = self._get_report_primary_move()
|
||
return getattr(move, 'lot', None) if move else None
|
||
|
||
def _get_report_trade_line(self):
|
||
lot = self._get_report_primary_lot()
|
||
if not lot:
|
||
return None
|
||
return getattr(lot, 'sale_line', None) or getattr(lot, 'line', None)
|
||
|
||
def _get_report_insurance_fee(self):
|
||
for fee in self.fees or []:
|
||
product = getattr(fee, 'product', None)
|
||
name = ((getattr(product, 'name', '') or '')).strip().lower()
|
||
if 'insurance' in name:
|
||
return fee
|
||
return None
|
||
|
||
def _get_report_incoming_amount_data(self):
|
||
total = Decimal('0.0')
|
||
currency = None
|
||
for move in (self.incoming_moves or []):
|
||
move_amount, move_currency = self._get_report_incoming_move_amount(
|
||
move)
|
||
total += move_amount
|
||
if not currency and move_currency:
|
||
currency = move_currency
|
||
return total, currency
|
||
|
||
def _get_report_incoming_move_amount(self, move):
|
||
quantity = Decimal(str(getattr(move, 'quantity', 0) or 0))
|
||
unit_price = getattr(move, 'unit_price', None)
|
||
if unit_price not in (None, ''):
|
||
move_currency = getattr(move, 'currency', None)
|
||
return quantity * Decimal(str(unit_price or 0)), move_currency
|
||
|
||
lot = getattr(move, 'lot', None)
|
||
line = getattr(lot, 'line', None) if lot else None
|
||
if not lot or not line:
|
||
return Decimal('0.0'), None
|
||
|
||
lot_quantity = Decimal(str(
|
||
lot.get_current_quantity_converted() or 0))
|
||
line_unit_price = Decimal(str(getattr(line, 'unit_price', 0) or 0))
|
||
trade = getattr(line, 'purchase', None)
|
||
line_currency = getattr(trade, 'currency', None) if trade else None
|
||
return lot_quantity * line_unit_price, line_currency
|
||
|
||
@staticmethod
|
||
def _get_report_currency_text(currency):
|
||
return (
|
||
getattr(currency, 'rec_name', None)
|
||
or getattr(currency, 'code', None)
|
||
or getattr(currency, 'symbol', None)
|
||
or '')
|
||
|
||
@staticmethod
|
||
def _format_report_amount(value):
|
||
if value in (None, ''):
|
||
return ''
|
||
value = Decimal(str(value or 0)).quantize(Decimal('0.01'))
|
||
return format(value, 'f')
|
||
|
||
@staticmethod
|
||
def _format_report_quantity(value, digits='0.001'):
|
||
if value in (None, ''):
|
||
return ''
|
||
quantity = Decimal(str(value or 0)).quantize(Decimal(digits))
|
||
text = format(quantity, 'f')
|
||
return text.rstrip('0').rstrip('.') or '0'
|
||
|
||
def _get_report_trade(self):
|
||
line = self._get_report_trade_line()
|
||
if not line:
|
||
return None
|
||
return getattr(line, 'sale', None) or getattr(line, 'purchase', None)
|
||
|
||
@staticmethod
|
||
def _get_report_sale_from_lot(lot):
|
||
sale_line = getattr(lot, 'sale_line', None) if lot else None
|
||
sale = getattr(sale_line, 'sale', None) if sale_line else None
|
||
return sale
|
||
|
||
def _get_report_sales(self):
|
||
sales = []
|
||
seen = set()
|
||
|
||
def add_sale(sale):
|
||
if not sale:
|
||
return
|
||
sale_id = getattr(sale, 'id', None)
|
||
key = sale_id if sale_id is not None else id(sale)
|
||
if key in seen:
|
||
return
|
||
seen.add(key)
|
||
sales.append(sale)
|
||
|
||
for move in (self.incoming_moves or self.moves or []):
|
||
add_sale(self._get_report_sale_from_lot(
|
||
getattr(move, 'lot', None)))
|
||
for lot_qt in getattr(self, 'lotqt', []) or []:
|
||
add_sale(self._get_report_sale_from_lot(
|
||
getattr(lot_qt, 'lot_s', None)))
|
||
return sales
|
||
|
||
def _get_report_physical_lots(self):
|
||
lots = []
|
||
seen = set()
|
||
for move in (self.incoming_moves or []):
|
||
lot = getattr(move, 'lot', None)
|
||
if not lot or getattr(lot, 'lot_type', None) != 'physic':
|
||
continue
|
||
lot_id = getattr(lot, 'id', None)
|
||
key = lot_id if lot_id is not None else id(lot)
|
||
if key in seen:
|
||
continue
|
||
seen.add(key)
|
||
lots.append(lot)
|
||
return lots
|
||
|
||
def _get_report_invoice_lines_from_physical_lots(self):
|
||
lines = []
|
||
seen = set()
|
||
for lot in self._get_report_physical_lots():
|
||
for name in (
|
||
'sale_invoice_line',
|
||
'sale_invoice_line_prov',
|
||
'invoice_line',
|
||
'invoice_line_prov'):
|
||
line = getattr(lot, name, None)
|
||
if not line:
|
||
continue
|
||
line_id = getattr(line, 'id', None)
|
||
key = line_id if line_id is not None else id(line)
|
||
if key in seen:
|
||
continue
|
||
seen.add(key)
|
||
lines.append(line)
|
||
break
|
||
return lines
|
||
|
||
@staticmethod
|
||
def _get_report_invoice_line_snapshot_quantity(line):
|
||
invoice = getattr(line, 'invoice', None)
|
||
weights = getattr(invoice, '_get_report_invoice_line_weights', None)
|
||
if callable(weights):
|
||
quantity, _ = weights(line)
|
||
unit_getter = getattr(invoice, '_get_report_invoice_line_unit', None)
|
||
unit = unit_getter(line) if callable(unit_getter) else None
|
||
return Decimal(str(quantity or 0)), unit
|
||
|
||
snapshots = list(getattr(line, 'lot_weight_snapshots', []) or [])
|
||
if snapshots:
|
||
quantity = sum(
|
||
Decimal(str(getattr(snapshot, 'net_quantity', 0) or 0))
|
||
for snapshot in snapshots)
|
||
unit = getattr(snapshots[0], 'unit', None)
|
||
return quantity, unit
|
||
|
||
report_net = getattr(line, 'report_net', '')
|
||
try:
|
||
quantity = Decimal(str(report_net or 0))
|
||
except Exception:
|
||
quantity = Decimal(0)
|
||
if not quantity:
|
||
quantity = Decimal(str(getattr(line, 'quantity', 0) or 0))
|
||
return quantity, getattr(line, 'unit', None)
|
||
|
||
@staticmethod
|
||
def _get_report_unit_text(unit):
|
||
return (
|
||
getattr(unit, 'symbol', None)
|
||
or getattr(unit, 'rec_name', None)
|
||
or getattr(unit, 'name', None)
|
||
or '')
|
||
|
||
def _get_report_insurance_invoice_quantity(self):
|
||
total = Decimal(0)
|
||
unit = None
|
||
matched = False
|
||
for line in self._get_report_invoice_lines_from_physical_lots():
|
||
quantity, line_unit = self._get_report_invoice_line_snapshot_quantity(
|
||
line)
|
||
total += quantity
|
||
if not unit and line_unit:
|
||
unit = line_unit
|
||
matched = True
|
||
if not matched:
|
||
return None, None
|
||
return total, unit
|
||
|
||
def _get_report_weight_totals(self):
|
||
net = Decimal('0')
|
||
gross = Decimal('0')
|
||
for move in (self.incoming_moves or self.moves or []):
|
||
lot = getattr(move, 'lot', None)
|
||
if lot:
|
||
lot_net = (
|
||
lot.get_current_quantity()
|
||
if hasattr(lot, 'get_current_quantity')
|
||
else lot.get_current_quantity_converted()
|
||
if hasattr(lot, 'get_current_quantity_converted')
|
||
else getattr(move, 'quantity', 0)
|
||
)
|
||
lot_gross = (
|
||
lot.get_current_gross_quantity()
|
||
if hasattr(lot, 'get_current_gross_quantity')
|
||
else lot_net
|
||
)
|
||
net += Decimal(str(lot_net or 0))
|
||
gross += Decimal(str(lot_gross or 0))
|
||
else:
|
||
quantity = Decimal(str(getattr(move, 'quantity', 0) or 0))
|
||
net += quantity
|
||
gross += quantity
|
||
return net, gross
|
||
|
||
@property
|
||
def report_product_name(self):
|
||
line = self._get_report_trade_line()
|
||
product = getattr(line, 'product', None) if line else None
|
||
if product:
|
||
return product.name or ''
|
||
move = self._get_report_primary_move()
|
||
product = getattr(move, 'product', None) if move else None
|
||
return getattr(product, 'name', '') or ''
|
||
|
||
@property
|
||
def report_product_description(self):
|
||
line = self._get_report_trade_line()
|
||
product = getattr(line, 'product', None) if line else None
|
||
if product:
|
||
return product.description or ''
|
||
move = self._get_report_primary_move()
|
||
product = getattr(move, 'product', None) if move else None
|
||
return getattr(product, 'description', '') or ''
|
||
|
||
@property
|
||
def report_insurance_footer_ref(self):
|
||
return self.bl_number or self.number or ''
|
||
|
||
@property
|
||
def report_insurance_certificate_number(self):
|
||
return self.bl_number or self.number or ''
|
||
|
||
@property
|
||
def report_insurance_order_reference(self):
|
||
sales = self._get_report_sales()
|
||
if len(sales) != 1:
|
||
return ''
|
||
sale = sales[0]
|
||
return (
|
||
getattr(sale, 'report_melya_proforma_number', None)
|
||
or getattr(sale, 'report_deal', None)
|
||
or '')
|
||
|
||
@property
|
||
def report_insurance_account_of(self):
|
||
company = getattr(self, 'company', None)
|
||
party = getattr(company, 'party', None) if company else None
|
||
if not party:
|
||
return ''
|
||
address_get = getattr(party, 'address_get', None)
|
||
address = address_get() if callable(address_get) else None
|
||
return (
|
||
getattr(address, 'party_name', None)
|
||
or getattr(address, 'party_full_name', None)
|
||
or getattr(party, 'rec_name', None)
|
||
or '')
|
||
|
||
@property
|
||
def report_insurance_goods_description(self):
|
||
name = self.report_product_name
|
||
description = self.report_product_description
|
||
product = (
|
||
' - '.join(part for part in [name, description] if part)
|
||
if description and description != name else name or description)
|
||
quantity, unit = self._get_report_insurance_invoice_quantity()
|
||
if quantity is None:
|
||
return product
|
||
quantity_text = self._format_report_quantity(quantity, '0.0001')
|
||
unit_text = self._get_report_unit_text(unit)
|
||
quantity_label = ''.join(
|
||
part for part in [quantity_text, unit_text] if part)
|
||
return ' '.join(part for part in [quantity_label, product] if part)
|
||
|
||
@property
|
||
def report_insurance_invoice_number(self):
|
||
numbers = []
|
||
seen = set()
|
||
for line in self._get_report_invoice_lines_from_physical_lots():
|
||
invoice = getattr(line, 'invoice', None)
|
||
number = (
|
||
getattr(invoice, 'number', None)
|
||
or getattr(invoice, 'rec_name', None)
|
||
or '')
|
||
if not number or number in seen:
|
||
continue
|
||
seen.add(number)
|
||
numbers.append(number)
|
||
return ' '.join(numbers)
|
||
|
||
@property
|
||
def report_insurance_loading_port(self):
|
||
return getattr(self.from_location, 'name', '') or ''
|
||
|
||
@property
|
||
def report_insurance_discharge_port(self):
|
||
return getattr(self.to_location, 'name', '') or ''
|
||
|
||
@property
|
||
def report_insurance_transport(self):
|
||
if self.vessel and self.vessel.vessel_name:
|
||
return self.vessel.vessel_name
|
||
return self.transport_type or ''
|
||
|
||
@property
|
||
def report_insurance_amount(self):
|
||
insured_amount, insured_currency = self._get_report_incoming_amount_data()
|
||
if insured_amount:
|
||
insured_amount *= Decimal('1.10')
|
||
currency_text = self._get_report_currency_text(insured_currency)
|
||
amount_text = self._format_report_amount(insured_amount)
|
||
return ' '.join(part for part in [currency_text, amount_text] if part)
|
||
|
||
fee = self._get_report_insurance_fee()
|
||
if not fee:
|
||
return ''
|
||
currency = getattr(fee, 'currency', None)
|
||
currency_text = self._get_report_currency_text(currency)
|
||
amount = self._format_report_amount(fee.get_amount())
|
||
return ' '.join(part for part in [currency_text, amount] if part)
|
||
|
||
@property
|
||
def report_insurance_incoming_amount(self):
|
||
amount, currency = self._get_report_incoming_amount_data()
|
||
currency_text = self._get_report_currency_text(currency)
|
||
amount_text = self._format_report_amount(amount)
|
||
return ' '.join(part for part in [currency_text, amount_text] if part)
|
||
|
||
@property
|
||
def report_insurance_amount_insured(self):
|
||
amount, currency = self._get_report_incoming_amount_data()
|
||
insured_amount = amount * Decimal('1.10')
|
||
currency_text = self._get_report_currency_text(currency)
|
||
amount_text = self._format_report_amount(insured_amount)
|
||
return ' '.join(part for part in [currency_text, amount_text] if part)
|
||
|
||
@property
|
||
def report_insurance_surveyor(self):
|
||
if self.surveyor:
|
||
return self.surveyor.rec_name or ''
|
||
if self.controller:
|
||
return self.controller.rec_name or ''
|
||
fee = self._get_report_insurance_fee()
|
||
supplier = getattr(fee, 'supplier', None) if fee else None
|
||
return getattr(supplier, 'rec_name', '') or ''
|
||
|
||
@property
|
||
def report_insurance_contact_surveyor(self):
|
||
return self.report_insurance_surveyor
|
||
|
||
@property
|
||
def report_insurance_issue_place_and_date(self):
|
||
Date = Pool().get('ir.date')
|
||
address = None
|
||
if self.company and getattr(self.company, 'party', None):
|
||
address = self.company.party.address_get()
|
||
place = (
|
||
getattr(address, 'city', None)
|
||
or getattr(self.company.party, 'rec_name', None)
|
||
if self.company and getattr(self.company, 'party', None) else ''
|
||
) or ''
|
||
today = Date.today()
|
||
date_text = today.strftime('%d-%m-%Y') if today else ''
|
||
return ', '.join(part for part in [place, date_text] if part)
|
||
|
||
@property
|
||
def report_packing_product_class(self):
|
||
return self.report_product_name
|
||
|
||
@property
|
||
def report_packing_contract_number(self):
|
||
trade = self._get_report_trade()
|
||
return (
|
||
getattr(trade, 'reference', None)
|
||
or getattr(trade, 'number', None)
|
||
or self.reference
|
||
or self.number
|
||
or '')
|
||
|
||
@property
|
||
def report_packing_invoice_qty(self):
|
||
quantity = self.quantity if self.quantity not in (None, '') else 0
|
||
return self._format_report_quantity(quantity)
|
||
|
||
@property
|
||
def report_packing_invoice_qty_unit(self):
|
||
unit = self.unit
|
||
return (
|
||
getattr(unit, 'symbol', None)
|
||
or getattr(unit, 'rec_name', None)
|
||
or '')
|
||
|
||
@property
|
||
def report_packing_origin(self):
|
||
trade = self._get_report_trade()
|
||
return (
|
||
getattr(trade, 'product_origin', None)
|
||
or getattr(self.from_location, 'name', None)
|
||
or '')
|
||
|
||
@property
|
||
def report_packing_product(self):
|
||
return self.report_product_name
|
||
|
||
@property
|
||
def report_packing_counterparty_name(self):
|
||
trade = self._get_report_trade()
|
||
party = getattr(trade, 'party', None) if trade else None
|
||
if party:
|
||
return party.rec_name or ''
|
||
return getattr(self.supplier, 'rec_name', '') or ''
|
||
|
||
@property
|
||
def report_packing_ship_name(self):
|
||
if self.vessel and self.vessel.vessel_name:
|
||
return self.vessel.vessel_name
|
||
return self.transport_type or ''
|
||
|
||
@property
|
||
def report_packing_loading_port(self):
|
||
return getattr(self.from_location, 'name', '') or ''
|
||
|
||
@property
|
||
def report_packing_destination_port(self):
|
||
return getattr(self.to_location, 'name', '') or ''
|
||
|
||
@property
|
||
def report_packing_chunk_number(self):
|
||
return self.bl_number or self.number or ''
|
||
|
||
@property
|
||
def report_packing_chunk_date(self):
|
||
if self.bl_date:
|
||
return self.bl_date.strftime('%d-%m-%Y')
|
||
return ''
|
||
|
||
@property
|
||
def report_packing_today_date(self):
|
||
Date = Pool().get('ir.date')
|
||
today = Date.today()
|
||
if not today:
|
||
return ''
|
||
return f"{today.strftime('%B')} {today.day}, {today.year}"
|
||
|
||
@property
|
||
def report_packing_weight_unit(self):
|
||
line = self._get_report_trade_line()
|
||
unit = getattr(line, 'unit', None) if line else None
|
||
if unit:
|
||
return (
|
||
getattr(unit, 'symbol', None)
|
||
or getattr(unit, 'rec_name', None)
|
||
or '')
|
||
return self.report_packing_invoice_qty_unit or 'KGS'
|
||
|
||
@property
|
||
def report_packing_gross_weight(self):
|
||
_, gross = self._get_report_weight_totals()
|
||
return self._format_report_quantity(gross)
|
||
|
||
@property
|
||
def report_packing_net_weight(self):
|
||
net, _ = self._get_report_weight_totals()
|
||
return self._format_report_quantity(net)
|
||
|
||
@property
|
||
def report_coo_exporter(self):
|
||
company = getattr(self, 'company', None)
|
||
party = getattr(company, 'party', None) if company else None
|
||
if not party:
|
||
return ''
|
||
address = party.address_get() if hasattr(party, 'address_get') else None
|
||
lines = [getattr(party, 'rec_name', None) or getattr(party, 'name', None) or '']
|
||
if address and getattr(address, 'full_address', None):
|
||
lines.append(address.full_address)
|
||
return '\n'.join(filter(None, lines))
|
||
|
||
@property
|
||
def report_coo_consignee(self):
|
||
trade = self._get_report_trade()
|
||
party = getattr(trade, 'party', None) if trade else None
|
||
if not party:
|
||
return ''
|
||
address = party.address_get() if hasattr(party, 'address_get') else None
|
||
lines = [getattr(party, 'rec_name', None) or getattr(party, 'name', None) or '']
|
||
if address and getattr(address, 'full_address', None):
|
||
lines.append(address.full_address)
|
||
return '\n'.join(filter(None, lines))
|
||
|
||
@property
|
||
def report_coo_number(self):
|
||
return getattr(self, 'reference', None) or self.number or ''
|
||
|
||
@property
|
||
def report_coo_transport(self):
|
||
parts = []
|
||
if self.bl_number:
|
||
parts.append(f"B/L {self.bl_number}")
|
||
ship_name = self.report_packing_ship_name
|
||
if ship_name:
|
||
parts.append(ship_name)
|
||
if self.booking:
|
||
parts.append(f"Booking {self.booking}")
|
||
return ' - '.join(parts)
|
||
|
||
@property
|
||
def report_coo_origin_country(self):
|
||
return self.report_packing_origin or ''
|
||
|
||
@property
|
||
def report_coo_observations(self):
|
||
parts = []
|
||
contract = self.report_packing_contract_number
|
||
if contract:
|
||
parts.append(f"Contract: {contract}")
|
||
if self.note:
|
||
parts.append(self.note)
|
||
return '\n'.join(filter(None, parts))
|
||
|
||
@property
|
||
def report_coo_goods_description(self):
|
||
parts = [self.report_product_name, self.report_product_description]
|
||
if self.container:
|
||
container_numbers = ', '.join(
|
||
filter(None, (getattr(c, 'container_no', None) or '' for c in self.container)))
|
||
if container_numbers:
|
||
parts.append(f"Container(s): {container_numbers}")
|
||
return '\n'.join(filter(None, parts))
|
||
|
||
@property
|
||
def report_coo_net_weight(self):
|
||
return self.report_packing_net_weight
|
||
|
||
@property
|
||
def report_coo_gross_weight(self):
|
||
return self.report_packing_gross_weight
|
||
|
||
@property
|
||
def report_coo_weight_unit(self):
|
||
return self.report_packing_weight_unit or ''
|
||
|
||
@property
|
||
def report_coo_net_weight_display(self):
|
||
parts = [self.report_coo_net_weight, self.report_coo_weight_unit]
|
||
return ' '.join(part for part in parts if part)
|
||
|
||
@property
|
||
def report_coo_gross_weight_display(self):
|
||
parts = [self.report_coo_gross_weight, self.report_coo_weight_unit]
|
||
return ' '.join(part for part in parts if part)
|
||
|
||
@property
|
||
def report_coo_issue_date(self):
|
||
Date = Pool().get('ir.date')
|
||
today = Date.today()
|
||
if not today:
|
||
return ''
|
||
return today.strftime('%d-%m-%Y')
|
||
|
||
@staticmethod
|
||
def _report_record_key(record):
|
||
record_id = getattr(record, 'id', None)
|
||
return record_id if record_id is not None else id(record)
|
||
|
||
@classmethod
|
||
def _report_unique_records(cls, records):
|
||
unique = []
|
||
seen = set()
|
||
for record in records:
|
||
if not record:
|
||
continue
|
||
key = cls._report_record_key(record)
|
||
if key in seen:
|
||
continue
|
||
seen.add(key)
|
||
unique.append(record)
|
||
return unique
|
||
|
||
def _get_report_linkage_lots(self):
|
||
return self._report_unique_records(
|
||
getattr(move, 'lot', None)
|
||
for move in (self.incoming_moves or self.moves or []))
|
||
|
||
@staticmethod
|
||
def _report_linkage_lot_purchase_line(lot):
|
||
line = getattr(lot, 'line', None)
|
||
if line:
|
||
return line
|
||
get_virtual_lot = getattr(lot, 'getVlot_p', None)
|
||
if callable(get_virtual_lot):
|
||
try:
|
||
virtual_lot = get_virtual_lot()
|
||
except (IndexError, AttributeError):
|
||
virtual_lot = None
|
||
return getattr(virtual_lot, 'line', None)
|
||
return None
|
||
|
||
@staticmethod
|
||
def _report_linkage_lot_sale_line(lot):
|
||
line = getattr(lot, 'sale_line', None)
|
||
if line:
|
||
return line
|
||
get_virtual_lot = getattr(lot, 'getVlot_s', None)
|
||
if callable(get_virtual_lot):
|
||
try:
|
||
virtual_lot = get_virtual_lot()
|
||
except (IndexError, AttributeError):
|
||
virtual_lot = None
|
||
return getattr(virtual_lot, 'sale_line', None)
|
||
return None
|
||
|
||
def _get_report_linkage_purchase_lines(self):
|
||
return self._report_unique_records(
|
||
self._report_linkage_lot_purchase_line(lot)
|
||
for lot in self._get_report_linkage_lots())
|
||
|
||
def _get_report_linkage_sale_lines(self):
|
||
return self._report_unique_records(
|
||
self._report_linkage_lot_sale_line(lot)
|
||
for lot in self._get_report_linkage_lots())
|
||
|
||
def _get_report_linkage_purchase(self):
|
||
purchases = self._report_unique_records(
|
||
getattr(line, 'purchase', None)
|
||
for line in self._get_report_linkage_purchase_lines())
|
||
return purchases[0] if purchases else None
|
||
|
||
def _get_report_linkage_sale(self):
|
||
sales = self._report_unique_records(
|
||
getattr(line, 'sale', None)
|
||
for line in self._get_report_linkage_sale_lines())
|
||
return sales[0] if sales else None
|
||
|
||
@staticmethod
|
||
def _report_rec_name(record):
|
||
return (
|
||
getattr(record, 'rec_name', None)
|
||
or getattr(record, 'name', None)
|
||
or getattr(record, 'number', None)
|
||
or '')
|
||
|
||
@classmethod
|
||
def _report_number(cls, record):
|
||
return (
|
||
getattr(record, 'number', None)
|
||
or getattr(record, 'full_number', None)
|
||
or cls._report_rec_name(record)
|
||
or '')
|
||
|
||
@staticmethod
|
||
def _report_date(value):
|
||
if not value:
|
||
return ''
|
||
if hasattr(value, 'strftime'):
|
||
return value.strftime('%d %b %y')
|
||
return str(value)
|
||
|
||
@staticmethod
|
||
def _report_amount(value):
|
||
if value in (None, ''):
|
||
return ''
|
||
return format(Decimal(str(value or 0)).quantize(Decimal('0.001')), ',f')
|
||
|
||
@staticmethod
|
||
def _report_price(value):
|
||
if value in (None, ''):
|
||
return ''
|
||
return format(Decimal(str(value or 0)).quantize(Decimal('0.001')), 'f')
|
||
|
||
@classmethod
|
||
def _report_currency_code(cls, record):
|
||
currency = getattr(record, 'currency', None)
|
||
return (
|
||
getattr(currency, 'code', None)
|
||
or getattr(currency, 'name', None)
|
||
or getattr(currency, 'symbol', None)
|
||
or '')
|
||
|
||
@classmethod
|
||
def _report_unit_symbol(cls, line):
|
||
unit = getattr(line, 'unit', None)
|
||
return (
|
||
getattr(unit, 'symbol', None)
|
||
or getattr(unit, 'rec_name', None)
|
||
or getattr(unit, 'name', None)
|
||
or '')
|
||
|
||
@classmethod
|
||
def _report_line_quantity(cls, line):
|
||
return Decimal(str(
|
||
getattr(line, 'quantity_theorical', None)
|
||
or getattr(line, 'quantity', 0)
|
||
or 0))
|
||
|
||
@classmethod
|
||
def _report_line_amount(cls, line):
|
||
quantity = cls._report_line_quantity(line)
|
||
price = Decimal(str(getattr(line, 'unit_price', 0) or 0))
|
||
return quantity * price
|
||
|
||
def _get_report_linkage_product(self):
|
||
lines = (
|
||
self._get_report_linkage_purchase_lines()
|
||
or self._get_report_linkage_sale_lines())
|
||
product = getattr(lines[0], 'product', None) if lines else None
|
||
if product:
|
||
return product
|
||
move = self._get_report_primary_move()
|
||
return getattr(move, 'product', None) if move else None
|
||
|
||
def _get_report_linkage_fees(self):
|
||
fees = []
|
||
seen = set()
|
||
|
||
def add_fee(fee):
|
||
if not fee:
|
||
return
|
||
key = self._report_record_key(fee)
|
||
if key in seen:
|
||
return
|
||
seen.add(key)
|
||
fees.append(fee)
|
||
|
||
for fee in getattr(self, 'fees', []) or []:
|
||
add_fee(fee)
|
||
for line in (
|
||
self._get_report_linkage_purchase_lines()
|
||
+ self._get_report_linkage_sale_lines()):
|
||
for fee in getattr(line, 'fees', []) or []:
|
||
add_fee(fee)
|
||
return fees
|
||
|
||
@classmethod
|
||
def _report_fee_amount(cls, fee):
|
||
get_amount = getattr(fee, 'get_amount', None)
|
||
if callable(get_amount):
|
||
amount = get_amount()
|
||
else:
|
||
amount = None
|
||
if amount in (None, ''):
|
||
price = Decimal(str(getattr(fee, 'price', 0) or 0))
|
||
quantity = Decimal(str(getattr(fee, 'quantity', 0) or 0))
|
||
mode = getattr(fee, 'mode', None)
|
||
amount = price if mode == 'lumpsum' else price * quantity
|
||
amount = Decimal(str(amount or 0))
|
||
return -amount if getattr(fee, 'p_r', None) == 'pay' else amount
|
||
|
||
@classmethod
|
||
def _report_linkage_fee_label(cls, fee):
|
||
product = getattr(fee, 'product', None)
|
||
label = (
|
||
getattr(product, 'name', None)
|
||
or cls._report_rec_name(product)
|
||
or 'Fee')
|
||
if label.startswith('[') and '] ' in label:
|
||
label = label.split('] ', 1)[1]
|
||
return label
|
||
|
||
def _get_report_linkage_summary_rows(self):
|
||
purchase_lines = self._get_report_linkage_purchase_lines()
|
||
sale_lines = self._get_report_linkage_sale_lines()
|
||
purchase_total = -sum(
|
||
self._report_line_amount(line) for line in purchase_lines)
|
||
sale_total = sum(self._report_line_amount(line) for line in sale_lines)
|
||
|
||
rows = []
|
||
if purchase_lines:
|
||
rows.append((
|
||
'Purchases', '', purchase_total, purchase_total,
|
||
'major'))
|
||
if sale_lines:
|
||
rows.append(('Sales', '', sale_total, sale_total, 'major'))
|
||
|
||
fee_total = Decimal('0')
|
||
fee_rows = {}
|
||
for fee in self._get_report_linkage_fees():
|
||
label = self._report_linkage_fee_label(fee)
|
||
amount = self._report_fee_amount(fee)
|
||
fee_total += amount
|
||
fee_rows[label] = fee_rows.get(label, Decimal('0')) + amount
|
||
for label, amount in fee_rows.items():
|
||
rows.append(('Costs', label, amount, amount, 'cost'))
|
||
|
||
if rows:
|
||
rows.append(('Costs', 'Total', fee_total, fee_total, 'cost'))
|
||
pnl = purchase_total + sale_total + fee_total
|
||
rows.append(('P&L', '', pnl, pnl, 'pnl'))
|
||
return rows
|
||
|
||
def _get_report_linkage_movement_rows(self):
|
||
rows = []
|
||
purchase_lines = self._get_report_linkage_purchase_lines()
|
||
sale_lines = self._get_report_linkage_sale_lines()
|
||
if purchase_lines:
|
||
rows.append(('Purchase', '', '', '', '', '', '', '', '', ''))
|
||
for line in purchase_lines:
|
||
purchase = getattr(line, 'purchase', None)
|
||
rows.append(self._get_report_linkage_movement_row(
|
||
'Deal', purchase, line))
|
||
if sale_lines:
|
||
rows.append(('Sale', '', '', '', '', '', '', '', '', ''))
|
||
for line in sale_lines:
|
||
sale = getattr(line, 'sale', None)
|
||
rows.append(self._get_report_linkage_movement_row(
|
||
'Deal', sale, line))
|
||
if purchase_lines or sale_lines:
|
||
balance = (
|
||
sum(self._report_line_quantity(line) for line in purchase_lines)
|
||
- sum(self._report_line_quantity(line) for line in sale_lines))
|
||
unit = self._report_unit_symbol(
|
||
purchase_lines[0] if purchase_lines else sale_lines[0])
|
||
rows.append((
|
||
'Balance (Gain/Loss)', '', '', '',
|
||
' '.join(part for part in [
|
||
self._format_report_quantity(balance),
|
||
unit.upper() if unit else ''] if part),
|
||
'', '', '', '', ''))
|
||
return rows
|
||
|
||
def _get_report_linkage_movement_row(self, kind, trade, line):
|
||
product = getattr(line, 'product', None)
|
||
quantity = ' '.join(part for part in [
|
||
self._format_report_quantity(self._report_line_quantity(line)),
|
||
(self._report_unit_symbol(line) or '').upper()] if part)
|
||
incoterm = getattr(trade, 'incoterm', None)
|
||
period = getattr(line, 'period_at', None) or ''
|
||
if period == 'laycan':
|
||
period = 'Laycan'
|
||
return (
|
||
kind,
|
||
self._report_number(trade),
|
||
self._report_rec_name(getattr(trade, 'party', None)),
|
||
self._report_rec_name(product),
|
||
quantity,
|
||
getattr(incoterm, 'code', None) or '',
|
||
self._report_rec_name(getattr(trade, 'to_location', None)),
|
||
period,
|
||
self._report_date(getattr(line, 'from_del', None)),
|
||
self._report_date(getattr(line, 'to_del', None)),
|
||
)
|
||
|
||
def _get_report_linkage_pricing_rows(self):
|
||
rows = []
|
||
for label, lines in (
|
||
('Buy', self._get_report_linkage_purchase_lines()),
|
||
('Sell', self._get_report_linkage_sale_lines())):
|
||
for line in lines:
|
||
trade = (
|
||
getattr(line, 'purchase', None)
|
||
or getattr(line, 'sale', None))
|
||
currency = self._report_currency_code(trade)
|
||
unit = self._report_unit_symbol(line).upper()
|
||
price = self._report_price(getattr(line, 'unit_price', 0))
|
||
rows.append((
|
||
self._report_number(trade),
|
||
label,
|
||
(getattr(line, 'price_type', None) or 'Fixed').title(),
|
||
'',
|
||
self._report_rec_name(getattr(line, 'del_period', None)),
|
||
' '.join(part for part in [
|
||
price,
|
||
'/'.join(part for part in [currency, unit] if part)]
|
||
if part),
|
||
))
|
||
return rows
|
||
|
||
def _get_report_linkage_detail_rows(self):
|
||
rows = []
|
||
for line in self._get_report_linkage_sale_lines():
|
||
sale = getattr(line, 'sale', None)
|
||
rows.append(self._get_report_linkage_detail_row(
|
||
'A', 'Deal revenue', sale, line, self._report_line_amount(line)))
|
||
for line in self._get_report_linkage_purchase_lines():
|
||
purchase = getattr(line, 'purchase', None)
|
||
rows.append(self._get_report_linkage_detail_row(
|
||
'A', 'Deal expense', purchase, line,
|
||
-self._report_line_amount(line)))
|
||
for fee in self._get_report_linkage_fees():
|
||
rows.append(self._get_report_linkage_fee_detail_row(fee))
|
||
total = sum((row[7] for row in rows), Decimal('0'))
|
||
if rows:
|
||
rows.append(('A', 'Total', '', '', '', '', total, total, 'total'))
|
||
rows.append(('P&L', '', '', '', '', '', total, total, 'pnl'))
|
||
return rows
|
||
|
||
def _get_report_linkage_detail_row(self, group, label, trade, line, amount):
|
||
currency = self._report_currency_code(trade)
|
||
unit = self._report_unit_symbol(line).upper()
|
||
unit_price = self._report_price(getattr(line, 'unit_price', 0))
|
||
quantity = self._format_report_quantity(self._report_line_quantity(line))
|
||
return (
|
||
group,
|
||
label,
|
||
self._report_rec_name(getattr(trade, 'party', None)),
|
||
self._report_number(trade),
|
||
' '.join(part for part in [
|
||
unit_price,
|
||
'/'.join(part for part in [currency, unit] if part)]
|
||
if part),
|
||
quantity,
|
||
amount,
|
||
amount,
|
||
'line',
|
||
)
|
||
|
||
def _get_report_linkage_fee_detail_row(self, fee):
|
||
product = getattr(fee, 'product', None)
|
||
trade_line = (
|
||
getattr(fee, 'line', None)
|
||
or getattr(fee, 'sale_line', None)
|
||
or (self._get_report_linkage_purchase_lines()
|
||
or self._get_report_linkage_sale_lines()
|
||
or [None])[0])
|
||
trade = (
|
||
getattr(trade_line, 'purchase', None)
|
||
or getattr(trade_line, 'sale', None))
|
||
currency = self._report_currency_code(fee) or self._report_currency_code(trade)
|
||
unit = self._get_report_unit_text(getattr(fee, 'unit', None))
|
||
amount = self._report_fee_amount(fee)
|
||
quantity = getattr(fee, 'quantity', None)
|
||
if quantity in (None, '') and trade_line:
|
||
quantity = self._report_line_quantity(trade_line)
|
||
return (
|
||
'A',
|
||
self._report_linkage_fee_label(fee),
|
||
self._report_rec_name(getattr(fee, 'supplier', None)),
|
||
self._report_number(trade),
|
||
' '.join(part for part in [
|
||
self._report_price(getattr(fee, 'price', 0)),
|
||
'/'.join(part for part in [currency, unit.upper() if unit else ''] if part)]
|
||
if part),
|
||
self._format_report_quantity(quantity or 0),
|
||
amount,
|
||
amount,
|
||
'fee',
|
||
)
|
||
|
||
@classmethod
|
||
def _report_column(cls, rows, index, amount=False):
|
||
values = []
|
||
for row in rows:
|
||
value = row[index]
|
||
if amount:
|
||
value = cls._report_amount(value)
|
||
values.append(str(value or ''))
|
||
return '\n'.join(values)
|
||
|
||
@classmethod
|
||
def _report_table_rows(cls, rows, names, amount_names=()):
|
||
result = []
|
||
for row in rows:
|
||
values = {}
|
||
for index, name in enumerate(names):
|
||
value = row[index]
|
||
if name in amount_names:
|
||
value = cls._report_amount(value)
|
||
values[name] = str(value or '')
|
||
result.append(values)
|
||
return result
|
||
|
||
@property
|
||
def report_linkage_title(self):
|
||
purchase = self._get_report_linkage_purchase()
|
||
sale = self._get_report_linkage_sale()
|
||
reference = (
|
||
getattr(purchase, 'reference', None)
|
||
or getattr(sale, 'reference', None)
|
||
or getattr(self, 'reference', None)
|
||
or '')
|
||
numbers = '/'.join(filter(None, [
|
||
self._report_number(purchase),
|
||
self._report_number(sale),
|
||
]))
|
||
return ' '.join(part for part in ['Linkage', numbers, reference] if part)
|
||
|
||
@property
|
||
def report_linkage_desk(self):
|
||
purchase_lines = self._get_report_linkage_purchase_lines()
|
||
product = (
|
||
getattr(purchase_lines[0], 'product', None)
|
||
if purchase_lines else self._get_report_linkage_product())
|
||
return self._report_rec_name(product)
|
||
|
||
@property
|
||
def report_linkage_book(self):
|
||
purchase_lines = self._get_report_linkage_purchase_lines()
|
||
product = (
|
||
getattr(purchase_lines[0], 'product', None)
|
||
if purchase_lines else self._get_report_linkage_product())
|
||
code = getattr(product, 'code', None) if product else ''
|
||
period = self.report_linkage_strategy
|
||
return ' '.join(part for part in [code, period] if part)
|
||
|
||
@property
|
||
def report_linkage_strategy(self):
|
||
for line in (
|
||
self._get_report_linkage_purchase_lines()
|
||
or self._get_report_linkage_sale_lines()):
|
||
strategies = list(getattr(line, 'mtm', []) or [])
|
||
if strategies:
|
||
strategy = getattr(strategies[0], 'strategy', None) or strategies[0]
|
||
return self._report_rec_name(strategy)
|
||
period = getattr(line, 'del_period', None)
|
||
if period:
|
||
return self._report_rec_name(period)
|
||
return ''
|
||
|
||
@property
|
||
def report_linkage_bl_date(self):
|
||
return self.bl_date and self.bl_date.strftime('%A, %B %d, %Y') or ''
|
||
|
||
@property
|
||
def report_linkage_trading_unit(self):
|
||
purchase = self._get_report_linkage_purchase()
|
||
company = getattr(purchase, 'company', None) if purchase else None
|
||
if not company:
|
||
company = getattr(self, 'company', None)
|
||
party = getattr(company, 'party', None) if company else None
|
||
if party:
|
||
return self._report_rec_name(party)
|
||
sale = self._get_report_linkage_sale()
|
||
party = getattr(sale, 'party', None) if sale else None
|
||
return self._report_rec_name(party)
|
||
|
||
@property
|
||
def report_linkage_finance_user(self):
|
||
trade = self._get_report_linkage_purchase() or self._get_report_linkage_sale()
|
||
operator = getattr(trade, 'operator', None) if trade else None
|
||
return self._report_rec_name(operator)
|
||
|
||
@property
|
||
def report_linkage_summary_groups(self):
|
||
return self._report_column(self._get_report_linkage_summary_rows(), 0)
|
||
|
||
@property
|
||
def report_linkage_summary_cost_types(self):
|
||
return self._report_column(self._get_report_linkage_summary_rows(), 1)
|
||
|
||
@property
|
||
def report_linkage_summary_estimated(self):
|
||
return self._report_column(
|
||
self._get_report_linkage_summary_rows(), 2, amount=True)
|
||
|
||
@property
|
||
def report_linkage_summary_validated(self):
|
||
return self._report_column(
|
||
self._get_report_linkage_summary_rows(), 3, amount=True)
|
||
|
||
@property
|
||
def report_linkage_summary_rows(self):
|
||
return self._report_table_rows(
|
||
self._get_report_linkage_summary_rows(),
|
||
('group', 'cost_type', 'estimated', 'validated', 'kind'),
|
||
{'estimated', 'validated'})
|
||
|
||
@property
|
||
def report_linkage_movement_types(self):
|
||
return self._report_column(self._get_report_linkage_movement_rows(), 0)
|
||
|
||
@property
|
||
def report_linkage_movement_references(self):
|
||
return self._report_column(self._get_report_linkage_movement_rows(), 1)
|
||
|
||
@property
|
||
def report_linkage_movement_counterparts(self):
|
||
return self._report_column(self._get_report_linkage_movement_rows(), 2)
|
||
|
||
@property
|
||
def report_linkage_movement_commodities(self):
|
||
return self._report_column(self._get_report_linkage_movement_rows(), 3)
|
||
|
||
@property
|
||
def report_linkage_movement_quantities(self):
|
||
return self._report_column(self._get_report_linkage_movement_rows(), 4)
|
||
|
||
@property
|
||
def report_linkage_movement_deliveries(self):
|
||
return self._report_column(self._get_report_linkage_movement_rows(), 5)
|
||
|
||
@property
|
||
def report_linkage_movement_basis(self):
|
||
return self._report_column(self._get_report_linkage_movement_rows(), 6)
|
||
|
||
@property
|
||
def report_linkage_movement_periods(self):
|
||
return self._report_column(self._get_report_linkage_movement_rows(), 7)
|
||
|
||
@property
|
||
def report_linkage_movement_from_dates(self):
|
||
return self._report_column(self._get_report_linkage_movement_rows(), 8)
|
||
|
||
@property
|
||
def report_linkage_movement_to_dates(self):
|
||
return self._report_column(self._get_report_linkage_movement_rows(), 9)
|
||
|
||
@property
|
||
def report_linkage_movement_rows(self):
|
||
return self._report_table_rows(
|
||
self._get_report_linkage_movement_rows(),
|
||
('type', 'reference', 'counterpart', 'commodity', 'quantity',
|
||
'delivery', 'basis', 'period', 'from_date', 'to_date'))
|
||
|
||
@property
|
||
def report_linkage_bank_deal(self):
|
||
purchase = self._get_report_linkage_purchase()
|
||
return self._report_number(purchase)
|
||
|
||
@property
|
||
def report_linkage_bank_type(self):
|
||
return 'Buy' if self._get_report_linkage_purchase() else ''
|
||
|
||
@property
|
||
def report_linkage_bank_name(self):
|
||
sale = self._get_report_linkage_sale()
|
||
bank = getattr(sale, 'our_bank_account', None) if sale else None
|
||
bank_party = getattr(bank, 'bank', None) if bank else None
|
||
return self._report_rec_name(bank_party)
|
||
|
||
@property
|
||
def report_linkage_lc_type(self):
|
||
return 'N/A'
|
||
|
||
@property
|
||
def report_linkage_lc_number(self):
|
||
sale = self._get_report_linkage_sale()
|
||
return getattr(sale, 'lc_number', '') or ''
|
||
|
||
@property
|
||
def report_linkage_lc_amount(self):
|
||
return ''
|
||
|
||
@property
|
||
def report_linkage_lc_expiry_date(self):
|
||
sale = self._get_report_linkage_sale()
|
||
return self._report_date(getattr(sale, 'lc_date', None))
|
||
|
||
@property
|
||
def report_linkage_pricing_deals(self):
|
||
return self._report_column(self._get_report_linkage_pricing_rows(), 0)
|
||
|
||
@property
|
||
def report_linkage_pricing_sides(self):
|
||
return self._report_column(self._get_report_linkage_pricing_rows(), 1)
|
||
|
||
@property
|
||
def report_linkage_pricing_types(self):
|
||
return self._report_column(self._get_report_linkage_pricing_rows(), 2)
|
||
|
||
@property
|
||
def report_linkage_pricing_symbols(self):
|
||
return self._report_column(self._get_report_linkage_pricing_rows(), 3)
|
||
|
||
@property
|
||
def report_linkage_pricing_periods(self):
|
||
return self._report_column(self._get_report_linkage_pricing_rows(), 4)
|
||
|
||
@property
|
||
def report_linkage_pricing_input_prices(self):
|
||
return self._report_column(self._get_report_linkage_pricing_rows(), 5)
|
||
|
||
@property
|
||
def report_linkage_pricing_rows(self):
|
||
return self._report_table_rows(
|
||
self._get_report_linkage_pricing_rows(),
|
||
('deal', 'side', 'type', 'symbol', 'period', 'input_price'))
|
||
|
||
@property
|
||
def report_linkage_detail_groups(self):
|
||
return self._report_column(self._get_report_linkage_detail_rows(), 0)
|
||
|
||
@property
|
||
def report_linkage_detail_cost_types(self):
|
||
return self._report_column(self._get_report_linkage_detail_rows(), 1)
|
||
|
||
@property
|
||
def report_linkage_detail_counterparts(self):
|
||
return self._report_column(self._get_report_linkage_detail_rows(), 2)
|
||
|
||
@property
|
||
def report_linkage_detail_sources(self):
|
||
return self._report_column(self._get_report_linkage_detail_rows(), 3)
|
||
|
||
@property
|
||
def report_linkage_detail_unit_prices(self):
|
||
return self._report_column(self._get_report_linkage_detail_rows(), 4)
|
||
|
||
@property
|
||
def report_linkage_detail_quantities(self):
|
||
return self._report_column(self._get_report_linkage_detail_rows(), 5)
|
||
|
||
@property
|
||
def report_linkage_detail_estimated(self):
|
||
return self._report_column(
|
||
self._get_report_linkage_detail_rows(), 6, amount=True)
|
||
|
||
@property
|
||
def report_linkage_detail_validated(self):
|
||
return self._report_column(
|
||
self._get_report_linkage_detail_rows(), 7, amount=True)
|
||
|
||
@property
|
||
def report_linkage_detail_rows(self):
|
||
return self._report_table_rows(
|
||
self._get_report_linkage_detail_rows(),
|
||
('group', 'cost_type', 'counterpart', 'source', 'unit_price',
|
||
'quantity', 'estimated', 'validated', 'kind'),
|
||
{'estimated', 'validated'})
|
||
|
||
def get_rec_name(self, name=None):
|
||
if self.number:
|
||
return self.number + '[' + (self.vessel.vessel_name if self.vessel else '') + (('-' + self.travel_nb) if self.travel_nb else '') + ']'
|
||
else:
|
||
return str(self.id)
|
||
|
||
def create_fee(self,controller):
|
||
Fee = Pool().get('fee.fee')
|
||
Product = Pool().get('product.product')
|
||
if not controller:
|
||
logger.info("CREATE_FEE_SKIPPED:NO_CONTROLLER shipment=%s", self)
|
||
return
|
||
fee = Fee()
|
||
fee.shipment_in = self.id
|
||
fee.supplier = controller
|
||
fee.type = 'budgeted'
|
||
fee.p_r = 'pay'
|
||
price,mode,curr,unit = controller.get_sla_cost(self.to_location)
|
||
if not (price and mode and curr and unit):
|
||
logger.info(
|
||
"CREATE_FEE_SKIPPED:NO_SLA shipment=%s controller=%s location=%s",
|
||
self, controller, self.to_location)
|
||
return
|
||
fee.mode = mode
|
||
fee.currency = curr
|
||
fee.unit = unit
|
||
fee.quantity = self.get_bales() or 1
|
||
fee.product = Product.get_by_name('Reweighing')
|
||
fee.price = price
|
||
Fee.save([fee])
|
||
|
||
def get_controller(self):
|
||
ControllerCategory = Pool().get('party.category')
|
||
PartyCategory = Pool().get('party.party-party.category')
|
||
cc = ControllerCategory.search(['name','=','CONTROLLER'])
|
||
if cc:
|
||
cc = cc[0]
|
||
controllers = PartyCategory.search(['category','=',cc.id])
|
||
prioritized = []
|
||
for c in controllers:
|
||
if not c.party.IsAvailableForControl(self):
|
||
continue
|
||
gap, rule = c.party.get_controller_execution_priority(self)
|
||
prioritized.append((
|
||
1 if rule else 0,
|
||
gap if gap is not None else Decimal('-999999'),
|
||
c.party,
|
||
))
|
||
if prioritized:
|
||
prioritized.sort(key=lambda item: (item[0], item[1]), reverse=True)
|
||
return prioritized[0][2]
|
||
|
||
def get_instructions_html(self,inv_date,inv_nb):
|
||
vessel = self.vessel.vessel_name if self.vessel else ""
|
||
lines = [
|
||
"<p>Hi,</p>",
|
||
"<p>Please find details below for the requested control</p>",
|
||
]
|
||
lines.append(
|
||
"<p>"
|
||
f"<strong>BL number:</strong> {self.bl_number} | "
|
||
f"<strong>Vessel:</strong> {vessel} | "
|
||
f"<strong>ETA:</strong> {self.etad}"
|
||
"</p>"
|
||
)
|
||
|
||
if self.incoming_moves:
|
||
tot_net = sum([m.lot.get_current_quantity() for m in self.incoming_moves])
|
||
tot_gross = sum([m.lot.get_current_gross_quantity() for m in self.incoming_moves])
|
||
tot_bale = sum([m.lot.lot_qt for m in self.incoming_moves])
|
||
customer = self.incoming_moves[0].lot.sale_line.sale.party.name if self.incoming_moves[0].lot.sale_line else ""
|
||
unit = self.incoming_moves[0].lot.lot_unit_line.symbol
|
||
lines.append("<p>"
|
||
f"<strong>Customer:</strong> {customer} | "
|
||
f"<strong>Invoice Nb:</strong> {inv_nb} | "
|
||
f"<strong>Invoice Date:</strong> {inv_date}"
|
||
"</p>"
|
||
)
|
||
lines.append(
|
||
"<p>"
|
||
f"<strong>Nb Bales:</strong> {tot_bale} | "
|
||
f"<strong>Net Qt:</strong> {tot_net} {unit} | "
|
||
f"<strong>Gross Qt:</strong> {tot_gross} {unit}"
|
||
"</p>"
|
||
)
|
||
|
||
return "".join(lines)
|
||
|
||
# def get_instructions(self):
|
||
# lines = [
|
||
# "Hi,",
|
||
# "",
|
||
# "Please find details below for the requested control",
|
||
# f"BL number: {self.bl_number}",
|
||
# ""
|
||
# ]
|
||
|
||
# if self.incoming_moves:
|
||
# for m in self.incoming_moves:
|
||
# if m.lot:
|
||
# lines.append(
|
||
# f"Lot nb: {m.lot.lot_name} | "
|
||
# f"Net Qt: {m.lot.get_current_quantity()} {m.lot.lot_unit.symbol} | "
|
||
# f"Gross Qt: {m.lot.get_current_gross_quantity()} {m.lot.lot_unit.symbol}"
|
||
# )
|
||
|
||
# return "\n".join(lines)
|
||
|
||
def _create_lots_from_fintrade(self):
|
||
t = Table('freight_booking_lots')
|
||
cursor = Transaction().connection.cursor()
|
||
query = t.select(
|
||
t.BOOKING_NUMBER,
|
||
t.LOT_NUMBER,
|
||
t.LOT_NBR_BALES,
|
||
t.LOT_GROSS_WEIGHT,
|
||
t.LOT_NET_WEIGHT,
|
||
t.LOT_UOM,
|
||
t.LOT_QUALITY,
|
||
t.CUSTOMER,
|
||
t.SELL_PRICE_CURRENCY,
|
||
t.SELL_PRICE_UNIT,
|
||
t.SELL_PRICE,
|
||
t.SALE_INVOICE,
|
||
t.SELL_INV_AMOUNT,
|
||
t.SALE_INVOICE_DATE,
|
||
t.SELL_PREMIUM,
|
||
t.SALE_CONTRACT_NUMBER,
|
||
t.SALE_DECLARATION_KEY,
|
||
t.SHIPMENT_CHUNK_KEY,
|
||
where=(t.BOOKING_NUMBER == int(self.reference))
|
||
)
|
||
cursor.execute(*query)
|
||
rows = cursor.fetchall()
|
||
logger.info("ROWS:%s",rows)
|
||
inv_date = None
|
||
inv_nb = None
|
||
if rows:
|
||
sale_line = None
|
||
for row in rows:
|
||
logger.info("ROW:%s",row)
|
||
#Purchase & Sale creation
|
||
LotQt = Pool().get('lot.qt')
|
||
Lot = Pool().get('lot.lot')
|
||
LotAdd = Pool().get('lot.add.line')
|
||
Currency = Pool().get('currency.currency')
|
||
Product = Pool().get('product.product')
|
||
Party = Pool().get('party.party')
|
||
Uom = Pool().get('product.uom')
|
||
Sale = Pool().get('sale.sale')
|
||
SaleLine = Pool().get('sale.line')
|
||
dec_key = str(row[16]).strip()
|
||
chunk_key = str(row[17]).strip()
|
||
lot_unit = str(row[5]).strip().lower()
|
||
product = str(row[6]).strip().upper()
|
||
lot_net_weight = Decimal(row[4])
|
||
logger.info("LOT_NET_WEIGHT:%s",lot_net_weight)
|
||
lot_gross_weight = Decimal(row[3])
|
||
lot_bales = Decimal(row[2])
|
||
lot_number = row[1]
|
||
customer = str(row[7]).strip().upper()
|
||
sell_price_currency = str(row[8]).strip().upper()
|
||
sell_price_unit = str(row[9]).strip().lower()
|
||
inv_date = str(row[13]).strip()
|
||
inv_nb = str(row[11]).strip()
|
||
sell_price = Decimal(row[10])
|
||
premium = Decimal(row[14])
|
||
reference = Decimal(row[15])
|
||
logger.info("DECLARATION_KEY:%s",dec_key)
|
||
declaration = SaleLine.search(['note','=',dec_key])
|
||
if declaration:
|
||
sale_line = declaration[0]
|
||
logger.info("WITH_DEC:%s",sale_line)
|
||
vlot = sale_line.lots[0]
|
||
lqt = LotQt.search([('lot_s','=',vlot.id)])
|
||
if lqt:
|
||
for lq in lqt:
|
||
if lq.lot_p:
|
||
logger.info("VLOT_P:%s",lq.lot_p)
|
||
sale_line.quantity_theorical += round(lot_net_weight,2)
|
||
SaleLine.save([sale_line])
|
||
lq.lot_p.updateVirtualPart(round(lot_net_weight,2),self,lq.lot_s)
|
||
vlot.set_current_quantity(round(lot_net_weight,2),round(lot_gross_weight,2),1)
|
||
Lot.save([vlot])
|
||
else:
|
||
sale = Sale()
|
||
sale_line = SaleLine()
|
||
sale.party = Party.getPartyByName(customer,'CLIENT')
|
||
logger.info("SALE_PARTY:%s",sale.party)
|
||
sale.reference = reference
|
||
sale.from_location = self.from_location
|
||
sale.to_location = self.to_location
|
||
sale.company = 6
|
||
sale.payment_term = 2
|
||
if sale.party.addresses:
|
||
sale.invoice_address = sale.party.addresses[0]
|
||
sale.shipment_address = sale.party.addresses[0]
|
||
|
||
if sell_price_currency == 'USC':
|
||
sale.currency = Currency.get_by_name('USD')
|
||
sale_line.enable_linked_currency = True
|
||
sale_line.linked_currency = 1
|
||
sale_line.linked_unit = Uom.get_by_name(sell_price_unit)
|
||
sale_line.linked_price = round(sell_price,4)
|
||
else:
|
||
sale.currency = Currency.get_by_name(sell_price_currency)
|
||
sale_line.unit_price = round(sell_price,4)
|
||
sale_line.unit = Uom.get_by_name(sell_price_unit)
|
||
sale_line.premium = premium
|
||
Sale.save([sale])
|
||
sale_line.sale = sale.id
|
||
sale_line.quantity = round(lot_net_weight,2)
|
||
sale_line.quantity_theorical = round(lot_net_weight,2)
|
||
sale_line.product = Product.get_by_name('BRAZIL COTTON')
|
||
logger.info("PRODUCT:%s",sale_line.product)
|
||
sale_line.unit = Uom.get_by_name(lot_unit)
|
||
if sell_price_currency == 'USC':
|
||
sale_line.unit_price = sale_line.get_price_linked_currency()
|
||
sale_line.price_type = 'priced'
|
||
sale_line.created_by_code = False
|
||
sale_line.note = dec_key
|
||
SaleLine.save([sale_line])
|
||
|
||
#need to link the virtual part to the shipment
|
||
lqt = LotQt.search([('lot_s','=',sale_line.lots[0])])
|
||
if lqt:
|
||
lqt[0].lot_shipment_in = self
|
||
LotQt.save(lqt)
|
||
logger.info("SALE_LINKED_TO_SHIPMENT:%s",self)
|
||
|
||
ContractStart = Pool().get('contracts.start')
|
||
ContractDetail = Pool().get('contract.detail')
|
||
ct = ContractStart()
|
||
d = ContractDetail()
|
||
ct.type = 'Purchase'
|
||
ct.matched = True
|
||
ct.shipment_in = self
|
||
ct.lot = sale_line.lots[0]
|
||
ct.product = sale_line.product
|
||
ct.unit = sale_line.unit
|
||
d.party = Party.getPartyByName('FAIRCOT')
|
||
if sale_line.enable_linked_currency:
|
||
d.currency_unit = str(sale_line.linked_currency.id) + '_' + str(sale_line.linked_unit.id)
|
||
else:
|
||
d.currency_unit = str(sale.currency.id) + '_' + str(sale_line.unit.id)
|
||
d.quantity = sale_line.quantity
|
||
d.unit = sale_line.unit
|
||
d.price = sale_line.unit_price
|
||
d.price_type = 'priced'
|
||
d.crop = None
|
||
d.tol_min = 0
|
||
d.tol_max = 0
|
||
d.incoterm = None
|
||
d.reference = str(sale.id)
|
||
d.from_location = sale.from_location
|
||
d.to_location = sale.to_location
|
||
d.del_period = None
|
||
d.from_del = None
|
||
d.to_del = None
|
||
d.payment_term = sale.payment_term
|
||
ct.contracts = [d]
|
||
ContractFactory.create_contracts(
|
||
ct.contracts,
|
||
type_=ct.type,
|
||
ct=ct,
|
||
)
|
||
|
||
#Lots creation
|
||
vlot = sale_line.lots[0]
|
||
lqt = LotQt.search([('lot_s','=',vlot.id),('lot_p','>',0)])
|
||
if lqt and vlot.lot_quantity > 0:
|
||
lqt = lqt[0]
|
||
l = LotAdd()
|
||
l.lot_qt = lot_bales
|
||
l.lot_unit = Uom.get_by_name('bale')
|
||
l.lot_unit_line = Uom.get_by_name(lot_unit)
|
||
l.lot_quantity = round(lot_net_weight,2)
|
||
l.lot_gross_quantity = round(lot_gross_weight,2)
|
||
l.lot_premium = premium
|
||
l.lot_chunk_key = int(chunk_key)
|
||
logger.info("ADD_LOT:%s",int(chunk_key))
|
||
LotQt.add_physical_lots(lqt,[l])
|
||
|
||
return inv_date,inv_nb
|
||
|
||
def html_to_text(self,html_content):
|
||
text = re.sub(r"<br\s*/?>", "\n", html_content, flags=re.IGNORECASE)
|
||
text = re.sub(r"</p\s*>", "\n\n", text, flags=re.IGNORECASE)
|
||
text = re.sub(r"<[^>]+>", "", text)
|
||
return html.unescape(text).strip()
|
||
|
||
def create_service_order(self,so_payload):
|
||
response = requests.post(
|
||
"http://automation-service:8006/service-order",
|
||
json=so_payload,
|
||
timeout=10
|
||
)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
|
||
@classmethod
|
||
@ModelView.button
|
||
def send(cls, shipments):
|
||
Date = Pool().get('ir.date')
|
||
Attachment = Pool().get('ir.attachment')
|
||
|
||
for sh in shipments:
|
||
sh.result = "Email not sent"
|
||
attachment = []
|
||
if sh.add_bl:
|
||
attachments = Attachment.search([
|
||
('resource', '=', 'stock.shipment.in,' + str(sh.id)),
|
||
])
|
||
if attachments:
|
||
content_b64 = base64.b64encode(attachments[0].data).decode('ascii')
|
||
attachment = [
|
||
{
|
||
"filename": attachments[0].name,
|
||
"content": content_b64,
|
||
"content_type": "application/pdf"
|
||
}
|
||
]
|
||
|
||
if sh.controller:
|
||
Contact = Pool().get('party.contact_mechanism')
|
||
contact = Contact.search(['party','=',sh.controller.id])
|
||
if contact:
|
||
payload = {
|
||
"to": [contact[0].value],
|
||
"subject": "Request for control",
|
||
"body": sh.html_to_text(sh.instructions),
|
||
"attachments": attachment,
|
||
"meta": {
|
||
"shipment": sh.bl_number,
|
||
"controller": sh.controller.id
|
||
}
|
||
}
|
||
|
||
response = requests.post(
|
||
"http://automation-service:8006/mail",
|
||
json=payload,
|
||
timeout=10
|
||
)
|
||
|
||
response.raise_for_status()
|
||
data = response.json()
|
||
logger.info("SEND_FROM_SHIPMENT:%s",data)
|
||
now = datetime.datetime.now()
|
||
sh.result = f"Email sent on {now.strftime('%d/%m/%Y %H:%M')}"
|
||
sh.save()
|
||
|
||
if sh.fees:
|
||
fee = sh.fees[0]
|
||
so_payload = {
|
||
"ControllerAlfCode": sh.controller.get_alf(),
|
||
"CurrKey": '3',
|
||
"Point1PlaceKey": sh.from_location.get_places(),
|
||
"Point2PlaceKey": sh.to_location.get_places(),
|
||
"OrderReference": sh.reference,
|
||
"FeeTotalCost": float(fee.amount),
|
||
"FeeUnitPrice": float(fee.price),
|
||
"ContractNumbers": sh.number,
|
||
"OrderQuantityGW": float(sh.get_quantity()) if sh.get_quantity() else float(1),
|
||
"NumberOfPackingBales": int(fee.quantity) if fee.quantity else int(1),
|
||
"ChunkKeyList": sh.get_chunk_key()
|
||
}
|
||
|
||
logger.info("PAYLOAD:%s",so_payload)
|
||
data = sh.create_service_order(so_payload)
|
||
logger.info("SO_NUMBER:%s",data.get('service_order_number'))
|
||
sh.result += f" / SO Nb {data.get('service_order_number')}"
|
||
sh.service_order_key = int(data.get('service_order_key'))
|
||
sh.save()
|
||
|
||
@classmethod
|
||
@ModelView.button
|
||
def compute(cls, shipments):
|
||
Sof = Pool().get('sof.statement')
|
||
for sh in shipments:
|
||
if sh.sof:
|
||
for s in sh.sof:
|
||
laytime_start = s._applied_laytime_start()
|
||
if laytime_start:
|
||
s.laytime_commenced = laytime_start
|
||
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)
|
||
if not s.laytime_completed or s.laytime_allowed is None:
|
||
Sof.save([s])
|
||
continue
|
||
total_time = (s.laytime_completed - s.laytime_commenced).total_seconds() / 3600.0
|
||
logger.info("COMPUTE_DEDUCTION3:%s",total_time)
|
||
s.actual_time_used = round(total_time,2)
|
||
s.deductions = round(sum([e.duration for e in s.sof_events if e.deductible == True]),2)
|
||
effective_time = round(total_time - s.deductions,2)
|
||
s.laytime_used = effective_time
|
||
s.laytime_balance = round(s.laytime_allowed - s.laytime_used,2)
|
||
s.compensation_amount = s._applied_compensation_amount()
|
||
Sof.save([s])
|
||
|
||
def get_vessel_url(self):
|
||
return f"https://www.vesselfinder.com/en/vessels/VOS-TRAVELLER-IMO-{self.vessel.vessel_imo}"
|
||
#return URL(f"https://www.vesselfinder.com/en/vessels/VOS-TRAVELLER-IMO-{self.vessel.vessel_imo}", target='new')
|
||
|
||
def get_imo(self,name):
|
||
if self.vessel:
|
||
return f"imo:{self.vessel.vessel_imo}"
|
||
|
||
def get_sh(self, name):
|
||
return self.id
|
||
|
||
@classmethod
|
||
def default_transport_type(cls):
|
||
return 'vessel'
|
||
|
||
@classmethod
|
||
def default_cargo_mode(cls):
|
||
return 'bulk'
|
||
|
||
@classmethod
|
||
def default_from_location(cls):
|
||
location = Transaction().context.get(
|
||
'lot_shipping_planned_from_location')
|
||
if location:
|
||
return location
|
||
try:
|
||
return super().default_from_location()
|
||
except AttributeError:
|
||
return None
|
||
|
||
@classmethod
|
||
def default_to_location(cls):
|
||
location = Transaction().context.get(
|
||
'lot_shipping_planned_to_location')
|
||
if location:
|
||
return location
|
||
try:
|
||
return super().default_to_location()
|
||
except AttributeError:
|
||
return None
|
||
|
||
@fields.depends('transport_type', 'cargo_mode', 'vessel')
|
||
def on_change_transport_type(self):
|
||
if self.transport_type == 'truck':
|
||
self.vessel = None
|
||
self.cargo_mode = 'none'
|
||
elif self.cargo_mode == 'none':
|
||
self.cargo_mode = 'bulk'
|
||
|
||
@classmethod
|
||
def default_dashboard(cls):
|
||
return 1
|
||
|
||
def get_chunk_key(self):
|
||
keys = [m.lot.lot_chunk_key for m in self.incoming_moves if m.lot]
|
||
return ",".join(map(str, keys)) if keys else None
|
||
|
||
def get_quantity(self,name=None):
|
||
if self.incoming_moves:
|
||
return sum([(e.quantity if e.quantity else 0) for e in self.incoming_moves])
|
||
if self.lotqt:
|
||
return sum([(e.lot_quantity if e.lot_quantity else 0) for e in self.lotqt])
|
||
|
||
def _pnl_lot_ids(self):
|
||
lot_ids = []
|
||
seen = set()
|
||
|
||
def add(lot):
|
||
lot_id = getattr(lot, 'id', lot)
|
||
if not lot_id or lot_id in seen:
|
||
return
|
||
seen.add(lot_id)
|
||
lot_ids.append(lot_id)
|
||
|
||
for move in self.incoming_moves or []:
|
||
add(getattr(move, 'lot', None))
|
||
for lotqt in self.lotqt or []:
|
||
add(getattr(lotqt, 'lot_p', None))
|
||
add(getattr(lotqt, 'lot_s', None))
|
||
return lot_ids
|
||
|
||
def get_pnl_lines(self, name=None):
|
||
if not self.id:
|
||
return []
|
||
ValuationLine = Pool().get('valuation.valuation.line')
|
||
lines = ValuationLine.search(
|
||
[('shipment_in', '=', self.id)],
|
||
order=[('date', 'DESC'), ('id', 'DESC')])
|
||
return [line.id for line in lines]
|
||
|
||
@staticmethod
|
||
def _unique_condition_ids(conditions):
|
||
ids = []
|
||
seen = set()
|
||
for condition in conditions or []:
|
||
condition_id = getattr(condition, 'id', condition)
|
||
if not condition_id or condition_id in seen:
|
||
continue
|
||
seen.add(condition_id)
|
||
ids.append(condition_id)
|
||
return ids
|
||
|
||
def get_owner_charter_conditions(self, name=None):
|
||
return self._unique_condition_ids(
|
||
getattr(self.charter_party, 'conditions', None) or [])
|
||
|
||
def _get_purchase_lines_from_lots(self):
|
||
lines = []
|
||
seen = set()
|
||
for lot_qt in getattr(self, 'lotqt', []) or []:
|
||
line = getattr(getattr(lot_qt, 'lot_p', None), 'line', None)
|
||
line_id = getattr(line, 'id', None)
|
||
if not line or line_id in seen:
|
||
continue
|
||
seen.add(line_id)
|
||
lines.append(line)
|
||
return lines
|
||
|
||
def _get_sale_lines_from_lots(self):
|
||
lines = []
|
||
seen = set()
|
||
for lot_qt in getattr(self, 'lotqt', []) or []:
|
||
line = getattr(getattr(lot_qt, 'lot_s', None), 'sale_line', None)
|
||
line_id = getattr(line, 'id', None)
|
||
if not line or line_id in seen:
|
||
continue
|
||
seen.add(line_id)
|
||
lines.append(line)
|
||
return lines
|
||
|
||
def get_purchase_charter_conditions(self, name=None):
|
||
conditions = []
|
||
for line in self._get_purchase_lines_from_lots():
|
||
getter = getattr(line, 'get_effective_charter_conditions', None)
|
||
if getter:
|
||
conditions.extend(getter())
|
||
return self._unique_condition_ids(conditions)
|
||
|
||
def get_sale_charter_conditions(self, name=None):
|
||
conditions = []
|
||
for line in self._get_sale_lines_from_lots():
|
||
getter = getattr(line, 'get_effective_charter_conditions', None)
|
||
if getter:
|
||
conditions.extend(getter())
|
||
return self._unique_condition_ids(conditions)
|
||
|
||
def get_bales(self,name=None):
|
||
Lot = Pool().get('lot.lot')
|
||
lots = Lot.search(['lot_shipment_in','=',self.id])
|
||
if lots:
|
||
return sum([l.lot_qt for l in lots])
|
||
|
||
def get_unit(self,name=None):
|
||
if self.incoming_moves:
|
||
return self.incoming_moves[0].unit
|
||
|
||
def get_info(self,name):
|
||
if self.vessel:
|
||
vessel = Pool().get('trade.vessel')(self.vessel)
|
||
return vessel.get_info()
|
||
|
||
def get_anim(self,name):
|
||
gif_url = "http://vps107.geneva.hosting:8000/images/tanker.gif"
|
||
anim = f'<br><img src="{gif_url}" alt="loading..." style="margin-top:10px; width:80px;">'
|
||
return anim
|
||
|
||
def extract_sof_data_and_events_from_json(self, sof_json):
|
||
def parse_datetime(date_str, time_str):
|
||
dt = datetime.datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %H:%M")
|
||
return dt
|
||
|
||
def find_first_event(keywords):
|
||
for item in sof_json:
|
||
if any(kw.lower() in item['event'].lower() for kw in keywords):
|
||
return parse_datetime(item['date'], item['time']) - datetime.timedelta(hours=1)
|
||
return None
|
||
|
||
def find_last_event(keywords):
|
||
for item in reversed(sof_json):
|
||
if any(kw.lower() in item['event'].lower() for kw in keywords):
|
||
return parse_datetime(item['date'], item['time'])
|
||
return None
|
||
|
||
events = []
|
||
for item in sof_json:
|
||
try:
|
||
start = parse_datetime(item['date'], item['time'])
|
||
end_str = item.get("end_time")
|
||
end = datetime.datetime.strptime(end_str, "%Y-%m-%d %H:%M") if end_str else None
|
||
|
||
description = item["event"]
|
||
duration = (end - start).total_seconds() / 3600 if end else 0
|
||
deductible = any(word in description.lower() for word in ['rain', 'waiting', 'no loading'])
|
||
|
||
events.append({
|
||
'start': start,
|
||
'start_date': start.date(),
|
||
'start_time': start.time(),
|
||
'end': end,
|
||
'end_date': end.date() if end else None,
|
||
'end_time': end.time() if end else None,
|
||
'duration': duration,
|
||
'description': description,
|
||
'deductible': deductible,
|
||
})
|
||
except Exception as e:
|
||
logger.error("Error parsing SOF event: %s | %s", item, e)
|
||
|
||
# Extraction par mots-clés (inchangé)
|
||
arrival = find_first_event(["arrived", "anchor at", "proceed to anchorage"])
|
||
nor = find_first_event(["nor tendered", "nor accepted"])
|
||
start_pump = find_first_event(["start pumping", "commenced loading"])
|
||
end_pump = find_first_event(["end pumping", "completed loading"])
|
||
hoses_connected = find_first_event(["hose connected"])
|
||
hoses_disconnected = find_first_event(["hose off", "hose disconnected"])
|
||
sailing_time = find_first_event(["sailing", "sb for sailing", "sea pilot on board"])
|
||
|
||
return {
|
||
'arrival_time': arrival,
|
||
'notice_of_readiness_time': nor,
|
||
'start_pumping': start_pump,
|
||
'end_pumping': end_pump,
|
||
'hoses_connected': hoses_connected,
|
||
'hoses_disconnected': hoses_disconnected,
|
||
'sailing_time': sailing_time,
|
||
'events': events,
|
||
}
|
||
|
||
@classmethod
|
||
def validate(cls, shipments):
|
||
super(ShipmentIn, cls).validate(shipments)
|
||
Lot = Pool().get('lot.lot')
|
||
StockMove = Pool().get('stock.move')
|
||
Sof = Pool().get('sof.statement')
|
||
SoFEvent = Pool().get("sof.event")
|
||
for sh in shipments:
|
||
lots = Lot.search(['lot_shipment_in','=',sh.id])
|
||
if not lots:
|
||
if sh.lotqt:
|
||
lots = [sh.lotqt[0].lot_p]
|
||
if lots:
|
||
if sh.state == 'received':
|
||
for lot in lots:
|
||
if lot.lot_type == 'physic':
|
||
if sh.to_location.type == 'storage':
|
||
lot.lot_status = 'stock'
|
||
elif sh.to_location.type == 'supplier':
|
||
lot.lot_status = 'destination'
|
||
elif sh.to_location.type == 'customer':
|
||
lot.lot_status = 'delivered'
|
||
lot.lot_shipment_in = None
|
||
lot.lot_av = 'available'
|
||
Lot.save([lot])
|
||
elif sh.incoming_moves:
|
||
for m in sh.incoming_moves:
|
||
if sh.bl_date:
|
||
m.bldate = sh.bl_date
|
||
if sh.bl_number:
|
||
m.blnumber = sh.bl_number
|
||
if sh.bl_date or sh.bl_number:
|
||
StockMove.save([m])
|
||
for lot in lots:
|
||
if lot.lot_type == 'physic':
|
||
lot.lot_status = 'transit'
|
||
Lot.save([lot])
|
||
#update line valuation
|
||
Pnl = Pool().get('valuation.valuation')
|
||
for lot in lots:
|
||
Pnl.generate(lot.line if lot.line else lot.sale_line)
|
||
if sh.sof:
|
||
for sof in sh.sof:
|
||
if sof.chart:
|
||
sof.laytime_type = 'nor'
|
||
sof.laytime_clause_hour = datetime.time(6,0)
|
||
sof.demurrage_rate = 26000
|
||
sof.pumping_rate = 350
|
||
sof.laytime_type_o = 'nor'
|
||
sof.laytime_clause_hour_o = datetime.time(6,0)
|
||
sof.demurrage_rate_o = 22000
|
||
sof.pumping_rate_o = 300
|
||
sof.timebar_day = 60
|
||
sof.timebar_warn = 0
|
||
Sof.save([sof])
|
||
if sof.sof:
|
||
extracted_sof = sh.extract_sof_data_and_events_from_json(sof_json)
|
||
sof.arrival_time = extracted_sof['arrival_time']
|
||
sof.notice_of_readiness_time = extracted_sof['notice_of_readiness_time']
|
||
sof.start_pumping = extracted_sof['start_pumping']
|
||
sof.end_pumping = extracted_sof['end_pumping']
|
||
sof.hoses_connected = extracted_sof['hoses_connected']
|
||
sof.hoses_disconnected = extracted_sof['hoses_disconnected']
|
||
sof.sailing_time = extracted_sof['sailing_time']
|
||
Sof.save([sof])
|
||
for e in extracted_sof["events"]:
|
||
SoFEvent.create(
|
||
[
|
||
{
|
||
"statement": sof.id,
|
||
"start": e["start"],
|
||
"start_date": e["start_date"],
|
||
"start_time": e["start_time"],
|
||
"end": e["end"],
|
||
"end_date": e["end_date"],
|
||
"end_time": e["end_time"],
|
||
"duration": e["duration"],
|
||
"description": e["description"],
|
||
"deductible": e["deductible"],
|
||
}
|
||
]
|
||
)
|
||
|
||
class FindVessel(Wizard):
|
||
__name__ = 'stock.shipment.in.vf'
|
||
start_state = 'vf'
|
||
vf = StateAction('purchase_trade.url_vessel_finder')
|
||
|
||
def do_vf(self, action):
|
||
action['url'] = self.record.get_vessel_url()
|
||
return action, {}
|
||
|
||
|
||
class ShipmentOut(metaclass=PoolMeta):
|
||
__name__ = 'stock.shipment.out'
|
||
|
||
from_location = fields.Many2One('stock.location', 'From location')
|
||
to_location = fields.Many2One('stock.location', 'To location')
|
||
transport_type = fields.Selection([
|
||
('vessel', 'Vessel'),
|
||
('truck', 'Truck'),
|
||
('other', 'Other'),
|
||
], 'Transport type')
|
||
vessel = fields.Many2One('trade.vessel',"Vessel")
|
||
info = fields.Function(fields.Text("Info"),'get_info')
|
||
fees = fields.One2Many('fee.fee','shipment_in',"Fees")
|
||
lotqt = fields.One2Many('lot.qt','lot_shipment_in',"Lots")
|
||
quantity = fields.Function(fields.Numeric("Quantity"),'get_quantity')
|
||
unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit')
|
||
etl = fields.Date("Est. Loading")
|
||
eta = fields.Date("Est. Arrival")
|
||
etd = fields.Date("Est. Discharge")
|
||
unloaded = fields.Date("Unloaded")
|
||
booking = fields.Char("Booking Nb")
|
||
ref = fields.Char("Reference")
|
||
note = fields.Text("Notes")
|
||
|
||
@classmethod
|
||
def default_transport_type(cls):
|
||
return 'vessel'
|
||
|
||
def get_quantity(self,name=None):
|
||
if self.incoming_moves:
|
||
return sum([(e.quantity if e.quantity else 0) for e in self.incoming_moves])
|
||
|
||
def get_unit(self,name=None):
|
||
if self.incoming_moves:
|
||
return self.incoming_moves[0].unit
|
||
|
||
def get_info(self,name):
|
||
if self.vessel:
|
||
vessel = Pool().get('trade.vessel')(self.vessel)
|
||
return vessel.get_info()
|
||
|
||
from trytond.model import ModelSQL, ModelView, fields
|
||
from trytond.pool import PoolMeta, Pool
|
||
from trytond.wizard import Wizard, StateView, StateAction, StateTransition, Button
|
||
from trytond.transaction import Transaction
|
||
from trytond.report import Report
|
||
from trytond.i18n import gettext
|
||
from trytond.pyson import Eval
|
||
|
||
import datetime
|
||
import io
|
||
import re
|
||
import PyPDF2
|
||
|
||
class StatementOfFacts(ModelSQL, ModelView):
|
||
"Statement of Facts for Vessel Discharge"
|
||
__name__ = 'sof.statement'
|
||
|
||
_event_labels = {
|
||
'notice_of_readiness': 'Notice of Readiness',
|
||
'arrival': 'Arrival',
|
||
'all_fast': 'All Fast',
|
||
'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 = {
|
||
'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': (
|
||
'notice_of_readiness_date', 'notice_of_readiness_hour'),
|
||
'all_fast': ('all_fast_date', 'all_fast_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'),
|
||
'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'),
|
||
'laytime_completed': (
|
||
'laytime_completed_date', 'laytime_completed_hour'),
|
||
}
|
||
|
||
# === A. Informations générales ===
|
||
shipment = fields.Many2One('stock.shipment.in', 'Shipment', required=True, ondelete='CASCADE')
|
||
loading_sequence = fields.Char('Loading Sequence') # ex: "Lot A - Supplier X"
|
||
|
||
# === Termes contractuels ===
|
||
laytime_allowed = fields.Float('Laytime Allowed (hours)')
|
||
notice_of_readiness_time = fields.DateTime('Notice of Readiness Time')
|
||
|
||
laytime_type = fields.Selection([
|
||
('nor', 'NOR +'),
|
||
(None, 'Laytime start'),
|
||
], 'Laytime clause')
|
||
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_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 +'),
|
||
(None, 'Laytime start'),
|
||
], 'Laytime clause')
|
||
laytime_clause_hour_o = fields.Time("")
|
||
laytime_start_o = fields.DateTime('Laytime Start')
|
||
laytime_end_o = fields.DateTime('Laytime End')
|
||
demurrage_rate_o = fields.Numeric('Demurrage Rate ($/day)', digits=(16, 2))
|
||
pumping_rate_o = fields.Float('Pumping Rate (MT/hour)')
|
||
|
||
timebar_day = fields.Integer("TimeBar")
|
||
timebar_warn = fields.Integer("Warning TimeBar")
|
||
charter_party = fields.Many2One('stock.charter.party', "Charter Party")
|
||
applied_conditions = fields.Function(
|
||
fields.One2Many('charter.condition', '', "Available Conditions"),
|
||
'on_change_with_applied_conditions', setter='set_applied_conditions')
|
||
applied_condition = fields.Many2One(
|
||
'charter.condition', "Applied Condition",
|
||
domain=[('id', 'in', Eval('applied_conditions', []))],
|
||
depends=['applied_conditions'])
|
||
owner_charter_conditions = fields.Function(
|
||
fields.One2Many('charter.condition', '', "Owner Conditions"),
|
||
'get_owner_charter_conditions')
|
||
purchase_charter_conditions = fields.Function(
|
||
fields.One2Many('charter.condition', '', "Supplier Conditions"),
|
||
'get_purchase_charter_conditions')
|
||
sale_charter_conditions = fields.Function(
|
||
fields.One2Many('charter.condition', '', "Customer Conditions"),
|
||
'get_sale_charter_conditions')
|
||
chart = fields.Many2One('document.incoming',"Charter Party Terms")
|
||
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')
|
||
arrival_date = fields.Date('Arrival Date')
|
||
arrival_hour = fields.Time('Arrival Time')
|
||
all_fast = fields.DateTime('All Fast')
|
||
all_fast_date = fields.Date('All Fast Date')
|
||
all_fast_hour = fields.Time('All Fast 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')
|
||
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')
|
||
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)
|
||
compensation_type = fields.Selection([
|
||
('demurrage', 'Demurrage'),
|
||
('despatch', 'Despatch'),
|
||
(None, 'No Compensation'),
|
||
], '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')
|
||
|
||
@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):
|
||
self._set_default_applied_condition()
|
||
|
||
@fields.depends('shipment', 'charter_party', 'applied_condition')
|
||
def on_change_charter_party(self):
|
||
self._set_default_applied_condition()
|
||
|
||
def _set_default_applied_condition(self):
|
||
condition_ids = self.on_change_with_applied_conditions()
|
||
applied_condition_id = getattr(self.applied_condition, 'id', None)
|
||
if applied_condition_id in condition_ids:
|
||
return
|
||
self.applied_condition = None
|
||
if len(condition_ids) == 1:
|
||
self.applied_condition = condition_ids[0]
|
||
|
||
def get_owner_charter_conditions(self, name=None):
|
||
if self.shipment:
|
||
return self.shipment.get_owner_charter_conditions(name)
|
||
if self.charter_party:
|
||
return ShipmentIn._unique_condition_ids(self.charter_party.conditions)
|
||
return []
|
||
|
||
def get_purchase_charter_conditions(self, name=None):
|
||
if self.shipment:
|
||
return self.shipment.get_purchase_charter_conditions(name)
|
||
return []
|
||
|
||
def get_sale_charter_conditions(self, name=None):
|
||
if self.shipment:
|
||
return self.shipment.get_sale_charter_conditions(name)
|
||
return []
|
||
|
||
@fields.depends('shipment', 'charter_party')
|
||
def on_change_with_applied_conditions(self, name=None):
|
||
return ShipmentIn._unique_condition_ids(
|
||
self.get_owner_charter_conditions(name)
|
||
+ self.get_purchase_charter_conditions(name)
|
||
+ self.get_sale_charter_conditions(name))
|
||
|
||
def get_applied_conditions(self, name=None):
|
||
return self.on_change_with_applied_conditions(name)
|
||
|
||
@classmethod
|
||
def set_applied_conditions(cls, statements, name, value):
|
||
pass
|
||
|
||
@staticmethod
|
||
def _rate_category(rate):
|
||
rate_type = getattr(rate, 'rate_type', None)
|
||
category = getattr(rate_type, 'category', None)
|
||
if category:
|
||
return category
|
||
label = ' '.join(filter(None, [
|
||
getattr(rate_type, 'name', None),
|
||
getattr(rate, 'description', None),
|
||
])).lower()
|
||
for category in ('demurrage', 'despatch', 'pumping'):
|
||
if category in label:
|
||
return category
|
||
|
||
def _condition_rate(self, category):
|
||
condition = getattr(self, 'applied_condition', None)
|
||
for rate in getattr(condition, 'rates', None) or []:
|
||
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:
|
||
allowed = Decimal(str(condition.laytime_allowed))
|
||
if condition.laytime_unit in {
|
||
'days', 'wwd', 'wwd_shex', 'wwd_shinc'}:
|
||
allowed *= Decimal(24)
|
||
return float(allowed)
|
||
|
||
pumping_rate = self._applied_rate('pumping')
|
||
if pumping_rate:
|
||
return round(float(Decimal(str(quantity or 0)) / pumping_rate), 2)
|
||
|
||
def _applied_turn_delta(self):
|
||
condition = getattr(self, 'applied_condition', None)
|
||
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:
|
||
return self._offset_delta(value, unit)
|
||
|
||
if self.laytime_clause_hour:
|
||
t = self.laytime_clause_hour
|
||
elif self.laytime_clause_hour_o:
|
||
t = self.laytime_clause_hour_o
|
||
else:
|
||
return datetime.timedelta(0)
|
||
return datetime.timedelta(
|
||
hours=t.hour, minutes=t.minute, seconds=t.second)
|
||
|
||
@staticmethod
|
||
def _offset_delta(value, unit):
|
||
if value is None:
|
||
return datetime.timedelta(0)
|
||
hours = Decimal(str(value))
|
||
if unit == 'days':
|
||
hours *= Decimal(24)
|
||
return datetime.timedelta(hours=float(hours))
|
||
|
||
def _condition_event_datetime(self, event):
|
||
return {
|
||
'notice_of_readiness': self.notice_of_readiness_time,
|
||
'arrival': self.arrival_time,
|
||
'all_fast': self.all_fast,
|
||
'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)
|
||
|
||
def _laytime_start_rule_candidates(self):
|
||
condition = getattr(self, 'applied_condition', None)
|
||
rules = getattr(condition, 'laytime_start_rules', None) or []
|
||
candidates = []
|
||
for rule in sorted(rules, key=lambda r: getattr(r, 'sequence', 0) or 0):
|
||
base = self._condition_event_datetime(getattr(rule, 'event', None))
|
||
if not base:
|
||
continue
|
||
candidates.append(base + self._offset_delta(
|
||
getattr(rule, 'offset', None),
|
||
getattr(rule, 'offset_unit', None)))
|
||
return candidates
|
||
|
||
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:
|
||
return min(candidates) if rule == 'earliest_of' else max(candidates)
|
||
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):
|
||
base = self._laytime_start_base()
|
||
if not base:
|
||
return
|
||
condition = getattr(self, 'applied_condition', None)
|
||
rule = getattr(condition, 'laytime_start_rule', 'simple')
|
||
if rule in {'earliest_of', 'latest_of'}:
|
||
return base
|
||
return base + self._applied_turn_delta()
|
||
|
||
def _applied_laytime_end(self):
|
||
condition = getattr(self, 'applied_condition', None)
|
||
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):
|
||
balance = Decimal(str(self.laytime_balance or 0))
|
||
if balance < 0:
|
||
self.compensation_type = 'demurrage'
|
||
rate = self._applied_rate('demurrage') or Decimal(0)
|
||
return round(balance * rate / Decimal(24), 2)
|
||
if balance > 0:
|
||
rate = self._applied_rate('despatch')
|
||
if rate is not None:
|
||
self.compensation_type = 'despatch'
|
||
return round(balance * rate / Decimal(24), 2)
|
||
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)
|
||
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)
|
||
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_start_rule_text(self):
|
||
condition = getattr(self, 'applied_condition', None)
|
||
rule = getattr(condition, 'laytime_start_rule', 'simple')
|
||
if rule == 'earliest_of':
|
||
return 'Earliest Of'
|
||
if rule == 'latest_of':
|
||
return 'Latest Of'
|
||
start_event = getattr(condition, 'laytime_start_event', None)
|
||
return self._event_labels.get(
|
||
start_event, self._event_labels['notice_of_readiness'])
|
||
|
||
def _calculation_start_candidates_text(self):
|
||
condition = getattr(self, 'applied_condition', None)
|
||
rules = getattr(condition, 'laytime_start_rules', None) or []
|
||
parts = []
|
||
for rule in sorted(rules, key=lambda r: getattr(r, 'sequence', 0) or 0):
|
||
label = self._event_labels.get(getattr(rule, 'event', None), '')
|
||
offset = getattr(rule, 'offset', None)
|
||
if offset is not None:
|
||
label = '%s + %s %s' % (
|
||
label,
|
||
self._format_calculation_number(offset),
|
||
getattr(rule, 'offset_unit', None) or 'hours')
|
||
if label:
|
||
parts.append(label)
|
||
return '; '.join(parts)
|
||
|
||
def _calculation_rate_text(self, category):
|
||
rate = self._applied_rate(category)
|
||
if rate is not None:
|
||
return '%s / day' % self._format_calculation_number(rate)
|
||
return ''
|
||
|
||
def _calculation_pumping_rate_text(self):
|
||
rate = self._applied_rate('pumping')
|
||
if rate is not None:
|
||
return '%s MT/hour' % self._format_calculation_number(rate)
|
||
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._calculation_start_rule_text()
|
||
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()
|
||
|
||
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'
|
||
|
||
statement = fields.Many2One('sof.statement', "Statement", ondelete='CASCADE')
|
||
|
||
start = fields.DateTime("Start Time")
|
||
end = fields.DateTime("End Time")
|
||
|
||
start_date = fields.Date("Start Date")
|
||
start_time = fields.Time("Start Time (H)")
|
||
end_date = fields.Date("End Date")
|
||
end_time = fields.Time("End Time (H)")
|
||
|
||
duration = fields.Float("Duration")
|
||
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"""
|
||
__name__ = 'import.s'
|
||
|
||
pdf = fields.Binary('Statement of Facts PDF', required=True, filename='filename')
|
||
filename = fields.Char('SoF Filename',readonly=True)
|
||
charter = fields.Binary('Charter Party File', required=True, filename='charter_name')
|
||
charter_name = fields.Char('Charter Filename',readonly=True)
|
||
|
||
class ImportSoFWizard(Wizard):
|
||
"Import Statement of Facts Wizard"
|
||
__name__ = 'sof.import'
|
||
|
||
start = StateTransition()
|
||
|
||
sof = StateView('import.s', 'purchase_trade.view_sof_import_start', [
|
||
Button('Cancel', 'end', 'tryton-cancel'),
|
||
Button('Import', 'processing', 'tryton-ok')
|
||
])
|
||
|
||
processing = StateTransition()
|
||
|
||
def transition_start(self):
|
||
return 'sof'
|
||
|
||
def parse_pdf_text(self, binary_data):
|
||
text = ""
|
||
with io.BytesIO(binary_data) as pdf_file:
|
||
reader = PyPDF2.PdfReader(pdf_file)
|
||
for page in reader.pages:
|
||
text += page.extract_text()
|
||
return text
|
||
|
||
def parse_charter_text(self, binary_data):
|
||
try:
|
||
return binary_data.decode('utf-8')
|
||
except Exception:
|
||
return ''
|
||
|
||
def extract_fields_from_text(self,text):
|
||
def extract(pattern):
|
||
match = re.search(pattern, text)
|
||
return match.group(1).strip() if match else None
|
||
|
||
vessel = extract(r"Vessel Name:\s*(.+)")
|
||
port = extract(r"Port:\s*(.+)")
|
||
berth = extract(r"Berth:\s*(.+)")
|
||
cargo = extract(r"Cargo:\s*(.+)")
|
||
charter_ref = extract(r"Charter Party Ref:\s*(.+)")
|
||
laytime_allowed = extract(r"Laytime Allowed:\s*(\d+)\s*hours")
|
||
demurrage = extract(r"Demurrage Rate:\s*\$(\d[\d,]*)/day")
|
||
despatch = extract(r"Despatch Rate:\s*\$(\d[\d,]*)/day")
|
||
laytime_start_raw = extract(r"Laytime Start:\s*([\d\-: ]+)")
|
||
laytime_end_raw = extract(r"Laytime End:\s*([\d\-: ]+)")
|
||
quantity_raw = extract(r"Quantity:\s*([\d,]+)\s*MT")
|
||
quantity = float(quantity_raw.replace(',', '')) if quantity_raw else None
|
||
|
||
laytime_start = datetime.datetime.strptime(laytime_start_raw, "%Y-%m-%d %H:%M").date() if laytime_start_raw else None
|
||
laytime_end = datetime.datetime.strptime(laytime_end_raw, "%Y-%m-%d %H:%M").date() if laytime_end_raw else None
|
||
|
||
return {
|
||
'vessel_name': vessel,
|
||
'port': port,
|
||
'berth': berth,
|
||
'cargo': cargo,
|
||
'quantity': quantity,
|
||
'charter_ref': charter_ref,
|
||
'laytime_allowed': float(laytime_allowed) if laytime_allowed else None,
|
||
'laytime_start': laytime_start,
|
||
'laytime_end': laytime_end,
|
||
'demurrage_rate': float(demurrage.replace(',', '')) if demurrage else None,
|
||
'despatch_rate': float(despatch.replace(',', '')) if despatch else None,
|
||
}
|
||
|
||
def extract_sof_events(self, text):
|
||
"""
|
||
Extrait les événements datés à partir du texte brut du SOF PDF.
|
||
Format attendu (comme dans ton fichier) :
|
||
'15.05.2024 13:00 - 15.05.2024 18:00 Loading stopped due to rain'
|
||
"""
|
||
pattern = re.compile(
|
||
r'(\d{2}\.\d{2}\.\d{4})\s+(\d{2}:\d{2})\s*-\s*(\d{2}\.\d{2}\.\d{4})\s+(\d{2}:\d{2})\s+(.+)',
|
||
re.MULTILINE
|
||
)
|
||
events = []
|
||
logger.info("TEXT FOR EVENTS:\n%s", text)
|
||
matchs = pattern.finditer(text)
|
||
logger.info("MATCHS:%s",matchs)
|
||
for match in matchs:
|
||
logger.debug("MATCH: %s | %s -> %s", match.group(5), match.group(1), match.group(3))
|
||
start = datetime.datetime.strptime(f"{match.group(1)} {match.group(2)}", "%d.%m.%Y %H:%M")
|
||
end = datetime.datetime.strptime(f"{match.group(3)} {match.group(4)}", "%d.%m.%Y %H:%M")
|
||
description = match.group(5).strip()
|
||
duration = (end - start).total_seconds() / 3600
|
||
deductible = any(word in description.lower() for word in ['rain', 'waiting', 'no loading'])
|
||
events.append({
|
||
'start': start,
|
||
'end': end,
|
||
'duration': duration,
|
||
'description': description,
|
||
'deductible': deductible,
|
||
})
|
||
return events
|
||
|
||
def extract_sof_data_and_events(self, text):
|
||
def extract(pattern, fmt=None, default=None):
|
||
match = re.search(pattern, text, re.IGNORECASE)
|
||
if not match:
|
||
logger.warning("Pattern not found: %s", pattern)
|
||
return default
|
||
raw = match.group(1).strip()
|
||
try:
|
||
return datetime.datetime.strptime(raw, fmt) if fmt else raw
|
||
except Exception:
|
||
logger.error("Failed to parse date: %s", raw)
|
||
return default
|
||
|
||
# Nouvelles regex plus souples
|
||
arrival = extract(r"vessel.*arrived.*?(\d{2}\.\d{2}\.\d{4} \d{2}:\d{2})", "%d.%m.%Y %H:%M")
|
||
nor = extract(r"nor.*tendered.*?(\d{2}\.\d{2}\.\d{4} \d{2}:\d{2})", "%d.%m.%Y %H:%M")
|
||
start_pump = extract(r"start.*pumping.*?(\d{2}\.\d{2}\.\d{4} \d{2}:\d{2})", "%d.%m.%Y %H:%M")
|
||
end_pump = extract(r"end.*pumping.*?(\d{2}\.\d{2}\.\d{4} \d{2}:\d{2})", "%d.%m.%Y %H:%M")
|
||
|
||
logger.info("ARRIVAL: %s", arrival)
|
||
logger.info("NOR: %s", nor)
|
||
logger.info("START_PUMP: %s", start_pump)
|
||
logger.info("END_PUMP: %s", end_pump)
|
||
|
||
events = self.extract_sof_events(text)
|
||
|
||
return {
|
||
'arrival_time': arrival,
|
||
'notice_of_readiness_time': nor,
|
||
'start_pumping': start_pump,
|
||
'end_pumping': end_pump,
|
||
'hoses_connected': None,
|
||
'hoses_disconnected': None,
|
||
'sailing_time': None,
|
||
'events': events,
|
||
}
|
||
|
||
def extract_sof_data_and_events_from_json(self, sof_json):
|
||
def parse_datetime(date_str, time_str):
|
||
return datetime.datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %H:%M")
|
||
|
||
def find_first_event(keywords):
|
||
for item in sof_json:
|
||
if any(kw.lower() in item['event'].lower() for kw in keywords):
|
||
return parse_datetime(item['date'], item['time'])
|
||
return None
|
||
|
||
def find_last_event(keywords):
|
||
for item in reversed(sof_json):
|
||
if any(kw.lower() in item['event'].lower() for kw in keywords):
|
||
return parse_datetime(item['date'], item['time'])
|
||
return None
|
||
|
||
events = []
|
||
for item in sof_json:
|
||
try:
|
||
start = parse_datetime(item['date'], item['time'])
|
||
end_str = item.get("end_time")
|
||
end = datetime.datetime.strptime(end_str, "%Y-%m-%d %H:%M") if end_str else None
|
||
|
||
description = item["event"]
|
||
duration = (end - start).total_seconds() / 3600 if end else 0
|
||
deductible = any(word in description.lower() for word in ['rain', 'waiting', 'no loading'])
|
||
|
||
events.append({
|
||
'start': start,
|
||
'start_date': start.date(),
|
||
'start_time': start.time(),
|
||
'end': end,
|
||
'end_date': end.date() if end else None,
|
||
'end_time': end.time() if end else None,
|
||
'duration': duration,
|
||
'description': description,
|
||
'deductible': deductible,
|
||
})
|
||
except Exception as e:
|
||
logger.error("Error parsing SOF event: %s | %s", item, e)
|
||
|
||
# Extraction par mots-clés (inchangé)
|
||
arrival = find_first_event(["arrived", "anchor at", "proceed to anchorage"])
|
||
nor = find_first_event(["nor tendered", "nor accepted"])
|
||
start_pump = find_first_event(["start pumping", "commenced loading"])
|
||
end_pump = find_first_event(["end pumping", "completed loading"])
|
||
hoses_connected = find_first_event(["hose connected"])
|
||
hoses_disconnected = find_first_event(["hose off", "hose disconnected"])
|
||
sailing_time = find_first_event(["sailing", "sb for sailing", "sea pilot on board"])
|
||
|
||
return {
|
||
'arrival_time': arrival,
|
||
'notice_of_readiness_time': nor,
|
||
'start_pumping': start_pump,
|
||
'end_pumping': end_pump,
|
||
'hoses_connected': hoses_connected,
|
||
'hoses_disconnected': hoses_disconnected,
|
||
'sailing_time': sailing_time,
|
||
'events': events,
|
||
}
|
||
|
||
def transition_processing(self):
|
||
pool = Pool()
|
||
Statement = pool.get("sof.statement")
|
||
SoFEvent = pool.get("sof.event")
|
||
|
||
# Lecture des documents
|
||
# text_sof = self.parse_pdf_text(self.sof.pdf)
|
||
# logger.info("SOF TEXT:\n%s", text_sof)
|
||
text_charter = self.parse_charter_text(self.sof.charter)
|
||
|
||
# Extraction des champs
|
||
# extracted_main = self.extract_fields_from_text(text_sof)
|
||
# extracted_sof = self.extract_sof_data_and_events(text_sof)
|
||
sof_json = [
|
||
{
|
||
"date": "2025-02-02",
|
||
"time": "19:00",
|
||
"event": "S/B FOR ARRIVAL - PROCEED TO CAK NO.2 ANCHORAGE",
|
||
"end_time": "2025-02-03 21:30",
|
||
"duration": {"iso_8601": "PT26H30M", "text": "26h 30m"},
|
||
},
|
||
{
|
||
"date": "2025-02-02",
|
||
"time": "21:30",
|
||
"event": "DROP ANCHOR AT CAK NO.2 ANCHOR / ARR. CAN NOR TENDERED",
|
||
"end_time": "2025-02-02 21:40",
|
||
"duration": {"iso_8601": "-PT23H50M", "text": "10m"},
|
||
},
|
||
{
|
||
"date": "2025-02-02",
|
||
"time": "21:40",
|
||
"event": "BROUGHT UP ANCHOR/F.W.E",
|
||
"end_time": "2025-02-03 07:20",
|
||
"duration": {"iso_8601": "PT9H40M", "text": "9h 40m"},
|
||
},
|
||
{
|
||
"date": "2025-02-03",
|
||
"time": "07:20",
|
||
"event": "S.B.E. FOR SHIFTING",
|
||
"end_time": "2025-02-03 07:45",
|
||
"duration": {"iso_8601": "PT0H25M", "text": "25m"},
|
||
},
|
||
{
|
||
"date": "2025-02-03",
|
||
"time": "07:45",
|
||
"event": "ANCHOR AWEIGHT",
|
||
"end_time": "2025-02-03 10:45",
|
||
"duration": {"iso_8601": "PT3H0M", "text": "3h"},
|
||
},
|
||
{
|
||
"date": "2025-02-03",
|
||
"time": "10:45",
|
||
"event": "SEA PILOT ON BOARD",
|
||
"end_time": "2025-02-03 14:00",
|
||
"duration": {"iso_8601": "PT3H15M", "text": "3h 15m"},
|
||
},
|
||
{
|
||
"date": "2025-02-03",
|
||
"time": "14:00",
|
||
"event": "EXCHANGED SEA PILOT TO RIVER PILOT",
|
||
"end_time": "2025-02-03 17:45",
|
||
"duration": {"iso_8601": "PT3H45M", "text": "3h 45m"},
|
||
},
|
||
{
|
||
"date": "2025-02-03",
|
||
"time": "17:45",
|
||
"event": "DROP ANCHOR AT NANTONS DANGEROUS ANCHORAGE",
|
||
"end_time": "2025-02-03 18:00",
|
||
"duration": {"iso_8601": "PT0H15M", "text": "15m"},
|
||
},
|
||
{
|
||
"date": "2025-02-03",
|
||
"time": "18:00",
|
||
"event": "BROUGHT UP ANCHOR/F.W.E/PILOT OFF",
|
||
"end_time": "2025-02-04 06:30",
|
||
"duration": {"iso_8601": "PT12H30M", "text": "12h 30m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "06:30",
|
||
"event": "S.B.E. FOR BERTHING",
|
||
"end_time": "2025-02-04 07:10",
|
||
"duration": {"iso_8601": "PT0H40M", "text": "40m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "07:10",
|
||
"event": "PILOT ON BOARD",
|
||
"end_time": "2025-02-04 07:15",
|
||
"duration": {"iso_8601": "PT0H5M", "text": "5m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "07:15",
|
||
"event": "ANCHOR AWEIGHT",
|
||
"end_time": "2025-02-04 09:20",
|
||
"duration": {"iso_8601": "PT2H5M", "text": "2h 5m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "09:20",
|
||
"event": "FWD TUG MADE FAST",
|
||
"end_time": "2025-02-04 09:35",
|
||
"duration": {"iso_8601": "PT0H15M", "text": "15m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "09:35",
|
||
"event": "FIRST LINE ASHORE",
|
||
"end_time": "2025-02-04 08:50",
|
||
"duration": {
|
||
"iso_8601": "-PT0H45M",
|
||
"text": "Incohérence temporelle",
|
||
"alert": "Vérifier l'heure de TUG OFF",
|
||
},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "08:50",
|
||
"event": "TUG OFF",
|
||
"end_time": "2025-02-04 09:55",
|
||
"duration": {"iso_8601": "PT1H5M", "text": "1h 5m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "09:55",
|
||
"event": "ALL LINE MADE FAST/F.W.E / NOR ACCEPTED",
|
||
"end_time": "2025-02-04 10:15",
|
||
"duration": {"iso_8601": "PT0H20M", "text": "20m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "10:15",
|
||
"event": "GANGWAY DOWN/PILOT OFF",
|
||
"end_time": "2025-02-04 10:20",
|
||
"duration": {"iso_8601": "PT0H5M", "text": "5m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "10:20",
|
||
"event": "AGENT / BAMBRIATION ON BOARD",
|
||
"end_time": "2025-02-04 11:10",
|
||
"duration": {"iso_8601": "PT0H50M", "text": "50m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "11:10",
|
||
"event": "FREE-PRACTIQUE QUANTED",
|
||
"end_time": "2025-02-04 11:40",
|
||
"duration": {"iso_8601": "PT0H30M", "text": "30m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "11:40",
|
||
"event": "SURVEYOR & LOADING MASTER ON BOARD",
|
||
"end_time": "2025-02-04 11:40",
|
||
"duration": {
|
||
"iso_8601": "PT0H0M",
|
||
"text": "0m",
|
||
"note": "Début de SAFETY MEETING",
|
||
},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "11:40",
|
||
"event": "SAFETY MEETING",
|
||
"end_time": "2025-02-04 12:20",
|
||
"duration": {"iso_8601": "PT0H40M", "text": "40m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "11:45",
|
||
"event": "TANK INSPECTION",
|
||
"end_time": "2025-02-04 12:30",
|
||
"duration": {"iso_8601": "PT0H45M", "text": "45m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "13:10",
|
||
"event": 'CARGO HOSE CONNECTED (3 x 8")',
|
||
"end_time": "2025-02-04 13:25",
|
||
"duration": {"iso_8601": "PT0H15M", "text": "15m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "13:25",
|
||
"event": "LEAKAGE TESTED",
|
||
"end_time": "2025-02-04 13:45",
|
||
"duration": {"iso_8601": "PT0H20M", "text": "20m"},
|
||
},
|
||
{
|
||
"date": "2025-02-04",
|
||
"time": "14:15",
|
||
"event": "COMMENCED LOADING SULPHURIC ACID",
|
||
"end_time": "2025-02-05 08:50",
|
||
"duration": {"iso_8601": "PT18H35M", "text": "18h 35m"},
|
||
},
|
||
{
|
||
"date": "2025-02-05",
|
||
"time": "08:50",
|
||
"event": "COMPLETED LOADING SULPHURIC ACID",
|
||
"end_time": "2025-02-05 09:00",
|
||
"duration": {"iso_8601": "PT0H10M", "text": "10m"},
|
||
},
|
||
{
|
||
"date": "2025-02-05",
|
||
"time": "08:50",
|
||
"event": "AIR BLOWING",
|
||
"end_time": "2025-02-05 09:00",
|
||
"duration": {"iso_8601": "PT0H10M", "text": "10m"},
|
||
},
|
||
{
|
||
"date": "2025-02-05",
|
||
"time": "09:00",
|
||
"event": "VILLAGING, SAMPLING AND CALCULATION",
|
||
"end_time": "2025-02-05 11:00",
|
||
"duration": {"iso_8601": "PT2H0M", "text": "2h"},
|
||
},
|
||
{
|
||
"date": "2025-02-05",
|
||
"time": "10:40",
|
||
"event": 'HOSE OFF (308")',
|
||
"end_time": "2025-02-05 11:40",
|
||
"duration": {"iso_8601": "PT1H0M", "text": "1h"},
|
||
},
|
||
{
|
||
"date": "2025-02-05",
|
||
"time": "11:40",
|
||
"event": "CARGO DOCUMENT COMPLETED",
|
||
"end_time": "2025-02-05 11:40",
|
||
"duration": {"iso_8601": "PT0H0M", "text": "Événement final"},
|
||
},
|
||
]
|
||
extracted_sof = self.extract_sof_data_and_events_from_json(sof_json)
|
||
|
||
# Création du Statement principal
|
||
statement = Statement.create(
|
||
[
|
||
{
|
||
"shipment": self.records[0].id,
|
||
"document": self.sof.pdf,
|
||
"document_name": self.sof.filename,
|
||
"charter_text": text_charter,
|
||
# **extracted_main,
|
||
**{k: v for k, v in extracted_sof.items() if k != "events"},
|
||
}
|
||
]
|
||
)[0]
|
||
|
||
# Création des événements liés
|
||
for e in extracted_sof["events"]:
|
||
SoFEvent.create(
|
||
[
|
||
{
|
||
"statement": statement.id,
|
||
"start": e["start"],
|
||
"start_date": e["start_date"],
|
||
"start_time": e["start_time"],
|
||
"end": e["end"],
|
||
"end_date": e["end_date"],
|
||
"end_time": e["end_time"],
|
||
"duration": e["duration"],
|
||
"description": e["description"],
|
||
"deductible": e["deductible"],
|
||
}
|
||
]
|
||
)
|
||
|
||
return "end"
|
||
|
||
class SofUpdate(Wizard):
|
||
"Update shipment"
|
||
__name__ = "sof.update"
|
||
|
||
start = StateTransition()
|
||
|
||
def transition_start(self):
|
||
Shipment = Pool().get('stock.shipment.in')
|
||
Sof = Pool().get('sof.statement')
|
||
for r in self.records:
|
||
sh = Shipment(r.id)
|
||
sof = Sof()
|
||
sof.chart = 6
|
||
sof.sof = 5
|
||
sh.sof = [sof]
|
||
Shipment.save([sh])
|
||
|
||
return 'end'
|
||
|
||
class AccountMoveLine(metaclass=PoolMeta):
|
||
"Account move line"
|
||
__name__ = 'account.move.line'
|
||
revaluate = fields.Integer("Linked line")
|
||
lot = fields.Many2One('lot.lot',"Lot")
|
||
fee = fields.Many2One('fee.fee',"Fee")
|
||
|
||
class AccountMove(metaclass=PoolMeta):
|
||
"Account move"
|
||
__name__ = 'account.move'
|
||
|
||
def IsBankMove(self):
|
||
PM = Pool().get('account.invoice.payment.method')
|
||
if self.lines:
|
||
for m in self.lines:
|
||
pm = PM.search(['debit_account','=',m.account])
|
||
if pm:
|
||
return True
|
||
else:
|
||
pm = PM.search(['credit_account','=',m.account])
|
||
if pm:
|
||
return True
|
||
return False
|
||
|
||
class Account(metaclass=PoolMeta):
|
||
"Account"
|
||
__name__ = 'account.account'
|
||
|
||
def get_foreign_currencies(self):
|
||
Currency = Pool().get('currency.currency')
|
||
cursor = Transaction().connection.cursor()
|
||
cursor.execute("""
|
||
SELECT DISTINCT second_currency
|
||
FROM account_move_line
|
||
WHERE account = %s
|
||
AND second_currency IS NOT NULL
|
||
""", [self.id])
|
||
|
||
company_currency = self.company.currency
|
||
currencies = []
|
||
for (currency_id,) in cursor.fetchall():
|
||
cur = Currency(currency_id)
|
||
if cur != company_currency:
|
||
currencies.append(cur)
|
||
|
||
return currencies
|
||
|
||
def get_lines_curr(self,curr):
|
||
MoveLine = Pool().get('account.move.line')
|
||
lines = MoveLine.search([('account','=',self.id),('second_currency','=',curr.id),('revaluate','=',None),('reconciliation','=',None)])
|
||
return lines
|
||
|
||
def get_all_lines_curr(self,curr):
|
||
MoveLine = Pool().get('account.move.line')
|
||
lines = MoveLine.search([('account','=',self.id)])#,('second_currency','=',curr.id)])
|
||
return lines
|
||
|
||
def get_lines_curr_reval(self,id):
|
||
MoveLine = Pool().get('account.move.line')
|
||
lines = MoveLine.search([('account','=',self.id),('revaluate','=',id)])
|
||
return lines
|
||
|
||
def revaluate_fx(self,reval_date,after):
|
||
Currency = Pool().get('currency.currency')
|
||
MoveLine = Pool().get('account.move.line')
|
||
Configuration = Pool().get('account.configuration')
|
||
configuration = Configuration(1)
|
||
Period = Pool().get('account.period')
|
||
Move = Pool().get('account.move')
|
||
currencies = self.get_foreign_currencies()
|
||
logger.info("REVALUATE_ACCOUNT:%s",self.id)
|
||
if currencies:
|
||
for curr in currencies:
|
||
logger.info("REVALUATE_CURR:%s",curr.id)
|
||
if self.fx_eval_detail:
|
||
lines = self.get_lines_curr(curr)
|
||
logger.info("REVALUATE_DETAIL:%s",lines)
|
||
if lines:
|
||
for l in lines:
|
||
reval_lines = self.get_lines_curr_reval(l.id)
|
||
logger.info("REVALUATE_REVAL_LINES:%s",l)
|
||
with Transaction().set_context(date=reval_date):
|
||
amount_converted = Currency.compute(curr,l.amount_second_currency, self.company.currency)
|
||
amount_reval = sum([(e.debit-e.credit) for e in reval_lines])
|
||
to_add = amount_converted - ((l.debit-l.credit) + amount_reval)
|
||
if to_add != 0:
|
||
second_amount_to_add = Currency.compute(self.company.currency,to_add,curr)
|
||
line = MoveLine()
|
||
line.account = self.id
|
||
line.revaluate = l.id
|
||
line.credit = -to_add if to_add < 0 else 0
|
||
line.debit = to_add if to_add > 0 else 0
|
||
line.lot = l.lot
|
||
line.party = l.party
|
||
line.maturity_date = reval_date
|
||
logger.info("REVALUATE_ACC:%s",line)
|
||
line_ = MoveLine()
|
||
if to_add < 0:
|
||
line_.account = configuration.get_multivalue('currency_exchange_credit_account', company=self.company.id)
|
||
else:
|
||
line_.account = configuration.get_multivalue('currency_exchange_debit_account', company=self.company.id)
|
||
line_.credit = to_add if to_add > 0 else 0
|
||
line_.debit = -to_add if to_add < 0 else 0
|
||
line_.lot = l.lot
|
||
line_.maturity_date = reval_date
|
||
logger.info("REVALUATE_EX:%s",line_)
|
||
move = Move()
|
||
move.journal = configuration.get_multivalue('currency_exchange_journal', company=self.company.id)
|
||
period = Period.find(self.company, date=reval_date)
|
||
move.date = reval_date
|
||
move.period = period
|
||
#move.origin = forex
|
||
move.company = self.company
|
||
move.lines = [line,line_]
|
||
Move.save([move])
|
||
all_lines = self.get_all_lines_curr(curr)
|
||
sum_second_amount = sum([e.amount_second_currency for e in all_lines if e.amount_second_currency])
|
||
logger.info("REVALUATE_SUM_SECOND:%s",sum_second_amount)
|
||
if sum_second_amount == Decimal(0):
|
||
base_curr_sum = sum([(e.debit-e.credit) for e in all_lines])
|
||
if abs(base_curr_sum) > 0:
|
||
line = MoveLine()
|
||
line.account = self.id
|
||
line.party = 1904 #TBN
|
||
line.credit = base_curr_sum if base_curr_sum > 0 else 0
|
||
line.debit = -base_curr_sum if base_curr_sum < 0 else 0
|
||
line.maturity_date = reval_date
|
||
logger.info("REVALUATE_ACC_:%s",line)
|
||
line_ = MoveLine()
|
||
if base_curr_sum < 0:
|
||
line_.account = configuration.get_multivalue('currency_exchange_credit_account', company=self.company.id)
|
||
else:
|
||
line_.account = configuration.get_multivalue('currency_exchange_debit_account', company=self.company.id)
|
||
line_.credit = -base_curr_sum if base_curr_sum < 0 else 0
|
||
line_.debit = base_curr_sum if base_curr_sum > 0 else 0
|
||
line_.maturity_date = reval_date
|
||
logger.info("REVALUATE_EX:%s",line_)
|
||
move = Move()
|
||
move.journal = configuration.get_multivalue('currency_exchange_journal', company=self.company.id)
|
||
period = Period.find(self.company, date=reval_date)
|
||
move.date = reval_date
|
||
move.period = period
|
||
#move.origin = forex
|
||
move.company = self.company
|
||
move.lines = [line,line_]
|
||
Move.save([move])
|
||
|
||
else:
|
||
lines = self.get_all_lines_curr(curr)
|
||
with Transaction().set_context(date=reval_date):
|
||
sum_second_amount = sum([e.amount_second_currency for e in lines if e.amount_second_currency])
|
||
logger.info("REVALUATE_SECOND:%s",sum_second_amount)
|
||
amount_converted = Currency.compute(curr,sum_second_amount, self.company.currency)
|
||
logger.info("REVALUATE_SECOND2:%s",amount_converted)
|
||
total_amount = sum([(e.debit-e.credit) for e in lines])
|
||
logger.info("REVALUATE_SECOND3:%s",total_amount)
|
||
to_add = amount_converted - total_amount
|
||
if to_add != 0:
|
||
second_amount_to_add = Currency.compute(self.company.currency,to_add,curr)
|
||
line = MoveLine()
|
||
line.account = self.id
|
||
line.credit = -to_add if to_add < 0 else 0
|
||
line.debit = to_add if to_add > 0 else 0
|
||
line.party = self.company.party.id
|
||
line.maturity_date = reval_date
|
||
logger.info("REVALUATE_ACC:%s",line)
|
||
line_ = MoveLine()
|
||
if to_add < 0:
|
||
line_.account = configuration.get_multivalue('currency_exchange_credit_account', company=self.company.id)
|
||
else:
|
||
line_.account = configuration.get_multivalue('currency_exchange_debit_account', company=self.company.id)
|
||
line_.credit = to_add if to_add > 0 else 0
|
||
line_.debit = -to_add if to_add < 0 else 0
|
||
line_.maturity_date = reval_date
|
||
logger.info("REVALUATE_EX:%s",line_)
|
||
move = Move()
|
||
move.journal = configuration.get_multivalue('currency_exchange_journal', company=self.company.id)
|
||
period = Period.find(self.company, date=reval_date)
|
||
move.date = reval_date
|
||
move.period = period
|
||
#move.origin = forex
|
||
move.company = self.company
|
||
move.lines = [line,line_]
|
||
Move.save([move])
|
||
|
||
class Revaluate(Wizard):
|
||
'Revaluate'
|
||
__name__ = 'account.revaluate'
|
||
start = StateView(
|
||
'account.revaluate.start',
|
||
'purchase_trade.revaluate_start_view_form', [
|
||
Button("Cancel", 'end', 'tryton-cancel'),
|
||
Button("Revaluate", 'revaluate', 'tryton-ok', default=True),
|
||
])
|
||
revaluate = StateTransition()
|
||
|
||
def transition_revaluate(self):
|
||
Account = Pool().get('account.account')
|
||
accounts = Account.search([('fx_eval','=',True)])
|
||
if accounts:
|
||
for acc in accounts:
|
||
acc.revaluate_fx(self.start.revaluation_date,self.start.delete_after)
|
||
|
||
return 'end'
|
||
|
||
class RevaluateStart(ModelView):
|
||
"Revaluate"
|
||
__name__ = 'account.revaluate.start'
|
||
revaluation_date = fields.Date(
|
||
"Revaluation date")
|
||
delete_after = fields.Boolean(
|
||
"Delete lines after revaluation date")
|
||
|
||
@classmethod
|
||
def default_revaluation_date(cls):
|
||
Date = Pool().get('ir.date')
|
||
return Date.today()
|
||
|
||
@classmethod
|
||
def default_delete_after(cls):
|
||
return False
|
||
|
||
|
||
class ShipmentTemplateReportMixin:
|
||
|
||
@classmethod
|
||
def _get_purchase_trade_configuration(cls):
|
||
Configuration = Pool().get('purchase_trade.configuration')
|
||
configurations = Configuration.search([], limit=1)
|
||
return configurations[0] if configurations else None
|
||
|
||
@classmethod
|
||
def _get_action_report_path(cls, action):
|
||
if isinstance(action, dict):
|
||
return action.get('report') or ''
|
||
return getattr(action, 'report', '') or ''
|
||
|
||
@classmethod
|
||
def _resolve_template_path(cls, field_name, default_prefix):
|
||
config = cls._get_purchase_trade_configuration()
|
||
template = getattr(config, field_name, '') if config else ''
|
||
template = (template or '').strip()
|
||
if not template:
|
||
raise UserError('No template found')
|
||
if '/' not in template:
|
||
return f'{default_prefix}/{template}'
|
||
return template
|
||
|
||
@classmethod
|
||
def _get_resolved_action(cls, action):
|
||
report_path = cls._resolve_configured_report_path(action)
|
||
if isinstance(action, dict):
|
||
resolved = dict(action)
|
||
resolved['report'] = report_path
|
||
return resolved
|
||
setattr(action, 'report', report_path)
|
||
return action
|
||
|
||
@classmethod
|
||
def _execute(cls, records, header, data, action):
|
||
resolved_action = cls._get_resolved_action(action)
|
||
return super()._execute(records, header, data, resolved_action)
|
||
|
||
|
||
class ShipmentShippingReport(ShipmentTemplateReportMixin, BaseSupplierShipping):
|
||
__name__ = 'stock.shipment.in.shipping'
|
||
|
||
@classmethod
|
||
def _resolve_configured_report_path(cls, action):
|
||
return cls._resolve_template_path(
|
||
'shipment_shipping_report_template', 'stock')
|
||
|
||
|
||
class ShipmentInsuranceReport(ShipmentTemplateReportMixin, BaseSupplierShipping):
|
||
__name__ = 'stock.shipment.in.insurance'
|
||
|
||
@classmethod
|
||
def _resolve_configured_report_path(cls, action):
|
||
return cls._resolve_template_path(
|
||
'shipment_insurance_report_template', 'stock')
|
||
|
||
|
||
class ShipmentPackingListReport(ShipmentTemplateReportMixin, BaseSupplierShipping):
|
||
__name__ = 'stock.shipment.in.packing_list'
|
||
|
||
@classmethod
|
||
def _resolve_configured_report_path(cls, action):
|
||
return cls._resolve_template_path(
|
||
'shipment_packing_list_report_template', 'stock')
|
||
|
||
|
||
class ShipmentLinkageReport(ShipmentTemplateReportMixin, BaseSupplierShipping):
|
||
__name__ = 'stock.shipment.in.linkage'
|
||
|
||
@classmethod
|
||
def _resolve_configured_report_path(cls, action):
|
||
return cls._resolve_template_path(
|
||
'shipment_linkage_report_template', 'stock')
|
||
|
||
|
||
class LinkageTemplateReport(ShipmentTemplateReportMixin, BaseSupplierShipping):
|
||
@classmethod
|
||
def _resolve_configured_report_path(cls, action):
|
||
return cls._resolve_template_path(
|
||
'shipment_linkage_report_template', 'stock')
|
||
|
||
|
||
class LotReportLinkageRecord:
|
||
|
||
def __init__(self, lot, lot_report=None, linkage_status='PROVISIONAL'):
|
||
self.lot = lot
|
||
self.lot_report = lot_report
|
||
self.id = getattr(lot_report, 'id', None) or getattr(lot, 'id', None)
|
||
self.report_linkage_status = linkage_status
|
||
|
||
@property
|
||
def rec_name(self):
|
||
return (
|
||
getattr(self.lot_report, 'rec_name', None)
|
||
or getattr(self.lot, 'rec_name', None)
|
||
or getattr(self.lot, 'lot_name', None)
|
||
or str(self.id))
|
||
|
||
def set_lang(self, language=None):
|
||
set_lang = getattr(self.lot, 'set_lang', None)
|
||
if callable(set_lang):
|
||
set_lang(language)
|
||
|
||
def get_common_context(self, commoncontext):
|
||
shipment = getattr(self.lot, 'lot_shipment_in', None)
|
||
get_common_context = getattr(shipment, 'get_common_context', None)
|
||
if callable(get_common_context):
|
||
return get_common_context(commoncontext)
|
||
return None
|
||
|
||
@staticmethod
|
||
def _report_record_key(record):
|
||
return ShipmentIn._report_record_key(record)
|
||
|
||
@classmethod
|
||
def _report_unique_records(cls, records):
|
||
return ShipmentIn._report_unique_records(records)
|
||
|
||
@staticmethod
|
||
def _report_linkage_lot_purchase_line(lot):
|
||
return ShipmentIn._report_linkage_lot_purchase_line(lot)
|
||
|
||
@staticmethod
|
||
def _report_linkage_lot_sale_line(lot):
|
||
return ShipmentIn._report_linkage_lot_sale_line(lot)
|
||
|
||
@staticmethod
|
||
def _report_rec_name(record):
|
||
return ShipmentIn._report_rec_name(record)
|
||
|
||
@classmethod
|
||
def _report_number(cls, record):
|
||
return ShipmentIn._report_number(record)
|
||
|
||
@staticmethod
|
||
def _report_date(value):
|
||
return ShipmentIn._report_date(value)
|
||
|
||
@staticmethod
|
||
def _report_amount(value):
|
||
return ShipmentIn._report_amount(value)
|
||
|
||
@staticmethod
|
||
def _report_price(value):
|
||
return ShipmentIn._report_price(value)
|
||
|
||
@classmethod
|
||
def _report_currency_code(cls, record):
|
||
return ShipmentIn._report_currency_code(record)
|
||
|
||
@classmethod
|
||
def _report_unit_symbol(cls, line):
|
||
return ShipmentIn._report_unit_symbol(line)
|
||
|
||
@classmethod
|
||
def _report_line_quantity(cls, line):
|
||
return ShipmentIn._report_line_quantity(line)
|
||
|
||
@classmethod
|
||
def _report_line_amount(cls, line):
|
||
return ShipmentIn._report_line_amount(line)
|
||
|
||
@classmethod
|
||
def _report_fee_amount(cls, fee):
|
||
return ShipmentIn._report_fee_amount(fee)
|
||
|
||
@classmethod
|
||
def _report_linkage_fee_label(cls, fee):
|
||
return ShipmentIn._report_linkage_fee_label(fee)
|
||
|
||
@classmethod
|
||
def _report_column(cls, rows, index, amount=False):
|
||
return ShipmentIn._report_column(rows, index, amount=amount)
|
||
|
||
@classmethod
|
||
def _report_table_rows(cls, rows, names, amount_names=()):
|
||
return ShipmentIn._report_table_rows(rows, names, amount_names)
|
||
|
||
@staticmethod
|
||
def _format_report_quantity(value, digits='0.001'):
|
||
return ShipmentIn._format_report_quantity(value, digits=digits)
|
||
|
||
@staticmethod
|
||
def _get_report_unit_text(unit):
|
||
return ShipmentIn._get_report_unit_text(unit)
|
||
|
||
def _get_report_primary_move(self):
|
||
return getattr(self.lot, 'move', None)
|
||
|
||
def _get_report_linkage_lots(self):
|
||
return [self.lot]
|
||
|
||
_get_report_linkage_purchase_lines = (
|
||
ShipmentIn._get_report_linkage_purchase_lines)
|
||
_get_report_linkage_sale_lines = ShipmentIn._get_report_linkage_sale_lines
|
||
_get_report_linkage_purchase = ShipmentIn._get_report_linkage_purchase
|
||
_get_report_linkage_sale = ShipmentIn._get_report_linkage_sale
|
||
_get_report_linkage_product = ShipmentIn._get_report_linkage_product
|
||
|
||
def _get_report_linkage_fees(self):
|
||
shipment = getattr(self.lot, 'lot_shipment_in', None)
|
||
return list(getattr(shipment, 'fees', []) or [])
|
||
|
||
_get_report_linkage_summary_rows = (
|
||
ShipmentIn._get_report_linkage_summary_rows)
|
||
_get_report_linkage_movement_rows = (
|
||
ShipmentIn._get_report_linkage_movement_rows)
|
||
_get_report_linkage_movement_row = (
|
||
ShipmentIn._get_report_linkage_movement_row)
|
||
_get_report_linkage_pricing_rows = (
|
||
ShipmentIn._get_report_linkage_pricing_rows)
|
||
_get_report_linkage_detail_rows = (
|
||
ShipmentIn._get_report_linkage_detail_rows)
|
||
_get_report_linkage_detail_row = (
|
||
ShipmentIn._get_report_linkage_detail_row)
|
||
_get_report_linkage_fee_detail_row = (
|
||
ShipmentIn._get_report_linkage_fee_detail_row)
|
||
|
||
@property
|
||
def report_linkage_title(self):
|
||
title = ShipmentIn.report_linkage_title.fget(self)
|
||
status = getattr(self, 'report_linkage_status', None)
|
||
if status:
|
||
title = ' - '.join(part for part in [title, status] if part)
|
||
return title
|
||
|
||
report_linkage_desk = ShipmentIn.report_linkage_desk
|
||
report_linkage_book = ShipmentIn.report_linkage_book
|
||
report_linkage_strategy = ShipmentIn.report_linkage_strategy
|
||
|
||
@property
|
||
def report_linkage_bl_date(self):
|
||
shipment = getattr(self.lot, 'lot_shipment_in', None)
|
||
bl_date = getattr(shipment, 'bl_date', None)
|
||
if not bl_date:
|
||
move = self._get_report_primary_move()
|
||
bl_date = getattr(move, 'bldate', None)
|
||
return bl_date and bl_date.strftime('%A, %B %d, %Y') or ''
|
||
|
||
report_linkage_trading_unit = ShipmentIn.report_linkage_trading_unit
|
||
report_linkage_finance_user = ShipmentIn.report_linkage_finance_user
|
||
report_linkage_summary_groups = ShipmentIn.report_linkage_summary_groups
|
||
report_linkage_summary_cost_types = (
|
||
ShipmentIn.report_linkage_summary_cost_types)
|
||
report_linkage_summary_estimated = (
|
||
ShipmentIn.report_linkage_summary_estimated)
|
||
report_linkage_summary_validated = (
|
||
ShipmentIn.report_linkage_summary_validated)
|
||
report_linkage_summary_rows = ShipmentIn.report_linkage_summary_rows
|
||
report_linkage_movement_types = ShipmentIn.report_linkage_movement_types
|
||
report_linkage_movement_references = (
|
||
ShipmentIn.report_linkage_movement_references)
|
||
report_linkage_movement_counterparts = (
|
||
ShipmentIn.report_linkage_movement_counterparts)
|
||
report_linkage_movement_commodities = (
|
||
ShipmentIn.report_linkage_movement_commodities)
|
||
report_linkage_movement_quantities = (
|
||
ShipmentIn.report_linkage_movement_quantities)
|
||
report_linkage_movement_deliveries = (
|
||
ShipmentIn.report_linkage_movement_deliveries)
|
||
report_linkage_movement_basis = ShipmentIn.report_linkage_movement_basis
|
||
report_linkage_movement_periods = (
|
||
ShipmentIn.report_linkage_movement_periods)
|
||
report_linkage_movement_from_dates = (
|
||
ShipmentIn.report_linkage_movement_from_dates)
|
||
report_linkage_movement_to_dates = (
|
||
ShipmentIn.report_linkage_movement_to_dates)
|
||
report_linkage_movement_rows = ShipmentIn.report_linkage_movement_rows
|
||
report_linkage_bank_deal = ShipmentIn.report_linkage_bank_deal
|
||
report_linkage_bank_type = ShipmentIn.report_linkage_bank_type
|
||
report_linkage_bank_name = ShipmentIn.report_linkage_bank_name
|
||
report_linkage_lc_type = ShipmentIn.report_linkage_lc_type
|
||
report_linkage_lc_number = ShipmentIn.report_linkage_lc_number
|
||
report_linkage_lc_amount = ShipmentIn.report_linkage_lc_amount
|
||
report_linkage_lc_expiry_date = ShipmentIn.report_linkage_lc_expiry_date
|
||
report_linkage_pricing_deals = ShipmentIn.report_linkage_pricing_deals
|
||
report_linkage_pricing_sides = ShipmentIn.report_linkage_pricing_sides
|
||
report_linkage_pricing_types = ShipmentIn.report_linkage_pricing_types
|
||
report_linkage_pricing_symbols = ShipmentIn.report_linkage_pricing_symbols
|
||
report_linkage_pricing_periods = ShipmentIn.report_linkage_pricing_periods
|
||
report_linkage_pricing_input_prices = (
|
||
ShipmentIn.report_linkage_pricing_input_prices)
|
||
report_linkage_pricing_rows = ShipmentIn.report_linkage_pricing_rows
|
||
report_linkage_detail_groups = ShipmentIn.report_linkage_detail_groups
|
||
report_linkage_detail_cost_types = (
|
||
ShipmentIn.report_linkage_detail_cost_types)
|
||
report_linkage_detail_counterparts = (
|
||
ShipmentIn.report_linkage_detail_counterparts)
|
||
report_linkage_detail_sources = ShipmentIn.report_linkage_detail_sources
|
||
report_linkage_detail_unit_prices = (
|
||
ShipmentIn.report_linkage_detail_unit_prices)
|
||
report_linkage_detail_quantities = (
|
||
ShipmentIn.report_linkage_detail_quantities)
|
||
report_linkage_detail_estimated = (
|
||
ShipmentIn.report_linkage_detail_estimated)
|
||
report_linkage_detail_validated = (
|
||
ShipmentIn.report_linkage_detail_validated)
|
||
report_linkage_detail_rows = ShipmentIn.report_linkage_detail_rows
|
||
|
||
|
||
class LotReportLinkageReport(LinkageTemplateReport):
|
||
__name__ = 'lot.report.linkage'
|
||
linkage_status = 'PROVISIONAL'
|
||
|
||
@classmethod
|
||
def _get_records(cls, ids, model, data):
|
||
LotReport = Pool().get('lot.report')
|
||
records = LotReport.browse(ids)
|
||
linkage_records = []
|
||
for record in records:
|
||
if getattr(record, 'r_lot_type', None) != 'physic':
|
||
raise UserError(
|
||
'Linkage report is only available on physical lot lines.')
|
||
lot = getattr(record, 'r_lot_p', None) or getattr(
|
||
record, 'r_lot_s', None)
|
||
if not lot:
|
||
raise UserError(
|
||
'Linkage report needs a physical lot on the selected line.')
|
||
linkage_records.append(LotReportLinkageRecord(
|
||
lot, record, linkage_status=cls.linkage_status))
|
||
return linkage_records
|
||
|
||
|
||
class LotReportLinkageFinalReport(LotReportLinkageReport):
|
||
__name__ = 'lot.report.linkage.final'
|
||
linkage_status = 'FINAL'
|
||
|
||
|
||
class ShipmentCOOReport(ShipmentTemplateReportMixin, BaseSupplierShipping):
|
||
__name__ = 'stock.shipment.in.coo'
|
||
|
||
@classmethod
|
||
def _resolve_configured_report_path(cls, action):
|
||
return cls._resolve_template_path(
|
||
'shipment_coo_report_template', 'stock')
|