import re from dateutil.relativedelta import relativedelta from odoo import Command, fields from odoo.tools.misc import clean_context from odoo.tests import Form from odoo.addons.base.tests.common import BaseCommon class TestStockValuationCommon(BaseCommon): # Override @classmethod def _create_company(cls, **create_values): company = super()._create_company(**create_values) cls.env["account.chart.template"]._load( "generic_coa", company, install_demo=False ) return company # HELPER def _create_account_move(self, move_type, product, quantity=1.0, price_unit=1.0, post=True, **kwargs): invoice_vals = { "partner_id": self.vendor.id, "move_type": move_type, "invoice_date": kwargs.get('invoice_date', fields.Date.today()), "invoice_line_ids": [], } if kwargs.get('reversed_entry_id'): invoice_vals["reversed_entry_id"] = kwargs['reversed_entry_id'] invoice = self.env["account.move"].create(invoice_vals) product_uom = kwargs.get('product_uom') or product.uom_id self.env["account.move.line"].create({ "move_id": invoice.id, "display_type": "product", "name": "test line", "price_unit": price_unit, "quantity": quantity, "product_id": product.id, "product_uom_id": product_uom.id, "tax_ids": [(5, 0, 0)], }) if post: invoice.action_post() return invoice def _create_invoice(self, product=None, quantity=1.0, price_unit=None, post=True, **kwargs): return self._create_account_move("out_invoice", product, quantity, price_unit, post, **kwargs) def _create_bill(self, product=None, quantity=1.0, price_unit=None, post=True, **kwargs): return self._create_account_move("in_invoice", product, quantity, price_unit, post, **kwargs) def _create_credit_note(self, product, quantity=1.0, price_unit=1.0, post=True, **kwargs): move_type = kwargs.pop("move_type", "out_refund") return self._create_account_move(move_type, product, quantity, price_unit, post, **kwargs) def _refund(self, move_to_refund, quantity=None, post=True): reversal = self.env['account.move.reversal'].with_context(active_ids=move_to_refund.ids, active_model='account.move').create({ 'journal_id': move_to_refund.journal_id.id, }) credit_note = self.env['account.move'].browse(reversal.refund_moves()['res_id']) if quantity: credit_note.line_ids.quantity = quantity if post: credit_note.action_post() return credit_note def _close(self, auto_post=True, at_date=None): action = self.company.action_close_stock_valuation(at_date=at_date, auto_post=auto_post) return action['res_id'] and self.env['account.move'].browse(action['res_id']) def _use_price_diff(self): self.account_price_diff = self.env['account.account'].create({ 'name': 'Price Difference Account', 'code': '100102', 'account_type': 'asset_current', }) self.category_standard.property_price_difference_account_id = self.account_price_diff.id self.category_standard_auto.property_price_difference_account_id = self.account_price_diff.id return self.account_price_diff def _use_route_mto(self, product): if not self.route_mto.active: self.route_mto.active = True product.route_ids = [(4, self.route_mto.id)] return product def _use_multi_currencies(self, rates=None): date_1 = fields.Date.today() date_2 = date_1 + relativedelta(days=1) date_3 = date_2 + relativedelta(days=1) rates = rates or [ (fields.Date.to_string(date_1), 1), (fields.Date.to_string(date_2), 2), (fields.Date.to_string(date_3), 3), ] self.other_currency = self.setup_other_currency('EUR', rates=rates) def _use_multi_warehouses(self): self.other_warehouse = self.env['stock.warehouse'].create({ 'name': 'Other Warehouse', 'code': 'OWH', 'company_id': self.company.id, }) def _use_inventory_location_accounting(self): self.account_inventory = self.env['account.account'].create({ 'name': 'Inventory Account', 'code': '100101', 'account_type': 'asset_current', }) inventory_locations = self.env['stock.location'].search([('usage', '=', 'inventory'), ('company_id', '=', self.company.id)]) inventory_locations.valuation_account_id = self.account_inventory.id return self.account_inventory # Moves def _make_in_move(self, product, quantity, unit_cost=None, create_picking=False, company=None, **kwargs, ): """ Helper to create and validate a receipt move. :param product: Product to move :param quantity: Quantity to move :param unit_cost: Price unit :param create_picking: Create the picking containing the created move :param company: If set, the move is created in that company's context and warehouse defaults are resolved from that company's warehouse. :param **kwargs: stock.move fields that you can override ''location_id: origin location for the move ''location_dest_id: destination location for the move ''lot_ids: list of lot (split among the quantity) ''picking_type_id: picking type ''uom_id: Unit of measure ''owner_id: Consignment owner """ env = self.env['stock.move'].with_company(company).env if company else self.env if company: warehouse = env['stock.warehouse'].search([('company_id', '=', company.id)], limit=1) default_dest = warehouse.lot_stock_id.id default_picking_type = warehouse.in_type_id.id else: default_dest = self.stock_location.id default_picking_type = self.picking_type_in.id product_qty = quantity if kwargs.get('uom_id'): uom = self.env['uom.uom'].browse(kwargs.get('uom_id')) product_qty = uom._compute_quantity(quantity, product.uom_id) move_vals = { 'product_id': product.id, 'location_id': kwargs.get('location_id', self.supplier_location.id), 'location_dest_id': kwargs.get('location_dest_id', default_dest), 'product_uom': kwargs.get('uom_id', self.uom.id), 'product_uom_qty': quantity, 'picking_type_id': kwargs.get('picking_type_id', default_picking_type), } if unit_cost: move_vals['value_manual'] = unit_cost * product_qty move_vals['price_unit'] = unit_cost else: move_vals['value_manual'] = product.standard_price * product_qty in_move = env['stock.move'].create(move_vals) if create_picking: picking = env['stock.picking'].create({ 'picking_type_id': in_move.picking_type_id.id, 'location_id': in_move.location_id.id, 'location_dest_id': in_move.location_dest_id.id, 'owner_id': kwargs.get('owner_id', False), 'partner_id': kwargs.get('partner_id', False), }) in_move.picking_id = picking.id in_move._action_confirm() lot_ids = kwargs.get('lot_ids') if lot_ids: in_move.move_line_ids.unlink() in_move.move_line_ids = [Command.create({ 'location_id': self.supplier_location.id, 'location_dest_id': in_move.location_dest_id.id, 'quantity': quantity / len(lot_ids), 'product_id': product.id, 'lot_id': lot.id, }) for lot in lot_ids] else: in_move._action_assign() if not create_picking and kwargs.get('owner_id'): in_move.move_line_ids.owner_id = kwargs.get('owner_id') in_move.picked = True if create_picking: picking.button_validate() else: in_move._action_done() return in_move def _make_out_move(self, product, quantity, force_assign=True, create_picking=False, company=None, **kwargs, ): """ Helper to create and validate a delivery move. :param product: Product to move :param quantity: Quantity to move :param force_assign: Bypass reservation to force the required quantity :param create_picking: Create the picking containing the created move :param company: If set, the move is created in that company's context and warehouse defaults are resolved from that company's warehouse. :param **kwargs: stock.move fields that you can override ''location_id: origin location for the move ''location_dest_id: destination location for the move ''lot_ids: list of lot (split among the quantity) ''picking_type_id: picking type ''uom_id: Unit of measure ''owner_id: Consignment owner """ env = self.env['stock.move'].with_company(company).env if company else self.env if company: warehouse = env['stock.warehouse'].search([('company_id', '=', company.id)], limit=1) default_src = warehouse.lot_stock_id.id default_picking_type = warehouse.out_type_id.id else: default_src = self.stock_location.id default_picking_type = self.picking_type_out.id out_move = env['stock.move'].create({ 'product_id': product.id, 'location_id': kwargs.get('location_id', default_src), 'location_dest_id': kwargs.get('location_dest_id', self.customer_location.id), 'product_uom': kwargs.get('uom_id', self.uom.id), 'product_uom_qty': quantity, 'picking_type_id': kwargs.get('picking_type_id', default_picking_type), }) if create_picking: picking = env['stock.picking'].create({ 'picking_type_id': out_move.picking_type_id.id, 'location_id': out_move.location_id.id, 'location_dest_id': out_move.location_dest_id.id, }) out_move.picking_id = picking.id out_move._action_confirm() out_move._action_assign() lot_ids = kwargs.get('lot_ids') if lot_ids: out_move.move_line_ids.unlink() out_move.move_line_ids = [Command.create({ 'location_id': out_move.location_id.id, 'location_dest_id': self.customer_location.id, 'quantity': quantity / len(lot_ids), 'product_id': product.id, 'lot_id': lot.id, }) for lot in lot_ids] elif force_assign: out_move.quantity = quantity out_move.picked = True out_move._action_done() return out_move def _make_dropship_move(self, product, quantity, unit_cost=None, create_picking=False, company=None, **kwargs, ): kwargs.setdefault('location_dest_id', self.customer_location.id) return self._make_in_move(product, quantity, unit_cost, create_picking, company, **kwargs) def _make_return(self, move, quantity_to_return): stock_return_picking = Form(self.env['stock.return.picking'] .with_context(active_ids=[move.picking_id.id], active_id=move.picking_id.id, active_model='stock.picking')) stock_return_picking = stock_return_picking.save() stock_return_picking.product_return_moves.quantity = quantity_to_return stock_return_picking_action = stock_return_picking.action_create_returns() return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id']) return_pick.move_ids[0].quantity = quantity_to_return return_pick.move_ids[0].picked = True return_pick._action_done() return return_pick.move_ids # Post move processing def _add_move_line(self, move, **kwargs): old_price_unit = move._get_price_unit() self.env['stock.move.line'].create({ 'move_id': move.id, 'product_id': move.product_id.id, 'product_uom_id': move.product_uom.id, 'location_id': move.location_id.id, 'location_dest_id': move.location_dest_id.id, } | kwargs) move.value_manual = old_price_unit * move.quantity def _set_quantity(self, move, quantity): """Helper function to retroactively change the quantity of a move. The total value of the product will be recomputed as a result, regardless of the valuation method.""" price_unit = move._get_price_unit() move.quantity = quantity move.value_manual = price_unit * quantity # GETTER def _get_stock_valuation_move_lines(self): return self.env['account.move.line'].search([ ('account_id', '=', self.account_stock_valuation.id), ], order='date, id') def _get_stock_variation_move_lines(self): return self.env['account.move.line'].search([ ('account_id', '=', self.account_stock_variation.id), ], order='date, id') def _get_expense_move_lines(self): return self.env['account.move.line'].search([ ('account_id', '=', self.account_expense.id), ], order='date, id') def _url_extract_rec_id_and_model(self, url): # Extract model and record ID action_match = re.findall(r'action-([^/]+)', url) model_name = self.env.ref(action_match[0]).res_model rec_id = re.findall(r'/(\d+)$', url)[0] return rec_id, model_name @classmethod def setUpClass(cls): super().setUpClass() # To move to stock common later cls.route_mto = cls.env.ref('stock.route_warehouse0_mto') cls.company = cls.env['res.company'].create({'name': 'Inventory Test Company'}) cls.env["account.chart.template"]._load( "generic_coa", cls.company, install_demo=False ) cls.company = cls.company.with_company(cls.company.id) cls.env = cls.company.env cls.env.invalidate_all() # We use the admin on tour. cls.user_admin = cls.env.ref('base.user_admin') cls.user_admin.write({ 'company_id': cls.company.id, 'company_ids': cls.company.ids, }) cls.inventory_user = cls._create_new_internal_user(name='Inventory User', login='inventory_user', groups='stock.group_stock_user') cls.owner = cls._create_partner(name='Consignment Owner') cls.warehouse = cls.env['stock.warehouse'].search([('company_id', '=', cls.company.id)], limit=1) cls.stock_location = cls.warehouse.lot_stock_id cls.customer_location = cls.env.ref('stock.stock_location_customers') cls.supplier_location = cls.env.ref('stock.stock_location_suppliers') cls.inventory_location = cls.env['stock.location'].search([ ('usage', '=', 'inventory'), ('company_id', '=', cls.company.id) ], limit=1) cls.picking_type_in = cls.warehouse.in_type_id cls.picking_type_out = cls.warehouse.out_type_id cls.uom = cls.env.ref('uom.product_uom_unit') cls.uom_pack_of_6 = cls.env['uom.uom'].create({ 'name': 'Pack of 6', 'relative_uom_id': cls.uom.id, 'relative_factor': 6.0, }) cls.vendor = cls.env['res.partner'].create({ 'name': 'Test Vendor', 'company_id': cls.company.id, }) cls.other_company = cls._create_company(name="Other Company") cls.branch = cls._create_company(name="Branch Company", parent_id=cls.company.id) # Stock account cls.account_expense = cls.company.expense_account_id cls.account_stock_valuation = cls.company.account_stock_valuation_id cls.account_stock_variation = cls.account_stock_valuation.account_stock_variation_id cls.account_payable = cls.company.partner_id.property_account_payable_id cls.account_receivable = cls.company.partner_id.property_account_receivable_id cls.account_income = cls.company.income_account_id cls.category_standard = cls.env['product.category'].create({ 'name': 'Standard', 'property_valuation': 'periodic', 'property_cost_method': 'standard', }) cls.category_standard_auto = cls.category_standard.copy({ 'name': 'Standard Auto', 'property_valuation': 'real_time', }) cls.category_fifo = cls.env['product.category'].create({ 'name': 'Fifo', 'property_valuation': 'periodic', 'property_cost_method': 'fifo', }) cls.category_fifo_auto = cls.category_fifo.copy({ 'name': 'Fifo Auto', 'property_valuation': 'real_time', }) cls.category_avco = cls.env['product.category'].create({ 'name': 'Avco', 'property_valuation': 'periodic', 'property_cost_method': 'average', }) cls.category_avco_auto = cls.category_avco.copy({ 'name': 'Avco Auto', 'property_valuation': 'real_time', }) cls.product_common_vals = { "standard_price": 10.0, "list_price": 20.0, "uom_id": cls.uom.id, "is_storable": True, } cls.product = cls.env['product.product'].create( {**cls.product_common_vals, 'name': 'Storable Product'}).with_context(clean_context(cls.env.context)) cls.product_standard = cls.env['product.product'].create({ **cls.product_common_vals, 'name': 'Standard Product', 'categ_id': cls.category_standard.id, }).with_context(clean_context(cls.env.context)) cls.product_standard_auto = cls.env['product.product'].create({ **cls.product_common_vals, 'name': 'Standard Product Auto', 'categ_id': cls.category_standard_auto.id, }).with_context(clean_context(cls.env.context)) cls.product_fifo = cls.env['product.product'].create({ **cls.product_common_vals, 'name': 'Fifo Product', 'categ_id': cls.category_fifo.id, }).with_context(clean_context(cls.env.context)) cls.product_fifo_auto = cls.env['product.product'].create({ **cls.product_common_vals, 'name': 'Fifo Product Auto', 'categ_id': cls.category_fifo_auto.id, }).with_context(clean_context(cls.env.context)) cls.product_avco = cls.env['product.product'].create({ **cls.product_common_vals, 'name': 'Avco Product', 'categ_id': cls.category_avco.id, }).with_context(clean_context(cls.env.context)) cls.product_avco_auto = cls.env['product.product'].create({ **cls.product_common_vals, 'name': 'Avco Product Auto', 'categ_id': cls.category_avco_auto.id, }).with_context(clean_context(cls.env.context))