changeset c23f2df38d52 in modules/product_cost_warehouse:default
details: 
https://hg.tryton.org/modules/product_cost_warehouse?cmd=changeset&node=c23f2df38d52
description:
        Update and recompute cost price for move between warehouses

        - Book accounting when moving product between warehouses
        - Use the cost price of outgoing move as unit price of incoming moves 
of internal shipment

        issue10586
        review369311002
diffstat:

 __init__.py                                  |    2 +
 product.py                                   |   56 +++++++++++
 setup.py                                     |    1 +
 stock.py                                     |   98 ++++++++++++++++++++
 tests/scenario_account_stock_continental.rst |  131 +++++++++++++++++++++++++++
 tests/scenario_product_cost_warehouse.rst    |   57 +++++++++++-
 tests/test_product_cost_warehouse.py         |    5 +
 tryton.cfg                                   |    1 +
 8 files changed, 350 insertions(+), 1 deletions(-)

diffs (456 lines):

diff -r 2311b918f88b -r c23f2df38d52 __init__.py
--- a/__init__.py       Thu Jul 22 19:09:12 2021 +0200
+++ b/__init__.py       Wed Jul 28 00:37:51 2021 +0200
@@ -20,6 +20,8 @@
         product.Product,
         product.CostPrice,
         product.CostPriceRevision,
+        stock.Configuration,
+        stock.ConfigurationLocation,
         stock.Location,
         stock.Move,
         stock.ShipmentInternal,
diff -r 2311b918f88b -r c23f2df38d52 product.py
--- a/product.py        Thu Jul 22 19:09:12 2021 +0200
+++ b/product.py        Wed Jul 28 00:37:51 2021 +0200
@@ -88,6 +88,62 @@
         return domain
 
     @classmethod
+    def _domain_in_moves_cost(cls):
+        pool = Pool()
+        Company = pool.get('company.company')
+        context = Transaction().context
+        domain = super()._domain_in_moves_cost()
+        if context.get('company'):
+            company = Company(context['company'])
+            if company.cost_price_warehouse:
+                warehouse = context.get('warehouse')
+                domain = ['OR',
+                    domain,
+                    [
+                        ('from_location.type', '=', 'storage'),
+                        ('to_location.type', '=', 'storage'),
+                        ('from_location', 'not child_of', warehouse, 'parent'),
+                        ['OR',
+                            ('from_location.cost_warehouse', '!=', warehouse),
+                            ('from_location.cost_warehouse', '=', None),
+                            ],
+                        ['OR',
+                            ('to_location', 'child_of', warehouse, 'parent'),
+                            ('to_location.cost_warehouse', '=', warehouse),
+                            ],
+                        ]
+                    ]
+        return domain
+
+    @classmethod
+    def _domain_out_moves_cost(cls):
+        pool = Pool()
+        Company = pool.get('company.company')
+        context = Transaction().context
+        domain = super()._domain_out_moves_cost()
+        if context.get('company'):
+            company = Company(context['company'])
+            if company.cost_price_warehouse:
+                warehouse = context.get('warehouse')
+                domain = ['OR',
+                    domain,
+                    [
+                        ('from_location.type', '=', 'storage'),
+                        ('to_location.type', '=', 'storage'),
+                        ('to_location', 'not child_of', warehouse, 'parent'),
+                        ['OR',
+                            ('to_location.cost_warehouse', '!=', warehouse),
+                            ('to_location.cost_warehouse', '=', None),
+                            ],
+                        ['OR',
+                            ('from_location', 'child_of', warehouse, 'parent'),
+                            ('from_location.cost_warehouse', '=', warehouse),
+                            ],
+                        ]
+                    ]
+        return domain
+
+    @classmethod
     def _domain_storage_quantity(cls):
         pool = Pool()
         Company = pool.get('company.company')
diff -r 2311b918f88b -r c23f2df38d52 setup.py
--- a/setup.py  Thu Jul 22 19:09:12 2021 +0200
+++ b/setup.py  Wed Jul 28 00:37:51 2021 +0200
@@ -70,6 +70,7 @@
     get_require_version('proteus'),
     get_require_version('trytond_product_cost_fifo'),
     get_require_version('trytond_product_cost_history'),
+    get_require_version('trytond_account_stock_continental'),
     ]
 dependency_links = []
 if minor_version % 2:
diff -r 2311b918f88b -r c23f2df38d52 stock.py
--- a/stock.py  Thu Jul 22 19:09:12 2021 +0200
+++ b/stock.py  Wed Jul 28 00:37:51 2021 +0200
@@ -1,14 +1,41 @@
 # 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 trytond.i18n import gettext
 from trytond.model import fields
 from trytond.pool import PoolMeta, Pool
 from trytond.pyson import Eval, Bool
 from trytond.transaction import Transaction
 
+from trytond.modules.product import round_price
 from trytond.modules.stock.exceptions import MoveValidationError
 
 
+class Configuration(metaclass=PoolMeta):
+    __name__ = 'stock.configuration'
+
+    @classmethod
+    def __setup__(cls):
+        super().__setup__()
+        cls.shipment_internal_transit.domain = [
+            cls.shipment_internal_transit.domain,
+            ('cost_warehouse', '=', None),
+            ]
+
+
+class ConfigurationLocation(metaclass=PoolMeta):
+    __name__ = 'stock.configuration.location'
+
+    @classmethod
+    def __setup__(cls):
+        super().__setup__()
+        cls.shipment_internal_transit.domain = [
+            cls.shipment_internal_transit.domain,
+            ('cost_warehouse', '=', None),
+            ]
+
+
 class Location(metaclass=PoolMeta):
     __name__ = 'stock.location'
 
@@ -43,6 +70,52 @@
     def cost_warehouse(self):
         return self.from_cost_warehouse or self.to_cost_warehouse
 
+    @fields.depends('company', 'from_location', 'to_location')
+    def on_change_with_cost_price_required(self, name=None):
+        required = super().on_change_with_cost_price_required(name=name)
+        if (self.company and self.company.cost_price_warehouse
+                and self.from_location and self.to_location
+                and self.from_cost_warehouse != self.to_cost_warehouse):
+            required = True
+        return required
+
+    @classmethod
+    def get_unit_price_company(cls, moves, name):
+        pool = Pool()
+        ShipmentInternal = pool.get('stock.shipment.internal')
+        Uom = pool.get('product.uom')
+        prices = super().get_unit_price_company(moves, name)
+        for move in moves:
+            if (move.company.cost_price_warehouse
+                    and move.from_cost_warehouse != move.to_cost_warehouse
+                    and move.to_cost_warehouse
+                    and isinstance(move.shipment, ShipmentInternal)):
+                cost = total_qty = 0
+                for outgoing_move in move.shipment.outgoing_moves:
+                    if outgoing_move.product == move.product:
+                        qty = Uom.compute_qty(
+                            outgoing_move.uom, outgoing_move.quantity,
+                            move.product.default_uom)
+                        qty = Decimal(str(qty))
+                        cost += qty * outgoing_move.cost_price
+                        total_qty += qty
+                if cost and total_qty:
+                    cost_price = round_price(cost / total_qty)
+                    prices[move.id] = cost_price
+        return prices
+
+    def get_cost_price(self, product_cost_price=None):
+        pool = Pool()
+        ShipmentInternal = pool.get('stock.shipment.internal')
+        cost_price = super().get_cost_price(
+            product_cost_price=product_cost_price)
+        if (self.company.cost_price_warehouse
+                and self.from_cost_warehouse != self.to_cost_warehouse
+                and self.to_cost_warehouse
+                and isinstance(self.shipment, ShipmentInternal)):
+            cost_price = self.unit_price_company
+        return cost_price
+
     @classmethod
     def validate(cls, moves):
         pool = Pool()
@@ -72,6 +145,19 @@
                             from_=move.from_location.rec_name,
                             to=move.to_location.rec_name))
 
+    def _do(self):
+        cost_price = super()._do()
+        if (self.company.cost_price_warehouse
+                and self.from_location.type == 'storage'
+                and self.to_location.type == 'storage'
+                and self.from_cost_warehouse != self.to_cost_warehouse):
+            if self.from_cost_warehouse:
+                cost_price = self._compute_product_cost_price('out')
+            elif self.to_cost_warehouse:
+                cost_price = self._compute_product_cost_price(
+                    'in', self.unit_price_company)
+        return cost_price
+
     @property
     def _cost_price_pattern(self):
         pattern = super()._cost_price_pattern
@@ -112,6 +198,18 @@
         with Transaction().set_context(warehouse=warehouse):
             return super().get_fifo_move(quantity=quantity, date=date)
 
+    def _get_account_stock_move_type(self):
+        type_ = super()._get_account_stock_move_type()
+        if (self.company.cost_price_warehouse
+                and self.from_location.type == 'storage'
+                and self.to_location.type == 'storage'
+                and self.from_cost_warehouse != self.to_cost_warehouse):
+            if self.from_cost_warehouse and not self.to_cost_warehouse:
+                type_ = 'out_warehouse'
+            elif not self.from_cost_warehouse and self.to_cost_warehouse:
+                type_ = 'in_warehouse'
+        return type_
+
 
 class ShipmentInternal(metaclass=PoolMeta):
     __name__ = 'stock.shipment.internal'
diff -r 2311b918f88b -r c23f2df38d52 
tests/scenario_account_stock_continental.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/scenario_account_stock_continental.rst      Wed Jul 28 00:37:51 
2021 +0200
@@ -0,0 +1,131 @@
+==================================
+Account Stock Continental Scenario
+==================================
+
+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)
+    >>> from trytond.modules.account.tests.tools import (
+    ...     create_fiscalyear, create_chart, get_accounts)
+    >>> from trytond.modules.account_stock_continental.tests.tools import (
+    ...     add_stock_accounts)
+
+Activate product_cost_warehouse::
+
+    >>> config = activate_modules([
+    ...         'product_cost_warehouse', 'account_stock_continental'])
+
+    >>> Location = Model.get('stock.location')
+    >>> Product = Model.get('product.product')
+    >>> ProductCategory = Model.get('product.category')
+    >>> ProductConfiguration = Model.get('product.configuration')
+    >>> ProductTemplate = Model.get('product.template')
+    >>> ProductUom = Model.get('product.uom')
+    >>> ShipmentInternal = Model.get('stock.shipment.internal')
+    >>> StockConfiguration = Model.get('stock.configuration')
+
+Create company::
+
+    >>> _ = create_company()
+    >>> company = get_company()
+
+Create fiscal year::
+
+    >>> fiscalyear = create_fiscalyear(company)
+    >>> fiscalyear.account_stock_method = 'continental'
+    >>> fiscalyear.click('create_period')
+
+Create chart of accounts::
+
+    >>> _ = create_chart(company)
+    >>> accounts = add_stock_accounts(get_accounts(company), company)
+
+Create product category::
+
+    >>> account_category = ProductCategory(name="Account Category")
+    >>> account_category.accounting = True
+    >>> account_category.account_expense = accounts['expense']
+    >>> account_category.account_revenue = accounts['revenue']
+    >>> account_category.account_stock = accounts['stock']
+    >>> account_category.account_stock_in = accounts['stock_expense']
+    >>> account_category.account_stock_out = accounts['stock_expense']
+    >>> account_category.save()
+
+Create product::
+
+    >>> 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'
+    >>> template.account_category = account_category
+    >>> product, = template.products
+    >>> template.save()
+    >>> product, = template.products
+
+Set cost per warehouse::
+
+    >>> product_config = ProductConfiguration(1)
+    >>> product_config.cost_price_warehouse = True
+    >>> product_config.save()
+
+Create stock locations::
+
+    >>> warehouse1, = Location.find([('code', '=', 'WH')])
+    >>> warehouse2, = warehouse1.duplicate(default={'name': "Warhouse bis"})
+    >>> transit = Location(name="Transit", type='storage')
+    >>> transit.save()
+    >>> stock_config = StockConfiguration(1)
+    >>> stock_config.shipment_internal_transit = transit
+    >>> stock_config.save()
+    >>> supplier_loc, = Location.find([('code', '=', 'SUP')])
+
+Make 1 unit of product available @ 100 on 1st warehouse::
+
+    >>> StockMove = Model.get('stock.move')
+    >>> move = StockMove()
+    >>> move.product = product
+    >>> move.quantity = 1
+    >>> move.from_location = supplier_loc
+    >>> move.to_location = warehouse1.storage_location
+    >>> move.unit_price = Decimal('100')
+    >>> move.currency = company.currency
+    >>> move.click('do')
+
+    >>> accounts['stock'].reload()
+    >>> accounts['stock'].balance
+    Decimal('100.00')
+
+Transfer 1 product between warehouses::
+
+    >>> shipment = ShipmentInternal()
+    >>> shipment.from_location = warehouse1.storage_location
+    >>> shipment.to_location = warehouse2.storage_location
+    >>> move = shipment.moves.new()
+    >>> move.from_location = shipment.from_location
+    >>> move.to_location = shipment.to_location
+    >>> move.product = product
+    >>> move.quantity = 1
+    >>> shipment.click('wait')
+    >>> shipment.click('assign_force')
+
+    >>> shipment.click('ship')
+    >>> shipment.state
+    'shipped'
+    >>> accounts['stock'].reload()
+    >>> accounts['stock'].balance
+    Decimal('0.00')
+
+    >>> shipment.click('done')
+    >>> shipment.state
+    'done'
+    >>> accounts['stock'].reload()
+    >>> accounts['stock'].balance
+    Decimal('100.00')
diff -r 2311b918f88b -r c23f2df38d52 tests/scenario_product_cost_warehouse.rst
--- a/tests/scenario_product_cost_warehouse.rst Thu Jul 22 19:09:12 2021 +0200
+++ b/tests/scenario_product_cost_warehouse.rst Wed Jul 28 00:37:51 2021 +0200
@@ -11,7 +11,7 @@
     >>> from trytond.modules.company.tests.tools import (
     ...     create_company, get_company)
 
-Activate product_cost_warehouse::
+Activate modules::
 
     >>> config = activate_modules('product_cost_warehouse')
 
@@ -149,3 +149,58 @@
     >>> move.click('do')
     >>> move.state
     'done'
+
+Transfer 1 product between warehouses::
+
+    >>> ShipmentInternal = Model.get('stock.shipment.internal')
+    >>> shipment = ShipmentInternal()
+    >>> shipment.from_location = warehouse1.storage_location
+    >>> shipment.to_location = warehouse2.storage_location
+    >>> move = shipment.moves.new()
+    >>> move.from_location = shipment.from_location
+    >>> move.to_location = shipment.to_location
+    >>> move.product = product
+    >>> move.quantity = 1
+    >>> shipment.click('wait')
+    >>> shipment.state
+    'waiting'
+    >>> shipment.click('assign_force')
+    >>> shipment.state
+    'assigned'
+
+    >>> shipment.click('ship')
+    >>> shipment.state
+    'shipped'
+    >>> move, = shipment.outgoing_moves
+    >>> move.state
+    'done'
+    >>> move.cost_price
+    Decimal('90.0000')
+
+    >>> shipment.click('done')
+    >>> shipment.state
+    'done'
+    >>> move, = shipment.incoming_moves
+    >>> move.state
+    'done'
+    >>> move.cost_price
+    Decimal('85.0000')
+
+Recompute cost price for both warehouses::
+
+    >>> for warehouse in [warehouse1, warehouse2]:
+    ...     with config.set_context(warehouse=warehouse.id):
+    ...         recompute = Wizard('product.recompute_cost_price', [product])
+    ...         recompute.execute('recompute')
+
+Check cost prices::
+
+    >>> with config.set_context(warehouse=warehouse1.id):
+    ...     product = Product(product.id)
+    >>> product.cost_price
+    Decimal('90.0000')
+
+    >>> with config.set_context(warehouse=warehouse2.id):
+    ...     product = Product(product.id)
+    >>> product.cost_price
+    Decimal('85.0000')
diff -r 2311b918f88b -r c23f2df38d52 tests/test_product_cost_warehouse.py
--- a/tests/test_product_cost_warehouse.py      Thu Jul 22 19:09:12 2021 +0200
+++ b/tests/test_product_cost_warehouse.py      Wed Jul 28 00:37:51 2021 +0200
@@ -33,4 +33,9 @@
             tearDown=doctest_teardown, encoding='utf-8',
             checker=doctest_checker,
             optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
+    suite.addTests(doctest.DocFileSuite(
+            'scenario_account_stock_continental.rst',
+            tearDown=doctest_teardown, encoding='utf-8',
+            checker=doctest_checker,
+            optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
     return suite
diff -r 2311b918f88b -r c23f2df38d52 tryton.cfg
--- a/tryton.cfg        Thu Jul 22 19:09:12 2021 +0200
+++ b/tryton.cfg        Wed Jul 28 00:37:51 2021 +0200
@@ -6,6 +6,7 @@
     product
     stock
 extras_depend:
+    account_stock_continental
     product_cost_fifo
     product_cost_history
 xml:

Reply via email to