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:
1 parent
6030d08e4c
commit
6c1626fb34
8 files changed
+211
-2
No files matched your search
@@ -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
|
||||
|
||||
@@ -8,6 +8,7 @@ from . import stock
|
||||
|
||||
def register():
|
||||
Pool.register(
|
||||
account.Invoice,
|
||||
account.InvoiceLineStockMove,
|
||||
account.InvoiceLine,
|
||||
stock.Move,
|
||||
|
||||
+16
-1
@@ -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'
|
||||
|
||||
@@ -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.
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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'
|
||||
|
||||
@@ -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')
|
||||
@@ -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
|
||||
Reference in new issue
Block a user