--- a/trytond/trytond/modules/account_invoice_stock/__init__.py +++ b/trytond/trytond/modules/account_invoice_stock/__init__.py @@ -8,6 +8,7 @@ from .stock import * def register(): Pool.register( + Invoice, InvoiceLineStockMove, InvoiceLine, StockMove, --- a/trytond/trytond/modules/account_invoice_stock/account.py +++ b/trytond/trytond/modules/account_invoice_stock/account.py @@ -1,13 +1,27 @@ # 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, fields +from trytond.model import ModelSQL, ModelView, Workflow, fields from trytond.pool import Pool, PoolMeta from trytond.pyson import Eval - -__all__ = ['InvoiceLineStockMove', 'InvoiceLine'] +__all__ = ['InvoiceLineStockMove', 'InvoiceLine', 'Invoice'] +class Invoice(metaclass=PoolMeta): + __name__ = 'account.invoice' + + @classmethod + @ModelView.button + @Workflow.transition('posted') + def post(cls, invoices): + pool = Pool() + Move = pool.get('stock.move') + super().post(invoices) + moves = sum((l.stock_moves for i in invoices for l in i.lines), ()) + if moves: + Move.__queue__.update_unit_price(moves) + + class InvoiceLineStockMove(ModelSQL): 'Invoice Line - Stock Move' __name__ = 'account.invoice.line-stock.move' Index: /trytond/trytond/modules/account_invoice_stock/doc/index.rst =================================================================== --- a/trytond/trytond/modules/account_invoice_stock/doc/index.rst +++ b/trytond/trytond/modules/account_invoice_stock/doc/index.rst @@ -3,3 +3,5 @@ The account invoice stock module adds link between invoice lines and stock moves. +The unit price of the stock move is updated based on the average price of the +posted invoice lines that are linked to it. Index: /trytond/trytond/modules/account_invoice_stock/setup.py =================================================================== --- a/trytond/trytond/modules/account_invoice_stock/setup.py +++ b/trytond/trytond/modules/account_invoice_stock/setup.py @@ -57,6 +57,7 @@ requires.append(get_require_version('trytond_%s' % dep)) requires.append(get_require_version('trytond')) +tests_require = [get_require_version('proteus')] dependency_links = [] if minor_version % 2: dependency_links.append('https://trydevpi.tryton.org/') @@ -136,4 +137,5 @@ """, test_suite='tests', test_loader='trytond.test_loader:Loader', + tests_require=tests_require, ) Index: /trytond/trytond/modules/account_invoice_stock/stock.py =================================================================== --- a/trytond/trytond/modules/account_invoice_stock/stock.py +++ b/trytond/trytond/modules/account_invoice_stock/stock.py @@ -1,8 +1,11 @@ # 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 decimal import Decimal +from trytond.model import ModelView, Workflow, fields from trytond.pool import Pool, PoolMeta from trytond.transaction import Transaction +from trytond.pyson import Eval +from trytond.modules.product import round_price __all__ = ['StockMove', 'ShipmentOut'] @@ -10,8 +13,15 @@ __all__ = ['StockMove', 'ShipmentOut'] class StockMove(metaclass=PoolMeta): __name__ = 'stock.move' - invoice_lines = fields.Many2Many('account.invoice.line-stock.move', - 'stock_move', 'invoice_line', 'Invoice Lines') + invoice_lines = fields.Many2Many( + 'account.invoice.line-stock.move', 'stock_move', 'invoice_line', + "Invoice Lines", + domain=[ + ('product.default_uom_category', + '=', Eval('product_uom_category', -1)), + ('type', '=', 'line'), + ], + depends=['product_uom_category']) @property def invoiced_quantity(self): @@ -34,6 +67,37 @@ class StockMove(metaclass=PoolMeta): default.setdefault('invoice_lines', None) return super(StockMove, cls).copy(moves, default=default) + @classmethod + @ModelView.button + @Workflow.transition('done') + def do(cls, moves): + super().do(moves) + cls.update_unit_price(moves) + + @classmethod + def update_unit_price(cls, moves): + for move in moves: + if move.state == 'done': + unit_price = move._compute_unit_price() + if unit_price != move.unit_price: + move.unit_price = unit_price + cls.save(moves) + + def _compute_unit_price(self): + pool = Pool() + UoM = pool.get('product.uom') + amount, quantity = 0, 0 + for line in self.invoice_lines: + if line.invoice and line.invoice.state in {'posted', 'paid'}: + amount += line.amount + quantity += UoM.compute_qty( + line.unit, line.quantity, self.uom) + if not quantity: + unit_price = self.unit_price + else: + unit_price = round_price(amount / Decimal(str(quantity))) + return unit_price + class ShipmentOut(metaclass=PoolMeta): __name__ = 'stock.shipment.out' Index: /trytond/trytond/modules/account_invoice_stock/tests/scenario_account_invoice_stock.rst =================================================================== new file mode 100644 --- /dev/null +++ b/trytond/trytond/modules/account_invoice_stock/tests/scenario_account_invoice_stock.rst @@ -0,0 +1,140 @@ +======================== +Invoice - Stock Scenario +======================== + +Imports:: + + >>> from decimal import Decimal + >>> from proteus import Model, Wizard + >>> from trytond.tests.tools import activate_modules + >>> from trytond.modules.company.tests.tools import ( + ... create_company, get_company) + >>> from trytond.modules.account.tests.tools import ( + ... create_fiscalyear, create_chart, get_accounts) + >>> from trytond.modules.account_invoice.tests.tools import ( + ... set_fiscalyear_invoice_sequences) + +Install account_invoice_stock:: + + >>> config = activate_modules('account_invoice_stock') + +Create company:: + + >>> _ = create_company() + >>> company = get_company() + +Create fiscal year:: + + >>> fiscalyear = set_fiscalyear_invoice_sequences( + ... create_fiscalyear(company)) + >>> fiscalyear.click('create_period') + +Create chart of accounts:: + + >>> _ = create_chart(company) + >>> accounts = get_accounts(company) + +Create a party:: + + >>> Party = Model.get('party.party') + >>> party = Party(name="Party") + >>> party.save() + +Create an account category:: + + >>> ProductCategory = Model.get('product.category') + >>> account_category = ProductCategory(name="Account Category") + >>> account_category.accounting = True + >>> account_category.account_expense = accounts['expense'] + >>> account_category.account_revenue = accounts['revenue'] + >>> account_category.save() + +Create a product:: + + >>> ProductUom = Model.get('product.uom') + >>> unit, = ProductUom.find([('name', '=', 'Unit')]) + >>> ProductTemplate = Model.get('product.template') + >>> template = ProductTemplate() + >>> template.name = 'product' + >>> template.default_uom = unit + >>> template.type = 'goods' + >>> template.list_price = Decimal('40') + >>> template.account_category = account_category + >>> template.save() + >>> product, = template.products + +Get stock locations:: + + >>> Location = Model.get('stock.location') + >>> output_loc, = Location.find([('code', '=', 'OUT')]) + >>> customer_loc, = Location.find([('code', '=', 'CUS')]) + +Create a shipment:: + + >>> Shipment = Model.get('stock.shipment.out') + >>> Move = Model.get('stock.move') + >>> shipment = Shipment() + >>> shipment.customer = party + >>> move = shipment.outgoing_moves.new() + >>> move.product = product + >>> move.quantity = 10 + >>> move.from_location = output_loc + >>> move.to_location = customer_loc + >>> move.unit_price = Decimal('40.0000') + >>> shipment.click('wait') + >>> shipment.state + 'waiting' + >>> move, = shipment.outgoing_moves + +Create an invoice for half the quantity with higher price:: + + >>> Invoice = Model.get('account.invoice') + >>> invoice = Invoice(type='out') + >>> invoice.party = party + >>> line = invoice.lines.new() + >>> line.product = product + >>> line.quantity = 5 + >>> line.unit_price = Decimal('50.0000') + >>> line.stock_moves.append(Move(move.id)) + >>> invoice.click('post') + >>> invoice.state + 'posted' + +Check move unit price is not changed:: + + >>> move.reload() + >>> move.unit_price + Decimal('40.0000') + +Ship the products:: + + >>> shipment.click('assign_force') + >>> shipment.click('pack') + >>> shipment.click('done') + >>> shipment.state + 'done' + +Check move unit price has been updated:: + + >>> move.reload() + >>> move.unit_price + Decimal('50.0000') + +Create a second invoice for the remaining quantity cheaper:: + + >>> invoice = Invoice(type='out') + >>> invoice.party = party + >>> line = invoice.lines.new() + >>> line.product = product + >>> line.quantity = 5 + >>> line.unit_price = Decimal('40.0000') + >>> line.stock_moves.append(Move(move.id)) + >>> invoice.click('post') + >>> invoice.state + 'posted' + +Check move unit price has been updated again:: + + >>> move.reload() + >>> move.unit_price + Decimal('45.0000') Index: /trytond/trytond/modules/account_invoice_stock/tests/test_account_invoice_stock.py =================================================================== --- a/trytond/trytond/modules/account_invoice_stock/tests/test_account_invoice_stock.py +++ b/trytond/trytond/modules/account_invoice_stock/tests/test_account_invoice_stock.py @@ -1,8 +1,11 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. +import doctest import unittest import trytond.tests.test_tryton from trytond.tests.test_tryton import ModuleTestCase +from trytond.tests.test_tryton import doctest_teardown +from trytond.tests.test_tryton import doctest_checker class AccountInvoiceStockTestCase(ModuleTestCase): @@ -14,4 +17,9 @@ suite = trytond.tests.test_tryton.suite() suite.addTests(unittest.TestLoader().loadTestsFromTestCase( AccountInvoiceStockTestCase)) + suite.addTests(doctest.DocFileSuite( + 'scenario_account_invoice_stock.rst', + tearDown=doctest_teardown, encoding='utf-8', + checker=doctest_checker, + optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)) return suite Index: /trytond/trytond/modules/account_stock_landed_cost/stock.py =================================================================== --- a/trytond/trytond/modules/account_stock_landed_cost/stock.py +++ b/trytond/trytond/modules/account_stock_landed_cost/stock.py @@ -9,3 +9,9 @@ __name__ = 'stock.move' unit_landed_cost = fields.Numeric('Unit Landed Cost', digits=price_digits, readonly=True) + + def _compute_unit_price(self): + unit_price = super()._compute_unit_price() + if self.unit_landed_cost: + unit_price += self.unit_landed_cost + return unit_price Index: /trytond/trytond/modules/account_stock_landed_cost/tryton.cfg =================================================================== --- a/trytond/trytond/modules/account_stock_landed_cost/tryton.cfg +++ b/trytond/trytond/modules/account_stock_landed_cost/tryton.cfg @@ -7,6 +7,8 @@ product res stock +extra_depends: + account_invoice_stock xml: account.xml product.xml --- a/trytond/trytond/modules/product/product.py +++ b/trytond/trytond/modules/product/product.py @@ -20,7 +20,7 @@ from trytond.modules.company.model import ( __all__ = ['Template', 'Product', 'price_digits', 'TemplateFunction', 'ProductListPrice', 'ProductCostPriceMethod', 'ProductCostPrice', - 'TemplateCategory', 'TemplateCategoryAll'] + 'TemplateCategory', 'TemplateCategoryAll', 'round_price'] logger = logging.getLogger(__name__) STATES = { @@ -39,6 +39,11 @@ COST_PRICE_METHODS = [ price_digits = (16, config.getint('product', 'price_decimal', default=4)) +def round_price(value, rounding=None): + "Round price using the price digits" + return value.quantize( + Decimal(1) / 10 ** price_digits[1], rounding=rounding) + class Template( DeactivableMixin, ModelSQL, ModelView, CompanyMultiValueMixin):