Initial import from Docker volume

This commit is contained in:
root
2025-12-26 13:11:43 +00:00
commit 4998dc066a
13336 changed files with 1767801 additions and 0 deletions

View File

@@ -0,0 +1,197 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.pool import Pool
from . import purchase,sale,global_reporting,stock,derivative,lot,pricing,workflow,lc,dashboard,fee,payment_term,purchase_prepayment,cron,party,forex,outgoing,incoming,optional,association_tables, document_tracking, open_position, credit_risk
def register():
Pool.register(
association_tables.LCDocumentTypeProductProfile,
association_tables.LCLetterDocumentType,
association_tables.LCTemplateCountry,
association_tables.LCTemplateProduct,
association_tables.LCDocType,
association_tables.LCTemplate,
association_tables.ContractDocumentType,
document_tracking.LCDocumentReceived,
document_tracking.LCDocumentPrepared,
document_tracking.LCDiscrepancy,
incoming.LCIncoming,
outgoing.LCOutgoing,
incoming.ImportSwiftStart,
incoming.ImportSwift,
lc.LCMT700,
lc.LCMessage,
lc.CreateLCStart,
global_reporting.GRConfiguration,
module='purchase_trade', type_='model')
Pool.register(
incoming.ImportSwift,
incoming.PrepareDocuments,
incoming.AnalyzeConditions,
lc.CreateLCWizard,
module='purchase_trade', type_='wizard'
)
Pool.register(
credit_risk.Party,
credit_risk.CreditRiskRule,
credit_risk.PartyInternalLimit,
credit_risk.PartyInsuranceLimit,
credit_risk.PartyPaymentCondition,
credit_risk.PartyAcceptableCurrency,
dashboard.Dashboard,
dashboard.DashboardContext,
dashboard.Incoming,
dashboard.BotAction,
dashboard.News,
dashboard.Demos,
party.Party,
payment_term.PaymentTerm,
payment_term.PaymentTermLine,
purchase.Purchase,
purchase.Line,
purchase.Estimated,
purchase.Component,
purchase.Pricing,
purchase.Summary,
purchase.StockLocation,
purchase_prepayment.CreatePrepaymentStart,
purchase_prepayment.PrepaymentMessage,
purchase.Currency,
purchase.Unit,
purchase.PurchaseInvoiceReport,
purchase.PurchaseInvoiceContext,
open_position.OpenPosition,
open_position.OpenPositionReport,
open_position.OpenPositionContext,
optional.OptionalScenario,
fee.Fee,
fee.FeeLots,
purchase.FeeLots,
fee.Valuation,
fee.ValuationDyn,
derivative.Derivative,
derivative.DerivativeMatch,
derivative.MatchWizardStart,
derivative.DerivativeReport,
derivative.DerivativeReportContext,
fee.FeeReport,
fee.FeeContext,
forex.ForexCoverPhysicalContract,
forex.PForex,
forex.ForexBI,
purchase.PnlBI,
stock.Move,
stock.InvoiceLine,
stock.ShipmentIn,
stock.ShipmentInternal,
stock.ShipmentOut,
stock.StatementOfFacts,
stock.SoFEvent,
stock.ImportSoFStart,
stock.RevaluateStart,
stock.Account,
stock.AccountMoveLine,
stock.AccountMove,
stock.ContainerType,
stock.ShipmentContainer,
lot.Lot,
lot.LotQt,
lot.LotReport,
lot.LotContext,
lot.LotShippingStart,
lot.LotMatchingStart,
lot.LotWeighingStart,
lot.LotAddLot,
lot.LotInvoicingLot,
lot.LotInvoicingFee,
lot.LotInvoicingInv,
lot.LotAddLine,
lot.LotImportLot,
lot.LotInvoiceStart,
lot.LotMatchingLot,
lot.LotWeighingLot,
lot.ContractsStart,
lot.ContractDetail,
lot.LotFCR,
lot.LotMove,
lot.LotAccountingGraph,
workflow.ExecutionPlan,
workflow.Event,
workflow.Relation,
workflow.Option,
workflow.OptionLine,
cron.Cron,
cron.PriceCron,
purchase.DocType,
purchase.ContractDocumentType,
purchase.DocTemplate,
purchase.DocTypeTemplate,
purchase.Mtm,
module='purchase', type_='model')
Pool.register(
forex.Forex,
forex.ForexCoverFees,
forex.ForexCategory,
pricing.Component,
pricing.Mtm,
pricing.Estimated,
pricing.Pricing,
pricing.Period,
pricing.Trigger,
purchase.PurchaseCertification,
purchase.PurchaseCertificationWeightBasis,
purchase.PurchaseAssociation,
purchase.PurchaseCrop,
module='lot', type_='model')
Pool.register(
sale.Sale,
sale.SaleLine,
sale.SaleCreatePurchaseInput,
sale.Derivative,
sale.Valuation,
sale.Fee,
sale.Lot,
sale.FeeLots,
sale.Estimated,
sale.Component,
sale.Pricing,
sale.Summary,
forex.SForex,
forex.ForexCoverPhysicalSale,
sale.ContractDocumentType,
sale.Mtm,
sale.OpenPosition,
module='sale', type_='model')
Pool.register(
lot.LotShipping,
lot.LotMatching,
#lot.LotMatchingUnit,
lot.LotWeighing,
lot.CreateContracts,
lot.LotUnmatch,
lot.LotUnship,
lot.LotRemove,
lot.LotInvoice,
lot.LotAdding,
lot.LotImporting,
stock.FindVessel,
stock.SofUpdate,
stock.Revaluate,
purchase.GoToBi,
purchase_prepayment.CreatePrepaymentWizard,
purchase.PurchaseAllocationsWizard,
purchase.InvoicePayment,
stock.ImportSoFWizard,
dashboard.BotWizard,
dashboard.DashboardLoader,
forex.ForexReport,
purchase.PnlReport,
derivative.DerivativeMatchWizard,
module='purchase', type_='wizard')
Pool.register(
sale.SaleCreatePurchase,
sale.SaleAllocationsWizard,
module='sale', type_='wizard')

View File

@@ -0,0 +1,45 @@
from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import Pool, PoolMeta
class LCDocumentTypeProductProfile(ModelSQL):
"LC Document Type - Product Profile"
__name__ = 'lc.document.type-product.profile'
doc_type = fields.Many2One('lc.document.type', 'Document Type')
# product_profile = fields.Many2One('lc.product.profile', 'Product Profile')
class LCLetterDocumentType(ModelSQL):
"LC Letter - Document Type"
__name__ = 'lc.letter-document.type'
lc_out = fields.Many2One('lc.letter.outgoing', 'LC out')
lc_in = fields.Many2One('lc.letter.incoming', 'LC in')
type = fields.Many2One('document.type', 'Document Type')
sale = fields.Many2One('sale.sale', "Sale")
purchase = fields.Many2One('purchase.purchase', "Purchase")
class ContractDocumentType(metaclass=PoolMeta):
"Contract - Document Type"
__name__ = 'contract.document.type'
lc_out = fields.Many2One('lc.letter.outgoing', 'LC out')
lc_in = fields.Many2One('lc.letter.incoming', 'LC in')
class LCTemplateCountry(ModelSQL):
"LC Template - Country"
__name__ = 'lc.template-country'
template = fields.Many2One('lc.template', 'Template')
country = fields.Many2One('country.country', 'Country')
class LCTemplateProduct(ModelSQL):
"LC Template - Product"
__name__ = 'lc.template-product'
template = fields.Many2One('lc.template', 'Template')
product = fields.Many2One('product.product', 'Product')
class LCDocType(ModelSQL,ModelView):
"LC document - Type"
__name__ = 'lc.document.type'
name = fields.Char('Name')
class LCTemplate(ModelSQL):
"LC template"
__name__ = 'lc.template'
name = fields.Char('Name')

View File

@@ -0,0 +1,17 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.pool import Pool
from . import purchase, price
def register():
Pool.register(
purchase.Line,
price.Component,
price.Rule,
price.Period,
price.Pricing,
price.Trigger,
module='purchase', type_='model')

View File

@@ -0,0 +1,63 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.model import fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id
from trytond.model import (ModelSQL, ModelView)
class Component(ModelSQL, ModelView):
"Component"
__name__ = 'price.component'
line = fields.Many2One('purchase.line',"Line")
fix_type = fields.Many2One('price.fixtype',"Fixation type")
ratio = fields.Numeric("%")
price_index = fields.Many2One('price.price',"Price index")
price_value = fields.Numeric("Price")
calendar = fields.Many2One('price.calendar',"Calendar")
rule = fields.Many2One('price.rule',"Price rules")
nbdays = fields.Integer("Nb days")
quota = fields.Numeric("Quota")
class Rule(ModelSQL,ModelView):
"Rule"
__name__ = 'price.rule'
name = fields.Char("Name")
triggers = fields.One2Many('price.trigger','rule',"Triggers")
periods = fields.One2Many('price.period','rule',"Periods")
class Trigger(ModelSQL,ModelView):
"Trigger"
__name__ = "Trigger"
rule = fields.Many2One('price.rule',"Trigger")
trigger = fields.Selection([
(None, ''),
('bldate', 'BL date'),
('ctdate', 'Contract date'),
], 'Trigger')
before = fields.Integer("Nb days before")
after = fields.Integer("Nb days after")
class Period(ModelSQL,ModelView):
"Period"
__name__ = 'price.period'
rule = fields.Many2One('price.rule',"Rules")
from = fields.Date("From")
to = fields.Date("To")
class Pricing(ModelSQL,ModelView):
"Pricing"
__name__ = 'price.pricing'
pricing_date = fields.Date("Date")
price_component = fields.Many2One('price.component',"Component")
quantity = fields.Numeric("Qt")
settl_price = fields.Numeric("Settl. price")
fixed_qt = fields.Numeric("Fixed qt")
fixed_qt_price = fields.Numeric("Fixed qt price")
unfixed_qt = fields.Numeric("Unfixed qt")
unfixed_qt_price = fields.Numeric("Unfixed qt price")
eod_price = fields.Numeric("EOD price")

View File

@@ -0,0 +1,12 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.model import fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id
from trytond.model import (ModelSQL, ModelView)
class Line(metaclass=PoolMeta):
__name__ = 'purchase.line'
price_components = fields.One2Many('price.component','line',"Components")
price_pricing = fields.One2Many('price.pricing','line',"Pricing")

View File

@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="purchase_line_view_form">
<field name="model">purchase.line</field>
<field name="inherit" ref="purchase.purchase_line_view_form"/>
<field name="name">purchase_line_form</field>
</record>
</data>
</tryton>

View File

@@ -0,0 +1,8 @@
[tryton]
version=7.2.7
depends:
ir
purchase
res
xml:
purchase.xml

View File

@@ -0,0 +1,18 @@
<form>
<label name="fix_type"/>
<field name="fix_type"/>
<label name="ratio"/>
<field name="ratio"/>
<label name="price_index"/>
<field name="price_index"/>
<label name="price_value"/>
<field name="price_value"/>
<label name="calendar"/>
<field name="calendar"/>
<label name="rule"/>
<field name="rule"/>
<label name="nbdays"/>
<field name="nbdays"/>
<label name="quota"/>
<field name="quota"/>
</form>

View File

@@ -0,0 +1,10 @@
<tree>
<field name="fix_type"/>
<field name="ratio"/>
<field name="price_index"/>
<field name="price_value"/>
<field name="calendar"/>
<field name="rule"/>
<field name="nbdays"/>
<field name="quota"/>
</tree>

View File

@@ -0,0 +1,10 @@
<tree>
<field name="fix_type"/>
<field name="ratio"/>
<field name="price_index"/>
<field name="price_value"/>
<field name="calendar"/>
<field name="rule"/>
<field name="nbdays"/>
<field name="quota"/>
</tree>

View File

@@ -0,0 +1,6 @@
<form>
<label name="from"/>
<field name="from"/>
<label name="to"/>
<field name="to"/>
</form>

View File

@@ -0,0 +1,4 @@
<tree>
<field name="from"/>
<field name="to"/>
</tree>

View File

@@ -0,0 +1,4 @@
<tree>
<field name="from"/>
<field name="to"/>
</tree>

View File

@@ -0,0 +1,20 @@
<form>
<label name="pricing_date"/>
<field name="pricing_date"/>
<label name="price_component"/>
<field name="price_component"/>
<label name="quantity"/>
<field name="quantity"/>
<label name="settl_price"/>
<field name="settl_price"/>
<label name="fixed_qt"/>
<field name="fixed_qt"/>
<label name="fixed_qt_price"/>
<field name="fixed_qt_price"/>
<label name="unfixed_qt"/>
<field name="unfixed_qt"/>
<label name="unfixed_qt_price"/>
<field name="unfixed_qt_price"/>
<label name="eod_price"/>
<field name="eod_price"/>
</form>

View File

@@ -0,0 +1,11 @@
<tree>
<field name="pricing_date"/>
<field name="price_component"/>
<field name="quantity"/>
<field name="settl_price"/>
<field name="fixed_qt"/>
<field name="fixed_qt_price"/>
<field name="unfixed_qt"/>
<field name="unfixed_qt_price"/>
<field name="eod_price"/>
</tree>

View File

@@ -0,0 +1,11 @@
<tree>
<field name="pricing_date"/>
<field name="price_component"/>
<field name="quantity"/>
<field name="settl_price"/>
<field name="fixed_qt"/>
<field name="fixed_qt_price"/>
<field name="unfixed_qt"/>
<field name="unfixed_qt_price"/>
<field name="eod_price"/>
</tree>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="/form/notebook/notebook" position="inside">
<page string="Components" col="4" id="components">
<field name="price_components" mode="tree,form" colspan="4"
view_ids="price.component_view_tree_sequence,price.component_view_form"/>
</page>
<page string="Pricing" col="4" id="pricing">
<field name="price_pricing" mode="tree,form" colspan="4"
view_ids="price.pricing_view_tree_sequence,price.pricing_view_form"/>
</page>
</xpath>
</data>

View File

@@ -0,0 +1,11 @@
<form>
<label name="name"/>
<field name="name"/>
<label name="triggers"/>
<field name="triggers" mode="tree,form" colspan="4"
view_ids="price.trigger_view_tree_sequence,price.trigger_view_form"/>
<label name="periods"/>
<field name="periods" mode="tree,form" colspan="4"
view_ids="price.period_view_tree_sequence,price.period_view_form"/>
</form>

View File

@@ -0,0 +1,3 @@
<tree>
<field name="name"/>
</tree>

View File

@@ -0,0 +1,3 @@
<tree>
<field name="name"/>
</tree>

View File

@@ -0,0 +1,8 @@
<form>
<label name="trigger"/>
<field name="trigger"/>
<label name="before"/>
<field name="before"/>
<label name="after"/>
<field name="after"/>
</form>

View File

@@ -0,0 +1,5 @@
<tree>
<field name="trigger"/>
<field name="before"/>
<field name="after"/>
</tree>

View File

@@ -0,0 +1,5 @@
<tree>
<field name="trigger"/>
<field name="before"/>
<field name="after"/>
</tree>

View File

@@ -0,0 +1,333 @@
from trytond.model import ModelView, ModelSQL, fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
from trytond.transaction import Transaction
from trytond.model import Workflow
from trytond.report import Report
from datetime import date, timedelta
__all__ = ['Party', 'CreditRiskRule', 'CreditRiskReport']
__metaclass__ = PoolMeta
class Party(metaclass=PoolMeta):
"Extend party with credit risk fields"
__name__ = 'party.party'
credit_limit = fields.Numeric('Credit Limit', digits=(16, 2))
credit_currency = fields.Many2One('currency.currency', 'Credit Currency')
credit_exposure = fields.Function(
fields.Numeric('Credit Exposure', digits=(16, 2)),
'get_credit_exposure'
)
aging_0_30 = fields.Function(
fields.Numeric('Aging 0-30', digits=(16, 2)),
'get_aging_buckets'
)
aging_31_60 = fields.Function(
fields.Numeric('Aging 31-60', digits=(16, 2)),
'get_aging_buckets'
)
aging_61_90 = fields.Function(
fields.Numeric('Aging 61-90', digits=(16, 2)),
'get_aging_buckets'
)
aging_90p = fields.Function(
fields.Numeric('Aging >90', digits=(16, 2)),
'get_aging_buckets'
)
utilization = fields.Function(
fields.Numeric('Utilization %', digits=(5, 2)),
'get_utilization'
)
risk_score = fields.Function(
fields.Integer('Risk Score'),
'get_risk_score'
)
risk_level = fields.Selection([
(None,''),
('low', 'Low'),
('medium', 'Medium'),
('high', 'High'),
], 'Risk Level')
acceptable_currencies = fields.One2Many(
'party.acceptable.currency', 'party',
'Acceptable Currencies'
)
payment_conditions = fields.One2Many(
'party.payment.condition', 'party',
'Payment Conditions'
)
internal_limit = fields.One2Many(
'party.internal.limit', 'party',
'Internal limits'
)
insurance_limit = fields.One2Many(
'party.insurance.limit', 'party',
'Insurance limits'
)
# -------------------------
# Calculations
# -------------------------
def get_credit_exposure(self, name):
"""
Sum of open receivable move lines amounts for this party.
We take account.move.line where account.kind = 'receivable' and
reconciliation is None (i.e. open).
"""
MoveLine = Pool().get('account.move.line')
# Search open receivable lines for this party
lines = MoveLine.search([
('party', '=', self.id),
#('account.kind', '=', 'receivable'),
('reconciliation', '=', None),
])
amount = sum([l.amount or 0 for l in lines])
return amount
def get_aging_buckets(self, name):
"""
Returns a tuple (a0, a1, a2, a3) corresponding to the bucket requested.
We'll compute all buckets then return the requested field.
"""
MoveLine = Pool().get('account.move.line')
today = date.today()
lines = MoveLine.search([
('party', '=', self.id),
#('account.kind', '=', 'receivable'),
('reconciliation', '=', None),
])
a0 = a1 = a2 = a3 = 0
for l in lines:
# Try to use 'due_date' if present, else 'date'
line_date = getattr(l, 'due_date', None) or getattr(l, 'date', None) or today
if not line_date:
line_date = today
days = (today - line_date).days
amt = l.amount or 0
if days <= 30:
a0 += amt
elif days <= 60:
a1 += amt
elif days <= 90:
a2 += amt
else:
a3 += amt
if name == 'aging_0_30':
return a0
if name == 'aging_31_60':
return a1
if name == 'aging_61_90':
return a2
if name == 'aging_90p':
return a3
return 0
def get_utilization(self, name):
limit_ = self.credit_limit or 0
exposure = self.get_credit_exposure('credit_exposure')
if not limit_ or limit_ == 0:
return 0
util = (exposure / limit_) * 100
return float(round(util, 2))
def get_risk_score(self, name):
"""
Compute a simple risk score:
- base on utilization and >90 days exposure
- more sophisticated logic can be plugged via CreditRiskRule.
"""
score = 0
util = self.get_utilization('utilization') # percentage
if util > 100:
score += 50
elif util > 80:
score += 30
elif util > 50:
score += 10
overdue = self.get_aging_buckets('aging_90p')
if overdue > 0:
# scale by overdue relative to limit
limit = self.credit_limit or 1
score += int(min(40, (overdue / float(limit)) * 100))
# cap
if score > 100:
score = 100
# derive level
if score >= 70:
level = 'high'
elif score >= 30:
level = 'medium'
else:
level = 'low'
# ensure we persist risk_level locally (not stored field; update if present)
try:
# If we want to store it persistently, we would need a stored field
self.risk_level = level
except Exception:
pass
return int(score)
class CreditRiskRule(ModelSQL, ModelView):
"""
Simple rules table to tweak scoring thresholds.
Example usage: company-specific thresholds.
"""
__name__ = 'credit.risk.rule'
name = fields.Char('Name', required=True)
company = fields.Many2One('company.company', 'Company')
max_utilization_ok = fields.Float('Max Utilization OK', digits=(5, 2))
warn_utilization = fields.Float('Warn Utilization', digits=(5, 2))
block_on_over_limit = fields.Boolean('Block on Over Limit')
def __str__(self):
return self.name
class PartyInternalLimit(ModelSQL, ModelView):
"Party Internal Limit"
__name__ = 'party.internal.limit'
party = fields.Many2One('party.party', 'Party', required=True)
amount = fields.Numeric('Amount', digits=(16, 2), required=True)
currency = fields.Many2One('currency.currency', 'Currency', required=True)
date_from = fields.Date('From', required=True)
date_to = fields.Date('To', required=True)
remarks = fields.Char('Remarks')
class PartyInsuranceLimit(ModelSQL, ModelView):
"Party Insurance Limit"
__name__ = 'party.insurance.limit'
party = fields.Many2One('party.party', 'Party', required=True)
amount = fields.Numeric('Amount', digits=(16, 2))
currency = fields.Many2One('currency.currency', 'Currency')
date_from = fields.Date('From')
date_to = fields.Date('To')
insurer = fields.Char('Insurance')
policy = fields.Char('Policy')
class PartyPaymentCondition(ModelSQL, ModelView):
"Party Payment Condition"
__name__ = 'party.payment.condition'
party = fields.Many2One('party.party', 'Party', required=True)
payment_term = fields.Many2One('account.invoice.payment_term','Payment Term', required=True)
remaining_risk = fields.Numeric(
'Remaining Risk %', digits=(5, 2)
)
remarks = fields.Text('Remarks')
class PartyAcceptableCurrency(ModelSQL, ModelView):
"Party Acceptable Currency"
__name__ = 'party.acceptable.currency'
party = fields.Many2One(
'party.party', 'Party', required=True, ondelete='CASCADE'
)
currency = fields.Many2One(
'currency.currency', 'Currency', required=True
)
haircut = fields.Numeric('Haircut %', digits=(5, 2))
# class CreditRiskReport(ModelView):
# """
# A report model (non stored) to expose a table-query-like result via context.
# The get_context method returns 'rows' which is a list of dicts ready for templates.
# """
# __name__ = 'party.credit_risk_report'
# start_date = fields.Date('Start Date')
# end_date = fields.Date('End Date')
# company = fields.Many2One('company.company', 'Company')
# @classmethod
# def get_report_data(cls, start=None, end=None, company=None):
# """
# Return aggregated rows:
# - party_id, name, credit_limit, exposure, utilization, aging buckets, risk_score, risk_level, status
# We'll fetch parties with at least some receivable or credit limit set.
# """
# Party = Pool().get('party.party')
# MoveLine = Pool().get('account.move.line')
# # build parties list: those with credit limit or receivable lines
# parties = Party.search([
# ('credit_limit', '!=', None)
# ])
# # also include parties with receivable move lines
# ml_parties = MoveLine.search([('account.kind', '=', 'receivable')])
# ml_party_ids = set([l.party.id for l in ml_parties if l.party])
# for pid in ml_party_ids:
# try:
# p = Party(pid)
# if p not in parties:
# parties.append(p)
# except Exception:
# pass
# rows = []
# for p in parties:
# exposure = p.get_credit_exposure('credit_exposure')
# a0 = p.get_aging_buckets('aging_0_30')
# a1 = p.get_aging_buckets('aging_31_60')
# a2 = p.get_aging_buckets('aging_61_90')
# a3 = p.get_aging_buckets('aging_90p')
# utilization = p.get_utilization('utilization')
# score = p.get_risk_score('risk_score')
# level = getattr(p, 'risk_level', None) or (
# 'high' if score >= 70 else ('medium' if score >= 30 else 'low')
# )
# status = 'OK'
# if p.credit_limit and exposure > p.credit_limit:
# status = 'OVER_LIMIT'
# rows.append({
# 'party_id': p.id,
# 'party_name': p.name,
# 'credit_limit': p.credit_limit or 0,
# 'exposure': exposure,
# 'aging_0_30': a0,
# 'aging_31_60': a1,
# 'aging_61_90': a2,
# 'aging_90p': a3,
# 'utilization_pct': utilization,
# 'risk_score': score,
# 'risk_level': level,
# 'status': status,
# })
# # Optionally sort by risk_score desc
# rows = sorted(rows, key=lambda r: r['risk_score'], reverse=True)
# return rows
# @classmethod
# def get_context(cls, records, data):
# """
# Called by Report action to supply context.
# We will return dict with 'rows' and some summary aggregates.
# """
# start = data.get('start_date') if data else None
# end = data.get('end_date') if data else None
# company = data.get('company') if data else None
# rows = cls.get_report_data(start=start, end=end, company=company)
# total_limit = sum([r['credit_limit'] for r in rows])
# total_exposure = sum([r['exposure'] for r in rows])
# over_limit = [r for r in rows if r['status'] == 'OVER_LIMIT']
# ctx = {
# 'rows': rows,
# 'total_limit': total_limit,
# 'total_exposure': total_exposure,
# 'count_over_limit': len(over_limit),
# 'image_path': '/mnt/data/f9976760-c385-48ed-b51c-fa5868a0f0ab.png', # optional
# }
# return ctx

View File

@@ -0,0 +1,69 @@
<?xml version="1.0"?>
<tryton>
<data>
<!-- Inherit party form to add Credit Risk tab -->
<record model="ir.ui.view" id="view_party_credit_risk_form">
<field name="model">party.party</field>
<field name="inherit" ref="party.party_view_form"/>
<field name="name">party_view_form</field>
</record>
<record model="ir.ui.view" id="view_party_acc_risk_tree">
<field name="model">party.acceptable.currency</field>
<field name="type">tree</field>
<field name="name">party_acc_tree</field>
</record>
<record model="ir.ui.view" id="view_party_pay_risk_tree">
<field name="model">party.payment.condition</field>
<field name="type">tree</field>
<field name="name">party_pay_tree</field>
</record>
<record model="ir.ui.view" id="view_party_int_risk_tree">
<field name="model">party.internal.limit</field>
<field name="type">tree</field>
<field name="name">party_int_tree</field>
</record>
<record model="ir.ui.view" id="view_party_ins_risk_tree">
<field name="model">party.insurance.limit</field>
<field name="type">tree</field>
<field name="name">party_ins_tree</field>
</record>
<!-- <record model="ir.ui.view" id="view_credit_risk_report_tree">
<field name="model">party.credit_risk_report</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree>
<field name="start_date"/>
<field name="end_date"/>
<field name="company"/>
</tree>
</field>
</record>
<record model="ir.act_window" id="act_credit_risk_overview">
<field name="name">Credit Risk Overview</field>
<field name="res_model">party.credit_risk_report</field>
<field name="view_mode">form,tree</field>
<field name="view_id" eval="False"/>
<field name="target">current</field>
</record>
<record model="ir.ui.menu" id="menu_credit_risk">
<field name="name">Credit Risk</field>
<field name="parent" ref="party.menu_reporting"/>
<field name="action" ref="act_credit_risk_overview"/>
</record>
<record model="ir.action.report" id="party_credit_risk_report_action">
<field name="name">Credit Risk Overview</field>
<field name="model">party.credit_risk_report</field>
<field name="report_name">account_credit_risk.credit_risk_overview</field>
<field name="report">credit_risk_templates.xml</field>
<field name="direct_print" eval="False"/>
</record> -->
</data>
</tryton>

82
modules/purchase_trade/cron.py Executable file
View File

@@ -0,0 +1,82 @@
import requests
from decimal import getcontext, Decimal, ROUND_HALF_UP
from datetime import datetime
from trytond.model import fields
from trytond.model import (ModelSQL, ModelView)
from trytond.pool import Pool, PoolMeta
import logging
logger = logging.getLogger(__name__)
class Cron(metaclass=PoolMeta):
__name__ = 'ir.cron'
@classmethod
def __setup__(cls):
super().__setup__()
cls.method.selection.append(
('forex.cron|update_forex', "Update Forex Prices"))
class PriceCron(ModelSQL, ModelView):
"Price Cron"
__name__ = 'forex.cron'
frequency = fields.Selection([
('daily', "Daily"),
('weekly', "Weekly"),
('monthly', "Monthly"),
], "Frequency", required=True,
help="How frequently rates must be updated.")
last_update = fields.Date("Last Update", required=True)
@classmethod
def __setup__(cls):
super().__setup__()
cls._buttons.update({
'run': {},
})
@classmethod
def default_frequency(cls):
return 'daily'
@classmethod
def default_last_update(cls):
pool = Pool()
Date = pool.get('ir.date')
return Date.today()
@classmethod
@ModelView.button
def run(cls, crons):
cls.update_forex(crons)
@classmethod
def update_forex(cls):
Currency = Pool().get('currency.currency')
Rate = Pool().get('currency.currency.rate')
# On suppose que l'EUR existe déjà dans la base
eur, = Currency.search([('name', '=', 'EUR')])
# Appel API Frankfurter
url = "https://api.frankfurter.app/latest"
params = {"base": "EUR", "symbols": "USD"}
resp = requests.get(url, params=params)
resp.raise_for_status()
data = resp.json()
rate_value = round(Decimal(data["rates"]["USD"]),4)
rate_date = datetime.strptime(data["date"], "%Y-%m-%d").date()
logger.info(f"Taux EUR/USD : {rate_value} ({rate_date})")
# Vérifie si un taux existe déjà pour ce jour
existing = Rate.search([
('currency', '=', eur.id),
('date', '=', rate_date),
])
if not existing and rate_value:
rate = Rate(currency=eur, rate=round(1/rate_value,6), date=rate_date)
rate.save()
logger.info(f"Taux EUR/USD ajouté : {rate_value} ({rate_date})")
else:
logger.info(f"Taux déjà existant pour {rate_date}")

45
modules/purchase_trade/cron.xml Executable file
View File

@@ -0,0 +1,45 @@
<?xml version="1.0"?>
<tryton>
<data>
<record model="ir.ui.view" id="cron_view_list">
<field name="model">forex.cron</field>
<field name="type">tree</field>
<field name="name">cron_list</field>
</record>
<record model="ir.ui.view" id="cron_view_form">
<field name="model">forex.cron</field>
<field name="type">form</field>
<field name="name">cron_form</field>
</record>
<record model="ir.action.act_window" id="act_cron_form">
<field name="name">Scheduled Forex Updates</field>
<field name="res_model">forex.cron</field>
</record>
<record model="ir.action.act_window.view" id="act_cron_form_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="cron_view_list"/>
<field name="act_window" ref="act_cron_form"/>
</record>
<record model="ir.action.act_window.view" id="act_cron_form_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="cron_view_form"/>
<field name="act_window" ref="act_cron_form"/>
</record>
<!-- <menuitem parent="menu_price" action="act_cron_form" sequence="20" id="menu_cron_form"/> -->
<!-- <record model="ir.model.button" id="cron_run_button">
<field name="name">run</field>
<field name="string">Run</field>
<field name="model" search="[('model', '=', 'forex.cron')]"/>
</record> -->
<record model="ir.cron" id="cron_cron">
<field name="method">forex.cron|update_forex</field>
<field name="interval_number" eval="1"/>
<field name="interval_type">days</field>
</record>
</data>
</tryton>

View File

@@ -0,0 +1,773 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.model import ModelSQL, ModelView, fields, sequence_ordered, ModelSingleton
from trytond.pyson import Eval
from trytond.transaction import Transaction
from trytond.pool import Pool, PoolMeta
from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction
from decimal import getcontext, Decimal, ROUND_HALF_UP
import jwt
import datetime
import logging
from trytond.exceptions import UserWarning, UserError
import json
import shlex
from datetime import date
logger = logging.getLogger(__name__)
keywords = ["duplicate", "create", "add", "invoice", "reception", "pay", "delivery", "match"]
class DashboardLoader(Wizard):
'Load Dashboard'
__name__ = 'purchase.dashboard.loader'
start = StateAction('purchase_trade.act_dashboard_form')
def do_start(self, action):
pool = Pool()
Dashboard = pool.get('purchase.dashboard')
user_id = Transaction().user
if user_id == 0:
user_id = Transaction().context.get('user', user_id)
dashboard = Dashboard.search([('user_id', '=', user_id)])
if not dashboard:
dashboard, = Dashboard.create([{'user_id': user_id}])
else:
dashboard = dashboard[0]
action['views'].reverse()
return action, {'res_id': [dashboard.id]}
class DashboardContext(ModelView):
"Dashboard context"
__name__ = 'dashboard.context'
user = fields.Many2One('res.user', 'User')
company = fields.Many2One('company.company', 'Company', required=True)
@staticmethod
def default_company():
return Transaction().context.get('company')
@staticmethod
def default_user():
user_id = Transaction().user
if user_id == 0:
user_id = Transaction().context.get('user', user_id)
return user_id
class Demos(ModelSQL, ModelView):
'Demos'
__name__ = 'demos.demos'
title = fields.Char('Title', required=True)
category = fields.Char('Category')
icon = fields.Char('Icon') # exemple: "🚢" ou "📝"
active = fields.Boolean('Active')
video_file = fields.Char('File')
description = fields.Text('Description')
class News(ModelSQL, ModelView):
'News'
__name__ = 'news.news'
title = fields.Char('Title', required=True)
category = fields.Char('Category')
icon = fields.Char('Icon') # exemple: "🚢" ou "📝"
active = fields.Boolean('Active')
publish_date = fields.Date('Publish Date')
model = fields.Char('Model') # ex: 'purchase.purchase'
model_id = fields.Integer('Record ID')
class Dashboard(ModelSQL, ModelView):
"Dashbot"
__name__ = "purchase.dashboard"
stock = fields.One2Many('lot.lot','dashboard',"STOCK")
stock2 = fields.One2Many('lot.lot','dashboard',"PRODUCT")
stock3 = fields.One2Many('lot.lot','dashboard',"CURVE")
transit = fields.One2Many('stock.shipment.in','dashboard',"VESSEL IN TRANSIT")
document = fields.One2Many('document.incoming','dashboard',"Documents received")
html_content = fields.Function(fields.Text('HTML Content'),'get_news')
demos = fields.Function(fields.Text('Demos'),'get_demos')
#metabase = fields.Function(fields.Text("",states={'invisible': ~Eval('metabase',False)}),'gen_url')
metabase = fields.Text("",states={'invisible': ~Eval('metabase',False)})
chatbot = fields.Text("")
input = fields.Text("")
bi_id = fields.Integer("")
user = fields.Function(fields.Char(""), 'get_user')
fake = fields.Char("",readonly=True)
res_id = fields.Integer("Res id")
action_return = fields.Char("Action return")
actions = fields.One2Many('bot.action','dashboard',"Actions")
user_request = fields.Char("User request")
user_id = fields.Many2One('res.user',"User")
tremor = fields.Function(fields.Text(""),'get_tremor')
@classmethod
def default_bi_id(cls):
return 38
@classmethod
def default_res_id(cls):
return 0
@fields.depends('input')
def on_change_input(self):
logger.info("ENTERONCHANGE",self.input)
Lot = Pool().get('lot.lot')
self.metabase = None
if self.input is None:
self.chatbot = 'chatbot:' + json.dumps([{"type": "bot", "content": "🧠 Hi Admin, how can I help you?"}], ensure_ascii=False)
else:
dial = json.loads(self.input)
last_content = dial[-1]["content"]
last_type = dial[-1]["type"]
logger.info("ON_CHANGE_INPUT:%s",last_content)
message = ''
if "display" in last_content.lower() and "clients" in last_content.lower():
dial.append({"type": "bot", "content": "Clients displayed below"})
if self.bi_id != 7:
self.bi_id = 7
self.metabase = self.gen_url()
elif "display" in last_content.lower() and "sales" in last_content.lower():
dial.append({"type": "bot", "content": "Sales displayed below"})
if self.bi_id != 41:
self.bi_id = 41
self.metabase = self.gen_url()
elif "display" in last_content.lower() and "products" in last_content.lower():
dial.append({"type": "bot", "content": "Products displayed below"})
if self.bi_id != 38:
self.bi_id = 38
self.metabase = self.gen_url()
elif "open" in last_content.lower() and "shipment" in last_content.lower():
sh = self.FindInContent('shipment',last_content)
if sh:
dial.append({"type": "bot", "content": "Ok shipment opened in new tab"})
dial.append({"type": "do", "content": "open,stock.shipment.in," + str(sh.id) + ",0"})
else:
dial.append({"type": "bot", "content": "No shipment found"})
elif "where" in last_content.lower() and "vessel" in last_content.lower():
sh = self.FindInContent('shipment',last_content)
if sh:
dial.append({"type": "bot", "content": "Look at the map below"})
self.metabase = f"imo:{sh.vessel.vessel_imo}"
elif "graph" in last_content.lower() and "lot" in last_content.lower():
g,l,i = last_content.split()
lot = Lot(i)
if lot:
dial.append({"type": "bot", "content": "Graph displayed below"})
self.metabase = lot.get_flow_graph()
elif "pivot" in last_content.lower() and "lot" in last_content.lower():
g,l,i = last_content.split()
lot = Lot(i)
if lot:
dial.append({"type": "bot", "content": "Pivot displayed below"})
self.metabase = lot.get_pivot()
elif "display" in last_content.lower() and "map" in last_content.lower():
dial.append({"type": "bot", "content": "Map displayed below"})
self.metabase = self.get_map()
elif any(k in last_content.lower() for k in keywords):
logger.info("ACTION_RETURN:%s",self.action_return)
if "help" in last_content.lower():
if "duplicate" in last_content.lower():
dial.append({"type": "bot", "content": "duplicate [purchase/sale]* [party]* [qt]* [plan] [period] [price]"})
elif self.action_return:
model,res_id, res_nb = self.action_return.split(",")
z,keyword = model.split(".")
message = {
"type": "bot",
"content": (
f'Action done. '
f'<a href="#" class="tryton-link" data-model="{model}" '
f'data-id="{res_id}" data-view="form">Open {keyword} {res_nb}</a>'
)
}
dial.append(message)
else:
if "match" in last_content.lower():
dial.append({"type": "bot", "content": "Matching done"})
else:
dial.append({"type": "bot", "content": "Not done"})
else:
dial.append({"type": "bot", "content": "Ok done"})
self.chatbot = 'chatbot:' + json.dumps(dial, ensure_ascii=False)
logger.info("EXITONCHANGE",self.chatbot)
def get_last_two_fx_rates(self, from_code='USD', to_code='EUR'):
"""
Retourne (dernier_taux, avant_dernier_taux) pour le couple de devises.
"""
Currency = Pool().get('currency.currency')
CurrencyRate = Pool().get('currency.currency.rate')
# Récupérer les devises EUR et USD
from_currency = Currency.search([('name', '=', from_code)])[0]
to_currency = Currency.search([('name', '=', to_code)])[0]
# Recherche des taux de la devise de base (ex: USD)
rates = CurrencyRate.search(
[('currency', '=', to_currency.id)],
order=[('date', 'DESC')],
limit=2,
)
if not rates:
return None, None
# Calcul du taux EUR/USD
# Si la devise principale de la société est EUR, et que le taux stocké est
# "1 USD = X EUR", on veut l'inverse pour avoir EUR/USD
last_rate = rates[0].rate
prev_rate = rates[1].rate if len(rates) > 1 else None
# if from_currency != to_currency:
# last_rate = 1 / last_rate if last_rate else None
# prev_rate = 1 / prev_rate if prev_rate else None
if last_rate and prev_rate:
return round(1/last_rate,6), round(1/prev_rate,6)
def get_tremor(self,name):
Pnl = Pool().get('valuation.valuation')
pnls = Pnl.search(['id','>',0])
pnl_amount = "{:,.0f}".format(round(sum([e.amount for e in pnls]),0))
Open = Pool().get('open.position')
opens = Open.search(['id','>',0])
exposure = "{:,.0f}".format(round(sum([e.net_exposure for e in opens]),0))
ToPay = Pool().get('account.invoice')
topays = ToPay.search(['type','=','in'])
amounts = ToPay.get_amount_to_pay(topays)
total = sum(amounts.values(), Decimal(0))
topay = "{:,.0f}".format(round(total,0))
ToReceive = Pool().get('account.invoice')
toreceives = ToReceive.search(['type','=','out'])
amounts = ToPay.get_amount_to_pay(toreceives)
total = sum(amounts.values(), Decimal(0))
toreceive = "{:,.0f}".format(round(total,0))
Purchase = Pool().get('purchase.purchase')
draft = Purchase.search(['state','=','draft'])
draft_p = len(draft)
val = Purchase.search(['state','=','quotation'])
val_p = len(val)
conf = Purchase.search(['state','=','confirmed'])
conf_p = len(conf)
Sale = Pool().get('sale.sale')
draft = Sale.search(['state','=','draft'])
draft_s = len(draft)
val = Sale.search(['state','=','quotation'])
val_s = len(val)
conf = Sale.search(['state','=','confirmed'])
conf_s = len(conf)
Shipment = Pool().get('stock.shipment.in')
draft = Shipment.search(['state','=','draft'])
shipment_d = len(draft)
val = Purchase.search(['state','=','started'])
shipment_s = len(val)
conf = Purchase.search(['state','=','received'])
shipment_r = len(conf)
Lot = Pool().get('lot.lot')
lots = Lot.search(['sale_line','!=',None])
lot_m = len(lots)
val = Lot.search(['sale_line','=',None])
lot_a = len(val)
conf = Lot.search(['lot_type','=','physic'])
lot_al = len(conf)
Invoice = Pool().get('account.invoice')
invs = Invoice.search(['type','=','in'])
inv_p = len(invs)
invs = Invoice.search(['type','=','out'])
inv_s = len(invs)
AccountMove = Pool().get('account.move')
accs = AccountMove.search(['id','>',0])
move_cash = len(accs)
return (
"https://srv413259.hstgr.cloud/dashboard/index.html?pnl_amount="
+ str(pnl_amount)
+ "&exposure="
+ str(exposure)
+ "&topay="
+ str(topay)
+ "&toreceive="
+ str(toreceive)
+ "&draft_p="
+ str(draft_p)
+ "&val_p="
+ str(val_p)
+ "&conf_p="
+ str(conf_p)
+ "&draft_s="
+ str(draft_s)
+ "&val_s="
+ str(val_s)
+ "&conf_s="
+ str(conf_s)
+ "&shipment_d="
+ str(shipment_d)
+ "&shipment_s="
+ str(shipment_s)
+ "&shipment_r="
+ str(shipment_r)
+ "&lot_m="
+ str(lot_m)
+ "&lot_a="
+ str(lot_a)
+ "&lot_al="
+ str(lot_al)
+ "&inv_p="
+ str(inv_p)
+ "&inv_s="
+ str(inv_s)
+ "&move_cash="
+ str(move_cash)
)
def get_news(self, name):
News = Pool().get('news.news')
Date = Pool().get('ir.date')
news_list = News.search([('active', '=', True)], limit=5, order=[('publish_date', 'DESC')])
last_rate,prev_rate = self.get_last_two_fx_rates()
if last_rate and prev_rate:
variation = ((last_rate - prev_rate) / prev_rate) * 100 if prev_rate else 0
direction = "📈" if variation > 0 else "📉"
# 🆕 Création dune “fake news” locale
forex_news = News()
forex_news.icon = direction
forex_news.category = "Forex"
# Détermine la direction et le style couleur
if variation > 0:
arrow = "⬆️"
color = "#28a745" # vert
elif variation < 0:
arrow = "⬇️"
color = "#dc3545" # rouge
else:
arrow = "➡️"
color = "#6c757d" # gris neutre
# Contenu HTML enrichi
forex_news.title = (
f"EUR/USD: {last_rate:.4f}"
f"<span style='color:{color}; font-weight:bold;'>"
f" {variation:+.2f}%"
f"</span> "
)
forex_news.publish_date = Date.today()
forex_news.model = 'currency.currency' # par exemple 'forex.forex'
forex_news.model_id = 2
forex_news.active = True
# On insère la “news” au début de la liste
news_list.insert(0, forex_news)
html_parts = [
'<div class="last-news">',
' <div class="last-news-title">Last news</div>'
]
for n in news_list:
icon = n.icon or "📰"
category = n.category or "General"
title = n.title or ""
# content = (n.content or "").replace('\n', '<br/>') <div class="last-news-content">{content}</div>
publish_date = n.publish_date.strftime('%d-%m-%Y') if n.publish_date else ""
# Génération du lien Tryton
if n.model and n.model_id:
# 🆕 lien JS pour ouvrir un tab interne Tryton
js_link = (
f"javascript:Sao.Tab.create({{"
f"model: '{n.model}', "
f"res_id: {n.model_id}, "
f"mode: ['form'], "
f"target: 'new'"
f"}});"
)
title_html = (
f'<a href="{js_link}" style="text-decoration:none; color:#2a5db0;">'
f'{title}</a>'
)
else:
title_html = f'<span>{title}</span>'
html_parts.append(f'''
<div class="last-news-item" style="margin-bottom: 8px;">
<div class="last-news-category">{icon} {category}</div>
<div class="last-news-header" style="display:flex; justify-content:space-between; align-items:center;">
<div class="last-news-title-item"><strong>{title_html}</strong></div>
<div class="last-news-date" style="color:#666; font-size:0.9em;">{publish_date}</div>
</div>
</div>
''')
html_parts.append('</div>')
return "\n".join(html_parts)
def get_demos(self, name):
Demos = Pool().get('demos.demos')
html_parts = [
'<div class="demos" style="background-color:#f5f5f5; padding:12px; border-radius:12px;>',
' <div class="demos-title" style="font-size:1.2em; font-weight:bold; margin-bottom:10px;">🎬 Available Demo</div>'
]
demos = Demos.search([('active', '=', True)])
for n in demos:
icon = n.icon or "📰"
category = n.category or "General"
title = n.title or "Sans titre"
description = n.description or ""
# suppose que n.video_path contient un chemin du type '/videos/demo1.mp4'
video_url = f"/videos/{n.video_file}" if getattr(n, 'video_file', None) else None
# lien vers le lecteur vidéo du navigateur
video_link = (
f'<a href="{video_url}" target="_blank" '
'style="color:#007bff; text-decoration:none;">Play the video 🎥</a>'
if video_url else
'<span style="color:gray;">No available video</span>'
)
html_parts.append(f'''
<div class="demo-item" style="margin-bottom: 12px; border-bottom:1px solid #eee; padding-bottom:8px;">
<div class="demo-category" style="font-size:0.9em; color:#666;">{icon} {category}</div>
<div class="demo-header" style="display:flex; justify-content:space-between; align-items:center;">
<div class="demo-title" style="font-weight:bold;">{title}</div>
<div class="demo-link">{video_link}</div>
</div>
<div class="demo-description" style="font-size:0.85em; color:#555;">{description}</div>
</div>
''')
html_parts.append('</div>')
return "\n".join(html_parts)
def get_map(self):
departure = { "name":"SANTOS","lat": str(-23.9), "lon": str(-46.3) }
arrival = { "name":"ISTANBUL","lat": str(41), "lon": str(29) }
data = {
"highlightedCountryNames": [{"name":"Turkey"},{"name":"Brazil"}],
"routes": [[
{ "lon": -46.3, "lat": -23.9 },
{ "lon": -30.0, "lat": -20.0 },
{ "lon": -30.0, "lat": 0.0 },
{ "lon": -6.0, "lat": 35.9 },
{ "lon": 15.0, "lat": 38.0 },
{ "lon": 29.0, "lat": 41.0 }
]],
"boats": [{
"name": "CARIBBEAN 1",
"imo": "1234567",
"lon": -30.0,
"lat": 0.0,
"status": "En route",
"links": [
{ "text": "Voir sur VesselFinder", "url": "https://www.vesselfinder.com" },
{ "text": "Détails techniques", "url": "https://example.com/tech" }
],
"actions": [
{ "type": "track", "id": "123", "label": "Suivre ce bateau" },
{ "type": "details", "id": "123", "label": "Voir détails" }
]
}],
"cottonStocks": [
{ "name":"Mali","lat": 12.65, "lon": -8.0, "amount": 300 },
{ "name":"Egypte","lat": 30.05, "lon": 31.25, "amount": 500 },
{ "name":"Irak","lat": 33.0, "lon": 44.0, "amount": 150 }
],
"departures": [departure],
"arrivals": [arrival]
}
return "d3:" + json.dumps(data)
def FindInContent(self,what,content):
if what == 'shipment':
Shipment = Pool().get('stock.shipment.in')
shipments = Shipment.search([('id','>',0),('vessel','!=',None)],order=[('create_date', 'DESC')])
if shipments:
for sh in shipments:
if sh.vessel.vessel_name.lower() in content.lower():
return sh
elif what == 'purchase':
Purchase = Pool().get('purchase.purchase')
purchases = Purchase.search([('id','>',0)],order=[('create_date', 'DESC')])
if purchases:
for pu in purchases:
if pu.party.name.lower() in content.lower():
return pu
def gen_url(self,name=None):
payload = {
"resource": {"dashboard": self.bi_id},
"params": {},
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30),
}
token = jwt.encode(payload, "798f256d3119a3292bf121196c2a38dddf2cad155c0b6b0b444efc34c6db197c", algorithm="HS256")
logger.info("TOKEN:%s",token)
return f"metabase:http://vps107.geneva.hosting:3000/embed/dashboard/{token}#bordered=true&titled=true"
# @fields.depends('input')
# def on_change_with_metabase(self):
# return self.gen_url()
# @classmethod
# def default_html_content(cls):
# return 'http://62.72.36.116:3000/question/41-sales-by-clients-and-suppliers'
def get_user(self,name):
User = Pool().get('res.user')
user = User(Transaction().user)
return "Welcome " + user.name + " !"
class Incoming(metaclass=PoolMeta):
__name__ = 'document.incoming'
dashboard = fields.Many2One('purchase.dashboard',"Dashboard")
class BotWizard(Wizard):
"Bot wizard"
__name__ = "bot.wizard"
start = StateTransition()
def get_model(self,keyword):
if keyword == 'purchase':
return 'purchase.purchase'
elif keyword == 'sale':
return 'sale.sale'
elif 'prepayment' in keyword:
return 'account.invoice'
def transition_start(self):
Dashboard = Pool().get('purchase.dashboard')
Party = Pool().get('party.party')
Account = Pool().get('account.account')
Currency = Pool().get('currency.currency')
Purchase = Pool().get('purchase.purchase')
Sale = Pool().get('sale.sale')
Invoice = Pool().get('account.invoice')
InvoiceLine = Pool().get('account.invoice.line')
StockMove = Pool().get('stock.move')
ExecutionPlan = Pool().get('workflow.plan')
DelPeriod = Pool().get('product.month')
Lot = Pool().get('lot.lot')
LotQt = Pool().get('lot.qt')
LotAdd = Pool().get('lot.add.line')
Date = Pool().get('ir.date')
d = Dashboard(self.records[0])
context = Transaction().context
if d:
d.action_return = ""
Dashboard.save([d])
parts = shlex.split(context['user_request'])
do_, mo, cp, fl, un, un2, un3 = (parts + [None]*7)[:7]
if do_ in keywords:
logger.info("BOT_WIZARD:%s",fl)
parties = Party.search(['id','>',0])
party = None
model = self.get_model(mo)
if cp:
for p in parties:
if cp.lower() == p.name.lower():
party = p
break
if do_ == "duplicate":
if mo == "help":
return 'end'
if mo == "purchase" or mo == "sale":
if party:
Model = Pool().get(model)
if fl:
line_price = None
line_period = None
if un:
plan = ExecutionPlan.search(['name','=', un])
if plan:
models = Model.search([('party','=',party.id),('plan','=',plan[0].id)],order=[('create_date', 'DESC')])
if un3:
line_price = Decimal(un3)
line_period = DelPeriod.search(['month_name','=',un2])[0]
else:
if un2.isdigit():
line_price = Decimal(un2)
else:
periods = DelPeriod.search(['month_name','=',un])
if periods:
line_period = periods[0]
if un2.isdigit():
line_price = Decimal(un2)
else:
if un.isdigit():
line_price = Decimal(un)
else:
models = Model.search([('party','=',party.id)],order=[('create_date', 'DESC')])
if models:
model_id = models[0].id
new_model, = Model.copy([model_id], default={})
Model.save([new_model])
Model.quote([new_model])
logger.info("BOT_WIZARD:%s",new_model.lines)
if new_model.lines and fl:
line = new_model.lines[0]
line.quantity = Decimal(fl)
line.quantity_theorical = Decimal(fl)
line.premium = Decimal(0)
if line_period:
line.del_period = line_period
if line_price:
if line.linked_price > 0:
line.linked_price = line_price
line.unit_price = line.get_price_linked_currency()
else:
line.unit_price = line_price
Pool().get(mo + '.line').save([line])
d.action_return = model + ',' + str(new_model.id) + ',' + str(new_model.number)
Dashboard.save([d])
elif do_ == "match":
Lml = Pool().get('lot.matching.lot')
lmp = Lml()
lms = Lml()
p = Purchase.search(['number','=',mo])
if p:
p = p[0]
s = Sale.search(['number','=',cp])
if s:
s = s[0]
lp = p.lines[0].lots[1]
lmp.lot_id = lp.id
lqt = lp.getLotQt()
lmp.lot_r_id = lqt[0].id if lqt else None
lmp.lot_matched_qt = lp.lot_quantity
ls = s.lines[0].lots[0]
lms.lot_r_id = ls.getLotQt()[0].id
lms.lot_matched_qt = lp.lot_quantity
lms.lot_sale = s
LotQt.match_lots([lmp],[lms])
Sale._process_shipment([s])
elif do_ == "add":
if mo == "lot":
purchase = Purchase.search([('number','=',cp)])
if purchase:
purchase = purchase[0]
vlot = purchase.lines[0].lots[0]
lqt = LotQt.search([('lot_p','=',vlot.id)])
if lqt and vlot.lot_quantity > 0:
lqt = lqt[0]
l = LotAdd()
l.lot_qt = vlot.lot_quantity
l.lot_unit = purchase.lines[0].unit
l.lot_unit_line = l.lot_unit
l.lot_quantity = l.lot_qt
l.lot_gross_quantity = l.lot_qt
l.lot_premium = Decimal(0)
lot_id = LotQt.add_physical_lots(lqt,[l])
d.action_return = 'lot.lot,' + str(lot_id) + ',' + str(lot_id)
Dashboard.save([d])
elif do_ == "reception":
if mo == "lot":
lot = Lot(int(cp))
if lot.lot_move:
StockMove.do([lot.lot_move[0].move])
d.action_return = 'stock.move,' + str(lot.lot_move[0].move.id) + ',' + str(lot.lot_move[0].move.id)
Dashboard.save([d])
elif do_ == "delivery":
if mo == "lot":
lot = Lot(int(cp))
if lot.lot_move:
StockMove.do([lot.lot_move[1].move])
d.action_return = 'stock.move,' + str(lot.lot_move[1].move.id) + ',' + str(lot.lot_move[1].move.id)
Dashboard.save([d])
elif do_ == "invoice":
if mo == "lot":
lot = Lot(int(cp))
Purchase._process_invoice([lot.line.purchase],[lot],'prov')
if lot.invoice_line_prov:
invoice = Invoice(lot.invoice_line_prov.invoice.id)
invoice.invoice_date = Date.today()
if fl == "prepayment":
if un:
invoice.call_deposit(Account(748),un)
else:
invoice.call_deposit(Account(748),'Deposit')
Invoice.validate_invoice([invoice])
d.action_return = 'account.invoice,' + str(lot.invoice_line_prov.invoice.id) + ',' + str(lot.invoice_line_prov.invoice.number)
Dashboard.save([d])
elif do_ == "pay":
if mo == "invoice":
#PayInvoice = Pool().get('account.invoice.pay')
PaymentMethod = Pool().get('account.invoice.payment.method')
inv = Invoice.search(['number','=',cp])
if inv:
pm = PaymentMethod.search(['id','>',0])[0]
inv = inv[0]
lines = inv.pay_invoice(
inv.amount_to_pay, pm, Date.today(),
'Payment of invoice ' + inv.number, 0, party=inv.party.id)
if lines:
d.action_return = 'account.invoice,' + str(inv.id) + ',' + inv.number
Dashboard.save([d])
elif do_ == "create":
if mo == "prepaymentP" or mo == "prepaymentS":
if party:
if party.addresses:
invoice = Invoice()
invoice.type = 'in' if mo == "prepaymentP" else 'out'
invoice.journal = 2 if mo == "prepaymentP" else 1#invoice.set_journal()
logger.info("WIZARD_PREPAY:%s",invoice.journal)
invoice.invoice_date = Date.today()
invoice.party = party.id
invoice.invoice_address = party.addresses[0]
invoice.account = party.account_payable_used if mo == "prepaymentP" else party.account_receivable_used
invoice.payment_term = 6
invoice.description = 'Prepayment supplier' if mo == "prepaymentP" else 'Prepayment client'
if un:
curr = Currency.search(['name','=', un])
if curr:
invoice.currency = curr[0].id
line = InvoiceLine()
line.account = 748
line.quantity = 1
line.unit_price = Decimal(fl)
invoice.lines = [line]
Invoice.save([invoice])
Invoice.post([invoice])
d.action_return = model + ',' + str(invoice.id)+ ',' + str(invoice.number)
Dashboard.save([d])
return 'end'
class BotAction(ModelSQL, ModelView):
"Bot Action"
__name__ = "bot.action"
dashboard = fields.Many2One('purchase.dashboard',"Dashboard")
type = fields.Selection([
('duplicate', 'duplicate'),
('invoice', 'invoice'),
('receive', 'receive'),
('add physic', 'add physic'),
('match', 'match'),
('link', 'link'),
('create', 'create')])
model = fields.Char("Model")
am_qt = fields.Numeric("Amount/Quantity")
unit = fields.Char("Unit/Currency")
keyword = fields.Char("Keyword")
cp = fields.Char("Party")
model_id = fields.Integer("Model id")
res_id = fields.Integer("Res id")
state = fields.Selection([
('todo', 'todo'),
('done', 'done'),
])
@classmethod
def state_default(cls):
return 'todo'
@classmethod
def state_dashboard(cls):
return 1

View File

@@ -0,0 +1,167 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.icon" id="sale_icon">
<field name="name">tradon-sale</field>
<field name="path">icons/tradon-sale.svg</field>
</record>
<record model="ir.ui.icon" id="invoice_icon">
<field name="name">tradon-account</field>
<field name="path">icons/tradon-account.svg</field>
</record>
<record model="ir.ui.icon" id="payment_icon">
<field name="name">tradon-price</field>
<field name="path">icons/tradon-price.svg</field>
</record>
<record model="ir.ui.icon" id="allocation_icon">
<field name="name">tradon-allocation</field>
<field name="path">icons/tradon-allocation.svg</field>
</record>
<record model="ir.ui.icon" id="purchase_icon">
<field name="name">tradon-purchase</field>
<field name="path">icons/tradon-purchase.svg</field>
</record>
<record model="ir.ui.icon" id="stock_shipment_in">
<field name="name">tradon-shipment-in</field>
<field name="path">icons/tradon-shipment-in.svg</field>
</record>
<record model="ir.ui.view" id="dashboard_view_form">
<field name="model">purchase.dashboard</field>
<field name="type">form</field>
<field name="name">dashboard_form</field>
</record>
<record model="ir.ui.view" id="dashboard_view_list">
<field name="model">purchase.dashboard</field>
<field name="type">tree</field>
<field name="name">dashboard_list</field>
</record>
<record model="ir.action.act_window" id="act_dashboard_form">
<field name="name">Dashboard</field>
<field name="res_model">purchase.dashboard</field>
<field name="context_model">dashboard.context</field>
<field name="context_domain"
eval="[('user_id', '=', Eval('user', -1))]"
pyson="1"/>
</record>
<record model="ir.action.act_window.view"
id="act_dashboard_view2">
<field name="sequence" eval="2"/>
<field name="view" ref="dashboard_view_form"/>
<field name="act_window" ref="act_dashboard_form"/>
</record>
<record model="ir.action.act_window.view"
id="act_dashboard_view">
<field name="sequence" eval="1"/>
<field name="view" ref="dashboard_view_list"/>
<field name="act_window" ref="act_dashboard_form"/>
</record>
<record model="ir.action.wizard" id="act_dashboard_loader">
<field name="name">Load Dashboard</field>
<field name="wiz_name">purchase.dashboard.loader</field>
</record>
<record model="ir.action.act_window" id="act_sale_not_confirmed_form">
<field name="name">Sales not confirmed</field>
<field name="res_model">sale.sale</field>
<field
name="domain"
eval="[('state', '!=', 'confirmed')]"
pyson="1"/>
</record>
<record model="ir.action.act_window" id="act_sale_not_shipped_form">
<field name="name">Sales not shipped</field>
<field name="res_model">sale.sale</field>
<field
name="domain"
eval="[('shipment_state', '=', None)]"
pyson="1"/>
</record>
<record model="ir.action.act_window" id="act_invoice_not_validated_form">
<field name="name">Invoices not validated</field>
<field name="res_model">account.invoice</field>
<field
name="domain"
eval="[('state', '=', 'draft')]"
pyson="1"/>
</record>
<record model="ir.action.act_window" id="act_invoice_not_paid_form">
<field name="name">Invoices not paid</field>
<field name="res_model">account.invoice</field>
<field
name="domain"
eval="[('state', '!=', 'paid')]"
pyson="1"/>
</record>
<record model="ir.action.act_window" id="act_payment_not_received_form">
<field name="name">Payments not received</field>
<field name="res_model">account.payment</field>
<field
name="domain"
eval="[('state', '=', 'draft')]"
pyson="1"/>
</record>
<record model="ir.action.act_window" id="act_payment_not_done_form">
<field name="name">Payments not done</field>
<field name="res_model">account.payment</field>
<field
name="domain"
eval="[('state', '=', 'draft')]"
pyson="1"/>
</record>
<record model="ir.action.act_window" id="act_allocation_not_matched_form">
<field name="name">Lots not matched</field>
<field name="res_model">lot.report</field>
</record>
<record model="ir.action.act_window" id="act_allocation_matched_form">
<field name="name">Lots matched</field>
<field name="res_model">lot.report</field>
<field name="domain" eval="[('r_lot_matched', '>', 0)]" pyson="1"/>
<field name="context" eval="{'purchase':None,'sale':None,'shipment':None,'type':'matched','state':'all','wh':'all','group':'by_physic'}" pyson="1"/>
</record>
<record model="ir.action.act_window" id="act_shipment_not_received_form">
<field name="name">Shipments not received</field>
<field name="res_model">stock.shipment.in</field>
<field
name="domain"
eval="[('state', '!=', 'received')]"
pyson="1"/>
</record>
<record model="ir.action.act_window" id="act_shipment_not_ordered_form">
<field name="name">Shipments not ordered</field>
<field name="res_model">stock.shipment.in</field>
<field
name="domain"
eval="[('state', '!=', 'received')]"
pyson="1"/>
</record>
<record model="ir.action.act_window" id="act_purchase_not_confirmed_form">
<field name="name">Purchases not confirmed</field>
<field name="res_model">purchase.purchase</field>
<field
name="domain"
eval="[('state', '!=', 'confirmed')]"
pyson="1"/>
</record>
<record model="ir.action.act_window" id="act_purchase_not_shipped_form">
<field name="name">Purchases not shipped</field>
<field name="res_model">purchase.purchase</field>
<field
name="domain"
eval="[('shipment_state', '=', 'none')]"
pyson="1"/>
</record>
<record model="ir.action.wizard" id="act_bot_wizard">
<field name="name">Bot Wizard</field>
<field name="wiz_name">bot.wizard</field>
<field name="model">purchase.dashboard</field>
</record>
<menuitem
action="act_dashboard_loader"
sequence="1"
id="menu_purchase_dashboard"/>
</data>
</tryton>

View File

@@ -0,0 +1,401 @@
import datetime
from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import Pool
from trytond.transaction import Transaction
from sql import Literal
from sql.functions import CurrentTimestamp
from trytond.wizard import Button, StateTransition, StateView, Wizard
from trytond.pyson import Bool, Eval, Id, If
class DerivativeMatch(ModelSQL, ModelView):
"Derivative Match"
__name__ = 'derivative.match'
long_position = fields.Many2One('derivative.derivative', 'Long Position', required=True)
short_position = fields.Many2One('derivative.derivative', 'Short Position', required=True)
quantity = fields.Numeric('Matched Quantity', digits='unit', required=True)
match_date = fields.Date('Match Date', required=True)
method = fields.Selection([
('fifo', 'FIFO'),
('lifo', 'LIFO'),
('manual', 'Manual'),
], 'Method', required=True)
pnl = fields.Function(fields.Numeric('PnL', digits='currency'), 'get_pnl')
def get_pnl(self, name):
return (self.short_position.price - self.long_position.price) * self.quantity
class Derivative(ModelSQL,ModelView):
"Derivative"
__name__ = 'derivative.derivative'
purchase = fields.Many2One('purchase.purchase',"Purchase")
line = fields.Many2One('purchase.line',"Purch. Line")
efp = fields.Boolean("EFP")
price_index = fields.Many2One('price.price',"Curve")
nb_ct = fields.Integer("Nb ct")
price = fields.Numeric("Entry price",digits='currency')
exit_price = fields.Numeric("Exit price",digits='currency')
product = fields.Many2One('product.product',"Product")
party = fields.Many2One('party.party','Supplier')
currency = fields.Function(fields.Many2One('currency.currency',"Cur"),'get_cur')
quantity = fields.Function(fields.Numeric("Quantity",digits='unit'),'get_qt')
alloc_qty = fields.Function(fields.Numeric("Alloc. Qty",digits='unit'),'get_alloc_qt')
unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit')
amount = fields.Function(fields.Numeric("Amount",digits='currency'),'get_amount')
currency2 = fields.Function(fields.Many2One('currency.currency',"Cur"),'get_cur2')
trade_date = fields.Date('Trade Date')
maturity_date = fields.Date('Maturity')
open_qty = fields.Numeric('Open Quantity', digits='unit')
matches_long = fields.One2Many('derivative.match', 'long_position', 'Matches (Long)', states={'invisible': (Eval('direction') == 'long')})
matches_short = fields.One2Many('derivative.match', 'short_position', 'Matches (Short)', states={'invisible': (Eval('direction') == 'short')})
direction = fields.Selection([
('long', 'Long'),
('short', 'Short'),
], 'Direction', required=True)
state = fields.Selection([
('open', 'Open'),
('closed', 'Closed'),
], 'State', required=True)
@classmethod
def default_state(cls):
return 'open'
def get_amount(self,name):
if self.price_index and self.line and self.price and self.nb_ct:
#pi = Pool().get('price.price')(self.price_index)
return self.price_index.get_amount_nb_ct(self.price,self.nb_ct,self.line.unit,self.line.purchase.currency if self.line.purchase else self.sale_line.sale.currency)
def get_cur(self,name):
if self.price_index:
#pi = Pool().get('price.price')(self.price_index)
return self.price_index.price_currency
def get_cur2(self,name):
if self.purchase:
return self.purchase.currency
def get_unit(self,name):
if self.line:
return self.line.unit
def get_qt(self,name):
if self.line:
line = self.line
else:
if self.purchase and self.purchase.lines:
line = self.purchase.lines[0]
else:
line = None
if self.price_index and self.nb_ct and line:
return self.price_index.get_qt(self.nb_ct,line.unit)
def get_alloc_qt(self,name):
if self.line:
line = self.line
else:
if self.purchase and self.purchase.lines:
line = self.purchase.lines[0]
else:
line = None
if self.price_index and self.nb_ct and line:
if line.price_summary:
return line.price_summary[0].fixed_qt
class MatchWizardStart(ModelView):
"Match Selected Derivatives"
__name__ = 'derivative.match.start'
method = fields.Selection([
('fifo', 'FIFO'),
('lifo', 'LIFO'),
('manual', 'Manual'),
], 'Method', required=True)
quantity = fields.Numeric(
'Quantity to Match',
digits='unit',
required=True
)
class DerivativeMatchWizard(Wizard):
"Derivative Match Wizard"
__name__ = 'derivative.match.wizard'
start = StateView(
'derivative.match.start',
'purchase_trade.derivative_match_start_form_view',
[
Button('Cancel', 'end', 'tryton-cancel'),
Button('Match', 'match', 'tryton-ok', default=True),
]
)
match = StateTransition()
def transition_match(self):
pool = Pool()
Derivative = pool.get('derivative.derivative')
Date = pool.get('ir.date')
Match = pool.get('derivative.match')
active_ids = Transaction().context.get('active_ids', [])
if not active_ids:
return 'end'
positions = Derivative.browse(active_ids)
longs = [
p for p in positions
if p.direction == 'long'
and p.state == 'open'
and p.open_qty > 0
]
shorts = [
p for p in positions
if p.direction == 'short'
and p.state == 'open'
and p.open_qty > 0
]
if not longs or not shorts:
return 'end'
# FIFO / LIFO ordering
reverse = self.start.method == 'lifo'
longs.sort(key=lambda p: p.trade_date or datetime.date.min, reverse=reverse)
shorts.sort(key=lambda p: p.trade_date or datetime.date.min, reverse=reverse)
remaining_qty = self.start.quantity
for long_pos in longs:
if remaining_qty <= 0:
break
for short_pos in shorts:
if remaining_qty <= 0:
break
match_qty = min(
long_pos.open_qty,
short_pos.open_qty,
remaining_qty
)
if match_qty <= 0:
continue
Match.create([{
'long_position': long_pos.id,
'short_position': short_pos.id,
'quantity': match_qty,
'method': self.start.method,
'match_date': Date.today(),
}])
long_pos.open_qty -= match_qty
short_pos.open_qty -= match_qty
remaining_qty -= match_qty
if long_pos.open_qty == 0:
long_pos.state = 'closed'
if short_pos.open_qty == 0:
short_pos.state = 'closed'
long_pos.save()
short_pos.save()
return 'end'
class DerivativeReport(ModelSQL, ModelView):
"Derivative Position Report"
__name__ = 'derivative.report'
r_derivative = fields.Many2One(
'derivative.derivative',
"Derivative"
)
r_trade_date = fields.Date("Trade Date")
r_maturity_date = fields.Date("Maturity")
r_product = fields.Many2One(
'product.product',
"Product"
)
r_party = fields.Many2One(
'party.party',
"Counterparty"
)
r_direction = fields.Selection([
(None, ''),
('long', 'Long'),
('short', 'Short'),
], 'Direction')
r_state = fields.Selection([
('open', 'Open'),
('closed', 'Closed'),
], 'State')
r_quantity = fields.Function(fields.Numeric("Initial Qty",digits='unit'),'get_qt')
r_open_qty = fields.Numeric(
"Open Qty",
digits='unit'
)
r_price = fields.Numeric(
"Entry Price",
digits='currency'
)
r_currency = fields.Function(fields.Many2One('currency.currency',"Currency"),'get_cur')
def get_cur(self,name):
if self.r_derivative:
if self.r_derivative.price_index:
return self.r_derivative.price_index.price_currency
def get_qt(self,name):
if self.r_derivative:
if self.r_derivative.line:
line = self.r_derivative.line
else:
if self.r_derivative.purchase and self.r_derivative.purchase.lines:
line = self.r_derivative.purchase.lines[0]
else:
line = None
if self.r_derivative.price_index and self.r_derivative.nb_ct and line:
return self.r_derivative.price_index.get_qt(self.r_derivative.nb_ct,line.unit)
# ------------------------------------------------------------
# TABLE QUERY
# ------------------------------------------------------------
@classmethod
def table_query(cls):
pool = Pool()
Derivative = pool.get('derivative.derivative')
d = Derivative.__table__()
context = Transaction().context
product = context.get('product')
party = context.get('party')
direction = context.get('direction')
state = context.get('state')
trade_from = context.get('trade_from')
trade_to = context.get('trade_to')
maturity_from = context.get('maturity_from')
maturity_to = context.get('maturity_to')
open_only = context.get('open_only')
wh = Literal(True)
if product:
wh &= (d.product == product)
if party:
wh &= (d.party == party)
if direction:
wh &= (d.direction == direction)
if state:
wh &= (d.state == state)
if open_only:
wh &= (d.open_qty > 0)
if trade_from:
wh &= (d.trade_date >= trade_from)
if trade_to:
wh &= (d.trade_date <= trade_to)
if maturity_from:
wh &= (d.maturity_date >= maturity_from)
if maturity_to:
wh &= (d.maturity_date <= maturity_to)
query = d.select(
# mandatory technical fields
Literal(0).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
Literal(None).as_('write_uid'),
Literal(None).as_('write_date'),
d.id.as_('id'),
d.id.as_('r_derivative'),
d.trade_date.as_('r_trade_date'),
d.maturity_date.as_('r_maturity_date'),
d.product.as_('r_product'),
d.party.as_('r_party'),
d.direction.as_('r_direction'),
d.state.as_('r_state'),
#d.quantity.as_('r_quantity'),
d.open_qty.as_('r_open_qty'),
d.price.as_('r_price'),
where=wh
)
return query
# ------------------------------------------------------------
# SEARCH NAME
# ------------------------------------------------------------
@classmethod
def search_rec_name(cls, name, clause):
_, operator, operand, *extra = clause
return [
'OR',
('r_product', operator, operand, *extra),
('r_party', operator, operand, *extra),
]
class DerivativeReportContext(ModelView):
"Derivative Report Context"
__name__ = 'derivative.report.context'
trade_from = fields.Date("Trade Date From")
trade_to = fields.Date("Trade Date To")
maturity_from = fields.Date("Maturity From")
maturity_to = fields.Date("Maturity To")
product = fields.Many2One(
'product.product',
"Product"
)
party = fields.Many2One(
'party.party',
"Counterparty"
)
direction = fields.Selection([
('long', 'Long'),
('short', 'Short'),
], 'Direction')
state = fields.Selection([
('open', 'Open'),
('closed', 'Closed'),
], 'State')
open_only = fields.Boolean("Open Positions Only")
@classmethod
def default_trade_from(cls):
return datetime.date(1999, 1, 1)
@classmethod
def default_trade_to(cls):
pool = Pool()
Date = pool.get('ir.date')
return Date.today()

View File

@@ -0,0 +1,109 @@
<tryton>
<data>
<record model="ir.ui.icon" id="derivative_icon">
<field name="name">tradon-derivative</field>
<field name="path">icons/tradon-derivative.svg</field>
</record>
<record model="ir.ui.view" id="derivative_view_tree_sequence">
<field name="model">derivative.derivative</field>
<field name="type">tree</field>
<field name="name">derivative_tree_sequence</field>
<field name="priority" eval="20"/>
</record>
<record model="ir.ui.view" id="derivative_view_tree">
<field name="model">derivative.derivative</field>
<field name="type">tree</field>
<field name="name">derivative_tree</field>
<field name="priority" eval="10"/>
</record>
<record model="ir.ui.view" id="derivative_view_form">
<field name="model">derivative.derivative</field>
<field name="type">form</field>
<field name="name">derivative_form</field>
</record>
<record model="ir.action.act_window" id="act_derivative_form">
<field name="name">Derivative</field>
<field name="res_model">derivative.derivative</field>
</record>
<record model="ir.action.act_window.view" id="act_derivative_form_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="derivative_view_tree_sequence"/>
<field name="act_window" ref="act_derivative_form"/>
</record>
<record model="ir.action.act_window.view" id="act_derivative_form_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="derivative_view_form"/>
<field name="act_window" ref="act_derivative_form"/>
</record>
<record model="ir.ui.view" id="derivative_report_tree">
<field name="model">derivative.report</field>
<field name="type">tree</field>
<field name="name">derivative_report_tree</field>
</record>
<record model="ir.ui.view" id="derivative_report_context_form">
<field name="model">derivative.report.context</field>
<field name="type">form</field>
<field name="name">derivative_context_form</field>
</record>
<record model="ir.ui.view" id="derivative_match_start_form_view">
<field name="model">derivative.match.start</field>
<field name="type">form</field>
<field name="name">derivative_match_start_form</field>
</record>
<record model="ir.ui.view" id="derivative_match_tree">
<field name="model">derivative.match</field>
<field name="type">tree</field>
<field name="name">derivative_match_tree</field>
</record>
<record model="ir.ui.view" id="derivative_match_tree2">
<field name="model">derivative.match</field>
<field name="type">tree</field>
<field name="name">derivative_match_tree2</field>
</record>
<record model="ir.action.act_window" id="act_dr_form">
<field name="name">Derivative Report</field>
<field name="res_model">derivative.report</field>
<field name="context_model">derivative.report.context</field>
</record>
<record model="ir.action.act_window.view" id="act_dr_form_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="derivative_report_tree"/>
<field name="act_window" ref="act_dr_form"/>
</record>
<record model="ir.action.wizard" id="action_derivative_match">
<field name="name">Match Selected Positions</field>
<field name="wiz_name">derivative.match.wizard</field>
</record>
<record model="ir.action.keyword" id="keyword_derivative_match">
<field name="keyword">form_action</field>
<field name="model">derivative.report,-1</field>
<field name="action" ref="action_derivative_match"/>
</record>
<menuitem
name="Derivative"
sequence="99"
id="menu_derivative"
icon="tradon-derivative"/>
<menuitem
id="menu_derivative_report"
parent="menu_derivative"
sequence="20"
action="act_dr_form"
name="Derivative Report"/>
<menuitem
id="menu_derivative_der"
parent="menu_derivative"
sequence="10"
action="act_derivative_form"
name="Derivative"/>
</data>
</tryton>

Binary file not shown.

View File

@@ -0,0 +1,107 @@
from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import Pool
from datetime import datetime
class LCDocumentReceived(ModelSQL, ModelView):
'LC Document Received'
__name__ = 'lc.document.received'
lc = fields.Many2One('lc.letter.outgoing', 'LC', required=True)
document_type = fields.Many2One('lc.document.type', 'Document Type', required=True)
received = fields.Boolean('Received')
received_date = fields.Date('Received Date')
document_ref = fields.Char('Document Reference')
attachment = fields.Many2One('ir.attachment', 'Attachment')
discrepancy = fields.Boolean('Discrepancy')
discrepancy_details = fields.Text('Discrepancy Details')
status = fields.Selection([
('pending', 'Pending'),
('received', 'Received'),
('checked', 'Checked'),
('discrepant', 'Discrepant'),
('accepted', 'Accepted')
], 'Status')
notes = fields.Text('Notes')
@staticmethod
def default_status():
return 'pending'
@staticmethod
def default_received():
return False
@classmethod
def create(cls, vlist):
vlist = [v.copy() for v in vlist]
for vals in vlist:
if vals.get('received') and not vals.get('received_date'):
vals['received_date'] = datetime.now().date()
if vals.get('received') and vals.get('status') == 'pending':
vals['status'] = 'received'
return super().create(vlist)
class LCDocumentPrepared(ModelSQL, ModelView):
'LC Document Prepared'
__name__ = 'lc.document.prepared'
lc = fields.Many2One('lc.letter.incoming', 'LC', required=True)
document_type = fields.Many2One('lc.document.type', 'Document Type', required=True)
required = fields.Boolean('Required')
responsible = fields.Many2One('res.user', 'Responsible')
deadline = fields.Date('Deadline')
prepared = fields.Boolean('Prepared')
prepared_date = fields.Date('Prepared Date')
attachment = fields.Many2One('ir.attachment', 'Attachment')
status = fields.Selection([
('pending', 'Pending'),
('in_progress', 'In Progress'),
('prepared', 'Prepared'),
('presented', 'Presented'),
('accepted', 'Accepted')
], 'Status')
notes = fields.Text('Notes')
@staticmethod
def default_status():
return 'pending'
@staticmethod
def default_required():
return True
@staticmethod
def default_prepared():
return False
class LCDiscrepancy(ModelSQL, ModelView):
'LC Discrepancy'
__name__ = 'lc.discrepancy'
lc = fields.Many2One('lc.letter.incoming', 'LC', required=True)
document_type = fields.Many2One('lc.document.type', 'Document Type')
discrepancy_type = fields.Selection([
('document', 'Document Discrepancy'),
('date', 'Date Discrepancy'),
('amount', 'Amount Discrepancy'),
('condition', 'Condition Discrepancy'),
('other', 'Other')
], 'Discrepancy Type', required=True)
description = fields.Text('Description', required=True)
severity = fields.Selection([
('minor', 'Minor'),
('major', 'Major'),
('critical', 'Critical')
], 'Severity')
resolved = fields.Boolean('Resolved')
resolution = fields.Text('Resolution')
resolved_date = fields.Date('Resolved Date')
resolved_by = fields.Many2One('res.user', 'Resolved By')
@staticmethod
def default_severity():
return 'minor'
@staticmethod
def default_resolved():
return False

682
modules/purchase_trade/fee.py Executable file
View File

@@ -0,0 +1,682 @@
from functools import wraps
from trytond.model import fields
from trytond.report import Report
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id, If
from trytond.model import (ModelSQL, ModelView)
from trytond.tools import is_full_text, lstrip_wildcard
from trytond.transaction import Transaction, inactive_records
from decimal import getcontext, Decimal, ROUND_HALF_UP
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
from sql.conditionals import Case
from sql import Column, Literal
from sql.functions import CurrentTimestamp, DateTrunc
from trytond.wizard import Button, StateTransition, StateView, Wizard
from itertools import chain, groupby
from operator import itemgetter
import datetime
import logging
from collections import defaultdict
from trytond.exceptions import UserWarning, UserError
logger = logging.getLogger(__name__)
VALTYPE = [
('priced', 'Price'),
('pur. priced', 'Pur. price'),
('pur. efp', 'Pur. efp'),
('sale priced', 'Sale price'),
('sale efp', 'Sale efp'),
('line fee', 'Line fee'),
('pur. fee', 'Pur. fee'),
('sale fee', 'Sale fee'),
('shipment fee', 'Shipment fee'),
('market', 'Market'),
('derivative', 'Derivative'),
]
class Valuation(ModelSQL,ModelView):
"Valuation"
__name__ = 'valuation.valuation'
purchase = fields.Many2One('purchase.purchase',"Purchase")
line = fields.Many2One('purchase.line',"Purch. Line")
date = fields.Date("Date")
type = fields.Selection(VALTYPE, "Type")
reference = fields.Char("Reference")
counterparty = fields.Many2One('party.party',"Counterparty")
product = fields.Many2One('product.product',"Product")
state = fields.Char("State")
price = fields.Numeric("Price",digits='unit')
currency = fields.Many2One('currency.currency',"Cur")
quantity = fields.Numeric("Quantity",digits='unit')
unit = fields.Many2One('product.uom',"Unit")
amount = fields.Numeric("Amount",digits='unit')
mtm = fields.Numeric("Mtm",digits='unit')
lot = fields.Many2One('lot.lot',"Lot")
class ValuationDyn(ModelSQL,ModelView):
"Valuation"
__name__ = 'valuation.valuation.dyn'
r_purchase = fields.Many2One('purchase.purchase',"Purchase")
r_line = fields.Many2One('purchase.line',"Line")
r_date = fields.Date("Date")
r_type = fields.Selection(VALTYPE, "Type")
r_reference = fields.Char("Reference")
r_counterparty = fields.Many2One('party.party',"Counterparty")
r_product = fields.Many2One('product.product',"Product")
r_state = fields.Char("State")
r_price = fields.Numeric("Price",digits='r_unit')
r_currency = fields.Many2One('currency.currency',"Cur")
r_quantity = fields.Numeric("Quantity",digits='r_unit')
r_unit = fields.Many2One('product.uom',"Unit")
r_amount = fields.Numeric("Amount",digits='r_unit')
r_mtm = fields.Numeric("Mtm",digits='r_unit')
r_lot = fields.Many2One('lot.lot',"Lot")
@classmethod
def table_query(cls):
Valuation = Pool().get('valuation.valuation')
val = Valuation.__table__()
context = Transaction().context
group_pnl = context.get('group_pnl')
wh = (val.id > 0)
# query = val.select(
# Literal(0).as_('create_uid'),
# CurrentTimestamp().as_('create_date'),
# Literal(None).as_('write_uid'),
# Literal(None).as_('write_date'),
# val.id.as_('id'),
# val.purchase.as_('r_purchase'),
# val.line.as_('r_line'),
# val.date.as_('r_date'),
# val.type.as_('r_type'),
# val.reference.as_('r_reference'),
# val.counterparty.as_('r_counterparty'),
# val.product.as_('r_product'),
# val.state.as_('r_state'),
# val.price.as_('r_price'),
# val.currency.as_('r_currency'),
# val.quantity.as_('r_quantity'),
# val.unit.as_('r_unit'),
# val.amount.as_('r_amount'),
# val.mtm.as_('r_mtm'),
# val.lot.as_('r_lot'),
# where=wh)
#if group_pnl==True:
query = val.select(
Literal(0).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
Literal(None).as_('write_uid'),
Literal(None).as_('write_date'),
Max(val.id).as_('id'),
Max(val.purchase).as_('r_purchase'),
Max(val.line).as_('r_line'),
Max(val.date).as_('r_date'),
val.type.as_('r_type'),
Max(val.reference).as_('r_reference'),
val.counterparty.as_('r_counterparty'),
Max(val.product).as_('r_product'),
val.state.as_('r_state'),
Avg(val.price).as_('r_price'),
Max(val.currency).as_('r_currency'),
Sum(val.quantity).as_('r_quantity'),
Max(val.unit).as_('r_unit'),
Sum(val.amount).as_('r_amount'),
Sum(val.mtm).as_('r_mtm'),
Max(val.lot).as_('r_lot'),
where=wh,
group_by=[val.type,val.counterparty,val.state])
return query
def filter_state(state):
def filter(func):
@wraps(func)
def wrapper(cls, fees):
fees = [f for f in fees if f.state == state]
return func(cls, fees)
return wrapper
return filter
class Fee(ModelSQL,ModelView):
"Fee"
__name__ = 'fee.fee'
line = fields.Many2One('purchase.line',"Line")
shipment_in = fields.Many2One('stock.shipment.in')
shipment_out = fields.Many2One('stock.shipment.out')
shipment_internal = fields.Many2One('stock.shipment.internal')
currency = fields.Many2One('currency.currency',"Currency")
supplier = fields.Many2One('party.party',"Supplier", required=True)
type = fields.Selection([
('budgeted', 'Budgeted'),
('ordered', 'Ordered'),
('actual', 'Actual'),
], "Type", required=True)
p_r = fields.Selection([
('pay', 'PAY'),
('rec', 'REC'),
], "P/R", required=True)
product = fields.Many2One('product.product',"Product", required=True, domain=[('type', '=', 'service')])
price = fields.Numeric("Price",digits=(1,4))
mode = fields.Selection([
('lumpsum', 'Lump sum'),
('perqt', 'Per qt'),
('pprice', '% price'),
('pcost', '% cost price'),
], 'Mode', required=True)
inherit_qt = fields.Boolean("Inh Qt")
quantity = fields.Function(fields.Numeric("Qt",digits='unit'),'get_quantity')
unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit')
inherit_shipment = fields.Boolean("Inh Sh",states={
'invisible': (Eval('shipment_in')),
})
purchase = fields.Many2One('purchase.purchase',"Purchase", ondelete='CASCADE')
amount = fields.Function(fields.Numeric("Amount", digits='currency'),'get_amount')
fee_lots = fields.Function(fields.Many2Many('lot.lot', None, None, "Lots"),'get_lots')#, searcher='search_lots')
lots = fields.Many2Many('fee.lots', 'fee', 'lot',"Lots",domain=[('id', 'in', Eval('fee_lots',-1))] )
lots_cp = fields.Integer("Lots number")
state = fields.Selection([
('not invoiced', 'Not invoiced'),
('invoiced', 'Invoiced'),
], string='State', readonly=True)
fee_landed_cost = fields.Function(fields.Boolean("Inventory"),'get_landed_status')
inv = fields.Function(fields.Many2One('account.invoice',"Invoice"),'get_invoice')
weight_type = fields.Selection([
('net', 'Net'),
('brut', 'Brut'),
], string='W. type')
def get_lots(self, name):
logger.info("GET_LOTS_LINE:%s",self.line)
logger.info("GET_LOTS_SHIPMENT_IN:%s",self.shipment_in)
Lot = Pool().get('lot.lot')
if self.line:
return self.line.lots
if self.shipment_in:
lots = Lot.search([('lot_shipment_in','=',self.shipment_in.id)])
logger.info("LOTSDOMAIN:%s",lots)
if lots:
return lots + [lots[0].getVlot_p()]
if self.shipment_internal:
return Lot.search([('lot_shipment_internal','=',self.shipment_internal.id)])
if self.shipment_out:
return Lot.search([('lot_shipment_out','=',self.shipment_out.id)])
return Lot.search(['id','>',0])
def get_cog(self,lot):
MoveLine = Pool().get('account.move.line')
Currency = Pool().get('currency.currency')
Date = Pool().get('ir.date')
AccountConfiguration = Pool().get('account.configuration')
account_configuration = AccountConfiguration(1)
Uom = Pool().get('product.uom')
ml = MoveLine.search([
('lot', '=', lot.id),
('fee', '=', self.id),
('account', '=', self.product.account_stock_in_used.id),
('origin', 'ilike', '%stock.move%'),
])
logger.info("GET_COG_FEE:%s",ml)
if ml:
return round(Decimal(sum([e.credit-e.debit for e in ml if e.description != 'Delivery fee'])),2)
@classmethod
def __setup__(cls):
super().__setup__()
cls._buttons.update({
'invoice': {
'invisible': (Eval('state') == 'invoiced'),
'depends': ['state'],
},
})
@classmethod
def default_state(cls):
return 'not invoiced'
@classmethod
def default_p_r(cls):
return 'pay'
def get_unit(self, name):
Lot = Pool().get('lot.lot')
if self.lots:
if self.lots[0].line:
return self.lots[0].line.unit
if self.lots[0].sale_line:
return self.lots[0].sale_line.unit
@classmethod
@ModelView.button
@filter_state('not invoiced')
def invoice(cls, fees):
Purchase = Pool().get('purchase.purchase')
FeeLots = Pool().get('fee.lots')
for fee in fees:
if fee.purchase:
fl = FeeLots.search([('fee','=',fee.id)])
logger.info("PROCESS_FROM_FEE:%s",fl)
Purchase._process_invoice([fee.purchase],[e.lot for e in fl],'service')
cls.write(fees, {'state': 'invoiced',})
@classmethod
def default_type(cls):
return 'budgeted'
@classmethod
def default_weight_type(cls):
return 'brut'
def get_price_per_qt(self):
price = Decimal(0)
if self.mode == 'lumpsum':
if self.quantity:
if self.quantity > 0:
return round(self.price / self.quantity,4)
elif self.mode == 'perqt':
return self.price
elif self.mode == 'pprice':
if self.line and self.price:
return round(self.price * Decimal(self.line.unit_price) / 100,4)
if self.sale_line and self.price:
return round(self.price * Decimal(self.sale_line.unit_price) / 100,4)
if self.shipment_in:
StockMove = Pool().get('stock.move')
sm = StockMove.search(['shipment','=','stock.shipment.in,'+str(self.shipment_in.id)])
if sm:
if sm[0].lot:
return round(self.price * Decimal(sm[0].lot.get_lot_price()) / 100,4)
return price
def get_invoice(self,name):
if self.purchase:
if self.purchase.invoices:
return self.purchase.invoices[0]
def get_landed_status(self,name):
if self.product:
return self.product.landed_cost
def get_quantity(self,name=None):
qt = self.get_fee_lots_qt()
if qt:
return qt
LotQt = Pool().get('lot.qt')
lqts = LotQt.search(['lot_shipment_in','=',self.shipment_in.id])
if lqts:
return Decimal(lqts[0].lot_quantity)
def get_amount(self,name=None):
sign = Decimal(1)
if self.price:
# if self.p_r:
# if self.p_r == 'pay':
# sign = -1
if self.mode == 'lumpsum':
return self.price * sign
elif self.mode == 'perqt':
if self.shipment_in:
StockMove = Pool().get('stock.move')
sm = StockMove.search(['shipment','=','stock.shipment.in,'+str(self.shipment_in.id)])
if sm:
unique_lots = {e.lot for e in sm if e.lot}
return round(self.price * Decimal(sum([e.get_current_quantity_converted() for e in unique_lots])) * sign,2)
LotQt = Pool().get('lot.qt')
lqts = LotQt.search(['lot_shipment_in','=',self.shipment_in.id])
if lqts:
return round(self.price * Decimal(lqts[0].lot_quantity) * sign,2)
return round((self.quantity if self.quantity else 0) * self.price * sign,2)
elif self.mode == 'pprice':
if self.line:
return round(self.price / 100 * self.line.unit_price * (self.quantity if self.quantity else 0) * sign,2)
if self.sale_line:
return round(self.price / 100 * self.sale_line.unit_price * (self.quantity if self.quantity else 0) * sign,2)
if self.shipment_in:
StockMove = Pool().get('stock.move')
sm = StockMove.search(['shipment','=','stock.shipment.in,'+str(self.shipment_in.id)])
if sm:
if sm[0].lot:
return round(self.price * Decimal(sum([e.lot.get_lot_price() for e in sm if e.lot])) / 100 * self.quantity * sign,2)
LotQt = Pool().get('lot.qt')
lqts = LotQt.search(['lot_shipment_in','=',self.shipment_in.id])
if lqts:
return round(self.price * Decimal(lqts[0].lot_p.get_lot_price()) / 100 * lqts[0].lot_quantity * sign,2)
@classmethod
def write(cls, *args):
super().write(*args)
fees = sum(args[::2], [])
for fee in fees:
fee.adjust_purchase_values()
@classmethod
def copy(cls, fees, default=None):
if default is None:
default = {}
else:
default = default.copy()
# Important : on vide le champ 'lots'
default.setdefault('lots', [])
return super().copy(fees, default=default)
def get_fee_lots_qt(self):
qt = Decimal(0)
FeeLots = Pool().get('fee.lots')
fee_lots = FeeLots.search([('fee', '=', self.id)])
if fee_lots:
qt = sum([e.lot.get_current_quantity_converted() for e in fee_lots])
logger.info("GET_FEE_LOTS_QT:%s",qt)
return qt
def adjust_purchase_values(self):
Purchase = Pool().get('purchase.purchase')
PurchaseLine = Pool().get('purchase.line')
logger.info("ADJUST_PURCHASE_VALUES:%s",self)
if self.type == 'ordered' and self.state == 'not invoiced' and self.purchase:
logger.info("ADJUST_PURCHASE_VALUES_QT:%s",self.purchase.lines[0].quantity)
if self.price != self.purchase.lines[0].unit_price:
self.purchase.lines[0].unit_price = self.price
if self.quantity != self.purchase.lines[0].quantity:
self.purchase.lines[0].quantity = self.quantity
if self.product != self.purchase.lines[0].product:
self.purchase.lines[0].product = self.product
PurchaseLine.save([self.purchase.lines[0]])
if self.supplier != self.purchase.party:
self.purchase.party = self.supplier
if self.currency != self.purchase.currency:
self.purchase.currency = self.currency
Purchase.save([self.purchase])
# @classmethod
# def validate(cls, fees):
# super(Fee, cls).validate(fees)
@classmethod
def create(cls, vlist):
vlist = [x.copy() for x in vlist]
records = super(Fee, cls).create(vlist)
qt_sh = Decimal(0)
qt_line = Decimal(0)
unit = None
for record in records:
FeeLots = Pool().get('fee.lots')
Lots = Pool().get('lot.lot')
LotQt = Pool().get('lot.qt')
if record.line:
for l in record.line.lots:
#if l.lot_type == 'physic':
fl = FeeLots()
fl.fee = record.id
fl.lot = l.id
fl.line = l.line.id
FeeLots.save([fl])
qt_line += l.get_current_quantity_converted()
unit = l.line.unit
if record.sale_line:
for l in record.sale_line.lots:
#if l.lot_type == 'physic':
fl = FeeLots()
fl.fee = record.id
fl.lot = l.id
fl.sale_line = l.sale_line.id
FeeLots.save([fl])
if record.shipment_in:
if record.shipment_in.state == 'draft'or record.shipment_in.state == 'started':
lots = Lots.search(['lot_shipment_in','=',record.shipment_in.id])
if lots:
for l in lots:
#if l.lot_type == 'physic':
fl = FeeLots()
fl.fee = record.id
fl.lot = l.id
FeeLots.save([fl])
qt_sh += l.get_current_quantity_converted()
unit = l.line.unit
else:
lqts = LotQt.search(['lot_shipment_in','=',record.shipment_in.id])
if lqts:
for l in lqts:
qt_sh += l.lot_p.get_current_quantity_converted()
unit = l.lot_p.line.unit
else:
raise UserError("You cannot add fee on received shipment!")
type = record.type
if type == 'ordered':
Purchase = Pool().get('purchase.purchase')
PurchaseLine = Pool().get('purchase.line')
pl = PurchaseLine()
pl.product = record.product
if record.line:
pl.quantity = round(qt_line,5)
if record.shipment_in:
pl.quantity = round(qt_sh,5)
logger.info("CREATE_PURHCASE_FOR_FEE_QT:%s",pl.quantity)
pl.unit = unit
pl.fee_ = record.id
if record.price:
pl.unit_price = round(Decimal(record.price),4)
p = Purchase()
p.lines = [pl]
p.party = record.supplier
if p.party.addresses:
p.invoice_address = p.party.addresses[0]
p.currency = record.currency
p.line_type = 'service'
Purchase.save([p])
return records
class FeeLots(ModelSQL,ModelView):
"Fee lots"
__name__ = 'fee.lots'
fee = fields.Many2One('fee.fee',"Fee",required=True, ondelete='CASCADE')
lot = fields.Many2One('lot.lot',"Lot",required=True, ondelete='CASCADE')
class FeeReport(
ModelSQL, ModelView):
"Fee Report"
__name__ = 'fee.report'
r_purchase_line = fields.Many2One('purchase.line', "Purchase line")
r_sale_line = fields.Many2One('sale.line', "Sale line")
r_shipment_in = fields.Many2One('stock.shipment.in', "Shipment in")
r_shipment_out = fields.Many2One('stock.shipment.out', "Shipment out")
r_shipment_internal = fields.Many2One('stock.shipment.internal', "Shipment internal")
r_fee_type = fields.Many2One('product.product', 'Fee type')
r_fee_counterparty = fields.Many2One('party.party', "Counterparty", required=True)
r_type = fields.Selection([
('ordered', 'Ordered'),
('budgeted', 'Budgeted'),
('actual', 'Actual')
], 'Type')
r_fee_paystatus = fields.Selection([
('pay', 'PAY'),
('rec', 'REC')
], 'Pay status')
r_mode = fields.Selection([
('lumpsum', 'Lump sum'),
('perqt', 'Per qt'),
('pprice', '% price'),
('pcost', '% cost price'),
], 'Mode', required=True)
r_fee_quantity = fields.Function(fields.Numeric("Qt",digits=(1,4)),'get_quantity')
r_fee_unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit')
r_purchase = fields.Many2One('purchase.purchase',"Purchase", ondelete='CASCADE')
r_fee_amount = fields.Function(fields.Numeric("Amount", digits=(1,4)),'get_amount')
r_inv = fields.Function(fields.Many2One('account.invoice',"Invoice"),'get_invoice')
r_state = fields.Selection([
('not invoiced', 'Not invoiced'),
('invoiced', 'Invoiced'),
], string='State', readonly=True)
#r_fee_lots = fields.Function(fields.Many2Many('lot.lot', None, None, "Lots"),'get_lots')#, searcher='search_lots')
#r_lots = fields.Many2Many('fee.lots', 'fee', 'lot',"Lots",domain=[('id', 'in', Eval('r_fee_lots',[]))] )
r_fee_currency = fields.Many2One('currency.currency',"Currency")
r_fee_price = fields.Numeric("Price",digits=(1,4))
r_shipment_origin = fields.Function(
fields.Reference(
selection=[
("stock.shipment.in", "In"),
("stock.shipment.out", "Out"),
("stock.shipment.internal", "Internal"),
],
string="Shipment",
),
"get_shipment_origin",
)
def get_invoice(self,name):
if self.r_purchase:
if self.r_purchase.invoices:
return self.r_purchase.invoices[0]
def get_shipment_origin(self, name):
if self.r_shipment_in:
return 'stock.shipment.in,' + str(self.r_shipment_in.id)
elif self.r_shipment_out:
return 'stock.shipment.out,' + str(self.r_shipment_out.id)
elif self.r_shipment_internal:
return 'stock.shipment.internal,' + str(self.r_shipment_internal.id)
return None
# def get_lots(self, name):
# if self.r_purchase_line:
# return self.r_purchase_line.lots
def get_unit(self, name):
if self.r_purchase_line:
return self.r_purchase_line.unit
def get_quantity(self,name=None):
Fee = Pool().get('fee.fee')
fee = Fee(self.id)
return fee.get_quantity()
def get_amount(self,name=None):
Fee = Pool().get('fee.fee')
fee = Fee(self.id)
return fee.get_amount()
@classmethod
def table_query(cls):
FeeReport = Pool().get('fee.fee')
fr = FeeReport.__table__()
Purchase = Pool().get('purchase.purchase')
pu = Purchase.__table__()
PurchaseLine = Pool().get('purchase.line')
pl = PurchaseLine.__table__()
Sale = Pool().get('sale.sale')
sa = Sale.__table__()
SaleLine = Pool().get('sale.line')
sl = SaleLine.__table__()
context = Transaction().context
party = context.get('party')
fee_type = context.get('fee_type')
purchase = context.get('purchase')
sale = context.get('sale')
shipment_in = context.get('shipment_in')
shipment_out = context.get('shipment_out')
shipment_internal = context.get('shipment_internal')
asof = context.get('asof')
todate = context.get('todate')
wh = ((fr.create_date >= asof) & ((fr.create_date-datetime.timedelta(1)) <= todate))
if party:
wh &= (fr.fee_counterparty == party)
if fee_type:
wh &= (fr.fee_type == fee_type)
if purchase:
wh &= (pu.id == purchase)
if sale:
wh &= (sa.id == sale)
if shipment_in:
wh &= (fr.shipment_in == shipment_in)
# if shipment_out:
# wh &= (fr.shipment_out == shipment_out)
query = fr.join(pl,'LEFT',condition=fr.line == pl.id).join(pu,'LEFT', condition=pl.purchase == pu.id).select(
Literal(0).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
Literal(None).as_('write_uid'),
Literal(None).as_('write_date'),
fr.id.as_('id'),
fr.line.as_('r_purchase_line'),
Literal(None).as_('r_sale_line'),
fr.shipment_in.as_('r_shipment_in'),
Literal(None).as_('r_shipment_out'),
fr.shipment_internal.as_('r_shipment_internal'),
fr.product.as_('r_fee_type'),
fr.supplier.as_('r_fee_counterparty'),
fr.type.as_('r_type'),
fr.p_r.as_('r_fee_paystatus'),
fr.mode.as_('r_mode'),
fr.state.as_('r_state'),
fr.purchase.as_('r_purchase'),
#fr.amount.as_('r_fee_amount'),
fr.price.as_('r_fee_price'),
fr.currency.as_('r_fee_currency'),
#fr.fee_lots.as_('r_fee_lots'),
#fr.lots.as_('r_lots'),
where=wh)
return query
@classmethod
def search_rec_name(cls, name, clause):
_, operator, operand, *extra = clause
if operator.startswith('!') or operator.startswith('not '):
bool_op = 'AND'
else:
bool_op = 'OR'
code_value = operand
if operator.endswith('like') and is_full_text(operand):
code_value = lstrip_wildcard(operand)
return [bool_op,
('r_fee_type', operator, operand, *extra),
('r_fee_counterparty', operator, operand, *extra),
]
class FeeContext(ModelView):
"Fee Context"
__name__ = 'fee.context'
asof = fields.Date("As of")
todate = fields.Date("To")
party = fields.Many2One('party.party', "Counterparty")
fee_type = fields.Many2One('product.product', 'Fee type')
purchase = fields.Many2One('purchase.purchase', "Purchase")
sale = fields.Many2One('sale.sale', "Sale")
shipment_in = fields.Many2One('stock.shipment.in',"Shipment In")
shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out")
shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal")
@classmethod
def default_asof(cls):
pool = Pool()
Date = pool.get('ir.date')
return Date.today().replace(day=1,month=1,year=1999)
@classmethod
def default_todate(cls):
pool = Pool()
Date = pool.get('ir.date')
return Date.today()

101
modules/purchase_trade/fee.xml Executable file
View File

@@ -0,0 +1,101 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="fee_lot_view_tree_sequence">
<field name="model">fee.lots</field>
<field name="type">tree</field>
<field name="name">fee_lot_tree_sequence</field>
</record>
<record model="ir.ui.view" id="fee_view_form">
<field name="model">fee.fee</field>
<field name="type">form</field>
<field name="name">fee_form</field>
</record>
<record model="ir.ui.view" id="fee_view_tree_sequence">
<field name="model">fee.fee</field>
<field name="type">tree</field>
<field name="name">fee_tree_sequence</field>
</record>
<record model="ir.ui.view" id="valuation_view_tree_sequence3">
<field name="model">valuation.valuation</field>
<field name="type">tree</field>
<field name="name">valuation_tree_sequence3</field>
</record>
<record model="ir.ui.view" id="valuation_view_graph">
<field name="model">valuation.valuation</field>
<field name="type">graph</field>
<field name="name">valuation_graph</field>
</record>
<record model="ir.ui.view" id="valuation_view_graph2">
<field name="model">valuation.valuation</field>
<field name="type">graph</field>
<field name="name">valuation_graph2</field>
</record>
<record model="ir.ui.view" id="valuation_view_tree_sequence4">
<field name="model">valuation.valuation.dyn</field>
<field name="type">tree</field>
<field name="name">valuation_tree_sequence4</field>
</record>
<record model="ir.ui.view" id="fee_view_tree_sequence2">
<field name="model">fee.fee</field>
<field name="type">tree</field>
<field name="name">fee_tree_sequence2</field>
</record>
<record model="ir.ui.view" id="fee_report_view_list">
<field name="model">fee.report</field>
<field name="type">tree</field>
<field name="name">fee_report_list</field>
</record>
<record model="ir.action.act_window" id="act_fee_report_form">
<field name="name">Fee report</field>
<field name="res_model">fee.report</field>
<field name="context_model">fee.context</field>
</record>
<record model="ir.action.act_window.view" id="act_fee_report_form_view">
<field name="sequence" eval="70"/>
<field name="view" ref="fee_report_view_list"/>
<field name="act_window" ref="act_fee_report_form"/>
</record>
<record model="ir.ui.view" id="fee_report_context_view_form">
<field name="model">fee.context</field>
<field name="type">form</field>
<field name="name">fee_report_context_form</field>
</record>
<record model="ir.model.button" id="fee_invoice_button">
<field name="model">fee.fee</field>
<field name="name">invoice</field>
<field name="string">Invoice</field>
</record>
<record model="ir.model.button-res.group" id="fee_invoice_button_group_admin">
<field name="button" ref="fee_invoice_button"/>
<field name="group" ref="purchase.group_purchase"/>
</record>
<record model="ir.model.access" id="access_fee">
<field name="model">fee.fee</field>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<record model="ir.model.access" id="access_fee_admin">
<field name="model">fee.fee</field>
<field name="group" ref="purchase.group_purchase"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<menuitem
name="Fee Report"
parent="purchase_trade.menu_global_reporting"
action="act_fee_report_form"
sequence="20"
id="menu_fee_report_form"/>
</data>
</tryton>

399
modules/purchase_trade/forex.py Executable file
View File

@@ -0,0 +1,399 @@
# forex.py
from trytond.model import ModelSingleton, ModelSQL, ModelView, fields
from trytond.pool import PoolMeta, Pool
from trytond.pyson import Bool, Eval, Id, If
from trytond.transaction import Transaction, check_access
from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction
import jwt
import datetime
from decimal import Decimal
import logging
logger = logging.getLogger(__name__)
__all__ = ['Forex', 'ForexCoverPhysicalContract', 'ForexCoverFees']
__metaclass__ = PoolMeta
class Forex(ModelSQL, ModelView):
'Forex Deal'
__name__ = 'forex.forex'
_rec_name = 'number'
company = fields.Many2One('company.company', 'Company', required=True)
number = fields.Char('Number', required=True)
ex_no_ctr = fields.Char('Internal Ref')
date = fields.Date('Date', required=True)
value_date = fields.Date('Maturity Date')
buy_currency = fields.Many2One('currency.currency', 'Buy Currency', required=True)
buy_amount = fields.Numeric('Buy Amount', digits=(16, 2))
for_currency = fields.Many2One('currency.currency', 'Sale Currency', required=True)
for_amount = fields.Numeric('Sale Amount', digits=(16, 2))
bank = fields.Many2One('bank', 'Party')
reason = fields.Many2One('forex.category', 'Category', required=True)
operation_type = fields.Selection([
('normal', 'Normal'),
('swap', 'Swap'),
], 'Operation Type', required=True)
spot_rate = fields.Numeric('Rate', digits=(16, 6))
differential = fields.Numeric('Differential', digits=(16, 6))
rate = fields.Numeric('Agreed Rate', digits=(16, 6), required=True)
confirmed = fields.Boolean('Confirmed')
accepts_bls = fields.Boolean('Accepts B/Ls')
voucher = fields.Boolean('Voucher')
remarks = fields.Text("Remarks")
cover_fees = fields.One2Many(
'forex.cover.fees', 'forex', 'Cover Fees'
)
invoice_line = fields.Many2One('account.invoice.line', 'Invoice Line',
readonly=True)
invoice_state = fields.Function(fields.Selection([
('', ''),
('invoiced', 'Invoiced'),
('paid', 'Paid'),
('cancelled', 'Cancelled'),
], "Invoice State",
help="The current state of the invoice "
"that the forex appears on."),
'get_invoice_state')
move = fields.Many2One('account.move', 'Move',
readonly=True)
move_state = fields.Function(fields.Selection([
('', ''),
('not executed', 'Not Executed'),
('executed', 'Executed'),
], "Move State",
help="The current state of the move "
"that the forex appears on."),
'get_move_state')
@classmethod
def __setup__(cls):
super(Forex, cls).__setup__()
# cls.__access__.add('agent')
cls._buttons.update({
'invoice': {
'invisible': True, #Bool(Eval('invoice_line')),
#'depends': ['invoice_line'],
},
'execute': {
'invisible': Bool(Eval('move')),
'depends': ['move'],
},
})
@staticmethod
def default_company():
return Transaction().context.get('company')
@classmethod
@ModelView.button
def invoice(cls, forexs):
pool = Pool()
Invoice = pool.get('account.invoice')
InvoiceLine = pool.get('account.invoice.line')
invoices = []
invoice_lines = []
to_save = []
for forex in forexs:
invoice = cls._get_invoice(forex)
invoices.append(invoice)
invoice_line = cls._get_invoice_line(invoice, forex)
invoice_lines.append(invoice_line)
forex.invoice_line = invoice_line
to_save.append(forex)
Invoice.save(invoices)
InvoiceLine.save(invoice_lines)
# Invoice.update_taxes(invoices)
cls.save(to_save)
@classmethod
@ModelView.button
def execute(cls, forexs):
Move = Pool().get('account.move')
Period = Pool().get('account.period')
for forex in forexs:
move = Move()
move_lines = forex._get_move_lines()
move.journal = cls.get_journal()
period = Period.find(forex.company, date=forex.value_date)
move.date = forex.value_date
move.period = period
#move.origin = forex
move.company = forex.company
move.lines = move_lines
Move.save([move])
forex.create_exchange_move(move.lines[1].id)
forex.move = move
cls.save([forex])
@classmethod
def get_journal(cls):
pool = Pool()
Journal = pool.get('account.journal')
journals = Journal.search([
('type', '=', 'cash'),
('name', '=', 'Forex'),
], limit=1)
if journals:
return journals[0]
@classmethod
def _get_invoice(cls, forex):
pool = Pool()
Invoice = pool.get('account.invoice')
payment_term = forex.bank.party.supplier_payment_term
return Invoice(
company=forex.company,
type='in',
journal=cls.get_journal(),
party=forex.bank.party,
invoice_address=forex.bank.party.address_get(type='invoice'),
currency=forex.for_currency,
account=forex.bank.party.account_payable_used,
payment_term=payment_term,
)
@classmethod
def _get_invoice_line(cls, invoice, forex):
pool = Pool()
InvoiceLine = pool.get('account.invoice.line')
Product = pool.get('product.product')
product = None
invoice_line = InvoiceLine()
ch_ct = Product.search(['name','=','Forex'])
if ch_ct:
product = ch_ct[0]
invoice_line.account = product.account_expense_used
invoice_line.unit = product.default_uom
amount = invoice.currency.round(forex.for_amount)
invoice_line.invoice = invoice
invoice_line.currency = invoice.currency
invoice_line.company = invoice.company
invoice_line.type = 'line'
# Use product.id to instantiate it with the correct context
invoice_line.product = product.id
invoice_line.quantity = 1
# invoice_line.on_change_product()
invoice_line.unit_price = amount
return invoice_line
def create_exchange_move(self,id):
Currency = Pool().get('currency.currency')
MoveLine = Pool().get('account.move.line')
Configuration = Pool().get('account.configuration')
configuration = Configuration(1)
Period = Pool().get('account.period')
Move = Pool().get('account.move')
PaymentMethod = Pool().get('account.invoice.payment.method')
pm = PaymentMethod.search(['name','=','Forex'])
with Transaction().set_context(date=self.value_date):
amount_converted = Currency.compute(self.buy_currency,self.buy_amount, self.company.currency)
to_add = amount_converted - self.for_amount
if to_add != 0:
second_amount_to_add = Currency.compute(self.company.currency,to_add,self.buy_currency)
line = MoveLine()
line.account = pm[0].debit_account
line.revaluate = id
line.credit = -to_add if to_add < 0 else 0
line.debit = to_add if to_add > 0 else 0
# line.amount_second_currency = second_amount_to_add
# line.second_currency = self.buy_currency
line.party = 33
line.maturity_date = self.value_date
logger.info("REVALUATE_ACC:%s",line)
line_ = MoveLine()
if to_add < 0:
line_.account = configuration.get_multivalue('currency_exchange_credit_account', company=self.company.id)
else:
line_.account = configuration.get_multivalue('currency_exchange_debit_account', company=self.company.id)
line_.credit = to_add if to_add > 0 else 0
line_.debit = -to_add if to_add < 0 else 0
# line_.amount_second_currency = -second_amount_to_add
# line_.second_currency = self.buy_currency
line_.maturity_date = self.value_date
logger.info("REVALUATE_EX:%s",line_)
move = Move()
move.journal = configuration.get_multivalue('currency_exchange_journal', company=self.company.id)
period = Period.find(self.company, date=self.value_date)
move.date = self.value_date
move.period = period
#move.origin = forex
move.company = self.company
move.lines = [line,line_]
Move.save([move])
def _get_move_lines(self):
'''
Return move line
'''
MoveLine = Pool().get('account.move.line')
Currency = Pool().get('currency.currency')
PaymentMethod = Pool().get('account.invoice.payment.method')
pm = PaymentMethod.search(['name','=','Forex'])
move_lines = []
if pm:
line = MoveLine()
line.amount_second_currency = self.buy_amount
line.second_currency = self.buy_currency
line.debit, line.credit = self.for_amount, 0
line.account = pm[0].debit_account
line.maturity_date = self.value_date
line.description = 'Forex'
line.party = self.bank.party
line.origin = self
move_lines.append(line)
line = MoveLine()
line.amount_second_currency = -self.buy_amount
line.second_currency = self.buy_currency
line.debit, line.credit = 0,self.for_amount
line.account = pm[0].credit_account
line.maturity_date = self.value_date
line.description = 'Forex'
line.party = self.bank.party
line.origin = self
move_lines.append(line)
return move_lines
def get_invoice_state(self, name):
state = ''
if self.invoice_line:
state = 'invoiced'
invoice = self.invoice_line.invoice
if invoice and invoice.state in {'paid', 'cancelled'}:
state = invoice.state
return state
def get_move_state(self, name):
state = 'not executed'
if self.move:
state = 'executed'
return state
@classmethod
def default_differential(cls):
return Decimal(0)
@fields.depends('buy_amount','for_amount','rate')
def on_change_rate(self):
if self.buy_amount and self.rate:
self.for_amount = (Decimal(self.buy_amount) * Decimal(self.rate))
self.spot_rate = None
self.differential = None
@fields.depends('buy_amount','for_amount','rate')
def on_change_for_amount(self):
if self.buy_amount and self.for_amount:
self.rate = round(Decimal(self.for_amount) / Decimal(self.buy_amount),6)
self.spot_rate = None
self.differential = None
@fields.depends('buy_amount','for_amount','spot_rate','differential','rate')
def on_change_spot_rate(self):
if self.buy_amount and self.rate and self.for_amount:
self.differential = Decimal(self.rate) - Decimal(self.spot_rate if self.spot_rate else 0)
@fields.depends('buy_amount','for_amount','spot_rate','differential','rate')
def on_change_differential(self):
if self.buy_amount and self.differential and self.for_amount:
self.spot_rate = Decimal(self.rate if self.rate else 0) - Decimal(self.differential)
class PForex(metaclass=PoolMeta):
'Forex Deal'
__name__ = 'forex.forex'
cover_physical_contracts = fields.One2Many(
'forex.cover.physical.contract', 'forex', 'Cover Physical Purchase'
)
class SForex(metaclass=PoolMeta):
'Forex Deal'
__name__ = 'forex.forex'
cover_physical_sales = fields.One2Many(
'forex.cover.physical.sale', 'forex', 'Cover Physical Sale'
)
class ForexCategory(ModelSQL, ModelView):
'Forex Category'
__name__ = 'forex.category'
name = fields.Char('Name')
class ForexCoverPhysicalContract(ModelSQL, ModelView):
'Forex Cover Physical Contract'
__name__ = 'forex.cover.physical.contract'
forex = fields.Many2One('forex.forex', 'Forex', required=True, ondelete='CASCADE')
contract = fields.Many2One('purchase.purchase', 'Purchase', required=True)
amount = fields.Numeric('Amount', digits=(16, 2))
quantity = fields.Numeric('Quantity', digits=(16, 5))
unit = fields.Many2One('product.uom',"Unit")
class ForexCoverPhysicalSale(ModelSQL, ModelView):
'Forex Cover Physical Contract'
__name__ = 'forex.cover.physical.sale'
forex = fields.Many2One('forex.forex', 'Forex', required=True, ondelete='CASCADE')
contract = fields.Many2One('sale.sale', 'Sale', required=True)
amount = fields.Numeric('Amount', digits=(16, 2))
quantity = fields.Numeric('Quantity', digits=(16, 5))
unit = fields.Many2One('product.uom',"Unit")
class ForexCoverFees(ModelSQL, ModelView):
'Forex Cover Fees'
__name__ = 'forex.cover.fees'
forex = fields.Many2One('forex.forex', 'Forex', required=True, ondelete='CASCADE')
description = fields.Char('Description')
amount = fields.Numeric('Amount', digits=(16, 2))
currency = fields.Many2One('currency.currency', 'Currency')
class ForexReport(Wizard):
'Forex report'
__name__ = 'forex.report'
start = StateAction('purchase_trade.act_forex_bi')
def do_start(self, action):
pool = Pool()
# action['views'].reverse()
return action, {'res_id': [1]}
class ForexBI(ModelSingleton,ModelSQL, ModelView):
'Forex BI'
__name__ = 'forex.bi'
input = fields.Text("BI")
metabase = fields.Function(fields.Text(""),'get_bi')
def get_bi(self,name=None):
Configuration = Pool().get('gr.configuration')
config = Configuration.search(['id','>',0])[0]
payload = {
"resource": {"dashboard": 3},
"params": {},
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30),
}
token = jwt.encode(payload, "798f256d3119a3292bf121196c2a38dddf2cad155c0b6b0b444efc34c6db197c", algorithm="HS256")
logger.info("TOKEN:%s",token)
if config.dark:
url = f"metabase:{config.bi}/embed/dashboard/{token}#theme=night&bordered=true&titled=true"
else:
url = f"metabase:{config.bi}/embed/dashboard/{token}#bordered=true&titled=true"
return url

186
modules/purchase_trade/forex.xml Executable file
View File

@@ -0,0 +1,186 @@
<?xml version="1.0"?>
<tryton>
<data>
<record model="res.group" id="group_forex">
<field name="name">Forex</field>
</record>
<record model="res.group" id="group_forex_admin">
<field name="name">Forex Administration</field>
<field name="parent" ref="group_forex"/>
</record>
<record model="res.user-res.group" id="user_admin_group_forex">
<field name="user" ref="res.user_admin"/>
<field name="group" ref="group_forex"/>
</record>
<record model="res.user-res.group" id="user_admin_group_forex_admin">
<field name="user" ref="res.user_admin"/>
<field name="group" ref="group_forex_admin"/>
</record>
<record model="ir.ui.icon" id="forex_icon">
<field name="name">tradon-forex</field>
<field name="path">icons/tradon-forex.svg</field>
</record>
<record model="ir.ui.view" id="view_forex_form">
<field name="model">forex.forex</field>
<field name="type">form</field>
<field name="name">forex_form</field>
</record>
<record model="ir.ui.view" id="view_forex_list">
<field name="model">forex.forex</field>
<field name="type">tree</field>
<field name="name">forex_list</field>
</record>
<record model="ir.action.act_window" id="act_forex_form">
<field name="name">Forex</field>
<field name="res_model">forex.forex</field>
</record>
<record model="ir.action.act_window.view" id="act_forex_form_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="view_forex_list"/>
<field name="act_window" ref="act_forex_form"/>
</record>
<record model="ir.action.act_window.view" id="act_forex_form_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="view_forex_form"/>
<field name="act_window" ref="act_forex_form"/>
</record>
<record model="ir.ui.view" id="view_forex_cover_physical_contract_tree">
<field name="model">forex.cover.physical.contract</field>
<field name="type">tree</field>
<field name="name">forex_cover_physical_contract_tree</field>
</record>
<record model="ir.ui.view" id="view_forex_cover_physical_contract_tree2">
<field name="model">forex.cover.physical.contract</field>
<field name="type">tree</field>
<field name="name">forex_cover_physical_contract_tree2</field>
</record>
<record model="ir.ui.view" id="view_forex_cover_physical_contract_form">
<field name="model">forex.cover.physical.contract</field>
<field name="type">form</field>
<field name="name">forex_cover_physical_contract_form</field>
</record>
<record model="ir.ui.view" id="view_forex_cover_physical_sale_tree">
<field name="model">forex.cover.physical.sale</field>
<field name="type">tree</field>
<field name="name">forex_cover_physical_sale_tree</field>
</record>
<record model="ir.ui.view" id="view_forex_cover_physical_sale_tree2">
<field name="model">forex.cover.physical.sale</field>
<field name="type">tree</field>
<field name="name">forex_cover_physical_sale_tree2</field>
</record>
<record model="ir.ui.view" id="view_forex_cover_physical_sale_form">
<field name="model">forex.cover.physical.sale</field>
<field name="type">form</field>
<field name="name">forex_cover_physical_sale_form</field>
</record>
<record model="ir.ui.view" id="view_forex_cover_fees_tree">
<field name="model">forex.cover.fees</field>
<field name="type">tree</field>
<field name="name">forex_cover_fees_tree</field>
</record>
<record model="ir.ui.view" id="forex_bi_view_graph2">
<field name="model">forex.bi</field>
<field name="type">form</field>
<field name="name">forex_bi_graph</field>
</record>
<record model="ir.action.act_window" id="act_forex_bi">
<field name="name">Forex BI</field>
<field name="res_model">forex.bi</field>
</record>
<record model="ir.action.act_window.view" id="act_forex_bi_view">
<field name="sequence" eval="30"/>
<field name="view" ref="forex_bi_view_graph2"/>
<field name="act_window" ref="act_forex_bi"/>
</record>
<record model="ir.ui.view" id="view_forex_cover_fees_form">
<field name="model">forex.cover.fees</field>
<field name="type">form</field>
<field name="name">forex_cover_fees_form</field>
</record>
<record model="ir.action.act_window.domain" id="act_forex_form_domain_to_invoice">
<field name="name">To execute</field>
<field name="sequence" eval="10"/>
<field name="domain" eval="[('move', '=', None)]" pyson="1"></field>
<field name="count" eval="True"></field>
<field name="act_window" ref="act_forex_form"/>
</record>
<record model="ir.action.act_window.domain" id="act_forex_form_domain_all">
<field name="name">All</field>
<field name="sequence" eval="9999"/>
<field name="domain"></field>
<field name="act_window" ref="act_forex_form"/>
</record>
<record model="ir.model.access" id="access_forex">
<field name="model">forex.forex</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_forex_forex">
<field name="model">forex.forex</field>
<field name="group" ref="group_forex"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<record model="ir.model.button" id="forex_invoice_button">
<field name="model">forex.forex</field>
<field name="name">invoice</field>
<field name="string">Invoice</field>
</record>
<record model="ir.model.button" id="forex_execute_button">
<field name="model">forex.forex</field>
<field name="name">execute</field>
<field name="string">Execute</field>
</record>
<record model="ir.action.wizard" id="act_forex_report">
<field name="name">Forex report</field>
<field name="wiz_name">forex.report</field>
</record>
<menuitem
name="Forex"
sequence="100"
id="menu_forex"
icon="tradon-forex" />
<menuitem
name="Contract"
action="act_forex_form"
parent="menu_forex"
sequence="10"
id="menu_contract" />
<menuitem
name="Forex Report"
parent="menu_forex"
action="act_forex_bi"
sequence="20"
id="menu_forex_bi"/>
<menuitem
name="Forex Report"
parent="purchase_trade.menu_global_reporting"
action="act_forex_bi"
sequence="25"
id="menu_gr_forex_bi"/>
<!-- <menuitem
name="Cash Forecast"
parent="menu_forex"
action="act_cash_forecast"
sequence="20"
id="menu_cf"/> -->
</data>
</tryton>

View File

@@ -0,0 +1,15 @@
from trytond.config import config
from trytond.i18n import gettext
from trytond.model import (
DeactivableMixin, ModelSingleton, ModelSQL, ModelView, Workflow, fields)
from trytond.pool import Pool
from trytond.pyson import Eval, If
from trytond.transaction import Transaction
from trytond.wizard import Button, StateTransition, StateView, Wizard
class GRConfiguration(ModelSingleton, ModelSQL, ModelView):
"Global Reporting Configuration"
__name__ = 'gr.configuration'
bi = fields.Char("BI connexion")
dark = fields.Boolean("Dark mode")

View File

@@ -0,0 +1,42 @@
<tryton>
<data>
<record model="ir.ui.icon" id="reporting_icon">
<field name="name">tradon-reporting</field>
<field name="path">icons/tradon-reporting.svg</field>
</record>
<record model="ir.ui.view" id="gr_configuration_view_form">
<field name="model">gr.configuration</field>
<field name="type">form</field>
<field name="name">gr_configuration_form</field>
</record>
<record model="ir.action.act_window" id="act_gr_configuration_form">
<field name="name">Configuration</field>
<field name="res_model">gr.configuration</field>
</record>
<record model="ir.action.act_window.view" id="act_gr_configuration_form_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="gr_configuration_view_form"/>
<field name="act_window" ref="act_gr_configuration_form"/>
</record>
<menuitem
name="Global Reporting"
sequence="5"
id="menu_global_reporting"
icon="tradon-reporting"/>
<menuitem
name="Configuration"
parent="menu_global_reporting"
sequence="0"
id="menu_global_configuration"
icon="tryton-settings"/>
<menuitem
parent="menu_global_configuration"
action="act_gr_configuration_form"
sequence="10"
id="menu_gr_configuration"
icon="tryton-list"/>
</data>
</tryton>

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M4 10v7h3v-7H4zm6 0v7h3v-7h-3zM2 22h19v-3H2v3zm14-12v7h3v-7h-3zm-4.5-9L2 6v2h19V6l-9.5-5z" fill="#577590"/>
</svg>

After

Width:  |  Height:  |  Size: 254 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M13 13v8h8v-8h-8zM3 21h8v-8H3v8zM3 3v8h8V3H3zm13.66-1.31L11 7.34 16.66 13l5.66-5.66-5.66-5.65z" fill="#F9844A"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 259 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M13 5H7v14h6c2.21 0 4-1.79 4-4V9c0-2.21-1.79-4-4-4zm-2 12V7h2c1.1 0 2 .9 2 2v6c0 1.1-.9 2-2 2h-2z" fill="#267f82"/>
</svg>

After

Width:  |  Height:  |  Size: 264 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M5 3 L5 21 L9 21 L9 14 L15 14 L15 11 L9 11 L9 6 L19 6 L19 3 Z
M16 11 L18 11 L20 13 L22 11 L24 11 L21.5 14 L24 17 L22 17 L20 15 L18 17 L16 17 L18.5 14 Z" fill="#267F82"/>
</svg>

After

Width:  |  Height:  |  Size: 330 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M11 17h2v-1h1c.55 0 1-.45 1-1v-3c0-.55-.45-1-1-1h-3v-1h4V8h-2V7h-2v1h-1c-.55 0-1 .45-1 1v3c0 .55.45 1 1 1h3v1H9v2h2v1zm9-13H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4V6h16v12z" fill="#F94144"/>
</svg>

After

Width:  |  Height:  |  Size: 386 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M7 18c-1.1 0-1.99.9-1.99 2S5.9 22 7 22s2-.9 2-2-.9-2-2-2zM1 2v2h2l3.6 7.59-1.35 2.45c-.16.28-.25.61-.25.96 0 1.1.9 2 2 2h12v-2H7.42c-.14 0-.25-.11-.25-.25l.03-.12.9-1.63h7.45c.75 0 1.41-.41 1.75-1.03l3.58-6.49c.08-.14.12-.31.12-.48 0-.55-.45-1-1-1H5.21l-.94-2H1zm16 16c-1.1 0-1.99.9-1.99 2s.89 2 1.99 2 2-.9 2-2-.9-2-2-2z" fill="#277DA1"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 486 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<!-- Fond de graphique (lignes) -->
<path d="M3 17h2v3H3zM7 12h2v8H7zM11 9h2v11h-2zM15 14h2v6h-2zM19 7h2v13h-2z" fill="#277DA1" opacity="0.3"/>
<!-- Barres du graphique -->
<path d="M3 17h2v3H3v-3zM7 12h2v8H7v-8zM11 9h2v11h-2V9zM15 14h2v6h-2v-6zM19 7h2v13h-2V7z" fill="#277DA1"/>
<!-- Axes du graphique -->
<path d="M3 20h18v1H3zM3 6v14h1V6z" fill="#277DA1"/>
<!-- Document (optionnel - pour symboliser le rapport) -->
<path d="M20 3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H4V5h16v14z" fill="none" stroke="#277DA1" stroke-width="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 721 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M20 4H4v2h16V4zm1 10v-2l-1-5H4l-1 5v2h1v6h10v-6h4v6h2v-6h1zm-9 4H6v-4h6v4z" fill="#90BE6D"/>
</svg>

After

Width:  |  Height:  |  Size: 239 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="64">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M4 8h3V4h14c1.1 0 2 .9 2 2v11h-2c0 1.66-1.34 3-3 3s-3-1.34-3-3H9c0 1.66-1.34 3-3 3s-3-1.34-3-3H1v-5zm14 10.5c.83 0 1.5-.67 1.5-1.5s-.67-1.5-1.5-1.5-1.5.67-1.5 1.5.67 1.5 1.5 1.5zm-13.5-9L2.54 12H7V9.5zm1.5 9c.83 0 1.5-.67 1.5-1.5s-.67-1.5-1.5-1.5-1.5.67-1.5 1.5.67 1.5 1.5 1.5z" fill="#F9C74F"/>
</svg>

After

Width:  |  Height:  |  Size: 434 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM4 12c0-4.42 3.58-8 8-8 1.85 0 3.55.63 4.9 1.69L5.69 16.9C4.63 15.55 4 13.85 4 12zm8 8c-1.85 0-3.55-.63-4.9-1.69L18.31 7.1C19.37 8.45 20 10.15 20 12c0 4.42-3.58 8-8 8z"/></svg>

After

Width:  |  Height:  |  Size: 371 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>

After

Width:  |  Height:  |  Size: 363 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 17c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm6-9h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6h1.9c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm0 12H6V10h12v10z"/></svg>

After

Width:  |  Height:  |  Size: 369 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M4 10v7h3v-7H4zm6 0v7h3v-7h-3zM2 22h19v-3H2v3zm14-12v7h3v-7h-3zm-4.5-9L2 6v2h19V6l-9.5-5z"/>
</svg>

After

Width:  |  Height:  |  Size: 239 B

View File

@@ -0,0 +1,565 @@
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 trytond.wizard import Wizard, StateView, StateTransition, Button
from datetime import datetime
import re
class LCIncoming(ModelSQL, ModelView, Workflow):
'LC Incoming'
__name__ = 'lc.letter.incoming'
name = fields.Char('Number')
type = fields.Selection([
('documentary', 'Documentary LC'),
('standby', 'Standby LC')
], 'Type', required=True)
sale = fields.Many2One('sale.sale', 'Sale')
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_in', 'doc_type', 'Required Documents')
# Workflow principal
state = fields.Selection([
(None, ''),
('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 Vente
receiving_bank = fields.Many2One('party.party', 'Receiving Bank')
confirming_bank_required = fields.Boolean('Confirmation Required')
confirmation_cost = fields.Numeric('Confirmation Cost', digits=(16, 2))
# Analyse de la LC reçue
risk_level = fields.Selection([
(None, ''),
('low', 'Low Risk'),
('medium', 'Medium Risk'),
('high', 'High Risk')
], 'Risk Level')
conditions_analysis = fields.Text('Conditions Analysis')
discrepancies_found = fields.One2Many('lc.discrepancy', 'lc', 'Discrepancies Found')
# Préparation documents
documents_prepared = fields.One2Many('lc.document.prepared', 'lc', 'Documents Prepared')
presentation_date = fields.Date('Presentation Date')
presentation_bank = fields.Many2One('party.party', 'Presentation Bank')
# Résultat présentation
acceptance_date = fields.Date('Acceptance Date')
payment_date = fields.Date('Payment Date')
refusal_reason = fields.Text('Refusal Reason')
swift_file = fields.Many2One('document.incoming',"Swift file")
swift_execute = fields.Boolean("Import")
swift_text = fields.Text("Message")
@classmethod
def __setup__(cls):
super(LCIncoming, 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'],
},
})
def import_swift(self, text):
"""
Instance method: parse the text and return a dict of {field: value}
NE PAS écrire ici. Retourne seulement les valeurs à écrire.
"""
extracted = self._parse_swift_mt700(text)
return {
field: value
for field, value in extracted.items()
if hasattr(self, field) and value is not None and value != ''
}
@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 = {}
# Si un fichier a été fourni et que swift_text n'est pas déjà présent,
# remplir swift_text depuis le fichier
if lc.swift_file and not lc.swift_text:
try:
update_vals['swift_text'] = lc.swift_file.data.decode('utf-8', errors='ignore')
except Exception:
# Si décodage échoue, on peut ignorer ou logguer; ici on ignore
pass
# Si on a du texte (soit déjà soit venant du fichier), extraire
text_to_parse = update_vals.get('swift_text') or lc.swift_text
if text_to_parse:
extracted = lc.import_swift(text_to_parse)
update_vals.update(extracted)
# Toujours désactiver le flag et passer en submitted
update_vals['swift_execute'] = False
update_vals['state'] = 'submitted'
# Appliquer les modifications via la méthode parente pour éviter récursion
if update_vals:
super(LCIncoming, cls).write([lc], update_vals)
@classmethod
def create(cls, vals_list):
# Créer d'abord les enregistrements
lcs = super(LCIncoming, cls).create(vals_list)
# Pour chaque couple (record, vals) => traiter si swift_execute demandé
for lc, vals in zip(lcs, vals_list):
cls._process_swift_for_record(lc, initial_vals=vals)
return lcs
@classmethod
def write(cls, lcs, vals):
# Appeler le parent pour appliquer vals initiaux
super(LCIncoming, 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)
@staticmethod
def default_state():
return 'draft'
@staticmethod
def default_version():
return 1
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
@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'})
@classmethod
def create_from_sale(cls, sale_id):
"""Méthode de base pour création depuis vente"""
Sale = Pool().get('sale.sale')
sale = Sale(sale_id)
return {
'sale': sale.id,
'company': sale.company.id,
'applicant': sale.party.id,
'beneficiary': sale.company.party.id,
'currency': sale.currency.id if sale.currency else None,
'port_of_loading': sale.from_location.id,
'port_of_discharge': sale.to_location.id,
'state': 'draft',
'type': 'documentary',
'risk_level': 'medium',
}
def _parse_swift_mt700(self, swift_text):
"""Parse le message SWIFT MT700"""
data = {}
# Ne pas appeler les extracteurs ici — passer des callables
patterns = {
'name': r':20:(.+)',
'issue_date': r':31C:(\d{6})',
'expiry_date': r':31D:(\d{6})',
'expiry_place': r':31D:\d{6}\s*(.+)',
# callables qui prendront swift_text en argument
'applicant': lambda txt: self._extract_from_swift(txt, ':50:'),
'beneficiary': lambda txt: self._extract_from_swift(txt, ':59:'),
'amount': r':32B:([A-Z]{3})\s*([\d,.\s]+)',
'issuing_bank': lambda txt: self._extract_from_swift(txt, ':52A:'),
'advising_bank': lambda txt: self._extract_from_swift(txt, ':53A:'),
'partial_shipment': r':43P:(.+)',
'transhipment': r':43T:(.+)',
'port_of_loading': lambda txt: self._extract_from_swift(txt, ':53A:'),
'port_of_discharge': lambda txt: self._extract_from_swift(txt, ':53A:'),
'latest_shipment_date': r':44C:(\d{6})',
'presentation_days': r':48:(\d+)',
}
for field, pattern in patterns.items():
if callable(pattern):
# appelle le callable avec le texte swift
try:
value = pattern(swift_text)
except Exception:
value = None
# n'ajouter que si on a une valeur non vide
if value:
data[field] = value.strip() if isinstance(value, str) else value
else:
match = re.search(pattern, swift_text, re.MULTILINE | re.IGNORECASE)
if match:
if field == 'amount':
# group(1) = currency, group(2) = montant
currency = match.group(1)
amount_str = match.group(2)
# normaliser le montant: enlever espaces et remplacer la virgule décimale par point
cleaned = amount_str.replace(' ', '').replace(',', '.')
try:
data['amount'] = float(cleaned)
data['currency'] = self._find_currency(currency)
except ValueError:
# si parse échoue, stocker en string pour debug
data['amount_raw'] = amount_str
elif field in ['issue_date', 'expiry_date', 'latest_shipment_date']:
date_str = match.group(1)
if len(date_str) == 6:
# SWIFT YYMMDD -> interpréter comme 20YY (ou adapter si besoin)
year = int(date_str[0:2]) + 2000
month = int(date_str[2:4])
day = int(date_str[4:6])
try:
data[field] = datetime(year, month, day).date()
except ValueError:
# date invalide : ne rien faire ou logger
pass
else:
# ici on sait qu'il y a un groupe capturant (1)
data[field] = match.group(1).strip()
return data
def _extract_from_swift(self, swift_text, field_tag):
"""Extrait les informations de partie depuis SWIFT"""
pattern = field_tag + r'\s*(.+)'
match = re.search(pattern, swift_text, re.MULTILINE | re.IGNORECASE)
if match:
party_name = match.group(1).strip().split(',')[0].strip()
return self._find_record_from_text(party_name)
return None
def _extract_name(self, text):
if not text:
return None
# On prend avant la première virgule
return text.split(',')[0].strip()
def _find_record_from_text(self, record_text):
Party = Pool().get('party.party')
Location = Pool().get('stock.location')
if not record_text:
return None
# Extraire le nom
name = self._extract_name(record_text)
if not name:
return None
# 1) Exact match
parties = Party.search([
('name', '=', name),
])
if parties:
return parties[0]
# 2) Match insensible aux accents et majuscules
parties = Party.search([
('name', 'ilike', name),
])
if parties:
return parties[0]
# 3) Match partiel (ex: "SAFTCO" dans "SAFTCO SA")
keyword = name.split()[0] # Premier mot du nom
parties = Party.search([
('name', 'ilike', f'%{keyword}%'),
])
if parties:
return parties[0]
locations = Location.search([
('name', '=', name),
])
if locations:
return locations[0]
# Aucun match trouvé
return None
def _find_currency(self, currency_code):
"""Trouve la devise par son code"""
Currency = Pool().get('currency.currency')
currencies = Currency.search([('code', '=', currency_code)], limit=1)
return currencies[0].id if currencies else None
def analyze_conditions(self):
"""Analyse automatique des conditions de la LC"""
analysis = []
risks = []
lc = self
# Vérification dates
if lc.expiry_date and lc.expiry_date < datetime.now().date():
risks.append("LC expirée")
if lc.latest_shipment_date and lc.latest_shipment_date < datetime.now().date():
risks.append("Date de shipment dépassée")
# Vérification documents
if not lc.required_documents:
risks.append("Aucun document spécifié")
# Vérification banques
if not lc.issuing_bank:
risks.append("Banque émettrice non spécifiée")
# Détermination niveau de risque
if len(risks) == 0:
lc.risk_level = 'low'
elif len(risks) <= 2:
lc.risk_level = 'medium'
else:
lc.risk_level = 'high'
lc.conditions_analysis = "\n".join(analysis + ["RISKS:"] + risks)
lc.save()
return lc.risk_level
def prepare_documents_checklist(self):
"""Prépare la checklist des documents à produire"""
lc = self
LCDocumentPrepared = Pool().get('lc.document.prepared')
# Crée les entrées pour chaque document requis
documents_to_prepare = []
if lc.required_documents:
for doc_type in lc.required_documents:
documents_to_prepare.append({
'lc': lc.id,
'document_type': doc_type.id,
'required': True,
'status': 'pending'
})
if documents_to_prepare:
LCDocumentPrepared.create(documents_to_prepare)
# lc.state = 'under_review'
lc.save()
return len(documents_to_prepare)
class ImportSwiftStart(ModelView):
'Import SWIFT Start'
__name__ = 'lc.import_swift.start'
file_ = fields.Binary('SWIFT MT700 file', required=True)
filename = fields.Char('Filename')
class ImportSwift(Wizard):
'Import SWIFT'
__name__ = 'lc.import_swift'
start = StateView(
'lc.import_swift.start',
'purchase_trade.import_swift_start_view_form',
[
Button('Cancel', 'end', 'tryton-cancel'),
Button('Import', 'import_', 'tryton-ok', default=True),
]
)
import_ = StateTransition()
def transition_import_(self):
LC = Pool().get('lc.letter.incoming')
active_id = Transaction().context.get('active_id')
lc = LC(active_id)
content = self.start.file_
text = content.decode('utf-8')
extracted = lc._parse_swift_mt700(text)
update_vals = {}
for field, value in extracted.items():
if hasattr(lc, field) and value:
update_vals[field] = value
# Met à jour tous les champs via write()
if update_vals:
LC.write([lc], update_vals)
# Passe l'état via write()
LC.write([lc], {'state': 'submitted'})
return 'end'
def end(self):
# Récupérer active_id du contexte
ctx = Transaction().context
active_id = ctx.get('active_id') or (ctx.get('active_ids') and ctx.get('active_ids')[0])
if not active_id:
return 'reload'
return {
'type': 'ir.actions.act_window',
'res_model': 'lc.letter.incoming',
'res_id': active_id,
'view_mode': 'form',
'target': 'new',
}
class AnalyzeConditions(Wizard):
"Analyze Conditions"
__name__ = "lc.analyze.conditions"
start = StateTransition()
def transition_start(self):
self.records[0].analyze_conditions()
return 'end'
def end(self):
return 'reload'
class PrepareDocuments(Wizard):
"Prepare Documents"
__name__ = "lc.prepare.doc"
start = StateTransition()
def transition_start(self):
self.records[0].prepare_documents_checklist()
return 'end'
def end(self):
return 'reload'

235
modules/purchase_trade/lc.py Executable file
View File

@@ -0,0 +1,235 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.model import fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id
from trytond.model import (ModelSQL, ModelView, sort)
from trytond.tools import is_full_text, lstrip_wildcard
from trytond.transaction import Transaction, inactive_records
from decimal import getcontext, Decimal, ROUND_HALF_UP
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
from sql.conditionals import Case
from sql import Column, Literal
from sql.functions import CurrentTimestamp, DateTrunc
from trytond.wizard import Wizard, StateView, StateTransition, StateAction, Button
from itertools import chain, groupby
from operator import itemgetter
import datetime
import logging
logger = logging.getLogger(__name__)
class CreateLCStart(ModelView):
'Create LC Start'
__name__ = 'create.lc.start'
base_amount = fields.Numeric(
'LC amount',
digits=(16, 2),
readonly=True,
help="LC amount"
)
percentage = fields.Numeric(
'Percentage',
digits=(16, 2),
readonly=False,
help="Percentage of the amount for the LC"
)
amount = fields.Numeric(
'Amount',
digits=(16, 2),
required=True,
help="LC amount, calculated but editable."
)
@fields.depends('percentage','base_amount')
def on_change_with_amount(self):
if self.percentage and self.percentage > 0:
return round(self.base_amount * self.percentage / 100,2)
class LCMessage(ModelView):
'LC Created Message'
__name__ = 'create.lc.message'
message = fields.Char('Message', readonly=True)
lc_in = fields.Many2One('lc.letter.incoming', 'LC in')
lc_out = fields.Many2One('lc.letter.outgoing', 'LC out')
class CreateLCWizard(Wizard):
'Create LC Wizard'
__name__ = 'create.lc'
start = StateView(
'create.lc.start',
'purchase_trade.create_lc_start_form',
[
Button('Cancel', 'end', 'tryton-cancel'),
Button('Create', 'create_lc', 'tryton-ok', default=True),
]
)
create_lc = StateTransition()
lc_id = None
message_in = StateView(
'create.lc.message',
'purchase_trade.create_lc_message_form',
[
Button('OK', 'end', 'tryton-ok'),
Button('See LC', 'see_lc_in', 'tryton-go-next'),
]
)
message_out = StateView(
'create.lc.message',
'purchase_trade.create_lc_message_form',
[
Button('OK', 'end', 'tryton-ok'),
Button('See LC', 'see_lc_out', 'tryton-go-next'),
]
)
see_lc_in = StateAction('purchase_trade.act_lc_in_form')
see_lc_out = StateAction('purchase_trade.act_lc_out_form')
def default_start(self, fields):
context = Transaction().context
active_model = context.get('active_model')
ids = context.get('active_ids')
percentage = Decimal(0)
amount = Decimal(0)
base_amount = Decimal(0)
for i in ids:
if active_model == 'sale.sale':
Sale = Pool().get('sale.sale')
sale = Sale(i)
if sale.lines:
line = sale.lines[0]
base_amount = line.amount
amount = base_amount * percentage / Decimal(100)
else:
Purchase = Pool().get('purchase.purchase')
purchase = Purchase(i)
if purchase.lines:
line = purchase.lines[0]
base_amount = line.amount
amount = base_amount * percentage / Decimal(100)
return {
'percentage': percentage,
'base_amount': base_amount,
'amount': amount,
}
def transition_create_lc(self):
pool = Pool()
context = Transaction().context
active_model = context.get('active_model')
id = Transaction().context['active_id']
if active_model == 'sale.sale':
LCIncoming = pool.get('lc.letter.incoming')
amount = self.start.amount
vals = LCIncoming.create_from_sale(id)
vals['amount'] = amount
lc = LCIncoming.create([vals])[0]
DocReq = Pool().get('contract.document.type')
docs = DocReq.search(['sale','=',id])
if docs:
for d in docs:
d.lc_in = lc.id
DocReq.save(docs)
self.message_in.lc_in = lc
return 'message_in'
else:
LCOutgoing = pool.get('lc.letter.outgoing')
amount = self.start.amount
vals = LCOutgoing.create_from_purchase(id)
vals['amount'] = amount
lc = LCOutgoing.create([vals])[0]
DocReq = Pool().get('contract.document.type')
docs = DocReq.search(['purchase','=',id])
if docs:
for d in docs:
d.lc_out = lc.id
DocReq.save(docs)
self.message_out.lc_out = lc
return 'message_out'
def default_message_in(self, fields):
return {
'message': 'The LC has been successfully created.',
}
def default_message_out(self, fields):
return {
'message': 'The LC has been successfully created.',
}
def do_see_lc_in(self, action):
action['views'].reverse() # pour ouvrir en form directement
return action, {'res_id':self.message_in.lc_in.id}
def do_see_lc_out(self, action):
action['views'].reverse() # pour ouvrir en form directement
return action, {'res_id':self.message_out.lc_out.id}
class LCMT700(ModelSQL, ModelView):
'Letter of Credit MT700'
__name__ = 'lc.mt700'
name = fields.Char("Name")
lc = fields.Text("Lc corpus")
format_lc = fields.Boolean("Format LC")
f_27 = fields.Char('27: Sequence of Total')
f_40A = fields.Char('40A: Form of Documentary Credit')
f_20 = fields.Char('20: Documentary Credit Number')
f_23 = fields.Char('23: Reference to Pre-Advice')
f_31C = fields.Date('31C: Date of Issue')
f_40E = fields.Char('40E: Applicable Rules')
f_31D = fields.Char('31D: Date and Place of Expiry')
f_51a = fields.Char('51a: Applicant Bank')
f_50 = fields.Char('50: Applicant')
f_59 = fields.Char('59: Beneficiary')
f_32B = fields.Numeric('32B: Currency Code, Amount')
f_39A = fields.Char('39A: Percentage Credit Amount Tolerance')
f_39C = fields.Char('39C: Additional Amounts Covered')
f_41a = fields.Char('41a: Available With ... By ...')
f_42C = fields.Char('42C: Drafts at ...')
f_42a = fields.Char('42a: Drawee')
f_42M = fields.Char('42M: Mixed Payment Details')
f_42P = fields.Char('42P: Negotiation/Deferred Payment Details')
f_43P = fields.Char('43P: Partial Shipments')
f_43T = fields.Char('43T: Transshipment')
f_44A = fields.Char('44A: Place of Taking in Charge/Dispatch from .../ Place of Receipt')
f_44E = fields.Char('44E: Port of Loading/Airport of Departure')
f_44F = fields.Char('44F: Port of Discharge/Airport of Destination')
f_44B = fields.Char('44B: Place of Final Destination/For Transportation to.../ Place of Delivery')
f_44C = fields.Date('44C: Latest Date of Shipment')
f_44D = fields.Char('44D: Shipment Period')
f_45A = fields.Text('45A: Description of Goods and/or Services')
f_46A = fields.Text('46A: Documents Required')
f_47A = fields.Text('47A: Additional Conditions')
f_71B = fields.Text('71B: Charges')
f_48 = fields.Char('48: Period for Presentation')
f_49 = fields.Char('49: Confirmation Instructions')
f_53a = fields.Char('53a: Reimbursing Bank')
f_78 = fields.Text('78: Instructions to the Paying/Accepting/Negotiating Bank')
f_57a = fields.Char('57a: Advise Through Bank')
f_72 = fields.Text('72: Sender to Receiver Information')
def get_formatted_lc(self):
lc_fields = [
'27', '40A', '20', '23', '31C', '40E', '31D', '51a', '50', '59', '32B', '39A', '39C', '41a', '42C', '42a',
'42M', '42P', '43P', '43T', '44A', '44E', '44F', '44B', '44C', '44D', '45A', '46A', '47A', '71B', '48', '49',
'53a', '78', '57a', '72'
]
formatted_lc = []
for field in lc_fields:
value = getattr(self, f'f_{field}', None)
logger.info("FORMATTEDLC:%s",value)
if value:
formatted_lc.append(f'{field}: {value}')
logger.info("FORMATTEDLC2:%s",formatted_lc)
return '\n'.join(formatted_lc)
@fields.depends('format_lc')
def on_change_with_lc(self):
if self.format_lc:
return self.get_formatted_lc()
else:
return ""

285
modules/purchase_trade/lc.xml Executable file
View File

@@ -0,0 +1,285 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="res.group" id="group_lc">
<field name="name">LC</field>
</record>
<record model="res.group" id="group_lc_admin">
<field name="name">LC Administration</field>
<field name="parent" ref="group_lc"/>
</record>
<record model="res.user-res.group" id="user_admin_group_lc">
<field name="user" ref="res.user_admin"/>
<field name="group" ref="group_lc"/>
</record>
<record model="res.user-res.group" id="user_admin_group_lc_admin">
<field name="user" ref="res.user_admin"/>
<field name="group" ref="group_lc_admin"/>
</record>
<record model="ir.ui.view" id="lc_view_list">
<field name="model">lc.mt700</field>
<field name="type">tree</field>
<field name="name">lc_list</field>
</record>
<record model="ir.ui.view" id="lc_view_form">
<field name="model">lc.mt700</field>
<field name="type">form</field>
<field name="name">lc_form</field>
</record>
<record model="ir.action.act_window" id="act_lc_form">
<field name="name">LC</field>
<field name="res_model">lc.mt700</field>
</record>
<record model="ir.action.act_window.view" id="act_lc_form_view">
<field name="sequence" eval="70"/>
<field name="view" ref="lc_view_list"/>
<field name="act_window" ref="act_lc_form"/>
</record>
<record model="ir.action.act_window.view" id="act_lc_form_view2">
<field name="sequence" eval="80"/>
<field name="view" ref="lc_view_form"/>
<field name="act_window" ref="act_lc_form"/>
</record>
<record model="ir.ui.view" id="lc_out_view_list">
<field name="model">lc.letter.outgoing</field>
<field name="type">tree</field>
<field name="name">lc_letter_outgoing_tree</field>
</record>
<record model="ir.ui.view" id="lc_out_view_form">
<field name="model">lc.letter.outgoing</field>
<field name="type">form</field>
<field name="name">lc_letter_outgoing_form</field>
</record>
<record model="ir.action.act_window" id="act_lc_out_form">
<field name="name">Outgoing LC</field>
<field name="res_model">lc.letter.outgoing</field>
</record>
<record model="ir.action.act_window.view" id="act_lc_out_form_view">
<field name="sequence" eval="70"/>
<field name="view" ref="lc_out_view_list"/>
<field name="act_window" ref="act_lc_out_form"/>
</record>
<record model="ir.action.act_window.view" id="act_lc_out_form_view2">
<field name="sequence" eval="80"/>
<field name="view" ref="lc_out_view_form"/>
<field name="act_window" ref="act_lc_out_form"/>
</record>
<record model="ir.ui.view" id="lc_doc_prepa_view_list">
<field name="model">lc.document.prepared</field>
<field name="type">tree</field>
<field name="name">document_prepared_tree</field>
</record>
<record model="ir.ui.view" id="lc_doc_prepa_view_form">
<field name="model">lc.document.prepared</field>
<field name="type">form</field>
<field name="name">document_prepared_form</field>
</record>
<record model="ir.ui.view" id="lc_doc_track_view_list">
<field name="model">lc.document.received</field>
<field name="type">tree</field>
<field name="name">document_tracking_tree</field>
</record>
<record model="ir.ui.view" id="lc_doc_track_view_form">
<field name="model">lc.document.received</field>
<field name="type">form</field>
<field name="name">document_tracking_form</field>
</record>
<record model="ir.ui.view" id="lc_in_view_list">
<field name="model">lc.letter.incoming</field>
<field name="type">tree</field>
<field name="name">lc_letter_incoming_tree</field>
</record>
<record model="ir.ui.view" id="lc_in_view_form">
<field name="model">lc.letter.incoming</field>
<field name="type">form</field>
<field name="name">lc_letter_incoming_form</field>
</record>
<record model="ir.action.act_window" id="act_lc_in_form">
<field name="name">Incoming LC</field>
<field name="res_model">lc.letter.incoming</field>
</record>
<record model="ir.action.act_window.view" id="act_lc_in_form_view">
<field name="sequence" eval="70"/>
<field name="view" ref="lc_in_view_list"/>
<field name="act_window" ref="act_lc_in_form"/>
</record>
<record model="ir.action.act_window.view" id="act_lc_in_form_view2">
<field name="sequence" eval="80"/>
<field name="view" ref="lc_in_view_form"/>
<field name="act_window" ref="act_lc_in_form"/>
</record>
<record model="ir.ui.view" id="lc_doc_view_list">
<field name="model">lc.document.type</field>
<field name="type">form</field>
<field name="name">lc_document_type_form</field>
</record>
<record model="ir.ui.view" id="import_swift_start_view_form">
<field name="model">lc.import_swift.start</field>
<field name="type">form</field>
<field name="name">import_swift_start_form</field>
</record>
<record model="ir.action.wizard" id="action_import_swift">
<field name="name">Import SWIFT</field>
<field name="wiz_name">lc.import_swift</field>
</record>
<record model="ir.action.keyword" id="swift_wizard_keyword">
<field name="keyword">form_action</field>
<field name="model">lc.letter.incoming,-1</field>
<field name="action" ref="action_import_swift"/>
</record>
<record model="ir.action.wizard" id="action_analyze_conditions">
<field name="name">Analyze conditions</field>
<field name="wiz_name">lc.analyze.conditions</field>
</record>
<record model="ir.action.keyword" id="analyze_wizard_keyword">
<field name="keyword">form_action</field>
<field name="model">lc.letter.incoming,-1</field>
<field name="action" ref="action_analyze_conditions"/>
</record>
<record model="ir.action.wizard" id="action_prepare_doc">
<field name="name">Prepare documents</field>
<field name="wiz_name">lc.prepare.doc</field>
</record>
<record model="ir.action.keyword" id="prepare_wizard_keyword">
<field name="keyword">form_action</field>
<field name="model">lc.letter.incoming,-1</field>
<field name="action" ref="action_prepare_doc"/>
</record>
<record model="ir.model.access" id="access_lc">
<field name="model">lc.letter.incoming</field>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<record model="ir.model.access" id="access_lc_lc">
<field name="model">lc.letter.incoming</field>
<field name="group" ref="group_lc_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<record model="ir.model.access" id="access_lc_out">
<field name="model">lc.letter.outgoing</field>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<record model="ir.model.access" id="access_lc_lc_out">
<field name="model">lc.letter.outgoing</field>
<field name="group" ref="group_lc_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<record model="ir.model.button" id="lc_in_cancel_button">
<field name="model">lc.letter.incoming</field>
<field name="name">cancel</field>
<field name="string">Cancel</field>
<field name="confirm">Are you sure you want to cancel the LC?</field>
</record>
<record model="ir.model.button" id="lc_in_draft_button">
<field name="model">lc.letter.incoming</field>
<field name="name">draft</field>
<field name="string">Reset to Draft</field>
</record>
<record model="ir.model.button" id="lc_in_submit_button">
<field name="model">lc.letter.incoming</field>
<field name="name">submit</field>
<field name="string">Submit</field>
<field name="confirm">Are you sure you want to submit the LC?</field>
</record>
<record model="ir.model.button" id="lc_in_approved_button">
<field name="model">lc.letter.incoming</field>
<field name="name">approve</field>
<field name="string">Approve</field>
<field name="confirm">Are you sure you want to approved the LC?</field>
</record>
<record model="ir.model.button" id="lc_out_cancel_button">
<field name="model">lc.letter.outgoing</field>
<field name="name">cancel</field>
<field name="string">Cancel</field>
<field name="confirm">Are you sure you want to cancel the LC?</field>
</record>
<record model="ir.model.button" id="lc_out_draft_button">
<field name="model">lc.letter.outgoing</field>
<field name="name">draft</field>
<field name="string">Reset to Draft</field>
</record>
<record model="ir.model.button" id="lc_out_submit_button">
<field name="model">lc.letter.outgoing</field>
<field name="name">submit</field>
<field name="string">Submit</field>
<field name="confirm">Are you sure you want to submit the LC?</field>
</record>
<record model="ir.model.button" id="lc_out_approved_button">
<field name="model">lc.letter.outgoing</field>
<field name="name">approve</field>
<field name="string">Approve</field>
<field name="confirm">Are you sure you want to approved the LC?</field>
</record>
<record model="ir.action.wizard" id="act_create_lc">
<field name="name">Create LC</field>
<field name="wiz_name">create.lc</field>
</record>
<record model="ir.action.keyword" id="act_create_lc_keyword">
<field name="keyword">form_action</field>
<field name="model">sale.sale,-1</field>
<field name="action" ref="act_create_lc"/>
</record>
<record model="ir.action.keyword" id="act_create_lc_keyword_purchase">
<field name="keyword">form_action</field>
<field name="model">purchase.purchase,-1</field>
<field name="action" ref="act_create_lc"/>
</record>
<record model="ir.ui.view" id="create_lc_start_form">
<field name="model">create.lc.start</field>
<field name="type">form</field>
<field name="name">create_lc_start_form</field>
</record>
<record model="ir.ui.view" id="create_lc_message_form">
<field name="model">create.lc.message</field>
<field name="type">form</field>
<field name="name">create_lc_message_form</field>
</record>
<!-- <menuitem
parent="bank.menu_banking"
sequence="99"
action="act_lc_form"
id="menu_lc_form"/> -->
<menuitem
parent="bank.menu_banking"
sequence="100"
action="act_lc_in_form"
id="menu_lc_in_form"/>
<menuitem
parent="bank.menu_banking"
sequence="101"
action="act_lc_out_form"
id="menu_lc_out_form"/>
</data>
</tryton>

3418
modules/purchase_trade/lot.py Executable file

File diff suppressed because it is too large Load Diff

389
modules/purchase_trade/lot.xml Executable file
View File

@@ -0,0 +1,389 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="lot_fcr_view_tree">
<field name="model">lot.fcr</field>
<field name="type">tree</field>
<field name="name">lot_fcr_tree</field>
</record>
<record model="ir.ui.view" id="lot_fcr_view_tree_sequence">
<field name="model">lot.fcr</field>
<field name="type">tree</field>
<field name="name">lot_fcr_tree_sequence</field>
</record>
<record model="ir.ui.view" id="lot_report_view_list">
<field name="model">lot.report</field>
<field name="type">tree</field>
<field name="name">lot_report_list</field>
</record>
<record model="ir.ui.view" id="lot_report_view_graph">
<field name="model">lot.report</field>
<field name="type">graph</field>
<field name="name">lot_report_graph</field>
</record>
<record model="ir.ui.view" id="view_lot_visgraph">
<field name="model">lot.lot</field>
<field name="type">graph</field>
<field name="name">lot_graph</field>
<field name="priority" eval="10"/>
</record>
<record model="ir.action.act_window" id="act_lot_visgraph">
<field name="name">Lots graph</field>
<field name="res_model">lot.lot</field>
</record>
<record model="ir.action.act_window.view" id="act_lot_visgraph_form_view">
<field name="sequence" eval="80"/>
<field name="view" ref="view_lot_visgraph"/>
<field name="act_window" ref="act_lot_visgraph"/>
</record>
<record model="ir.ui.view" id="view_lot_report_visgraph">
<field name="model">lot.report</field>
<field name="type">graph</field>
<field name="name">lot_graph</field>
</record>
<record model="ir.ui.view" id="lot_acc_graph_view_form">
<field name="model">lot.accounting</field>
<field name="type">form</field>
<field name="name">lot_acc_form</field>
</record>
<record model="ir.ui.view" id="lot_acc_graph_view_tree">
<field name="model">lot.accounting</field>
<field name="type">tree</field>
<field name="name">lot_acc_tree</field>
</record>
<record model="ir.action.act_window" id="act_lot_acc_form">
<field name="name">Accounting graph</field>
<field name="res_model">lot.accounting</field>
</record>
<record model="ir.action.act_window.view" id="act_lot_acc_form_view">
<field name="sequence" eval="80"/>
<field name="view" ref="lot_acc_graph_view_form"/>
<field name="act_window" ref="act_lot_acc_form"/>
</record>
<record model="ir.action.act_window" id="act_lot_fcr_form">
<field name="name">FCR</field>
<field name="res_model">lot.fcr</field>
</record>
<record model="ir.action.act_window.view" id="act_lot_fcr_form_view">
<field name="sequence" eval="80"/>
<field name="view" ref="lot_fcr_view_tree"/>
<field name="act_window" ref="act_lot_fcr_form"/>
</record>
<record model="ir.action.act_window" id="act_lot_report_form">
<field name="name">Lots management</field>
<field name="res_model">lot.report</field>
<field name="context_model">lot.context</field>
</record>
<record model="ir.action.act_window.view" id="act_lot_report_form_view">
<field name="sequence" eval="70"/>
<field name="view" ref="lot_report_view_list"/>
<field name="act_window" ref="act_lot_report_form"/>
</record>
<record model="ir.action.act_window.view" id="act_lot_report_form_graph2">
<field name="sequence" eval="75"/>
<field name="view" ref="view_lot_report_visgraph"/>
<field name="act_window" ref="act_lot_report_form"/>
</record>
<record model="ir.action.act_window.view" id="act_lot_report_form_graph">
<field name="sequence" eval="80"/>
<field name="view" ref="lot_report_view_graph"/>
<field name="act_window" ref="act_lot_report_form"/>
</record>
<record model="ir.ui.view" id="lot_report_context_view_form">
<field name="model">lot.context</field>
<field name="type">form</field>
<field name="name">lot_report_context_form</field>
</record>
<record model="ir.ui.view" id="shipping_view_form">
<field name="model">lot.shipping.start</field>
<field name="type">form</field>
<field name="name">lot_shipping_start_form</field>
</record>
<record model="ir.action.wizard" id="act_shipping">
<field name="name">🚢 Link lots to transport</field>
<field name="wiz_name">lot.shipping</field>
<field name="model">lot.report</field>
</record>
<record model="ir.action.keyword" id="act_shipping_keyword">
<field name="keyword">form_action</field>
<field name="model">lot.report,-1</field>
<field name="action" ref="act_shipping"/>
</record>
<record model="ir.ui.view" id="add_lot_view_form">
<field name="model">lot.add.lot</field>
<field name="type">form</field>
<field name="name">lot_add_start_form</field>
</record>
<record model="ir.ui.view" id="lot_view_tree_sequence5">
<field name="model">lot.add.line</field>
<field name="type">tree</field>
<field name="name">lot_tree_sequence5</field>
</record>
<record model="ir.ui.view" id="import_lot_view_form">
<field name="model">lot.import.lot</field>
<field name="type">form</field>
<field name="name">lot_import_start_form</field>
</record>
<record model="ir.ui.view" id="invoice_lot_view_form">
<field name="model">lot.invoice.start</field>
<field name="type">form</field>
<field name="name">lot_invoice_start_form</field>
</record>
<record model="ir.ui.view" id="invoicing_lot_view_tree">
<field name="model">lot.invoicing.lot</field>
<field name="type">tree</field>
<field name="name">lot_invoicing_lot_tree</field>
</record>
<record model="ir.ui.view" id="invoicing_fee_view_tree">
<field name="model">lot.invoicing.fee</field>
<field name="type">tree</field>
<field name="name">lot_invoicing_fee_tree</field>
</record>
<record model="ir.ui.view" id="invoicing_inv_view_tree">
<field name="model">lot.invoicing.inv</field>
<field name="type">tree</field>
<field name="name">lot_invoicing_inv_tree</field>
</record>
<record model="ir.ui.view" id="matching_view_form">
<field name="model">lot.matching.start</field>
<field name="type">form</field>
<field name="name">lot_matching_start_form</field>
</record>
<record model="ir.ui.view" id="matching_lot_view_tree">
<field name="model">lot.matching.lot</field>
<field name="type">tree</field>
<field name="name">lot_matching_lot_tree</field>
</record>
<record model="ir.ui.view" id="matching_lot_view_tree2">
<field name="model">lot.matching.lot</field>
<field name="type">tree</field>
<field name="name">lot_matching_lot_tree2</field>
</record>
<record model="ir.action.wizard" id="act_matching">
<field name="name">🔗 Apply matching</field>
<field name="wiz_name">lot.matching</field>
<field name="model">lot.report</field>
</record>
<record model="ir.action.keyword" id="act_matching_keyword">
<field name="keyword">form_action</field>
<field name="model">lot.report,-1</field>
<field name="action" ref="act_matching"/>
</record>
<record model="ir.ui.view" id="weighing_view_form">
<field name="model">lot.weighing.start</field>
<field name="type">form</field>
<field name="name">lot_weighing_start_form</field>
</record>
<record model="ir.ui.view" id="weighing_lot_view_tree">
<field name="model">lot.weighing.lot</field>
<field name="type">tree</field>
<field name="name">lot_weighing_lot_tree</field>
</record>
<record model="ir.action.wizard" id="act_weighing">
<field name="name">📦 Do weighing</field>
<field name="wiz_name">lot.weighing</field>
<field name="model">lot.report</field>
</record>
<record model="ir.action.keyword" id="act_weighing_keyword">
<field name="keyword">form_action</field>
<field name="model">lot.report,-1</field>
<field name="action" ref="act_weighing"/>
</record>
<record model="ir.ui.view" id="contracts_view_form">
<field name="model">contracts.start</field>
<field name="type">form</field>
<field name="name">contracts_start_form</field>
</record>
<record model="ir.ui.view" id="contract_detail_view_tree">
<field name="model">contract.detail</field>
<field name="type">tree</field>
<field name="name">contract_detail_tree</field>
</record>
<record model="ir.action.wizard" id="act_create_contracts">
<field name="name">📝 Create contracts</field>
<field name="wiz_name">create.contracts</field>
<field name="model">lot.report</field>
</record>
<record model="ir.action.keyword" id="act_create_contract_keyword">
<field name="keyword">form_action</field>
<field name="model">lot.report,-1</field>
<field name="action" ref="act_create_contracts"/>
</record>
<!-- <record model="ir.action.wizard" id="matching_unit">
<field name="name">Matching unit</field>
<field name="wiz_name">lot.matching_unit</field>
<field name="model">sale.line</field>
</record> -->
<record model="ir.action.wizard" id="act_adding">
<field name="name">📦 Add physical lots</field>
<field name="wiz_name">lot.add</field>
<field name="model">lot.report</field>
</record>
<record model="ir.action.keyword" id="act_adding_keyword">
<field name="keyword">form_action</field>
<field name="model">lot.report,-1</field>
<field name="action" ref="act_adding"/>
</record>
<record model="ir.action.wizard" id="act_importing">
<field name="name">📦 Import physical lots</field>
<field name="wiz_name">lot.import</field>
<field name="model">lot.report</field>
</record>
<record model="ir.action.keyword" id="act_importing_keyword">
<field name="keyword">form_action</field>
<field name="model">lot.report,-1</field>
<field name="action" ref="act_importing"/>
</record>
<record model="ir.action.wizard" id="act_removing">
<field name="name">📦 Remove physical lots</field>
<field name="wiz_name">lot.remove</field>
<field name="model">lot.report</field>
</record>
<record model="ir.action.keyword" id="act_removing_keyword">
<field name="keyword">form_action</field>
<field name="model">lot.report,-1</field>
<field name="action" ref="act_removing"/>
</record>
<record model="ir.action.wizard" id="act_invoicing">
<field name="name">🧾 Invoice physical lots</field>
<field name="wiz_name">lot.invoice</field>
<field name="model">lot.report</field>
</record>
<record model="ir.action.keyword" id="act_invoicing_keyword">
<field name="keyword">form_action</field>
<field name="model">lot.report,-1</field>
<field name="action" ref="act_invoicing"/>
</record>
<record model="ir.action.act_window" id="act_move_line_relate">
<field name="name">Account move's lines</field>
<field name="res_model">account.move.line</field>
<field
name="domain"
eval="[
If(Eval('active_model') == 'lot.report',
('lot', 'in', Eval('active_ids', [])), ()),
]"
pyson="1"/>
</record>
<record model="ir.action.act_window.view" id="act_move_line_relate_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="account.move_line_view_tree"/>
<field name="act_window" ref="act_move_line_relate"/>
</record>
<record model="ir.action.keyword" id="act_move_line_relate_keyword_lot_report">
<field name="keyword">form_relate</field>
<field name="model">lot.report,-1</field>
<field name="action" ref="act_move_line_relate"/>
</record>
<record model="ir.action.act_window" id="act_lot_acc_relate">
<field name="name">Accounting graph</field>
<field name="res_model">lot.accounting</field>
<field
name="domain"
eval="[
If(Eval('active_model') == 'lot.report',
('lot', 'in', Eval('active_ids', [])), ()),
]"
pyson="1"/>
</record>
<record model="ir.action.act_window.view" id="act_lot_acc_relate_tree">
<field name="sequence" eval="10"/>
<field name="view" ref="lot_acc_graph_view_tree"/>
<field name="act_window" ref="act_lot_acc_relate"/>
</record>
<record model="ir.action.act_window.view" id="act_lot_acc_relate_view1">
<field name="sequence" eval="20"/>
<field name="view" ref="lot_acc_graph_view_form"/>
<field name="act_window" ref="act_lot_acc_relate"/>
</record>
<record model="ir.action.keyword" id="act_lot_acc_relate_keyword_lot_report">
<field name="keyword">form_relate</field>
<field name="model">lot.report,-1</field>
<field name="action" ref="act_lot_acc_relate"/>
</record>
<menuitem
parent="purchase.menu_purchase"
action="act_lot_visgraph"
sequence="299"
id="menu_lot_graph"/>
<record model="ir.action.wizard" id="act_unmatch">
<field name="name">🔗 Unmatch</field>
<field name="wiz_name">lot.unmatch</field>
<field name="model">lot.report</field>
</record>
<record model="ir.action.keyword" id="act_unmatch_keyword">
<field name="keyword">form_action</field>
<field name="model">lot.report,-1</field>
<field name="action" ref="act_unmatch"/>
</record>
<record model="ir.action.wizard" id="act_unship">
<field name="name">🚢 Unship</field>
<field name="wiz_name">lot.unship</field>
<field name="model">lot.report</field>
</record>
<record model="ir.action.keyword" id="act_unship_keyword">
<field name="keyword">form_action</field>
<field name="model">lot.report,-1</field>
<field name="action" ref="act_unship"/>
</record>
<record model="ir.model.button" id="filters_button">
<field name="model">lot.matching.start</field>
<field name="name">filters</field>
<field name="string">FILTERS</field>
</record>
<record model="ir.model.button-res.group" id="filters_button_group_admin">
<field name="button" ref="filters_button"/>
<field name="group" ref="res.group_admin"/>
</record>
<record model="ir.model.access" id="access_lot">
<field name="model">lot.matching.start</field>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<record model="ir.model.access" id="access_lot_admin">
<field name="model">lot.matching.start</field>
<field name="group" ref="res.group_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<menuitem
parent="purchase.menu_purchase"
sequence="99"
action="act_lot_report_form"
id="menu_lot_report_form"/>
<!-- <menuitem
parent="purchase.menu_purchase"
sequence="100"
action="act_lot_form"
id="menu_lot_form"/> -->
<menuitem
parent="purchase.menu_purchase"
sequence="110"
action="act_lot_fcr_form"
id="menu_lot_fcr_form"/>
</data>
</tryton>

View File

@@ -0,0 +1,307 @@
from decimal import Decimal
from trytond.wizard import Wizard, StateView, StateTransition, Button
from trytond.model import ModelView, ModelSQL, fields
from trytond.pool import Pool, PoolMeta
from trytond.transaction import Transaction
from trytond.exceptions import UserError
import datetime
class Lot(metaclass=PoolMeta):
"Lot"
__name__ = 'lot.lot'
lot_role = fields.Selection([
('normal', 'Normal'),
('technical', 'Technical'),
], 'Role', required=True)
split_operations = fields.One2Many(
'lot.split.merge', 'source_lot', 'Split/Merge Ops'
)
@classmethod
def default_lot_role(cls):
return 'normal'
# ---------------------------------------------------------------------
# Technical helpers
# ---------------------------------------------------------------------
def _check_split_allowed(self):
if self.lot_role != 'normal':
raise UserError('Only normal lots can be split.')
if self.lot_type != 'physic':
raise UserError('Only physical lots can be split.')
if self.IsDelivered():
raise UserError('Delivered lots cannot be split.')
def _create_technical_parent(self):
Lot = Pool().get('lot.lot')
parent = Lot(
lot_name=f'TECH-{self.lot_name}',
lot_role='technical',
lot_type='virtual',
lot_product=self.lot_product,
lot_status=self.lot_status,
lot_av='locked',
)
Lot.save([parent])
return parent
def _clone_hist_with_ratio(self, ratio):
LotQtHist = Pool().get('lot.qt.hist')
hist = []
for h in self.lot_hist:
hist.append(
LotQtHist(
quantity_type=h.quantity_type,
quantity=h.quantity * ratio,
gross_quantity=h.gross_quantity * ratio,
)
)
return hist
# ---------------------------------------------------------------------
# SPLIT
# ---------------------------------------------------------------------
def split_by_qt(self, splits):
"""
splits = [
{'lot_qt': 10, 'name': 'Lot A'},
{'lot_qt': 20, 'name': 'Lot B'},
]
"""
self._check_split_allowed()
total_qt = sum([s['lot_qt'] for s in splits])
if total_qt > self.lot_qt:
raise UserError('Split quantity exceeds lot quantity.')
Lot = Pool().get('lot.lot')
Split = Pool().get('lot.split.merge')
parent = self._create_technical_parent()
children = []
for s in splits:
ratio = Decimal(s['lot_qt']) / Decimal(self.lot_qt)
child = Lot(
lot_name=s.get('name'),
lot_parent=parent,
lot_role='normal',
lot_type='physic',
lot_qt=s['lot_qt'],
lot_unit=self.lot_unit,
lot_product=self.lot_product,
lot_status=self.lot_status,
lot_av=self.lot_av,
lot_unit_line=self.lot_unit_line,
lot_hist=self._clone_hist_with_ratio(ratio),
)
children.append(child)
Lot.save(children)
# Update original lot
self.lot_qt -= total_qt
self.lot_hist = self._clone_hist_with_ratio(
Decimal(self.lot_qt) / Decimal(self.lot_qt + total_qt)
)
Lot.save([self])
ops = []
for c in children:
ops.append(
Split(
operation='split',
source_lot=self,
target_lot=c,
lot_qt=c.lot_qt,
quantity=c.get_current_quantity(),
date=datetime.datetime.now(),
)
)
Split.save(ops)
return children
def split_by_weight(self, splits):
"""
splits = [
{'quantity': Decimal('1200'), 'name': 'Lot A'},
{'quantity': Decimal('800'), 'name': 'Lot B'},
]
"""
self._check_split_allowed()
total_weight = sum([s['quantity'] for s in splits])
if total_weight > self.get_current_quantity():
raise UserError('Split weight exceeds lot weight.')
Lot = Pool().get('lot.lot')
Split = Pool().get('lot.split.merge')
parent = self._create_technical_parent()
children = []
for s in splits:
ratio = s['quantity'] / self.get_current_quantity()
child = Lot(
lot_name=s.get('name'),
lot_parent=parent,
lot_role='normal',
lot_type='physic',
lot_qt=float(self.lot_qt * ratio) if self.lot_qt else None,
lot_unit=self.lot_unit,
lot_product=self.lot_product,
lot_status=self.lot_status,
lot_av=self.lot_av,
lot_unit_line=self.lot_unit_line,
lot_hist=self._clone_hist_with_ratio(ratio),
)
children.append(child)
Lot.save(children)
ops = []
for c in children:
ops.append(
Split(
operation='split',
source_lot=self,
target_lot=c,
quantity=c.get_current_quantity(),
date=datetime.datetime.now(),
)
)
Split.save(ops)
self.lot_av = 'locked'
Lot.save([self])
return children
# ---------------------------------------------------------------------
# MERGE
# ---------------------------------------------------------------------
@classmethod
def merge_lots(cls, lots):
if len(lots) < 2:
raise UserError('At least two lots are required for merge.')
parent = lots[0].lot_parent
if not parent or parent.lot_role != 'technical':
raise UserError('Lots must share a technical parent.')
product = lots[0].lot_product
for l in lots:
if l.lot_product != product:
raise UserError('Cannot merge lots of different products.')
Lot = Pool().get('lot.lot')
Split = Pool().get('lot.split.merge')
total_qt = sum([l.lot_qt or 0 for l in lots])
total_weight = sum([l.get_current_quantity() for l in lots])
merged = Lot(
lot_name='MERGE-' + parent.lot_name,
lot_role='normal',
lot_type='physic',
lot_product=product,
lot_qt=total_qt,
lot_unit=lots[0].lot_unit,
lot_unit_line=lots[0].lot_unit_line,
)
merged.set_current_quantity(total_weight, total_weight, 1)
Lot.save([merged])
ops = []
for l in lots:
l.lot_av = 'locked'
ops.append(
Split(
operation='merge',
source_lot=l,
target_lot=merged,
lot_qt=l.lot_qt,
quantity=l.get_current_quantity(),
date=datetime.datetime.now(),
)
)
Lot.save(lots)
Split.save(ops)
return merged
class LotSplitMerge(ModelSQL, ModelView):
"Lot Split Merge"
__name__ = 'lot.split.merge'
operation = fields.Selection([
('split', 'Split'),
('merge', 'Merge'),
], 'Operation', required=True)
source_lot = fields.Many2One(
'lot.lot', 'Source Lot', required=True, ondelete='CASCADE'
)
target_lot = fields.Many2One(
'lot.lot', 'Target Lot', required=True, ondelete='CASCADE'
)
lot_qt = fields.Float('Elements count')
quantity = fields.Numeric('Weight', digits=(16, 5))
date = fields.DateTime('Date', required=True)
reversed_by = fields.Many2One(
'lot.split.merge', 'Reversed By'
)
class SplitWizardStart(ModelView):
"Split Wizard Start"
__name__ = 'lot.split.wizard.start'
mode = fields.Selection([
('qt', 'By quantity'),
('weight', 'By weight'),
], 'Mode', required=True)
class SplitWizard(Wizard):
"Lot Split Wizard"
__name____ = 'lot.split.wizard'
start = StateView(
'lot.split.wizard.start',
'purchase_trade.split_wizard_start_view',
[
Button('Cancel', 'end', 'tryton-cancel'),
Button('Split', 'split', 'tryton-ok', default=True),
]
)
split = StateTransition()
def transition_split(self):
Lot = Pool().get('lot.lot')
lot = Lot(Transaction().context['active_id'])
if self.start.mode == 'qt':
lot.split_by_qt([
{'lot_qt': lot.lot_qt / 2, 'name': lot.lot_name + '-1'},
{'lot_qt': lot.lot_qt / 2, 'name': lot.lot_name + '-2'},
])
else:
q = lot.get_current_quantity()
lot.split_by_weight([
{'quantity': q / 2, 'name': lot.lot_name + '-1'},
{'quantity': q / 2, 'name': lot.lot_name + '-2'},
])
return 'end'

View File

@@ -0,0 +1,292 @@
from functools import wraps
from trytond.model import ModelSingleton, ModelSQL, ModelView, fields
from trytond.report import Report
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id, If, PYSONEncoder
from trytond.model import (ModelSQL, ModelView)
from trytond.tools import (cursor_dict, is_full_text, lstrip_wildcard)
from trytond.transaction import Transaction, inactive_records
from decimal import getcontext, Decimal, ROUND_HALF_UP
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
from sql.conditionals import Case
from sql.functions import CurrentTimestamp, DateTrunc, Abs
from sql import Column, Literal, Union, Select
from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction
from itertools import chain, groupby
from operator import itemgetter
import datetime
import logging
import json
import jwt
from collections import defaultdict
from trytond.exceptions import UserWarning, UserError
logger = logging.getLogger(__name__)
class OpenPosition(ModelSQL):
"Open position"
__name__ = 'open.position'
product = fields.Many2One('product.product', "Product", required=True)
purchase = fields.Many2One('purchase.purchase',"Purchase")
line = fields.Many2One('purchase.line',"Line")
supplier = fields.Many2One('party.party', "Supplier", help="Optional counterparty aggregation")
currency = fields.Many2One('currency.currency', "Currency", required=True)
uom = fields.Many2One('product.uom', "Unit of Measure")
period_start = fields.Date("Period start")
period_end = fields.Date("Period end")
type = fields.Selection([
('priced', 'Priced'),
('hedge', 'Hedge'),
('physic', 'Physic'),
('shipped', 'Shipped'),
('open', 'Open')
], "Type")
physical_qty = fields.Numeric("Priced Quantity", digits=(16, 6))
hedged_qty = fields.Numeric("Hedged Quantity", digits=(16, 6))
net_exposure = fields.Numeric("Open Quantity", digits=(16, 6))
price = fields.Numeric("Price", digits=(16, 6))
amount = fields.Numeric("Amount", digits=(16, 6))
curve = fields.Many2One('price.price')
mtm_price = fields.Numeric("Mtm price", digits=(16, 6))
mtm = fields.Numeric("Mtm amount", digits=(16, 2),help="Unrealised MTM for open quantities (in currency)")
sensitivity = fields.Numeric("Sensitivity (per unit)", digits=(16, 6),help="MTM change per 1 unit change in price")
@classmethod
def create_from_sale_line(cls, line):
Date = Pool().get('ir.date')
OpenPosition = Pool().get('open.position')
op = OpenPosition.search([('sale_line','=',line.id)])
if op:
OpenPosition.delete(op)
vals = {
'product': line.product.id,
'sale': line.sale.id,
'sale_line': line.id,
'client': line.sale.party.id,
'currency': line.sale.currency.id,
'uom': line.unit.id,
'period_start': line.from_del,
'period_end': line.to_del
}
##OPEN PART###
vals['type'] = 'open'
vals['physical_qty'] = round(line.quantity_theorical - Decimal(line.quantity),5)
if vals['physical_qty'] == Decimal(0) and line.lots and len(line.lots)==1:
vals['physical_qty'] = Decimal(line.quantity)
vals['physical_qty'] = Decimal(vals['physical_qty'])*Decimal(-1)
vals['hedged_qty'] = Decimal(0)
vals['net_exposure'] = vals['physical_qty']
vals['price'] = line.unit_price
vals['amount'] = round(vals['price'] * vals['physical_qty'],2)
if line.mtm:
for mt in line.mtm:
vals['curve'] = mt.price_index.id
vals['mtm_price'] = round(Decimal(mt.price_index.get_price(Date.today(),line.unit,line.currency,True)),4)
vals['mtm'] = round(vals['mtm_price'] * vals['physical_qty'],2)
OpenPosition.create([vals])
else:
OpenPosition.create([vals])
@classmethod
def create_from_purchase_line(cls, line):
OpenPosition = Pool().get('open.position')
Date = Pool().get('ir.date')
op = OpenPosition.search([('line','=',line.id)])
if op:
OpenPosition.delete(op)
vals = {
'product': line.product.id,
'purchase': line.purchase.id,
'line': line.id,
'supplier': line.purchase.party.id,
'currency': line.purchase.currency.id,
'uom': line.unit.id,
'period_start': line.from_del,
'period_end': line.to_del
}
##OPEN PART###
vals['type'] = 'open'
vals['physical_qty'] = round(line.quantity_theorical - Decimal(line.quantity),5)
if vals['physical_qty'] == Decimal(0) and line.lots and len(line.lots)==1:
vals['physical_qty'] = Decimal(line.quantity)
vals['hedged_qty'] = Decimal(0)
vals['net_exposure'] = vals['physical_qty']
vals['price'] = line.unit_price
vals['amount'] = round(vals['price'] * vals['physical_qty'],2)
if line.mtm:
for mt in line.mtm:
vals['curve'] = mt.price_index.id
vals['mtm_price'] = round(Decimal(mt.price_index.get_price(Date.today(),line.unit,line.currency,True)),4)
vals['mtm'] = round(vals['mtm_price'] * vals['physical_qty'],2)
OpenPosition.create([vals])
else:
OpenPosition.create([vals])
##PHYSIC PART###
for lot in line.lots:
if not lot.invoice_line and not lot.invoice_line_prov and lot.lot_type == 'physic':
if lot.GetShipment('in'):
vals['type'] = 'shipped'
else:
vals['type'] = 'physic'
vals['physical_qty'] = lot.get_current_quantity_converted()
vals['hedged_qty'] = Decimal(0)
vals['net_exposure'] = vals['physical_qty']
vals['price'] = lot.get_lot_price()
vals['amount'] = round(vals['price'] * vals['physical_qty'],2)
if line.mtm:
for mt in line.mtm:
vals['curve'] = mt.price_index.id
vals['mtm_price'] = round(Decimal(mt.price_index.get_price(Date.today(),line.unit,line.currency,True)),4)
vals['mtm'] = round(vals['mtm_price'] * vals['physical_qty'],2)
OpenPosition.create([vals])
else:
OpenPosition.create([vals])
##DERIVATIVE PART###
if line.derivatives:
for d in line.derivatives:
vals['type'] = 'hedge'
vals['physical_qty'] = Decimal(0)
vals['hedged_qty'] = -d.quantity
vals['net_exposure'] = d.quantity
vals['price'] = d.price
vals['amount'] = round(vals['price'] * d.quantity,2)
vals['curve'] = d.price_index.id
vals['mtm_price'] = round(Decimal(d.price_index.get_price(Date.today(),line.unit,line.currency,True)),4)
vals['mtm'] = round(vals['mtm_price'] * d.quantity,2)
OpenPosition.create([vals])
class OpenPositionReport(
ModelSQL, ModelView):
"Open position report"
__name__ = 'open.position.report'
product = fields.Many2One('product.product', "Product", required=True)
purchase = fields.Many2One('purchase.purchase',"Purchase")
line = fields.Many2One('purchase.line',"Line")
sale = fields.Many2One('sale.sale',"Sale")
sale_line = fields.Many2One('sale.line',"Sale Line")
supplier = fields.Many2One('party.party', "Supplier")
client = fields.Many2One('party.party', "Client")
currency = fields.Many2One('currency.currency', "Currency", required=True)
uom = fields.Many2One('product.uom', "Unit of Measure")
period_start = fields.Date("Period start")
period_end = fields.Date("Period end")
type = fields.Selection([
('priced', 'Priced'),
('hedge', 'Hedge'),
('physic', 'Physic'),
('shipped', 'Shipped'),
('open', 'Open')
], "Type")
physical_qty = fields.Numeric("Priced Quantity", digits=(16, 5))
hedged_qty = fields.Numeric("Hedged Quantity", digits=(16, 5))
net_exposure = fields.Numeric("Open Quantity", digits=(16, 5))
price = fields.Numeric("Price", digits=(16, 4))
amount = fields.Numeric("Amount", digits=(16, 2))
curve = fields.Many2One('price.price')
mtm_price = fields.Numeric("Mtm price", digits=(16, 4))
mtm = fields.Numeric("Mtm amount", digits=(16, 2),help="Unrealised MTM for open quantities (in currency)")
pnl = fields.Numeric("Pnl", digits=(16, 2))
sensitivity = fields.Numeric("Sensitivity (per unit)", digits=(16, 1),help="MTM change per 1 unit change in price")
@classmethod
def table_query(cls):
OpenPosition = Pool().get('open.position')
op = OpenPosition.__table__()
context = Transaction().context
supplier = context.get('supplier')
purchase = context.get('purchase')
client = context.get('client')
sale = context.get('sale')
product = context.get('product')
asof = context.get('asof')
todate = context.get('todate')
state = context.get('state')
wh = Literal(True)
if supplier:
wh &= (op.supplier == supplier)
if client:
wh &= (op.client == client)
if product:
wh &= (op.product == product)
if purchase:
wh &= (op.purchase == purchase)
if sale:
wh &= (op.sale == sale)
# if asof and todate:
# wh &= (pu.purchase_date >= asof) & (pu.purchase_date <= todate)
query = (
op
.select(
Literal(0).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
Literal(0).as_('write_uid'),
Literal(0).as_('write_date'),
op.id.as_('id'),
op.product.as_('product'),
op.supplier.as_('supplier'),
op.client.as_('client'),
op.purchase.as_('purchase'),
op.sale.as_('sale'),
op.line.as_('line'),
op.sale_line.as_('sale_line'),
op.type.as_('type'),
op.currency.as_('currency'),
op.uom.as_('uom'),
op.period_start.as_('period_start'),
op.period_end.as_('period_end'),
op.physical_qty.as_('physical_qty'),
op.hedged_qty.as_('hedged_qty'),
op.net_exposure.as_('net_exposure'),
op.price.as_('price'),
op.amount.as_('amount'),
op.curve.as_('curve'),
op.mtm_price.as_('mtm_price'),
op.mtm.as_('mtm'),
(op.amount-op.mtm).as_('pnl'),
op.sensitivity.as_('sensitivity'),
where=wh
)
)
return query
class OpenPositionContext(ModelView):
"Open Position Context"
__name__ = 'open.position.context'
asof = fields.Date("As of")
todate = fields.Date("To")
supplier = fields.Many2One('party.party',"Supplier")
client = fields.Many2One('party.party',"Client")
product = fields.Many2One('product.product',"Product")
purchase = fields.Many2One('purchase.purchase', "Purchase")
sale = fields.Many2One('sale.sale',"Sale")
state = fields.Selection([
('all', 'All'),
('open', 'Open'),
('fixed', 'Fixed'),
('hedged', 'Hedged')
], 'State')
@classmethod
def default_asof(cls):
pool = Pool()
Date = pool.get('ir.date')
return Date.today().replace(day=1,month=1,year=1999)
@classmethod
def default_todate(cls):
pool = Pool()
Date = pool.get('ir.date')
return Date.today()
@classmethod
def default_state(cls):
return 'all'

View File

@@ -0,0 +1,30 @@
<tryton>
<data>
<record model="ir.ui.view" id="open_position_context_view_form">
<field name="model">open.position.context</field>
<field name="type">form</field>
<field name="name">open_position_context_form</field>
</record>
<record model="ir.ui.view" id="open_position_view_list">
<field name="model">open.position.report</field>
<field name="type">tree</field>
<field name="name">open_position_list</field>
</record>
<record model="ir.action.act_window" id="act_open_position_form">
<field name="name">Open position</field>
<field name="res_model">open.position.report</field>
<field name="context_model">open.position.context</field>
</record>
<record model="ir.action.act_window.view" id="act_open_position_form_view">
<field name="sequence" eval="70"/>
<field name="view" ref="open_position_view_list"/>
<field name="act_window" ref="act_open_position_form"/>
</record>
<menuitem
parent="purchase_trade.menu_global_reporting"
sequence="110"
action="act_open_position_form"
id="menu_open_position_form"/>
</data>
</tryton>

View File

@@ -0,0 +1,12 @@
from trytond.model import ModelSingleton, ModelSQL, ModelView, fields
class OptionalScenario(ModelSQL,ModelView):
"Optionals Scenarios"
__name__ = 'optional.scenario'
line = fields.Many2One('purchase.line',"Line")
name = fields.Char("Scenario")
premium = fields.Numeric("Premium", digits=(16,4))
from_location = fields.Many2One('stock.location',"Loading")
to_location = fields.Many2One('stock.location',"Destination")
activate = fields.Boolean("Activated")

View File

@@ -0,0 +1,10 @@
<tryton>
<data>
<record model="ir.ui.view" id="optional_view_tree">
<field name="model">optional.scenario</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">optional_tree</field>
</record>
</data>
</tryton>

View 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

17
modules/purchase_trade/party.py Executable file
View File

@@ -0,0 +1,17 @@
from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import PoolMeta
from trytond.exceptions import UserError
from trytond.modules.purchase_trade.purchase import (TRIGGERS)
__all__ = ['Party']
__metaclass__ = PoolMeta
class Party(metaclass=PoolMeta):
__name__ = 'party.party'
tol_min = fields.Numeric("Tol - in %")
tol_max = fields.Numeric("Tol + in %")
wb = fields.Many2One('purchase.weight.basis',"Weight basis")
association = fields.Many2One('purchase.association',"Association")

View File

@@ -0,0 +1,5 @@
<record model="ir.ui.view" id="party_view_form">
<field name="model">party.party</field>
<field name="inherit" ref="party.party_view_form"/>
<field name="name">party_form</field>
</record>

View File

@@ -0,0 +1,48 @@
from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import PoolMeta
from trytond.exceptions import UserError
from trytond.modules.purchase_trade.purchase import (TRIGGERS)
__all__ = ['PaymentTerm', 'PaymentTermLine']
__metaclass__ = PoolMeta
class PaymentTerm(metaclass=PoolMeta):
__name__ = 'account.invoice.payment_term'
# champs supplémentaires pour gérer vos Mixed Terms
is_mixed = fields.Boolean('Is Mixed Term', help="Indicates a mixed payment term")
class PaymentTermLine(metaclass=PoolMeta):
__name__ = 'account.invoice.payment_term.line'
# champs supplémentaires pour Atomic Term
trigger_event = fields.Selection(TRIGGERS, 'Trigger Event')
term_type = fields.Selection([
('advance', 'Advance'),
('cad', 'CAD'),
('open', 'Open'),
('lc', 'LC'),
('other', 'Other'),
], 'Term Type')
trigger_offset = fields.Integer('Trigger Offset')
offset_unit = fields.Selection([
('calendar', 'Calendar Days'),
('business', 'Business Days'),
], 'Offset Unit')
eom_flag = fields.Boolean('EOM Flag')
eom_mode = fields.Selection([
('standard', 'Standard'),
('before', 'Before EOM'),
('after', 'After EOM'),
], 'EOM Mode')
risk_classification = fields.Selection([
('fully_secured', 'Fully Secured'),
('partially_secured', 'Partially Secured'),
('unsecured', 'Unsecured'),
], 'Risk Classification')

View File

@@ -0,0 +1,24 @@
<?xml version="1.0"?>
<tryton>
<data>
<!-- Héritage de la vue Payment Term (form) -->
<record model="ir.ui.view" id="payment_term_purchase_trade_form">
<field name="model">account.invoice.payment_term</field>
<field name="inherit" ref="account_invoice.payment_term_view_form"/>
<field name="name">payment_term_purchase_trade_form</field>
</record>
<!-- Héritage de la vue Payment Term Line (form) -->
<record model="ir.ui.view" id="payment_term_line_purchase_trade_form">
<field name="model">account.invoice.payment_term.line</field>
<field name="inherit" ref="account_invoice.payment_term_line_view_form"/>
<field name="name">payment_term_line_purchase_trade_form</field>
</record>
<record model="ir.ui.view" id="payment_term_line_purchase_trade_tree">
<field name="model">account.invoice.payment_term.line</field>
<field name="inherit" ref="account_invoice.payment_term_line_view_list_sequence"/>
<field name="name">payment_term_line_purchase_trade_tree</field>
</record>
</data>
</tryton>

394
modules/purchase_trade/pricing.py Executable file
View File

@@ -0,0 +1,394 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.model import fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id
from trytond.model import (ModelSQL, ModelView)
from trytond.tools import is_full_text, lstrip_wildcard
from trytond.transaction import Transaction, inactive_records
from decimal import getcontext, Decimal, ROUND_HALF_UP
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
from sql.conditionals import Case
from sql import Column, Literal
from sql.functions import CurrentTimestamp, DateTrunc
from trytond.wizard import Button, StateTransition, StateView, Wizard
from itertools import chain, groupby
from operator import itemgetter
import datetime
import logging
from trytond.modules.purchase_trade.purchase import (TRIGGERS)
logger = logging.getLogger(__name__)
DAYTYPES = [
(None,''),
('before', 'Nb days before'),
('after', 'Nb days after'),
('first', 'First day'),
('last', 'Last day'),
('xth', 'Nth day'),
]
WEEKDAY_MAP = {
'monday': 0,
'tuesday': 1,
'wednesday': 2,
'thursday': 3,
'friday': 4,
'saturday': 5,
'sunday': 6
}
DAYS = [
(None,''),
('monday', 'Monday'),
('tuesday', 'Tuesday'),
('wednesday', 'Wednesday'),
('thursday', 'Thursday'),
('friday', 'Friday'),
('saturday', 'Saturday'),
('sunday', 'Sunday'),
]
class Estimated(ModelSQL, ModelView):
"Estimated date"
__name__ = 'pricing.estimated'
trigger = fields.Selection(TRIGGERS,"Trigger")
estimated_date = fields.Date("Estimated date")
class Mtm(ModelSQL, ModelView):
"Mtm"
__name__ = 'mtm.component'
fix_type = fields.Many2One('price.fixtype',"Fixation type")
ratio = fields.Numeric("%")
price_index = fields.Many2One('price.price',"Curve")
currency = fields.Function(fields.Many2One('currency.currency',"Curr."),'get_cur')
def get_cur(self,name):
if self.price_index:
PI = Pool().get('price.price')
pi = PI(self.price_index)
return pi.price_currency
class Component(ModelSQL, ModelView):
"Component"
__name__ = 'pricing.component'
fix_type = fields.Many2One('price.fixtype',"Fixation type")
ratio = fields.Numeric("%")
price_index = fields.Many2One('price.price',"Curve")
currency = fields.Function(fields.Many2One('currency.currency',"Curr."),'get_cur')
auto = fields.Boolean("Auto")
fallback = fields.Boolean("Fallback")
calendar = fields.Many2One('price.calendar',"Calendar")
nbdays = fields.Function(fields.Integer("Nb days"),'get_nbdays')
triggers = fields.One2Many('pricing.trigger','component',"Period rules")
pricing_date = fields.Date("Pricing date max")
def get_rec_name(self, name=None):
if self.price_index:
return '[' + self.fix_type.name + '] ' + self.price_index.price_index
else:
return '[' + self.fix_type.name + '] '
def get_cur(self,name):
if self.price_index:
PI = Pool().get('price.price')
pi = PI(self.price_index)
return pi.price_currency
def get_nbdays(self, name):
days = 0
if self.triggers:
for t in self.triggers:
l,l2 = t.getApplicationListDates(self.calendar)
days += len(l)
return days
@classmethod
def delete(cls, components):
for cp in components:
Pricing = Pool().get('pricing.pricing')
pricings = Pricing.search(['price_component','=',cp.id])
if pricings:
Pricing.delete(pricings)
super(Component, cls).delete(components)
class Pricing(ModelSQL,ModelView):
"Pricing"
__name__ = 'pricing.pricing'
pricing_date = fields.Date("Date")
price_component = fields.Many2One('pricing.component', "Component")#, domain=[('id', 'in', Eval('line.price_components'))], ondelete='CASCADE')
quantity = fields.Numeric("Qt",digits='unit')
settl_price = fields.Numeric("Settl. price",digits='unit')
fixed_qt = fields.Numeric("Fixed qt",digits='unit',readonly=True)
fixed_qt_price = fields.Numeric("Fixed qt price",digits='unit',readonly=True)
unfixed_qt = fields.Numeric("Unfixed qt",digits='unit',readonly=True)
unfixed_qt_price = fields.Numeric("Unfixed qt price",digits='unit',readonly=True)
eod_price = fields.Numeric("EOD price",digits='unit',readonly=True)
last = fields.Boolean("Last")
@classmethod
def default_fixed_qt(cls):
return Decimal(0)
@classmethod
def default_unfixed_qt(cls):
return Decimal(0)
@classmethod
def default_fixed_qt_price(cls):
return Decimal(0)
@classmethod
def default_unfixed_qt_price(cls):
return Decimal(0)
@classmethod
def default_quantity(cls):
return Decimal(0)
@classmethod
def default_settl_price(cls):
return Decimal(0)
@classmethod
def default_eod_price(cls):
return Decimal(0)
def get_fixed_price(self):
price = Decimal(0)
Pricing = Pool().get('pricing.pricing')
pricings = Pricing.search(['price_component','=',self.price_component.id],order=[('pricing_date', 'ASC')])
if pricings:
cumul_qt = Decimal(0)
cumul_qt_price = Decimal(0)
for pr in pricings:
cumul_qt += pr.quantity
cumul_qt_price += pr.quantity * pr.settl_price
if pr.id == self.id:
break
if cumul_qt > 0:
price = cumul_qt_price / cumul_qt
return round(price,4)
class Trigger(ModelSQL,ModelView):
"Period rules"
__name__ = "pricing.trigger"
component = fields.Many2One('pricing.component',"Component", ondelete='CASCADE')
pricing_period = fields.Many2One('pricing.period',"Pricing period")
from_p = fields.Date("From",
states={
'readonly': Eval('pricing_period') != None,
})
to_p = fields.Date("To",
states={
'readonly': Eval('pricing_period') != None,
})
average = fields.Boolean("Avg")
application_period = fields.Many2One('pricing.period',"Application period")
from_a = fields.Date("From",
states={
'readonly': Eval('application_period') != None,
})
to_a = fields.Date("To",
states={
'readonly': Eval('application_period') != None,
})
@fields.depends('pricing_period')
def on_change_with_application_period(self):
if not self.application_period and self.pricing_period:
return self.pricing_period
def getDateWithEstTrigger(self, period):
PP = Pool().get('pricing.period')
if period == 1:
pp = PP(self.pricing_period)
else:
pp = PP(self.application_period)
CO = Pool().get('pricing.component')
co = CO(self.component)
logger.info("DELDATEEST_:%s",co)
if co.line:
d = co.getEstimatedTriggerPurchase(pp.trigger)
else:
d = co.getEstimatedTriggerSale(pp.trigger)
logger.info("DELDATEEST:%s",d)
date_from,date_to,dates = pp.getDates(d)
logger.info("DELDATEEST2:%s",dates)
return date_from,date_to,d,pp.include,dates
def getApplicationListDates(self, cal):
ld = []
if self.application_period:
date_from, date_to, d, include,dates = self.getDateWithEstTrigger(2)
else:
date_from = self.from_a
date_to = self.to_a
d = None
include = False
ld, lprice = self.getListDates(date_from,date_to,d,include,cal,2,dates)
return ld, lprice
def getPricingListDates(self,cal):
ld = []
if self.pricing_period:
date_from, date_to, d, include,dates = self.getDateWithEstTrigger(1)
else:
date_from = self.from_p#datetime.datetime(self.from_p.year, self.from_p.month, self.from_p.day)
date_to = self.to_p#datetime.datetime(self.to_p.year, self.to_p.month, self.to_p.day)
d = None
include = False
ld, lprice = self.getListDates(date_from,date_to,d,include,cal,1,dates)
return ld, lprice
def getListDates(self,df,dt,t,i,cal,pricing,dates):
l = []
lprice = []
CAL = Pool().get('price.calendar')
if cal:
cal = CAL(cal)
if dates:
for d in dates:
if cal.IsQuote(d):
l.append(d)
if pricing == 1:
lprice.append(self.getprice(d))
return l, lprice
if df and dt:
current_date = datetime.datetime(df.year,df.month,df.day)
dt = datetime.datetime(dt.year,dt.month,dt.day)
while current_date <= dt:
if i or (not i and current_date != t):
if cal:
if cal.IsQuote(current_date):
l.append(current_date)
if pricing == 1:
lprice.append(self.getprice(current_date))
else:
l.append(current_date)
if pricing == 1:
lprice.append(self.getprice(current_date))
current_date += datetime.timedelta(days=1)
return l, lprice
def getprice(self,current_date):
PI = Pool().get('price.price')
PC = Pool().get('pricing.component')
pc = PC(self.component)
pi = PI(pc.price_index)
val = {}
val['date'] = current_date
val['price'] = pi.get_price(current_date,pc.line.unit if pc.line else pc.sale_line.unit,pc.line.currency if pc.line else pc.sale_line.currency)
val['avg'] = val['price']
val['avg_minus_1'] = val['price']
val['isAvg'] = self.average
return val
class Period(ModelSQL,ModelView):
"Period"
__name__ = 'pricing.period'
name = fields.Char("Name")
trigger = fields.Selection(TRIGGERS, 'Trigger')
include = fields.Boolean("Inc.")
startday = fields.Selection(DAYTYPES,"Start day")
nbds = fields.Integer("Nb")
endday = fields.Selection(DAYTYPES,"End day")
nbde = fields.Integer("Nb")
nbms = fields.Integer("Starting month")
nbme = fields.Integer("Ending month")
every = fields.Selection(DAYS,"Every")
nb_quotation = fields.Integer("Nb quotation")
@classmethod
def default_nbds(cls):
return 0
@classmethod
def default_nbde(cls):
return 0
@classmethod
def default_nbms(cls):
return 0
@classmethod
def default_nbme(cls):
return 0
def getDates(self,t):
date_from = None
date_to = None
dates = []
logger.info("GETDATES:%s",t)
logger.info("GETDATES:%s",self.every)
if t:
if self.every:
if t:
j = self.every
if j not in WEEKDAY_MAP:
raise ValueError(f"Invalid day : '{j}'")
weekday_target = WEEKDAY_MAP[j]
if self.trigger == 'delmonth':
first_day = t.replace(day=1)
days_to_add = (weekday_target - first_day.weekday()) % 7
current = first_day + datetime.timedelta(days=days_to_add)
while current.month == t.month:
dates.append(datetime.datetime(current.year, current.month, current.day))
current += datetime.timedelta(days=7)
logger.info("GETDATES:%s",dates)
elif self.nb_quotation > 0:
days_to_add = (weekday_target - t.weekday()) % 7
current = t + datetime.timedelta(days=days_to_add)
while len(dates) < self.nb_quotation:
dates.append(datetime.datetime(current.year, current.month, current.day))
current += datetime.timedelta(days=7)
logger.info("GETDATES:%s",dates)
elif self.nb_quotation < 0:
days_to_sub = (t.weekday() - weekday_target) % 7
current = t - datetime.timedelta(days=days_to_sub)
while len(dates) < -self.nb_quotation:
dates.append(datetime.datetime(current.year, current.month, current.day))
current -= datetime.timedelta(days=7)
logger.info("GETDATES:%s",dates)
else:
if self.startday == 'before':
date_from = t - datetime.timedelta(days=(self.nbds if self.nbds else 0))
elif self.startday == 'after':
date_from = t + datetime.timedelta(days=(self.nbds if self.nbds else 0))
elif self.startday == 'first':
date_from = datetime.datetime(t.year, t.month % 12 + (self.nbms if self.nbms else 0), 1)
elif self.startday == 'last':
date_from = datetime.datetime(t.year, t.month % 12 + 1, 1) - datetime.timedelta(days=1)
elif self.startday == 'xth':
date_from = datetime.datetime(t.year, t.month % 12, (self.nbds if self.nbds else 1))
else:
date_from = datetime.datetime(t.year, t.month, t.day)
if self.endday == 'before':
date_to = t - datetime.timedelta(days=(self.nbde if self.nbde else 0))
elif self.endday == 'after':
date_to = t + datetime.timedelta(days=(self.nbde if self.nbde else 0))
elif self.endday == 'first':
date_to = datetime.datetime(t.year, t.month % 12 + (self.nbme if self.nbme else 0), 1)
elif self.endday == 'last':
date_to = datetime.datetime(t.year, t.month % 12 + 1, 1) - datetime.timedelta(days=1)
elif self.endday == 'xth':
date_to = datetime.datetime(t.year, t.month % 12, (self.nbds if self.nbds else 1))
else:
date_to = date_from
return date_from, date_to, dates

View File

@@ -0,0 +1,108 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="summary_view_tree_sequence">
<field name="model">sale.pricing.summary</field>
<field name="type">tree</field>
<field name="priority" eval="20"/>
<field name="name">summary_tree_sequence</field>
</record>
<record model="ir.ui.view" id="summary_view_tree_sequence2">
<field name="model">purchase.pricing.summary</field>
<field name="type">tree</field>
<field name="priority" eval="20"/>
<field name="name">summary_tree_sequence</field>
</record>
<record model="ir.ui.view" id="estimated_view_tree">
<field name="model">pricing.estimated</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">estimated_tree</field>
</record>
<record model="ir.ui.view" id="component_view_tree">
<field name="model">pricing.component</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">component_tree</field>
</record>
<record model="ir.ui.view" id="component_view_tree_sequence">
<field name="model">pricing.component</field>
<field name="type">tree</field>
<field name="priority" eval="20"/>
<field name="name">component_tree_sequence</field>
</record>
<record model="ir.ui.view" id="component_view_tree_sequence2">
<field name="model">pricing.component</field>
<field name="type">tree</field>
<field name="priority" eval="20"/>
<field name="name">component_tree_sequence2</field>
</record>
<record model="ir.ui.view" id="component_view_form">
<field name="model">pricing.component</field>
<field name="type">form</field>
<field name="name">component_form</field>
</record>
<record model="ir.ui.view" id="component_view_form2">
<field name="model">pricing.component</field>
<field name="type">form</field>
<field name="name">component_form2</field>
</record>
<record model="ir.ui.view" id="pricing_view_tree">
<field name="model">pricing.pricing</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">pricing_tree</field>
</record>
<record model="ir.ui.view" id="pricing_view_tree_sequence">
<field name="model">pricing.pricing</field>
<field name="type">tree</field>
<field name="priority" eval="20"/>
<field name="name">pricing_tree_sequence</field>
</record>
<record model="ir.ui.view" id="pricing_view_form">
<field name="model">pricing.pricing</field>
<field name="type">form</field>
<field name="name">pricing_form</field>
</record>
<record model="ir.ui.view" id="trigger_view_tree">
<field name="model">pricing.trigger</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">trigger_tree</field>
</record>
<record model="ir.ui.view" id="trigger_view_tree_sequence">
<field name="model">pricing.trigger</field>
<field name="type">tree</field>
<field name="priority" eval="20"/>
<field name="name">trigger_tree_sequence</field>
</record>
<record model="ir.ui.view" id="trigger_view_form">
<field name="model">pricing.trigger</field>
<field name="type">form</field>
<field name="name">trigger_form</field>
</record>
<record model="ir.ui.view" id="period_view_tree">
<field name="model">pricing.period</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">period_tree</field>
</record>
<record model="ir.ui.view" id="period_view_tree_sequence">
<field name="model">pricing.period</field>
<field name="type">tree</field>
<field name="priority" eval="20"/>
<field name="name">period_tree_sequence</field>
</record>
<record model="ir.ui.view" id="period_view_form">
<field name="model">pricing.period</field>
<field name="type">form</field>
<field name="name">period_form</field>
</record>
</data>
</tryton>

1393
modules/purchase_trade/purchase.py Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,153 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.icon" id="account_icon">
<field name="name">tryton-account</field>
<field name="path">icons/tryton-account.svg</field>
</record>
<record model="ir.ui.icon" id="account_open_icon">
<field name="name">tryton-account-open</field>
<field name="path">icons/tryton-account-open.svg</field>
</record>
<record model="ir.ui.icon" id="account_close_icon">
<field name="name">tryton-account-close</field>
<field name="path">icons/tryton-account-close.svg</field>
</record>
<record model="ir.ui.icon" id="account_block_icon">
<field name="name">tryton-account-block</field>
<field name="path">icons/tryton-account-block.svg</field>
</record>
<record model="ir.ui.view" id="purchase_view_form">
<field name="model">purchase.purchase</field>
<field name="inherit" ref="purchase.purchase_view_form"/>
<field name="name">purchase_form</field>
</record>
<record model="ir.ui.view" id="purchase_view_tree">
<field name="model">purchase.purchase</field>
<field name="inherit" ref="purchase.purchase_view_tree"/>
<field name="name">purchase_tree</field>
</record>
<record model="ir.ui.view" id="purchase_line_view_form">
<field name="model">purchase.line</field>
<field name="inherit" ref="purchase.purchase_line_view_form"/>
<field name="name">purchase_line_form</field>
</record>
<record model="ir.ui.view" id="purchase_line_view_tree_sequence">
<field name="model">purchase.line</field>
<field name="inherit" ref="purchase.purchase_line_view_tree_sequence"/>
<field name="name">purchase_line_tree_sequence</field>
</record>
<record model="ir.action.wizard" id="act_bi">
<field name="name">🧠 Go to BI</field>
<field name="wiz_name">purchase.bi</field>
<field name="model">purchase.purchase</field>
</record>
<record model="ir.action.keyword" id="act_bi_keyword">
<field name="keyword">form_action</field>
<field name="model">purchase.purchase,-1</field>
<field name="action" ref="act_bi"/>
</record>
<record model="ir.action.url" id="url_bi">
<field name="name">Go to BI</field>
<field name="url">http://vps107.geneva.hosting:3000</field>
</record>
<record model="ir.action.wizard" id="act_purchase_allocations_wizard">
<field name="name">Lots Management</field>
<field name="wiz_name">purchase.allocations.wizard</field>
<field name="model">purchase.purchase</field>
</record>
<!-- Menu Relate dans Purchase -->
<record model="ir.action.keyword" id="act_purchase_allocations_keyword">
<field name="keyword">form_relate</field>
<field name="model">purchase.purchase,-1</field>
<field name="action" ref="act_purchase_allocations_wizard"/>
</record>
<record model="ir.action.wizard" id="act_open_payment">
<field name="name">Payments</field>
<field name="wiz_name">purchase.invoice.payment</field>
<field name="model">purchase.invoice.report</field>
</record>
<record model="ir.action.keyword" id="act_open_payment_keyword1">
<field name="keyword">tree_open</field>
<field name="model">purchase.invoice.report,-1</field>
<field name="action" ref="act_open_payment"/>
</record>
<record model="ir.action.keyword" id="act_open_account_keyword2">
<field name="keyword">form_relate</field>
<field name="model">purchase.invoice.report,-1</field>
<field name="action" ref="act_open_payment"/>
</record>
<record model="ir.ui.view" id="pur_inv_report_context_view_form">
<field name="model">purchase.invoice.context</field>
<field name="type">form</field>
<field name="name">pur_inv_report_context_form</field>
</record>
<record model="ir.ui.view" id="pur_inv_report_view_list">
<field name="model">purchase.invoice.report</field>
<field name="type">tree</field>
<field name="name">pur_inv_report_list</field>
</record>
<record model="ir.action.act_window" id="act_pur_inv_report_form">
<field name="name">Invoicing Report</field>
<field name="res_model">purchase.invoice.report</field>
<field name="context_model">purchase.invoice.context</field>
</record>
<record model="ir.action.act_window.view" id="act_pur_inv_report_form_view">
<field name="sequence" eval="70"/>
<field name="view" ref="pur_inv_report_view_list"/>
<field name="act_window" ref="act_pur_inv_report_form"/>
</record>
<record model="ir.ui.view" id="pnl_bi_view_graph">
<field name="model">pnl.bi</field>
<field name="type">form</field>
<field name="name">pnl_bi_graph</field>
</record>
<record model="ir.action.act_window" id="act_pnl_bi">
<field name="name">Pnl BI</field>
<field name="res_model">pnl.bi</field>
</record>
<record model="ir.action.act_window.view" id="act_pnl_bi_view">
<field name="sequence" eval="30"/>
<field name="view" ref="pnl_bi_view_graph"/>
<field name="act_window" ref="act_pnl_bi"/>
</record>
<record model="ir.action.wizard" id="act_pnl_report">
<field name="name">Pnl report</field>
<field name="wiz_name">pnl.report</field>
</record>
<record model="ir.ui.view" id="mtm_view_form">
<field name="model">mtm.component</field>
<field name="type">form</field>
<field name="name">mtm_form</field>
</record>
<record model="ir.ui.view" id="mtm_view_tree">
<field name="model">mtm.component</field>
<field name="type">tree</field>
<field name="name">mtm_tree</field>
</record>
<menuitem
name="Pnl Report"
parent="purchase_trade.menu_global_reporting"
action="act_pnl_bi"
sequence="110"
id="menu_pnl_bi"/>
<menuitem
parent="purchase_trade.menu_global_reporting"
sequence="100"
action="act_pur_inv_report_form"
id="menu_pur_inv_report_form"/>
</data>
</tryton>

View File

@@ -0,0 +1,147 @@
from decimal import Decimal
from trytond.model import ModelView, fields
from trytond.wizard import Wizard, StateView, StateTransition, StateAction, Button
from trytond.pool import Pool, PoolMeta
from trytond.transaction import Transaction
import logging
import json
from collections import defaultdict
from trytond.exceptions import UserWarning, UserError
logger = logging.getLogger(__name__)
__all__ = ['CreatePrepaymentStart', 'CreatePrepaymentWizard']
__metaclass__ = PoolMeta
class CreatePrepaymentStart(ModelView):
'Create Prepayment Start'
__name__ = 'purchase.create_prepayment.start'
purchase_amount = fields.Numeric(
'Purchase amount',
digits=(16, 2),
readonly=True,
help="Purchase amount"
)
percentage = fields.Numeric(
'Percentage',
digits=(16, 2),
readonly=False,
help="Percentage of the purchase amount to prepay."
)
amount = fields.Numeric(
'Amount',
digits=(16, 2),
required=True,
help="Prepayment amount, calculated but editable."
)
@fields.depends('percentage','purchase_amount')
def on_change_with_amount(self):
if self.percentage and self.percentage > 0:
return round(self.purchase_amount * self.percentage / 100,2)
class PrepaymentMessage(ModelView):
'Prepayment Created Message'
__name__ = 'purchase.create_prepayment.message'
message = fields.Char('Message', readonly=True)
invoice = fields.Many2One('account.invoice', 'Invoice')
class CreatePrepaymentWizard(Wizard):
'Create Prepayment Wizard'
__name__ = 'purchase.create_prepayment'
start = StateView(
'purchase.create_prepayment.start',
'purchase_trade.create_prepayment_start_form',
[
Button('Cancel', 'end', 'tryton-cancel'),
Button('Create', 'create_invoice', 'tryton-ok', default=True),
]
)
create_invoice = StateTransition()
invoice_id = None
message = StateView(
'purchase.create_prepayment.message',
'purchase_trade.create_prepayment_message_form',
[
Button('OK', 'end', 'tryton-ok'),
Button('See Prepayment', 'see_invoice', 'tryton-go-next'),
]
)
see_invoice = StateAction('account_invoice.act_invoice_form')
def default_start(self, fields):
Purchase = Pool().get('purchase.purchase')
context = Transaction().context
ids = context.get('active_ids')
percentage = Decimal(0)
amount = Decimal(0)
purchase_amount = Decimal(0)
for i in ids:
purchase = Purchase(i)
pt = purchase.payment_term.name
if 'ADV' in pt:
before = pt.split("ADV")[0].strip()
percentage = Decimal(int(before.split("%")[0].strip()))
if purchase.lines:
line = purchase.lines[0]
purchase_amount = line.amount
amount = purchase_amount * percentage / Decimal(100)
return {
'percentage': percentage,
'purchase_amount': purchase_amount,
'amount': amount,
}
def transition_create_invoice(self):
pool = Pool()
Invoice = pool.get('account.invoice')
InvoiceLine = pool.get('account.invoice.line')
Product = pool.get('product.product')
Purchase = pool.get('purchase.purchase')
purchase = Purchase(Transaction().context['active_id'])
amount = self.start.amount
# Trouver le produit "Prepayment"
prepayment_product = Product.search([('code', '=', 'Prepayment')])
if prepayment_product:
prepayment_product = prepayment_product[0]
invoice = Invoice(
company=purchase.company,
type='in',
party=purchase.party,
invoice_address=purchase.invoice_address,
currency=purchase.currency,
account=purchase.party.account_payable_used,
payment_term=purchase.payment_term,
description="Prepayment"
)
invoice_line = InvoiceLine(
product=prepayment_product,
description='Prepayment for %s' % purchase.rec_name,
quantity=1,
unit=prepayment_product.default_uom,
unit_price=amount,
account=prepayment_product.account_stock_used,
origin=purchase.lines[0] if purchase.lines else purchase
)
invoice.lines = [invoice_line]
invoice.set_journal()
invoice.save()
self.message.invoice = invoice
return 'message'
def default_message(self, fields):
return {
'message': 'The prepayment invoice has been successfully created.',
}
def do_see_invoice(self, action):
action['views'].reverse() # pour ouvrir en form directement
logger.info("*************SEE_INVOICE******************:%s",self.message.invoice)
return action, {'res_id':self.message.invoice.id}

View File

@@ -0,0 +1,29 @@
<?xml version="1.0"?>
<tryton>
<data>
<!-- Wizard Action -->
<record model="ir.action.wizard" id="act_purchase_create_prepayment">
<field name="name">Create Prepayment</field>
<field name="wiz_name">purchase.create_prepayment</field>
</record>
<!-- Attacher au menu/formulaire Purchase -->
<record model="ir.action.keyword" id="act_purchase_create_prepayment_keyword">
<field name="keyword">form_action</field>
<field name="model">purchase.purchase,-1</field>
<field name="action" ref="act_purchase_create_prepayment"/>
</record>
<!-- View for Wizard -->
<record model="ir.ui.view" id="create_prepayment_start_form">
<field name="model">purchase.create_prepayment.start</field>
<field name="type">form</field>
<field name="name">create_prepayment_start_form</field>
</record>
<record model="ir.ui.view" id="create_prepayment_message_form">
<field name="model">purchase.create_prepayment.message</field>
<field name="type">form</field>
<field name="name">create_prepayment_message_form</field>
</record>
</data>
</tryton>

898
modules/purchase_trade/sale.py Executable file
View File

@@ -0,0 +1,898 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.model import fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id, If, PYSONEncoder
from trytond.model import (ModelSQL, ModelView)
from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction
from trytond.transaction import Transaction, inactive_records
from decimal import getcontext, Decimal, ROUND_HALF_UP
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
from sql.conditionals import Case
from sql import Column, Literal
from sql.functions import CurrentTimestamp, DateTrunc
import datetime
import logging
import json
from trytond.exceptions import UserWarning, UserError
logger = logging.getLogger(__name__)
VALTYPE = [
('priced', 'Price'),
('fee', 'Fee'),
('market', 'Market'),
('derivative', 'Derivative'),
]
class ContractDocumentType(metaclass=PoolMeta):
"Contract - Document Type"
__name__ = 'contract.document.type'
# lc_out = fields.Many2One('lc.letter.outgoing', 'LC out')
# lc_in = fields.Many2One('lc.letter.incoming', 'LC in')
sale = fields.Many2One('sale.sale', "Sale")
class Estimated(metaclass=PoolMeta):
"Estimated date"
__name__ = 'pricing.estimated'
sale_line = fields.Many2One('sale.line',"Line")
class FeeLots(metaclass=PoolMeta):
"Fee lots"
__name__ = 'fee.lots'
sale_line = fields.Many2One('sale.line',"Line")
class OpenPosition(metaclass=PoolMeta):
"Open position"
__name__ = 'open.position'
sale = fields.Many2One('sale.sale',"Sale")
sale_line = fields.Many2One('sale.line',"Sale Line")
client = fields.Many2One('party.party',"Client")
class Mtm(metaclass=PoolMeta):
"Mtm"
__name__ = 'mtm.component'
sale_line = fields.Many2One('sale.line',"Line")
class Component(metaclass=PoolMeta):
"Component"
__name__ = 'pricing.component'
sale_line = fields.Many2One('sale.line',"Line")
quota_sale = fields.Function(fields.Numeric("Quota",digits='unit'),'get_quota_sale')
unit_sale = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit_sale')
def getDelMonthDateSale(self):
PM = Pool().get('product.month')
if self.sale_line and hasattr(self.sale_line, 'del_period') and self.sale_line.del_period:
pm = PM(self.sale_line.del_period)
if pm:
logger.info("DELMONTHDATE:%s",pm.beg_date)
return pm.beg_date
def getEstimatedTriggerSale(self,t):
logger.info("GETTRIGGER:%s",t)
if t == 'delmonth':
return self.getDelMonthDateSale()
PE = Pool().get('pricing.estimated')
Date = Pool().get('ir.date')
pe = PE.search([('sale_line','=',self.sale_line),('trigger','=',t)])
if pe:
return pe[0].estimated_date
else:
return Date.today()
def get_unit_sale(self, name):
if self.sale_line:
return self.sale_line.unit
def get_quota_sale(self, name):
if self.sale_line:
if self.sale_line.quantity:
return round(self.sale_line.quantity_theorical / (self.nbdays if self.nbdays > 0 else 1),4)
class Pricing(metaclass=PoolMeta):
"Pricing"
__name__ = 'pricing.pricing'
sale_line = fields.Many2One('sale.line',"Lines")
unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit_sale')
def get_unit_sale(self,name):
if self.sale_line:
return self.sale_line.unit
def get_eod_price_sale(self):
if self.sale_line:
return round((self.fixed_qt * self.fixed_qt_price + self.unfixed_qt * self.unfixed_qt_price)/Decimal(self.sale_line.quantity),4)
return Decimal(0)
class Summary(ModelSQL,ModelView):
"Pricing summary"
__name__ = 'sale.pricing.summary'
sale_line = fields.Many2One('sale.line',"Lines")
price_component = fields.Many2One('pricing.component',"Component")
quantity = fields.Numeric("Qt",digits=(1,4))
fixed_qt = fields.Numeric("Fixed qt",digits=(1,4))
unfixed_qt = fields.Numeric("Unfixed qt",digits=(1,4))
price = fields.Numeric("Price",digits=(1,4))
progress = fields.Float("Fix. progress")
ratio = fields.Numeric("Ratio")
def get_name(self):
if self.price_component:
return self.price_component.get_rec_name()
return ""
def get_last_price(self):
Date = Pool().get('ir.date')
if self.price_component:
pc = Pool().get('pricing.component')(self.price_component)
if pc.price_index:
PI = Pool().get('price.price')
pi = PI(pc.price_index)
return pi.get_price(Date.today(),self.sale_line.unit,self.sale_line.sale.currency,True)
@classmethod
def table_query(cls):
SalePricing = Pool().get('pricing.pricing')
sp = SalePricing.__table__()
SaleComponent = Pool().get('pricing.component')
sc = SaleComponent.__table__()
#wh = Literal(True)
context = Transaction().context
group_pnl = context.get('group_pnl')
if group_pnl:
return None
query = sp.join(sc,'LEFT',condition=sp.price_component == sc.id).select(
Literal(0).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
Literal(None).as_('write_uid'),
Literal(None).as_('write_date'),
Max(sp.id).as_('id'),
sp.sale_line.as_('sale_line'),
sp.price_component.as_('price_component'),
Max(sp.fixed_qt+sp.unfixed_qt).as_('quantity'),
Max(sp.fixed_qt).as_('fixed_qt'),
(Min(sp.unfixed_qt)).as_('unfixed_qt'),
Max(Case((sp.last, sp.eod_price),else_=0)).as_('price'),
(Max(sp.fixed_qt)/Max(sp.fixed_qt+sp.unfixed_qt)).as_('progress'),
Max(sc.ratio).as_('ratio'),
#where=wh,
group_by=[sp.sale_line,sp.price_component])
return query
class Lot(metaclass=PoolMeta):
__name__ = 'lot.lot'
sale_line = fields.Many2One('sale.line',"Sale")
lot_quantity_sale = fields.Function(fields.Numeric("Net weight",digits='lot_unit'),'get_qt')
lot_gross_quantity_sale = fields.Function(fields.Numeric("Gross weight",digits='lot_unit'),'get_gross_qt')
def get_qt(self, name):
quantity = self.lot_quantity
if self.lot_hist:
for h in self.lot_hist:
if h.quantity_type.id == 3:
quantity = h.quantity
return quantity
def get_gross_qt(self, name):
quantity = self.lot_quantity
if self.lot_hist:
for h in self.lot_hist:
if h.quantity_type.id == 3:
quantity = h.quantity
return quantity
def getClient(self):
if self.sale_line:
return Pool().get('sale.sale')(self.sale_line.sale).party.id
def getSale(self):
if self.sale_line:
return self.sale_line.sale.id
class Sale(metaclass=PoolMeta):
__name__ = 'sale.sale'
from_location = fields.Many2One('stock.location', 'From location',domain=[('type', "!=", 'customer')])
to_location = fields.Many2One('stock.location', 'To location',domain=[('type', "!=", 'supplier')])
shipment_out = fields.Many2One('stock.shipment.out','Sales')
pnl = fields.One2Many('valuation.valuation', 'sale', 'Pnl')
derivatives = fields.One2Many('derivative.derivative', 'sale', 'Derivative')
#plans = fields.One2Many('workflow.plan','sale',"Execution plans")
forex = fields.One2Many('forex.cover.physical.sale','contract',"Forex",readonly=True)
broker = fields.Many2One('party.party',"Broker",domain=[('categories.parent', 'child_of', [4])])
tol_min = fields.Numeric("Tol - in %")
tol_max = fields.Numeric("Tol + in %")
# certification = fields.Selection([
# (None, ''),
# ('bci', 'BCI'),
# ],"Certification")
# weight_basis = fields.Selection([
# (None, ''),
# ('ncsw', 'NCSW'),
# ('nlw', 'NLW'),
# ], 'Weight basis')
certif = fields.Many2One('purchase.certification',"Certification")
wb = fields.Many2One('purchase.weight.basis',"Weight basis")
association = fields.Many2One('purchase.association',"Association")
crop = fields.Many2One('purchase.crop',"Crop")
viewer = fields.Function(fields.Text(""),'get_viewer')
doc_template = fields.Many2One('doc.template',"Template")
required_documents = fields.Many2Many(
'contract.document.type', 'sale', 'doc_type', 'Required Documents')
@classmethod
def default_viewer(cls):
country_start = "Zobiland"
data = {
"highlightedCountryName": country_start
}
return "d3:" + json.dumps(data)
@fields.depends('doc_template','required_documents')
def on_change_with_required_documents(self):
if self.doc_template:
return self.doc_template.type
def get_viewer(self, name=None):
country_start = ''
dep_name = ''
arr_name = ''
departure = ''
arrival = ''
if self.party and self.party.addresses:
if self.party.addresses[0].country:
country_start = self.party.addresses[0].country.name
if self.from_location:
lat_from = self.from_location.lat
lon_from = self.from_location.lon
dep_name = self.from_location.name
departure = { "name":dep_name,"lat": str(lat_from), "lon": str(lon_from) }
if self.to_location:
lat_to = self.to_location.lat
lon_to = self.to_location.lon
arr_name = self.to_location.name
arrival = { "name":arr_name,"lat": str(lat_to), "lon": str(lon_to) }
data = {
"highlightedCountryNames": [{"name":country_start}],
"routePoints": [
{ "lon": -46.3, "lat": -23.9 },
{ "lon": -30.0, "lat": -20.0 },
{ "lon": -30.0, "lat": 0.0 },
{ "lon": -6.0, "lat": 35.9 },
{ "lon": 15.0, "lat": 38.0 },
{ "lon": 29.0, "lat": 41.0 }
],
"boats": [
# {"name": "CARIBBEAN 1",
# "imo": "1234567",
# "lon": -30.0,
# "lat": 0.0,
# "status": "En route",
# "links": [
# { "text": "Voir sur VesselFinder", "url": "https://www.vesselfinder.com" },
# { "text": "Détails techniques", "url": "https://example.com/tech" }
# ],
# "actions": [
# { "type": "track", "id": "123", "label": "Suivre ce bateau" },
# { "type": "details", "id": "123", "label": "Voir détails" }
# ]}
],
"cottonStocks": [
# { "name":"Mali","lat": 12.65, "lon": -8.0, "amount": 300 },
# { "name":"Egypte","lat": 30.05, "lon": 31.25, "amount": 500 },
# { "name":"Irak","lat": 33.0, "lon": 44.0, "amount": 150 }
],
"departures": [departure],
"arrivals": [arrival]
}
return "d3:" + json.dumps(data)
@fields.depends('party','from_location','to_location')
def on_change_with_viewer(self):
return self.get_viewer()
def getLots(self):
if self.lines:
if self.lines.lots:
return [l for l in self.lines.lots]
@classmethod
def validate(cls, sales):
super(Sale, cls).validate(sales)
Line = Pool().get('sale.line')
Date = Pool().get('ir.date')
for sale in sales:
for line in sale.lines:
if not line.quantity_theorical and line.quantity > 0:
line.quantity_theorical = line.quantity
Line.save([line])
if line.lots:
line_p = line.lots[0].line
if line_p:
#compute pnl
Pnl = Pool().get('valuation.valuation')
pnl = Pnl.search([('line','=',line_p.id)])
if pnl:
Pnl.delete(pnl)
pnl_lines = []
pnl_lines.extend(line_p.get_pnl_fee_lines())
pnl_lines.extend(line_p.get_pnl_price_lines())
pnl_lines.extend(line_p.get_pnl_der_lines())
Pnl.save(pnl_lines)
if line.quantity_theorical:
OpenPosition = Pool().get('open.position')
OpenPosition.create_from_sale_line(line)
if line.price_type == 'basis' and line.lots: #line.price_pricing and line.price_components and
unit_price = line.get_basis_price()
if unit_price != line.unit_price:
Line = Pool().get('sale.line')
line.unit_price = unit_price
Line.save([line])
if line.price_type == 'efp':
if line.derivatives:
for d in line.derivatives:
line.unit_price = d.price_index.get_price(Date.today(),line.unit,line.currency,True)
Line.save([line])
class SaleLine(metaclass=PoolMeta):
__name__ = 'sale.line'
del_period = fields.Many2One('product.month',"Delivery Period")
lots = fields.One2Many('lot.lot','sale_line',"Lots",readonly=True)
fees = fields.One2Many('fee.fee', 'sale_line', 'Fees')
quantity_theorical = fields.Numeric("Th. quantity", digits='unit', readonly=True)
premium = fields.Numeric("Premium/Discount",digits='unit')
price_type = fields.Selection([
('cash', 'Cash Price'),
('priced', 'Priced'),
('basis', 'Basis'),
('efp', 'EFP'),
], 'Price type')
progress = fields.Function(fields.Float("Fix. progress",
states={
'invisible': Eval('price_type') != 'basis',
}),'get_progress')
from_del = fields.Date("From")
to_del = fields.Date("To")
price_components = fields.One2Many('pricing.component','sale_line',"Components")
mtm = fields.One2Many('mtm.component','sale_line',"Mtm")
derivatives = fields.One2Many('derivative.derivative','sale_line',"Derivatives")
price_pricing = fields.One2Many('pricing.pricing','sale_line',"Pricing")
price_summary = fields.One2Many('sale.pricing.summary','sale_line',"Summary")
estimated_date = fields.One2Many('pricing.estimated','sale_line',"Estimated date")
tol_min = fields.Numeric("Tol - in %",states={
'readonly': (Eval('inherit_tol')),
})
tol_max = fields.Numeric("Tol + in %",states={
'readonly': (Eval('inherit_tol')),
})
inherit_tol = fields.Boolean("Inherit tolerance")
tol_min_v = fields.Function(fields.Numeric("Qt min"),'get_tol_min')
tol_max_v = fields.Function(fields.Numeric("Qt max"),'get_tol_max')
certif = fields.Many2One('purchase.certification',"Certification",states={'readonly': (Eval('inherit_cer')),})
# certification = fields.Selection([
# (None, ''),
# ('bci', 'BCI'),
# ],"Certification",states={'readonly': (Eval('inherit_cer')),})
inherit_cer = fields.Boolean("Inherit certification")
enable_linked_currency = fields.Boolean("Linked currencies")
linked_price = fields.Numeric("Price", digits='unit',states={
'invisible': (~Eval('enable_linked_currency')),
})
linked_currency = fields.Many2One('currency.linked',"Currency",states={
'invisible': (~Eval('enable_linked_currency')),
})
linked_unit = fields.Many2One('product.uom', 'Unit',states={
'invisible': (~Eval('enable_linked_currency')),
})
premium = fields.Numeric("Premium/Discount",digits='unit')
fee_ = fields.Many2One('fee.fee',"Fee")
@classmethod
def default_price_type(cls):
return 'priced'
@classmethod
def default_inherit_tol(cls):
return True
@classmethod
def default_enable_linked_currency(cls):
return False
@classmethod
def default_inherit_cer(cls):
return True
# @fields.depends('quantity')
# def on_change_with_quantity_theorical(self):
# if not self.quantity_theorical:
# return self.quantity
def get_tol_min(self,name):
if self.inherit_tol:
if self.sale.tol_min and self.quantity_theorical:
return round((1-(self.sale.tol_min/100))*Decimal(self.quantity_theorical),3)
else:
if self.tol_min and self.quantity_theorical:
return round((1-(self.tol_min/100))*Decimal(self.quantity_theorical),3)
def get_tol_max(self,name):
if self.inherit_tol:
if self.sale.tol_max and self.quantity_theorical:
return round((1+(self.sale.tol_max/100))*Decimal(self.quantity_theorical),3)
else:
if self.tol_max and self.quantity_theorical:
return round((1+(self.tol_max/100))*Decimal(self.quantity_theorical),3)
def get_progress(self,name):
PS = Pool().get('sale.pricing.summary')
ps = PS.search(['sale_line','=',self.id])
if ps:
return sum((e.progress if e.progress else 0) * (e.ratio if e.ratio else 0) / 100 for e in ps)
def getVirtualLot(self):
if self.lots:
return [l for l in self.lots if l.lot_type=='virtual'][0]
def get_price(self,lot_premium=0):
return (self.unit_price + Decimal(lot_premium) + (self.premium if self.premium else Decimal(0))) if self.unit_price else Decimal(0)
def get_basis_price(self):
price = Decimal(0)
for pc in self.price_components:
PP = Pool().get('sale.pricing.summary')
pp = PP.search([('price_component','=',pc.id),('sale_line','=',self.id)])
if pp:
price += pp[0].price * (pc.ratio / 100)
return round(price,4)
def get_price_linked_currency(self,lot_premium=0):
if self.linked_unit:
Uom = Pool().get('product.uom')
qt = Uom.compute_qty(self.unit, float(1), self.linked_unit)
return round(((self.linked_price + Decimal(lot_premium) + (self.premium if self.premium else Decimal(0))) * self.linked_currency.factor) * Decimal(qt), 4)
else:
return round((self.linked_price + Decimal(lot_premium) + (self.premium if self.premium else Decimal(0))) * self.linked_currency.factor, 4)
@fields.depends('id','unit','quantity','unit_price','price_pricing','price_type','price_components','premium','estimated_date','lots','fees','enable_linked_currency','linked_price','linked_currency','linked_unit')
def on_change_with_unit_price(self, name=None):
Date = Pool().get('ir.date')
logger.info("ONCHANGEUNITPRICE:%s",self.unit_price)
if self.price_type == 'basis' and self.lots: #self.price_pricing and self.price_components and
logger.info("ONCHANGEUNITPRICE_IN:%s",self.get_basis_price())
return self.get_basis_price()
if self.enable_linked_currency and self.linked_price and self.linked_currency and self.price_type == 'priced':
return self.get_price_linked_currency()
if self.price_type == 'efp':
if hasattr(self, 'derivatives') and self.derivatives:
for d in self.derivatives:
return d.price_index.get_price(Date.today(),self.unit,self.sale.currency,True)
return self.get_price()
def check_from_to(self,tr):
if tr.pricing_period:
date_from,date_to, d, include, dates = tr.getDateWithEstTrigger(1)
if date_from:
tr.from_p = date_from.date()
if date_to:
tr.to_p = date_to.date()
if tr.application_period:
date_from,date_to, d, include, dates = tr.getDateWithEstTrigger(2)
if date_from:
tr.from_a = date_from.date()
if date_to:
tr.to_a = date_to.date()
TR = Pool().get('pricing.trigger')
TR.save([tr])
def check_pricing(self):
if self.price_components:
for pc in self.price_components:
if not pc.auto:
Pricing = Pool().get('pricing.pricing')
pricings = Pricing.search(['price_component','=',pc.id],order=[('pricing_date', 'ASC')])
if pricings:
cumul_qt = Decimal(0)
index = 0
for pr in pricings:
cumul_qt += pr.quantity
pr.fixed_qt = cumul_qt
pr.fixed_qt_price = pr.get_fixed_price()
pr.unfixed_qt = Decimal(pr.sale_line.quantity_theorical) - pr.fixed_qt
pr.unfixed_qt_price = pr.fixed_qt_price
pr.eod_price = pr.get_eod_price_sale()
if index == len(pricings) - 1:
pr.last = True
index += 1
Pricing.save([pr])
if pc.triggers and pc.auto:
prDate = []
prPrice = []
apDate = []
apPrice = []
for t in pc.triggers:
prD, prP = t.getPricingListDates(pc.calendar)
apD, apP = t.getApplicationListDates(pc.calendar)
prDate.extend(prD)
prPrice.extend(prP)
apDate.extend(apD)
apPrice.extend(apP)
if pc.quota_sale:
prPrice = self.get_avg(prPrice)
self.generate_pricing(pc,apDate,prPrice)
def get_avg(self,lprice):
l = len(lprice)
if l > 0 :
cumulprice = float(0)
i = 1
for p in lprice:
if i > 1:
p['avg_minus_1'] = cumulprice / (i-1)
cumulprice += p['price']
p['avg'] = cumulprice / i
i += 1
return lprice
def getnearprice(self,pl,d,t,max_date=None):
if pl:
pl_sorted = sorted(pl, key=lambda x: x['date'])
pminus = pl_sorted[0]
if not max_date:
max_date = d.date()
for p in pl_sorted:
if p['date'].date() == d.date():
if p['isAvg'] and t == 'avg':
return p[t]
if not p['isAvg'] and t == 'avg':
return p['price']
elif p['date'].date() > d.date():
if pminus != p:
return pminus[t]
else:
return Decimal(0)
pminus = p
return pl_sorted[len(pl)-1][t]
return Decimal(0)
def generate_pricing(self,pc,dl,pl):
Pricing = Pool().get('pricing.pricing')
pricing = Pricing.search(['price_component','=',pc.id])
if pricing:
Pricing.delete(pricing)
cumul_qt = 0
index = 0
dl_sorted = sorted(dl)
for d in dl_sorted:
if pc.pricing_date and d.date() > pc.pricing_date:
break
p = Pricing()
p.sale_line = self.id
logger.info("GENEDATE:%s",d)
logger.info("TYPEDATE:%s",type(d))
p.pricing_date = d.date()
p.price_component = pc.id
p.quantity = round(Decimal(pc.quota_sale),4)
price = round(Decimal(self.getnearprice(pl,d,'price',pc.pricing_date)),4)
p.settl_price = price
if price > 0:
cumul_qt += pc.quota_sale
p.fixed_qt = round(Decimal(cumul_qt),4)
p.fixed_qt_price = round(Decimal(self.getnearprice(pl,d,'avg',pc.pricing_date)),4)
#p.fixed_qt_price = p.get_fixed_price()
if p.fixed_qt_price == 0:
p.fixed_qt_price = round(Decimal(self.getnearprice(pl,d,'avg_minus_1',pc.pricing_date)),4)
p.unfixed_qt = round(Decimal(self.quantity_theorical) - Decimal(cumul_qt),4)
if p.unfixed_qt < 0.001:
p.unfixed_qt = Decimal(0)
p.fixed_qt = Decimal(self.quantity_theorical)
if price > 0:
logger.info("GENERATE_1:%s",price)
p.unfixed_qt_price = price
else:
pr = Decimal(pc.price_index.get_price(p.pricing_date,self.unit,self.sale.currency,True))
pr = round(pr,4)
logger.info("GENERATE_2:%s",pr)
p.unfixed_qt_price = pr
p.eod_price = p.get_eod_price_sale()
if (index == len(dl)-1) or (pc.pricing_date and (index < len(dl)-1 and dl_sorted[index+1].date() > pc.pricing_date)):
p.last = True
logger.info("GENERATE_3:%s",p.unfixed_qt_price)
Pricing.save([p])
index += 1
# @classmethod
# def write(cls, records, values):
# if 'quantity' in values:
# for record in records:
# old_qt = record.quantity
# new_qt = values['quantity']
# logger.info("WRITE_OLD_QT:%s",old_qt)
# logger.info("WRITE_NEW_QT:%s",new_qt)
# if old_qt != new_qt:
# LotQt = Pool().get('lot.qt')
# lqts = LotQt.search(['lot_s','=',record.lots[0]])
# if len(lqts)>1:
# raise UserError("You cannot changed quantity with open quantities defined !")
# return
# elif len(lqts)==1:
# if lqts[0].lot_p or lqts[0].lot_shipment_origin:
# raise UserError("You cannot changed quantity with open quantities defined !")
# return
# lqts[0].lot_quantity = new_qt
# LotQt.save(lqts)
# super().write(records, values)
@classmethod
def delete(cls, lines):
pool = Pool()
LotQt = pool.get('lot.qt')
Valuation = pool.get('valuation.valuation')
OpenPosition = pool.get('open.position')
for line in lines:
if line.lots:
vlot_s = line.lots[0].getVlot_s()
lqts = LotQt.search([('lot_s','=',vlot_s.id),('lot_p','!=',None),('lot_quantity','>',0)])
if lqts:
raise UserError("You cannot delete matched sale")
return
lqts = LotQt.search([('lot_s','=',vlot_s.id)])
LotQt.delete(lqts)
valuations = Valuation.search([('lot','in',line.lots)])
if valuations:
Valuation.delete(valuations)
op = OpenPosition.search(['sale_line','=',line.id])
if op:
OpenPosition.delete(op)
super(SaleLine, cls).delete(lines)
@classmethod
def copy(cls, lines, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('lots', None)
default.setdefault('quantity', Decimal(0))
default.setdefault('quantity_theorical', None)
default.setdefault('price_pricing', None)
return super().copy(lines, default=default)
@classmethod
def validate(cls, salelines):
LotQtHist = Pool().get('lot.qt.hist')
LotQtType = Pool().get('lot.qt.type')
super(SaleLine, cls).validate(salelines)
for line in salelines:
if line.price_components:
for pc in line.price_components:
if pc.triggers:
for tr in pc.triggers:
line.check_from_to(tr)
line.check_pricing()
#no lot need to create one with line quantity
logger.info("FROM_VALIDATE_LINE:%s",line.created_by_code)
if not line.created_by_code:
if not line.lots and line.product.type != 'service' and line.quantity != Decimal(0):
Lot = Pool().get('lot.lot')
lot = Lot()
lot.sale_line = line.id
lot.lot_qt = line.quantity
lot.lot_unit_line = line.unit
lot.lot_quantity = line.quantity
lot.lot_status = 'forecast'
lot.lot_type = 'virtual'
lot.lot_product = line.product
lqtt = LotQtType.search([('sequence','=',1)])
if lqtt:
lqh = LotQtHist()
lqh.quantity_type = lqtt[0]
lqh.quantity = lot.lot_quantity
lqh.gross_quantity = lot.lot_quantity
lot.lot_hist = [lqh]
if line.quantity > 0:
Lot.save([lot])
#check if fees need to be updated
if line.fees:
for fee in line.fees:
fl_check = FeeLots.search([('fee','=',fee.id),('lot','=',lot.id),('sale_line','=',line.id)])
if not fl_check:
fl = FeeLots()
fl.fee = fee.id
fl.lot = lot.id
fl.sale_line = line.id
FeeLots.save([fl])
#generate valuation for purchase and sale
LotQt = Pool().get('lot.qt')
if line.lots:
for lot in line.lots:
lqts = LotQt.search([('lot_s','=',lot.id),('lot_p','>',0)])
logger.info("VALIDATE_SL:%s",lqts)
if lqts:
purchase_lines = [e.lot_p.line for e in lqts]
if purchase_lines:
for pl in purchase_lines:
Pnl = Pool().get('valuation.valuation')
pnl = Pnl.search([('line','=',pl.id)])
if pnl:
Pnl.delete(pnl)
pnl_lines = []
pnl_lines.extend(pl.get_pnl_fee_lines())
pnl_lines.extend(pl.get_pnl_price_lines())
pnl_lines.extend(pl.get_pnl_der_lines())
Pnl.save(pnl_lines)
class SaleCreatePurchase(Wizard):
"Create mirror purchase"
__name__ = "sale.create.mirror"
start = StateTransition()
purchase = StateView(
'sale.create.input',
'purchase_trade.create_purchase_view_form', [
Button("Cancel", 'end', 'tryton-cancel'),
Button("Create", 'creating', 'tryton-ok', default=True),
])
creating = StateTransition()
def transition_start(self):
return 'purchase'
def transition_creating(self):
Purchase = Pool().get('purchase.purchase')
PL = Pool().get('purchase.line')
LotQt = Pool().get('lot.qt')
p = None
pl = None
for r in self.records:
if r.lines:
p = Purchase()
p.party = self.purchase.party
p.incoterm = self.purchase.incoterm
p.payment_term = self.purchase.payment_term
p.from_location = self.purchase.from_location
p.to_location = self.purchase.to_location
Purchase.save([p])
pl = PL()
pl.quantity = r.lines[0].quantity
pl.unit = r.lines[0].unit
pl.product = r.lines[0].product
pl.unit_price = self.purchase.unit_price
pl.currency = self.purchase.currency
pl.purchase = p.id
PL.save([pl])
#Match if requested
if self.purchase.match:
#Increase forecasted virtual part matched
if pl:
if pl.lots:
qt = Decimal(pl.quantity)
vlot_p = pl.getVirtualLot()
vlot_s = self.records[0].lines[0].getVirtualLot()
if not vlot_p.updateVirtualPart(None,qt,vlot_p,None,vlot_s):
vlot_p.createVirtualPart(qt,pl.unit,vlot_p,None,vlot_s)
#Decrease forecasted virtual part non matched
lqts = LotQt.search([('lot_p','=',vlot_p)])
if lqts:
vlot_p.updateVirtualPart(lqts[0],-qt)
lqts = LotQt.search([('lot_s','=',vlot_s)])
if lqts:
vlot_p.updateVirtualPart(lqts[0],-qt)
return 'end'
def end(self):
return 'reload'
class SaleCreatePurchaseInput(ModelView):
"Create purchase mirror"
__name__ = "sale.create.input"
party = fields.Many2One('party.party',"Supplier")
incoterm = fields.Many2One('incoterm.incoterm',"Incoterm",domain=[('location', '=', False)])
payment_term = fields.Many2One('account.invoice.payment_term', "Payment Term")
from_location = fields.Many2One('stock.location',"From location")
to_location = fields.Many2One('stock.location',"To location")
unit_price = fields.Numeric("Price")
currency = fields.Many2One('currency.currency',"Currency")
match = fields.Boolean("Match open quantity")
class Derivative(metaclass=PoolMeta):
"Derivative"
__name__ = 'derivative.derivative'
sale = fields.Many2One('sale.sale',"Sale")
sale_line = fields.Many2One('sale.line',"Line")
class Valuation(metaclass=PoolMeta):
"Valuation"
__name__ = 'valuation.valuation'
sale = fields.Many2One('sale.sale',"Sale")
sale_line = fields.Many2One('sale.line',"Line")
class ValuationDyn(metaclass=PoolMeta):
"Valuation"
__name__ = 'valuation.valuation.dyn'
r_sale = fields.Many2One('sale.sale',"Sale")
r_sale_line = fields.Many2One('sale.line',"Line")
@classmethod
def table_query(cls):
Valuation = Pool().get('valuation.valuation')
val = Valuation.__table__()
context = Transaction().context
group_pnl = context.get('group_pnl')
wh = (val.id > 0)
query = val.select(
Literal(0).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
Literal(None).as_('write_uid'),
Literal(None).as_('write_date'),
Max(val.id).as_('id'),
Max(val.purchase).as_('r_purchase'),
Max(val.sale).as_('r_sale'),
Max(val.line).as_('r_line'),
Max(val.date).as_('r_date'),
val.type.as_('r_type'),
Max(val.reference).as_('r_reference'),
val.counterparty.as_('r_counterparty'),
Max(val.product).as_('r_product'),
val.state.as_('r_state'),
Avg(val.price).as_('r_price'),
Max(val.currency).as_('r_currency'),
Sum(val.quantity).as_('r_quantity'),
Max(val.unit).as_('r_unit'),
Sum(val.amount).as_('r_amount'),
Sum(val.mtm).as_('r_mtm'),
Max(val.lot).as_('r_lot'),
where=wh,
group_by=[val.purchase,val.sale])
return query
class Fee(metaclass=PoolMeta):
"Fee"
__name__ = 'fee.fee'
sale_line = fields.Many2One('sale.line',"Line")
class SaleAllocationsWizard(Wizard):
'Open Allocations report from Sale without modal'
__name__ = 'sale.allocations.wizard'
start_state = 'open_report'
open_report = StateAction('purchase_trade.act_lot_report_form')
def do_open_report(self, action):
sale_id = Transaction().context.get('active_id')
if not sale_id:
raise ValueError("No active sale ID in context")
action['context_model'] = 'lot.context'
action['pyson_context'] = PYSONEncoder().encode({
'sale': sale_id,
})
return action, {}

56
modules/purchase_trade/sale.xml Executable file
View File

@@ -0,0 +1,56 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="sale_view_form">
<field name="model">sale.sale</field>
<field name="inherit" ref="sale.sale_view_form"/>
<field name="name">sale_form</field>
</record>
<record model="ir.ui.view" id="sale_line_view_form">
<field name="model">sale.line</field>
<field name="inherit" ref="sale.sale_line_view_form"/>
<field name="name">sale_line_form</field>
</record>
<record model="ir.ui.view" id="sale_line_view_tree_sequence">
<field name="model">sale.line</field>
<field name="inherit" ref="sale.sale_line_view_tree_sequence"/>
<field name="name">sale_line_tree_sequence</field>
</record>
<record model="ir.ui.view" id="sale_lot_view_tree_sequence">
<field name="model">lot.lot</field>
<field name="inherit" ref="lot.lot_view_tree_sequence"/>
<field name="name">sale_lot_tree_sequence</field>
</record>
<record model="ir.ui.view" id="create_purchase_view_form">
<field name="model">sale.create.input</field>
<field name="type">form</field>
<field name="name">create_purchase_start_form</field>
</record>
<record model="ir.action.wizard" id="act_creating">
<field name="name">Create purchase mirror</field>
<field name="wiz_name">sale.create.mirror</field>
<field name="model">sale.sale</field>
</record>
<record model="ir.action.keyword" id="act_creating_keyword">
<field name="keyword">form_action</field>
<field name="model">sale.sale,-1</field>
<field name="action" ref="act_creating"/>
</record>
<record model="ir.action.wizard" id="act_sale_allocations_wizard">
<field name="name">Lots Management</field>
<field name="wiz_name">sale.allocations.wizard</field>
<field name="model">sale.sale</field>
</record>
<!-- Menu Relate dans Purchase -->
<record model="ir.action.keyword" id="act_sale_allocations_keyword">
<field name="keyword">form_relate</field>
<field name="model">sale.sale,-1</field>
<field name="action" ref="act_sale_allocations_wizard"/>
</record>
</data>
</tryton>

Some files were not shown because too many files have changed in this diff Show More