changeset 6542d4065178 in modules/product_cost_history:default
details: 
https://hg.tryton.org/modules/product_cost_history?cmd=changeset;node=6542d4065178
description:
        Use stock move as history source

        issue8858
        review280481002
diffstat:

 CHANGELOG                               |    2 +
 __init__.py                             |    3 +-
 doc/index.rst                           |   11 +-
 product.py                              |  140 +++++++++++++++++++++++++---
 setup.py                                |    4 +-
 tests/scenario_product_cost_history.rst |  156 ++++++++++++++++++++++++++++++++
 tests/test_product_cost_history.py      |    8 +
 tryton.cfg                              |    1 +
 view/product_cost_history_tree.xml      |    3 +-
 9 files changed, 301 insertions(+), 27 deletions(-)

diffs (443 lines):

diff -r 99f760c933cf -r 6542d4065178 CHANGELOG
--- a/CHANGELOG Wed Dec 04 11:09:36 2019 +0100
+++ b/CHANGELOG Sun Jan 26 22:09:09 2020 +0100
@@ -1,3 +1,5 @@
+* Use stock move as history source for goods and assets
+
 Version 5.4.0 - 2019-11-04
 * Bug fixes (see mercurial logs for details)
 
diff -r 99f760c933cf -r 6542d4065178 __init__.py
--- a/__init__.py       Wed Dec 04 11:09:36 2019 +0100
+++ b/__init__.py       Sun Jan 26 22:09:09 2020 +0100
@@ -9,6 +9,7 @@
 
 def register():
     Pool.register(
-        product.ProductCostPrice,
+        product.Product,
+        product.CostPrice,
         product.ProductCostHistory,
         module='product_cost_history', type_='model')
diff -r 99f760c933cf -r 6542d4065178 doc/index.rst
--- a/doc/index.rst     Wed Dec 04 11:09:36 2019 +0100
+++ b/doc/index.rst     Sun Jan 26 22:09:09 2020 +0100
@@ -1,8 +1,9 @@
 Product Cost History Module
 ###########################
 
-The Product Cost History Module enable the historization of the cost
-price on the product. Every time the product cost is updated, the new
-value is stored in an historization table. The module also add a *Cost
-History* button on the bottom of the product form that allow to look
-at the historical values of the cost price.
+The Product Cost History Module adds a *Cost History* relate on the product
+form showing the cost price evolution of the product. The history is based on
+the cost price stored on the incoming stock moves for goods and assets and
+based on the history table for service.
+When a historic cost price is needed the value is taken from this history for
+goods and assets.
diff -r 99f760c933cf -r 6542d4065178 product.py
--- a/product.py        Wed Dec 04 11:09:36 2019 +0100
+++ b/product.py        Sun Jan 26 22:09:09 2020 +0100
@@ -1,16 +1,58 @@
 # 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 sql import Column
+from decimal import Decimal
+
+from sql import Column, Literal, Window
 from sql.aggregate import Max
 from sql.conditionals import Coalesce
+from sql.functions import CurrentTimestamp, LastValue
 
 from trytond.model import ModelView, ModelSQL, fields
 from trytond.pool import Pool, PoolMeta
+from trytond.transaction import Transaction
 
-__all__ = ['ProductCostPrice', 'ProductCostHistory']
+
+def convert_from(table, tables):
+    right, condition = tables[None]
+    if table:
+        table = table.join(right, condition=condition)
+    else:
+        table = right
+    for k, sub_tables in tables.items():
+        if k is None:
+            continue
+        table = convert_from(table, sub_tables)
+    return table
 
 
-class ProductCostPrice(metaclass=PoolMeta):
+class Product(metaclass=PoolMeta):
+    __name__ = 'product.product'
+
+    def get_multivalue(self, name, **pattern):
+        context = Transaction().context
+        if (name == 'cost_price'
+                and context.get('_datetime')
+                and self.type in ['goods', 'assets']):
+            cost_price = self.get_cost_price_at(
+                context['_datetime'].date())
+            if cost_price is not None:
+                return cost_price
+        return super().get_multivalue(name, **pattern)
+
+    def get_cost_price_at(self, date):
+        pool = Pool()
+        CostHistory = pool.get('product.product.cost_history')
+        records = CostHistory.search([
+                ('date', '<=', date),
+                ('product', '=', self.id),
+                ], limit=1, order=[('date', 'DESC')])
+        if records:
+            record, = records
+            return record.cost_price.quantize(
+                Decimal(str(10. ** -self.__class__.cost_price.digits[1])))
+
+
+class CostPrice(metaclass=PoolMeta):
     __name__ = 'product.cost_price'
     _history = True
 
@@ -19,7 +61,7 @@
     'History of Product Cost'
     __name__ = 'product.product.cost_history'
     product = fields.Many2One('product.product', "Product")
-    date = fields.DateTime('Date')
+    date = fields.Date('Date')
     cost_price = fields.Numeric('Cost Price')
 
     @classmethod
@@ -30,20 +72,82 @@
     @classmethod
     def table_query(cls):
         pool = Pool()
-        ProductCostPrice = pool.get('product.cost_price')
-        history = ProductCostPrice.__table_history__()
-        return history.select(Max(Column(history, '__id')).as_('id'),
-            Max(history.create_uid).as_('create_uid'),
-            Max(history.create_date).as_('create_date'),
-            Max(history.write_uid).as_('write_uid'),
-            Max(history.write_date).as_('write_date'),
-            Coalesce(history.write_date,
-                history.create_date).as_('date'),
-            history.product.as_('product'),
-            history.cost_price.as_('cost_price'),
-            group_by=(history.id,
-                Coalesce(history.write_date, history.create_date),
-                history.product, history.cost_price))
+        Move = pool.get('stock.move')
+        Product = pool.get('product.product')
+        Template = pool.get('product.template')
+        CostPrice = pool.get('product.cost_price')
+        move = Move.__table__()
+        product = Product.__table__()
+        template = Template.__table__()
+        history = CostPrice.__table_history__()
+        database = Transaction().database
+
+        tables, clause = Move.search_domain([
+                ('state', '=', 'done'),
+                Product()._domain_moves_cost(),
+                # Incoming moves
+                ('to_location.type', '=', 'storage'),
+                ('from_location.type', '!=', 'storage'),
+                ], tables={
+                None: (move, None),
+                })
+
+        if database.has_window_functions():
+            window = Window(
+                [move.effective_date, move.product],
+                order_by=[move.write_date.asc, move.id.asc])
+            cost_price = LastValue(move.cost_price, window=window)
+        else:
+            cost_price = cls.cost_price.sql_cast(move.cost_price)
+
+        move_history = convert_from(None, tables).select(
+            (move.id * 2).as_('id'),
+            move.effective_date.as_('date'),
+            move.product.as_('product'),
+            cost_price.as_('cost_price'),
+            where=clause)
+        query = move_history.select(
+            Max(move_history.id).as_('id'),
+            Literal(0).as_('create_uid'),
+            CurrentTimestamp().as_('create_date'),
+            Literal(None).as_('write_uid'),
+            Literal(None).as_('write_date'),
+            move_history.date.as_('date'),
+            move_history.product.as_('product'),
+            Max(move_history.cost_price).as_('cost_price'),
+            group_by=[move_history.date, move_history.product])
+
+        price_datetime = Coalesce(history.write_date, history.create_date)
+        price_date = cls.date.sql_cast(price_datetime)
+        if database.has_window_functions():
+            window = Window(
+                [price_date, history.product],
+                order_by=[price_datetime.asc])
+            cost_price = LastValue(history.cost_price, window=window)
+        else:
+            cost_price = cls.cost_price.sql_cast(history.cost_price)
+
+        price_history = (history
+            .join(product, condition=history.product == product.id)
+            .join(template, condition=product.template == template.id)
+            .select(
+                (Column(history, '__id') * 2 + 1).as_('id'),
+                price_date.as_('date'),
+                history.product.as_('product'),
+                cost_price.as_('cost_price'),
+                where=~template.type.in_(['goods', 'assets'])))
+
+        query |= price_history.select(
+            Max(price_history.id).as_('id'),
+            Literal(0).as_('create_uid'),
+            CurrentTimestamp().as_('create_date'),
+            Literal(None).as_('write_uid'),
+            Literal(None).as_('write_date'),
+            price_history.date.as_('date'),
+            price_history.product.as_('product'),
+            Max(price_history.cost_price).as_('cost_price'),
+            group_by=[price_history.date, price_history.product])
+        return query
 
     def get_rec_name(self, name):
         return str(self.date)
diff -r 99f760c933cf -r 6542d4065178 setup.py
--- a/setup.py  Wed Dec 04 11:09:36 2019 +0100
+++ b/setup.py  Sun Jan 26 22:09:09 2020 +0100
@@ -57,6 +57,7 @@
         requires.append(get_require_version('trytond_%s' % dep))
 requires.append(get_require_version('trytond'))
 
+tests_require = [get_require_version('proteus')]
 dependency_links = []
 if minor_version % 2:
     dependency_links.append('https://trydevpi.tryton.org/')
@@ -84,7 +85,7 @@
         ),
     package_data={
         'trytond.modules.product_cost_history': (info.get('xml', [])
-            + ['tryton.cfg', 'view/*.xml', 'locale/*.po']),
+            + ['tryton.cfg', 'view/*.xml', 'locale/*.po', 'tests/*.rst']),
         },
     classifiers=[
         'Development Status :: 5 - Production/Stable',
@@ -134,4 +135,5 @@
     """,
     test_suite='tests',
     test_loader='trytond.test_loader:Loader',
+    tests_require=tests_require,
     )
diff -r 99f760c933cf -r 6542d4065178 tests/scenario_product_cost_history.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/scenario_product_cost_history.rst   Sun Jan 26 22:09:09 2020 +0100
@@ -0,0 +1,156 @@
+====================
+Product Cost History
+====================
+
+Imports::
+
+    >>> import datetime as dt
+    >>> import time
+    >>> 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()
+    >>> now = dt.datetime.now()
+
+Install product_cost_history Module::
+
+    >>> config = activate_modules('product_cost_history')
+
+Create company::
+
+    >>> _ = create_company()
+    >>> company = get_company()
+
+Create product::
+
+    >>> ProductUom = Model.get('product.uom')
+    >>> ProductTemplate = Model.get('product.template')
+    >>> Product = Model.get('product.product')
+    >>> unit, = ProductUom.find([('name', '=', 'Unit')])
+
+    >>> template = ProductTemplate()
+    >>> template.name = 'Product'
+    >>> template.default_uom = unit
+    >>> template.type = 'goods'
+    >>> template.list_price = Decimal('200.0000')
+    >>> product, = template.products
+    >>> product.cost_price = Decimal('100.0000')
+    >>> 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')])
+
+Cost history is empty::
+
+    >>> ProductCostHistory = Model.get('product.product.cost_history')
+    >>> ProductCostHistory.find([])
+    []
+
+Create some moves at different dates and with different cost::
+
+    >>> StockMove = Model.get('stock.move')
+    >>> product.cost_price = Decimal('100.0000')
+    >>> product.save()
+    >>> StockMove(
+    ...     product=product,
+    ...     quantity=1,
+    ...     from_location=supplier_loc,
+    ...     to_location=storage_loc,
+    ...     unit_price=Decimal('100.0000'),
+    ...     effective_date=today - dt.timedelta(days=2)).click('do')
+    >>> modify_cost_price = Wizard('product.modify_cost_price', [product])
+    >>> modify_cost_price.form.cost_price = '90.0000'
+    >>> modify_cost_price.execute('modify')
+    >>> StockMove(
+    ...     product=product,
+    ...     quantity=1,
+    ...     from_location=supplier_loc,
+    ...     to_location=storage_loc,
+    ...     unit_price=Decimal('90.0000'),
+    ...     effective_date=today - dt.timedelta(days=1)).click('do')
+    >>> modify_cost_price = Wizard('product.modify_cost_price', [product])
+    >>> modify_cost_price.form.cost_price = '120.0000'
+    >>> modify_cost_price.execute('modify')
+    >>> StockMove(
+    ...     product=product,
+    ...     quantity=1,
+    ...     from_location=supplier_loc,
+    ...     to_location=storage_loc,
+    ...     unit_price=Decimal('120.0000'),
+    ...     effective_date=today - dt.timedelta(days=1)).click('do')
+    >>> modify_cost_price = Wizard('product.modify_cost_price', [product])
+    >>> modify_cost_price.form.cost_price = '110.0000'
+    >>> modify_cost_price.execute('modify')
+    >>> StockMove(
+    ...     product=product,
+    ...     quantity=1,
+    ...     from_location=supplier_loc,
+    ...     to_location=storage_loc,
+    ...     unit_price=Decimal('110.0000'),
+    ...     effective_date=today).click('do')
+
+
+Check cost history::
+
+    >>> order = [('date', 'ASC')]
+    >>> [c.cost_price for c in ProductCostHistory.find([], order=order)] == [
+    ...     Decimal('100.0000'), Decimal('120.0000'), Decimal('110.0000')]
+    True
+    >>> [str(c.date) for c in ProductCostHistory.find([], order=order)] == [
+    ...     str(today - dt.timedelta(days=2)),
+    ...     str(today - dt.timedelta(days=1)),
+    ...     str(today)]
+    True
+
+Check cost price history on product::
+
+    >>> product.reload()
+    >>> product.cost_price
+    Decimal('110.0000')
+
+    >>> with config.set_context(_datetime=now - dt.timedelta(days=3)):
+    ...     product = Product(product.id)
+    >>> product.cost_price
+    Decimal('0')
+
+    >>> with config.set_context(_datetime=now - dt.timedelta(days=2)):
+    ...     product = Product(product.id)
+    >>> product.cost_price
+    Decimal('100.0000')
+
+    >>> with config.set_context(_datetime=now - dt.timedelta(days=1)):
+    ...     product = Product(product.id)
+    >>> product.cost_price
+    Decimal('120.0000')
+
+
+Create service::
+
+    >>> template = ProductTemplate()
+    >>> template.name = 'Service'
+    >>> template.default_uom = unit
+    >>> template.type = 'service'
+    >>> template.list_price = Decimal('50.0000')
+    >>> service, = template.products
+    >>> service.cost_price = Decimal('30.0000')
+    >>> template.save()
+    >>> sevice, = template.products
+
+Update cost price::
+
+    >>> service.cost_price += 5
+    >>> service.save()
+
+Check cost history::
+
+    >>> history, = ProductCostHistory.find([('product', '=', service.id)])
+    >>> history.cost_price == Decimal('35.0000')
+    True
diff -r 99f760c933cf -r 6542d4065178 tests/test_product_cost_history.py
--- a/tests/test_product_cost_history.py        Wed Dec 04 11:09:36 2019 +0100
+++ b/tests/test_product_cost_history.py        Sun Jan 26 22:09:09 2020 +0100
@@ -1,8 +1,11 @@
 # 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 doctest
 import unittest
 import trytond.tests.test_tryton
 from trytond.tests.test_tryton import ModuleTestCase
+from trytond.tests.test_tryton import doctest_teardown
+from trytond.tests.test_tryton import doctest_checker
 
 
 class ProductCostHistoryTestCase(ModuleTestCase):
@@ -14,4 +17,9 @@
     suite = trytond.tests.test_tryton.suite()
     suite.addTests(unittest.TestLoader().loadTestsFromTestCase(
         ProductCostHistoryTestCase))
+    suite.addTests(doctest.DocFileSuite(
+            'scenario_product_cost_history.rst',
+            tearDown=doctest_teardown, encoding='utf-8',
+            checker=doctest_checker,
+            optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
     return suite
diff -r 99f760c933cf -r 6542d4065178 tryton.cfg
--- a/tryton.cfg        Wed Dec 04 11:09:36 2019 +0100
+++ b/tryton.cfg        Sun Jan 26 22:09:09 2020 +0100
@@ -3,5 +3,6 @@
 depends:
     ir
     product
+    stock
 xml:
     product.xml
diff -r 99f760c933cf -r 6542d4065178 view/product_cost_history_tree.xml
--- a/view/product_cost_history_tree.xml        Wed Dec 04 11:09:36 2019 +0100
+++ b/view/product_cost_history_tree.xml        Sun Jan 26 22:09:09 2020 +0100
@@ -3,7 +3,6 @@
 this repository contains the full copyright notices and license terms. -->
 <tree>
     <field name="product"/>
-    <field name="date" widget="date"/>
-    <field name="date" widget="time" string="Time"/>
+    <field name="date"/>
     <field name="cost_price"/>
 </tree>

Reply via email to