398 lines
13 KiB
Python
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 |