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:
1 parent
737a459d23
commit
b695344b63
8 files changed
+350
-1
No files matched your search
@@ -20,6 +20,8 @@ def register():
|
||||
product.Product,
|
||||
product.CostPrice,
|
||||
product.CostPriceRevision,
|
||||
stock.Configuration,
|
||||
stock.ConfigurationLocation,
|
||||
stock.Location,
|
||||
stock.Move,
|
||||
stock.ShipmentInternal,
|
||||
|
||||
+56
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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
|
||||
@@ -6,6 +6,7 @@ depends:
|
||||
product
|
||||
stock
|
||||
extras_depend:
|
||||
account_stock_continental
|
||||
product_cost_fifo
|
||||
product_cost_history
|
||||
xml:
|
||||
|
||||
Reference in new issue
Block a user