changeset d36ed05253a0 in modules/account_invoice_stock:default
details: 
https://hg.tryton.org/modules/account_invoice_stock?cmd=changeset;node=d36ed05253a0
description:
        Update unit price of stock moves based on posted invoice lines

        We update the unit price of the stock move when it is done and when a 
linked
        invoice is posted. This allow to keep an accurate cost price 
computation when
        price varies between the order and the invoice.

        issue7280
        review311431002
diffstat:

 CHANGELOG                                |    2 +
 __init__.py                              |    1 +
 account.py                               |   17 +++-
 doc/index.rst                            |    2 +
 setup.py                                 |    2 +
 stock.py                                 |   41 ++++++++-
 tests/scenario_account_invoice_stock.rst |  140 +++++++++++++++++++++++++++++++
 tests/test_account_invoice_stock.py      |    8 +
 8 files changed, 211 insertions(+), 2 deletions(-)

diffs (314 lines):

diff -r 423979d039bc -r d36ed05253a0 CHANGELOG
--- a/CHANGELOG Sat Jun 06 10:30:11 2020 +0100
+++ b/CHANGELOG Wed Jun 24 23:10:46 2020 +0200
@@ -1,3 +1,5 @@
+* Update unit price of stock moves based on posted invoice lines
+
 Version 5.6.0 - 2020-05-04
 * Bug fixes (see mercurial logs for details)
 * Keep stock moves when crediting an invoice
diff -r 423979d039bc -r d36ed05253a0 __init__.py
--- a/__init__.py       Sat Jun 06 10:30:11 2020 +0100
+++ b/__init__.py       Wed Jun 24 23:10:46 2020 +0200
@@ -8,6 +8,7 @@
 
 def register():
     Pool.register(
+        account.Invoice,
         account.InvoiceLineStockMove,
         account.InvoiceLine,
         stock.Move,
diff -r 423979d039bc -r d36ed05253a0 account.py
--- a/account.py        Sat Jun 06 10:30:11 2020 +0100
+++ b/account.py        Wed Jun 24 23:10:46 2020 +0200
@@ -1,11 +1,26 @@
 # 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, If
 from trytond.transaction import Transaction
 
 
+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'
diff -r 423979d039bc -r d36ed05253a0 doc/index.rst
--- a/doc/index.rst     Sat Jun 06 10:30:11 2020 +0100
+++ b/doc/index.rst     Wed Jun 24 23:10:46 2020 +0200
@@ -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 with the average price of the
+posted invoice lines that are linked to it.
diff -r 423979d039bc -r d36ed05253a0 setup.py
--- a/setup.py  Sat Jun 06 10:30:11 2020 +0100
+++ b/setup.py  Wed Jun 24 23:10:46 2020 +0200
@@ -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,
     )
diff -r 423979d039bc -r d36ed05253a0 stock.py
--- a/stock.py  Sat Jun 06 10:30:11 2020 +0100
+++ b/stock.py  Wed Jun 24 23:10:46 2020 +0200
@@ -1,10 +1,14 @@
 # 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
+
 
 class Move(metaclass=PoolMeta):
     __name__ = 'stock.move'
@@ -14,6 +18,7 @@
         domain=[
             ('product.default_uom_category',
                 '=', Eval('product_uom_category', -1)),
+            ('type', '=', 'line'),
             ['OR',
                 ('invoice.type', 'in', Eval('invoice_types', [])),
                 ('invoice_type', 'in', Eval('invoice_types', [])),
@@ -63,6 +68,40 @@
             default.setdefault('invoice_lines', None)
         return super().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')
+        Currency = pool.get('currency.currency')
+        amount, quantity = 0, 0
+        for line in self.invoice_lines:
+            if line.invoice and line.invoice.state in {'posted', 'paid'}:
+                with Transaction().set_context(date=self.effective_date):
+                    amount += Currency.compute(
+                        line.invoice.currency, line.amount, self.currency)
+                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'
diff -r 423979d039bc -r d36ed05253a0 tests/scenario_account_invoice_stock.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/scenario_account_invoice_stock.rst  Wed Jun 24 23:10:46 2020 +0200
@@ -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')
diff -r 423979d039bc -r d36ed05253a0 tests/test_account_invoice_stock.py
--- a/tests/test_account_invoice_stock.py       Sat Jun 06 10:30:11 2020 +0100
+++ b/tests/test_account_invoice_stock.py       Wed Jun 24 23:10:46 2020 +0200
@@ -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

Reply via email to