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]