313 lines
13 KiB
Python
313 lines
13 KiB
Python
from trytond.model import ModelSQL, ModelView, fields
|
|
from trytond.pool import Pool
|
|
from trytond.exceptions import UserError
|
|
from decimal import Decimal, ROUND_HALF_UP
|
|
from datetime import datetime as dt
|
|
import datetime
|
|
import requests
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class WeightReport(ModelSQL, ModelView):
|
|
'Weight Report'
|
|
__name__ = 'weight.report'
|
|
_rec_name = 'reference'
|
|
# Identification
|
|
lab = fields.Char('Laboratory', required=True)
|
|
|
|
# Report Information
|
|
reference = fields.Char('Reference')
|
|
file_no = fields.Char('File Number')
|
|
report_date = fields.Date('Report Date')
|
|
|
|
# Contract Information
|
|
contract_no = fields.Char('Contract Number')
|
|
invoice_no = fields.Char('Invoice Number')
|
|
lc_no = fields.Char('LC Number')
|
|
origin = fields.Char('Origin')
|
|
commodity = fields.Char('Commodity')
|
|
|
|
# Parties Information
|
|
seller = fields.Many2One('party.party','Seller', required=True)
|
|
buyer = fields.Many2One('party.party','Buyer', required=True)
|
|
carrier = fields.Many2One('party.party','Carrier')
|
|
|
|
# Shipment Information
|
|
vessel = fields.Many2One('trade.vessel','Vessel')
|
|
bl_no = fields.Char('B/L Number')
|
|
bl_date = fields.Date('B/L Date')
|
|
port_loading = fields.Many2One('stock.location','Port of Loading')
|
|
port_destination = fields.Many2One('stock.location','Port of Destination')
|
|
arrival_date = fields.Date('Arrival Date')
|
|
weighing_place = fields.Char('Weighing Place')
|
|
weighing_method = fields.Char('Weighing Method')
|
|
weight_date = fields.Date('Weight Date')
|
|
bales = fields.Integer('Number of Bales')
|
|
|
|
# Weights Information
|
|
gross_landed_kg = fields.Numeric('Gross Landed (kg)', digits=(16, 2))
|
|
tare_kg = fields.Numeric('Tare Weight (kg)', digits=(16, 2))
|
|
net_landed_kg = fields.Numeric('Net Landed (kg)', digits=(16, 2))
|
|
invoice_net_kg = fields.Numeric('Invoice Net (kg)', digits=(16, 2))
|
|
gain_loss_kg = fields.Numeric('Gain/Loss (kg)', digits=(16, 2))
|
|
gain_loss_percent = fields.Numeric('Gain/Loss (%)', digits=(16, 2))
|
|
remote_weight_report_keys = fields.Text('Remote WR Keys', readonly=True)
|
|
remote_weight_report_sent_at = fields.DateTime(
|
|
'Remote WR Sent At', readonly=True)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._order = [('report_date', 'DESC')]
|
|
cls._buttons.update({
|
|
'create_remote_weight_reports': {},
|
|
})
|
|
|
|
def get_rec_name(self, name):
|
|
items = [self.lab]
|
|
if self.reference:
|
|
items.append('[%s]' % self.reference)
|
|
return ' '.join(items)
|
|
|
|
def create_remote_weight_report(self, wr_payload):
|
|
response = requests.post(
|
|
"http://automation-service:8006/weight-report",
|
|
json=wr_payload,
|
|
timeout=10
|
|
)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
def get_related_shipments(self):
|
|
ShipmentWR = Pool().get('shipment.wr')
|
|
links = ShipmentWR.search([('wr', '=', self.id)])
|
|
return [link.shipment_in for link in links if link.shipment_in]
|
|
|
|
def get_source_shipment(self):
|
|
shipments = self.get_related_shipments()
|
|
if not shipments:
|
|
raise UserError('No shipment is linked to this weight report.')
|
|
unique_shipments = {shipment.id: shipment for shipment in shipments}
|
|
if len(unique_shipments) > 1:
|
|
raise UserError(
|
|
'This weight report is linked to multiple shipments.')
|
|
return next(iter(unique_shipments.values()))
|
|
|
|
def get_remote_weight_report_lots(self, shipment):
|
|
lots = []
|
|
seen = set()
|
|
for move in shipment.incoming_moves or []:
|
|
lot = getattr(move, 'lot', None)
|
|
if (not lot or lot.lot_type != 'physic'
|
|
or lot.id in seen):
|
|
continue
|
|
seen.add(lot.id)
|
|
lots.append(lot)
|
|
if not lots:
|
|
raise UserError(
|
|
'No physical lot was found on the incoming moves.')
|
|
return lots
|
|
|
|
def validate_remote_weight_report_context(self, shipment):
|
|
if not shipment.controller:
|
|
raise UserError(
|
|
'A controller is required before creating remote weight reports.')
|
|
if not shipment.returned_id:
|
|
raise UserError(
|
|
'A returned ID is required before creating remote weight reports.')
|
|
if not shipment.agent:
|
|
raise UserError(
|
|
'A booking agent is required before creating remote weight reports.')
|
|
if not shipment.to_location:
|
|
raise UserError(
|
|
'A destination location is required before creating remote weight reports.')
|
|
if not self.bales:
|
|
raise UserError(
|
|
'The global weight report must define the number of bales.')
|
|
if not self.report_date or not self.weight_date:
|
|
raise UserError(
|
|
'Report date and weight date are required.')
|
|
|
|
def build_remote_weight_report_payload(self, shipment, lot):
|
|
if not lot.lot_chunk_key:
|
|
raise UserError(
|
|
'Each physical lot must have a chunk key before export.')
|
|
factor_net = self.net_landed_kg / self.bales
|
|
factor_gross = self.gross_landed_kg / self.bales
|
|
lot_ratio = Decimal(lot.lot_qt) / self.bales
|
|
return {
|
|
"chunk_key": lot.lot_chunk_key,
|
|
"gross_weight": float(round(
|
|
Decimal(lot.lot_qt) * factor_gross, 5)),
|
|
"net_weight": float(round(
|
|
Decimal(lot.lot_qt) * factor_net, 5)),
|
|
"tare_total": float(round(self.tare_kg * lot_ratio, 5)),
|
|
"bags": int(lot.lot_qt),
|
|
"surveyor_code": shipment.controller.get_alf(),
|
|
"place_key": shipment.to_location.get_places(),
|
|
"report_date": int(self.report_date.strftime("%Y%m%d")),
|
|
"weight_date": int(self.weight_date.strftime("%Y%m%d")),
|
|
"agent": shipment.agent.get_alf(),
|
|
"forwarder_ref": shipment.returned_id,
|
|
}
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def create_remote_weight_reports(cls, reports):
|
|
to_save = []
|
|
for report in reports:
|
|
shipment = report.get_source_shipment()
|
|
report.validate_remote_weight_report_context(shipment)
|
|
lots = report.get_remote_weight_report_lots(shipment)
|
|
created = []
|
|
for lot in lots:
|
|
payload = report.build_remote_weight_report_payload(
|
|
shipment, lot)
|
|
logger.info("REMOTE_WR_PAYLOAD:%s", payload)
|
|
data = report.create_remote_weight_report(payload)
|
|
created.append(
|
|
f"{lot.rec_name}: {data.get('weight_report_key')}")
|
|
report.remote_weight_report_keys = '\n'.join(created)
|
|
report.remote_weight_report_sent_at = datetime.datetime.now()
|
|
to_save.append(report)
|
|
if to_save:
|
|
cls.save(to_save)
|
|
# @classmethod
|
|
# @ModelView.button_action('weight_report.act_import_json')
|
|
# def import_json(cls, reports):
|
|
# pass
|
|
|
|
# @classmethod
|
|
# @ModelView.button_action('weight_report.act_export_json')
|
|
# def export_json(cls, reports):
|
|
# pass
|
|
|
|
@classmethod
|
|
def create_from_json(cls, json_data):
|
|
"""Crée un rapport à partir de données JSON"""
|
|
pool = Pool()
|
|
Party = pool.get('party.party')
|
|
Vessel = pool.get('trade.vessel')
|
|
Location = pool.get('stock.location')
|
|
|
|
# Préparer les données
|
|
report = {}
|
|
|
|
# 1. Identification (champs simples)
|
|
report['lab'] = json_data.get('lab', '')
|
|
|
|
# 2. Report Information
|
|
report_data = json_data.get('report', {})
|
|
report['reference'] = report_data.get('reference', '')
|
|
report['file_no'] = report_data.get('file_no', '')
|
|
|
|
# Conversion de la date (format: "28 October 2025")
|
|
|
|
def parse_date(date_str):
|
|
logger.info("TRY_TO_PARSE:%s",date_str)
|
|
for fmt in ('%d %B %Y', '%d %b %Y'):
|
|
try:
|
|
return dt.strptime(date_str, fmt).date()
|
|
except ValueError:
|
|
pass
|
|
return None
|
|
|
|
report['report_date'] = parse_date(report_data.get('date', ''))
|
|
# 3. Contract Information
|
|
contract_data = json_data.get('contract', {})
|
|
report['contract_no'] = contract_data.get('contract_no', '')
|
|
report['invoice_no'] = contract_data.get('invoice_no', '')
|
|
report['lc_no'] = contract_data.get('lc_no', '')
|
|
report['origin'] = contract_data.get('origin', '')
|
|
report['commodity'] = contract_data.get('commodity', '')
|
|
|
|
# 4. Parties Information (Many2One)
|
|
parties_data = json_data.get('parties', {})
|
|
|
|
# Recherche ou création des parties
|
|
seller_name = parties_data.get('seller', '')
|
|
if seller_name:
|
|
seller = Party.getPartyByName(seller_name)
|
|
report['seller'] = seller.id if seller else None
|
|
|
|
buyer_name = parties_data.get('buyer', '')
|
|
if buyer_name:
|
|
buyer = Party.getPartyByName(buyer_name)
|
|
report['buyer'] = buyer.id if buyer else None
|
|
|
|
carrier_name = parties_data.get('carrier', '')
|
|
if carrier_name:
|
|
carrier = Party.getPartyByName(carrier_name)
|
|
report['carrier'] = carrier.id if carrier else None
|
|
|
|
# 5. Shipment Information
|
|
shipment_data = json_data.get('shipment', {})
|
|
|
|
# Recherche du navire par nom
|
|
vessel_name = shipment_data.get('vessel', '')
|
|
if vessel_name:
|
|
vessel = Vessel.search([('vessel_name', '=', vessel_name)], limit=1)
|
|
report['vessel'] = vessel[0].id if vessel else None
|
|
|
|
report['bl_no'] = shipment_data.get('bl_no', '')
|
|
|
|
# Conversion de la date B/L (format: "16-Aug-2025")
|
|
bl_date_str = shipment_data.get('bl_date', '')
|
|
if bl_date_str:
|
|
try:
|
|
report['bl_date'] = dt.strptime(bl_date_str, '%d-%b-%Y').date()
|
|
except:
|
|
report['bl_date'] = None
|
|
|
|
# Ports (Many2One - nécessite recherche par nom)
|
|
port_loading_name = shipment_data.get('port_loading', '')
|
|
if port_loading_name:
|
|
port_loading = Location.search([
|
|
('name', '=', port_loading_name),
|
|
('type', '=', 'supplier')
|
|
], limit=1)
|
|
report['port_loading'] = port_loading[0].id if port_loading else None
|
|
|
|
port_destination_name = shipment_data.get('port_destination', '')
|
|
if port_destination_name:
|
|
port_destination = Location.search([
|
|
('name', '=', port_destination_name),
|
|
('type', '=', 'customer')
|
|
], limit=1)
|
|
report['port_destination'] = port_destination[0].id if port_destination else None
|
|
|
|
# Conversion de la date d'arrivée (format: "20-Oct-2025")
|
|
arrival_date_str = shipment_data.get('arrival_date', '')
|
|
if arrival_date_str:
|
|
try:
|
|
report['arrival_date'] = dt.strptime(arrival_date_str, '%d-%b-%Y').date()
|
|
except:
|
|
report['arrival_date'] = None
|
|
|
|
report['weighing_place'] = shipment_data.get('weighing_place', '')
|
|
report['weighing_method'] = shipment_data.get('weighing_method', '')
|
|
report['bales'] = int(shipment_data.get('bales', 0) or 0)
|
|
|
|
# 6. Weights Information
|
|
weights_data = json_data.get('weights', {})
|
|
|
|
gross = Decimal(str(weights_data.get('gross_landed_kg', 0) or 0))
|
|
tare = Decimal(str(weights_data.get('tare_kg', 0) or 0))
|
|
net = Decimal(str(weights_data.get('net_landed_kg', 0) or 0))
|
|
invoice = Decimal(str(weights_data.get('invoice_net_kg', 0) or 0))
|
|
gain_loss = Decimal(str(weights_data.get('gain_loss_kg', 0) or 0))
|
|
gain_loss_percent = Decimal(str(weights_data.get('gain_loss_percent', 0) or 0))
|
|
|
|
# Arrondir à 2 décimales
|
|
report['weight_date'] = parse_date(weights_data.get('weight_date', ''))
|
|
report['gross_landed_kg'] = gross.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
|
|
report['tare_kg'] = tare.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
|
|
report['net_landed_kg'] = net.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
|
|
report['invoice_net_kg'] = invoice.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
|
|
report['gain_loss_kg'] = gain_loss.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
|
|
report['gain_loss_percent'] = gain_loss_percent.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
|
|
|
|
# 7. Création du rapport
|
|
return cls.create([report])[0]
|