diff --git a/CHANGELOG b/CHANGELOG index 98613f6..4c242f4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Allow to sale and purchase goods indicating second uom (extra depends) * Adapt to latest patched version of stock module Version 3.4.0 - 2015-06-08 diff --git a/__init__.py b/__init__.py index b85ec4e..09feca7 100644 --- a/__init__.py +++ b/__init__.py @@ -3,6 +3,8 @@ from trytond.pool import Pool from .product import * from .stock import * +from .sale import * +from .purchase import * def register(): @@ -18,4 +20,9 @@ def register(): PeriodCacheLot, Inventory, InventoryLine, + SaleLine, + PurchaseLine, module='stock_second_uom', type_='model') + Pool.register( + ReturnSale, + module='stock_second_uom', type_='wizard') diff --git a/locale/ca_ES.po b/locale/ca_ES.po index ece39af..fcac3dc 100644 --- a/locale/ca_ES.po +++ b/locale/ca_ES.po @@ -51,6 +51,46 @@ msgctxt "field:product.template,use_second_uom:" msgid "Use Second UOM" msgstr "Utilitza segona UdM" +msgctxt "field:purchase.line,product_second_uom_category:" +msgid "Product Second UoM Category" +msgstr "Categoria segona UdM del producte" + +msgctxt "field:purchase.line,second_quantity:" +msgid "Second Quantity" +msgstr "Segona quantitat" + +msgctxt "field:purchase.line,second_unit:" +msgid "Second Unit" +msgstr "Segona unitat" + +msgctxt "field:purchase.line,second_unit_digits:" +msgid "Second Unit Digits" +msgstr "Dígits segona UdM" + +msgctxt "field:purchase.line,use_second_unit:" +msgid "Use Second Unit" +msgstr "Utilitza segona unitat" + +msgctxt "field:sale.line,product_second_uom_category:" +msgid "Product Second UoM Category" +msgstr "Categoria segona UdM del producte" + +msgctxt "field:sale.line,second_quantity:" +msgid "Second Quantity" +msgstr "Segona quantitat" + +msgctxt "field:sale.line,second_unit:" +msgid "Second Unit" +msgstr "Segona unitat" + +msgctxt "field:sale.line,second_unit_digits:" +msgid "Second Unit Digits" +msgstr "Dígits segona UdM" + +msgctxt "field:sale.line,use_second_unit:" +msgid "Use Second Unit" +msgstr "Utilitza segona unitat" + msgctxt "field:stock.inventory.line,second_expected_quantity:" msgid "Second UoM Expected Quantity" msgstr "Quantitat esperada segona UdM" diff --git a/locale/es_ES.po b/locale/es_ES.po index 9988f12..c5db42d 100644 --- a/locale/es_ES.po +++ b/locale/es_ES.po @@ -52,6 +52,46 @@ msgctxt "field:product.template,use_second_uom:" msgid "Use Second UOM" msgstr "Usa segunda UdM" +msgctxt "field:purchase.line,product_second_uom_category:" +msgid "Product Second UoM Category" +msgstr "Categoria segunda UdM del producto" + +msgctxt "field:purchase.line,second_quantity:" +msgid "Second Quantity" +msgstr "Segunda cantidad" + +msgctxt "field:purchase.line,second_unit:" +msgid "Second Unit" +msgstr "Segunda unidad" + +msgctxt "field:purchase.line,second_unit_digits:" +msgid "Second Unit Digits" +msgstr "Dígitos segunda UdM" + +msgctxt "field:purchase.line,use_second_unit:" +msgid "Use Second Unit" +msgstr "Usa segunda unidad" + +msgctxt "field:sale.line,product_second_uom_category:" +msgid "Product Second UoM Category" +msgstr "Categoria segunda UdM del producto" + +msgctxt "field:sale.line,second_quantity:" +msgid "Second Quantity" +msgstr "Segunda cantidad" + +msgctxt "field:sale.line,second_unit:" +msgid "Second Unit" +msgstr "Segunda unidad" + +msgctxt "field:sale.line,second_unit_digits:" +msgid "Second Unit Digits" +msgstr "Dígitos segunda UdM" + +msgctxt "field:sale.line,use_second_unit:" +msgid "Use Second Unit" +msgstr "Usa segunda unidad" + msgctxt "field:stock.inventory.line,second_expected_quantity:" msgid "Second UoM Expected Quantity" msgstr "Cantidad esperada segunda UdM" diff --git a/purchase.py b/purchase.py new file mode 100644 index 0000000..0218046 --- /dev/null +++ b/purchase.py @@ -0,0 +1,85 @@ +# 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 Eval + +__all__ = ['PurchaseLine'] +__metaclass__ = PoolMeta + +STATES = { + 'invisible': (Eval('type') != 'line') | ~Eval('use_second_unit', False), + 'required': (Eval('type') == 'line') & Eval('use_second_unit', False), + 'readonly': ~Eval('_parent_sale', {}), + } +DEPENDS = ['type', 'use_second_unit'] + + +class PurchaseLine: + __name__ = 'purchase.line' + use_second_unit = fields.Function(fields.Boolean('Use Second Unit'), + 'on_change_with_use_second_unit') + second_quantity = fields.Float("Second Quantity", + digits=(16, Eval('second_unit_digits', 2)), + states=STATES, depends=DEPENDS + ['second_unit_digits']) + second_unit = fields.Many2One("product.uom", "Second Unit", domain=[ + ('category', '=', Eval('product_second_uom_category')), + ], + states=STATES, depends=DEPENDS + ['product_second_uom_category']) + second_unit_digits = fields.Function(fields.Integer('Second Unit Digits'), + 'on_change_with_second_unit_digits') + product_second_uom_category = fields.Function( + fields.Many2One('product.uom.category', 'Product Second UoM Category'), + 'on_change_with_product_second_uom_category') + + @fields.depends('product') + def on_change_with_use_second_unit(self, name=None): + if self.product and self.product.use_second_uom: + return True + return False + + @fields.depends('second_unit') + def on_change_with_second_unit_digits(self, name=None): + if self.second_unit: + return self.second_unit.digits + return 2 + + @fields.depends('product') + def on_change_with_product_second_uom_category(self, name=None): + if self.product and self.product.use_second_uom: + return self.product.second_uom.category.id + + @fields.depends('product', 'second_unit') + def on_change_product(self): + res = super(PurchaseLine, self).on_change_product() + if self.product and self.product.use_second_uom: + if (not self.second_unit + or self.second_unit.category + != self.product.second_uom.category): + self.second_unit = self.product.second_uom + res['second_unit'] = self.product.second_uom.id + res['second_unit.rec_name'] = self.product.second_uom.rec_name + res['second_unit_digits'] = self.product.second_uom.digits + return res + + def get_move(self): + pool = Pool() + Uom = pool.get('product.uom') + + move = super(PurchaseLine, self).get_move() + + if move and self.use_second_unit: + skip = set(self.moves_recreated) + second_quantity = abs(self.second_quantity) + for move in self.moves: + if move not in skip: + second_quantity -= Uom.compute_qty( + move.second_uom, + move.second_quantity, + self.second_unit) + + second_quantity = max( + Uom.round(second_quantity, self.second_unit.rounding), 0) + move.second_quantity = second_quantity + move.second_uom = self.second_unit + return move diff --git a/purchase.xml b/purchase.xml new file mode 100644 index 0000000..2d92c52 --- /dev/null +++ b/purchase.xml @@ -0,0 +1,24 @@ + + + + + + purchase.line + + purchase_line_form + + + + purchase.line + + purchase_line_tree + + + + purchase.line + + purchase_line_tree + + + diff --git a/sale.py b/sale.py new file mode 100644 index 0000000..272d4e1 --- /dev/null +++ b/sale.py @@ -0,0 +1,102 @@ +# 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 Eval + +__all__ = ['SaleLine', 'ReturnSale'] +__metaclass__ = PoolMeta + +STATES = { + 'invisible': (Eval('type') != 'line') | ~Eval('use_second_unit', False), + 'required': (Eval('type') == 'line') & Eval('use_second_unit', False), + 'readonly': ~Eval('_parent_sale', {}), + } +DEPENDS = ['type', 'use_second_unit'] + + +class SaleLine: + __name__ = 'sale.line' + use_second_unit = fields.Function(fields.Boolean('Use Second Unit'), + 'on_change_with_use_second_unit') + second_quantity = fields.Float("Second Quantity", + digits=(16, Eval('second_unit_digits', 2)), + states=STATES, depends=DEPENDS + ['second_unit_digits']) + second_unit = fields.Many2One("product.uom", "Second Unit", domain=[ + ('category', '=', Eval('product_second_uom_category')), + ], + states=STATES, depends=DEPENDS + ['product_second_uom_category']) + second_unit_digits = fields.Function(fields.Integer('Second Unit Digits'), + 'on_change_with_second_unit_digits') + product_second_uom_category = fields.Function( + fields.Many2One('product.uom.category', 'Product Second UoM Category'), + 'on_change_with_product_second_uom_category') + + @fields.depends('product') + def on_change_with_use_second_unit(self, name=None): + if self.product and self.product.use_second_uom: + return True + return False + + @fields.depends('second_unit') + def on_change_with_second_unit_digits(self, name=None): + if self.second_unit: + return self.second_unit.digits + return 2 + + @fields.depends('product') + def on_change_with_product_second_uom_category(self, name=None): + if self.product and self.product.use_second_uom: + return self.product.second_uom.category.id + + @fields.depends('product', 'second_unit') + def on_change_product(self): + res = super(SaleLine, self).on_change_product() + if self.product and self.product.use_second_uom: + if (not self.second_unit + or self.second_unit.category + != self.product.second_uom.category): + self.second_unit = self.product.second_uom + res['second_unit'] = self.product.second_uom.id + res['second_unit.rec_name'] = self.product.second_uom.rec_name + res['second_unit_digits'] = self.product.second_uom.digits + return res + + def get_move(self, shipment_type): + pool = Pool() + Uom = pool.get('product.uom') + + move = super(SaleLine, self).get_move(shipment_type) + + if move and self.use_second_unit: + skip = set(self.moves_recreated) + second_quantity = abs(self.second_quantity) + for move in self.moves: + if move not in skip: + second_quantity -= Uom.compute_qty( + move.second_uom, + move.second_quantity, + self.second_unit) + + second_quantity = max( + Uom.round(second_quantity, self.second_unit.rounding), 0) + move.second_quantity = second_quantity + move.second_uom = self.second_unit + return move + + +class ReturnSale: + __name__ = 'sale.return_sale' + + def do_return_(self, action): + Sale = Pool().get('sale.sale') + + action, data = super(ReturnSale, self).do_return_(action) + return_sale_ids = data.get('res_id', []) + if return_sale_ids: + for sale in Sale.browse(return_sale_ids): + for line in sale.lines: + if line.second_quantity: + line.second_quantity *= -1 + line.save() + return action, data diff --git a/sale.xml b/sale.xml new file mode 100644 index 0000000..a0d8d72 --- /dev/null +++ b/sale.xml @@ -0,0 +1,24 @@ + + + + + + sale.line + + sale_line_form + + + + sale.line + + sale_line_tree + + + + sale.line + + sale_line_tree + + + diff --git a/tests/scenario_stock_second_uom_extras_depend.rst b/tests/scenario_stock_second_uom_extras_depend.rst new file mode 100644 index 0000000..59ec155 --- /dev/null +++ b/tests/scenario_stock_second_uom_extras_depend.rst @@ -0,0 +1,725 @@ +======================================= +Second UoM Scenario with extras depends +======================================= + +============= +General Setup +============= + +Imports:: + + >>> import datetime + >>> from dateutil.relativedelta import relativedelta + >>> from decimal import Decimal + >>> from proteus import config, Model, Wizard + >>> today = datetime.date.today() + >>> last_month = today - relativedelta(months=1) + +Create database:: + + >>> config = config.set_trytond() + >>> config.pool.test = True + +Install stock_second_uom Module:: + + >>> Module = Model.get('ir.module.module') + >>> stock_module, = Module.find([('name', '=', 'stock_second_uom')]) + >>> stock_module.click('install') + >>> Wizard('ir.module.module.install_upgrade').execute('upgrade') + +Install product_raw_variant Module:: + + >>> Module = Model.get('ir.module.module') + >>> stock_module, = Module.find([('name', '=', 'product_raw_variant')]) + >>> stock_module.click('install') + >>> Wizard('ir.module.module.install_upgrade').execute('upgrade') + +Install stock_lot Module:: + + >>> Module = Model.get('ir.module.module') + >>> stock_module, = Module.find([('name', '=', 'stock_lot')]) + >>> stock_module.click('install') + >>> Wizard('ir.module.module.install_upgrade').execute('upgrade') + +Install sale Module:: + + >>> Module = Model.get('ir.module.module') + >>> stock_module, = Module.find([('name', '=', 'sale')]) + >>> stock_module.click('install') + >>> Wizard('ir.module.module.install_upgrade').execute('upgrade') + +Install purchase Module:: + + >>> Module = Model.get('ir.module.module') + >>> stock_module, = Module.find([('name', '=', 'purchase')]) + >>> stock_module.click('install') + >>> Wizard('ir.module.module.install_upgrade').execute('upgrade') + +Create company:: + + >>> Currency = Model.get('currency.currency') + >>> CurrencyRate = Model.get('currency.currency.rate') + >>> Company = Model.get('company.company') + >>> Party = Model.get('party.party') + >>> company_config = Wizard('company.company.config') + >>> company_config.execute('company') + >>> company = company_config.form + >>> party = Party(name='Dunder Mifflin') + >>> party.save() + >>> company.party = party + >>> currencies = Currency.find([('code', '=', 'USD')]) + >>> if not currencies: + ... currency = Currency(name='US Dollar', symbol='$', code='USD', + ... rounding=Decimal('0.01'), mon_grouping='[3, 3, 0]', + ... mon_decimal_point='.') + ... currency.save() + ... CurrencyRate(date=today + relativedelta(month=1, day=1), + ... rate=Decimal('1.0'), currency=currency).save() + ... else: + ... currency, = currencies + >>> company.currency = currency + >>> company_config.execute('add') + >>> company, = Company.find() + +Reload the context:: + + >>> User = Model.get('res.user') + >>> config._context = User.get_preferences(True, config.context) + +Create fiscal year:: + + >>> FiscalYear = Model.get('account.fiscalyear') + >>> Sequence = Model.get('ir.sequence') + >>> SequenceStrict = Model.get('ir.sequence.strict') + >>> fiscalyear = FiscalYear(name=str(today.year)) + >>> fiscalyear.start_date = today + relativedelta(month=1, day=1) + >>> fiscalyear.end_date = today + relativedelta(month=12, day=31) + >>> fiscalyear.company = company + >>> post_move_seq = Sequence(name=str(today.year), code='account.move', + ... company=company) + >>> post_move_seq.save() + >>> fiscalyear.post_move_sequence = post_move_seq + >>> invoice_seq = SequenceStrict(name=str(today.year), + ... code='account.invoice', company=company) + >>> invoice_seq.save() + >>> fiscalyear.out_invoice_sequence = invoice_seq + >>> fiscalyear.in_invoice_sequence = invoice_seq + >>> fiscalyear.out_credit_note_sequence = invoice_seq + >>> fiscalyear.in_credit_note_sequence = invoice_seq + >>> fiscalyear.save() + >>> FiscalYear.create_period([fiscalyear.id], config.context) + +Create chart of accounts:: + + >>> AccountTemplate = Model.get('account.account.template') + >>> Account = Model.get('account.account') + >>> Journal = Model.get('account.journal') + >>> account_template, = AccountTemplate.find([('parent', '=', None)]) + >>> create_chart = Wizard('account.create_chart') + >>> create_chart.execute('account') + >>> create_chart.form.account_template = account_template + >>> create_chart.form.company = company + >>> create_chart.execute('create_account') + >>> receivable, = Account.find([ + ... ('kind', '=', 'receivable'), + ... ('company', '=', company.id), + ... ]) + >>> payable, = Account.find([ + ... ('kind', '=', 'payable'), + ... ('company', '=', company.id), + ... ]) + >>> revenue, = Account.find([ + ... ('kind', '=', 'revenue'), + ... ('company', '=', company.id), + ... ]) + >>> expense, = Account.find([ + ... ('kind', '=', 'expense'), + ... ('company', '=', company.id), + ... ]) + >>> create_chart.form.account_receivable = receivable + >>> create_chart.form.account_payable = payable + >>> create_chart.execute('create_properties') + >>> cash, = Account.find([ + ... ('kind', '=', 'other'), + ... ('name', '=', 'Main Cash'), + ... ('company', '=', company.id), + ... ]) + >>> cash_journal, = Journal.find([('type', '=', 'cash')]) + >>> cash_journal.credit_account = cash + >>> cash_journal.debit_account = cash + >>> cash_journal.save() + +Create payment term:: + + >>> PaymentTerm = Model.get('account.invoice.payment_term') + >>> PaymentTermLine = Model.get('account.invoice.payment_term.line') + >>> payment_term = PaymentTerm(name='Direct') + >>> payment_term_line = PaymentTermLine(type='remainder', days=0) + >>> payment_term.lines.append(payment_term_line) + >>> payment_term.save() + +Get stock locations:: + + >>> Location = Model.get('stock.location') + >>> supplier_loc, = Location.find([('code', '=', 'SUP')]) + >>> storage_loc, = Location.find([('code', '=', 'STO')]) + >>> customer_loc, = Location.find([('code', '=', 'CUS')]) + +Create parties:: + + >>> Party = Model.get('party.party') + >>> supplier = Party(name='Supplier') + >>> supplier.save() + >>> customer = Party(name='Customer') + >>> customer.save() + +Create products:: + + >>> ProductUom = Model.get('product.uom') + >>> ProductTemplate = Model.get('product.template') + >>> kg, = ProductUom.find([('name', '=', 'Kilogram')]) + >>> unit, = ProductUom.find([('name', '=', 'Unit')]) + >>> template = ProductTemplate() + >>> template.name = 'Product' + >>> template.default_uom = kg + >>> template.second_uom = unit + >>> template.type = 'goods' + >>> template.purchasable = True + >>> template.salable = True + >>> template.list_price = Decimal('300') + >>> template.cost_price = Decimal('80') + >>> template.cost_price_method = 'average' + >>> template.account_expense = expense + >>> template.account_revenue = revenue + >>> template.save() + >>> product_wo_2uom, = template.products + >>> product_w_2uom = template.products.new() + >>> product_w_2uom.use_second_uom = True + >>> product_w_2uom.save() + + >>> LotType = Model.get('stock.lot.type') + >>> template = ProductTemplate() + >>> template.name = 'Product' + >>> template.default_uom = kg + >>> template.second_uom = unit + >>> template.type = 'goods' + >>> template.purchasable = True + >>> template.salable = True + >>> template.list_price = Decimal('300') + >>> template.cost_price = Decimal('80') + >>> template.cost_price_method = 'average' + >>> template.account_expense = expense + >>> template.account_revenue = revenue + >>> for lot_type in LotType.find([]): + ... template.lot_required.append(lot_type) + >>> template.save() + >>> product_lot_wo_2uom, = template.products + >>> product_lot_w_2uom = template.products.new() + >>> product_lot_w_2uom.use_second_uom = True + >>> product_lot_w_2uom.save() + +Purchase products two month ago:: + + >>> Purchase = Model.get('purchase.purchase') + >>> purchase = Purchase() + >>> purchase.party = supplier + >>> purchase.date = last_month - relativedelta(months=1) + >>> purchase.payment_term = payment_term + >>> purchase.invoice_method = 'manual' + >>> purchase_line = purchase.lines.new() + >>> purchase_line.product = product_wo_2uom + >>> purchase_line.quantity = 100 + >>> purchase_line = purchase.lines.new() + >>> purchase_line.product = product_w_2uom + >>> purchase_line.quantity = 200 + >>> purchase_line.second_quantity = 10 + >>> purchase_line = purchase.lines.new() + >>> purchase_line.product = product_lot_wo_2uom + >>> purchase_line.quantity = 25 + >>> purchase_line = purchase.lines.new() + >>> purchase_line.product = product_lot_w_2uom + >>> purchase_line.quantity = 75 + >>> purchase_line.second_quantity = 6 + >>> purchase.click('quote') + >>> purchase.click('confirm') + >>> purchase.click('process') + >>> purchase.state + u'processing' + >>> len(purchase.moves), len(purchase.shipment_returns) + (4, 0) + >>> for move in purchase.moves: + ... if move.product in (product_wo_2uom, product_lot_wo_2uom): + ... (move.second_uom == None, move.second_quantity == None) + ... elif move.product == product_w_2uom: + ... (move.second_uom == unit, move.second_quantity == 10) + ... elif move.product == product_lot_w_2uom: + ... (move.second_uom == unit, move.second_quantity == 6) + (True, True) + (True, True) + (True, True) + (True, True) + +Validate Shipments one month ago:: + + >>> ShipmentIn = Model.get('stock.shipment.in') + >>> Move = Model.get('stock.move') + >>> Lot = Model.get('stock.lot') + >>> shipment_in = ShipmentIn() + >>> shipment_in.supplier = supplier + >>> shipment_in.effective_date = last_month + >>> for move in purchase.moves: + ... incoming_move = Move(id=move.id) + ... if move.product == product_lot_wo_2uom: + ... lot_wo_2uom = Lot( + ... product=product_lot_wo_2uom, + ... number=str(product_lot_wo_2uom.id)) + ... lot_wo_2uom.save() + ... incoming_move.lot = lot_wo_2uom + ... elif move.product == product_lot_w_2uom: + ... lot_w_2uom = Lot( + ... product=product_lot_w_2uom, + ... number=str(product_lot_wo_2uom.id)) + ... lot_w_2uom.save() + ... incoming_move.lot = lot_w_2uom + ... shipment_in.incoming_moves.append(incoming_move) + >>> shipment_in.save() + >>> shipment_in.click('receive') + >>> shipment_in.click('done') + +Check available quantities by product:: + + >>> with config.set_context({'locations': [storage_loc.id], 'stock_date_end': today}): + ... product_wo_2uom.reload() + ... product_wo_2uom.quantity + ... product_wo_2uom.second_quantity + ... product_w_2uom.reload() + ... product_w_2uom.quantity + ... product_w_2uom.second_quantity + ... product_lot_wo_2uom.reload() + ... product_lot_wo_2uom.quantity + ... product_lot_wo_2uom.second_quantity + ... product_lot_w_2uom.reload() + ... product_lot_w_2uom.quantity + ... product_lot_w_2uom.second_quantity + 100.0 + 0.0 + 200.0 + 10.0 + 25.0 + 0.0 + 75.0 + 6.0 + +Check available quantities by lot:: + + >>> with config.set_context({'locations': [storage_loc.id], 'stock_date_end': today}): + ... lot_wo_2uom.reload() + ... lot_wo_2uom.quantity + ... lot_wo_2uom.second_quantity + ... lot_w_2uom.reload() + ... lot_w_2uom.quantity + ... lot_w_2uom.second_quantity + 25.0 + 0.0 + 75.0 + 6.0 + +Create an inventory:: + + >>> Inventory = Model.get('stock.inventory') + >>> inventory = Inventory() + >>> inventory.date = last_month + relativedelta(days=5) + >>> inventory.location = storage_loc + >>> inventory.save() + >>> inventory.click('complete_lines') + >>> len(inventory.lines) + 4 + >>> for line in inventory.lines: + ... if line.product == product_wo_2uom: + ... line.expected_quantity == 100.0 + ... line.second_expected_quantity == 0.0 + ... line.quantity = 80.0 + ... elif line.product == product_w_2uom: + ... line.expected_quantity == 200.0 + ... line.second_expected_quantity == 10.0 + ... line.quantity = 190.0 + ... line.second_quantity = 8 + ... elif line.product == product_lot_wo_2uom and line.lot == lot_wo_2uom: + ... line.expected_quantity == 25.0 + ... line.second_expected_quantity == 0.0 + ... line.quantity = 30.0 + ... elif line.product == product_lot_w_2uom and line.lot == lot_w_2uom: + ... line.expected_quantity == 75.0 + ... line.second_expected_quantity == 6.0 + ... line.quantity = 85.0 + ... line.second_quantity = 7 + True + True + True + True + True + True + True + True + >>> inventory.save() + >>> inventory.click('confirm') + +Check available quantities:: + + >>> with config.set_context({'locations': [storage_loc.id], 'stock_date_end': today}): + ... product_wo_2uom.reload() + ... product_wo_2uom.quantity + ... product_wo_2uom.second_quantity + ... product_w_2uom.reload() + ... product_w_2uom.quantity + ... product_w_2uom.second_quantity + ... product_lot_wo_2uom.reload() + ... product_lot_wo_2uom.quantity + ... product_lot_wo_2uom.second_quantity + ... product_lot_w_2uom.reload() + ... product_lot_w_2uom.quantity + ... product_lot_w_2uom.second_quantity + ... lot_wo_2uom.reload() + ... lot_wo_2uom.quantity + ... lot_wo_2uom.second_quantity + ... lot_w_2uom.reload() + ... lot_w_2uom.quantity + ... lot_w_2uom.second_quantity + 80.0 + 0.0 + 190.0 + 8.0 + 30.0 + 0.0 + 85.0 + 7.0 + 30.0 + 0.0 + 85.0 + 7.0 + +Create a period:: + + >>> Period = Model.get('stock.period') + >>> period = Period() + >>> period.date = last_month + relativedelta(days=10) + >>> period.company = company + >>> period.save() + >>> period.click('close') + >>> period.reload() + >>> for cache in period.caches: + ... if (cache.product == product_wo_2uom + ... and cache.location == storage_loc): + ... cache.internal_quantity == 80.0 + ... cache.second_internal_quantity == 0.0 + ... elif (cache.product == product_w_2uom + ... and cache.location == storage_loc): + ... cache.internal_quantity == 190.0 + ... cache.second_internal_quantity == 8 + ... elif (cache.product == product_lot_wo_2uom + ... and cache.location == storage_loc): + ... cache.internal_quantity == 30.0 + ... cache.second_internal_quantity == 0.0 + ... elif (cache.product == product_lot_w_2uom + ... and cache.location == storage_loc): + ... cache.internal_quantity == 85.0 + ... cache.second_internal_quantity == 7 + True + True + True + True + True + True + True + True + +Check available quantities:: + + >>> with config.set_context({'locations': [storage_loc.id], 'stock_date_end': today}): + ... product_wo_2uom.reload() + ... product_wo_2uom.quantity + ... product_wo_2uom.second_quantity + ... product_w_2uom.reload() + ... product_w_2uom.quantity + ... product_w_2uom.second_quantity + ... product_lot_wo_2uom.reload() + ... product_lot_wo_2uom.quantity + ... product_lot_wo_2uom.second_quantity + ... product_lot_w_2uom.reload() + ... product_lot_w_2uom.quantity + ... product_lot_w_2uom.second_quantity + ... lot_wo_2uom.reload() + ... lot_wo_2uom.quantity + ... lot_wo_2uom.second_quantity + ... lot_w_2uom.reload() + ... lot_w_2uom.quantity + ... lot_w_2uom.second_quantity + 80.0 + 0.0 + 190.0 + 8.0 + 30.0 + 0.0 + 85.0 + 7.0 + 30.0 + 0.0 + 85.0 + 7.0 + +Create an inventory decreasing quantity in main UoM and increasing in second +UoM:: + + >>> Inventory = Model.get('stock.inventory') + >>> inventory = Inventory() + >>> inventory.date = last_month + relativedelta(days=15) + >>> inventory.location = storage_loc + >>> inventory.save() + >>> inventory.click('complete_lines') + >>> len(inventory.lines) + 4 + >>> for line in inventory.lines: + ... if line.product == product_w_2uom: + ... line.quantity = 180.0 + ... line.second_quantity = 9 + ... elif line.product == product_lot_w_2uom: + ... line.quantity = 90.0 + ... line.second_quantity = 5 + >>> inventory.save() + >>> inventory.click('confirm') + >>> inventory.reload() + >>> inventory_moves = [m for l in inventory.lines for m in l.moves] + >>> len(inventory_moves) + 2 + >>> for move in inventory_moves: + ... if move.product == product_w_2uom: + ... move.quantity == 10.0 + ... move.second_quantity == -1.0 + ... elif move.product == product_lot_w_2uom: + ... move.quantity == 5.0 + ... move.second_quantity == -2.0 + True + True + True + True + +Sale products:: + + >>> Sale = Model.get('sale.sale') + >>> sale = Sale() + >>> sale.party = customer + >>> sale.date = last_month + relativedelta(days=18) + >>> sale.payment_term = payment_term + >>> sale.invoice_method = 'manual' + >>> sale_line = sale.lines.new() + >>> sale_line.product = product_wo_2uom + >>> sale_line.quantity = 40.0 + >>> sale_line = sale.lines.new() + >>> sale_line.product = product_w_2uom + >>> sale_line.quantity = 30.0 + >>> sale_line.second_quantity = 2 + >>> sale_line = sale.lines.new() + >>> sale_line.product = product_lot_wo_2uom + >>> sale_line.quantity = 10.0 + >>> sale_line = sale.lines.new() + >>> sale_line.product = product_lot_w_2uom + >>> sale_line.quantity = 80.0 + >>> sale_line.second_quantity = 4 + >>> sale.save() + >>> Sale.quote([sale.id], config.context) + >>> Sale.confirm([sale.id], config.context) + >>> Sale.process([sale.id], config.context) + >>> sale.state + u'processing' + >>> sale.reload() + >>> len(sale.shipments), len(sale.shipment_returns), len(sale.moves) + (1, 0, 4) + >>> for move in sale.moves: + ... if move.product in (product_wo_2uom, product_lot_wo_2uom): + ... move.second_uom == None and move.second_quantity == None + ... elif move.product == product_w_2uom: + ... move.second_uom == unit and move.second_quantity == 2 + ... elif move.product == product_lot_w_2uom: + ... move.second_uom == unit and move.second_quantity == 4 + True + True + True + True + +Check sale shpiment inventory moves:: + + >>> shipment_out, = sale.shipments + >>> len(shipment_out.inventory_moves) + 4 + >>> for move in shipment_out.inventory_moves: + ... if move.product == product_wo_2uom: + ... (move.second_uom == None, move.second_quantity == None) + ... elif move.product == product_w_2uom: + ... (move.second_uom == unit, move.second_quantity == 2) + ... elif move.product == product_lot_wo_2uom: + ... (move.second_uom == None, move.second_quantity == None) + ... move.lot = lot_wo_2uom + ... elif move.product == product_lot_w_2uom: + ... (move.second_uom == unit, move.second_quantity == 4) + ... move.lot = lot_w_2uom + (True, True) + (True, True) + (True, True) + (True, True) + >>> shipment_out.save() + +Assign sale shipment:: + + >>> shipment_out.click('assign_try') + True + +.. TODO Check available quantities and forecast quantities:: +.. +.. >>> with config.set_context({'locations': [storage_loc.id], 'stock_date_end': today}): +.. ... product_wo_2uom.reload() +.. ... product_wo_2uom.quantity +.. ... product_wo_2uom.second_quantity +.. ... product_wo_2uom.forecast_quantity +.. ... product_wo_2uom.second_forecast_quantity +.. ... product_w_2uom.reload() +.. ... product_w_2uom.quantity +.. ... product_w_2uom.second_quantity +.. ... product_w_2uom.forecast_quantity +.. ... product_w_2uom.second_forecast_quantity +.. ... product_lot_wo_2uom.reload() +.. ... product_lot_wo_2uom.quantity +.. ... product_lot_wo_2uom.second_quantity +.. ... product_lot_wo_2uom.forecast_quantity +.. ... product_lot_wo_2uom.second_forecast_quantity +.. ... product_lot_w_2uom.reload() +.. ... product_lot_w_2uom.quantity +.. ... product_lot_w_2uom.second_quantity +.. ... product_lot_w_2uom.forecast_quantity +.. ... product_lot_w_2uom.second_forecast_quantity +.. ... lot_wo_2uom.reload() +.. ... lot_wo_2uom.quantity +.. ... lot_wo_2uom.second_quantity +.. ... lot_wo_2uom.forecast_quantity +.. ... lot_wo_2uom.second_forecast_quantity +.. ... lot_w_2uom.reload() +.. ... lot_w_2uom.quantity +.. ... lot_w_2uom.second_quantity +.. ... lot_w_2uom.forecast_quantity +.. ... lot_w_2uom.second_forecast_quantity +.. 80.0 +.. 0.0 +.. 40.0 +.. 0.0 +.. 180.0 +.. 9.0 +.. 150.0 +.. 7.0 +.. 30.0 +.. 0.0 +.. 20.0 +.. 0.0 +.. 90.0 +.. 5.0 +.. 10.0 +.. 1.0 +.. 30.0 +.. 0.0 +.. 20.0 +.. 0.0 +.. 90.0 +.. 5.0 +.. 10.0 +.. 1.0 + +Finalize the shipment:: + + >>> shipment_out.reload() + >>> shipment_out.click('pack') + >>> shipment_out.reload() + >>> shipment_out.click('done') + +Create return sale:: + + >>> return_sale = Wizard('sale.return_sale', [sale]) + >>> return_sale.execute('return_') + >>> returned_sale, = Sale.find([ + ... ('state', '=', 'draft'), + ... ]) + >>> sorted([(x.quantity, x.second_quantity) for x in returned_sale.lines]) + [(-80.0, -4.0), (-40.0, None), (-30.0, -2.0), (-10.0, None)] + >>> for sale_line in returned_sale.lines: + ... if sale_line.product == product_wo_2uom: + ... sale_line.quantity = -25 + ... elif sale_line.product == product_w_2uom: + ... sale_line.quantity = -15 + ... sale_line.second_quantity = -1 + ... elif sale_line.product == product_lot_wo_2uom: + ... sale_line.quantity = -2 + ... elif sale_line.product == product_lot_w_2uom: + ... sale_line.quantity = -10 + ... sale_line.second_quantity = -1 + >>> returned_sale.save() + >>> returned_sale.click('quote') + >>> returned_sale.click('confirm') + >>> returned_sale.click('process') + >>> returned_sale.state + u'processing' + >>> len(returned_sale.shipments), len(returned_sale.shipment_returns) + (0, 1) + +Validate return shipment:: + + >>> shipment_return, = returned_sale.shipment_returns + >>> for move in shipment_return.incoming_moves: + ... if move.product == product_wo_2uom: + ... move.second_quantity == None + ... elif move.product == product_w_2uom: + ... move.second_quantity == 1 + ... elif move.product == product_lot_wo_2uom: + ... move.second_quantity == None + ... move.lot = lot_wo_2uom + ... elif move.product == product_lot_w_2uom: + ... move.second_quantity == 1 + ... move.lot = lot_w_2uom + True + True + True + True + >>> shipment_return.save() + >>> shipment_return.click('receive') + >>> shipment_return.click('done') + +Check available quantities:: + + >>> with config.set_context({'locations': [storage_loc.id], 'stock_date_end': today}): + ... product_wo_2uom.reload() + ... product_wo_2uom.quantity + ... product_wo_2uom.second_quantity + ... product_w_2uom.reload() + ... product_w_2uom.quantity + ... product_w_2uom.second_quantity + ... product_lot_wo_2uom.reload() + ... product_lot_wo_2uom.quantity + ... product_lot_wo_2uom.second_quantity + ... product_lot_w_2uom.reload() + ... product_lot_w_2uom.quantity + ... product_lot_w_2uom.second_quantity + ... lot_wo_2uom.reload() + ... lot_wo_2uom.quantity + ... lot_wo_2uom.second_quantity + ... lot_w_2uom.reload() + ... lot_w_2uom.quantity + ... lot_w_2uom.second_quantity + 65.0 + 0.0 + 165.0 + 8.0 + 22.0 + 0.0 + 20.0 + 2.0 + 22.0 + 0.0 + 20.0 + 2.0 + diff --git a/tests/test_stock_second_uom.py b/tests/test_stock_second_uom.py index c2a543f..6709d71 100644 --- a/tests/test_stock_second_uom.py +++ b/tests/test_stock_second_uom.py @@ -28,4 +28,8 @@ def suite(): suite.addTests(doctest.DocFileSuite('scenario_stock_second_uom.rst', setUp=doctest_setup, tearDown=doctest_teardown, encoding='utf-8', optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)) + suite.addTests(doctest.DocFileSuite( + 'scenario_stock_second_uom_extras_depend.rst', + setUp=doctest_setup, tearDown=doctest_teardown, encoding='utf-8', + optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)) return suite diff --git a/tryton.cfg b/tryton.cfg index cf15123..827dc03 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -5,6 +5,10 @@ depends: extras_depend: product_raw_variant stock_lot + sale + purchase xml: product.xml stock.xml + sale.xml + purchase.xml diff --git a/view/purchase_line_form.xml b/view/purchase_line_form.xml new file mode 100644 index 0000000..23e364a --- /dev/null +++ b/view/purchase_line_form.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/view/purchase_line_tree.xml b/view/purchase_line_tree.xml new file mode 100644 index 0000000..f1a0cd8 --- /dev/null +++ b/view/purchase_line_tree.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/view/sale_line_form.xml b/view/sale_line_form.xml new file mode 100644 index 0000000..23e364a --- /dev/null +++ b/view/sale_line_form.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/view/sale_line_tree.xml b/view/sale_line_tree.xml new file mode 100644 index 0000000..f1a0cd8 --- /dev/null +++ b/view/sale_line_tree.xml @@ -0,0 +1,9 @@ + + + + + + + +