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