Update cost of moves when recomputing product cost

issue8795
issue7271
review260381003
This commit is contained in:
Cédric Krier
2020-01-26 15:29:23 +01:00
parent 26e4b57969
commit d2daba9cb8
4 changed files with 332 additions and 33 deletions
+2
View File
@@ -1,3 +1,5 @@
* Update cost of moves when recomputing product cost
Version 5.4.0 - 2019-11-04
* Bug fixes (see mercurial logs for details)
+199 -33
View File
@@ -1,6 +1,9 @@
# 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 datetime
import datetime as dt
from decimal import Decimal
from trytond.config import config
from trytond.transaction import Transaction
from trytond.pool import Pool, PoolMeta
@@ -21,34 +24,26 @@ class Template(metaclass=PoolMeta):
class Product(metaclass=PoolMeta):
__name__ = 'product.product'
def _get_available_fifo_moves(self):
def _get_available_fifo_moves(self, date=None, offset=0, limit=None):
pool = Pool()
Move = pool.get('stock.move')
return Move.search([
('product', '=', self.id),
('state', '=', 'done'),
self._domain_moves_cost,
('fifo_quantity_available', '>', 0),
('to_location.type', '=', 'storage'),
('from_location.type', 'in', ['supplier', 'production']),
('to_location.type', '=', 'storage'),
], order=[('effective_date', 'DESC'), ('id', 'DESC')])
def _get_fifo_quantity(self):
pool = Pool()
Location = pool.get('stock.location')
domain = [
('product', '=', self.id),
self._domain_moves_cost(),
('from_location.type', 'in', ['supplier', 'production']),
('to_location.type', '=', 'storage'),
]
if not date:
domain.append(('fifo_quantity_available', '>', 0))
else:
domain.append(('effective_date', '<=', date))
return Move.search(
domain,
offset=offset, limit=limit,
order=[('effective_date', 'DESC'), ('id', 'DESC')])
locations = Location.search([
('type', '=', 'storage'),
])
stock_date_end = datetime.date.today()
location_ids = [l.id for l in locations]
with Transaction().set_context(
locations=location_ids,
stock_date_end=stock_date_end):
return self.__class__(self.id).quantity
def get_fifo_move(self, quantity=0.0):
def get_fifo_move(self, quantity=0.0, date=None):
'''
Return a list of (move, qty) where move is the move to be
consumed and qty is the quantity (in the product default uom)
@@ -58,13 +53,30 @@ class Product(metaclass=PoolMeta):
pool = Pool()
Uom = pool.get('product.uom')
avail_qty = self._get_fifo_quantity()
avail_qty = self._get_storage_quantity(date=date)
if date:
# On recomputation, we must pretend
# outgoing moves are not yet done.
avail_qty += quantity
fifo_moves = []
moves = self._get_available_fifo_moves()
for move in moves:
qty = Uom.compute_qty(move.uom,
move.fifo_quantity_available,
self.default_uom, round=False)
size = config.getint('cache', 'record')
def moves():
offset, limit = 0, size
while True:
moves = self._get_available_fifo_moves(
date=date, offset=offset, limit=limit)
if not moves:
break
for move in moves:
yield move
offset += size
for move in moves():
qty = move.fifo_quantity_available if not date else move.quantity
qty = Uom.compute_qty(
move.uom, qty, self.default_uom, round=False)
avail_qty -= qty
if avail_qty <= quantity:
@@ -78,5 +90,159 @@ class Product(metaclass=PoolMeta):
fifo_moves.reverse()
return fifo_moves
def recompute_cost_price_fifo(self):
return self.recompute_cost_price_average()
def recompute_cost_price_fifo(self, start=None):
pool = Pool()
Move = pool.get('stock.move')
Currency = pool.get('currency.currency')
Uom = pool.get('product.uom')
digits = self.__class__.cost_price.digits
domain = [
('product', '=', self.id),
self._domain_moves_cost(),
['OR',
[
('to_location.type', '=', 'storage'),
('from_location.type', '!=', 'storage'),
],
[
('from_location.type', '=', 'storage'),
('to_location.type', '!=', 'storage'),
],
],
]
if start:
domain.append(('effective_date', '>=', start))
moves = Move.search(
domain, order=[('effective_date', 'ASC'), ('id', 'ASC')])
cost_price = Decimal(0)
quantity = 0
if start:
domain.remove(('effective_date', '>=', start))
domain.append(('effective_date', '<', start))
domain.append(
('from_location.type', 'in', ['supplier', 'production']))
prev_moves = Move.search(
domain,
order=[('effective_date', 'DESC'), ('id', 'DESC')],
limit=1)
if prev_moves:
move, = prev_moves
cost_price = move.cost_price
quantity = self._get_storage_quantity(
date=start - dt.timedelta(days=1))
quantity = Decimal(str(quantity))
def in_move(move):
return (move.from_location.type in ['supplier', 'production']
or move.to_location.type == 'supplier')
def out_move(move):
return not in_move(move)
def compute_fifo_cost_price(quantity, date):
fifo_moves = self.get_fifo_move(
float(quantity),
date=current_moves[-1].effective_date)
cost_price = Decimal(0)
consumed_qty = 0
for move, move_qty in fifo_moves:
consumed_qty += move_qty
with Transaction().set_context(date=move.effective_date):
unit_price = Currency.compute(
move.currency, move.unit_price,
move.company.currency, round=False)
unit_price = Uom.compute_price(
move.uom, unit_price, move.product.default_uom)
cost_price += unit_price * Decimal(str(move_qty))
if consumed_qty:
return (cost_price / Decimal(str(consumed_qty))).quantize(
Decimal(str(10.0 ** -digits[1])))
current_moves = []
current_out_qty = 0
current_cost_price = cost_price
for move in moves:
if (current_moves
and current_moves[-1].effective_date
!= move.effective_date):
Move.write([
m for m in filter(in_move, current_moves)
if m.cost_price != current_cost_price],
dict(cost_price=current_cost_price))
out_moves = list(filter(out_move, current_moves))
if out_moves:
fifo_cost_price = compute_fifo_cost_price(
current_out_qty, current_moves[-1].effective_date)
if fifo_cost_price is None:
fifo_cost_price = current_cost_price
Move.write([
m for m in out_moves
if m.cost_price != fifo_cost_price],
dict(cost_price=fifo_cost_price))
if quantity:
cost_price = (
((current_cost_price * (
quantity + current_out_qty))
- (fifo_cost_price * current_out_qty))
/ quantity)
else:
cost_price = Decimal(0)
current_cost_price = cost_price.quantize(
Decimal(str(10.0 ** -digits[1])))
current_moves.clear()
current_out_qty = 0
current_moves.append(move)
qty = Uom.compute_qty(move.uom, move.quantity, self.default_uom)
qty = Decimal(str(qty))
if move.from_location.type == 'storage':
qty *= -1
if in_move(move):
with Transaction().set_context(date=move.effective_date):
unit_price = Currency.compute(
move.currency, move.unit_price,
move.company.currency, round=False)
unit_price = Uom.compute_price(
move.uom, unit_price, self.default_uom)
if quantity + qty > 0 and quantity >= 0:
cost_price = (
(cost_price * quantity) + (unit_price * qty)
) / (quantity + qty)
elif qty > 0:
cost_price = unit_price
current_cost_price = cost_price.quantize(
Decimal(str(10.0 ** -digits[1])))
else:
current_out_qty += -qty
quantity += qty
Move.write([
m for m in filter(in_move, current_moves)
if m.cost_price != current_cost_price],
dict(cost_price=current_cost_price))
out_moves = list(filter(out_move, current_moves))
if out_moves:
fifo_cost_price = compute_fifo_cost_price(
current_out_qty, current_moves[-1].effective_date)
if fifo_cost_price is None:
fifo_cost_price = current_cost_price
Move.write([
m for m in out_moves
if m.cost_price != fifo_cost_price],
dict(cost_price=fifo_cost_price))
if quantity:
cost_price = (
((current_cost_price * (
quantity + current_out_qty))
- (fifo_cost_price * current_out_qty))
/ quantity)
else:
cost_price = Decimal(0)
current_cost_price = cost_price.quantize(
Decimal(str(10.0 ** -digits[1])))
return current_cost_price
@@ -0,0 +1,126 @@
======================================
Product Cost FIFO Recompute Cost Price
======================================
Imports::
>>> import datetime as dt
>>> from dateutil.relativedelta import relativedelta
>>> 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
>>> today = dt.date.today()
Install stock Module::
>>> config = activate_modules('product_cost_fifo')
Create company::
>>> _ = create_company()
>>> company = get_company()
Create product::
>>> ProductUom = Model.get('product.uom')
>>> ProductTemplate = Model.get('product.template')
>>> unit, = ProductUom.find([('name', '=', 'Unit')])
>>> template = ProductTemplate()
>>> template.name = 'Product'
>>> template.default_uom = unit
>>> template.type = 'goods'
>>> template.list_price = Decimal('300')
>>> template.cost_price_method = 'fifo'
>>> product, = template.products
>>> product.cost_price = Decimal('80')
>>> template.save()
>>> product, = template.products
Get stock locations::
>>> Location = Model.get('stock.location')
>>> supplier_loc, = Location.find([('code', '=', 'SUP')])
>>> storage_loc, = Location.find([('code', '=', 'STO')])
>>> customer_loc, = Location.find([('code', '=', 'CUS')])
Create some moves::
>>> StockMove = Model.get('stock.move')
>>> StockMove(
... product=product,
... quantity=1,
... from_location=supplier_loc,
... to_location=storage_loc,
... unit_price=Decimal('100'),
... effective_date=today - dt.timedelta(days=2)).click('do')
>>> StockMove(
... product=product,
... quantity=2,
... from_location=supplier_loc,
... to_location=storage_loc,
... unit_price=Decimal('120'),
... effective_date=today - dt.timedelta(days=1)).click('do')
>>> StockMove(
... product=product,
... quantity=2,
... from_location=storage_loc,
... to_location=customer_loc,
... unit_price=Decimal('300'),
... effective_date=today - dt.timedelta(days=1)).click('do')
>>> StockMove(
... product=product,
... quantity=3,
... from_location=supplier_loc,
... to_location=storage_loc,
... unit_price=Decimal('100'),
... effective_date=today).click('do')
>>> StockMove(
... product=product,
... quantity=2,
... from_location=storage_loc,
... to_location=customer_loc,
... unit_price=Decimal('300'),
... effective_date=today).click('do')
>>> StockMove(
... product=product,
... quantity=1,
... from_location=storage_loc,
... to_location=customer_loc,
... unit_price=Decimal('300'),
... effective_date=today).click('do')
>>> [m.cost_price for m in StockMove.find([])]
[Decimal('100.0000'), Decimal('110.0000'), Decimal('105.0000'), Decimal('110.0000'), Decimal('113.3333'), Decimal('100.0000')]
>>> product.reload()
>>> product.cost_price
Decimal('100.0000')
Recompute cost price::
>>> recompute = Wizard('product.recompute_cost_price', [product])
>>> recompute.execute('recompute')
>>> [m.cost_price for m in StockMove.find([])]
[Decimal('106.6667'), Decimal('106.6667'), Decimal('105.0000'), Decimal('110.0000'), Decimal('113.3333'), Decimal('100.0000')]
>>> product.reload()
>>> product.cost_price
Decimal('99.9999')
Recompute cost price from a date::
>>> recompute = Wizard('product.recompute_cost_price', [product])
>>> recompute.form.from_ = today - dt.timedelta(days=1)
>>> recompute.execute('recompute')
>>> [m.cost_price for m in StockMove.find([])]
[Decimal('106.6667'), Decimal('106.6667'), Decimal('105.0000'), Decimal('110.0000'), Decimal('113.3333'), Decimal('100.0000')]
>>> product.reload()
>>> product.cost_price
Decimal('99.9999')
+5
View File
@@ -32,4 +32,9 @@ def suite():
tearDown=doctest_teardown, encoding='utf-8',
checker=doctest_checker,
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
suite.addTests(doctest.DocFileSuite(
'scenario_product_cost_fifo_recompute_cost_price.rst',
tearDown=doctest_teardown, encoding='utf-8',
checker=doctest_checker,
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
return suite