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
This commit is contained in:
Cédric Krier committed 2020-06-24 23:10:46 +02:00
1 parent 6030d08e4c
commit 6c1626fb34
8 files changed
+211 -2

No files matched your search

+2
View File
@@ -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
+1
View File
@@ -8,6 +8,7 @@ from . import stock
def register():
Pool.register(
account.Invoice,
account.InvoiceLineStockMove,
account.InvoiceLine,
stock.Move,
+16 -1
View File
@@ -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'
+2
View File
@@ -3,3 +3,5 @@ Account Invoice Stock Module
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.
+2
View File
@@ -57,6 +57,7 @@ for dep in info.get('depends', []):
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 @@ setup(name=name,
""",
test_suite='tests',
test_loader='trytond.test_loader:Loader',
tests_require=tests_require,
)
+40 -1
View File
@@ -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 @@ class Move(metaclass=PoolMeta):
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 @@ class Move(metaclass=PoolMeta):
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'
+140
View File
@@ -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')
+8
View File
@@ -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 @@ def suite():
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