From 99c65d9b2e4d6611dd64f4566a10b641459cf87a Mon Sep 17 00:00:00 2001 From: AaronHForgeFlow Date: Fri, 4 Mar 2022 10:56:07 +0100 Subject: [PATCH] [15.0][IMP] Tests for stock valuation [FIX] rma: rma_custmer_user has no write permissions in partner, so compute method fails. [IMP] rma: use rma user in tests [FIX] rma_account: move_line_id field string [IMP] rma, rma_account, rma_sale, rma_purchase: tests for stock valuation [FIX] account_move_line_rma_order_line: minor lint, make auto-install --- .../test_account_move_line_rma_order_line.py | 292 ------------------ rma/models/res_partner.py | 6 +- rma/models/stock_rule.py | 3 + rma/tests/test_rma.py | 225 +++++++++++--- rma_account/models/rma_order_line.py | 2 +- rma_account/tests/__init__.py | 1 + rma_account/tests/test_rma_account.py | 2 +- rma_account/tests/test_rma_stock_account.py | 143 +++++++++ rma_purchase/models/procurement.py | 12 +- rma_purchase/tests/__init__.py | 1 + .../tests/test_rma_stock_account_purchase.py | 81 +++++ rma_sale/models/procurement.py | 12 +- rma_sale/tests/__init__.py | 4 +- rma_sale/tests/test_rma_stock_account_sale.py | 74 +++++ 14 files changed, 502 insertions(+), 356 deletions(-) delete mode 100644 account_move_line_rma_order_line/tests/test_account_move_line_rma_order_line.py create mode 100644 rma_account/tests/test_rma_stock_account.py create mode 100644 rma_purchase/tests/test_rma_stock_account_purchase.py create mode 100644 rma_sale/tests/test_rma_stock_account_sale.py diff --git a/account_move_line_rma_order_line/tests/test_account_move_line_rma_order_line.py b/account_move_line_rma_order_line/tests/test_account_move_line_rma_order_line.py deleted file mode 100644 index 766c1ba4..00000000 --- a/account_move_line_rma_order_line/tests/test_account_move_line_rma_order_line.py +++ /dev/null @@ -1,292 +0,0 @@ -# © 2017-2022 ForgeFlow S.L. (www.forgeflow.com) -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). - -from odoo.tests import common - - -class TestAccountMoveLineRmaOrderLine(common.SavepointCase): - @classmethod - def setUpClass(cls): - super(TestAccountMoveLineRmaOrderLine, cls).setUpClass() - cls.rma_model = cls.env["rma.order"] - cls.rma_line_model = cls.env["rma.order.line"] - cls.rma_refund_wiz = cls.env["rma.refund"] - cls.rma_add_stock_move = cls.env["rma_add_stock_move"] - cls.rma_make_picking = cls.env["rma_make_picking.wizard"] - cls.invoice_model = cls.env["account.move"] - cls.stock_picking_model = cls.env["stock.picking"] - cls.invoice_line_model = cls.env["account.move.line"] - cls.product_model = cls.env["product.product"] - cls.product_ctg_model = cls.env["product.category"] - cls.acc_type_model = cls.env["account.account.type"] - cls.account_model = cls.env["account.account"] - cls.aml_model = cls.env["account.move.line"] - cls.res_users_model = cls.env["res.users"] - - cls.partner1 = cls.env.ref("base.res_partner_1") - cls.location_stock = cls.env.ref("stock.stock_location_stock") - cls.company = cls.env.ref("base.main_company") - cls.group_rma_user = cls.env.ref("rma.group_rma_customer_user") - cls.group_account_invoice = cls.env.ref("account.group_account_invoice") - cls.group_account_manager = cls.env.ref("account.group_account_manager") - cls.stock_location = cls.env.ref("stock.stock_location_stock") - wh = cls.env.ref("stock.warehouse0") - cls.stock_rma_location = wh.lot_rma_id - cls.customer_location = cls.env.ref("stock.stock_location_customers") - cls.supplier_location = cls.env.ref("stock.stock_location_suppliers") - # Create account for Goods Received Not Invoiced - acc_type = cls._create_account_type("equity", "other", "equity") - name = "Goods Received Not Invoiced" - code = "grni" - cls.account_grni = cls._create_account(acc_type, name, code, cls.company) - - # Create account for Cost of Goods Sold - acc_type = cls._create_account_type("expense", "other", "expense") - name = "Cost of Goods Sold" - code = "cogs" - cls.account_cogs = cls._create_account(acc_type, name, code, cls.company) - # Create account for Inventory - acc_type = cls._create_account_type("asset", "other", "asset") - name = "Inventory" - code = "inventory" - cls.account_inventory = cls._create_account(acc_type, name, code, cls.company) - # Create Product - cls.product = cls._create_product() - cls.product_uom_id = cls.env.ref("uom.product_uom_unit") - # Create users - cls.rma_user = cls._create_user( - "rma_user", [cls.group_rma_user, cls.group_account_invoice], cls.company - ) - cls.account_invoice = cls._create_user( - "account_invoice", [cls.group_account_invoice], cls.company - ) - cls.account_manager = cls._create_user( - "account_manager", [cls.group_account_manager], cls.company - ) - - @classmethod - def _create_user(cls, login, groups, company): - """ Create a user.""" - group_ids = [group.id for group in groups] - user = cls.res_users_model.with_context({"no_reset_password": True}).create( - { - "name": "Test User", - "login": login, - "password": "demo", - "email": "test@yourcompany.com", - "company_id": company.id, - "company_ids": [(4, company.id)], - "groups_id": [(6, 0, group_ids)], - } - ) - return user.id - - @classmethod - def _create_account_type(cls, name, type, internal_group): - acc_type = cls.acc_type_model.create( - {"name": name, "type": type, "internal_group": internal_group} - ) - return acc_type - - @classmethod - def _create_account(cls, acc_type, name, code, company): - """Create an account.""" - account = cls.account_model.create( - { - "name": name, - "code": code, - "user_type_id": acc_type.id, - "company_id": company.id, - } - ) - return account - - @classmethod - def _create_product(cls): - """Create a Product.""" - # group_ids = [group.id for group in groups] - product_ctg = cls.product_ctg_model.create( - { - "name": "test_product_ctg", - "property_stock_valuation_account_id": cls.account_inventory.id, - "property_valuation": "real_time", - "property_stock_account_input_categ_id": cls.account_grni.id, - "property_stock_account_output_categ_id": cls.account_cogs.id, - } - ) - product = cls.product_model.create( - { - "name": "test_product", - "categ_id": product_ctg.id, - "type": "product", - "standard_price": 1.0, - "list_price": 1.0, - } - ) - return product - - @classmethod - def _create_picking(cls, partner): - return cls.stock_picking_model.create( - { - "partner_id": partner.id, - "picking_type_id": cls.env.ref("stock.picking_type_in").id, - "location_id": cls.stock_location.id, - "location_dest_id": cls.supplier_location.id, - } - ) - - @classmethod - def _prepare_move(cls, product, qty, src, dest, picking_in): - res = { - "partner_id": cls.partner1.id, - "product_id": product.id, - "name": product.partner_ref, - "state": "confirmed", - "product_uom": cls.product_uom_id.id or product.uom_id.id, - "product_uom_qty": qty, - "origin": "Test RMA", - "location_id": src.id, - "location_dest_id": dest.id, - "picking_id": picking_in.id, - } - return res - - @classmethod - def _create_rma(cls, products2move, partner): - picking_in = cls._create_picking(partner) - moves = [] - for item in products2move: - move_values = cls._prepare_move( - item[0], item[1], cls.stock_location, cls.customer_location, picking_in - ) - moves.append(cls.env["stock.move"].create(move_values)) - - rma_id = cls.rma_model.create( - { - "reference": "0001", - "type": "customer", - "partner_id": partner.id, - "company_id": cls.env.ref("base.main_company").id, - } - ) - for move in moves: - wizard = cls.rma_add_stock_move.new( - { - "move_ids": [(6, 0, move.ids)], - "rma_id": rma_id.id, - "partner_id": move.partner_id.id, - } - ) - # data = wizard._prepare_rma_line_from_stock_move(move) - wizard.add_lines() - - # CHECK ME: this code duplicate rma lines, what is the porpourse? - # if move.product_id.rma_customer_operation_id: - # move.product_id.rma_customer_operation_id.in_route_id = False - # move.product_id.categ_id.rma_customer_operation_id = False - # move.product_id.rma_customer_operation_id = False - # wizard._prepare_rma_line_from_stock_move(move) - # cls.line = cls.rma_line_model.create(data) - return rma_id - - def _get_balance(self, domain): - """ - Call read_group method and return the balance of particular account. - """ - aml_rec = self.aml_model.read_group( - domain, ["debit", "credit", "account_id"], ["account_id"] - ) - if aml_rec: - return aml_rec[0].get("debit", 0) - aml_rec[0].get("credit", 0) - else: - return 0.0 - - def _check_account_balance(self, account_id, rma_line=None, expected_balance=0.0): - """ - Check the balance of the account - """ - domain = [("account_id", "=", account_id)] - if rma_line: - domain.extend([("rma_line_id", "=", rma_line.id)]) - - balance = self._get_balance(domain) - if rma_line: - self.assertEqual( - balance, - expected_balance, - "Balance is not %s for rma Line %s." - % (str(expected_balance), rma_line.name), - ) - - def test_rma_invoice(self): - """Test that the rma line moves from the rma order to the - account move line and to the invoice line. - """ - products2move = [ - (self.product, 1), - ] - rma = self._create_rma(products2move, self.partner1) - rma_line = rma.rma_line_ids - for rma in rma_line: - if rma.price_unit == 0: - rma.price_unit = 1.0 - rma_line.action_rma_approve() - wizard = self.rma_make_picking.with_context( - { - "active_id": 1, - "active_ids": rma_line.ids, - "active_model": "rma.order.line", - "picking_type": "incoming", - } - ).create({}) - operation = self.env["rma.operation"].search( - [("type", "=", "customer"), ("refund_policy", "=", "received")], limit=1 - ) - rma_line.write({"operation_id": operation.id}) - rma_line.write({"refund_policy": "received"}) - - wizard._create_picking() - res = rma_line.action_view_in_shipments() - if "res_id" in res: - picking = self.env["stock.picking"].browse(res["res_id"]) - else: - picking_ids = self.env["stock.picking"].search(res["domain"]) - picking = self.env["stock.picking"].browse(picking_ids) - picking.move_lines.write({"quantity_done": 1.0}) - picking.button_validate() - # decreasing cogs - expected_balance = -1.0 - for record in rma_line: - self._check_account_balance( - self.account_cogs.id, rma_line=record, expected_balance=expected_balance - ) - make_refund = self.rma_refund_wiz.with_context( - { - "customer": True, - "active_ids": rma_line.ids, - "active_model": "rma.order.line", - } - ).create( - { - "description": "Test refund", - } - ) - for item in make_refund.item_ids: - item.write( - { - "qty_to_refund": 1.0, - } - ) - make_refund.invoice_refund() - rma_line.refund_line_ids.move_id.filtered( - lambda x: x.state != "posted" - ).action_post() - for aml in rma_line.refund_line_ids.move_id.invoice_line_ids: - if aml.product_id == rma_line.product_id and aml.move_id: - self.assertEqual( - aml.rma_line_id, - rma_line, - "Rma Order line has not been copied from the invoice to " - "the account move line.", - ) diff --git a/rma/models/res_partner.py b/rma/models/res_partner.py index 0a263d4f..f2acd754 100644 --- a/rma/models/res_partner.py +++ b/rma/models/res_partner.py @@ -1,4 +1,4 @@ -# Copyright 2017 ForgeFlow +# Copyright 2017-22 ForgeFlow # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import fields, models @@ -14,7 +14,9 @@ class ResPartner(models.Model): rma_line_ids = fields.One2many( comodel_name="rma.order.line", string="RMAs", inverse_name="partner_id" ) - rma_line_count = fields.Integer(compute="_compute_rma_line_count") + rma_line_count = fields.Integer( + compute="_compute_rma_line_count", compute_sudo=True + ) def action_open_partner_rma(self): action = self.env.ref("rma.action_rma_customer_lines") diff --git a/rma/models/stock_rule.py b/rma/models/stock_rule.py index a3da529f..6a042544 100644 --- a/rma/models/stock_rule.py +++ b/rma/models/stock_rule.py @@ -35,6 +35,9 @@ class StockRule(models.Model): res["partner_id"] = line.delivery_address_id.id else: res["partner_id"] = line.rma_id.partner_id.id + # We are not checking the reference move here because if stock account + # is not installed, there is no way to know the cost of the stock move + # so better use the standard cost in this case. company_id = res["company_id"] company = self.env["res.company"].browse(company_id) cost = product_id.with_company(company).standard_price diff --git a/rma/tests/test_rma.py b/rma/tests/test_rma.py index aa33ce31..4b561f64 100644 --- a/rma/tests/test_rma.py +++ b/rma/tests/test_rma.py @@ -11,15 +11,18 @@ class TestRma(common.TransactionCase): @classmethod def setUpClass(cls): super(TestRma, cls).setUpClass() - + # models cls.rma_make_picking = cls.env["rma_make_picking.wizard"] cls.make_supplier_rma = cls.env["rma.order.line.make.supplier.rma"] cls.rma_add_stock_move = cls.env["rma_add_stock_move"] + cls.product_ctg_model = cls.env["product.category"] cls.stockpicking = cls.env["stock.picking"] cls.rma = cls.env["rma.order"] cls.rma_line = cls.env["rma.order.line"] cls.rma_op = cls.env["rma.operation"] cls.product_product_model = cls.env["product.product"] + cls.res_users_model = cls.env["res.users"] + # References and records cls.rma_cust_replace_op_id = cls.env.ref("rma.rma_operation_customer_replace") cls.rma_sup_replace_op_id = cls.env.ref("rma.rma_operation_supplier_replace") cls.rma_ds_replace_op_id = cls.env.ref("rma.rma_operation_ds_replace") @@ -31,16 +34,34 @@ class TestRma(common.TransactionCase): cls.product_2 = cls._create_product("PT2") cls.product_3 = cls._create_product("PT3") cls.uom_unit = cls.env.ref("uom.product_uom_unit") - cls.env.user.company_id.group_rma_delivery_address = True - cls.env.user.company_id.group_rma_lines = True + cls.company = cls.env.company + cls.company.group_rma_delivery_address = True + cls.company.group_rma_lines = True cls.partner_id = cls.env.ref("base.res_partner_2") cls.stock_location = cls.env.ref("stock.stock_location_stock") - wh = cls.env.ref("stock.warehouse0") - cls.stock_rma_location = wh.lot_rma_id + cls.wh = cls.env.ref("stock.warehouse0") + cls.stock_rma_location = cls.wh.lot_rma_id cls.customer_location = cls.env.ref("stock.stock_location_customers") cls.supplier_location = cls.env.ref("stock.stock_location_suppliers") cls.product_uom_id = cls.env.ref("uom.product_uom_unit") + cls.g_rma_customer_user = cls.env.ref("rma.group_rma_customer_user") + cls.g_rma_supplier_user = cls.env.ref("rma.group_rma_supplier_user") + cls.g_account_manager = cls.env.ref("account.group_account_manager") + cls.g_rma_manager = cls.env.ref("rma.group_rma_manager") + cls.g_stock_user = cls.env.ref("stock.group_stock_user") + cls.g_stock_manager = cls.env.ref("stock.group_stock_manager") + + cls.rma_basic_user = cls._create_user( + "rma worker", + [cls.g_stock_user, cls.g_rma_customer_user, cls.g_rma_supplier_user], + cls.company, + ) + cls.rma_manager_user = cls._create_user( + "rma manager", + [cls.g_stock_manager, cls.g_rma_manager, cls.g_account_manager], + cls.company, + ) # Customer RMA: products2move = [(cls.product_1, 3), (cls.product_2, 5), (cls.product_3, 2)] cls.rma_customer_id = cls._create_rma_from_move( @@ -59,11 +80,64 @@ class TestRma(common.TransactionCase): products2move, "supplier", cls.env.ref("base.res_partner_2"), dropship=False ) + @classmethod + def _create_user(cls, login, groups, company): + group_ids = [group.id for group in groups] + user = cls.res_users_model.create( + { + "name": login, + "login": login, + "email": "example@yourcompany.com", + "company_id": company.id, + "company_ids": [(4, company.id)], + "groups_id": [(6, 0, group_ids)], + } + ) + return user + + @classmethod + def _receive_rma(cls, rma_line_ids): + wizard = cls.rma_make_picking.with_context( + **{ + "active_ids": rma_line_ids.ids, + "active_model": "rma.order.line", + "picking_type": "incoming", + "active_id": 1, + } + ).create({}) + wizard._create_picking() + res = rma_line_ids.action_view_in_shipments() + picking = cls.env["stock.picking"].browse(res["res_id"]) + picking.action_assign() + for mv in picking.move_lines: + mv.quantity_done = mv.product_uom_qty + picking._action_done() + return picking + + @classmethod + def _deliver_rma(cls, rma_line_ids): + wizard = cls.rma_make_picking.with_context( + **{ + "active_ids": rma_line_ids.ids, + "active_model": "rma.order.line", + "picking_type": "outgoing", + "active_id": 1, + } + ).create({}) + wizard._create_picking() + res = rma_line_ids.action_view_out_shipments() + picking = cls.env["stock.picking"].browse(res["res_id"]) + picking.action_assign() + for mv in picking.move_lines: + mv.quantity_done = mv.product_uom_qty + picking._action_done() + return picking + @classmethod def _create_product_category( cls, rma_approval_policy, rma_customer_operation_id, rma_supplier_operation_id ): - return cls.env["product.category"].create( + return cls.product_ctg_model.create( { "name": "Test category", "rma_approval_policy": rma_approval_policy, @@ -79,45 +153,115 @@ class TestRma(common.TransactionCase): ) @classmethod - def _create_picking(cls, partner): + def _create_picking(cls, partner, picking_type): return cls.stockpicking.create( { "partner_id": partner.id, - "picking_type_id": cls.env.ref("stock.picking_type_in").id, + "picking_type_id": picking_type.id, "location_id": cls.stock_location.id, "location_dest_id": cls.supplier_location.id, } ) + @classmethod + def _do_picking(cls, picking): + """Do picking with only one move on the given date.""" + picking.action_confirm() + picking.action_assign() + for ml in picking.move_lines: + ml.filtered( + lambda m: m.state != "waiting" + ).quantity_done = ml.product_uom_qty + picking.button_validate() + + @classmethod + def _create_inventory(cls, product, qty, location): + """ + Creates inventory of a product on a specific location, this will be used + eventually to create a inventory at specific cost, that will be received in + a customer RMA or delivered in a supplier RMA + """ + inventory = ( + cls.env["stock.quant"] + .create( + { + "location_id": location.id, + "product_id": product.id, + "inventory_quantity": qty, + } + ) + .action_apply_inventory() + ) + return inventory + + @classmethod + def _get_picking_type(cls, wh, loc1, loc2): + picking_type = cls.env["stock.picking.type"].search( + [ + ("warehouse_id", "=", wh.id), + ("default_location_src_id", "=", loc1.id), + ("default_location_dest_id", "=", loc2.id), + ], + limit=1, + ) + if picking_type: + return picking_type + picking_type = cls.env["stock.picking.type"].create( + { + "name": loc1.name + " to " + loc2.name, + "sequence_code": loc1.name + " to " + loc2.name, + "code": "incoming", + "warehouse_id": wh.id, + "default_location_src_id": loc1.id, + "default_location_dest_id": loc2.id, + } + ) + return picking_type + @classmethod def _create_rma_from_move( cls, products2move, r_type, partner, dropship, supplier_address_id=None ): - picking_in = cls._create_picking(partner) moves = [] if r_type == "customer": + picking_type = cls._get_picking_type( + cls.wh, cls.stock_location, cls.customer_location + ) + picking = cls._create_picking(partner, picking_type) for item in products2move: + product = item[0] + product_qty = item[1] + cls._create_inventory(product, product_qty, cls.stock_location) move_values = cls._prepare_move( - item[0], - item[1], + product, + product_qty, cls.stock_location, cls.customer_location, - picking_in, + picking, ) moves.append(cls.env["stock.move"].create(move_values)) else: + picking_type = cls._get_picking_type( + cls.wh, cls.supplier_location, cls.stock_rma_location + ) + picking = cls._create_picking(partner, picking_type) for item in products2move: + product = item[0] + product_qty = item[1] + cls._create_inventory(product, product_qty, cls.stock_location) move_values = cls._prepare_move( - item[0], - item[1], + product, + product_qty, cls.supplier_location, cls.stock_rma_location, - picking_in, + picking, ) moves.append(cls.env["stock.move"].create(move_values)) + # Process the picking + cls._do_picking(picking) # Create the RMA from the stock_move - rma_id = cls.rma.create( + rma_id = cls.rma.with_user(cls.rma_basic_user).create( { "reference": "0001", "type": r_type, @@ -127,7 +271,7 @@ class TestRma(common.TransactionCase): ) for move in moves: if r_type == "customer": - wizard = cls.rma_add_stock_move.new( + wizard = cls.rma_add_stock_move.with_user(cls.rma_basic_user).new( { "move_ids": [(4, move.id)], "rma_id": rma_id.id, @@ -144,12 +288,14 @@ class TestRma(common.TransactionCase): "active_model": "rma.order", } ).default_get([str(move.id), str(cls.partner_id.id)]) - data = wizard.with_context( - customer=1 - )._prepare_rma_line_from_stock_move(move) + data = ( + wizard.with_user(cls.rma_basic_user) + .with_context(customer=1) + ._prepare_rma_line_from_stock_move(move) + ) else: - wizard = cls.rma_add_stock_move.new( + wizard = cls.rma_add_stock_move.with_user(cls.rma_basic_user).new( { "move_ids": [(4, move.id)], "rma_id": rma_id.id, @@ -165,7 +311,9 @@ class TestRma(common.TransactionCase): "active_model": "rma.order", } ).default_get([str(move.id), str(cls.partner_id.id)]) - data = wizard._prepare_rma_line_from_stock_move(move) + data = wizard.with_user( + cls.rma_basic_user + )._prepare_rma_line_from_stock_move(move) data["type"] = "supplier" if dropship: data.update( @@ -173,21 +321,15 @@ class TestRma(common.TransactionCase): operation_id=cls.rma_ds_replace_op_id.id, supplier_address_id=supplier_address_id.id, ) - cls.line = cls.rma_line.create(data) + cls.line = cls.rma_line.with_user(cls.rma_basic_user).create(data) cls.line._onchange_product_id() cls.line._onchange_operation_id() cls.line.action_rma_to_approve() rma_id._get_default_type() - rma_id._compute_in_shipment_count() - rma_id._compute_out_shipment_count() - rma_id._compute_supplier_line_count() - rma_id._compute_line_count() rma_id.action_view_in_shipments() rma_id.action_view_out_shipments() rma_id.action_view_lines() - rma_id.partner_id.action_open_partner_rma() - rma_id.partner_id._compute_rma_line_count() return rma_id @classmethod @@ -205,23 +347,8 @@ class TestRma(common.TransactionCase): "product_uom_qty": qty, "location_id": location_id, "location_dest_id": dest.id, - "move_line_ids": [ - ( - 0, - 0, - { - "product_id": product.id, - "product_uom_id": product.uom_id.id, - "qty_done": qty, - "location_id": location_id, - "location_dest_id": dest.id, - "package_id": False, - "owner_id": False, - "lot_id": False, - }, - ) - ], "picking_id": picking_in.id, + "price_unit": product.standard_price, } def _check_equal_quantity(self, qty1, qty2, msg): @@ -511,8 +638,12 @@ class TestRma(common.TransactionCase): 2, "Wrong qty_delivered", ) - self.line.action_rma_done() - self.assertEqual(self.line.state, "done", "Wrong State") + self.rma_customer_id.rma_line_ids.action_rma_done() + self.assertEqual( + self.rma_customer_id.rma_line_ids.mapped("state"), + ["done", "done", "done"], + "Wrong State", + ) self.rma_customer_id.action_view_in_shipments() self.rma_customer_id.action_view_out_shipments() self.rma_customer_id.action_view_lines() diff --git a/rma_account/models/rma_order_line.py b/rma_account/models/rma_order_line.py index 7fd95800..47172871 100644 --- a/rma_account/models/rma_order_line.py +++ b/rma_account/models/rma_order_line.py @@ -71,7 +71,7 @@ class RmaOrderLine(models.Model): move_line_ids = fields.One2many( comodel_name="account.move.line", inverse_name="rma_line_id", - string="Refund Lines", + string="Journal Items", copy=False, index=True, readonly=True, diff --git a/rma_account/tests/__init__.py b/rma_account/tests/__init__.py index 4edd8b55..bdce7c96 100644 --- a/rma_account/tests/__init__.py +++ b/rma_account/tests/__init__.py @@ -2,3 +2,4 @@ # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) from . import test_rma_account +from . import test_rma_stock_account diff --git a/rma_account/tests/test_rma_account.py b/rma_account/tests/test_rma_account.py index 6d07bf3a..bbeed9af 100644 --- a/rma_account/tests/test_rma_account.py +++ b/rma_account/tests/test_rma_account.py @@ -1,4 +1,4 @@ -# Copyright 2017-18 ForgeFlow S.L. +# Copyright 2017-22 ForgeFlow S.L. # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) from odoo import fields diff --git a/rma_account/tests/test_rma_stock_account.py b/rma_account/tests/test_rma_stock_account.py new file mode 100644 index 00000000..ee663988 --- /dev/null +++ b/rma_account/tests/test_rma_stock_account.py @@ -0,0 +1,143 @@ +# Copyright 2017-22 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo.tests.common import Form + +# pylint: disable=odoo-addons-relative-import +from odoo.addons.rma.tests.test_rma import TestRma + + +class TestRmaStockAccount(TestRma): + @classmethod + def setUpClass(cls): + super(TestRmaStockAccount, cls).setUpClass() + cls.acc_type_model = cls.env["account.account.type"] + cls.account_model = cls.env["account.account"] + cls.g_account_user = cls.env.ref("account.group_account_user") + # we create new products to ensure previous layers do not affect when + # running FIFO + cls.product_fifo_1 = cls._create_product("product_fifo1") + cls.product_fifo_2 = cls._create_product("product_fifo2") + cls.product_fifo_3 = cls._create_product("product_fifo3") + cls.rma_basic_user.write({"groups_id": [(4, cls.g_account_user.id)]}) + # The product category created in the base module is not automated valuation + # we have to create a new category here + # Create account for Goods Received Not Invoiced + acc_type = cls._create_account_type("equity", "other") + name = "Goods Received Not Invoiced" + code = "grni" + cls.account_grni = cls._create_account(acc_type, name, code, cls.company, True) + # Create account for Cost of Goods Sold + acc_type = cls._create_account_type("expense", "other") + name = "Cost of Goods Sold" + code = "cogs" + cls.account_cogs = cls._create_account(acc_type, name, code, cls.company, False) + # Create account for Inventory + acc_type = cls._create_account_type("asset", "other") + name = "Inventory" + code = "inventory" + cls.account_inventory = cls._create_account( + acc_type, name, code, cls.company, False + ) + product_ctg = cls.product_ctg_model.create( + { + "name": "test_product_ctg", + "property_stock_valuation_account_id": cls.account_inventory.id, + "property_valuation": "real_time", + "property_stock_account_input_categ_id": cls.account_grni.id, + "property_stock_account_output_categ_id": cls.account_cogs.id, + "rma_approval_policy": "one_step", + "rma_customer_operation_id": cls.rma_cust_replace_op_id.id, + "rma_supplier_operation_id": cls.rma_sup_replace_op_id.id, + "property_cost_method": "fifo", + } + ) + # We use FIFO to test the cost is taken from the original layers + cls.product_fifo_1.categ_id = product_ctg + cls.product_fifo_2.categ_id = product_ctg + cls.product_fifo_3.categ_id = product_ctg + + @classmethod + def _create_account_type(cls, name, a_type): + acc_type = cls.acc_type_model.create( + {"name": name, "type": a_type, "internal_group": name} + ) + return acc_type + + @classmethod + def _create_account(cls, acc_type, name, code, company, reconcile): + """Create an account.""" + account = cls.account_model.create( + { + "name": name, + "code": code, + "user_type_id": acc_type.id, + "company_id": company.id, + "reconcile": reconcile, + } + ) + return account + + def check_accounts_used( + self, account_move, debit_account=False, credit_account=False + ): + debit_line = account_move.mapped("line_ids").filtered(lambda l: l.debit) + credit_line = account_move.mapped("line_ids").filtered(lambda l: l.credit) + if debit_account: + self.assertEqual(debit_line.account_id.code, debit_account) + if credit_account: + self.assertEqual(credit_line.account_id.code, credit_account) + + def test_01_cost_from_standard(self): + """ + Test the price unit is taken from the standard cost when there is no reference + """ + self.product_fifo_1.standard_price = 15 + rma_line = Form(self.rma_line.with_user(self.rma_basic_user)) + rma_line.partner_id = self.partner_id + rma_line.product_id = self.product_fifo_1 + rma_line.price_unit = 1234 + rma_line = rma_line.save() + rma_line.action_rma_to_approve() + picking = self._receive_rma(rma_line) + self.assertEqual(picking.move_lines.stock_valuation_layer_ids.value, 15.0) + account_move = picking.move_lines.stock_valuation_layer_ids.account_move_id + self.check_accounts_used( + account_move, debit_account="inventory", credit_account="cogs" + ) + + def test_02_cost_from_move(self): + """ + Test the price unit is taken from the cost of the stock move when the + reference is the stock move + """ + # Set a standard price on the products + self.product_fifo_1.standard_price = 10 + self.product_fifo_2.standard_price = 20 + self.product_fifo_3.standard_price = 30 + self._create_inventory( + self.product_fifo_1, 20.0, self.env.ref("stock.stock_location_customers") + ) + products2move = [ + (self.product_fifo_1, 3), + (self.product_fifo_2, 5), + (self.product_fifo_3, 2), + ] + rma_customer_id = self._create_rma_from_move( + products2move, + "customer", + self.env.ref("base.res_partner_2"), + dropship=False, + ) + # Set an incorrect price in the RMA (this should not affect cost) + rma_customer_id.rma_line_ids.price_unit = 999 + rma_customer_id.rma_line_ids.action_rma_to_approve() + picking = self._receive_rma(rma_customer_id.rma_line_ids) + # Test the value in the layers of the incoming stock move is used + for rma_line in rma_customer_id.rma_line_ids: + value_origin = rma_line.reference_move_id.stock_valuation_layer_ids.value + move_product = picking.move_lines.filtered( + lambda l: l.product_id == rma_line.product_id + ) + value_used = move_product.stock_valuation_layer_ids.value + self.assertEqual(value_used, -value_origin) diff --git a/rma_purchase/models/procurement.py b/rma_purchase/models/procurement.py index 854181f5..63993911 100644 --- a/rma_purchase/models/procurement.py +++ b/rma_purchase/models/procurement.py @@ -37,16 +37,18 @@ class StockRule(models.Model): if moves: # TODO: Should we be smart in the choice of the move? layers = moves.mapped("stock_valuation_layer_ids") - cost = layers[-1].unit_cost - res["price_unit"] = cost + if layers: + cost = layers[-1].unit_cost + res["price_unit"] = cost elif line.account_move_line_id.purchase_line_id: purchase_lines = line.account_move_line_id.purchase_line_id moves = purchase_lines.mapped("move_ids") if moves: layers = moves.mapped("stock_valuation_layer_ids") - cost = layers[-1].unit_cost - # TODO: Should we be smart in the choice of the move? - res["price_unit"] = cost + if layers: + cost = layers[-1].unit_cost + # TODO: Should we be smart in the choice of the move? + res["price_unit"] = cost return res diff --git a/rma_purchase/tests/__init__.py b/rma_purchase/tests/__init__.py index c7f1b5f2..4d1e8f9e 100644 --- a/rma_purchase/tests/__init__.py +++ b/rma_purchase/tests/__init__.py @@ -1 +1,2 @@ from . import test_rma_purchase +from . import test_rma_stock_account_purchase diff --git a/rma_purchase/tests/test_rma_stock_account_purchase.py b/rma_purchase/tests/test_rma_stock_account_purchase.py new file mode 100644 index 00000000..2bb25bf1 --- /dev/null +++ b/rma_purchase/tests/test_rma_stock_account_purchase.py @@ -0,0 +1,81 @@ +# Copyright 2017-22 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo.fields import Datetime +from odoo.tests.common import Form + +# pylint: disable=odoo-addons-relative-import +from odoo.addons.rma_account.tests.test_rma_stock_account import TestRmaStockAccount + + +class TestRmaStockAccountPurchase(TestRmaStockAccount): + @classmethod + def setUpClass(cls): + super(TestRmaStockAccountPurchase, cls).setUpClass() + cls.pol_model = cls.env["purchase.order.line"] + cls.po_model = cls.env["purchase.order"] + # Create PO: + cls.product_fifo_1.standard_price = 1234 + cls.po = cls.po_model.create( + { + "partner_id": cls.partner_id.id, + } + ) + cls.pol_1 = cls.pol_model.create( + { + "name": cls.product_fifo_1.name, + "order_id": cls.po.id, + "product_id": cls.product_fifo_1.id, + "product_qty": 20.0, + "product_uom": cls.product_fifo_1.uom_id.id, + "price_unit": 100.0, + "date_planned": Datetime.now(), + } + ) + cls.po.button_confirm() + cls._do_picking(cls.po.picking_ids) + + def test_01_cost_from_po_move(self): + """ + Test the price unit is taken from the cost of the stock move associated to + the PO + """ + self.product_fifo_1.standard_price = 1234 # this should not be taken + supplier_view = self.env.ref("rma_purchase.view_rma_line_form") + rma_line = Form( + self.rma_line.with_context(supplier=1).with_user(self.rma_basic_user), + view=supplier_view.id, + ) + rma_line.partner_id = self.po.partner_id + rma_line.purchase_order_line_id = self.pol_1 + rma_line.price_unit = 4356 + rma_line = rma_line.save() + rma_line.action_rma_to_approve() + picking = self._deliver_rma(rma_line) + # The price is not the standard price, is the value of the incoming layer + # of the PO + rma_move_value = picking.move_lines.stock_valuation_layer_ids.value + po_move_value = self.po.picking_ids.mapped( + "move_lines.stock_valuation_layer_ids" + )[-1].value + self.assertEqual(-rma_move_value, po_move_value) + # Test the accounts used + account_move = picking.move_lines.stock_valuation_layer_ids.account_move_id + self.check_accounts_used(account_move, "grni", "inventory") + # Now forcing a refund to check the stock journals + rma_line.refund_policy = "ordered" + make_refund = ( + self.env["rma.refund"] + .with_context( + **{ + "customer": True, + "active_ids": rma_line.ids, + "active_model": "rma.order.line", + } + ) + .create({"description": "Test refund"}) + ) + make_refund.invoice_refund() + rma_line.refund_line_ids.move_id.action_post() + account_move = rma_line.mapped("refund_line_ids.move_id") + self.check_accounts_used(account_move, credit_account="grni") diff --git a/rma_sale/models/procurement.py b/rma_sale/models/procurement.py index 07b291e6..1b3248ee 100644 --- a/rma_sale/models/procurement.py +++ b/rma_sale/models/procurement.py @@ -37,14 +37,16 @@ class StockRule(models.Model): if moves: # TODO: Should we be smart in the choice of the move? layers = moves.mapped("stock_valuation_layer_ids") - cost = layers[-1].unit_cost - res["price_unit"] = cost + if layers: + cost = layers[-1].unit_cost + res["price_unit"] = cost elif line.account_move_line_id: sale_lines = line.account_move_line_id.sale_line_ids moves = sale_lines.mapped("move_ids") if moves: layers = moves.mapped("stock_valuation_layer_ids") - cost = layers[-1].unit_cost - # TODO: Should we be smart in the choice of the move? - res["price_unit"] = cost + if layers: + cost = layers[-1].unit_cost + # TODO: Should we be smart in the choice of the move? + res["price_unit"] = cost return res diff --git a/rma_sale/tests/__init__.py b/rma_sale/tests/__init__.py index a4340678..2d622b80 100644 --- a/rma_sale/tests/__init__.py +++ b/rma_sale/tests/__init__.py @@ -1,4 +1,2 @@ -# Copyright 2020 ForgeFlow S.L. -# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) - from . import test_rma_sale +from . import test_rma_stock_account_sale diff --git a/rma_sale/tests/test_rma_stock_account_sale.py b/rma_sale/tests/test_rma_stock_account_sale.py new file mode 100644 index 00000000..07eca91f --- /dev/null +++ b/rma_sale/tests/test_rma_stock_account_sale.py @@ -0,0 +1,74 @@ +# Copyright 2022 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo.tests.common import Form + +# pylint: disable=odoo-addons-relative-import +from odoo.addons.rma_account.tests.test_rma_stock_account import TestRmaStockAccount + + +class TestRmaStockAccountSale(TestRmaStockAccount): + @classmethod + def setUpClass(cls): + super(TestRmaStockAccountSale, cls).setUpClass() + customer1 = cls.env["res.partner"].create({"name": "Customer 1"}) + cls.product_fifo_1.standard_price = 1234 + cls._create_inventory(cls.product_fifo_1, 20.0, cls.env.ref("rma.location_rma")) + cls.so1 = cls.env["sale.order"].create( + { + "partner_id": customer1.id, + "partner_invoice_id": customer1.id, + "partner_shipping_id": customer1.id, + "order_line": [ + ( + 0, + 0, + { + "name": cls.product_fifo_1.name, + "product_id": cls.product_fifo_1.id, + "product_uom_qty": 20.0, + "product_uom": cls.product_fifo_1.uom_id.id, + "price_unit": 800, + }, + ), + ], + "pricelist_id": cls.env.ref("product.list0").id, + } + ) + cls.so1.action_confirm() + for ml in cls.so1.picking_ids.move_line_ids: + ml.qty_done = ml.product_uom_qty + cls.so1.picking_ids.button_validate() + + def test_01_cost_from_so_move(self): + """ + Test the price unit is taken from the cost of the stock move associated to + the SO + """ + so_line = self.so1.order_line.filtered( + lambda r: r.product_id == self.product_fifo_1 + ) + self.product_fifo_1.standard_price = 5678 # this should not be taken + customer_view = self.env.ref("rma_sale.view_rma_line_form") + rma_line = Form( + self.rma_line.with_context(customer=1).with_user(self.rma_basic_user), + view=customer_view.id, + ) + rma_line.partner_id = self.so1.partner_id + rma_line.sale_line_id = so_line + rma_line.price_unit = 4356 + rma_line = rma_line.save() + rma_line.action_rma_to_approve() + picking = self._receive_rma(rma_line) + # The price is not the standard price, is the value of the outgoing layer + # of the SO + rma_move_value = picking.move_lines.stock_valuation_layer_ids.value + so_move_value = self.so1.picking_ids.mapped( + "move_lines.stock_valuation_layer_ids" + )[-1].value + self.assertEqual(rma_move_value, -so_move_value) + # Test the accounts used + account_move = picking.move_lines.stock_valuation_layer_ids.account_move_id + self.check_accounts_used( + account_move, debit_account="inventory", credit_account="cogs" + )