Duplicate custom

This commit is contained in:
2026-06-13 15:21:36 +02:00
parent c15b13cb91
commit e4e2087463
5 changed files with 329 additions and 38 deletions

View File

@@ -34,9 +34,10 @@ from . import (
dimension,
weight_report,
backtoback,
service,
invoice,
)
service,
invoice,
duplicate,
)
def register():
Pool.register(
@@ -91,17 +92,19 @@ def register():
ctrm_reporting.CTRMGLReconciliation,
ctrm_reporting.CTRMGLReconciliationContext,
configuration.Configuration,
pricing.ImportPricesStart,
pricing.ImportPricesResult,
module='purchase_trade', type_='model')
pricing.ImportPricesStart,
pricing.ImportPricesResult,
duplicate.TradeCustomDuplicateStart,
module='purchase_trade', type_='model')
Pool.register(
incoming.ImportSwift,
incoming.PrepareDocuments,
incoming.AnalyzeConditions,
lc.CreateLCWizard,
pricing.ImportPrices,
module='purchase_trade', type_='wizard'
)
incoming.ImportSwift,
incoming.PrepareDocuments,
incoming.AnalyzeConditions,
lc.CreateLCWizard,
pricing.ImportPrices,
duplicate.TradeCustomDuplicate,
module='purchase_trade', type_='wizard'
)
Pool.register(
credit_risk.Party,
credit_risk.CreditRiskRule,
@@ -321,11 +324,11 @@ def register():
purchase_prepayment.CreatePrepaymentWizard,
purchase.PurchaseAllocationsWizard,
purchase.InvoicePayment,
stock.ImportSoFWizard,
dashboard.BotWizard,
dashboard.DashboardLoader,
forex.ForexReport,
purchase.PnlReport,
stock.ImportSoFWizard,
dashboard.BotWizard,
dashboard.DashboardLoader,
forex.ForexReport,
purchase.PnlReport,
purchase.PositionReport,
valuation.ValuationProcess,
derivative.DerivativeMatchWizard,

View File

@@ -0,0 +1,227 @@
# 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 decimal import Decimal
from trytond.exceptions import UserError
from trytond.model import ModelView, fields
from trytond.pool import Pool
from trytond.pyson import Bool, Eval
from trytond.transaction import Transaction
from trytond.wizard import Button, StateTransition, StateView, Wizard
from trytond.modules.purchase_trade.service import ContractFactory
PRICE_TYPES = [
('cash', 'Cash Price'),
('priced', 'Priced'),
('basis', 'Basis'),
('efp', 'EFP'),
]
class TradeCustomDuplicateStart(ModelView):
"Custom Duplicate"
__name__ = 'trade.custom_duplicate.start'
source_model = fields.Selection([
('purchase.purchase', 'Purchase'),
('sale.sale', 'Sale'),
], "Source", readonly=True)
source = fields.Reference("Source Record", selection=[
('purchase.purchase', 'Purchase'),
('sale.sale', 'Sale'),
], readonly=True)
party = fields.Many2One('party.party', "Counterparty", required=True)
currency = fields.Many2One('currency.currency', "Currency", required=True)
payment_term = fields.Many2One(
'account.invoice.payment_term', "Payment Term", required=True)
incoterm = fields.Many2One('incoterm.incoterm', "Incoterm", required=True)
quantity = fields.Numeric("Contractual Quantity", digits='unit',
required=True)
unit = fields.Many2One('product.uom', "Unit", readonly=True)
unit_price = fields.Numeric("Price", digits=(16, 4), required=True)
price_type = fields.Selection(PRICE_TYPES, "Price Type", required=True)
clear_fees = fields.Boolean("Clear Fees")
clear_pricing_components = fields.Boolean("Clear Pricing Components")
create_matched_mirror = fields.Boolean("Create matched mirror contract")
mirror_party = fields.Many2One('party.party', "Mirror Counterparty",
states={
'invisible': ~Bool(Eval('create_matched_mirror')),
'required': Bool(Eval('create_matched_mirror')),
})
class TradeCustomDuplicate(Wizard):
"Custom Duplicate"
__name__ = 'trade.custom_duplicate'
start = StateView(
'trade.custom_duplicate.start',
'purchase_trade.trade_custom_duplicate_start_view_form', [
Button("Cancel", 'end', 'tryton-cancel'),
Button("Duplicate", 'duplicate', 'tryton-ok', default=True),
])
duplicate = StateTransition()
@staticmethod
def _trade_line(record):
lines = [
line for line in (record.lines or [])
if getattr(line, 'type', 'line') == 'line' and line.product]
if len(lines) > 1:
raise UserError(
"Custom duplicate only supports contracts with one trade line.")
return lines[0] if lines else None
@staticmethod
def _source_model(record):
name = record.__name__
if name not in {'purchase.purchase', 'sale.sale'}:
raise UserError("Custom duplicate only supports purchases and sales.")
return name
@staticmethod
def _opposite_type(source_model):
return 'Sale' if source_model == 'purchase.purchase' else 'Purchase'
def default_start(self, fields):
if len(self.records) != 1:
raise UserError("Please select a single purchase or sale.")
record = self.records[0]
source_model = self._source_model(record)
line = self._trade_line(record)
if not line:
raise UserError("The selected contract has no trade line to duplicate.")
return {
'source_model': source_model,
'source': '%s,%s' % (source_model, record.id),
'party': record.party.id if record.party else None,
'currency': record.currency.id if record.currency else None,
'payment_term': (
record.payment_term.id if record.payment_term else None),
'incoterm': record.incoterm.id if record.incoterm else None,
'quantity': (
getattr(line, 'quantity_theorical', None)
or getattr(line, 'quantity', None)),
'unit': line.unit.id if line.unit else None,
'unit_price': getattr(line, 'unit_price', None),
'price_type': getattr(line, 'price_type', None) or 'priced',
'clear_fees': False,
'clear_pricing_components': False,
'create_matched_mirror': False,
}
@staticmethod
def _copy_contract(record, options):
Model = Pool().get(record.__name__)
default = {
'party': options.party.id,
'currency': options.currency.id,
'payment_term': options.payment_term.id,
'incoterm': options.incoterm.id,
}
new_record, = Model.copy([record], default=default)
new_record = Model(new_record.id)
TradeCustomDuplicate._apply_party_addresses(new_record, options.party)
new_record.save()
return Model(new_record.id)
@staticmethod
def _apply_party_addresses(record, party):
addresses = list(getattr(party, 'addresses', None) or [])
address = addresses[0] if addresses else None
if hasattr(record, 'invoice_address'):
record.invoice_address = address
if hasattr(record, 'shipment_address'):
record.shipment_address = address
@staticmethod
def _apply_line_options(record, options):
line = TradeCustomDuplicate._trade_line(record)
if not line:
raise UserError("The duplicated contract has no trade line.")
line.quantity = options.quantity
line.quantity_theorical = options.quantity
line.unit_price = options.unit_price
line.price_type = options.price_type
if hasattr(line, 'currency'):
line.currency = options.currency
if options.clear_fees and hasattr(line, 'fees'):
line.fees = []
if (options.clear_pricing_components
and hasattr(line, 'price_components')):
line.price_components = []
line.save()
return line.__class__(line.id)
@staticmethod
def _first_virtual_lot(line):
for lot in line.lots or []:
if getattr(lot, 'lot_type', None) == 'virtual':
return lot
if hasattr(line, 'getVirtualLot'):
return line.getVirtualLot()
return None
@staticmethod
def _create_matched_mirror(record, line, options):
source_model = record.__name__
mirror_type = TradeCustomDuplicate._opposite_type(source_model)
source_lot = TradeCustomDuplicate._first_virtual_lot(line)
if not source_lot:
raise UserError(
"The duplicated line has no virtual lot to match.")
if not options.unit:
raise UserError(
"The duplicated line has no unit for mirror contract creation.")
ContractDetail = Pool().get('contract.detail')
ContractsStart = Pool().get('contracts.start')
from_location, to_location = ContractFactory._get_mirror_locations(
record, mirror_type)
detail = ContractDetail()
detail.party = options.mirror_party
detail.currency = options.currency
detail.currency_unit = "%s_%s" % (options.currency.id, options.unit.id)
detail.incoterm = options.incoterm
detail.payment_term = options.payment_term
detail.quantity = options.quantity
detail.unit = options.unit
detail.price = options.unit_price
detail.price_type = options.price_type
detail.reference = getattr(record, 'reference', None)
detail.from_location = from_location
detail.to_location = to_location
detail.tol_min = getattr(record, 'tol_min', None)
detail.tol_max = getattr(record, 'tol_max', None)
detail.crop = getattr(record, 'crop', None)
detail.del_period = getattr(record, 'del_period', None)
ct = ContractsStart()
ct.type = mirror_type
ct.matched = True
ct.product = line.product
ct.unit = line.unit
ct.quantity = options.quantity
ct.lot = source_lot
with Transaction().set_context(active_ids=[]):
ContractFactory.create_contracts([detail], type_=mirror_type, ct=ct)
def transition_duplicate(self):
record = self.records[0]
options = self.start
new_record = self._copy_contract(record, options)
new_line = self._apply_line_options(new_record, options)
if options.create_matched_mirror:
self._create_matched_mirror(new_record, new_line, options)
return 'end'
def end(self):
return 'reload'

View File

@@ -48,15 +48,30 @@ this repository contains the full copyright notices and license terms. -->
<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.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.ui.view" id="trade_custom_duplicate_start_view_form">
<field name="model">trade.custom_duplicate.start</field>
<field name="type">form</field>
<field name="name">trade_custom_duplicate_start_form</field>
</record>
<record model="ir.action.wizard" id="act_purchase_custom_duplicate">
<field name="name">Custom Duplicate</field>
<field name="wiz_name">trade.custom_duplicate</field>
<field name="model">purchase.purchase</field>
</record>
<record model="ir.action.keyword" id="act_purchase_custom_duplicate_keyword">
<field name="keyword">form_action</field>
<field name="model">purchase.purchase,-1</field>
<field name="action" ref="act_purchase_custom_duplicate"/>
</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>

View File

@@ -35,16 +35,26 @@ this repository contains the full copyright notices and license terms. -->
<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 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_custom_duplicate">
<field name="name">Custom Duplicate</field>
<field name="wiz_name">trade.custom_duplicate</field>
<field name="model">sale.sale</field>
</record>
<record model="ir.action.keyword" id="act_sale_custom_duplicate_keyword">
<field name="keyword">form_action</field>
<field name="model">sale.sale,-1</field>
<field name="action" ref="act_sale_custom_duplicate"/>
</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">
@@ -59,4 +69,4 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">sale_btb_form</field>
</record>
</data>
</tryton>
</tryton>

View File

@@ -0,0 +1,36 @@
<form col="4">
<label name="source_model"/>
<field name="source_model"/>
<label name="source"/>
<field name="source"/>
<newline/>
<label name="party"/>
<field name="party"/>
<label name="currency"/>
<field name="currency"/>
<newline/>
<label name="payment_term"/>
<field name="payment_term"/>
<label name="incoterm"/>
<field name="incoterm"/>
<newline/>
<label name="quantity"/>
<field name="quantity"/>
<label name="unit"/>
<field name="unit"/>
<newline/>
<label name="unit_price"/>
<field name="unit_price"/>
<label name="price_type"/>
<field name="price_type"/>
<newline/>
<label name="clear_fees"/>
<field name="clear_fees"/>
<label name="clear_pricing_components"/>
<field name="clear_pricing_components"/>
<newline/>
<label name="create_matched_mirror"/>
<field name="create_matched_mirror"/>
<label name="mirror_party"/>
<field name="mirror_party"/>
</form>