Initial import from Docker volume
This commit is contained in:
398
modules/purchase_trade/outgoing.py
Normal file
398
modules/purchase_trade/outgoing.py
Normal file
@@ -0,0 +1,398 @@
|
||||
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
|
||||
Reference in New Issue
Block a user