from trytond.model import ModelSQL, ModelView, Workflow, fields from trytond.pool import Pool from trytond.transaction import Transaction from trytond.pyson import Bool, Eval, Id, If, PYSONEncoder from datetime import datetime class LCOutgoing(ModelSQL, ModelView, Workflow): 'LC Outgoing' __name__ = 'lc.letter.outgoing' name = fields.Char('Number') type = fields.Selection([ ('documentary', 'Documentary LC'), ('standby', 'Standby LC') ], 'Type', required=True) # Références aux commandes (achat ET vente) purchase = fields.Many2One('purchase.purchase', 'Purchase') company = fields.Many2One('company.company', 'Company') applicant = fields.Many2One('party.party', 'Applicant') beneficiary = fields.Many2One('party.party', 'Beneficiary') # Banques issuing_bank = fields.Many2One('party.party', 'Issuing Bank') advising_bank = fields.Many2One('party.party', 'Advising Bank') confirming_bank = fields.Many2One('party.party', 'Confirming Bank') reimbursing_bank = fields.Many2One('party.party', 'Reimbursing Bank') # Montants et conditions amount = fields.Numeric('Amount', digits=(16, 2)) currency = fields.Many2One('currency.currency', 'Currency') tolerance_plus = fields.Numeric('Tolerance + %', digits=(6, 2)) tolerance_minus = fields.Numeric('Tolerance - %', digits=(6, 2)) # Conditions de livraison incoterm = fields.Many2One('incoterm.incoterm', 'Incoterm') port_of_loading = fields.Many2One('stock.location','Port of Loading') port_of_discharge = fields.Many2One('stock.location','Port of Discharge') final_destination = fields.Many2One('stock.location','Final Destination') partial_shipment = fields.Selection([ (None,''), ('allowed', 'Allowed'), ('not_allowed', 'Not Allowed') ], 'Partial Shipment') transhipment = fields.Selection([ (None,''), ('allowed', 'Allowed'), ('not_allowed', 'Not Allowed') ], 'Transhipment') # Dates critiques latest_shipment_date = fields.Date('Latest Shipment Date') issue_date = fields.Date('Issue Date') expiry_date = fields.Date('Expiry Date') expiry_place = fields.Char('Expiry Place') presentation_days = fields.Integer('Presentation Days') # Règles et conditions ruleset = fields.Selection([ (None,''), ('ucp600', 'UCP 600'), ('isp98', 'ISP 98'), ('urdg758', 'URDG 758') ], 'Ruleset') required_documents = fields.Many2Many( 'contract.document.type', 'lc_out', 'doc_type', 'Required Documents') # Workflow principal state = fields.Selection([ ('draft', 'Draft'), ('submitted', 'Submitted'), ('approved', 'Approved'), ('cancelled', 'Cancelled') ], 'State', readonly=True) version = fields.Integer('Version', readonly=True) # Documents et pièces jointes attachments = fields.One2Many('ir.attachment', 'resource', 'Attachments', domain=[('resource', '=', Eval('id'))], depends=['id']) # Champs techniques swift_message = fields.Text('SWIFT Message') swift_type = fields.Char('SWIFT Type') bank_reference = fields.Char('Bank Reference') our_reference = fields.Char('Our Reference') # Champs spécifiques Achat bank_instructions = fields.Text('Instructions to Bank') application_date = fields.Date('Application Date') credit_availability = fields.Selection([ (None,''), ('by_payment', 'By Payment'), ('by_deferred_payment', 'By Deferred Payment'), ('by_acceptance', 'By Acceptance'), ('by_negotiation', 'By Negotiation') ], 'Credit Availability') # Suivi documents fournisseur documents_received = fields.One2Many('lc.document.received', 'lc', 'Documents Received') documents_status = fields.Function(fields.Selection([ ('pending', 'Pending'), ('partial', 'Partially Received'), ('complete', 'Complete'), ('discrepant', 'Discrepant') ], 'Documents Status'), 'get_documents_status') # Dates importantes amendment_deadline = fields.Date('Amendment Deadline') documents_deadline = fields.Date('Documents Deadline') swift_file = fields.Many2One('document.incoming',"Swift file") swift_execute = fields.Boolean("Create") swift_text = fields.Text("Message") @staticmethod def default_state(): return 'draft' @staticmethod def default_version(): return 1 @classmethod def __setup__(cls): super(LCOutgoing, cls).__setup__() cls._transitions = set(( ('draft', 'submitted'), ('submitted', 'approved'), ('draft', 'cancelled'), ('submitted', 'cancelled'), ('cancelled', 'draft'), )) cls._buttons.update({ 'cancel': { 'invisible': Eval('state').in_(['cancelled', 'approved']), 'depends': ['state'], }, 'draft': { 'invisible': Eval('state') != 'cancelled', 'depends': ['state'], }, 'submit': { 'invisible': Eval('state') != 'draft', 'depends': ['state'], }, 'approve': { 'invisible': Eval('state') != 'submitted', 'depends': ['state'], }, }) @classmethod @ModelView.button @Workflow.transition('cancelled') def cancel(cls, lcs): pass @classmethod @ModelView.button @Workflow.transition('draft') def draft(cls, lcs): pass @classmethod @ModelView.button @Workflow.transition('submitted') def submit(cls, lcs): cls.write(lcs, {'state': 'submitted'}) @classmethod @ModelView.button @Workflow.transition('approved') def approve(cls, lcs): for lc in lcs: cls.write([lc], {'state': 'approved'}) def get_rec_name(self, name): if self.name: return f"{self.name}" return f"LC - {self.create_date}" @classmethod def search_rec_name(cls, name, clause): return ['OR', ('name',) + tuple(clause[1:]), ] @classmethod def write(cls, lcs, vals): # Appeler le parent pour appliquer vals initiaux super(LCOutgoing, cls).write(lcs, vals) # Puis traiter chaque record si nécessaire. # On passe `vals` pour savoir si swift_execute a été fourni dans l'appel initial. for lc in lcs: cls._process_swift_for_record(lc, initial_vals=vals) @classmethod def _process_swift_for_record(cls, lc, initial_vals=None): """ Traite un enregistrement unique : récupère swift_text si nécessaire, appelle import_swift et applique les writes via super().write (pour éviter de ré-appeler notre override write). `initial_vals` : dict des valeurs passées lors du create/write initial (peut être None). """ # Vérifier si on doit exécuter should_execute = False if initial_vals and 'swift_execute' in initial_vals: should_execute = bool(initial_vals.get('swift_execute')) else: should_execute = bool(lc.swift_execute) if not should_execute: return update_vals = {} swift_message = lc.generate_swift_mt700() # Toujours désactiver le flag et passer en submitted update_vals['swift_execute'] = False update_vals['swift_text'] = swift_message # Appliquer les modifications via la méthode parente pour éviter récursion if update_vals: super(LCOutgoing, cls).write([lc], update_vals) @classmethod def create_from_purchase(cls, purchase_id): """Méthode de base pour création depuis achat""" Purchase = Pool().get('purchase.purchase') purchase = Purchase(purchase_id) return { 'purchase': purchase.id, 'company': purchase.company.id, 'applicant': purchase.company.party.id, 'beneficiary': purchase.party.id, 'currency': purchase.currency.id if purchase.currency else None, 'state': 'draft', 'type': 'documentary', 'application_date': datetime.now().date(), 'bank_instructions': 'Standard LC issuance instructions', } def get_documents_status(self, name): if not self.documents_received: return 'pending' received_count = len([d for d in self.documents_received if d.received]) required_count = len(self.lc.required_documents) if self.lc.required_documents else 0 if received_count == 0: return 'pending' elif received_count < required_count: return 'partial' elif any(d.discrepancy for d in self.documents_received): return 'discrepant' else: return 'complete' def generate_swift_mt700(self): """Génère le message SWIFT MT700 pour la banque""" swift_template = """ :27: SEQUENCE OF TOTAL 1/1 :40A: FORM OF DOCUMENTARY CREDIT IRREVOCABLE :20: DOCUMENTARY CREDIT NUMBER {lc_number} :31C: DATE OF ISSUE {issue_date} :31D: DATE AND PLACE OF EXPIRY {expiry_date} {expiry_place} :50: APPLICANT {applicant} :59: BENEFICIARY {beneficiary} :32B: CURRENCY CODE, AMOUNT {currency} {amount} :41A: AVAILABLE WITH... BY... {available_with} :43P: PARTIAL SHIPMENTS {partial_shipment} :43T: TRANSSHIPMENT {transhipment} :44A: LOADING ON BOARD/DISPATCH/TAKING IN CHARGE AT/FROM {port_of_loading} :44B: FOR TRANSPORTATION TO {port_of_discharge} :44C: LATEST DATE OF SHIPMENT {latest_shipment_date} :45A: DESCRIPTION OF GOODS AND/OR SERVICES {goods_description} :46A: DOCUMENTS REQUIRED {documents_required} :47A: ADDITIONAL CONDITIONS {additional_conditions} :71B: CHARGES ALL BANK CHARGES OUTSIDE ISSUING BANK ARE FOR BENEFICIARY'S ACCOUNT :48: PERIOD FOR PRESENTATION {presentation_days} DAYS AFTER SHIPMENT DATE :49: CONFIRMATION INSTRUCTIONS WITHOUT :78: INSTRUCTIONS TO PAYING/ACCEPTING/NEGOTIATING BANK {bank_instructions} """ lc = self swift_text = swift_template.format( lc_number=lc.name or "TO_BE_ASSIGNED", issue_date=lc.issue_date.strftime("%y%m%d") if lc.issue_date else datetime.now().strftime("%y%m%d"), expiry_date=lc.expiry_date.strftime("%y%m%d") if lc.expiry_date else "", expiry_place=lc.expiry_place or "", applicant=lc.applicant.rec_name if lc.applicant else "", beneficiary=lc.beneficiary.rec_name if lc.beneficiary else "", currency=lc.currency.code if lc.currency else "", amount=str(lc.amount) if lc.amount else "", available_with="ANY BANK BY NEGOTIATION", partial_shipment=lc.partial_shipment or "NOT ALLOWED", transhipment=lc.transhipment or "NOT ALLOWED", port_of_loading=lc.port_of_loading.name if lc.port_of_loading else "", port_of_discharge=lc.port_of_discharge.name if lc.port_of_discharge else "", latest_shipment_date=lc.latest_shipment_date.strftime("%y%m%d") if lc.latest_shipment_date else "", goods_description=self._get_goods_description(), documents_required=self._get_documents_swift(), additional_conditions=self._get_additional_conditions(), presentation_days=lc.presentation_days or 21, bank_instructions=self.bank_instructions or "PLEASE FORWARD ALL DOCUMENTS TO US IN ONE LOT BY COURIER." ) # Crée le message SWIFT # SwiftMessage = Pool().get('lc.swift.message') # swift_message = SwiftMessage.create([{ # 'lc': lc.id, # 'message_type': 'MT700', # 'direction': 'outgoing', # 'message_text': swift_text, # 'message_date': datetime.now(), # 'status': 'draft', # 'reference': f"{lc.name}_MT700" if lc.name else None, # }])[0] return swift_text def _get_goods_description(self): """Description des marchandises pour SWIFT""" lc = self if lc.purchase and lc.purchase.lines: desc = [] for line in lc.purchase.lines: desc.append(f"{line.quantity} {line.unit.rec_name} of {line.product.rec_name}") return "\n".join(desc) return "AS PER PROFORMA INVOICE" def _get_documents_swift(self): """Liste des documents pour SWIFT""" lc = self docs = [] if lc.required_documents: for doc in lc.required_documents: docs.append(f"+ {doc.name}") return "\n".join(docs) if docs else "COMMERCIAL INVOICE\nPACKING LIST\nBILL OF LADING" def _get_additional_conditions(self): """Conditions additionnelles""" lc = self conditions = [] if lc.incoterm: conditions.append(f"INCOTERMS {lc.incoterm.code}") if lc.tolerance_plus or lc.tolerance_minus: tolerance = f"TOLERANCE {lc.tolerance_plus or 0}/+{lc.tolerance_minus or 0} PERCENT" conditions.append(tolerance) return "\n".join(conditions) def send_to_bank(self): """Envoie la LC à la banque""" self.lc.state = 'submitted' self.lc.save() return True