diff --git a/account_move_line_rma_order_line/README.rst b/account_move_line_rma_order_line/README.rst new file mode 100644 index 000000000..d8f62172a --- /dev/null +++ b/account_move_line_rma_order_line/README.rst @@ -0,0 +1,66 @@ +.. image:: https://img.shields.io/badge/license-AGPLv3-blue.svg + :target: https://www.gnu.org/licenses/agpl.html + :alt: License: AGPL-3 + +========================== +Account Move Line RMA Line +========================== + +This module will add the RMA order line to journal items. + +The ultimate goal is to establish the RMA order line as one of the key +fields to reconcile the Goods Received Not Invoiced accrual account. + + +Usage +===== + +The RMA order line will be automatically copied to the journal items. + +* When a supplier invoice is created referencing RMA orders, the + RMA order line will be copied to the corresponding journal item. + +* When a stock move is validated and generates a journal entry, the RMA + order line is copied to the account move line. + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/92/9.0 + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Jordi Ballester Alomar +* Aarón Henríquez Quintana + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/account_move_line_rma_order_line/__init__.py b/account_move_line_rma_order_line/__init__.py new file mode 100644 index 000000000..e7cb8a0c7 --- /dev/null +++ b/account_move_line_rma_order_line/__init__.py @@ -0,0 +1,42 @@ +from . import models + +import logging +from odoo import api, SUPERUSER_ID + +_logger = logging.getLogger(__name__) + + +def post_init_hook(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + aml_model = env["account.move.line"] + sm_model = env["stock.move"] + svl_model = env["stock.valuation.layer"] + aml_moves = aml_model.search([("rma_line_id", "!=", False)]) + sm_moves = sm_model.search([("rma_line_id", "!=", False)]) + for account_move in aml_moves.mapped("move_id"): + for aml_w_rma in account_move.line_ids.filtered( + lambda x: x.product_id + and x.account_id.id + != x.product_id.categ_id.property_stock_valuation_account_id.id + and x.rma_line_id + ): + move_lines_without_rma = account_move.line_ids.filtered( + lambda x: x.product_id.id == aml_w_rma.product_id.id + and not x.rma_line_id + and aml_w_rma.name in x.name + ) + if move_lines_without_rma: + move_lines_without_rma.write( + { + "rma_line_id": aml_w_rma.rma_line_id.id, + } + ) + for move in sm_moves: + current_layers = svl_model.search([("stock_move_id", "=", move.id)]) + if current_layers: + for aml in current_layers.mapped("account_move_id.line_ids").filtered( + lambda x: x.account_id.id + != move.product_id.categ_id.property_stock_valuation_account_id.id + and not x.rma_line_id + ): + aml.rma_line_id = move.rma_line_id.id diff --git a/account_move_line_rma_order_line/__manifest__.py b/account_move_line_rma_order_line/__manifest__.py new file mode 100644 index 000000000..443c256ae --- /dev/null +++ b/account_move_line_rma_order_line/__manifest__.py @@ -0,0 +1,19 @@ +# © 2017-2022 ForgeFlow S.L. (www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + "name": "Account Move Line Rma Order Line", + "summary": "Introduces the rma order line to the journal items", + "version": "16.0.1.1.0", + "author": "ForgeFlow, " "Odoo Community Association (OCA)", + "website": "https://github.com/ForgeFlow/stock-rma", + "category": "Generic", + "depends": ["stock_account", "rma_account"], + "license": "AGPL-3", + "data": [], + "installable": True, + "maintainers": ["ChisOForgeFlow"], + "development_status": "Beta", + "post_init_hook": "post_init_hook", + "auto_install": True, +} diff --git a/account_move_line_rma_order_line/models/__init__.py b/account_move_line_rma_order_line/models/__init__.py new file mode 100644 index 000000000..4edac2425 --- /dev/null +++ b/account_move_line_rma_order_line/models/__init__.py @@ -0,0 +1,2 @@ +from . import stock_move +from . import account_move diff --git a/account_move_line_rma_order_line/models/account_move.py b/account_move_line_rma_order_line/models/account_move.py new file mode 100644 index 000000000..98a91efff --- /dev/null +++ b/account_move_line_rma_order_line/models/account_move.py @@ -0,0 +1,33 @@ +# © 2017-2022 ForgeFlow S.L. (www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import models + + +class AccountMove(models.Model): + + _inherit = "account.move" + + def _stock_account_prepare_anglo_saxon_out_lines_vals(self): + product_model = self.env["product.product"] + res = super()._stock_account_prepare_anglo_saxon_out_lines_vals() + for line in res: + if line.get("product_id", False): + product = product_model.browse(line.get("product_id", False)) + if ( + line.get("account_id") + != product.categ_id.property_stock_valuation_account_id.id + ): + current_move = self.browse(line.get("move_id", False)) + current_rma = current_move.invoice_line_ids.filtered( + lambda x: x.rma_line_id and x.product_id.id == product.id + ).mapped("rma_line_id") + if len(current_rma) == 1: + line.update({"rma_line_id": current_rma.id}) + elif len(current_rma) > 1: + find_with_label_rma = current_rma.filtered( + lambda x: x.name == line.get("name") + ) + if len(find_with_label_rma) == 1: + line.update({"rma_line_id": find_with_label_rma.id}) + return res diff --git a/account_move_line_rma_order_line/models/stock_move.py b/account_move_line_rma_order_line/models/stock_move.py new file mode 100644 index 000000000..e2e01bc34 --- /dev/null +++ b/account_move_line_rma_order_line/models/stock_move.py @@ -0,0 +1,23 @@ +# © 2017-2022 ForgeFlow S.L. (www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import api, models + + +class StockMove(models.Model): + _inherit = "stock.move" + + @api.model + def _prepare_account_move_line( + self, qty, cost, credit_account_id, debit_account_id, svl_id, description + ): + res = super(StockMove, self)._prepare_account_move_line( + qty, cost, credit_account_id, debit_account_id, svl_id, description + ) + for line in res: + if ( + line[2]["account_id"] + != self.product_id.categ_id.property_stock_valuation_account_id.id + ): + line[2]["rma_line_id"] = self.rma_line_id.id + return res diff --git a/account_move_line_rma_order_line/readme/CONTRIBUTORS.rst b/account_move_line_rma_order_line/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..b647a292e --- /dev/null +++ b/account_move_line_rma_order_line/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Christopher Ormaza diff --git a/account_move_line_rma_order_line/readme/DESCRIPTION.rst b/account_move_line_rma_order_line/readme/DESCRIPTION.rst new file mode 100644 index 000000000..d1ae4599c --- /dev/null +++ b/account_move_line_rma_order_line/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +This module will add the RMA order line to journal items. + +The ultimate goal is to establish the RMA order line as one of the key +fields to reconcile the Goods Received Not Invoiced accrual account. diff --git a/account_move_line_rma_order_line/readme/USAGE.rst b/account_move_line_rma_order_line/readme/USAGE.rst new file mode 100644 index 000000000..bd5357e84 --- /dev/null +++ b/account_move_line_rma_order_line/readme/USAGE.rst @@ -0,0 +1,7 @@ +The RMA order line will be automatically copied to the journal items. + +* When a supplier invoice is created referencing RMA orders, the + RMA order line will be copied to the corresponding journal item. + +* When a stock move is validated and generates a journal entry, the RMA + order line is copied to the account move line. diff --git a/account_move_line_rma_order_line/tests/__init__.py b/account_move_line_rma_order_line/tests/__init__.py new file mode 100644 index 000000000..641042a11 --- /dev/null +++ b/account_move_line_rma_order_line/tests/__init__.py @@ -0,0 +1 @@ +from . import test_account_move_line_rma_order_line 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 new file mode 100644 index 000000000..4d6328b6a --- /dev/null +++ b/account_move_line_rma_order_line/tests/test_account_move_line_rma_order_line.py @@ -0,0 +1,276 @@ +# © 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.TransactionCase): + @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.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 = "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 = "expense" + name = "Goods Delivered Not Invoiced" + code = "gdni" + cls.account_cogs = cls._create_account(acc_type, name, code, cls.company) + # Create account for Inventory + acc_type = "asset_cash" + 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(cls, acc_type, name, code, company, reconcile=False): + """Create an account.""" + account = cls.account_model.create( + { + "name": name, + "code": code, + "account_type": acc_type, + "company_id": company.id, + "reconcile": reconcile, + } + ) + return account + + @classmethod + def _create_product(cls): + """Create a Product.""" + 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, + } + ) + wizard.add_lines() + + 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_ids.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/setup/account_move_line_rma_order_line/odoo/addons/account_move_line_rma_order_line b/setup/account_move_line_rma_order_line/odoo/addons/account_move_line_rma_order_line new file mode 120000 index 000000000..5b84b3857 --- /dev/null +++ b/setup/account_move_line_rma_order_line/odoo/addons/account_move_line_rma_order_line @@ -0,0 +1 @@ +../../../../account_move_line_rma_order_line \ No newline at end of file diff --git a/setup/account_move_line_rma_order_line/setup.py b/setup/account_move_line_rma_order_line/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/account_move_line_rma_order_line/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)