Files
tradon/modules/purchase_trade/outgoing.py
2025-12-26 13:11:43 +00:00

398 lines
13 KiB
Python

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