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