changeset f3fcb65989bf in modules/product_cost_fifo:default
details: 
https://hg.tryton.org/modules/product_cost_fifo?cmd=changeset;node=f3fcb65989bf
description:
        Update cost of moves when recomputing product cost

        issue8795
        issue7271
        review260381003
diffstat:

 CHANGELOG                                                 |    2 +
 product.py                                                |  232 ++++++++++++-
 tests/scenario_product_cost_fifo_recompute_cost_price.rst |  126 +++++++
 tests/test_product_cost_fifo.py                           |    5 +
 4 files changed, 332 insertions(+), 33 deletions(-)

diffs (416 lines):

diff -r 694da1aa4572 -r f3fcb65989bf CHANGELOG
--- a/CHANGELOG Wed Dec 04 11:09:36 2019 +0100
+++ b/CHANGELOG Sun Jan 26 15:29:23 2020 +0100
@@ -1,3 +1,5 @@
+* Update cost of moves when recomputing product cost
+
 Version 5.4.0 - 2019-11-04
 * Bug fixes (see mercurial logs for details)
 
diff -r 694da1aa4572 -r f3fcb65989bf product.py
--- a/product.py        Wed Dec 04 11:09:36 2019 +0100
+++ b/product.py        Sun Jan 26 15:29:23 2020 +0100
@@ -1,6 +1,9 @@
 # This file is part of Tryton.  The COPYRIGHT file at the top level of
 # this repository contains the full copyright notices and license terms.
-import datetime
+import datetime as dt
+from decimal import Decimal
+
+from trytond.config import config
 from trytond.transaction import Transaction
 from trytond.pool import Pool, PoolMeta
 
@@ -21,34 +24,26 @@
 class Product(metaclass=PoolMeta):
     __name__ = 'product.product'
 
-    def _get_available_fifo_moves(self):
+    def _get_available_fifo_moves(self, date=None, offset=0, limit=None):
         pool = Pool()
         Move = pool.get('stock.move')
-        return Move.search([
-                ('product', '=', self.id),
-                ('state', '=', 'done'),
-                self._domain_moves_cost,
-                ('fifo_quantity_available', '>', 0),
-                ('to_location.type', '=', 'storage'),
-                ('from_location.type', 'in', ['supplier', 'production']),
-                ('to_location.type', '=', 'storage'),
-                ], order=[('effective_date', 'DESC'), ('id', 'DESC')])
 
-    def _get_fifo_quantity(self):
-        pool = Pool()
-        Location = pool.get('stock.location')
+        domain = [
+            ('product', '=', self.id),
+            self._domain_moves_cost(),
+            ('from_location.type', 'in', ['supplier', 'production']),
+            ('to_location.type', '=', 'storage'),
+            ]
+        if not date:
+            domain.append(('fifo_quantity_available', '>', 0))
+        else:
+            domain.append(('effective_date', '<=', date))
+        return Move.search(
+            domain,
+            offset=offset, limit=limit,
+            order=[('effective_date', 'DESC'), ('id', 'DESC')])
 
-        locations = Location.search([
-            ('type', '=', 'storage'),
-            ])
-        stock_date_end = datetime.date.today()
-        location_ids = [l.id for l in locations]
-        with Transaction().set_context(
-                locations=location_ids,
-                stock_date_end=stock_date_end):
-            return self.__class__(self.id).quantity
-
-    def get_fifo_move(self, quantity=0.0):
+    def get_fifo_move(self, quantity=0.0, date=None):
         '''
         Return a list of (move, qty) where move is the move to be
         consumed and qty is the quantity (in the product default uom)
@@ -58,13 +53,30 @@
         pool = Pool()
         Uom = pool.get('product.uom')
 
-        avail_qty = self._get_fifo_quantity()
+        avail_qty = self._get_storage_quantity(date=date)
+        if date:
+            # On recomputation, we must pretend
+            # outgoing moves are not yet done.
+            avail_qty += quantity
         fifo_moves = []
-        moves = self._get_available_fifo_moves()
-        for move in moves:
-            qty = Uom.compute_qty(move.uom,
-                    move.fifo_quantity_available,
-                    self.default_uom, round=False)
+
+        size = config.getint('cache', 'record')
+
+        def moves():
+            offset, limit = 0, size
+            while True:
+                moves = self._get_available_fifo_moves(
+                    date=date, offset=offset, limit=limit)
+                if not moves:
+                    break
+                for move in moves:
+                    yield move
+                offset += size
+
+        for move in moves():
+            qty = move.fifo_quantity_available if not date else move.quantity
+            qty = Uom.compute_qty(
+                move.uom, qty, self.default_uom, round=False)
             avail_qty -= qty
 
             if avail_qty <= quantity:
@@ -78,5 +90,159 @@
         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 -r 694da1aa4572 -r f3fcb65989bf 
tests/scenario_product_cost_fifo_recompute_cost_price.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/scenario_product_cost_fifo_recompute_cost_price.rst Sun Jan 26 
15:29:23 2020 +0100
@@ -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 -r 694da1aa4572 -r f3fcb65989bf tests/test_product_cost_fifo.py
--- a/tests/test_product_cost_fifo.py   Wed Dec 04 11:09:36 2019 +0100
+++ b/tests/test_product_cost_fifo.py   Sun Jan 26 15:29:23 2020 +0100
@@ -32,4 +32,9 @@
             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

Reply via email to