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
This commit is contained in:
Cédric Krier committed 2021-07-28 00:37:51 +02:00
1 parent 737a459d23
commit b695344b63
8 files changed
+350 -1

No files matched your search

+2
View File
@@ -20,6 +20,8 @@ def register():
product.Product,
product.CostPrice,
product.CostPriceRevision,
stock.Configuration,
stock.ConfigurationLocation,
stock.Location,
stock.Move,
stock.ShipmentInternal,
+56
View File
@@ -87,6 +87,62 @@ class Product(metaclass=PoolMeta):
]
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()
+1
View File
@@ -70,6 +70,7 @@ tests_require = [
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:
+98
View File
@@ -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 @@ class Move(metaclass=PoolMeta):
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 @@ class Move(metaclass=PoolMeta):
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 @@ class Move(metaclass=PoolMeta):
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'
@@ -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')
+56 -1
View File
@@ -11,7 +11,7 @@ Imports::
>>> 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 @@ Forbid direct move between warehouses::
>>> 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')
+5
View File
@@ -33,4 +33,9 @@ def suite():
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
+1
View File
@@ -6,6 +6,7 @@ depends:
product
stock
extras_depend:
account_stock_continental
product_cost_fifo
product_cost_history
xml: