add cost recalculation patches
This commit is contained in:
parent
e513bf349b
commit
4b42d9a3aa
|
@ -0,0 +1,18 @@
|
|||
diff --git a/production.py b/production.py
|
||||
index afdcdc3..85e6ec5 100644
|
||||
--- a/trytond/trytond/modules/production/production.py
|
||||
+++ b/trytond/trytond/modules/production/production.py
|
||||
@@ -467,6 +467,13 @@ class Production(Workflow, ModelSQL, ModelView):
|
||||
if move.production_input not in productions:
|
||||
cls.__queue__.set_cost([move.production_input])
|
||||
productions.add(move.production_input)
|
||||
+ pending_productions = cls.search([
|
||||
+ ('inputs', '=', None),
|
||||
+ ('state', '!=', 'cancel'),
|
||||
+ ])
|
||||
+ for production in pending_productions:
|
||||
+ cls.__queue__.set_cost([production])
|
||||
+ productions.add(production)
|
||||
Move.write(moves, {'production_cost_price_updated': False})
|
||||
|
||||
@classmethod
|
|
@ -1,13 +1,14 @@
|
|||
diff --git a/move.py b/move.py
|
||||
index 94fa17d..7e4d733 100644
|
||||
index 1b41353..85c89eb 100644
|
||||
--- a/trytond/trytond/modules/product_cost_fifo/move.py
|
||||
+++ b/trytond/trytond/modules/product_cost_fifo/move.py
|
||||
@@ -63,7 +63,7 @@ class Move(metaclass=PoolMeta):
|
||||
cost_price += move_unit_price * Decimal(str(move_qty))
|
||||
|
||||
@@ -105,6 +105,9 @@ class Move(metaclass=PoolMeta):
|
||||
move_qty = Uom.compute_qty(self.product.default_uom, move_qty,
|
||||
- move.uom, round=False)
|
||||
+ move.uom, round=True)
|
||||
move.uom, round=False)
|
||||
move.fifo_quantity = (move.fifo_quantity or 0.0) + move_qty
|
||||
+ # Due to float, the fifo quantity result can exceed the quantity.
|
||||
+ assert move.quantity >= move.fifo_quantity - move.uom.rounding
|
||||
+ move.fifo_quantity = min(move.fifo_quantity, move.quantity)
|
||||
to_save.append(move)
|
||||
if to_save:
|
||||
# TODO save in do method when product change
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
From d23677f089ff7e283e30a1aaa0e1ed2e42c31fa9 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?=C3=80ngel=20=C3=80lvarez?= <angel@nan-tic.com>
|
||||
Date: Thu, 8 Apr 2021 11:53:18 +0200
|
||||
Subject: [PATCH] Use original cost price for returned move
|
||||
|
||||
and factorize the cost price of move for cost computation
|
||||
|
||||
issue9440
|
||||
review327491003
|
||||
---
|
||||
move.py | 23 +++++------------------
|
||||
product.py | 23 ++---------------------
|
||||
2 files changed, 7 insertions(+), 39 deletions(-)
|
||||
|
||||
diff --git a/move.py b/move.py
|
||||
index 9e20a1d..423a4bd 100644
|
||||
--- a/trytond/trytond/modules/product_cost_fifo/move.py
|
||||
+++ b/trytond/trytond/modules/product_cost_fifo/move.py
|
||||
@@ -68,7 +68,6 @@ class Move(metaclass=PoolMeta):
|
||||
'''
|
||||
pool = Pool()
|
||||
Uom = pool.get('product.uom')
|
||||
- Currency = pool.get('currency.currency')
|
||||
|
||||
total_qty = Uom.compute_qty(self.uom, self.quantity,
|
||||
self.product.default_uom, round=False)
|
||||
@@ -81,16 +80,7 @@ class Move(metaclass=PoolMeta):
|
||||
to_save = []
|
||||
for move, move_qty in fifo_moves:
|
||||
consumed_qty += move_qty
|
||||
- if move.from_location.type in {'supplier', 'production'}:
|
||||
- with Transaction().set_context(date=move.effective_date):
|
||||
- move_unit_price = Currency.compute(
|
||||
- move.currency, move.unit_price,
|
||||
- self.company.currency, round=False)
|
||||
- move_unit_price = Uom.compute_price(
|
||||
- move.uom, move_unit_price, move.product.default_uom)
|
||||
- else:
|
||||
- move_unit_price = move.cost_price or 0
|
||||
- cost_price += move_unit_price * Decimal(str(move_qty))
|
||||
+ cost_price += move.get_cost_price() * Decimal(str(move_qty))
|
||||
|
||||
move_qty = Uom.compute_qty(self.product.default_uom, move_qty,
|
||||
move.uom, round=False)
|
||||
@@ -110,12 +100,9 @@ class Move(metaclass=PoolMeta):
|
||||
'cost_price', **self._cost_price_pattern)
|
||||
|
||||
# Compute average cost price
|
||||
- unit_price = self.unit_price
|
||||
- self.unit_price = Uom.compute_price(
|
||||
- self.product.default_uom, cost_price, self.uom)
|
||||
- average_cost_price = self._compute_product_cost_price('out')
|
||||
- self.unit_price = unit_price
|
||||
-
|
||||
+ average_cost_price = self._compute_product_cost_price(
|
||||
+ 'out', product_cost_price=cost_price)
|
||||
+
|
||||
if cost_price:
|
||||
digits = self.__class__.cost_price.digits
|
||||
cost_price = cost_price.quantize(
|
||||
@@ -126,7 +113,7 @@ class Move(metaclass=PoolMeta):
|
||||
|
||||
def _do(self):
|
||||
cost_price = super(Move, self)._do()
|
||||
- if (self.from_location.type in ('supplier', 'production')
|
||||
+ if (self.from_location.type != 'storage'
|
||||
and self.to_location.type == 'storage'
|
||||
and self.product.cost_price_method == 'fifo'):
|
||||
cost_price = self._compute_product_cost_price('in')
|
||||
diff --git a/product.py b/product.py
|
||||
index b7e25bb..13e7d95 100644
|
||||
--- a/trytond/trytond/modules/product_cost_fifo/product.py
|
||||
+++ b/trytond/trytond/modules/product_cost_fifo/product.py
|
||||
@@ -4,7 +4,6 @@ import datetime as dt
|
||||
from decimal import Decimal
|
||||
|
||||
from trytond.config import config
|
||||
-from trytond.transaction import Transaction
|
||||
from trytond.pool import Pool, PoolMeta
|
||||
|
||||
__all__ = ['Template', 'Product']
|
||||
@@ -92,7 +91,6 @@ class Product(metaclass=PoolMeta):
|
||||
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
|
||||
|
||||
@@ -148,16 +146,7 @@ class Product(metaclass=PoolMeta):
|
||||
consumed_qty = 0
|
||||
for move, move_qty in fifo_moves:
|
||||
consumed_qty += move_qty
|
||||
- if move.from_location.type in {'supplier', 'production'}:
|
||||
- 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)
|
||||
- else:
|
||||
- unit_price = move.cost_price or 0
|
||||
- cost_price += unit_price * Decimal(str(move_qty))
|
||||
+ cost_price += move.get_cost_price() * Decimal(str(move_qty))
|
||||
if consumed_qty:
|
||||
return (cost_price / Decimal(str(consumed_qty))).quantize(
|
||||
Decimal(str(10.0 ** -digits[1])))
|
||||
@@ -203,15 +192,7 @@ class Product(metaclass=PoolMeta):
|
||||
if move.from_location.type == 'storage':
|
||||
qty *= -1
|
||||
if in_move(move):
|
||||
- if move.from_location.type in {'supplier', 'production'}:
|
||||
- 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)
|
||||
- else:
|
||||
- unit_price = cost_price
|
||||
+ unit_price = move.get_cost_price(product_cost_price=cost_price)
|
||||
if quantity + qty > 0 and quantity >= 0:
|
||||
cost_price = (
|
||||
(cost_price * quantity) + (unit_price * qty)
|
||||
--
|
||||
2.25.1
|
|
@ -0,0 +1,52 @@
|
|||
From 017277e73dbe9120a983d4c3bf27a7bc3e5ee9e6 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?=C3=80ngel=20=C3=80lvarez?= <angel@nan-tic.com>
|
||||
Date: Thu, 8 Apr 2021 12:07:12 +0200
|
||||
Subject: [PATCH] product_cost_fifo: Do not update average cost price if the
|
||||
quantity is negative
|
||||
|
||||
This is the same test done in Move._compute_product_cost_price. When the stock
|
||||
quantity is below zero, the average cost price should not change.
|
||||
|
||||
issue9484
|
||||
---
|
||||
product.py | 11 +++++++----
|
||||
1 file changed, 7 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/product.py b/product.py
|
||||
index 13e7d95..415d833 100644
|
||||
--- a/trytond/trytond/modules/product_cost_fifo/product.py
|
||||
+++ b/trytond/trytond/modules/product_cost_fifo/product.py
|
||||
@@ -138,9 +138,7 @@ class Product(metaclass=PoolMeta):
|
||||
return move.from_location.type == 'storage'
|
||||
|
||||
def compute_fifo_cost_price(quantity, date):
|
||||
- fifo_moves = self.get_fifo_move(
|
||||
- float(quantity),
|
||||
- date=current_moves[-1].effective_date)
|
||||
+ fifo_moves = self.get_fifo_move(float(quantity), date=date)
|
||||
|
||||
cost_price = Decimal(0)
|
||||
consumed_qty = 0
|
||||
@@ -151,6 +149,11 @@ class Product(metaclass=PoolMeta):
|
||||
return (cost_price / Decimal(str(consumed_qty))).quantize(
|
||||
Decimal(str(10.0 ** -digits[1])))
|
||||
|
||||
+ # For each day, process the incoming moves first
|
||||
+ # in order to keep quantity positive where possible
|
||||
+ # We do not re-browse because we expect only small changes
|
||||
+ moves = sorted(moves, key=lambda m: (
|
||||
+ m.effective_date, out_move(m), m.id))
|
||||
current_moves = []
|
||||
current_out_qty = 0
|
||||
current_cost_price = cost_price
|
||||
@@ -173,7 +176,7 @@ class Product(metaclass=PoolMeta):
|
||||
m for m in out_moves
|
||||
if m.cost_price != fifo_cost_price],
|
||||
dict(cost_price=fifo_cost_price))
|
||||
- if quantity:
|
||||
+ if quantity > 0 and quantity + current_out_qty >= 0:
|
||||
cost_price = (
|
||||
((current_cost_price * (
|
||||
quantity + current_out_qty))
|
||||
--
|
||||
2.25.1
|
|
@ -0,0 +1,36 @@
|
|||
From 75cb0d7a6bbadbef4b133772454be2f33118db7f Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?=C3=80ngel=20=C3=80lvarez?= <angel@nan-tic.com>
|
||||
Date: Thu, 8 Apr 2021 12:11:17 +0200
|
||||
Subject: [PATCH] Enforce filling cost price of move
|
||||
|
||||
We set cost price only for outgoing or incoming moves.
|
||||
|
||||
issue9397
|
||||
---
|
||||
move.py | 4 ++--
|
||||
1 file changed, 2 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/move.py b/move.py
|
||||
index 423a4bd..f4a7b61 100644
|
||||
--- a/trytond/trytond/modules/product_cost_fifo/move.py
|
||||
+++ b/trytond/trytond/modules/product_cost_fifo/move.py
|
||||
@@ -102,7 +102,7 @@ class Move(metaclass=PoolMeta):
|
||||
# Compute average cost price
|
||||
average_cost_price = self._compute_product_cost_price(
|
||||
'out', product_cost_price=cost_price)
|
||||
-
|
||||
+
|
||||
if cost_price:
|
||||
digits = self.__class__.cost_price.digits
|
||||
cost_price = cost_price.quantize(
|
||||
@@ -126,7 +126,7 @@ class Move(metaclass=PoolMeta):
|
||||
and self.product.cost_price_method == 'fifo'):
|
||||
fifo_cost_price, cost_price = (
|
||||
self._update_fifo_out_product_cost_price())
|
||||
- if self.cost_price is None:
|
||||
+ if self.cost_price_required and self.cost_price is None:
|
||||
self.cost_price = fifo_cost_price
|
||||
return cost_price
|
||||
|
||||
--
|
||||
2.25.1
|
|
@ -0,0 +1,189 @@
|
|||
From eaae8d1ba4909042e276065d106c47e9117b2a13 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?=C3=80ngel=20=C3=80lvarez?= <angel@nan-tic.com>
|
||||
Date: Thu, 8 Apr 2021 10:40:42 +0200
|
||||
Subject: [PATCH] product_cost_fifo: Use all moves to compute FIFO
|
||||
|
||||
We must consider any incoming moves (ex: inventory) for the computation of the
|
||||
FIFO otherwise the back computation to find first in moves does not pick enough
|
||||
moves. As not all incoming moves have a unit price, we use the current cost
|
||||
price as fallback.
|
||||
Also in re-computation, the in or out move test should only rely on the storage
|
||||
location usage in order to properly compute the average cost.
|
||||
|
||||
issue9274
|
||||
|
||||
product_cost_fifo: Keep last cost price when quantity is zero
|
||||
|
||||
When there is no quantity in stock, we keep the last cost price instead of
|
||||
setting it to zero. This gives the same behavior between on move computation
|
||||
and on product recomputation.
|
||||
|
||||
issue9443
|
||||
---
|
||||
product.py | 41 +++++++++++--------
|
||||
...product_cost_fifo_recompute_cost_price.rst | 23 +++++++----
|
||||
2 files changed, 38 insertions(+), 26 deletions(-)
|
||||
|
||||
diff --git a/product.py b/product.py
|
||||
index 0c204bd..b7e25bb 100644
|
||||
--- a/trytond/trytond/modules/product_cost_fifo/product.py
|
||||
+++ b/trytond/trytond/modules/product_cost_fifo/product.py
|
||||
@@ -30,7 +30,7 @@ class Product(metaclass=PoolMeta):
|
||||
domain = [
|
||||
('product', '=', self.id),
|
||||
self._domain_moves_cost(),
|
||||
- ('from_location.type', 'in', ['supplier', 'production']),
|
||||
+ ('from_location.type', '!=', 'storage'),
|
||||
('to_location.type', '=', 'storage'),
|
||||
]
|
||||
if not date:
|
||||
@@ -134,11 +134,10 @@ class Product(metaclass=PoolMeta):
|
||||
quantity = Decimal(str(quantity))
|
||||
|
||||
def in_move(move):
|
||||
- return (move.from_location.type in ['supplier', 'production']
|
||||
- or move.to_location.type == 'supplier')
|
||||
+ return move.to_location.type == 'storage'
|
||||
|
||||
def out_move(move):
|
||||
- return not in_move(move)
|
||||
+ return move.from_location.type == 'storage'
|
||||
|
||||
def compute_fifo_cost_price(quantity, date):
|
||||
fifo_moves = self.get_fifo_move(
|
||||
@@ -149,12 +148,15 @@ class Product(metaclass=PoolMeta):
|
||||
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)
|
||||
+ if move.from_location.type in {'supplier', 'production'}:
|
||||
+ 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)
|
||||
+ else:
|
||||
+ unit_price = move.cost_price or 0
|
||||
cost_price += unit_price * Decimal(str(move_qty))
|
||||
if consumed_qty:
|
||||
return (cost_price / Decimal(str(consumed_qty))).quantize(
|
||||
@@ -189,7 +191,7 @@ class Product(metaclass=PoolMeta):
|
||||
- (fifo_cost_price * current_out_qty))
|
||||
/ quantity)
|
||||
else:
|
||||
- cost_price = Decimal(0)
|
||||
+ cost_price = current_cost_price
|
||||
current_cost_price = cost_price.quantize(
|
||||
Decimal(str(10.0 ** -digits[1])))
|
||||
current_moves.clear()
|
||||
@@ -201,12 +203,15 @@ class Product(metaclass=PoolMeta):
|
||||
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 move.from_location.type in {'supplier', 'production'}:
|
||||
+ 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)
|
||||
+ else:
|
||||
+ unit_price = cost_price
|
||||
if quantity + qty > 0 and quantity >= 0:
|
||||
cost_price = (
|
||||
(cost_price * quantity) + (unit_price * qty)
|
||||
@@ -215,7 +220,7 @@ class Product(metaclass=PoolMeta):
|
||||
cost_price = unit_price
|
||||
current_cost_price = cost_price.quantize(
|
||||
Decimal(str(10.0 ** -digits[1])))
|
||||
- else:
|
||||
+ elif out_move(move):
|
||||
current_out_qty += -qty
|
||||
quantity += qty
|
||||
|
||||
diff --git a/tests/scenario_product_cost_fifo_recompute_cost_price.rst b/tests/scenario_product_cost_fifo_recompute_cost_price.rst
|
||||
index f8b952f..ff76174 100644
|
||||
--- a/trytond/trytond/modules/product_cost_fifo/tests/scenario_product_cost_fifo_recompute_cost_price.rst
|
||||
+++ b/trytond/trytond/modules/product_cost_fifo/tests/scenario_product_cost_fifo_recompute_cost_price.rst
|
||||
@@ -45,6 +45,7 @@ Get stock locations::
|
||||
>>> supplier_loc, = Location.find([('code', '=', 'SUP')])
|
||||
>>> storage_loc, = Location.find([('code', '=', 'STO')])
|
||||
>>> customer_loc, = Location.find([('code', '=', 'CUS')])
|
||||
+ >>> lost_found, = Location.find([('name', '=', "Lost and Found")])
|
||||
|
||||
Create some moves::
|
||||
|
||||
@@ -65,6 +66,12 @@ Create some moves::
|
||||
... effective_date=today - dt.timedelta(days=1)).click('do')
|
||||
>>> StockMove(
|
||||
... product=product,
|
||||
+ ... quantity=1,
|
||||
+ ... from_location=lost_found,
|
||||
+ ... to_location=storage_loc,
|
||||
+ ... effective_date=today - dt.timedelta(days=1)).click('do')
|
||||
+ >>> StockMove(
|
||||
+ ... product=product,
|
||||
... quantity=2,
|
||||
... from_location=storage_loc,
|
||||
... to_location=customer_loc,
|
||||
@@ -88,17 +95,16 @@ Create some moves::
|
||||
... product=product,
|
||||
... quantity=1,
|
||||
... from_location=storage_loc,
|
||||
- ... to_location=customer_loc,
|
||||
- ... unit_price=Decimal('300'),
|
||||
+ ... to_location=lost_found,
|
||||
... 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')]
|
||||
+ [Decimal('100.0000'), Decimal('116.6666'), Decimal('106.6666'), Decimal('110.0000'), Decimal('113.3333'), Decimal('113.3333'), Decimal('100.0000')]
|
||||
|
||||
>>> product.reload()
|
||||
>>> product.cost_price
|
||||
- Decimal('100.0000')
|
||||
+ Decimal('99.9998')
|
||||
|
||||
Recompute cost price::
|
||||
|
||||
@@ -106,11 +112,12 @@ Recompute cost price::
|
||||
>>> 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')]
|
||||
+ [Decimal('111.1111'), Decimal('111.1111'), Decimal('106.6666'), Decimal('110.0000'), Decimal('113.3333'), Decimal('113.3333'), Decimal('100.0000')]
|
||||
|
||||
>>> product.reload()
|
||||
>>> product.cost_price
|
||||
- Decimal('99.9999')
|
||||
+ Decimal('99.9998')
|
||||
+
|
||||
|
||||
Recompute cost price from a date::
|
||||
|
||||
@@ -119,8 +126,8 @@ Recompute cost price from a date::
|
||||
>>> 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')]
|
||||
+ [Decimal('111.1111'), Decimal('111.1111'), Decimal('106.6666'), Decimal('110.0000'), Decimal('113.3333'), Decimal('113.3333'), Decimal('100.0000')]
|
||||
|
||||
>>> product.reload()
|
||||
>>> product.cost_price
|
||||
- Decimal('99.9999')
|
||||
+ Decimal('99.9998')
|
||||
--
|
||||
2.25.1
|
|
@ -0,0 +1,195 @@
|
|||
# HG changeset patch
|
||||
# User Cédric Krier <cedric.krier@b2ck.com>
|
||||
Use all moves to compute FIFO
|
||||
|
||||
product_cost_fifo: Use all moves to compute FIFO
|
||||
|
||||
We must consider any incoming moves (ex: inventory) for the computation of the
|
||||
FIFO otherwise the back computation to find first in moves does not pick enough
|
||||
moves. As not all incoming moves have a unit price, we use the current cost
|
||||
price as fallback.
|
||||
Also in re-computation, the in or out move test should only rely on the storage
|
||||
location usage in order to properly compute the average cost.
|
||||
|
||||
issue9274
|
||||
|
||||
review321471002
|
||||
|
||||
Index: move.py
|
||||
===================================================================
|
||||
--- a/trytond/trytond/modules/product_cost_fifo/move.py
|
||||
+++ b/trytond/trytond/modules/product_cost_fifo/move.py
|
||||
@@ -81,13 +81,15 @@
|
||||
to_save = []
|
||||
for move, move_qty in fifo_moves:
|
||||
consumed_qty += move_qty
|
||||
-
|
||||
- with Transaction().set_context(date=move.effective_date):
|
||||
- move_unit_price = Currency.compute(
|
||||
- move.currency, move.unit_price,
|
||||
- self.company.currency, round=False)
|
||||
- move_unit_price = Uom.compute_price(move.uom, move_unit_price,
|
||||
- move.product.default_uom)
|
||||
+ if move.from_location.type in {'supplier', 'production'}:
|
||||
+ with Transaction().set_context(date=move.effective_date):
|
||||
+ move_unit_price = Currency.compute(
|
||||
+ move.currency, move.unit_price,
|
||||
+ self.company.currency, round=False)
|
||||
+ move_unit_price = Uom.compute_price(
|
||||
+ move.uom, move_unit_price, move.product.default_uom)
|
||||
+ else:
|
||||
+ move_unit_price = move.cost_price or 0
|
||||
cost_price += move_unit_price * Decimal(str(move_qty))
|
||||
|
||||
move_qty = Uom.compute_qty(self.product.default_uom, move_qty,
|
||||
Index: product.py
|
||||
===================================================================
|
||||
--- a/trytond/trytond/modules/product_cost_fifo/product.py
|
||||
+++ b/trytond/trytond/modules/product_cost_fifo/product.py
|
||||
@@ -31,7 +31,7 @@
|
||||
domain = [
|
||||
('product', '=', self.id),
|
||||
self._domain_moves_cost(),
|
||||
- ('from_location.type', 'in', ['supplier', 'production']),
|
||||
+ ('from_location.type', '!=', 'storage'),
|
||||
('to_location.type', '=', 'storage'),
|
||||
]
|
||||
if not date:
|
||||
@@ -137,11 +137,10 @@
|
||||
quantity = Decimal(str(quantity))
|
||||
|
||||
def in_move(move):
|
||||
- return (move.from_location.type in ['supplier', 'production']
|
||||
- or move.to_location.type == 'supplier')
|
||||
+ return move.to_location.type == 'storage'
|
||||
|
||||
def out_move(move):
|
||||
- return not in_move(move)
|
||||
+ return move.from_location.type == 'storage'
|
||||
|
||||
def compute_fifo_cost_price(quantity, date):
|
||||
fifo_moves = self.get_fifo_move(
|
||||
@@ -152,12 +151,15 @@
|
||||
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)
|
||||
+ if move.from_location.type in {'supplier', 'production'}:
|
||||
+ 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)
|
||||
+ else:
|
||||
+ unit_price = move.cost_price or 0
|
||||
cost_price += unit_price * Decimal(str(move_qty))
|
||||
if consumed_qty:
|
||||
return round_price(cost_price / Decimal(str(consumed_qty)))
|
||||
@@ -204,12 +206,15 @@
|
||||
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 move.from_location.type in {'supplier', 'production'}:
|
||||
+ 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)
|
||||
+ else:
|
||||
+ unit_price = cost_price
|
||||
if quantity + qty > 0 and quantity >= 0:
|
||||
cost_price = (
|
||||
(cost_price * quantity) + (unit_price * qty)
|
||||
@@ -217,7 +222,7 @@
|
||||
elif qty > 0:
|
||||
cost_price = unit_price
|
||||
current_cost_price = round_price(cost_price)
|
||||
- else:
|
||||
+ elif out_move(move):
|
||||
current_out_qty += -qty
|
||||
quantity += qty
|
||||
|
||||
Index: tests/scenario_product_cost_fifo_recompute_cost_price.rst
|
||||
===================================================================
|
||||
--- a/trytond/trytond/modules/product_cost_fifo/tests/scenario_product_cost_fifo_recompute_cost_price.rst
|
||||
+++ b/trytond/trytond/modules/product_cost_fifo/tests/scenario_product_cost_fifo_recompute_cost_price.rst
|
||||
@@ -45,6 +45,7 @@
|
||||
>>> supplier_loc, = Location.find([('code', '=', 'SUP')])
|
||||
>>> storage_loc, = Location.find([('code', '=', 'STO')])
|
||||
>>> customer_loc, = Location.find([('code', '=', 'CUS')])
|
||||
+ >>> lost_found, = Location.find([('name', '=', "Lost and Found")])
|
||||
|
||||
Create some moves::
|
||||
|
||||
@@ -65,6 +66,12 @@
|
||||
... effective_date=today - dt.timedelta(days=1)).click('do')
|
||||
>>> StockMove(
|
||||
... product=product,
|
||||
+ ... quantity=1,
|
||||
+ ... from_location=lost_found,
|
||||
+ ... to_location=storage_loc,
|
||||
+ ... effective_date=today - dt.timedelta(days=1)).click('do')
|
||||
+ >>> StockMove(
|
||||
+ ... product=product,
|
||||
... quantity=2,
|
||||
... from_location=storage_loc,
|
||||
... to_location=customer_loc,
|
||||
@@ -88,17 +95,16 @@
|
||||
... product=product,
|
||||
... quantity=1,
|
||||
... from_location=storage_loc,
|
||||
- ... to_location=customer_loc,
|
||||
- ... unit_price=Decimal('300'),
|
||||
+ ... to_location=lost_found,
|
||||
... 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')]
|
||||
+ [Decimal('100.0000'), Decimal('116.6666'), Decimal('106.6666'), Decimal('110.0000'), Decimal('113.3333'), Decimal('113.3333'), Decimal('100.0000')]
|
||||
|
||||
>>> product.reload()
|
||||
>>> product.cost_price
|
||||
- Decimal('100.0000')
|
||||
+ Decimal('99.9998')
|
||||
|
||||
Recompute cost price::
|
||||
|
||||
@@ -106,11 +112,11 @@
|
||||
>>> 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')]
|
||||
+ [Decimal('111.1111'), Decimal('111.1111'), Decimal('106.6666'), Decimal('110.0000'), Decimal('113.3333'), Decimal('113.3333'), Decimal('100.0000')]
|
||||
|
||||
>>> product.reload()
|
||||
>>> product.cost_price
|
||||
- Decimal('99.9998')
|
||||
+ Decimal('100.0000')
|
||||
|
||||
Recompute cost price from a date::
|
||||
|
||||
@@ -119,8 +125,8 @@
|
||||
>>> 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')]
|
||||
+ [Decimal('111.1111'), Decimal('111.1111'), Decimal('106.6666'), Decimal('110.0000'), Decimal('113.3333'), Decimal('113.3333'), Decimal('100.0000')]
|
||||
|
||||
>>> product.reload()
|
||||
>>> product.cost_price
|
||||
- Decimal('99.9998')
|
||||
+ Decimal('100.0000')
|
|
@ -0,0 +1,136 @@
|
|||
From a5edc544456d361175a4de1ed2fc12948cd67118 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?=C3=80ngel=20=C3=80lvarez?= <angel@nan-tic.com>
|
||||
Date: Thu, 8 Apr 2021 09:15:58 +0200
|
||||
Subject: [PATCH] Keep cost of unused input products
|
||||
|
||||
We set the average cost price of the input products as the unit price of the
|
||||
output of the same product. This way the cost price computation for this
|
||||
product does not change.
|
||||
|
||||
issue9637
|
||||
review316051002
|
||||
---
|
||||
production.py | 85 +++++++++++++++++++++++++++++++++++++++++++--------
|
||||
1 file changed, 73 insertions(+), 12 deletions(-)
|
||||
|
||||
diff --git a/production.py b/production.py
|
||||
index 2bd52d1..13ec112 100644
|
||||
--- a/trytond/trytond/modules/production/production.py
|
||||
+++ b/trytond/trytond/modules/production/production.py
|
||||
@@ -1,7 +1,7 @@
|
||||
# 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 decimal import Decimal
|
||||
-
|
||||
+from collections import defaultdict
|
||||
from sql import Null
|
||||
|
||||
from trytond.i18n import gettext
|
||||
@@ -21,6 +21,12 @@ BOM_CHANGES = ['bom', 'product', 'quantity', 'uom', 'warehouse', 'location',
|
||||
'company', 'inputs', 'outputs']
|
||||
|
||||
|
||||
+
|
||||
+def round_price(value, rounding=None):
|
||||
+ "Round price using the price digits"
|
||||
+ return value.quantize(
|
||||
+ Decimal(1) / 10 ** price_digits[1], rounding=rounding)
|
||||
+
|
||||
class Production(Workflow, ModelSQL, ModelView):
|
||||
"Production"
|
||||
__name__ = 'production'
|
||||
@@ -461,26 +467,81 @@ class Production(Workflow, ModelSQL, ModelView):
|
||||
productions.add(move.production_input)
|
||||
Move.write(moves, {'production_cost_price_updated': False})
|
||||
|
||||
+ @property
|
||||
+ def _list_price_context(self):
|
||||
+ return {
|
||||
+ 'company': self.company.id,
|
||||
+ }
|
||||
+
|
||||
@classmethod
|
||||
def set_cost(cls, productions):
|
||||
pool = Pool()
|
||||
Uom = pool.get('product.uom')
|
||||
Move = pool.get('stock.move')
|
||||
|
||||
- digits = Move.unit_price.digits
|
||||
- digit = Decimal(str(10 ** -digits[1]))
|
||||
moves = []
|
||||
for production in productions:
|
||||
- if not production.quantity or not production.uom:
|
||||
- continue
|
||||
- if production.company.currency.is_zero(
|
||||
- production.cost - production.output_cost):
|
||||
- continue
|
||||
- unit_price = production.cost / Decimal(str(production.quantity))
|
||||
+ sum_ = Decimal(0)
|
||||
+ prices = {}
|
||||
+ cost = production.cost
|
||||
+
|
||||
+ input_quantities = defaultdict(Decimal)
|
||||
+ input_costs = defaultdict(Decimal)
|
||||
+ for input_ in production.inputs:
|
||||
+ if input_.cost_price is not None:
|
||||
+ cost_price = input_.cost_price
|
||||
+ else:
|
||||
+ cost_price = input_.product.cost_price
|
||||
+ input_quantities[input_.product] += (
|
||||
+ Decimal(str(input_.internal_quantity)))
|
||||
+ input_costs[input_.product] += (
|
||||
+ Decimal(str(input_.internal_quantity)) * cost_price)
|
||||
+ outputs = []
|
||||
for output in production.outputs:
|
||||
- if output.product == production.product:
|
||||
- output.unit_price = Uom.compute_price(
|
||||
- production.uom, unit_price, output.uom).quantize(digit)
|
||||
+ product = output.product
|
||||
+ if input_quantities.get(output.product):
|
||||
+ cost_price = (
|
||||
+ input_costs[product] / input_quantities[product])
|
||||
+ unit_price = round_price(Uom.compute_price(
|
||||
+ product.default_uom, cost_price, output.uom))
|
||||
+ if output.unit_price != unit_price:
|
||||
+ output.unit_price = unit_price
|
||||
+ moves.append(output)
|
||||
+ cost -= min(
|
||||
+ unit_price * Decimal(str(output.quantity)), cost)
|
||||
+ else:
|
||||
+ outputs.append(output)
|
||||
+
|
||||
+ for output in outputs:
|
||||
+ product = output.product
|
||||
+ with Transaction().set_context(production._list_price_context):
|
||||
+ list_price = product.list_price_used
|
||||
+ product_price = (Decimal(str(output.quantity))
|
||||
+ * Uom.compute_price(
|
||||
+ product.default_uom, list_price, output.uom))
|
||||
+ prices[output] = product_price
|
||||
+ sum_ += product_price
|
||||
+
|
||||
+ if not sum_ and production.product:
|
||||
+ prices.clear()
|
||||
+ for output in outputs:
|
||||
+ if output.product == production.product:
|
||||
+ quantity = Uom.compute_qty(
|
||||
+ output.uom, output.quantity,
|
||||
+ output.product.default_uom, round=False)
|
||||
+ quantity = Decimal(str(quantity))
|
||||
+ prices[output] = quantity
|
||||
+ sum_ += quantity
|
||||
+
|
||||
+ for output in outputs:
|
||||
+ if sum_:
|
||||
+ ratio = prices.get(output, 0) / sum_
|
||||
+ else:
|
||||
+ ratio = Decimal(1) / len(outputs)
|
||||
+ quantity = Decimal(str(output.quantity))
|
||||
+ unit_price = round_price(cost * ratio / quantity)
|
||||
+ if output.unit_price != unit_price:
|
||||
+ output.unit_price = unit_price
|
||||
moves.append(output)
|
||||
Move.save(moves)
|
||||
|
||||
--
|
||||
2.25.1
|
|
@ -0,0 +1,263 @@
|
|||
From cda3ebad04807a8a059ffbe5e57082b0c92fb7b0 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:24:55 +0200
|
||||
Subject: [PATCH] Update cost of moves when recomputing product cost
|
||||
|
||||
issue8795
|
||||
issue7271
|
||||
---
|
||||
__init__.py | 2 +
|
||||
ir.py | 14 ++++
|
||||
production.py | 16 ++++
|
||||
production.xml | 5 ++
|
||||
stock.py | 21 +++++
|
||||
tests/scenario_production_set_cost.rst | 105 +++++++++++++++++++++++++
|
||||
tests/test_production.py | 4 +
|
||||
7 files changed, 167 insertions(+)
|
||||
create mode 100644 ir.py
|
||||
create mode 100644 tests/scenario_production_set_cost.rst
|
||||
|
||||
diff --git a/__init__.py b/__init__.py
|
||||
index b8909ab..94292a3 100644
|
||||
--- a/trytond/trytond/modules/production/__init__.py
|
||||
+++ b/trytond/trytond/modules/production/__init__.py
|
||||
@@ -7,6 +7,7 @@ from .bom import *
|
||||
from .product import *
|
||||
from .production import *
|
||||
from .stock import *
|
||||
+from . import ir
|
||||
|
||||
|
||||
def register():
|
||||
@@ -27,6 +28,7 @@ def register():
|
||||
ProductionLeadTime,
|
||||
Location,
|
||||
Move,
|
||||
+ ir.Cron,
|
||||
module='production', type_='model')
|
||||
Pool.register(
|
||||
Assign,
|
||||
diff --git a/ir.py b/ir.py
|
||||
new file mode 100644
|
||||
index 0000000..3ccc082
|
||||
--- a/trytond/trytond/modules/production/ir.py
|
||||
+++ b/trytond/trytond/modules/production/ir.py
|
||||
@@ -0,0 +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.pool import PoolMeta
|
||||
+
|
||||
+
|
||||
+class Cron(metaclass=PoolMeta):
|
||||
+ __name__ = 'ir.cron'
|
||||
+
|
||||
+ @classmethod
|
||||
+ def __setup__(cls):
|
||||
+ super().__setup__()
|
||||
+ cls.method.selection.extend([
|
||||
+ ('production|set_cost_from_moves', "Set Cost from Moves"),
|
||||
+ ])
|
||||
diff --git a/production.py b/production.py
|
||||
index acd7a35..2bd52d1 100644
|
||||
--- a/trytond/trytond/modules/production/production.py
|
||||
+++ b/trytond/trytond/modules/production/production.py
|
||||
@@ -445,6 +445,22 @@ class Production(Workflow, ModelSQL, ModelView):
|
||||
move.save()
|
||||
self._set_move_planned_date()
|
||||
|
||||
+ @classmethod
|
||||
+ def set_cost_from_moves(cls):
|
||||
+ pool = Pool()
|
||||
+ Move = pool.get('stock.move')
|
||||
+ productions = set()
|
||||
+ moves = Move.search([
|
||||
+ ('production_cost_price_updated', '=', True),
|
||||
+ ('production_input', '!=', None),
|
||||
+ ],
|
||||
+ order=[('effective_date', 'ASC')])
|
||||
+ for move in moves:
|
||||
+ if move.production_input not in productions:
|
||||
+ cls.__queue__.set_cost([move.production_input])
|
||||
+ productions.add(move.production_input)
|
||||
+ Move.write(moves, {'production_cost_price_updated': False})
|
||||
+
|
||||
@classmethod
|
||||
def set_cost(cls, productions):
|
||||
pool = Pool()
|
||||
diff --git a/production.xml b/production.xml
|
||||
index e67fbb5..2f46f8c 100644
|
||||
--- a/trytond/trytond/modules/production/production.xml
|
||||
+++ b/trytond/trytond/modules/production/production.xml
|
||||
@@ -305,5 +305,10 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="name">assign_failed_form</field>
|
||||
</record>
|
||||
|
||||
+ <record model="ir.cron" id="cron_set_cost_from_moves">
|
||||
+ <field name="method">production|set_cost_from_moves</field>
|
||||
+ <field name="interval_number" eval="1"/>
|
||||
+ <field name="interval_type">days</field>
|
||||
+ </record>
|
||||
</data>
|
||||
</tryton>
|
||||
diff --git a/stock.py b/stock.py
|
||||
index 092cdba..0b8bbc3 100644
|
||||
--- a/trytond/trytond/modules/production/stock.py
|
||||
+++ b/trytond/trytond/modules/production/stock.py
|
||||
@@ -32,6 +32,12 @@ class Move(metaclass=PoolMeta):
|
||||
readonly=True, select=True, ondelete='CASCADE',
|
||||
domain=[('company', '=', Eval('company'))],
|
||||
depends=['company'])
|
||||
+ production_cost_price_updated = fields.Boolean(
|
||||
+ "Cost Price Updated", readonly=True,
|
||||
+ states={
|
||||
+ 'invisible': ~Eval('production_input') & (Eval('state') == 'done'),
|
||||
+ },
|
||||
+ depends=['production_input', 'state'])
|
||||
|
||||
def set_effective_date(self):
|
||||
if not self.effective_date and self.production_input:
|
||||
@@ -39,3 +45,18 @@ class Move(metaclass=PoolMeta):
|
||||
if not self.effective_date and self.production_output:
|
||||
self.effective_date = self.production_output.effective_date
|
||||
super(Move, self).set_effective_date()
|
||||
+
|
||||
+ @classmethod
|
||||
+ def write(cls, *args):
|
||||
+ super().write(*args)
|
||||
+ cost_price_update = []
|
||||
+ actions = iter(args)
|
||||
+ for moves, values in zip(actions, actions):
|
||||
+ for move in moves:
|
||||
+ if (move.state == 'done'
|
||||
+ and move.production_input
|
||||
+ and 'cost_price' in values):
|
||||
+ cost_price_update.append(move)
|
||||
+ if cost_price_update:
|
||||
+ cls.write(
|
||||
+ cost_price_update, {'production_cost_price_updated': True})
|
||||
diff --git a/tests/scenario_production_set_cost.rst b/tests/scenario_production_set_cost.rst
|
||||
new file mode 100644
|
||||
index 0000000..0117fba
|
||||
--- /dev/null
|
||||
+++ b/trytond/trytond/modules/production/tests/scenario_production_set_cost.rst
|
||||
@@ -0,0 +1,105 @@
|
||||
+===================
|
||||
+Production Set Cost
|
||||
+===================
|
||||
+
|
||||
+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
|
||||
+
|
||||
+Install production Module::
|
||||
+
|
||||
+ >>> config = activate_modules('production')
|
||||
+
|
||||
+Create company::
|
||||
+
|
||||
+ >>> _ = create_company()
|
||||
+ >>> company = get_company()
|
||||
+
|
||||
+Create main 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.producible = True
|
||||
+ >>> template.list_price = Decimal(20)
|
||||
+ >>> template.save()
|
||||
+ >>> product, = template.products
|
||||
+
|
||||
+Create component::
|
||||
+
|
||||
+ >>> template = ProductTemplate()
|
||||
+ >>> template.name = 'component'
|
||||
+ >>> template.default_uom = unit
|
||||
+ >>> template.type = 'goods'
|
||||
+ >>> template.save()
|
||||
+ >>> component, = template.products
|
||||
+ >>> component.cost_price = Decimal(5)
|
||||
+ >>> component.save()
|
||||
+
|
||||
+Create Bill of Material::
|
||||
+
|
||||
+ >>> BOM = Model.get('production.bom')
|
||||
+ >>> bom = BOM(name='product')
|
||||
+ >>> input = bom.inputs.new()
|
||||
+ >>> input.product = component
|
||||
+ >>> input.quantity = 2
|
||||
+ >>> output = bom.outputs.new()
|
||||
+ >>> output.product = product
|
||||
+ >>> output.quantity = 1
|
||||
+ >>> bom.save()
|
||||
+
|
||||
+Make a production::
|
||||
+
|
||||
+ >>> Production = Model.get('production')
|
||||
+ >>> production = Production()
|
||||
+ >>> production.product = product
|
||||
+ >>> production.bom = bom
|
||||
+ >>> production.quantity = 2
|
||||
+ >>> production.click('wait')
|
||||
+ >>> production.click('assign_force')
|
||||
+ >>> production.click('run')
|
||||
+ >>> production.click('done')
|
||||
+
|
||||
+Check output price::
|
||||
+
|
||||
+ >>> production.cost
|
||||
+ Decimal('20.0000')
|
||||
+ >>> output, = production.outputs
|
||||
+ >>> output.unit_price
|
||||
+ Decimal('10.0000')
|
||||
+
|
||||
+
|
||||
+Change cost of input::
|
||||
+
|
||||
+ >>> Move = Model.get('stock.move')
|
||||
+ >>> input, = production.inputs
|
||||
+ >>> Move.write([input], {'cost_price': Decimal(6)}, config.context)
|
||||
+ >>> input.reload()
|
||||
+ >>> bool(input.production_cost_price_updated)
|
||||
+ True
|
||||
+
|
||||
+Launch cron task::
|
||||
+
|
||||
+ >>> Cron = Model.get('ir.cron')
|
||||
+ >>> Company = Model.get('company.company')
|
||||
+ >>> cron_set_cost, = Cron.find([
|
||||
+ ... ('method', '=', 'production|set_cost_from_moves'),
|
||||
+ ... ])
|
||||
+ >>> cron_set_cost.companies.append(Company(company.id))
|
||||
+ >>> cron_set_cost.click('run_once')
|
||||
+
|
||||
+ >>> output.reload()
|
||||
+ >>> output.unit_price
|
||||
+ Decimal('12.0000')
|
||||
+ >>> input.reload()
|
||||
+ >>> bool(input.production_cost_price_updated)
|
||||
+ False
|
||||
diff --git a/tests/test_production.py b/tests/test_production.py
|
||||
index 2bcf2ec..e9f596c 100644
|
||||
--- a/trytond/trytond/modules/production/tests/test_production.py
|
||||
+++ b/trytond/trytond/modules/production/tests/test_production.py
|
||||
@@ -54,4 +54,8 @@ def suite():
|
||||
tearDown=doctest_teardown, encoding='utf-8',
|
||||
checker=doctest_checker,
|
||||
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
|
||||
+ suite.addTests(doctest.DocFileSuite('scenario_production_set_cost.rst',
|
||||
+ tearDown=doctest_teardown, encoding='utf-8',
|
||||
+ checker=doctest_checker,
|
||||
+ optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
|
||||
return suite
|
||||
--
|
||||
2.25.1
|
|
@ -0,0 +1,427 @@
|
|||
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
|
|
@ -0,0 +1,73 @@
|
|||
From f3cddba895305b7ecef3c07fb5db6c356a7bc8e2 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?=C3=80ngel=20=C3=80lvarez?= <angel@nan-tic.com>
|
||||
Date: Thu, 8 Apr 2021 11:56:28 +0200
|
||||
Subject: [PATCH] Use original cost price for returned move
|
||||
|
||||
and factorize the cost price of move for cost computation
|
||||
|
||||
issue9440
|
||||
review327491003
|
||||
---
|
||||
stock.py | 34 ++++++++++++++++++++++++++++++++++
|
||||
1 file changed, 34 insertions(+)
|
||||
|
||||
diff --git a/stock.py b/stock.py
|
||||
index 8d34c30..28f1116 100644
|
||||
--- a/trytond/trytond/modules/sale/stock.py
|
||||
+++ b/trytond/trytond/modules/sale/stock.py
|
||||
@@ -1,5 +1,6 @@
|
||||
# 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 decimal import Decimal
|
||||
from functools import wraps
|
||||
|
||||
from trytond.i18n import gettext
|
||||
@@ -11,6 +12,12 @@ from trytond.pool import Pool, PoolMeta
|
||||
__all__ = ['ShipmentOut', 'ShipmentOutReturn', 'Move']
|
||||
|
||||
|
||||
+def round_price(value, rounding=None):
|
||||
+ "Round price using the price digits"
|
||||
+ return value.quantize(
|
||||
+ Decimal(1) / 10 ** price_digits[1], rounding=rounding)
|
||||
+
|
||||
+
|
||||
def process_sale(moves_field):
|
||||
def _process_sale(func):
|
||||
@wraps(func)
|
||||
@@ -155,6 +162,33 @@ class Move(metaclass=PoolMeta):
|
||||
category = self.origin.unit.category.id
|
||||
return category
|
||||
|
||||
+ def get_cost_price(self, product_cost_price=None):
|
||||
+ pool = Pool()
|
||||
+ SaleLine = pool.get('sale.line')
|
||||
+ Sale = pool.get('sale.sale')
|
||||
+ # For return sale's move use the cost price of the original sale
|
||||
+ if (isinstance(self.origin, SaleLine)
|
||||
+ and self.origin.quantity < 0
|
||||
+ and self.from_location.type != 'storage'
|
||||
+ and self.to_location.type == 'storage'
|
||||
+ and isinstance(self.origin.origin, Sale)):
|
||||
+ sale = self.origin.origin
|
||||
+ cost = Decimal(0)
|
||||
+ qty = Decimal(0)
|
||||
+ for move in sale.moves:
|
||||
+ if (move.state == 'done'
|
||||
+ and move.from_location.type == 'storage'
|
||||
+ and move.to_location.type == 'customer'
|
||||
+ and move.product == self.product):
|
||||
+ move_quantity = Decimal(str(move.internal_quantity))
|
||||
+ cost_price = move.get_cost_price(
|
||||
+ product_cost_price=move.cost_price)
|
||||
+ qty += move_quantity
|
||||
+ cost += cost_price * move_quantity
|
||||
+ if qty:
|
||||
+ product_cost_price = round_price(cost / qty)
|
||||
+ return super().get_cost_price(product_cost_price=product_cost_price)
|
||||
+
|
||||
@property
|
||||
def origin_name(self):
|
||||
pool = Pool()
|
||||
--
|
||||
2.25.1
|
|
@ -0,0 +1,43 @@
|
|||
From 479eff31f971acef41cc0fe77513305c3bb2b4a7 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?=C3=80ngel=20=C3=80lvarez?= <angel@nan-tic.com>
|
||||
Date: Thu, 8 Apr 2021 11:58:52 +0200
|
||||
Subject: [PATCH] Use original cost price for returned move
|
||||
|
||||
and factorize the cost price of move for cost computation
|
||||
|
||||
issue9440
|
||||
review327491003
|
||||
---
|
||||
stock.py | 9 +--------
|
||||
1 file changed, 1 insertion(+), 8 deletions(-)
|
||||
|
||||
diff --git a/stock.py b/stock.py
|
||||
index 46dc496..84b00ca 100644
|
||||
--- a/trytond/trytond/modules/sale_supply_drop_shipment/stock.py
|
||||
+++ b/trytond/trytond/modules/sale_supply_drop_shipment/stock.py
|
||||
@@ -404,7 +404,6 @@ class ShipmentDrop(Workflow, ModelSQL, ModelView):
|
||||
pool = Pool()
|
||||
UoM = pool.get('product.uom')
|
||||
Move = pool.get('stock.move')
|
||||
- Currency = pool.get('currency.currency')
|
||||
|
||||
to_save = []
|
||||
cost_exp = Decimal(str(10.0 ** -Move.cost_price.digits[1]))
|
||||
@@ -424,14 +423,8 @@ class ShipmentDrop(Workflow, ModelSQL, ModelView):
|
||||
if s_move.state == 'cancel':
|
||||
continue
|
||||
internal_quantity = Decimal(str(s_move.internal_quantity))
|
||||
- with Transaction().set_context(date=s_move.effective_date):
|
||||
- unit_price = Currency.compute(
|
||||
- s_move.currency, s_move.unit_price,
|
||||
- s_move.company.currency, round=False)
|
||||
- unit_price = UoM.compute_price(
|
||||
- s_move.uom, unit_price, s_move.product.default_uom)
|
||||
product_cost[s_move.product] += (
|
||||
- unit_price * internal_quantity)
|
||||
+ s_move.get_cost_price() * internal_quantity)
|
||||
|
||||
quantity = UoM.compute_qty(
|
||||
s_move.uom, s_move.quantity, s_move.product.default_uom,
|
||||
--
|
||||
2.25.1
|
18
series
18
series
|
@ -44,7 +44,7 @@ issue10009.diff # [trytond] Remove active test from sequences and languages acti
|
|||
|
||||
issue9616.diff # [analytic_invoice] Analytic move is not created when closing an asset
|
||||
|
||||
fifo_quantity_round.diff # issue9664 [product_cost_fifo] Round fifo_quantity before save, to avoid problems when compare with quantity [Remove on 5.8]
|
||||
#fifo_quantity_round.diff # issue9664 [product_cost_fifo] Round fifo_quantity before save, to avoid problems when compare with quantity [Remove on 5.8]
|
||||
|
||||
account_payment_search_payment_amount.diff # [account_payment] Optimize searcher search_payment_amount
|
||||
|
||||
|
@ -70,3 +70,19 @@ project_invoice_progress_compute_qty.diff # [project_invoice] Twice compute qty
|
|||
issue10068.diff # [trytond] Use safe_join in SharedDataMiddlewareIndex
|
||||
|
||||
issue10053.diff # [party] Configure available identifiers
|
||||
|
||||
# Cost and Recompute Cost issues
|
||||
production_cost_fifo-Update-cost-of-moves-when-recomputing-product-cost.patch #[issue8795 + issue7271] Remove on 5.8
|
||||
production-Update-cost-of-moves-when-recomputing-product-cost.patch #[issue8795 + issue7271] Remove on 5.8
|
||||
stock-Update-cost-of-moves-when-recomputing-product-cost.patch #[issue8795 + issue7271] Remove on 5.8
|
||||
production-Keep-cost-of-unused-input-products.patch #[issue9637] Remove on 5.8
|
||||
cost_price_in_productions_without_inputs.diff # [production]
|
||||
product_cost_fifo-Use-all-moves-to-compute-FIFO.patch #[issue9443 issue9274] Remove on 5.8
|
||||
product-cost-fifo-Use-original-cost-price-for-returned-move.patch #[issue9440] Remove on 6.0
|
||||
stock-Use-original-cost-price-for-returned-move.patch #[issue9440] Remove on 6.0
|
||||
sale-Use-original-cost-price-for-returned-move.patch #[issue9440] Remove on 6.0
|
||||
sale_supply_drop_shippment-Use-original-cost-price-for-returned-move.patch #[issue9440] Remove on 6.0
|
||||
product_cost_fifo-Do-not-update-average-cost-price-i.patch #[issue9484] Remove on 6.0
|
||||
product_cost_fifo-Enforce-filling-cost-price-of-move.patch #[issue9397] Remove on 6.0
|
||||
stock-Enforce-filling-cost-price-of-move.patch #[issue9397] Remove on 6.0
|
||||
# End Cost and Recompute Cost issues
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
From 443f3cb536af34952e95205bc3334b6ff77b1c39 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?=C3=80ngel=20=C3=80lvarez?= <angel@nan-tic.com>
|
||||
Date: Thu, 8 Apr 2021 12:20:12 +0200
|
||||
Subject: [PATCH] stock: Enforce filling cost price of move
|
||||
|
||||
We set cost price only for outgoing or incoming moves.
|
||||
|
||||
issue9397
|
||||
---
|
||||
move.py | 23 ++++++++++++++++++++---
|
||||
1 file changed, 20 insertions(+), 3 deletions(-)
|
||||
|
||||
diff --git a/move.py b/move.py
|
||||
index 19f4401..839a393 100644
|
||||
--- a/trytond/trytond/modules/stock/move.py
|
||||
+++ b/trytond/trytond/modules/stock/move.py
|
||||
@@ -266,8 +266,15 @@ class Move(Workflow, ModelSQL, ModelView):
|
||||
'invisible': Eval('state') != 'done',
|
||||
},
|
||||
depends=['state'])
|
||||
- cost_price = fields.Numeric('Cost Price', digits=price_digits,
|
||||
- readonly=True)
|
||||
+ cost_price = fields.Numeric(
|
||||
+ "Cost Price", digits=price_digits, readonly=True,
|
||||
+ states={
|
||||
+ 'invisible': ~Eval('cost_price_required'),
|
||||
+ 'required': (
|
||||
+ (Eval('state') == 'done')
|
||||
+ & Eval('cost_price_required', False)),
|
||||
+ },
|
||||
+ depends=['cost_price_required'])
|
||||
currency = fields.Many2One('currency.currency', 'Currency',
|
||||
states={
|
||||
'invisible': ~Eval('unit_price_required'),
|
||||
@@ -279,6 +286,9 @@ class Move(Workflow, ModelSQL, ModelView):
|
||||
unit_price_required = fields.Function(
|
||||
fields.Boolean('Unit Price Required'),
|
||||
'on_change_with_unit_price_required')
|
||||
+ cost_price_required = fields.Function(
|
||||
+ fields.Boolean("Cost Price Required"),
|
||||
+ 'on_change_with_cost_price_required')
|
||||
assignation_required = fields.Function(
|
||||
fields.Boolean('Assignation Required'),
|
||||
'on_change_with_assignation_required')
|
||||
@@ -416,6 +426,13 @@ class Move(Workflow, ModelSQL, ModelView):
|
||||
return True
|
||||
return False
|
||||
|
||||
+ @fields.depends('from_location', 'to_location')
|
||||
+ def on_change_with_cost_price_required(self, name=None):
|
||||
+ from_type = self.from_location.type if self.from_location else None
|
||||
+ to_type = self.to_location.type if self.to_location else None
|
||||
+ return ((from_type != 'storage' and to_type == 'storage')
|
||||
+ or (from_type == 'storage' and to_type != 'storage'))
|
||||
+
|
||||
@fields.depends('from_location')
|
||||
def on_change_with_assignation_required(self, name=None):
|
||||
if self.from_location:
|
||||
@@ -620,7 +637,7 @@ class Move(Workflow, ModelSQL, ModelView):
|
||||
cost_values.append(
|
||||
(move.product, cost_price,
|
||||
move._cost_price_pattern))
|
||||
- if move.cost_price is None:
|
||||
+ if move.cost_price_required and move.cost_price is None:
|
||||
if cost_price is None:
|
||||
cost_price = move.product.get_multivalue(
|
||||
'cost_price', **move._cost_price_pattern)
|
||||
--
|
||||
2.25.1
|
|
@ -0,0 +1,657 @@
|
|||
From 5ddcf090b2ffba088a81e764a00c36984f101798 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:23:41 +0200
|
||||
Subject: [PATCH] Update cost of moves when recomputing product cost
|
||||
|
||||
issue8795
|
||||
issue7271
|
||||
---
|
||||
__init__.py | 3 +
|
||||
ir.py | 15 ++
|
||||
move.py | 18 +-
|
||||
product.py | 188 +++++++++++++++---
|
||||
product.xml | 13 +-
|
||||
tests/scenario_stock_average_cost_price.rst | 2 +
|
||||
...rio_stock_recompute_average_cost_price.rst | 160 +++++++++++++++
|
||||
tests/test_stock.py | 5 +
|
||||
view/recompute_cost_price_start_form.xml | 7 +
|
||||
9 files changed, 377 insertions(+), 34 deletions(-)
|
||||
create mode 100644 ir.py
|
||||
create mode 100644 tests/scenario_stock_recompute_average_cost_price.rst
|
||||
create mode 100644 view/recompute_cost_price_start_form.xml
|
||||
|
||||
diff --git a/__init__.py b/__init__.py
|
||||
index daf5784..6d65b2e 100644
|
||||
--- a/trytond/trytond/modules/stock/__init__.py
|
||||
+++ b/trytond/trytond/modules/stock/__init__.py
|
||||
@@ -11,6 +11,7 @@ from . import inventory
|
||||
from . import configuration
|
||||
from . import party
|
||||
from . import res
|
||||
+from . import ir
|
||||
|
||||
from .move import StockMixin
|
||||
|
||||
@@ -41,6 +42,7 @@ def register():
|
||||
product.ProductByLocationContext,
|
||||
product.ProductQuantitiesByWarehouse,
|
||||
product.ProductQuantitiesByWarehouseContext,
|
||||
+ product.RecomputeCostPriceStart,
|
||||
inventory.Inventory,
|
||||
inventory.InventoryLine,
|
||||
inventory.CountSearch,
|
||||
@@ -49,6 +51,7 @@ def register():
|
||||
configuration.ConfigurationSequence,
|
||||
configuration.ConfigurationLocation,
|
||||
res.User,
|
||||
+ ir.Cron,
|
||||
module='stock', type_='model')
|
||||
Pool.register(
|
||||
shipment.AssignShipmentOut,
|
||||
diff --git a/ir.py b/ir.py
|
||||
new file mode 100644
|
||||
index 0000000..0508a53
|
||||
--- /a//trytond/trytond/modules/stock/ir.py
|
||||
+++ b/trytond/trytond/modules/stock/ir.py
|
||||
@@ -0,0 +1,15 @@
|
||||
+# 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.pool import PoolMeta
|
||||
+
|
||||
+
|
||||
+class Cron(metaclass=PoolMeta):
|
||||
+ __name__ = 'ir.cron'
|
||||
+
|
||||
+ @classmethod
|
||||
+ def __setup__(cls):
|
||||
+ super().__setup__()
|
||||
+ cls.method.selection.extend([
|
||||
+ ('product.product|recompute_cost_price_from_moves',
|
||||
+ "Recompute Cost Price from Moves"),
|
||||
+ ])
|
||||
diff --git a/move.py b/move.py
|
||||
index 7995ab4..42cf691 100644
|
||||
--- a/trytond/trytond/modules/stock/move.py
|
||||
+++ b/trytond/trytond/modules/stock/move.py
|
||||
@@ -246,6 +246,12 @@ class Move(Workflow, ModelSQL, ModelView):
|
||||
'readonly': Eval('state') != 'draft',
|
||||
},
|
||||
depends=['unit_price_required', 'state'])
|
||||
+ unit_price_updated = fields.Boolean(
|
||||
+ "Unit Price Updated", readonly=True,
|
||||
+ states={
|
||||
+ 'invisible': Eval('state') != 'done',
|
||||
+ },
|
||||
+ depends=['state'])
|
||||
cost_price = fields.Numeric('Cost Price', digits=price_digits,
|
||||
readonly=True)
|
||||
currency = fields.Many2One('currency.currency', 'Currency',
|
||||
@@ -270,7 +276,7 @@ class Move(Workflow, ModelSQL, ModelView):
|
||||
'from_location', 'to_location', 'company', 'currency'])
|
||||
cls._deny_modify_done_cancel = (cls._deny_modify_assigned |
|
||||
set(['planned_date', 'effective_date', 'state']))
|
||||
- cls._allow_modify_closed_period = set()
|
||||
+ cls._allow_modify_closed_period = {'cost_price'}
|
||||
|
||||
t = cls.__table__()
|
||||
cls._sql_constraints += [
|
||||
@@ -346,6 +352,10 @@ class Move(Workflow, ModelSQL, ModelView):
|
||||
def default_company():
|
||||
return Transaction().context.get('company')
|
||||
|
||||
+ @classmethod
|
||||
+ def default_unit_price_updated(cls):
|
||||
+ return True
|
||||
+
|
||||
@staticmethod
|
||||
def default_currency():
|
||||
Company = Pool().get('company.company')
|
||||
@@ -678,6 +688,7 @@ class Move(Workflow, ModelSQL, ModelView):
|
||||
super(Move, cls).write(*args)
|
||||
|
||||
to_write = []
|
||||
+ unit_price_update = []
|
||||
actions = iter(args)
|
||||
for moves, values in zip(actions, actions):
|
||||
if any(f not in cls._allow_modify_closed_period for f in values):
|
||||
@@ -691,8 +702,13 @@ class Move(Workflow, ModelSQL, ModelView):
|
||||
to_write.extend(([move], {
|
||||
'internal_quantity': internal_quantity,
|
||||
}))
|
||||
+ if move.state == 'done' and 'unit_price' in values:
|
||||
+ unit_price_update.append(move)
|
||||
+
|
||||
if to_write:
|
||||
cls.write(*to_write)
|
||||
+ if unit_price_update:
|
||||
+ cls.write(unit_price_update, {'unit_price_updated': True})
|
||||
|
||||
@classmethod
|
||||
def delete(cls, moves):
|
||||
diff --git a/product.py b/product.py
|
||||
index 1d1d448..8de40b1 100644
|
||||
--- a/trytond/trytond/modules/stock/product.py
|
||||
+++ b/trytond/trytond/modules/stock/product.py
|
||||
@@ -9,7 +9,8 @@ from sql import Literal, Null, Select
|
||||
from sql.aggregate import Max
|
||||
from sql.functions import CurrentTimestamp
|
||||
from sql.conditionals import Coalesce
|
||||
-
|
||||
+from trytond.wizard import (
|
||||
+ Wizard, StateTransition, StateAction, StateView, Button)
|
||||
from trytond.i18n import gettext
|
||||
from trytond.model import ModelSQL, ModelView, fields
|
||||
from trytond.model.exceptions import AccessError
|
||||
@@ -102,12 +103,12 @@ class Template(metaclass=PoolMeta):
|
||||
super(Template, cls).write(*args)
|
||||
|
||||
@classmethod
|
||||
- def recompute_cost_price(cls, templates):
|
||||
+ def recompute_cost_price(cls, templates, start=None):
|
||||
pool = Pool()
|
||||
Product = pool.get('product.product')
|
||||
|
||||
products = [p for t in templates for p in t.products]
|
||||
- Product.recompute_cost_price(products)
|
||||
+ Product.recompute_cost_price(products, start=start)
|
||||
|
||||
|
||||
class Product(StockMixin, object, metaclass=PoolMeta):
|
||||
@@ -211,17 +212,48 @@ class Product(StockMixin, object, metaclass=PoolMeta):
|
||||
return quantities
|
||||
|
||||
@classmethod
|
||||
- def recompute_cost_price(cls, products):
|
||||
+ def recompute_cost_price_from_moves(cls):
|
||||
+ pool = Pool()
|
||||
+ Move = pool.get('stock.move')
|
||||
+ products = set()
|
||||
+ for move in Move.search([
|
||||
+ ('unit_price_updated', '=', True),
|
||||
+ cls._domain_moves_cost(),
|
||||
+ ],
|
||||
+ order=[('effective_date', 'ASC')]):
|
||||
+ if move.product not in products:
|
||||
+ cls.__queue__.recompute_cost_price(
|
||||
+ [move.product], start=move.effective_date)
|
||||
+ products.add(move.product)
|
||||
+
|
||||
+ @classmethod
|
||||
+ def recompute_cost_price(cls, products, start=None):
|
||||
+ pool = Pool()
|
||||
+ Move = pool.get('stock.move')
|
||||
digits = cls.cost_price.digits
|
||||
costs = defaultdict(list)
|
||||
for product in products:
|
||||
if product.type == 'service':
|
||||
continue
|
||||
- cost = getattr(product,
|
||||
- 'recompute_cost_price_%s' % product.cost_price_method)()
|
||||
+ cost = getattr(
|
||||
+ product, 'recompute_cost_price_%s' %
|
||||
+ product.cost_price_method)(start)
|
||||
cost = cost.quantize(Decimal(str(10.0 ** -digits[1])))
|
||||
costs[cost].append(product)
|
||||
|
||||
+ updated = []
|
||||
+ for sub_products in grouped_slice(products):
|
||||
+ domain = [
|
||||
+ ('unit_price_updated', '=', True),
|
||||
+ cls._domain_moves_cost(),
|
||||
+ ('product', 'in', [p.id for p in sub_products]),
|
||||
+ ]
|
||||
+ if start:
|
||||
+ domain.append(('effective_date', '>=', start))
|
||||
+ updated += Move.search(domain, order=[])
|
||||
+ if updated:
|
||||
+ Move.write(updated, {'unit_price_updated': False})
|
||||
+
|
||||
if not costs:
|
||||
return
|
||||
|
||||
@@ -231,45 +263,93 @@ class Product(StockMixin, object, metaclass=PoolMeta):
|
||||
to_write.append({'cost_price': cost})
|
||||
|
||||
# Enforce check access for account_stock*
|
||||
- with Transaction().set_context(_check_access=True):
|
||||
+ with Transaction().set_context(_check_access=False):
|
||||
cls.write(*to_write)
|
||||
|
||||
- def recompute_cost_price_fixed(self):
|
||||
+ def recompute_cost_price_fixed(self, start=None):
|
||||
return self.cost_price
|
||||
|
||||
- @property
|
||||
- def _domain_moves_cost(self):
|
||||
+ @classmethod
|
||||
+ def _domain_moves_cost(cls):
|
||||
"Returns the domain for moves to use in cost computation"
|
||||
context = Transaction().context
|
||||
return [
|
||||
('company', '=', context.get('company')),
|
||||
+ ('state', '=', 'done'),
|
||||
]
|
||||
|
||||
- def recompute_cost_price_average(self):
|
||||
+ def _get_storage_quantity(self, date=None):
|
||||
+ pool = Pool()
|
||||
+ Location = pool.get('stock.location')
|
||||
+
|
||||
+ locations = Location.search([
|
||||
+ ('type', '=', 'storage'),
|
||||
+ ])
|
||||
+ if not date:
|
||||
+ date = datetime.date.today()
|
||||
+ location_ids = [l.id for l in locations]
|
||||
+ with Transaction().set_context(
|
||||
+ locations=location_ids,
|
||||
+ with_childs=False,
|
||||
+ stock_date_end=date):
|
||||
+ return self.__class__(self.id).quantity
|
||||
+
|
||||
+ def recompute_cost_price_average(self, start=None):
|
||||
pool = Pool()
|
||||
Move = pool.get('stock.move')
|
||||
Currency = pool.get('currency.currency')
|
||||
Uom = pool.get('product.uom')
|
||||
|
||||
- moves = Move.search([
|
||||
- ('product', '=', self.id),
|
||||
- ('state', '=', 'done'),
|
||||
- self._domain_moves_cost,
|
||||
- ['OR',
|
||||
- [
|
||||
- ('to_location.type', '=', 'storage'),
|
||||
- ('from_location.type', '!=', 'storage'),
|
||||
- ],
|
||||
- [
|
||||
- ('from_location.type', '=', 'storage'),
|
||||
- ('to_location.type', '!=', 'storage'),
|
||||
- ],
|
||||
- ],
|
||||
- ], order=[('effective_date', 'ASC'), ('id', 'ASC')])
|
||||
+ 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 - datetime.timedelta(days=1))
|
||||
+ quantity = Decimal(str(quantity))
|
||||
+ current_moves = []
|
||||
+ 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 current_moves
|
||||
+ if m.cost_price != current_cost_price],
|
||||
+ dict(cost_price=current_cost_price))
|
||||
+ current_moves.clear()
|
||||
+ 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':
|
||||
@@ -280,16 +360,23 @@ class Product(StockMixin, object, metaclass=PoolMeta):
|
||||
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)
|
||||
+ 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])))
|
||||
quantity += qty
|
||||
- return cost_price
|
||||
+
|
||||
+ Move.write([
|
||||
+ m for m in current_moves
|
||||
+ if m.cost_price != current_cost_price],
|
||||
+ dict(cost_price=current_cost_price))
|
||||
+ return current_cost_price
|
||||
|
||||
|
||||
class ProductByLocationContext(ModelView):
|
||||
@@ -530,9 +617,40 @@ class OpenProductQuantitiesByWarehouse(Wizard):
|
||||
class RecomputeCostPrice(Wizard):
|
||||
'Recompute Cost Price'
|
||||
__name__ = 'product.recompute_cost_price'
|
||||
- start_state = 'recompute'
|
||||
+ start = StateView(
|
||||
+ 'product.recompute_cost_price.start',
|
||||
+ 'stock.recompute_cost_price_start_view_form', [
|
||||
+ Button("Cancel", 'end'),
|
||||
+ Button("Recompute", 'recompute', default=True)])
|
||||
recompute = StateTransition()
|
||||
|
||||
+ def default_start(self, fields):
|
||||
+ pool = Pool()
|
||||
+ Move = pool.get('stock.move')
|
||||
+ Product = pool.get('product.product')
|
||||
+ Template = pool.get('product.template')
|
||||
+ context = Transaction().context
|
||||
+
|
||||
+ if context['active_model'] == 'product.product':
|
||||
+ products = Product.browse(context['active_ids'])
|
||||
+ elif context['active_model'] == 'product.template':
|
||||
+ templates = Template.browse(context['active_ids'])
|
||||
+ products = sum((t.products for t in templates), ())
|
||||
+
|
||||
+ from_ = None
|
||||
+ for sub_products in grouped_slice(products):
|
||||
+ moves = Move.search([
|
||||
+ ('unit_price_updated', '=', True),
|
||||
+ Product._domain_moves_cost(),
|
||||
+ ('product', 'in', [p.id for p in sub_products]),
|
||||
+ ],
|
||||
+ order=[('effective_date', 'ASC')],
|
||||
+ limit=1)
|
||||
+ if moves:
|
||||
+ move, = moves
|
||||
+ from_ = min(from_ or datetime.date.max, move.effective_date)
|
||||
+ return {'from_': from_}
|
||||
+
|
||||
def transition_recompute(self):
|
||||
pool = Pool()
|
||||
Product = pool.get('product.product')
|
||||
@@ -542,8 +660,14 @@ class RecomputeCostPrice(Wizard):
|
||||
|
||||
if context['active_model'] == 'product.product':
|
||||
products = Product.browse(context['active_ids'])
|
||||
- Product.recompute_cost_price(products)
|
||||
+ Product.recompute_cost_price(products, start=self.start.from_)
|
||||
elif context['active_model'] == 'product.template':
|
||||
templates = Template.browse(context['active_ids'])
|
||||
- Template.recompute_cost_price(templates)
|
||||
+ Template.recompute_cost_price(templates, start=self.start.from_)
|
||||
return 'end'
|
||||
+
|
||||
+
|
||||
+class RecomputeCostPriceStart(ModelView):
|
||||
+ "Recompute Cost Price"
|
||||
+ __name__ = 'product.recompute_cost_price.start'
|
||||
+ from_ = fields.Date("From")
|
||||
diff --git a/product.xml b/product.xml
|
||||
index b74a8ed..fbe6d49 100644
|
||||
--- a/trytond/trytond/modules/stock/product.xml
|
||||
+++ b/trytond/trytond/modules/stock/product.xml
|
||||
@@ -32,6 +32,18 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="group" ref="product.group_product_admin"/>
|
||||
</record>
|
||||
|
||||
+ <record model="ir.ui.view" id="recompute_cost_price_start_view_form">
|
||||
+ <field name="model">product.recompute_cost_price.start</field>
|
||||
+ <field name="type">form</field>
|
||||
+ <field name="name">recompute_cost_price_start_form</field>
|
||||
+ </record>
|
||||
+
|
||||
+ <record model="ir.cron" id="cron_recompute_cost_price_from_moves">
|
||||
+ <field name="method">product.product|recompute_cost_price_from_moves</field>
|
||||
+ <field name="interval_number" eval="1"/>
|
||||
+ <field name="interval_type">days</field>
|
||||
+ </record>
|
||||
+
|
||||
<record model="ir.ui.view" id="location_quantity_view_tree">
|
||||
<field name="model">stock.location</field>
|
||||
<field name="type">tree</field>
|
||||
@@ -184,6 +196,5 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="action" ref="act_product_quantities_warehouse"/>
|
||||
<field name="group" ref="group_stock"/>
|
||||
</record>
|
||||
-
|
||||
</data>
|
||||
</tryton>
|
||||
diff --git a/tests/scenario_stock_average_cost_price.rst b/tests/scenario_stock_average_cost_price.rst
|
||||
index 8223c5c..1af0e0d 100644
|
||||
--- a/trytond/trytond/modules/stock/tests/scenario_stock_average_cost_price.rst
|
||||
+++ b/trytond/trytond/modules/stock/tests/scenario_stock_average_cost_price.rst
|
||||
@@ -153,6 +153,7 @@ Change Cost Price to 125, to force to write recomputed price later::
|
||||
Recompute Cost Price::
|
||||
|
||||
>>> recompute = Wizard('product.recompute_cost_price', [product])
|
||||
+ >>> recompute.execute('recompute')
|
||||
>>> product.cost_price
|
||||
Decimal('175.0000')
|
||||
|
||||
@@ -250,5 +251,6 @@ Change Cost Price to 5, to force to write recomputed price later::
|
||||
Recompute Cost Price::
|
||||
|
||||
>>> recompute = Wizard('product.recompute_cost_price', [negative_product])
|
||||
+ >>> recompute.execute('recompute')
|
||||
>>> negative_product.cost_price
|
||||
Decimal('2.0000')
|
||||
diff --git a/tests/scenario_stock_recompute_average_cost_price.rst b/tests/scenario_stock_recompute_average_cost_price.rst
|
||||
new file mode 100644
|
||||
index 0000000..f733b8f
|
||||
--- /dev/null
|
||||
+++ b/trytond/trytond/modules/stock/tests/scenario_stock_recompute_average_cost_price.rst
|
||||
@@ -0,0 +1,160 @@
|
||||
+==================================
|
||||
+Stock Recompute Average 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('stock')
|
||||
+
|
||||
+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 = 'average'
|
||||
+ >>> 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=storage_loc,
|
||||
+ ... to_location=customer_loc,
|
||||
+ ... unit_price=Decimal('300'),
|
||||
+ ... effective_date=today - dt.timedelta(days=1)).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=3,
|
||||
+ ... from_location=supplier_loc,
|
||||
+ ... to_location=storage_loc,
|
||||
+ ... unit_price=Decimal('100'),
|
||||
+ ... effective_date=today).click('do')
|
||||
+
|
||||
+ >>> [m.cost_price for m in StockMove.find([])]
|
||||
+ [Decimal('105.0000'), Decimal('120.0000'), Decimal('100.0000'), Decimal('100.0000')]
|
||||
+
|
||||
+ >>> product.reload()
|
||||
+ >>> product.cost_price
|
||||
+ Decimal('105.0000')
|
||||
+
|
||||
+Recompute cost price::
|
||||
+
|
||||
+ >>> recompute = Wizard('product.recompute_cost_price', [product])
|
||||
+ >>> recompute.execute('recompute')
|
||||
+
|
||||
+ >>> [m.cost_price for m in StockMove.find([])]
|
||||
+ [Decimal('105.0000'), Decimal('120.0000'), Decimal('120.0000'), Decimal('100.0000')]
|
||||
+
|
||||
+ >>> product.reload()
|
||||
+ >>> product.cost_price
|
||||
+ Decimal('105.0000')
|
||||
+
|
||||
+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('105.0000'), Decimal('120.0000'), Decimal('120.0000'), Decimal('100.0000')]
|
||||
+
|
||||
+ >>> product.reload()
|
||||
+ >>> product.cost_price
|
||||
+ Decimal('105.0000')
|
||||
+
|
||||
+Update unit price of a move::
|
||||
+
|
||||
+ >>> move, = StockMove.find([
|
||||
+ ... ('from_location', '=', supplier_loc.id),
|
||||
+ ... ('effective_date', '=', today - dt.timedelta(days=1)),
|
||||
+ ... ])
|
||||
+ >>> bool(move.unit_price_updated)
|
||||
+ False
|
||||
+ >>> move.unit_price = Decimal('130')
|
||||
+ >>> move.save()
|
||||
+ >>> bool(move.unit_price_updated)
|
||||
+ True
|
||||
+
|
||||
+ >>> recompute = Wizard('product.recompute_cost_price', [product])
|
||||
+ >>> recompute.form.from_ = move.effective_date + dt.timedelta(days=1)
|
||||
+ >>> recompute.execute('recompute')
|
||||
+ >>> move.reload()
|
||||
+ >>> bool(move.unit_price_updated)
|
||||
+ True
|
||||
+
|
||||
+ >>> recompute = Wizard('product.recompute_cost_price', [product])
|
||||
+ >>> recompute.form.from_ == move.effective_date
|
||||
+ True
|
||||
+ >>> recompute.execute('recompute')
|
||||
+ >>> move.reload()
|
||||
+ >>> bool(move.unit_price_updated)
|
||||
+ False
|
||||
+ >>> [m.cost_price for m in StockMove.find([])]
|
||||
+ [Decimal('107.5000'), Decimal('130.0000'), Decimal('130.0000'), Decimal('100.0000')]
|
||||
+
|
||||
+Launch cron task::
|
||||
+
|
||||
+ >>> move.unit_price = Decimal('120')
|
||||
+ >>> move.save()
|
||||
+
|
||||
+ >>> Cron = Model.get('ir.cron')
|
||||
+ >>> Company = Model.get('company.company')
|
||||
+ >>> cron_recompute, = Cron.find([
|
||||
+ ... ('method', '=', 'product.product|recompute_cost_price_from_moves'),
|
||||
+ ... ])
|
||||
+ >>> cron_recompute.companies.append(Company(company.id))
|
||||
+ >>> cron_recompute.click('run_once')
|
||||
+
|
||||
+ >>> move.reload()
|
||||
+ >>> bool(move.unit_price_updated)
|
||||
+ False
|
||||
+ >>> [m.cost_price for m in StockMove.find([])]
|
||||
+ [Decimal('105.0000'), Decimal('120.0000'), Decimal('120.0000'), Decimal('100.0000')]
|
||||
diff --git a/tests/test_stock.py b/tests/test_stock.py
|
||||
index 60c5cc2..8fda0ea 100644
|
||||
--- a/trytond/trytond/modules/stock/tests/test_stock.py
|
||||
+++ b/trytond/trytond/modules/stock/tests/test_stock.py
|
||||
@@ -1437,6 +1437,11 @@ def suite():
|
||||
tearDown=doctest_teardown, encoding='utf-8',
|
||||
checker=doctest_checker,
|
||||
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
|
||||
+ suite.addTests(doctest.DocFileSuite(
|
||||
+ 'scenario_stock_recompute_average_cost_price.rst',
|
||||
+ tearDown=doctest_teardown, encoding='utf-8',
|
||||
+ checker=doctest_checker,
|
||||
+ optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
|
||||
suite.addTests(doctest.DocFileSuite(
|
||||
'scenario_stock_inventory.rst',
|
||||
tearDown=doctest_teardown, encoding='utf-8',
|
||||
diff --git a/view/recompute_cost_price_start_form.xml b/view/recompute_cost_price_start_form.xml
|
||||
new file mode 100644
|
||||
index 0000000..70f258f
|
||||
--- /dev/null
|
||||
+++ b/trytond/trytond/modules/stock/view/recompute_cost_price_start_form.xml
|
||||
@@ -0,0 +1,7 @@
|
||||
+<?xml version="1.0"?>
|
||||
+<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
+this repository contains the full copyright notices and license terms. -->
|
||||
+<form>
|
||||
+ <label name="from_"/>
|
||||
+ <field name="from_"/>
|
||||
+</form>
|
||||
--
|
||||
2.25.1
|
|
@ -0,0 +1,127 @@
|
|||
From 3cdf3e96289fb280363f1df7f5a1d66a3447a0bf Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?=C3=80ngel=20=C3=80lvarez?= <angel@nan-tic.com>
|
||||
Date: Thu, 8 Apr 2021 11:54:47 +0200
|
||||
Subject: [PATCH] Use original cost price for returned move
|
||||
|
||||
and factorize the cost price of move for cost computation
|
||||
|
||||
issue9440
|
||||
review327491003
|
||||
---
|
||||
move.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++++---------
|
||||
1 file changed, 53 insertions(+), 9 deletions(-)
|
||||
|
||||
diff --git a/move.py b/move.py
|
||||
index df296f3..19f4401 100644
|
||||
--- a/trytond/trytond/modules/stock/move.py
|
||||
+++ b/trytond/trytond/modules/stock/move.py
|
||||
@@ -1,4 +1,4 @@
|
||||
-# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
+7# 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 hashlib
|
||||
@@ -40,6 +40,12 @@ LOCATION_DOMAIN = [
|
||||
LOCATION_DEPENDS = ['state']
|
||||
|
||||
|
||||
+def round_price(value, rounding=None):
|
||||
+ "Round price using the price digits"
|
||||
+ return value.quantize(
|
||||
+ Decimal(1) / 10 ** price_digits[1], rounding=rounding)
|
||||
+
|
||||
+
|
||||
class StockMixin(object):
|
||||
'''Mixin class with helper to setup stock quantity field.'''
|
||||
__slots__ = ()
|
||||
@@ -246,6 +252,14 @@ class Move(Workflow, ModelSQL, ModelView):
|
||||
'readonly': Eval('state') != 'draft',
|
||||
},
|
||||
depends=['unit_price_required', 'state'])
|
||||
+ unit_price_company = fields.Function(
|
||||
+ fields.Numeric("Unit Price", digits=price_digits,
|
||||
+ states={
|
||||
+ 'invisible': ~Eval('unit_price_required'),
|
||||
+ },
|
||||
+ depends=['unit_price_required'],
|
||||
+ help="Unit price in company currency."),
|
||||
+ 'get_unit_price_company')
|
||||
unit_price_updated = fields.Boolean(
|
||||
"Unit Price Updated", readonly=True,
|
||||
states={
|
||||
@@ -407,6 +421,28 @@ class Move(Workflow, ModelSQL, ModelView):
|
||||
if self.from_location:
|
||||
return self.from_location.type in {'storage', 'view'}
|
||||
|
||||
+ @classmethod
|
||||
+ def get_unit_price_company(cls, moves, name):
|
||||
+ pool = Pool()
|
||||
+ Currency = pool.get('currency.currency')
|
||||
+ Uom = pool.get('product.uom')
|
||||
+ Date = pool.get('ir.date')
|
||||
+ today = Date.today()
|
||||
+ prices = {}
|
||||
+ for move in moves:
|
||||
+ if move.unit_price is not None:
|
||||
+ date = move.effective_date or move.planned_date or today
|
||||
+ with Transaction().set_context(date=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)
|
||||
+ prices[move.id] = round_price(unit_price)
|
||||
+ else:
|
||||
+ prices[move.id] = None
|
||||
+ return prices
|
||||
+
|
||||
@staticmethod
|
||||
def _get_shipment():
|
||||
'Return list of Model names for shipment Reference'
|
||||
@@ -472,7 +508,7 @@ class Move(Workflow, ModelSQL, ModelView):
|
||||
def search_rec_name(cls, name, clause):
|
||||
return [('product.rec_name',) + tuple(clause[1:])]
|
||||
|
||||
- def _compute_product_cost_price(self, direction):
|
||||
+ def _compute_product_cost_price(self, direction, product_cost_price=None):
|
||||
"""
|
||||
Update the cost price on the given product.
|
||||
The direction must be "in" if incoming and "out" if outgoing.
|
||||
@@ -490,13 +526,7 @@ class Move(Workflow, ModelSQL, ModelView):
|
||||
|
||||
qty = Decimal(str(qty))
|
||||
product_qty = Decimal(str(self.product.quantity))
|
||||
- # convert wrt currency
|
||||
- with Transaction().set_context(date=self.effective_date):
|
||||
- unit_price = Currency.compute(self.currency, self.unit_price,
|
||||
- self.company.currency, round=False)
|
||||
- # convert wrt to the uom
|
||||
- unit_price = Uom.compute_price(self.uom, unit_price,
|
||||
- self.product.default_uom)
|
||||
+ unit_price = self.get_cost_price(product_cost_price=product_cost_price)
|
||||
cost_price = self.product.get_multivalue(
|
||||
'cost_price', **self._cost_price_pattern)
|
||||
if product_qty + qty > 0 and product_qty >= 0:
|
||||
@@ -616,6 +646,20 @@ class Move(Workflow, ModelSQL, ModelView):
|
||||
('company', self.company.id),
|
||||
)
|
||||
|
||||
+ def get_cost_price(self, product_cost_price=None):
|
||||
+ "Return the cost price of the move for computation"
|
||||
+ with Transaction().set_context(date=self.effective_date):
|
||||
+ if (self.from_location.type in {'supplier', 'production'}
|
||||
+ or self.to_location.type == 'supplier'):
|
||||
+ return self.unit_price_company
|
||||
+ elif product_cost_price is not None:
|
||||
+ return product_cost_price
|
||||
+ elif self.cost_price is not None:
|
||||
+ return self.cost_price
|
||||
+ else:
|
||||
+ return self.product.get_multivalue(
|
||||
+ 'cost_price', **self._cost_price_pattern)
|
||||
+
|
||||
@classmethod
|
||||
def _cost_price_context(cls, moves):
|
||||
pool = Pool()
|
||||
--
|
||||
2.25.1
|
Loading…
Reference in New Issue