Duplicate custom
This commit is contained in:
@@ -170,6 +170,127 @@ class TradeCustomDuplicate(Wizard):
|
|||||||
return line.getVirtualLot()
|
return line.getVirtualLot()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _line_side(line):
|
||||||
|
if getattr(line, '__name__', None) == 'purchase.line':
|
||||||
|
return 'purchase'
|
||||||
|
if getattr(line, '__name__', None) == 'sale.line':
|
||||||
|
return 'sale'
|
||||||
|
if getattr(line, 'purchase', None):
|
||||||
|
return 'purchase'
|
||||||
|
if getattr(line, 'sale', None):
|
||||||
|
return 'sale'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _line_quantity(line):
|
||||||
|
quantity = (
|
||||||
|
getattr(line, 'quantity_theorical', None)
|
||||||
|
or getattr(line, 'quantity', None)
|
||||||
|
or Decimal(0))
|
||||||
|
return Decimal(str(quantity)).quantize(Decimal("0.00001"))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _ensure_open_virtual_lot(line):
|
||||||
|
product = getattr(line, 'product', None)
|
||||||
|
if not product or getattr(product, 'type', None) == 'service':
|
||||||
|
return None
|
||||||
|
|
||||||
|
quantity = TradeCustomDuplicate._line_quantity(line)
|
||||||
|
if quantity <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
side = TradeCustomDuplicate._line_side(line)
|
||||||
|
if side not in {'purchase', 'sale'}:
|
||||||
|
return None
|
||||||
|
|
||||||
|
lot = TradeCustomDuplicate._first_virtual_lot(line)
|
||||||
|
if not lot:
|
||||||
|
lot = TradeCustomDuplicate._create_open_virtual_lot(
|
||||||
|
line, side, quantity)
|
||||||
|
|
||||||
|
TradeCustomDuplicate._ensure_open_lot_qt(lot, side, quantity)
|
||||||
|
return lot
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_open_virtual_lot(line, side, quantity):
|
||||||
|
pool = Pool()
|
||||||
|
Lot = pool.get('lot.lot')
|
||||||
|
LotQtHist = pool.get('lot.qt.hist')
|
||||||
|
LotQtType = pool.get('lot.qt.type')
|
||||||
|
|
||||||
|
lot = Lot()
|
||||||
|
if side == 'purchase':
|
||||||
|
lot.line = line.id
|
||||||
|
lot.lot_qt = None
|
||||||
|
lot.lot_unit = None
|
||||||
|
else:
|
||||||
|
lot.sale_line = line.id
|
||||||
|
lot.lot_qt = quantity
|
||||||
|
lot.lot_unit_line = line.unit
|
||||||
|
lot.lot_quantity = quantity
|
||||||
|
lot.lot_gross_quantity = None
|
||||||
|
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 = quantity
|
||||||
|
lqh.gross_quantity = quantity
|
||||||
|
lot.lot_hist = [lqh]
|
||||||
|
Lot.save([lot])
|
||||||
|
|
||||||
|
lot = Lot(lot.id)
|
||||||
|
TradeCustomDuplicate._sync_fee_lots(line, lot, side)
|
||||||
|
return lot
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _sync_fee_lots(line, lot, side):
|
||||||
|
fees = list(getattr(line, 'fees', None) or [])
|
||||||
|
if not fees:
|
||||||
|
return
|
||||||
|
FeeLots = Pool().get('fee.lots')
|
||||||
|
line_field = 'line' if side == 'purchase' else 'sale_line'
|
||||||
|
for fee in fees:
|
||||||
|
domain = [
|
||||||
|
('fee', '=', fee.id),
|
||||||
|
('lot', '=', lot.id),
|
||||||
|
(line_field, '=', line.id),
|
||||||
|
]
|
||||||
|
if not FeeLots.search(domain):
|
||||||
|
fee_lot = FeeLots()
|
||||||
|
fee_lot.fee = fee.id
|
||||||
|
fee_lot.lot = lot.id
|
||||||
|
setattr(fee_lot, line_field, line.id)
|
||||||
|
FeeLots.save([fee_lot])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _ensure_open_lot_qt(lot, side, quantity):
|
||||||
|
LotQt = Pool().get('lot.qt')
|
||||||
|
if side == 'purchase':
|
||||||
|
domain = [
|
||||||
|
('lot_p', '=', lot.id),
|
||||||
|
('lot_s', '=', None),
|
||||||
|
('lot_shipment_in', '=', None),
|
||||||
|
('lot_shipment_internal', '=', None),
|
||||||
|
('lot_shipment_out', '=', None),
|
||||||
|
]
|
||||||
|
create = lambda: lot.createVirtualPart(quantity, None, None)
|
||||||
|
else:
|
||||||
|
domain = [
|
||||||
|
('lot_p', '=', None),
|
||||||
|
('lot_s', '=', lot.id),
|
||||||
|
('lot_shipment_in', '=', None),
|
||||||
|
('lot_shipment_internal', '=', None),
|
||||||
|
('lot_shipment_out', '=', None),
|
||||||
|
]
|
||||||
|
create = lambda: lot.createVirtualPart(
|
||||||
|
quantity, None, lot.id, 'only sale')
|
||||||
|
if not LotQt.search(domain):
|
||||||
|
create()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _create_matched_mirror(record, line, options):
|
def _create_matched_mirror(record, line, options):
|
||||||
source_model = record.__name__
|
source_model = record.__name__
|
||||||
@@ -222,6 +343,7 @@ class TradeCustomDuplicate(Wizard):
|
|||||||
options = self.start
|
options = self.start
|
||||||
new_record = self._copy_contract(record, options)
|
new_record = self._copy_contract(record, options)
|
||||||
new_line = self._apply_line_options(new_record, options)
|
new_line = self._apply_line_options(new_record, options)
|
||||||
|
TradeCustomDuplicate._ensure_open_virtual_lot(new_line)
|
||||||
self.start.duplicated_record_id = new_record.id
|
self.start.duplicated_record_id = new_record.id
|
||||||
if options.create_matched_mirror:
|
if options.create_matched_mirror:
|
||||||
self._create_matched_mirror(new_record, new_line, options)
|
self._create_matched_mirror(new_record, new_line, options)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from trytond.modules.purchase_trade import sale as sale_module
|
|||||||
from trytond.modules.purchase_trade import stock as stock_module
|
from trytond.modules.purchase_trade import stock as stock_module
|
||||||
from trytond.modules.purchase_trade import fee as fee_module
|
from trytond.modules.purchase_trade import fee as fee_module
|
||||||
from trytond.modules.purchase_trade import pricing as pricing_module
|
from trytond.modules.purchase_trade import pricing as pricing_module
|
||||||
|
from trytond.modules.purchase_trade import duplicate as duplicate_module
|
||||||
from trytond.modules.purchase_trade import company_defaults
|
from trytond.modules.purchase_trade import company_defaults
|
||||||
from trytond.modules.purchase_trade.service import ContractFactory
|
from trytond.modules.purchase_trade.service import ContractFactory
|
||||||
|
|
||||||
@@ -6053,6 +6054,126 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
self.assertIsNone(
|
self.assertIsNone(
|
||||||
ContractFactory._get_shipment_origin(ContractsStartMock()))
|
ContractFactory._get_shipment_origin(ContractsStartMock()))
|
||||||
|
|
||||||
|
def test_custom_duplicate_creates_purchase_open_virtual_lot(self):
|
||||||
|
'custom duplicate creates the purchase opening lot.qt explicitly'
|
||||||
|
saved_lots = []
|
||||||
|
virtual_parts = []
|
||||||
|
|
||||||
|
class FakeLot:
|
||||||
|
def __init__(self, id=None):
|
||||||
|
self.id = id
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def save(cls, lots):
|
||||||
|
for lot in lots:
|
||||||
|
lot.id = 42
|
||||||
|
saved_lots.append(lot)
|
||||||
|
|
||||||
|
def createVirtualPart(self, quantity, shipment, sale_lot):
|
||||||
|
virtual_parts.append((self.id, quantity, shipment, sale_lot))
|
||||||
|
|
||||||
|
class FakeLotQt:
|
||||||
|
@classmethod
|
||||||
|
def search(cls, domain):
|
||||||
|
return []
|
||||||
|
|
||||||
|
class FakeLotQtHist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class FakeLotQtType:
|
||||||
|
@classmethod
|
||||||
|
def search(cls, domain):
|
||||||
|
return [Mock()]
|
||||||
|
|
||||||
|
pool = Mock()
|
||||||
|
pool.get.side_effect = lambda name: {
|
||||||
|
'lot.lot': FakeLot,
|
||||||
|
'lot.qt': FakeLotQt,
|
||||||
|
'lot.qt.hist': FakeLotQtHist,
|
||||||
|
'lot.qt.type': FakeLotQtType,
|
||||||
|
}[name]
|
||||||
|
line = SimpleNamespace(
|
||||||
|
__name__='purchase.line',
|
||||||
|
id=10,
|
||||||
|
lots=[],
|
||||||
|
product=Mock(type='goods'),
|
||||||
|
unit=Mock(),
|
||||||
|
quantity=Decimal('0'),
|
||||||
|
quantity_theorical=Decimal('125'),
|
||||||
|
fees=[],
|
||||||
|
purchase=Mock(),
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(duplicate_module, 'Pool', return_value=pool):
|
||||||
|
lot = duplicate_module.TradeCustomDuplicate._ensure_open_virtual_lot(
|
||||||
|
line)
|
||||||
|
|
||||||
|
self.assertEqual(lot.id, 42)
|
||||||
|
self.assertEqual(saved_lots[0].line, 10)
|
||||||
|
self.assertEqual(saved_lots[0].lot_quantity, Decimal('125.00000'))
|
||||||
|
self.assertEqual(virtual_parts, [
|
||||||
|
(42, Decimal('125.00000'), None, None)])
|
||||||
|
|
||||||
|
def test_custom_duplicate_creates_sale_open_virtual_lot(self):
|
||||||
|
'custom duplicate creates the sale opening lot.qt explicitly'
|
||||||
|
saved_lots = []
|
||||||
|
virtual_parts = []
|
||||||
|
|
||||||
|
class FakeLot:
|
||||||
|
def __init__(self, id=None):
|
||||||
|
self.id = id
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def save(cls, lots):
|
||||||
|
for lot in lots:
|
||||||
|
lot.id = 43
|
||||||
|
saved_lots.append(lot)
|
||||||
|
|
||||||
|
def createVirtualPart(self, quantity, shipment, lot_s, mode):
|
||||||
|
virtual_parts.append((self.id, quantity, shipment, lot_s, mode))
|
||||||
|
|
||||||
|
class FakeLotQt:
|
||||||
|
@classmethod
|
||||||
|
def search(cls, domain):
|
||||||
|
return []
|
||||||
|
|
||||||
|
class FakeLotQtHist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class FakeLotQtType:
|
||||||
|
@classmethod
|
||||||
|
def search(cls, domain):
|
||||||
|
return []
|
||||||
|
|
||||||
|
pool = Mock()
|
||||||
|
pool.get.side_effect = lambda name: {
|
||||||
|
'lot.lot': FakeLot,
|
||||||
|
'lot.qt': FakeLotQt,
|
||||||
|
'lot.qt.hist': FakeLotQtHist,
|
||||||
|
'lot.qt.type': FakeLotQtType,
|
||||||
|
}[name]
|
||||||
|
line = SimpleNamespace(
|
||||||
|
__name__='sale.line',
|
||||||
|
id=11,
|
||||||
|
lots=[],
|
||||||
|
product=Mock(type='goods'),
|
||||||
|
unit=Mock(),
|
||||||
|
quantity=Decimal('0'),
|
||||||
|
quantity_theorical=Decimal('50'),
|
||||||
|
fees=[],
|
||||||
|
sale=Mock(),
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(duplicate_module, 'Pool', return_value=pool):
|
||||||
|
lot = duplicate_module.TradeCustomDuplicate._ensure_open_virtual_lot(
|
||||||
|
line)
|
||||||
|
|
||||||
|
self.assertEqual(lot.id, 43)
|
||||||
|
self.assertEqual(saved_lots[0].sale_line, 11)
|
||||||
|
self.assertEqual(saved_lots[0].lot_quantity, Decimal('50.00000'))
|
||||||
|
self.assertEqual(virtual_parts, [
|
||||||
|
(43, Decimal('50.00000'), None, 43, 'only sale')])
|
||||||
|
|
||||||
def test_lot_matching_rejects_purchase_quantity_above_available(self):
|
def test_lot_matching_rejects_purchase_quantity_above_available(self):
|
||||||
'apply matching cannot consume more than available purchase quantity'
|
'apply matching cannot consume more than available purchase quantity'
|
||||||
class LotQtMock:
|
class LotQtMock:
|
||||||
|
|||||||
Reference in New Issue
Block a user