mirror of
https://github.com/NaN-tic/trytond-patches.git
synced 2023-12-14 06:03:03 +01:00
427 lines
16 KiB
Diff
427 lines
16 KiB
Diff
From b82c2659c6703f1f25dbeb4f698734e147483a8f Mon Sep 17 00:00:00 2001
|
|
From: =?UTF-8?q?=C3=80ngel=20=C3=80lvarez?= <angel@nan-tic.com>
|
|
Date: Wed, 7 Apr 2021 12:25:52 +0200
|
|
Subject: [PATCH] Update cost of moves when recomputing product cost issue8795
|
|
issue7271
|
|
|
|
---
|
|
product.py | 235 +++++++++++++++---
|
|
...product_cost_fifo_recompute_cost_price.rst | 126 ++++++++++
|
|
tests/test_product_cost_fifo.py | 5 +
|
|
3 files changed, 331 insertions(+), 35 deletions(-)
|
|
create mode 100644 tests/scenario_product_cost_fifo_recompute_cost_price.rst
|
|
|
|
diff --git a/product.py b/product.py
|
|
index 86a6faa..0c204bd 100644
|
|
--- a/trytond/trytond/modules/product_cost_fifo/product.py
|
|
+++ b/trytond/trytond/modules/product_cost_fifo/product.py
|
|
@@ -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,25 @@ 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', '!=', 'storage'),
|
|
- ('to_location.type', '=', 'storage'),
|
|
- ], order=[('effective_date', 'DESC'), ('id', 'DESC')])
|
|
-
|
|
- def _get_fifo_quantity(self):
|
|
- pool = Pool()
|
|
- Location = pool.get('stock.location')
|
|
-
|
|
- 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):
|
|
+ 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')])
|
|
+
|
|
+ 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 +52,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 +89,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
|
|
diff --git a/tests/scenario_product_cost_fifo_recompute_cost_price.rst b/tests/scenario_product_cost_fifo_recompute_cost_price.rst
|
|
new file mode 100644
|
|
index 0000000..f8b952f
|
|
--- /dev/null
|
|
+++ b/trytond/trytond/modules/product_cost_fifo/tests/scenario_product_cost_fifo_recompute_cost_price.rst
|
|
@@ -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')
|
|
diff --git a/tests/test_product_cost_fifo.py b/tests/test_product_cost_fifo.py
|
|
index 7b81f5e..5ee6fb6 100644
|
|
--- a/trytond/trytond/modules/product_cost_fifo/tests/test_product_cost_fifo.py
|
|
+++ b/trytond/trytond/modules/product_cost_fifo/tests/test_product_cost_fifo.py
|
|
@@ -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
|
|
--
|
|
2.25.1
|