# Copyright 2013 Julius Network Solutions # Copyright 2015 Clear Corp # Copyright 2016 OpenSynergy Indonesia # Copyright 2017 ForgeFlow S.L. # Copyright 2018 Hibou Corp. # Copyright 2023 Quartile Limited # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from datetime import datetime from odoo.exceptions import ValidationError from odoo.tests.common import TransactionCase class TestStockPicking(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() # Get the MTO route and activate it if necessary cls.mto_route = cls.env.ref("stock.route_warehouse0_mto") cls.mto_route.write({"active": True}) cls.product = cls.env["product.product"].create( { "name": "Test Product", "type": "product", "standard_price": 1.0, } ) cls.product_2 = cls.env.ref("product.product_product_5") cls.product_categ = cls.env.ref("product.product_category_5") cls.valuation_account = cls.env["account.account"].create( { "name": "Test stock valuation", "code": "tv", "account_type": "liability_current", "reconcile": True, "company_id": cls.env.ref("base.main_company").id, } ) cls.stock_input_account = cls.env["account.account"].create( { "name": "Test stock input", "code": "tsti", "account_type": "expense", "reconcile": True, "company_id": cls.env.ref("base.main_company").id, } ) cls.stock_output_account = cls.env["account.account"].create( { "name": "Test stock output", "code": "tout", "account_type": "income", "reconcile": True, "company_id": cls.env.ref("base.main_company").id, } ) cls.stock_journal = cls.env["account.journal"].create( {"name": "Stock Journal", "code": "STJTEST", "type": "general"} ) cls.analytic_distribution = dict( {str(cls.env.ref("analytic.analytic_agrolait").id): 100.0} ) cls.warehouse = cls.env.ref("stock.warehouse0") cls.location = cls.warehouse.lot_stock_id cls.dest_location = cls.env.ref("stock.stock_location_customers") cls.outgoing_picking_type = cls.env.ref("stock.picking_type_out") cls.incoming_picking_type = cls.env.ref("stock.picking_type_in") cls.product_categ.update( { "property_valuation": "real_time", "property_stock_valuation_account_id": cls.valuation_account.id, "property_stock_account_input_categ_id": cls.stock_input_account.id, "property_stock_account_output_categ_id": cls.stock_output_account.id, "property_stock_journal": cls.stock_journal.id, } ) cls.product.update({"categ_id": cls.product_categ.id}) def _create_analytic_applicability(self): # analytic.analytic_agrolait belongs to analytic.analytic_plan_projects return self.env["account.analytic.applicability"].create( { "business_domain": "stock_move", "applicability": "optional", "analytic_plan_id": self.env.ref("analytic.analytic_plan_projects").id, } ) def _create_picking( self, location_id, location_dest_id, picking_type_id, analytic_distribution=False, ): picking_data = { "picking_type_id": picking_type_id.id, "move_type": "direct", "location_id": location_id.id, "location_dest_id": location_dest_id.id, } picking = self.env["stock.picking"].create(picking_data) move_data = { "picking_id": picking.id, "product_id": self.product.id, "location_id": location_id.id, "location_dest_id": location_dest_id.id, "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "date_deadline": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "name": self.product.name, "procure_method": "make_to_stock", "product_uom": self.product.uom_id.id, "product_uom_qty": 1.0, "analytic_distribution": analytic_distribution or False, } self.env["stock.move"].create(move_data) return picking def __update_qty_on_hand_product(self, product, new_qty): self.env["stock.quant"]._update_available_quantity( product, self.location, new_qty ) def _confirm_picking_no_error(self, picking): picking.action_confirm() self.assertEqual(picking.state, "assigned") def _picking_done_no_error(self, picking): picking.move_ids.quantity_done = 1.0 picking.button_validate() self.assertEqual(picking.state, "done") def _check_account_move_no_error(self, picking): criteria1 = [ ["ref", "=", "{} - {}".format(picking.name, picking.product_id.name)] ] acc_moves = self.env["account.move"].search(criteria1) self.assertTrue(len(acc_moves) > 0) def _check_analytic_account_no_error(self, picking): move = picking.move_ids[0] criteria2 = [["move_id.ref", "=", picking.name]] acc_lines = self.env["account.move.line"].search(criteria2) for acc_line in acc_lines: if acc_line.account_id == self.valuation_account: self.assertEqual(acc_line.analytic_distribution, False) else: self.assertEqual( acc_line.analytic_distribution, move.analytic_distribution ) def _check_no_analytic_account(self, picking): criteria2 = [ ("move_id.ref", "=", picking.name), ("analytic_distribution", "!=", False), ] line_count = self.env["account.move.line"].search_count(criteria2) self.assertEqual(line_count, 0) def _check_analytic_consistency(self, picking): for move_line in picking.move_line_ids: self.assertEqual( move_line.analytic_distribution, move_line.move_id.analytic_distribution ) def test_outgoing_picking_with_analytic(self): picking = self._create_picking( self.location, self.dest_location, self.outgoing_picking_type, self.analytic_distribution, ) self.__update_qty_on_hand_product(self.product, 1) self._confirm_picking_no_error(picking) self._picking_done_no_error(picking) self._check_account_move_no_error(picking) self._check_analytic_account_no_error(picking) self._check_analytic_consistency(picking) def test_outgoing_picking_without_analytic_optional(self): # Create a general optional applicability for stock moves. self._create_analytic_applicability() # Create a another applicability which makes the analytic mandatory only for # incoming stock moves. i.e. applicability should be optional for the outgoing applicability_specific = self._create_analytic_applicability() applicability_specific.write( { "stock_picking_type_id": self.incoming_picking_type.id, "applicability": "mandatory", } ) picking = self._create_picking( self.location, self.dest_location, self.outgoing_picking_type, ) self.__update_qty_on_hand_product(self.product, 1) self._confirm_picking_no_error(picking) self._picking_done_no_error(picking) self._check_account_move_no_error(picking) self._check_no_analytic_account(picking) self._check_analytic_consistency(picking) def test_outgoing_picking_without_analytic_mandatory(self): # Create a general mandatory applicability for stock moves. applicability_general = self._create_analytic_applicability() applicability_general.write({"applicability": "mandatory"}) # Create a another applicability which makes the analytic optional only for # incoming stock moves. applicability_specific = self._create_analytic_applicability() applicability_specific.write( {"stock_picking_type_id": self.incoming_picking_type.id} ) picking = self._create_picking( self.location, self.dest_location, self.outgoing_picking_type, ) self.__update_qty_on_hand_product(self.product, 1) self._confirm_picking_no_error(picking) with self.assertRaises(ValidationError): self._picking_done_no_error(picking) def test_incoming_picking_with_analytic(self): picking = self._create_picking( self.location, self.dest_location, self.incoming_picking_type, self.analytic_distribution, ) self.__update_qty_on_hand_product(self.product, 1) self._confirm_picking_no_error(picking) self._picking_done_no_error(picking) self._check_account_move_no_error(picking) self._check_analytic_account_no_error(picking) self._check_analytic_consistency(picking) def test_picking_add_extra_move_line(self): picking = self._create_picking( self.location, self.dest_location, self.outgoing_picking_type, self.analytic_distribution, ) move_before = picking.move_ids self.env["stock.move.line"].create( { "product_id": self.product_2.id, "location_id": self.location.id, "location_dest_id": self.dest_location.id, "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "product_uom_id": self.product_2.uom_id.id, "reserved_uom_qty": 1.0, "analytic_distribution": self.analytic_distribution, "company_id": self.env.company.id, "picking_id": picking.id, } ) move_after = picking.move_ids - move_before self.assertEqual(self.analytic_distribution, move_after.analytic_distribution) def test__prepare_procurement_values(self): picking = self._create_picking( self.location, self.dest_location, self.outgoing_picking_type, self.analytic_distribution, ) values = picking.move_ids._prepare_procurement_values() self.assertEqual(self.analytic_distribution, values["analytic_distribution"]) picking = self._create_picking( self.location, self.dest_location, self.outgoing_picking_type, ) values = picking.move_ids._prepare_procurement_values() self.assertEqual(values.get("analytic_distribution"), None) def test_procurement_analytic(self): rule = self.env["stock.rule"].create( { "name": "Test MTO Rule", "action": "pull", "location_src_id": self.location.id, "location_dest_id": self.dest_location.id, "procure_method": "make_to_order", "route_id": self.mto_route.id, "picking_type_id": self.outgoing_picking_type.id, } ) # Manually creating a stock move as would result from the rule being triggered move_values = rule._get_stock_move_values( self.product, 10, self.product.uom_id, self.dest_location, "Test Move", False, self.env.company, { "analytic_distribution": self.analytic_distribution, "date_planned": datetime.now(), }, ) move = self.env["stock.move"].create(move_values) # Check that the analytic_distribution data was passed correctly self.assertEqual( move.analytic_distribution, self.analytic_distribution, "Analytic distribution not correctly propagated.", )