Update cost of moves when recomputing product cost
issue8795 issue7271 review260381003
This commit is contained in:
@@ -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
@@ -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')
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user