diff --git a/account_move_line_rma_order_line/__manifest__.py b/account_move_line_rma_order_line/__manifest__.py index 285f3c1d..77666408 100644 --- a/account_move_line_rma_order_line/__manifest__.py +++ b/account_move_line_rma_order_line/__manifest__.py @@ -17,4 +17,5 @@ "maintainers": ["ChisOForgeFlow"], "development_status": "Beta", "post_init_hook": "post_init_hook", + "auto_install": True, } 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 index 8907d16d..7f564cf5 100644 --- 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 @@ -82,14 +82,14 @@ class TestAccountMoveLineRmaOrderLine(common.SavepointCase): return user.id @classmethod - def _create_account_type(cls, name, atype, internal_group): + def _create_account_type(cls, name, account_type, internal_group): acc_type = cls.acc_type_model.create( - {"name": name, "type": atype, "internal_group": internal_group} + {"name": name, "type": account_type, "internal_group": internal_group} ) return acc_type @classmethod - def _create_account(cls, acc_type, name, code, company): + def _create_account(cls, acc_type, name, code, company, reconcile=False): """Create an account.""" account = cls.account_model.create( { @@ -97,6 +97,7 @@ class TestAccountMoveLineRmaOrderLine(common.SavepointCase): "code": code, "user_type_id": acc_type.id, "company_id": company.id, + "reconcile": reconcile, } ) return account diff --git a/rma/models/res_partner.py b/rma/models/res_partner.py index 0a263d4f..040a1d46 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,10 @@ 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 d7924685..faa3dd4d 100644 --- a/rma/tests/test_rma.py +++ b/rma/tests/test_rma.py @@ -11,15 +11,18 @@ class TestRma(common.SavepointCase): @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.SavepointCase): 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_manager = cls.env.ref("rma.group_rma_manager") + 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_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.SavepointCase): 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,124 @@ class TestRma(common.SavepointCase): ) @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.onchange_picking_type() + 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.inventory"].create( + { + "name": "Initial inventory", + "line_ids": [ + ( + 0, + 0, + { + "product_id": product.id, + "product_uom_id": product.uom_id.id, + "product_qty": qty, + "location_id": location.id, + }, + ) + ], + } + ) + inventory._action_start() + inventory.action_validate() + 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 +280,7 @@ class TestRma(common.SavepointCase): ) 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 +297,14 @@ class TestRma(common.SavepointCase): "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 +320,9 @@ class TestRma(common.SavepointCase): "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 +330,15 @@ class TestRma(common.SavepointCase): 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 +356,8 @@ class TestRma(common.SavepointCase): "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 +647,12 @@ class TestRma(common.SavepointCase): 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 3310893b..1a3abf60 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 b86eb3bd..0413d60f 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_account_unreconciled/tests/test_rma_account_unreconciled.py b/rma_account_unreconciled/tests/test_rma_account_unreconciled.py index c82111ad..db0e6c5f 100644 --- a/rma_account_unreconciled/tests/test_rma_account_unreconciled.py +++ b/rma_account_unreconciled/tests/test_rma_account_unreconciled.py @@ -10,7 +10,9 @@ class TestRmaAccountUnreconciled(TestRma): def setUpClass(cls): super().setUpClass() cls.rma_refund_wiz = cls.env["rma.refund"] - for categ in cls.rma_customer_id.mapped("rma_line_ids.product_id.categ_id"): + for categ in cls.rma_customer_id.with_user(cls.rma_manager_user).mapped( + "rma_line_ids.product_id.categ_id" + ): categ.write( { "property_valuation": "real_time", @@ -27,7 +29,9 @@ class TestRmaAccountUnreconciled(TestRma): "reconcile": True, } ) - for product in cls.rma_customer_id.mapped("rma_line_ids.product_id"): + for product in cls.rma_customer_id.with_user(cls.rma_manager_user).mapped( + "rma_line_ids.product_id" + ): product.write( { "standard_price": 10.0, @@ -80,7 +84,9 @@ class TestRmaAccountUnreconciled(TestRma): } ) make_refund.invoice_refund() - self.rma_customer_id.rma_line_ids.refund_line_ids.move_id.filtered( + self.rma_customer_id.with_user( + self.rma_manager_user + ).rma_line_ids.refund_line_ids.move_id.filtered( lambda x: x.state != "posted" ).action_post() for rma_line in self.rma_customer_id.rma_line_ids: @@ -98,7 +104,7 @@ class TestRmaAccountUnreconciled(TestRma): 3, ) for rma_line in self.rma_customer_id.rma_line_ids: - aml_domain = rma_line.action_view_unreconciled().get("domain") + aml_domain = rma_line.sudo().action_view_unreconciled().get("domain") aml_lines = ( aml_domain and self.env["account.move.line"].search(aml_domain) or False ) 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..cea43d82 --- /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" + )