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: