diff --git a/modules/purchase_trade/__init__.py b/modules/purchase_trade/__init__.py
index 483dc84..d46906b 100755
--- a/modules/purchase_trade/__init__.py
+++ b/modules/purchase_trade/__init__.py
@@ -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,
diff --git a/modules/purchase_trade/duplicate.py b/modules/purchase_trade/duplicate.py
new file mode 100644
index 0000000..9e445a7
--- /dev/null
+++ b/modules/purchase_trade/duplicate.py
@@ -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'
diff --git a/modules/purchase_trade/purchase.xml b/modules/purchase_trade/purchase.xml
index e2153fe..9b8b6eb 100755
--- a/modules/purchase_trade/purchase.xml
+++ b/modules/purchase_trade/purchase.xml
@@ -48,15 +48,30 @@ this repository contains the full copyright notices and license terms. -->
purchase.bi
purchase.purchase
-
- form_action
- purchase.purchase,-1
-
-
-
- Go to BI
- http://vps107.geneva.hosting:3000
-
+
+ form_action
+ purchase.purchase,-1
+
+
+
+ trade.custom_duplicate.start
+ form
+ trade_custom_duplicate_start_form
+
+
+ Custom Duplicate
+ trade.custom_duplicate
+ purchase.purchase
+
+
+ form_action
+ purchase.purchase,-1
+
+
+
+ Go to BI
+ http://vps107.geneva.hosting:3000
+
Lots Management
diff --git a/modules/purchase_trade/sale.xml b/modules/purchase_trade/sale.xml
index a164d93..029190a 100755
--- a/modules/purchase_trade/sale.xml
+++ b/modules/purchase_trade/sale.xml
@@ -35,16 +35,26 @@ this repository contains the full copyright notices and license terms. -->
sale.create.mirror
sale.sale
-
- form_action
- sale.sale,-1
-
-
-
-
- Lots Management
- sale.allocations.wizard
- sale.sale
+
+ form_action
+ sale.sale,-1
+
+
+
+ Custom Duplicate
+ trade.custom_duplicate
+ sale.sale
+
+
+ form_action
+ sale.sale,-1
+
+
+
+
+ Lots Management
+ sale.allocations.wizard
+ sale.sale
@@ -59,4 +69,4 @@ this repository contains the full copyright notices and license terms. -->
sale_btb_form
-
\ No newline at end of file
+
diff --git a/modules/purchase_trade/view/trade_custom_duplicate_start_form.xml b/modules/purchase_trade/view/trade_custom_duplicate_start_form.xml
new file mode 100644
index 0000000..0ffda33
--- /dev/null
+++ b/modules/purchase_trade/view/trade_custom_duplicate_start_form.xml
@@ -0,0 +1,36 @@
+