diff --git a/__init__.py b/__init__.py index 57ba423a6c..60c7057b79 100644 --- a/__init__.py +++ b/__init__.py @@ -20,6 +20,8 @@ def register(): product.Product, product.CostPrice, product.CostPriceRevision, + stock.Configuration, + stock.ConfigurationLocation, stock.Location, stock.Move, stock.ShipmentInternal, diff --git a/product.py b/product.py index 2dd1c82556..f040eb9259 100644 --- a/product.py +++ b/product.py @@ -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() diff --git a/setup.py b/setup.py index fe776f4b7a..0901bd745f 100755 --- a/setup.py +++ b/setup.py @@ -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: diff --git a/stock.py b/stock.py index c7d4b8ad90..edaff3f7f3 100644 --- a/stock.py +++ b/stock.py @@ -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' diff --git a/tests/scenario_account_stock_continental.rst b/tests/scenario_account_stock_continental.rst new file mode 100644 index 0000000000..fad51a612e --- /dev/null +++ b/tests/scenario_account_stock_continental.rst @@ -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 --git a/tests/scenario_product_cost_warehouse.rst b/tests/scenario_product_cost_warehouse.rst index 9e36ae9663..87582c545a 100644 --- a/tests/scenario_product_cost_warehouse.rst +++ b/tests/scenario_product_cost_warehouse.rst @@ -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') diff --git a/tests/test_product_cost_warehouse.py b/tests/test_product_cost_warehouse.py index 1e5810faad..e353e5382f 100644 --- a/tests/test_product_cost_warehouse.py +++ b/tests/test_product_cost_warehouse.py @@ -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 diff --git a/tryton.cfg b/tryton.cfg index 0449e1d220..cc4239e74c 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -6,6 +6,7 @@ depends: product stock extras_depend: + account_stock_continental product_cost_fifo product_cost_history xml: