diff --git a/.travis.yml b/.travis.yml index 63fcf16b..2d1d80fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -36,10 +36,10 @@ env: global: - VERSION="13.0" TESTS="0" LINT_CHECK="0" MAKEPOT="0" - install: - - git clone -b master-eficent https://github.com/ForgeFlow/maintainer-quality-tools.git $HOME/maintainer-quality-tools - - export PATH=${HOME}/maintainer-quality-tools/travis:${PATH} - - travis_install_nightly +install: + - git clone -b master-eficent https://github.com/ForgeFlow/maintainer-quality-tools.git $HOME/maintainer-quality-tools + - export PATH=${HOME}/maintainer-quality-tools/travis:${PATH} + - travis_install_nightly script: - travis_run_tests diff --git a/LICENSE b/LICENSE index 02bbb60b..65c5ca88 100644 --- a/LICENSE +++ b/LICENSE @@ -162,4 +162,4 @@ General Public License ever published by the Free Software Foundation. whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the -Library. \ No newline at end of file +Library. diff --git a/rma/README.rst b/rma/README.rst new file mode 100644 index 00000000..4cf46b44 --- /dev/null +++ b/rma/README.rst @@ -0,0 +1,136 @@ +====================================== +RMA (Return Merchandise Authorization) +====================================== + +A Return Merchandise Authorization (RMA), is a part of the process of +returning a product in order to receive a refund, replacement, or repair +during the product's warranty period. + +The purchaser of the product must contact the manufacturer (or distributor +or retailer) to obtain authorization to return the product. + +The resulting RMA number must be displayed on or included in the returned +product's packaging. + +The issuance of an RMA is a key gatekeeping moment in the reverse logistics +cycle, providing the vendor with a final opportunity to diagnose and correct +the customer's problem with the product (such as improper installation or +configuration) before the customer permanently relinquishes ownership +of the product to the manufacturer, commonly referred to as a return. + +As returns are costly for the vendor and inconvenient for the customer, +any return that can be prevented benefits both parties. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Security +~~~~~~~~ + +Go to Settings > Users and assign the appropiate permissions to users. +Different security groups grant distinct levels of access to the RMA features. + +* Users in group "RMA Customer User" or "RMA Supplier User" can access to, + create and process RMA's associated to customers or suppliers respectively. + +* Users in group "RMA Manager" can access to, create, approve and process RMA's + associated to both customers and suppliers. + +RMA Approval Policy +~~~~~~~~~~~~~~~~~~~ + +There are two RMA approval policies in product catogories: + +* One step: Always auto-approve RMAs that only contain products within + categories with this policy. +* Two steps: A RMA order containing a product within a category with this + policy will request the RMA manager approval. + +In order to change the approval policy of a product category follow the next +steps: + +#. Go to *Inventory > Configuration > Products > Product Categories*. +#. Select one and change the field *RMA Approval Policy* to your convenience. + +Other Settings +~~~~~~~~~~~~~~ + +#. Go to RMA > Configuration > Settings > Return Merchandising + Authorization and select the option "Display 3 fields on rma: partner, + invoice address, delivery address" if needed. +#. Go to RMA > Configuration > Warehouse management > Warehouses and add + a default RMA location and RMA picking type for customers and suppliers RMA + picking type. In case the warehouse is configured to use routes, you need to + create at least one route per rma type with at least two push rules (one for + inbound another for outbound) it's very important to select the type of + operation supplier if we are moving in the company and customer if we are + moving out of the company. + +Usage +===== + +RMA are accessible though Inventory menu. There's four menus, divided by type. +Users can access to the list of RMA or RMA lines. + +Create an RMA: + +#. Select a partner. Enter RMA lines associated to an existing picking, or + manually. +#. Request approval and approve. +#. Click on RMA Lines button. +#. Click on more and select an option: "Receive products", "Create Delivery + Order". +#. Go back to the RMA. Set the RMA to done if not further action is required. + +Known issues / Roadmap +====================== + +* Picking operations report in customer RMA dropshipping case is showing + "Vendor Address" while it should be "Customer Address". +* Dropshipping always counted as a delivery on the smart buttons. +* Uninstall hook. +* Constraints instead of required fields on rma.order.line. +* Rename type field on rma.order and rma.order.line + +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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ForgeFlow S.L. + +Contributors +~~~~~~~~~~~~ + +* Jordi Ballester Alomar +* Aaron Henriquez +* Lois Rilo +* Bhavesh Odedra +* Akim Juillerat +* Alexandre Fayolle +* Chafique Delli +* Héctor Villarreal + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the ForgeFlow. + +This module is part of the `ForgeFlow/stock-rma `_ project on GitHub. + diff --git a/rma/__init__.py b/rma/__init__.py new file mode 100644 index 00000000..79030e0e --- /dev/null +++ b/rma/__init__.py @@ -0,0 +1,5 @@ +# Copyright (C) 2017-20 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from . import models +from . import wizards diff --git a/rma/__manifest__.py b/rma/__manifest__.py new file mode 100644 index 00000000..873c7b36 --- /dev/null +++ b/rma/__manifest__.py @@ -0,0 +1,38 @@ +# Copyright (C) 2017-20 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +{ + "name": "RMA (Return Merchandise Authorization)", + "version": "13.0.1.0.0", + "license": "LGPL-3", + "category": "RMA", + "summary": "Introduces the return merchandise authorization (RMA) process " + "in odoo", + "author": "ForgeFlow", + "website": "https://github.com/ForgeFlow/stock-rma", + "depends": ["stock", "mail", "web"], + "demo": ["demo/stock_demo.xml"], + "data": [ + "security/rma.xml", + "security/ir.model.access.csv", + "data/rma_sequence.xml", + "data/stock_data.xml", + "data/rma_operation.xml", + "report/rma_report.xml", + "report/rma_report_templates.xml", + "views/rma_order_view.xml", + "views/rma_operation_view.xml", + "views/rma_order_line_view.xml", + "views/stock_view.xml", + "views/stock_warehouse.xml", + "views/product_view.xml", + "views/res_partner_view.xml", + "views/res_config_settings_views.xml", + "views/rma_menu.xml", + "wizards/rma_make_picking_view.xml", + "wizards/rma_add_stock_move_view.xml", + "wizards/rma_order_line_make_supplier_rma_view.xml", + ], + "installable": True, + "application": True, +} diff --git a/rma/data/rma_operation.xml b/rma/data/rma_operation.xml new file mode 100644 index 00000000..9d1fc02f --- /dev/null +++ b/rma/data/rma_operation.xml @@ -0,0 +1,45 @@ + + + + Replace After Receive + RPL-C + ordered + received + customer + + + + + + Replace + RPL-S + ordered + ordered + supplier + + + + + + Dropship - Deliver to vendor + DS-RPL-C + ordered + no + customer + + + + + + + Dropship - Deliver to customer + DS-RPL-S + no + ordered + supplier + + + + + + diff --git a/rma/data/rma_sequence.xml b/rma/data/rma_sequence.xml new file mode 100644 index 00000000..d025881e --- /dev/null +++ b/rma/data/rma_sequence.xml @@ -0,0 +1,42 @@ + + + + Customer RMA sequence + rma.order.customer + 5 + RMAG/%(year)s/ + + + + + + Supplier RMA sequence + rma.order.supplier + 5 + RTVG/%(year)s/ + + + + + + + Customer RMA Line sequence + rma.order.line.customer + 5 + RMA/%(year)s/ + + + + + + + Supplier RMA Line sequence + rma.order.line.supplier + 5 + RTV/%(year)s/ + + + + + + diff --git a/rma/data/stock_data.xml b/rma/data/stock_data.xml new file mode 100644 index 00000000..08d4e687 --- /dev/null +++ b/rma/data/stock_data.xml @@ -0,0 +1,216 @@ + + + + WH RMA + internal + + + + + DOC + RCO + 5 + + + + + INC + RCI + 5 + + + + + DOS + RSO + 5 + + + + + INS + RSI + 5 + + + + + DSS + DSS + 5 + + + + + RMA → Customer + + RMA → Customer + + + + outgoing + + + + Customer → RMA + + Customer → RMA + + + + incoming + + + + RMA -> Supplier + + RMA -> Supplier + + + + outgoing + + + + Supplier -> RMA + + Supplier -> RMA + + + + incoming + + + + Customer -> Supplier + + Customer -> Supplier + + + + incoming + + + + Supplier -> Customer + + Supplier -> Customer + + + + incoming + + + + + + + + + + + + RMA Customer + + 10 + + + + + + + + RMA Supplier + + 10 + + + + + + + + RMA Dropship + + 10 + + + + + + + + Customer → RMA + pull + + + + make_to_stock + + + + + + RMA → Customer + pull + + + + make_to_stock + + + + + + RMA → Supplier + pull + + + + make_to_stock + + + + + + Supplier → RMA + pull + + + + make_to_stock + + + + + + Customer → Supplier + pull + + + make_to_stock + + + + + + Supplier → Customer + pull + + + make_to_stock + + + + + diff --git a/rma/demo/stock_demo.xml b/rma/demo/stock_demo.xml new file mode 100644 index 00000000..0c22b4df --- /dev/null +++ b/rma/demo/stock_demo.xml @@ -0,0 +1,121 @@ + + + + + + RMA Customer Sequence out + RMA/CUST/OUT + 5 + + + + + RMA Customer Sequence in + RMA/CUST/IN + 5 + + + + + RMA Supplier Sequence out + RMA/SUPP/OUT + 5 + + + + + RMA Supplier Sequence in + RMA/SUPP/IN + 5 + + + + + RMA Dropship + RMA/DS + 5 + + + + + + Customer -> Supplier + + + + + incoming + + + + Supplier -> Customer + + + + + incoming + + + + + RMA Customer + 10 + + + + + + + + RMA Supplier + 10 + + + + + + + + RMA Dropship + 10 + + + + + + + + + Customer → Supplier + pull + + + + make_to_stock + + + + + + Supplier → Customer + pull + + + + make_to_stock + + + + + + + + + + diff --git a/rma/models/__init__.py b/rma/models/__init__.py new file mode 100644 index 00000000..ae4f5826 --- /dev/null +++ b/rma/models/__init__.py @@ -0,0 +1,13 @@ +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from . import rma_order +from . import rma_order_line +from . import rma_operation +from . import stock +from . import stock_warehouse +from . import product +from . import product_category +from . import procurement +from . import res_partner +from . import res_company +from . import res_config_settings diff --git a/rma/models/procurement.py b/rma/models/procurement.py new file mode 100644 index 00000000..132376a9 --- /dev/null +++ b/rma/models/procurement.py @@ -0,0 +1,52 @@ +# Copyright (C) 2017-20 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo import fields, models + + +class StockRule(models.Model): + _inherit = "stock.rule" + + def _get_stock_move_values( + self, + product_id, + product_qty, + product_uom, + location_id, + name, + origin, + company_id, + values, + ): + res = super(StockRule, self)._get_stock_move_values( + product_id, + product_qty, + product_uom, + location_id, + name, + origin, + company_id, + values, + ) + if "rma_line_id" in values: + line = values.get("rma_line_id") + res["rma_line_id"] = line.id + if line.delivery_address_id: + res["partner_id"] = line.delivery_address_id.id + else: + res["partner_id"] = line.rma_id.partner_id.id + dest_loc = self.env["stock.location"].browse([res["location_dest_id"]])[0] + if dest_loc.usage == "internal": + res["price_unit"] = line.price_unit + return res + + +class ProcurementGroup(models.Model): + _inherit = "procurement.group" + + rma_id = fields.Many2one( + comodel_name="rma.order", string="RMA", ondelete="set null" + ) + rma_line_id = fields.Many2one( + comodel_name="rma.order.line", string="RMA line", ondelete="set null" + ) diff --git a/rma/models/product.py b/rma/models/product.py new file mode 100644 index 00000000..418e6860 --- /dev/null +++ b/rma/models/product.py @@ -0,0 +1,18 @@ +# Copyright (C) 2017-20 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + rma_customer_operation_id = fields.Many2one( + comodel_name="rma.operation", string="Default RMA Customer Operation" + ) + rma_supplier_operation_id = fields.Many2one( + comodel_name="rma.operation", string="Default RMA Supplier Operation" + ) + rma_approval_policy = fields.Selection( + related="categ_id.rma_approval_policy", readonly=True + ) diff --git a/rma/models/product_category.py b/rma/models/product_category.py new file mode 100644 index 00000000..b528731c --- /dev/null +++ b/rma/models/product_category.py @@ -0,0 +1,26 @@ +# Copyright 2017 ForgeFlow +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo import fields, models + + +class ProductCategory(models.Model): + _inherit = "product.category" + + rma_approval_policy = fields.Selection( + selection=[("one_step", "One step"), ("two_step", "Two steps")], + string="RMA Approval Policy", + required=True, + default="one_step", + help="Options: \n " + "* One step: Always auto-approve RMAs that only contain " + "products within categories with this policy.\n" + "* Two steps: A RMA containing a product within a category with " + "this policy will request the RMA manager approval.", + ) + rma_customer_operation_id = fields.Many2one( + comodel_name="rma.operation", string="Default RMA Customer Operation" + ) + rma_supplier_operation_id = fields.Many2one( + comodel_name="rma.operation", string="Default RMA Supplier Operation" + ) diff --git a/rma/models/res_company.py b/rma/models/res_company.py new file mode 100644 index 00000000..1ed19e1d --- /dev/null +++ b/rma/models/res_company.py @@ -0,0 +1,14 @@ +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + group_rma_delivery_address = fields.Boolean( + string="RMA addresses", + help="Display 3 fields on rma: partner, invoice address, delivery address", + ) + + group_rma_lines = fields.Boolean( + string="Use RMA groups", help="Group RMA lines in one RMA group" + ) diff --git a/rma/models/res_config_settings.py b/rma/models/res_config_settings.py new file mode 100644 index 00000000..4d889880 --- /dev/null +++ b/rma/models/res_config_settings.py @@ -0,0 +1,20 @@ +# Copyright (C) 2017-20 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + group_rma_delivery_address = fields.Boolean( + related="company_id.group_rma_delivery_address", + implied_group="rma.group_rma_delivery_invoice_address", + readonly=False, + ) + + group_rma_lines = fields.Boolean( + related="company_id.group_rma_lines", + readonly=False, + implied_group="rma.group_rma_groups", + ) diff --git a/rma/models/res_partner.py b/rma/models/res_partner.py new file mode 100644 index 00000000..4fad21cc --- /dev/null +++ b/rma/models/res_partner.py @@ -0,0 +1,23 @@ +# Copyright 2017 ForgeFlow +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + def _compute_rma_line_count(self): + for rec in self: + rec.rma_line_count = len(rec.rma_line_ids) + + 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") + + def action_open_partner_rma(self): + action = self.env.ref("rma.action_rma_customer_lines") + result = action.read()[0] + result["context"] = {"search_default_partner_id": self.id} + return result diff --git a/rma/models/rma_operation.py b/rma/models/rma_operation.py new file mode 100644 index 00000000..104dc9bf --- /dev/null +++ b/rma/models/rma_operation.py @@ -0,0 +1,98 @@ +# Copyright 2017-18 ForgeFlow +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo import api, fields, models + + +class RmaOperation(models.Model): + _name = "rma.operation" + _description = "RMA Operation" + + @api.model + def _default_warehouse_id(self): + company_id = self.env.user.company_id.id + warehouse = self.env["stock.warehouse"].search( + [("company_id", "=", company_id)], limit=1 + ) + return warehouse + + @api.model + def _default_customer_location_id(self): + return self.env.ref("stock.stock_location_customers") or False + + @api.model + def _default_supplier_location_id(self): + return self.env.ref("stock.stock_location_suppliers") or False + + @api.model + def _default_routes(self): + op_type = self.env.context.get("default_type") + if op_type == "customer": + return self.env.ref("rma.route_rma_customer") + elif op_type == "supplier": + return self.env.ref("rma.route_rma_supplier") + + name = fields.Char("Description", required=True) + code = fields.Char("Code", required=True) + active = fields.Boolean(string="Active", default=True) + receipt_policy = fields.Selection( + [ + ("no", "Not required"), + ("ordered", "Based on Ordered Quantities"), + ("delivered", "Based on Delivered Quantities"), + ], + string="Receipts Policy", + default="no", + ) + delivery_policy = fields.Selection( + [ + ("no", "Not required"), + ("ordered", "Based on Ordered Quantities"), + ("received", "Based on Received Quantities"), + ], + string="Delivery Policy", + default="no", + ) + in_route_id = fields.Many2one( + comodel_name="stock.location.route", + string="Inbound Route", + domain=[("rma_selectable", "=", True)], + default=lambda self: self._default_routes(), + ) + out_route_id = fields.Many2one( + comodel_name="stock.location.route", + string="Outbound Route", + domain=[("rma_selectable", "=", True)], + default=lambda self: self._default_routes(), + ) + customer_to_supplier = fields.Boolean( + string="The customer will send to the supplier" + ) + supplier_to_customer = fields.Boolean( + string="The supplier will send to the customer" + ) + in_warehouse_id = fields.Many2one( + comodel_name="stock.warehouse", + string="Inbound Warehouse", + default=lambda self: self._default_warehouse_id(), + ) + out_warehouse_id = fields.Many2one( + comodel_name="stock.warehouse", + string="Outbound Warehouse", + default=lambda self: self._default_warehouse_id(), + ) + location_id = fields.Many2one("stock.location", "Send To This Company Location") + type = fields.Selection( + [("customer", "Customer"), ("supplier", "Supplier")], + string="Used in RMA of this type", + required=True, + ) + rma_line_ids = fields.One2many( + comodel_name="rma.order.line", inverse_name="operation_id", string="RMA lines" + ) + company_id = fields.Many2one( + comodel_name="res.company", + string="Company", + required=True, + default=lambda self: self.env.user.company_id, + ) diff --git a/rma/models/rma_order.py b/rma/models/rma_order.py new file mode 100644 index 00000000..c84ddc80 --- /dev/null +++ b/rma/models/rma_order.py @@ -0,0 +1,295 @@ +# Copyright (C) 2017-20 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from datetime import datetime + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class RmaOrder(models.Model): + _name = "rma.order" + _description = "RMA Group" + _inherit = ["mail.thread"] + + @api.model + def _get_default_type(self): + if "supplier" in self.env.context: + return "supplier" + return "customer" + + def _compute_in_shipment_count(self): + for rec in self: + picking_ids = [] + if not rec.rma_line_ids: + rec.in_shipment_count = 0 + continue + for line in rec.rma_line_ids: + for move in line.move_ids: + if move.location_dest_id.usage == "internal": + picking_ids.append(move.picking_id.id) + else: + if line.customer_to_supplier: + picking_ids.append(move.picking_id.id) + shipments = list(set(picking_ids)) + rec.in_shipment_count = len(shipments) + + def _compute_out_shipment_count(self): + picking_ids = [] + for rec in self: + if not rec.rma_line_ids: + rec.out_shipment_count = 0 + continue + for line in rec.rma_line_ids: + for move in line.move_ids: + if move.location_dest_id.usage in ("supplier", "customer"): + if not line.customer_to_supplier: + picking_ids.append(move.picking_id.id) + shipments = list(set(picking_ids)) + rec.out_shipment_count = len(shipments) + + def _compute_supplier_line_count(self): + self.supplier_line_count = len( + self.rma_line_ids.filtered(lambda r: r.supplier_rma_line_ids) + ) + + def _compute_line_count(self): + for rec in self: + rec.line_count = len(rec._get_valid_lines()) + + @api.depends("rma_line_ids", "rma_line_ids.state") + def _compute_state(self): + for rec in self: + rma_line_done = self.env["rma.order.line"].search_count( + [("id", "in", rec.rma_line_ids.ids), ("state", "=", "done")] + ) + rma_line_approved = self.env["rma.order.line"].search_count( + [("id", "in", rec.rma_line_ids.ids), ("state", "=", "approved")] + ) + rma_line_to_approve = self.env["rma.order.line"].search_count( + [("id", "in", rec.rma_line_ids.ids), ("state", "=", "to_approve")] + ) + if rma_line_done != 0: + state = "done" + elif rma_line_approved != 0: + state = "approved" + elif rma_line_to_approve != 0: + state = "to_approve" + else: + state = "draft" + rec.state = state + + @api.model + def _default_date_rma(self): + return datetime.now() + + @api.model + def _default_warehouse_id(self): + warehouse = self.env["stock.warehouse"].search( + [("company_id", "=", self.env.user.company_id.id)], limit=1 + ) + return warehouse + + name = fields.Char(string="Group Number", index=True, copy=False) + type = fields.Selection( + [("customer", "Customer"), ("supplier", "Supplier")], + string="Type", + required=True, + default=lambda self: self._get_default_type(), + readonly=True, + ) + reference = fields.Char( + string="Partner Reference", help="The partner reference of this RMA order." + ) + comment = fields.Text("Additional Information") + date_rma = fields.Datetime( + string="Order Date", index=True, default=lambda self: self._default_date_rma() + ) + partner_id = fields.Many2one( + comodel_name="res.partner", string="Partner", required=True + ) + rma_line_ids = fields.One2many("rma.order.line", "rma_id", string="RMA lines") + in_shipment_count = fields.Integer( + compute="_compute_in_shipment_count", string="# of Invoices" + ) + out_shipment_count = fields.Integer( + compute="_compute_out_shipment_count", string="# of Outgoing Shipments" + ) + line_count = fields.Integer(compute="_compute_line_count", string="# of RMA lines") + supplier_line_count = fields.Integer( + compute="_compute_supplier_line_count", string="# of Supplier RMAs" + ) + company_id = fields.Many2one( + "res.company", + string="Company", + required=True, + default=lambda self: self.env.user.company_id, + ) + assigned_to = fields.Many2one( + comodel_name="res.users", + track_visibility="onchange", + default=lambda self: self.env.uid, + ) + requested_by = fields.Many2one( + comodel_name="res.users", + track_visibility="onchange", + default=lambda self: self.env.uid, + ) + in_warehouse_id = fields.Many2one( + comodel_name="stock.warehouse", + string="Inbound Warehouse", + required=True, + default=_default_warehouse_id, + ) + customer_to_supplier = fields.Boolean("The customer will send to the supplier") + supplier_to_customer = fields.Boolean("The supplier will send to the customer") + supplier_address_id = fields.Many2one( + comodel_name="res.partner", + string="Supplier Address", + help="Address of the supplier in case of Customer RMA operation " "dropship.", + ) + customer_address_id = fields.Many2one( + comodel_name="res.partner", + string="Customer Address", + help="Address of the customer in case of Supplier RMA operation " "dropship.", + ) + state = fields.Selection( + compute=_compute_state, + selection=[ + ("draft", "Draft"), + ("to_approve", "To Approve"), + ("approved", "Approved"), + ("done", "Done"), + ], + string="State", + default="draft", + store=True, + ) + + @api.constrains("partner_id", "rma_line_ids") + def _check_partner_id(self): + if self.rma_line_ids and self.partner_id != self.mapped( + "rma_line_ids.partner_id" + ): + raise UserError(_("Group partner and RMA's partner must be the same.")) + if len(self.mapped("rma_line_ids.partner_id")) > 1: + raise UserError(_("All grouped RMA's should have same partner.")) + + @api.model + def create(self, vals): + if self.env.context.get("supplier") or vals.get("type") == "supplier": + vals["name"] = self.env["ir.sequence"].next_by_code("rma.order.supplier") + else: + vals["name"] = self.env["ir.sequence"].next_by_code("rma.order.customer") + return super(RmaOrder, self).create(vals) + + def action_view_in_shipments(self): + action = self.env.ref("stock.action_picking_tree_all") + result = action.read()[0] + picking_ids = [] + for line in self.rma_line_ids: + for move in line.move_ids: + if move.location_dest_id.usage == "internal": + picking_ids.append(move.picking_id.id) + else: + if line.customer_to_supplier: + picking_ids.append(move.picking_id.id) + if picking_ids: + shipments = list(set(picking_ids)) + # choose the view_mode accordingly + if len(shipments) > 1: + result["domain"] = [("id", "in", shipments)] + else: + res = self.env.ref("stock.view_picking_form", False) + result["views"] = [(res and res.id or False, "form")] + result["res_id"] = shipments[0] + return result + + def action_view_out_shipments(self): + action = self.env.ref("stock.action_picking_tree_all") + result = action.read()[0] + picking_ids = [] + for line in self.rma_line_ids: + for move in line.move_ids: + if move.location_dest_id.usage in ("supplier", "customer"): + if not line.customer_to_supplier: + picking_ids.append(move.picking_id.id) + if picking_ids: + shipments = list(set(picking_ids)) + # choose the view_mode accordingly + if len(shipments) != 1: + result["domain"] = [("id", "in", shipments)] + else: + res = self.env.ref("stock.view_picking_form", False) + result["views"] = [(res and res.id or False, "form")] + result["res_id"] = shipments[0] + return result + + def _get_valid_lines(self): + """:return: A recordset of rma lines. + """ + self.ensure_one() + return self.rma_line_ids + + def action_view_lines(self): + if self.type == "customer": + action = self.env.ref("rma.action_rma_customer_lines") + res = self.env.ref("rma.view_rma_line_form", False) + else: + action = self.env.ref("rma.action_rma_supplier_lines") + res = self.env.ref("rma.view_rma_line_supplier_form", False) + result = action.read()[0] + lines = self._get_valid_lines() + # choose the view_mode accordingly + if len(lines.ids) != 1: + result["domain"] = [("id", "in", lines.ids)] + else: + result["views"] = [(res and res.id or False, "form")] + result["res_id"] = lines.id + result["context"] = {} + return result + + def action_view_supplier_lines(self): + action = self.env.ref("rma.action_rma_supplier_lines") + result = action.read()[0] + lines = self.rma_line_ids + for line_id in lines: + related_lines = [line.id for line in line_id.supplier_rma_line_ids] + # choose the view_mode accordingly + if len(related_lines) != 1: + result["domain"] = [("id", "in", related_lines)] + else: + res = self.env.ref("rma.view_rma_line_supplier_form", False) + result["views"] = [(res and res.id or False, "form")] + result["res_id"] = related_lines[0] + return result + + @api.onchange("in_warehouse_id") + def _onchange_in_warehouse_id(self): + if self.in_warehouse_id and self.rma_line_ids: + self.rma_line_ids.write( + { + "in_warehouse_id": self.in_warehouse_id.id, + "location_id": self.in_warehouse_id.lot_rma_id.id, + } + ) + + @api.onchange("customer_to_supplier", "supplier_address_id") + def _onchange_customer_to_supplier(self): + if self.type == "customer" and self.rma_line_ids: + self.rma_line_ids.write( + { + "customer_to_supplier": self.customer_to_supplier, + "supplier_address_id": self.supplier_address_id.id, + } + ) + + @api.onchange("supplier_to_customer", "customer_address_id") + def _onchange_supplier_to_customer(self): + if self.type == "supplier" and self.rma_line_ids: + self.rma_line_ids.write( + { + "supplier_to_customer": self.supplier_to_customer, + "customer_address_id": self.customer_address_id.id, + } + ) diff --git a/rma/models/rma_order_line.py b/rma/models/rma_order_line.py new file mode 100644 index 00000000..1cc7753d --- /dev/null +++ b/rma/models/rma_order_line.py @@ -0,0 +1,724 @@ +# Copyright (C) 2017-20 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +import operator + +from odoo import _, api, fields, models +from odoo.exceptions import UserError, ValidationError + +ops = {"=": operator.eq, "!=": operator.ne} + + +class RmaOrderLine(models.Model): + _name = "rma.order.line" + _description = "RMA" + _inherit = ["mail.thread"] + + @api.model + def _get_default_type(self): + if "supplier" in self.env.context: + return "supplier" + return "customer" + + @api.model + def _default_warehouse_id(self): + rma_id = self.env.context.get("default_rma_id", False) + warehouse = self.env["stock.warehouse"] + if rma_id: + rma = self.env["rma.order"].browse(rma_id) + warehouse = self.env["stock.warehouse"].search( + [("company_id", "=", rma.company_id.id)], limit=1 + ) + return warehouse + + @api.model + def _default_location_id(self): + wh = self._default_warehouse_id() + return wh.lot_rma_id + + @api.onchange("partner_id") + def _onchange_delivery_address(self): + self.delivery_address_id = self.env["res.partner"].browse( + self.partner_id.address_get(["delivery"])["delivery"] + ) + + def _compute_in_shipment_count(self): + for line in self: + picking_ids = [] + for move in line.move_ids: + if move.location_dest_id.usage == "internal": + picking_ids.append(move.picking_id.id) + else: + if line.customer_to_supplier: + picking_ids.append(move.picking_id.id) + shipments = list(set(picking_ids)) + line.in_shipment_count = len(shipments) + + def _compute_out_shipment_count(self): + picking_ids = [] + for line in self: + for move in line.move_ids: + if move.location_dest_id.usage in ("supplier", "customer"): + if not line.customer_to_supplier: + picking_ids.append(move.picking_id.id) + shipments = list(set(picking_ids)) + line.out_shipment_count = len(shipments) + + def _get_rma_move_qty(self, states, direction="in"): + for rec in self: + product_obj = self.env["uom.uom"] + qty = 0.0 + if direction == "in": + op = ops["="] + else: + op = ops["!="] + for move in rec.move_ids.filtered( + lambda m: m.state in states and op(m.location_id.usage, rec.type) + ): + qty += product_obj._compute_quantity(move.product_uom_qty, rec.uom_id) + return qty + + @api.depends( + "move_ids", + "move_ids.state", + "qty_received", + "receipt_policy", + "product_qty", + "type", + ) + def _compute_qty_to_receive(self): + for rec in self: + rec.qty_to_receive = 0.0 + if rec.receipt_policy == "ordered": + rec.qty_to_receive = rec.product_qty - rec.qty_received + elif rec.receipt_policy == "delivered": + rec.qty_to_receive = rec.qty_delivered - rec.qty_received + + @api.depends( + "move_ids", + "move_ids.state", + "delivery_policy", + "product_qty", + "type", + "qty_delivered", + "qty_received", + ) + def _compute_qty_to_deliver(self): + for rec in self: + rec.qty_to_deliver = 0.0 + if rec.delivery_policy == "ordered": + rec.qty_to_deliver = rec.product_qty - rec.qty_delivered + elif rec.delivery_policy == "received": + rec.qty_to_deliver = rec.qty_received - rec.qty_delivered + + @api.depends("move_ids", "move_ids.state", "type") + def _compute_qty_incoming(self): + for rec in self: + qty = rec._get_rma_move_qty( + ("draft", "confirmed", "assigned"), direction="in" + ) + rec.qty_incoming = qty + + @api.depends("move_ids", "move_ids.state", "type") + def _compute_qty_received(self): + for rec in self: + qty = rec._get_rma_move_qty("done", direction="in") + rec.qty_received = qty + + @api.depends("move_ids", "move_ids.state", "type") + def _compute_qty_outgoing(self): + for rec in self: + qty = rec._get_rma_move_qty( + ("draft", "confirmed", "assigned"), direction="out" + ) + rec.qty_outgoing = qty + + @api.depends("move_ids", "move_ids.state", "type") + def _compute_qty_delivered(self): + for rec in self: + qty = rec._get_rma_move_qty("done", direction="out") + rec.qty_delivered = qty + + @api.model + def _get_supplier_rma_qty(self): + return sum( + self.supplier_rma_line_ids.filtered(lambda r: r.state != "cancel").mapped( + "product_qty" + ) + ) + + @api.depends( + "customer_to_supplier", + "supplier_rma_line_ids", + "move_ids", + "move_ids.state", + "qty_received", + "receipt_policy", + "product_qty", + "type", + ) + def _compute_qty_supplier_rma(self): + for rec in self: + if rec.customer_to_supplier: + supplier_rma_qty = rec._get_supplier_rma_qty() + rec.qty_to_supplier_rma = rec.product_qty - supplier_rma_qty + rec.qty_in_supplier_rma = supplier_rma_qty + else: + rec.qty_to_supplier_rma = 0.0 + rec.qty_in_supplier_rma = 0.0 + + def _compute_rma_line_count(self): + for rec in self.filtered(lambda r: r.type == "customer"): + rec.rma_line_count = len(rec.supplier_rma_line_ids) + for rec in self.filtered(lambda r: r.type == "supplier"): + rec.rma_line_count = len(rec.customer_rma_id) + + delivery_address_id = fields.Many2one( + comodel_name="res.partner", + string="Partner delivery address", + readonly=True, + states={"draft": [("readonly", False)]}, + help="This address will be used to deliver repaired or replacement " + "products.", + ) + rma_id = fields.Many2one( + comodel_name="rma.order", + string="RMA Group", + track_visibility="onchange", + readonly=True, + ) + name = fields.Char( + string="Reference", + required=True, + default="/", + readonly=True, + states={"draft": [("readonly", False)]}, + help="Add here the supplier RMA #. Otherwise an internal code is" " assigned.", + copy=False, + ) + description = fields.Text(string="Description") + conditions = fields.Html(string="Terms and conditions") + origin = fields.Char( + string="Source Document", + readonly=True, + states={"draft": [("readonly", False)]}, + help="Reference of the document that produced this rma.", + ) + state = fields.Selection( + selection=[ + ("draft", "Draft"), + ("to_approve", "To Approve"), + ("approved", "Approved"), + ("done", "Done"), + ], + string="State", + default="draft", + track_visibility="onchange", + ) + operation_id = fields.Many2one( + comodel_name="rma.operation", + string="Operation", + readonly=True, + states={"draft": [("readonly", False)]}, + ) + assigned_to = fields.Many2one( + comodel_name="res.users", + track_visibility="onchange", + default=lambda self: self.env.uid, + ) + requested_by = fields.Many2one( + comodel_name="res.users", + track_visibility="onchange", + default=lambda self: self.env.uid, + ) + partner_id = fields.Many2one( + comodel_name="res.partner", + required=True, + store=True, + track_visibility="onchange", + string="Partner", + readonly=True, + states={"draft": [("readonly", False)]}, + ) + sequence = fields.Integer( + default=10, help="Gives the sequence of this line when displaying the rma." + ) + product_id = fields.Many2one( + comodel_name="product.product", + string="Product", + ondelete="restrict", + required=True, + readonly=True, + states={"draft": [("readonly", False)]}, + ) + product_tracking = fields.Selection(related="product_id.tracking") + lot_id = fields.Many2one( + comodel_name="stock.production.lot", + string="Lot/Serial Number", + readonly=True, + states={"draft": [("readonly", False)]}, + ) + product_qty = fields.Float( + string="Return Qty", + copy=False, + default=1.0, + digits="Product Unit of Measure", + readonly=True, + states={"draft": [("readonly", False)]}, + ) + uom_id = fields.Many2one( + comodel_name="uom.uom", + string="Unit of Measure", + required=True, + readonly=True, + states={"draft": [("readonly", False)]}, + ) + price_unit = fields.Monetary( + string="Price Unit", readonly=True, states={"draft": [("readonly", False)]} + ) + in_shipment_count = fields.Integer( + compute="_compute_in_shipment_count", string="# of Shipments" + ) + out_shipment_count = fields.Integer( + compute="_compute_out_shipment_count", string="# of Deliveries" + ) + move_ids = fields.One2many( + "stock.move", "rma_line_id", string="Stock Moves", readonly=True, copy=False + ) + reference_move_id = fields.Many2one( + comodel_name="stock.move", + string="Originating Stock Move", + copy=False, + readonly=True, + states={"draft": [("readonly", False)]}, + ) + currency_id = fields.Many2one( + "res.currency", + string="Currency", + default=lambda self: self.env.user.company_id.currency_id, + ) + company_id = fields.Many2one( + comodel_name="res.company", + string="Company", + required=True, + default=lambda self: self.env.user.company_id, + ) + type = fields.Selection( + selection=[("customer", "Customer"), ("supplier", "Supplier")], + string="Type", + required=True, + default=lambda self: self._get_default_type(), + readonly=True, + ) + customer_to_supplier = fields.Boolean( + "The customer will send to the supplier", + readonly=True, + states={"draft": [("readonly", False)]}, + ) + supplier_to_customer = fields.Boolean( + "The supplier will send to the customer", + readonly=True, + states={"draft": [("readonly", False)]}, + ) + receipt_policy = fields.Selection( + [ + ("no", "Not required"), + ("ordered", "Based on Ordered Quantities"), + ("delivered", "Based on Delivered Quantities"), + ], + required=True, + string="Receipts Policy", + default="no", + readonly=True, + states={"draft": [("readonly", False)]}, + ) + delivery_policy = fields.Selection( + [ + ("no", "Not required"), + ("ordered", "Based on Ordered Quantities"), + ("received", "Based on Received Quantities"), + ], + required=True, + string="Delivery Policy", + default="no", + readonly=True, + states={"draft": [("readonly", False)]}, + ) + in_route_id = fields.Many2one( + "stock.location.route", + string="Inbound Route", + required=True, + domain=[("rma_selectable", "=", True)], + readonly=True, + states={"draft": [("readonly", False)]}, + ) + out_route_id = fields.Many2one( + "stock.location.route", + string="Outbound Route", + required=True, + domain=[("rma_selectable", "=", True)], + readonly=True, + states={"draft": [("readonly", False)]}, + ) + in_warehouse_id = fields.Many2one( + comodel_name="stock.warehouse", + string="Inbound Warehouse", + required=True, + readonly=True, + states={"draft": [("readonly", False)]}, + default=lambda self: self._default_warehouse_id(), + ) + out_warehouse_id = fields.Many2one( + comodel_name="stock.warehouse", + string="Outbound Warehouse", + required=True, + readonly=True, + states={"draft": [("readonly", False)]}, + default=lambda self: self._default_warehouse_id(), + ) + location_id = fields.Many2one( + comodel_name="stock.location", + string="Send To This Company Location", + required=True, + readonly=True, + states={"draft": [("readonly", False)]}, + default=lambda self: self._default_location_id(), + ) + customer_rma_id = fields.Many2one( + "rma.order.line", string="Customer RMA line", ondelete="cascade" + ) + supplier_rma_line_ids = fields.One2many("rma.order.line", "customer_rma_id") + rma_line_count = fields.Integer( + compute="_compute_rma_line_count", string="# of RMA lines associated" + ) + supplier_address_id = fields.Many2one( + comodel_name="res.partner", + readonly=True, + states={"draft": [("readonly", False)]}, + string="Supplier Address", + help="Address of the supplier in case of Customer RMA operation " "dropship.", + ) + customer_address_id = fields.Many2one( + comodel_name="res.partner", + readonly=True, + states={"draft": [("readonly", False)]}, + string="Customer Address", + help="Address of the customer in case of Supplier RMA operation " "dropship.", + ) + qty_to_receive = fields.Float( + string="Qty To Receive", + digits="Product Unit of Measure", + compute="_compute_qty_to_receive", + store=True, + ) + qty_incoming = fields.Float( + string="Incoming Qty", + copy=False, + readonly=True, + digits="Product Unit of Measure", + compute="_compute_qty_incoming", + store=True, + ) + qty_received = fields.Float( + string="Qty Received", + copy=False, + digits="Product Unit of Measure", + compute="_compute_qty_received", + store=True, + ) + qty_to_deliver = fields.Float( + string="Qty To Deliver", + copy=False, + digits="Product Unit of Measure", + readonly=True, + compute="_compute_qty_to_deliver", + store=True, + ) + qty_outgoing = fields.Float( + string="Outgoing Qty", + copy=False, + readonly=True, + digits="Product Unit of Measure", + compute="_compute_qty_outgoing", + store=True, + ) + qty_delivered = fields.Float( + string="Qty Delivered", + copy=False, + digits="Product Unit of Measure", + readonly=True, + compute="_compute_qty_delivered", + store=True, + ) + qty_to_supplier_rma = fields.Float( + string="Qty to send to Supplier RMA", + digits="Product Unit of Measure", + readonly=True, + compute="_compute_qty_supplier_rma", + store=True, + ) + qty_in_supplier_rma = fields.Float( + string="Qty in Supplier RMA", + digits="Product Unit of Measure", + readonly=True, + compute="_compute_qty_supplier_rma", + store=True, + ) + under_warranty = fields.Boolean( + string="Under Warranty?", readonly=True, states={"draft": [("readonly", False)]} + ) + + def _prepare_rma_line_from_stock_move(self, sm, lot=False): + if not self.type: + self.type = self._get_default_type() + if self.type == "customer": + operation = ( + sm.product_id.rma_customer_operation_id + or sm.product_id.categ_id.rma_customer_operation_id + ) + else: + operation = ( + sm.product_id.rma_supplier_operation_id + or sm.product_id.categ_id.rma_supplier_operation_id + ) + + if not operation: + operation = self.env["rma.operation"].search( + [("type", "=", self.type)], limit=1 + ) + if not operation: + raise ValidationError(_("Please define an operation first.")) + + if not operation.in_route_id or not operation.out_route_id: + route = self.env["stock.location.route"].search( + [("rma_selectable", "=", True)], limit=1 + ) + if not route: + raise ValidationError(_("Please define an RMA route.")) + + if not operation.in_warehouse_id or not operation.out_warehouse_id: + warehouse = self.env["stock.warehouse"].search( + [("company_id", "=", self.company_id.id), ("lot_rma_id", "!=", False)], + limit=1, + ) + if not warehouse: + raise ValidationError( + _("Please define a warehouse with a default RMA " "location.") + ) + + data = { + "product_id": sm.product_id.id, + "lot_id": lot and lot.id or False, + "origin": sm.picking_id.name or sm.name, + "uom_id": sm.product_uom.id, + "product_qty": sm.product_uom_qty, + "delivery_address_id": sm.picking_id.partner_id.id, + "operation_id": operation.id, + "receipt_policy": operation.receipt_policy, + "delivery_policy": operation.delivery_policy, + "in_warehouse_id": operation.in_warehouse_id.id or warehouse.id, + "out_warehouse_id": operation.out_warehouse_id.id or warehouse.id, + "in_route_id": operation.in_route_id.id or route.id, + "out_route_id": operation.out_route_id.id or route.id, + "location_id": ( + operation.location_id.id + or operation.in_warehouse_id.lot_rma_id.id + or warehouse.lot_rma_id.id + ), + } + return data + + @api.onchange("reference_move_id") + def _onchange_reference_move_id(self): + self.ensure_one() + sm = self.reference_move_id + if not sm: + return + if sm.move_line_ids.lot_id: + if len(sm.move_line_ids.lot_id) > 1: + raise UserError(_("To manage lots use RMA groups.")) + else: + data = self._prepare_rma_line_from_stock_move( + sm, lot=sm.move_line_ids.lot_id[0] + ) + self.update(data) + else: + data = self._prepare_rma_line_from_stock_move(sm, lot=False) + self.update(data) + self._remove_other_data_origin("reference_move_id") + + @api.constrains("reference_move_id", "partner_id") + def _check_move_partner(self): + for rec in self: + if ( + rec.reference_move_id + and rec.reference_move_id.picking_id.partner_id != rec.partner_id + ): + raise ValidationError( + _( + "RMA customer and originating stock move customer " + "doesn't match." + ) + ) + + def _remove_other_data_origin(self, exception): + if not exception == "reference_move_id": + self.reference_move_id = False + return True + + def action_rma_to_approve(self): + self.write({"state": "to_approve"}) + for rec in self: + if rec.product_id.rma_approval_policy == "one_step": + rec.action_rma_approve() + return True + + def action_rma_draft(self): + if self.in_shipment_count or self.out_shipment_count: + raise UserError(_("You cannot reset to draft a RMA with related pickings.")) + self.write({"state": "draft"}) + return True + + def action_rma_approve(self): + self.write({"state": "approved"}) + return True + + def action_rma_done(self): + self.write({"state": "done"}) + return True + + @api.model + def create(self, vals): + if not vals.get("name") or vals.get("name") == "/": + if self.env.context.get("supplier"): + vals["name"] = self.env["ir.sequence"].next_by_code( + "rma.order.line.supplier" + ) + else: + vals["name"] = self.env["ir.sequence"].next_by_code( + "rma.order.line.customer" + ) + return super(RmaOrderLine, self).create(vals) + + @api.onchange("product_id") + def _onchange_product_id(self): + result = {} + if not self.product_id: + return result + self.uom_id = self.product_id.uom_id.id + self.price_unit = self.product_id.standard_price + if not self.type: + self.type = self._get_default_type() + if self.type == "customer": + self.operation_id = ( + self.product_id.rma_customer_operation_id + or self.product_id.categ_id.rma_customer_operation_id + ) + else: + self.operation_id = ( + self.product_id.rma_supplier_operation_id + or self.product_id.categ_id.rma_supplier_operation_id + ) + if self.lot_id.product_id != self.product_id: + self.lot_id = False + return {"domain": {"lot_id": [("product_id", "=", self.product_id.id)]}} + + @api.onchange("operation_id") + def _onchange_operation_id(self): + result = {} + if not self.operation_id: + return result + self.receipt_policy = self.operation_id.receipt_policy + self.delivery_policy = self.operation_id.delivery_policy + self.in_warehouse_id = self.operation_id.in_warehouse_id + self.out_warehouse_id = self.operation_id.out_warehouse_id + self.location_id = ( + self.operation_id.location_id or self.in_warehouse_id.lot_rma_id + ) + self.customer_to_supplier = self.operation_id.customer_to_supplier + self.supplier_to_customer = self.operation_id.supplier_to_customer + self.in_route_id = self.operation_id.in_route_id + self.out_route_id = self.operation_id.out_route_id + return result + + @api.onchange("customer_to_supplier", "type") + def _onchange_receipt_policy(self): + if self.type == "supplier" and self.customer_to_supplier: + self.receipt_policy = "no" + elif self.type == "customer" and self.supplier_to_customer: + self.delivery_policy = "no" + + @api.onchange("lot_id") + def _onchange_lot_id(self): + product = self.lot_id.product_id + if product: + self.product_id = product + self.uom_id = product.uom_id + + def action_view_in_shipments(self): + action = self.env.ref("stock.action_picking_tree_all") + result = action.read()[0] + picking_ids = [] + for line in self: + for move in line.move_ids: + if move.location_dest_id.usage == "internal": + picking_ids.append(move.picking_id.id) + else: + if line.customer_to_supplier: + picking_ids.append(move.picking_id.id) + + shipments = list(set(picking_ids)) + # choose the view_mode accordingly + if len(shipments) != 1: + result["domain"] = "[('id', 'in', " + str(shipments) + ")]" + elif len(shipments) == 1: + res = self.env.ref("stock.view_picking_form", False) + result["views"] = [(res and res.id or False, "form")] + result["res_id"] = shipments[0] + return result + + def action_view_out_shipments(self): + action = self.env.ref("stock.action_picking_tree_all") + result = action.read()[0] + picking_ids = [] + for line in self: + for move in line.move_ids: + if move.location_dest_id.usage in ("supplier", "customer"): + if not line.customer_to_supplier: + picking_ids.append(move.picking_id.id) + shipments = list(set(picking_ids)) + # choose the view_mode accordingly + if len(shipments) != 1: + result["domain"] = "[('id', 'in', " + str(shipments) + ")]" + elif len(shipments) == 1: + res = self.env.ref("stock.view_picking_form", False) + result["views"] = [(res and res.id or False, "form")] + result["res_id"] = shipments[0] + return result + + def action_view_rma_lines(self): + if self.type == "customer": + # from customer we link to supplier rma + action = self.env.ref("rma.action_rma_supplier_lines") + rma_lines = self.supplier_rma_line_ids.ids + res = self.env.ref("rma.view_rma_line_supplier_form", False) + else: + # from supplier we link to customer rma + action = self.env.ref("rma.action_rma_customer_lines") + rma_lines = self.customer_rma_id.ids + res = self.env.ref("rma.view_rma_line_form", False) + result = action.read()[0] + # choose the view_mode accordingly + if rma_lines and len(rma_lines) != 1: + result["domain"] = rma_lines.ids + elif len(rma_lines) == 1: + result["views"] = [(res and res.id or False, "form")] + result["res_id"] = rma_lines[0] + return result + + @api.constrains("partner_id", "rma_id") + def _check_partner_id(self): + if self.rma_id and self.partner_id != self.rma_id.partner_id: + raise ValidationError( + _("Group partner and RMA's partner must be the same.") + ) diff --git a/rma/models/stock.py b/rma/models/stock.py new file mode 100644 index 00000000..60ec2ebe --- /dev/null +++ b/rma/models/stock.py @@ -0,0 +1,46 @@ +# Copyright (C) 2017-20 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo import api, fields, models + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + def action_assign(self): + """When you try to bring back a product from a customer location, + it may happen that there is no quants available to perform the + picking.""" + res = super(StockPicking, self).action_assign() + for picking in self: + for move in picking.move_lines: + if ( + move.rma_line_id + and move.state == "confirmed" + and move.location_id.usage == "customer" + ): + move.force_assign() + return res + + +class StockMove(models.Model): + _inherit = "stock.move" + + rma_line_id = fields.Many2one( + "rma.order.line", string="RMA line", ondelete="restrict" + ) + + @api.model + def create(self, vals): + if vals.get("group_id"): + group = self.env["procurement.group"].browse(vals["group_id"]) + if group.rma_line_id: + vals["rma_line_id"] = group.rma_line_id.id + return super(StockMove, self).create(vals) + + def _action_assign(self): + res = super(StockMove, self)._action_assign() + for move in self: + if move.rma_line_id: + move.partner_id = move.rma_line_id.partner_id.id or False + return res diff --git a/rma/models/stock_warehouse.py b/rma/models/stock_warehouse.py new file mode 100644 index 00000000..6273c771 --- /dev/null +++ b/rma/models/stock_warehouse.py @@ -0,0 +1,269 @@ +# Copyright (C) 2017-20 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo import _, fields, models + + +class StockWarehouse(models.Model): + _inherit = "stock.warehouse" + + lot_rma_id = fields.Many2one( + comodel_name="stock.location", string="RMA Location" + ) # not readonly to have the possibility to edit location and + # propagate to rma rules (add a auto-update when writing this field?) + rma_cust_out_type_id = fields.Many2one( + comodel_name="stock.picking.type", string="RMA Customer out Type", readonly=True + ) + rma_sup_out_type_id = fields.Many2one( + comodel_name="stock.picking.type", string="RMA Supplier out Type", readonly=True + ) + rma_cust_in_type_id = fields.Many2one( + comodel_name="stock.picking.type", string="RMA Customer in Type", readonly=True + ) + rma_sup_in_type_id = fields.Many2one( + comodel_name="stock.picking.type", string="RMA Supplier in Type", readonly=True + ) + rma_in_this_wh = fields.Boolean( + string="RMA in this Warehouse", + help="If set, it will create RMA location, picking types and routes " + "for this warehouse.", + ) + rma_customer_in_pull_id = fields.Many2one( + comodel_name="stock.rule", string="RMA Customer In Rule" + ) + rma_customer_out_pull_id = fields.Many2one( + comodel_name="stock.rule", string="RMA Customer Out Rule" + ) + rma_supplier_in_pull_id = fields.Many2one( + comodel_name="stock.rule", string="RMA Supplier In Rule" + ) + rma_supplier_out_pull_id = fields.Many2one( + comodel_name="stock.rule", string="RMA Supplier Out Rule" + ) + + def _get_rma_types(self): + return [ + self.rma_cust_out_type_id, + self.rma_sup_out_type_id, + self.rma_cust_in_type_id, + self.rma_sup_in_type_id, + ] + + def _rma_types_available(self): + self.ensure_one() + rma_types = self._get_rma_types() + for r_type in rma_types: + if not r_type: + return False + return True + + def write(self, vals): + if "rma_in_this_wh" in vals: + if vals.get("rma_in_this_wh"): + for wh in self: + # RMA location: + if not wh.lot_rma_id: + wh.lot_rma_id = self.env["stock.location"].create( + { + "name": "RMA", + "usage": "internal", + "location_id": wh.lot_stock_id.id, + "company_id": wh.company_id.id, + } + ) + # RMA types + if not wh._rma_types_available(): + wh._create_rma_picking_types() + else: + for r_type in wh._get_rma_types(): + if r_type: + r_type.active = True + # RMA rules: + wh._create_or_update_rma_pull() + else: + for wh in self: + for r_type in wh._get_rma_types(): + if r_type: + r_type.active = False + # Unlink rules: + self.mapped("rma_customer_in_pull_id").unlink() + self.mapped("rma_customer_out_pull_id").unlink() + self.mapped("rma_supplier_in_pull_id").unlink() + self.mapped("rma_supplier_out_pull_id").unlink() + return super(StockWarehouse, self).write(vals) + + def _create_rma_picking_types(self): + picking_type_obj = self.env["stock.picking.type"] + customer_loc, supplier_loc = self._get_partner_locations() + for wh in self: + other_pick_type = picking_type_obj.search( + [("warehouse_id", "=", wh.id)], order="sequence desc", limit=1 + ) + color = other_pick_type.color if other_pick_type else 0 + max_sequence = other_pick_type and other_pick_type.sequence or 0 + # create rma_cust_out_type_id: + rma_cust_out_type_id = picking_type_obj.create( + { + "name": _("Customer RMA Deliveries"), + "warehouse_id": wh.id, + "code": "outgoing", + "use_create_lots": True, + "use_existing_lots": False, + "sequence_id": self.env.ref("rma.seq_picking_type_rma_cust_out").id, + "default_location_src_id": wh.lot_rma_id.id, + "default_location_dest_id": customer_loc.id, + "sequence": max_sequence, + "color": color, + "sequence_code": "RMA → Customer", + } + ) + # create rma_sup_out_type_id: + rma_sup_out_type_id = picking_type_obj.create( + { + "name": _("Supplier RMA Deliveries"), + "warehouse_id": wh.id, + "code": "outgoing", + "use_create_lots": True, + "use_existing_lots": False, + "sequence_id": self.env.ref("rma.seq_picking_type_rma_sup_out").id, + "default_location_src_id": wh.lot_rma_id.id, + "default_location_dest_id": supplier_loc.id, + "sequence": max_sequence, + "color": color, + "sequence_code": "Customer → RMA", + } + ) + # create rma_cust_in_type_id: + rma_cust_in_type_id = picking_type_obj.create( + { + "name": _("Customer RMA Receipts"), + "warehouse_id": wh.id, + "code": "incoming", + "use_create_lots": True, + "use_existing_lots": False, + "sequence_id": self.env.ref("rma.seq_picking_type_rma_cust_in").id, + "default_location_src_id": customer_loc.id, + "default_location_dest_id": wh.lot_rma_id.id, + "sequence": max_sequence, + "color": color, + "sequence_code": "RMA -> Supplier", + } + ) + # create rma_sup_in_type_id: + rma_sup_in_type_id = picking_type_obj.create( + { + "name": _("Supplier RMA Receipts"), + "warehouse_id": wh.id, + "code": "incoming", + "use_create_lots": True, + "use_existing_lots": False, + "sequence_id": self.env.ref("rma.seq_picking_type_rma_sup_in").id, + "default_location_src_id": supplier_loc.id, + "default_location_dest_id": wh.lot_rma_id.id, + "sequence": max_sequence, + "color": color, + "sequence_code": "Supplier -> RMA", + } + ) + wh.write( + { + "rma_cust_out_type_id": rma_cust_out_type_id.id, + "rma_sup_out_type_id": rma_sup_out_type_id.id, + "rma_cust_in_type_id": rma_cust_in_type_id.id, + "rma_sup_in_type_id": rma_sup_in_type_id.id, + } + ) + return True + + def get_rma_rules_dict(self): + self.ensure_one() + rma_rules = dict() + customer_loc, supplier_loc = self._get_partner_locations() + rma_rules["rma_customer_in"] = { + "name": self._format_rulename(self, customer_loc, self.lot_rma_id.name), + "action": "pull", + "warehouse_id": self.id, + "company_id": self.company_id.id, + "location_src_id": customer_loc.id, + "location_id": self.lot_rma_id.id, + "procure_method": "make_to_stock", + "route_id": self.env.ref("rma.route_rma_customer").id, + "picking_type_id": self.rma_cust_in_type_id.id, + "active": True, + } + rma_rules["rma_customer_out"] = { + "name": self._format_rulename(self, self.lot_rma_id, customer_loc.name), + "action": "pull", + "warehouse_id": self.id, + "company_id": self.company_id.id, + "location_src_id": self.lot_rma_id.id, + "location_id": customer_loc.id, + "procure_method": "make_to_stock", + "route_id": self.env.ref("rma.route_rma_customer").id, + "picking_type_id": self.rma_cust_out_type_id.id, + "active": True, + } + rma_rules["rma_supplier_in"] = { + "name": self._format_rulename(self, supplier_loc, self.lot_rma_id.name), + "action": "pull", + "warehouse_id": self.id, + "company_id": self.company_id.id, + "location_src_id": supplier_loc.id, + "location_id": self.lot_rma_id.id, + "procure_method": "make_to_stock", + "route_id": self.env.ref("rma.route_rma_supplier").id, + "picking_type_id": self.rma_sup_in_type_id.id, + "active": True, + } + rma_rules["rma_supplier_out"] = { + "name": self._format_rulename(self, self.lot_rma_id, supplier_loc.name), + "action": "pull", + "warehouse_id": self.id, + "company_id": self.company_id.id, + "location_src_id": self.lot_rma_id.id, + "location_id": supplier_loc.id, + "procure_method": "make_to_stock", + "route_id": self.env.ref("rma.route_rma_supplier").id, + "picking_type_id": self.rma_sup_out_type_id.id, + "active": True, + } + return rma_rules + + def _create_or_update_rma_pull(self): + rule_obj = self.env["stock.rule"] + for wh in self: + rules_dict = wh.get_rma_rules_dict() + if wh.rma_customer_in_pull_id: + wh.rma_customer_in_pull_id.write(rules_dict["rma_customer_in"]) + else: + wh.rma_customer_in_pull_id = rule_obj.create( + rules_dict["rma_customer_in"] + ) + + if wh.rma_customer_out_pull_id: + wh.rma_customer_out_pull_id.write(rules_dict["rma_customer_out"]) + else: + wh.rma_customer_out_pull_id = rule_obj.create( + rules_dict["rma_customer_out"] + ) + + if wh.rma_supplier_in_pull_id: + wh.rma_supplier_in_pull_id.write(rules_dict["rma_supplier_in"]) + else: + wh.rma_supplier_in_pull_id = rule_obj.create( + rules_dict["rma_supplier_in"] + ) + + if wh.rma_supplier_out_pull_id: + wh.rma_supplier_out_pull_id.write(rules_dict["rma_supplier_out"]) + else: + wh.rma_supplier_out_pull_id = rule_obj.create( + rules_dict["rma_supplier_out"] + ) + return True + + +class StockLocationRoute(models.Model): + _inherit = "stock.location.route" + + rma_selectable = fields.Boolean(string="Selectable on RMA Lines") diff --git a/rma/readme/CONFIGURE.rst b/rma/readme/CONFIGURE.rst new file mode 100644 index 00000000..740b1e14 --- /dev/null +++ b/rma/readme/CONFIGURE.rst @@ -0,0 +1,41 @@ +Security +-------- + +Go to Settings > Users and assign the appropiate permissions to users. +Different security groups grant distinct levels of access to the RMA features. + +* Users in group "RMA Customer User" or "RMA Supplier User" can access to, + create and process RMA's associated to customers or suppliers respectively. + +* Users in group "RMA Manager" can access to, create, approve and process RMA's + associated to both customers and suppliers. + +RMA Approval Policy +------------------- + +There are two RMA approval policies in product catogories: + +* One step: Always auto-approve RMAs that only contain products within + categories with this policy. +* Two steps: A RMA order containing a product within a category with this + policy will request the RMA manager approval. + +In order to change the approval policy of a product category follow the next +steps: + +#. Go to *Inventory > Configuration > Products > Product Categories*. +#. Select one and change the field *RMA Approval Policy* to your convenience. + +Other Settings +-------------- + +#. Go to RMA > Configuration > Settings > Return Merchandising + Authorization and select the option "Display 3 fields on rma: partner, + invoice address, delivery address" if needed. +#. Go to RMA > Configuration > Warehouse management > Warehouses and add + a default RMA location and RMA picking type for customers and suppliers RMA + picking type. In case the warehouse is configured to use routes, you need to + create at least one route per rma type with at least two push rules (one for + inbound another for outbound) it's very important to select the type of + operation supplier if we are moving in the company and customer if we are + moving out of the company. diff --git a/rma/readme/CONTRIBUTORS.rst b/rma/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..61bac6ba --- /dev/null +++ b/rma/readme/CONTRIBUTORS.rst @@ -0,0 +1,8 @@ +* Jordi Ballester Alomar +* Aaron Henriquez +* Lois Rilo +* Bhavesh Odedra +* Akim Juillerat +* Alexandre Fayolle +* Chafique Delli +* Héctor Villarreal diff --git a/rma/readme/DESCRIPTION.rst b/rma/readme/DESCRIPTION.rst new file mode 100644 index 00000000..6ca0172a --- /dev/null +++ b/rma/readme/DESCRIPTION.rst @@ -0,0 +1,18 @@ +A Return Merchandise Authorization (RMA), is a part of the process of +returning a product in order to receive a refund, replacement, or repair +during the product's warranty period. + +The purchaser of the product must contact the manufacturer (or distributor +or retailer) to obtain authorization to return the product. + +The resulting RMA number must be displayed on or included in the returned +product's packaging. + +The issuance of an RMA is a key gatekeeping moment in the reverse logistics +cycle, providing the vendor with a final opportunity to diagnose and correct +the customer's problem with the product (such as improper installation or +configuration) before the customer permanently relinquishes ownership +of the product to the manufacturer, commonly referred to as a return. + +As returns are costly for the vendor and inconvenient for the customer, +any return that can be prevented benefits both parties. diff --git a/rma/readme/ROADMAP.rst b/rma/readme/ROADMAP.rst new file mode 100644 index 00000000..ca6afe09 --- /dev/null +++ b/rma/readme/ROADMAP.rst @@ -0,0 +1,6 @@ +* Picking operations report in customer RMA dropshipping case is showing + "Vendor Address" while it should be "Customer Address". +* Dropshipping always counted as a delivery on the smart buttons. +* Uninstall hook. +* Constraints instead of required fields on rma.order.line. +* Rename type field on rma.order and rma.order.line diff --git a/rma/readme/USAGE.rst b/rma/readme/USAGE.rst new file mode 100644 index 00000000..b0ee9ff4 --- /dev/null +++ b/rma/readme/USAGE.rst @@ -0,0 +1,12 @@ +RMA are accessible though Inventory menu. There's four menus, divided by type. +Users can access to the list of RMA or RMA lines. + +Create an RMA: + +#. Select a partner. Enter RMA lines associated to an existing picking, or + manually. +#. Request approval and approve. +#. Click on RMA Lines button. +#. Click on more and select an option: "Receive products", "Create Delivery + Order". +#. Go back to the RMA. Set the RMA to done if not further action is required. diff --git a/rma/report/rma_report.xml b/rma/report/rma_report.xml new file mode 100644 index 00000000..b2222392 --- /dev/null +++ b/rma/report/rma_report.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/rma/report/rma_report_templates.xml b/rma/report/rma_report_templates.xml new file mode 100644 index 00000000..39617ee4 --- /dev/null +++ b/rma/report/rma_report_templates.xml @@ -0,0 +1,200 @@ + + + + + + + + + + + + diff --git a/rma/security/ir.model.access.csv b/rma/security/ir.model.access.csv new file mode 100644 index 00000000..e899b9c9 --- /dev/null +++ b/rma/security/ir.model.access.csv @@ -0,0 +1,11 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_rma_user_customer,rma.order.customer.user,model_rma_order,group_rma_customer_user,1,1,1,1 +access_rma_user_supplier,rma.order.supplier.user,model_rma_order,group_rma_supplier_user,1,1,1,1 +access_rma_manager,rma.order.manager,model_rma_order,group_rma_manager,1,1,1,1 +access_rma_line_customer_user,rma.order.line.customer.user,model_rma_order_line,group_rma_customer_user,1,1,1,1 +access_rma_line_supplier_user,rma.order.line.customer.supplier,model_rma_order_line,group_rma_supplier_user,1,1,1,1 +access_rma_line_manager,rma.order.line,model_rma_order_line,group_rma_manager,1,1,1,1 +access_rma_operation_manager,access_rma_operation,model_rma_operation,group_rma_manager,1,1,1,1 +access_rma_operation_customer_user,access_rma_operation,model_rma_operation,group_rma_customer_user,1,0,0,0 +access_rma_operation_supplier_user,access_rma_operation,model_rma_operation,group_rma_supplier_user,1,0,0,0 +access_rma_order_line_user,access_rma_order_line,model_rma_order_line,base.group_user,1,0,0,0 diff --git a/rma/security/rma.xml b/rma/security/rma.xml new file mode 100644 index 00000000..d0ea0ffc --- /dev/null +++ b/rma/security/rma.xml @@ -0,0 +1,73 @@ + + + + + + RMA + + 30 + + + + RMA Customer User + + + + + + RMA Supplier User + + + + + + RMA Manager + + + + + + + Addresses in RMA + + + + + + RMA Groups + + + + + + + + + + + + + rma order multi-company + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + + rma order line multi-company + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + + rma operation multi-company + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + + + diff --git a/rma/static/description/icon.png b/rma/static/description/icon.png new file mode 100644 index 00000000..ce758c20 Binary files /dev/null and b/rma/static/description/icon.png differ diff --git a/rma/tests/__init__.py b/rma/tests/__init__.py new file mode 100644 index 00000000..e485a52f --- /dev/null +++ b/rma/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2017-20 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from . import test_rma diff --git a/rma/tests/test_rma.py b/rma/tests/test_rma.py new file mode 100644 index 00000000..d85b670b --- /dev/null +++ b/rma/tests/test_rma.py @@ -0,0 +1,794 @@ +# © 2017 ForgeFlow +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo.exceptions import ValidationError +from odoo.tests import common + + +class TestRma(common.SavepointCase): + """ Test the routes and the quantities """ + + @classmethod + def setUpClass(cls): + super(TestRma, cls).setUpClass() + + 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.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.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") + cls.category = cls._create_product_category( + "one_step", cls.rma_cust_replace_op_id, cls.rma_sup_replace_op_id + ) + cls.product_id = cls._create_product("PT0") + cls.product_1 = cls._create_product("PT1") + 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.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.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") + # Customer RMA: + products2move = [(cls.product_1, 3), (cls.product_2, 5), (cls.product_3, 2)] + cls.rma_customer_id = cls._create_rma_from_move( + products2move, "customer", cls.env.ref("base.res_partner_2"), dropship=False + ) + # Dropship: + cls.rma_droship_id = cls._create_rma_from_move( + products2move, + "customer", + cls.env.ref("base.res_partner_2"), + dropship=True, + supplier_address_id=cls.env.ref("base.res_partner_3"), + ) + # Supplier RMA: + cls.rma_supplier_id = cls._create_rma_from_move( + products2move, "supplier", cls.env.ref("base.res_partner_2"), dropship=False + ) + + @classmethod + def _create_product_category( + cls, rma_approval_policy, rma_customer_operation_id, rma_supplier_operation_id + ): + return cls.env["product.category"].create( + { + "name": "Test category", + "rma_approval_policy": rma_approval_policy, + "rma_customer_operation_id": rma_customer_operation_id.id, + "rma_supplier_operation_id": rma_supplier_operation_id.id, + } + ) + + @classmethod + def _create_product(cls, name): + return cls.product_product_model.create( + {"name": name, "categ_id": cls.category.id, "type": "product"} + ) + + @classmethod + def _create_picking(cls, partner): + return cls.stockpicking.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 _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": + 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)) + else: + for item in products2move: + move_values = cls._prepare_move( + item[0], + item[1], + cls.supplier_location, + cls.stock_rma_location, + picking_in, + ) + moves.append(cls.env["stock.move"].create(move_values)) + # Create the RMA from the stock_move + rma_id = cls.rma.create( + { + "reference": "0001", + "type": r_type, + "partner_id": partner.id, + "company_id": cls.env.ref("base.main_company").id, + } + ) + for move in moves: + if r_type == "customer": + wizard = cls.rma_add_stock_move.new( + { + "move_ids": [(4, move.id)], + "rma_id": rma_id.id, + "partner_id": move.partner_id.id, + } + ) + wizard.with_context( + { + "move_ids": [(4, move.id)], + "reference_move_id": move.id, + "customer": True, + "active_ids": rma_id.id, + "partner_id": move.partner_id.id, + "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) + + else: + wizard = cls.rma_add_stock_move.new( + { + "move_ids": [(4, move.id)], + "rma_id": rma_id.id, + "partner_id": move.partner_id.id, + } + ) + wizard.with_context( + { + "move_ids": [(4, move.id)], + "reference_move_id": move.id, + "active_ids": rma_id.id, + "partner_id": move.partner_id.id, + "active_model": "rma.order", + } + ).default_get([str(move.id), str(cls.partner_id.id)]) + data = wizard._prepare_rma_line_from_stock_move(move) + data["type"] = "supplier" + if dropship: + data.update( + customer_to_supplier=dropship, + 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._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 + def _prepare_move(cls, product, qty, src, dest, picking_in): + location_id = src.id + + return { + "name": product.name, + "partner_id": picking_in.partner_id.id, + "origin": picking_in.name, + "company_id": picking_in.picking_type_id.warehouse_id.company_id.id, + "product_id": product.id, + "product_uom": product.uom_id.id, + "state": "draft", + "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, + } + + def _check_equal_quantity(self, qty1, qty2, msg): + self.assertEquals(qty1, qty2, msg) + + def test_01_rma_order_line(self): + for line in self.rma_customer_id.rma_line_ids: + line.with_context( + {"default_rma_id": line.rma_id.id} + )._default_warehouse_id() + line._default_location_id() + line._onchange_delivery_address() + line._compute_in_shipment_count() + line._compute_out_shipment_count() + + # check assert if call reference_move_id onchange + self.assertEquals(line.product_id, line.reference_move_id.product_id) + self.assertEquals(line.product_qty, line.reference_move_id.product_uom_qty) + self.assertEquals( + line.location_id.location_id, line.reference_move_id.location_id + ) + self.assertEquals(line.origin, line.reference_move_id.picking_id.name) + self.assertEquals( + line.delivery_address_id, line.reference_move_id.picking_partner_id + ) + self.assertEquals( + line.qty_to_receive, line.reference_move_id.product_uom_qty + ) + line._onchange_product_id() + line._onchange_operation_id() + # check assert if call operation_id onchange + self.assertEquals(line.operation_id.receipt_policy, line.receipt_policy) + + data = {"customer_to_supplier": line.customer_to_supplier} + line = self.rma_line.new(data) + line._onchange_receipt_policy() + + data = {"lot_id": line.lot_id.id} + line = self.rma_line.new(data) + line._onchange_lot_id() + + line.action_view_in_shipments() + line.action_view_out_shipments() + self.rma_customer_id.action_view_supplier_lines() + with self.assertRaises(ValidationError): + line.rma_id.partner_id = self.partner_id.id + self.rma_customer_id.rma_line_ids[0].partner_id = self.env.ref( + "base.res_partner_3" + ).id + self.rma_customer_id.action_view_supplier_lines() + + def test_02_customer_rma(self): + self.rma_customer_id.rma_line_ids.action_rma_to_approve() + wizard = self.rma_make_picking.with_context( + { + "active_ids": self.rma_customer_id.rma_line_ids.ids, + "active_model": "rma.order.line", + "picking_type": "incoming", + "active_id": 1, + } + ).create({}) + wizard._create_picking() + res = self.rma_customer_id.rma_line_ids.action_view_in_shipments() + self.assertTrue("res_id" in res, "Incorrect number of pickings" "created") + picking = self.env["stock.picking"].browse(res["res_id"]) + self.assertEquals(len(picking), 1, "Incorrect number of pickings created") + moves = picking.move_lines + self.assertEquals(len(moves), 3, "Incorrect number of moves created") + lines = self.rma_customer_id.rma_line_ids + self.assertEquals( + list(set(lines.mapped("qty_received"))), [0], "Wrong qty received" + ) + self.assertEquals( + list(set(lines.mapped("qty_to_deliver"))), [0], "Wrong qty to deliver" + ) + self.assertEquals( + list(set(lines.mapped("qty_outgoing"))), [0], "Wrong qty outgoing" + ) + self.assertEquals( + list(set(lines.mapped("qty_delivered"))), [0], "Wrong qty delivered" + ) + # product specific + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_1).qty_to_receive, + 3, + "Wrong qty to receive", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_1).qty_incoming, + 3, + "Wrong qty incoming", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_2).qty_to_receive, + 5, + "Wrong qty to receive", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_2).qty_incoming, + 5, + "Wrong qty incoming", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_3).qty_to_receive, + 2, + "Wrong qty to receive", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_3).qty_incoming, + 2, + "Wrong qty incoming", + ) + + picking.action_assign() + for mv in picking.move_lines: + mv.quantity_done = mv.product_uom_qty + picking.action_done() + lines = self.rma_customer_id.rma_line_ids + self.assertEquals( + list(set(lines.mapped("qty_to_receive"))), [0], "Wrong qty to_receive" + ) + self.assertEquals( + list(set(lines.mapped("qty_incoming"))), [0], "Wrong qty incoming" + ) + self.assertEquals( + list(set(lines.mapped("qty_outgoing"))), [0], "Wrong qty outgoing" + ) + self.assertEquals( + list(set(lines.mapped("qty_delivered"))), [0], "Wrong qty delivered" + ) + # product specific + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_1).qty_received, + 3, + "Wrong qty received", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_1).qty_to_deliver, + 3, + "Wrong qty to_deliver", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_2).qty_received, + 5, + "Wrong qty received", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_2).qty_to_deliver, + 5, + "Wrong qty to_deliver", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_3).qty_received, + 2, + "Wrong qty received", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_3).qty_to_deliver, + 2, + "Wrong qty to_deliver", + ) + + wizard = self.rma_make_picking.with_context( + { + "active_id": 1, + "active_ids": self.rma_customer_id.rma_line_ids.ids, + "active_model": "rma.order.line", + "picking_type": "outgoing", + } + ).create({}) + wizard._create_picking() + res = self.rma_customer_id.rma_line_ids.action_view_out_shipments() + self.assertTrue("res_id" in res, "Incorrect number of pickings" "created") + picking = self.env["stock.picking"].browse(res["res_id"]) + moves = picking.move_lines + self.assertEquals(len(moves), 3, "Incorrect number of moves created") + lines = self.rma_customer_id.rma_line_ids + self.assertEquals( + list(set(lines.mapped("qty_to_receive"))), [0], "Wrong qty to_receive" + ) + self.assertEquals( + list(set(lines.mapped("qty_incoming"))), [0], "Wrong qty incoming" + ) + self.assertEquals( + list(set(lines.mapped("qty_delivered"))), [0], "Wrong qty delivered" + ) + + # product specific + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_1).qty_to_deliver, + 3, + "Wrong qty to_deliver", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_1).qty_outgoing, + 3, + "Wrong qty outgoing", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_2).qty_to_deliver, + 5, + "Wrong qty to_deliver", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_2).qty_outgoing, + 5, + "Wrong qty outgoing", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_3).qty_to_deliver, + 2, + "Wrong qty to_deliver", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_3).qty_outgoing, + 2, + "Wrong qty outgoing", + ) + + picking.action_assign() + for mv in picking.move_lines: + mv.quantity_done = mv.product_uom_qty + picking.action_done() + lines = self.rma_customer_id.rma_line_ids + self.assertEquals( + list(set(lines.mapped("qty_to_receive"))), [0], "Wrong qty to_receive" + ) + self.assertEquals( + list(set(lines.mapped("qty_incoming"))), [0], "Wrong qty incoming" + ) + self.assertEquals( + list(set(lines.mapped("qty_outgoing"))), [0], "Wrong qty_outgoing" + ) + + # product specific + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_1).qty_received, + 3, + "Wrong qty_received", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_1).qty_delivered, + 3, + "Wrong qty_delivered", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_2).qty_received, + 5, + "Wrong qty_received", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_2).qty_delivered, + 5, + "Wrong qty_delivered", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_3).qty_received, + 2, + "Wrong qty_received", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_3).qty_delivered, + 2, + "Wrong qty_delivered", + ) + + self.line.action_rma_done() + self.assertEquals(self.line.state, "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() + + # DROPSHIP + def test_03_dropship(self): + for line in self.rma_droship_id.rma_line_ids: + line.operation_id = self.rma_ds_replace_op_id + line._onchange_operation_id() + line._onchange_delivery_address() + line.action_rma_to_approve() + line.action_rma_approve() + wizard = self.rma_make_picking.with_context( + { + "active_id": 1, + "active_ids": self.rma_droship_id.rma_line_ids.ids, + "active_model": "rma.order.line", + "picking_type": "incoming", + } + ).create({}) + wizard._create_picking() + res = self.rma_droship_id.rma_line_ids.action_view_in_shipments() + self.assertTrue("res_id" in res, "Incorrect number of pickings" "created") + picking = self.env["stock.picking"].browse(res["res_id"]) + self.assertEquals(len(picking), 1, "Incorrect number of pickings created") + moves = picking.move_lines + self.assertEquals(len(moves), 3, "Incorrect number of moves created") + wizard = self.make_supplier_rma.with_context( + { + "active_ids": self.rma_droship_id.rma_line_ids.ids, + "active_model": "rma.order.line", + "active_id": 1, + } + ).create({}) + res = wizard.make_supplier_rma() + supplier_rma = self.rma.browse(res["res_id"]) + lines = supplier_rma.rma_line_ids + self.assertEquals( + list(set(lines.mapped("qty_received"))), [0], "Wrong qty_received" + ) + self.assertEquals( + list(set(lines.mapped("qty_outgoing"))), [0], "Wrong qty_outgoing" + ) + self.assertEquals( + list(set(lines.mapped("qty_delivered"))), [0], "qty_delivered" + ) + + # product specific + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_1).qty_to_deliver, + 3, + "Wrong qty_to_deliver", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_2).qty_to_deliver, + 5, + "Wrong qty_to_deliver", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_3).qty_to_deliver, + 2, + "Wrong qty_to_deliver", + ) + + lines = self.rma_droship_id.rma_line_ids + self._check_equal_quantity( + lines.filtered( + lambda l: l.product_id == self.product_1 + ).qty_in_supplier_rma, + 3, + "Wrong qty_in_supplier_rma", + ) + self._check_equal_quantity( + lines.filtered( + lambda l: l.product_id == self.product_2 + ).qty_in_supplier_rma, + 5, + "Wrong qty_in_supplier_rma", + ) + self._check_equal_quantity( + lines.filtered( + lambda l: l.product_id == self.product_3 + ).qty_in_supplier_rma, + 2, + "Wrong qty_in_supplier_rma", + ) + + self.assertEquals( + list(set(lines.mapped("qty_to_supplier_rma"))), + [0], + "Wrong qty_to_supplier_rma", + ) + + for line in self.rma_droship_id.rma_line_ids: + line.action_rma_done() + + self.assertEquals(line.mapped("state"), ["done"], "Wrong State") + + # Supplier RMA + def test_04_supplier_rma(self): + self.rma_supplier_id.rma_line_ids.action_rma_to_approve() + self.rma_supplier_id.rma_line_ids.operation_id = self.rma_sup_replace_op_id + self.rma_supplier_id.rma_line_ids._onchange_operation_id() + self.rma_supplier_id.rma_line_ids._onchange_delivery_address() + wizard = self.rma_make_picking.with_context( + { + "active_ids": self.rma_supplier_id.rma_line_ids.ids, + "active_model": "rma.order.line", + "picking_type": "outgoing", + "active_id": 2, + } + ).create({}) + wizard._create_picking() + res = self.rma_supplier_id.rma_line_ids.action_view_out_shipments() + self.assertTrue("res_id" in res, "Incorrect number of pickings" "created") + picking = self.env["stock.picking"].browse(res["res_id"]) + moves = picking.move_lines + self.assertEquals(len(moves), 3, "Incorrect number of moves created") + + lines = self.rma_supplier_id.rma_line_ids + self.assertEquals( + list(set(lines.mapped("qty_received"))), [0], "Wrong qty_received" + ) + self.assertEquals( + list(set(lines.mapped("qty_delivered"))), [0], "qty_delivered" + ) + + # product specific + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_1).qty_to_receive, + 3, + "Wrong qty_to_receive", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_1).qty_to_deliver, + 3, + "Wrong qty_to_deliver", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_2).qty_to_receive, + 5, + "Wrong qty_to_receive", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_2).qty_to_deliver, + 5, + "Wrong qty_to_deliver", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_3).qty_to_receive, + 2, + "Wrong qty_to_receive", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_3).qty_to_deliver, + 2, + "Wrong qty_to_deliver", + ) + self.assertEquals( + list(set(lines.mapped("qty_incoming"))), [0], "Wrong qty_incoming" + ) + picking.action_assign() + for mv in picking.move_lines: + mv.quantity_done = mv.product_uom_qty + picking.action_done() + + self.assertEquals( + list(set(lines.mapped("qty_incoming"))), [0], "Wrong qty_incoming" + ) + self.assertEquals( + list(set(lines.mapped("qty_to_deliver"))), [0], "Wrong qty_to_deliver" + ) + self.assertEquals( + list(set(lines.mapped("qty_received"))), [0], "Wrong qty_received" + ) + self.assertEquals(list(set(lines.mapped("qty_outgoing"))), [0], "qty_outgoing") + + # product specific + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_1).qty_delivered, + 3, + "Wrong qty_delivered", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_1).qty_to_receive, + 3, + "Wrong qty_to_receive", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_1).qty_received, + 0, + "Wrong qty_received", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_2).qty_delivered, + 5, + "Wrong qty_delivered", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_2).qty_received, + 0, + "Wrong qty_received", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_3).qty_delivered, + 2, + "Wrong qty_delivered", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_3).qty_received, + 0, + "Wrong qty_received", + ) + + wizard = self.rma_make_picking.with_context( + { + "active_id": 1, + "active_ids": self.rma_supplier_id.rma_line_ids.ids, + "active_model": "rma.order.line", + "picking_type": "incoming", + } + ).create({}) + wizard._create_picking() + res = self.rma_supplier_id.rma_line_ids.action_view_in_shipments() + self.assertTrue("res_id" in res, "Incorrect number of pickings" "created") + pickings = self.env["stock.picking"].browse(res["res_id"]) + self.assertEquals(len(pickings), 1, "Incorrect number of pickings created") + picking_in = pickings[0] + moves = picking_in.move_lines + self.assertEquals(len(moves), 3, "Incorrect number of moves created") + + lines = self.rma_supplier_id.rma_line_ids + self.assertEquals( + list(set(lines.mapped("qty_to_deliver"))), [0], "qty_to_deliver" + ) + + # product specific + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_1).qty_to_receive, + 3, + "Wrong qty_to_receive", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_2).qty_to_receive, + 5, + "Wrong qty_to_receive", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_3).qty_to_receive, + 2, + "Wrong qty_to_receive", + ) + + picking_in.action_confirm() + picking_in.action_assign() + for mv in picking_in.move_line_ids: + mv.qty_done = mv.product_uom_qty + picking_in.action_done() + self.assertEquals( + list(set(lines.mapped("qty_outgoing"))), [0], "Wrong qty_outgoing" + ) + self.assertEquals( + list(set(lines.mapped("qty_incoming"))), [0], "Wrong qty_incoming" + ) + self.assertEquals( + list(set(lines.mapped("qty_to_deliver"))), [0], "qty_to_deliver" + ) + + # product specific + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_1).qty_delivered, + 3, + "Wrong qty_delivered", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_1).qty_received, + 3, + "Wrong qty_received", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_2).qty_delivered, + 5, + "Wrong qty_delivered", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_2).qty_received, + 5, + "Wrong qty_received", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_3).qty_delivered, + 2, + "Wrong qty_delivered", + ) + self._check_equal_quantity( + lines.filtered(lambda l: l.product_id == self.product_3).qty_received, + 2, + "Wrong qty_received", + ) + for line in self.rma_supplier_id.rma_line_ids: + line.action_rma_done() + self.assertEquals(line.mapped("state"), ["done"], "Wrong State") diff --git a/rma/views/product_view.xml b/rma/views/product_view.xml new file mode 100644 index 00000000..ffb37dcd --- /dev/null +++ b/rma/views/product_view.xml @@ -0,0 +1,40 @@ + + + + product.category.form + product.category + + + + + + + + + + + + + + product.template.stock.property.form.inherit + product.template + + + + + + + + + + + + diff --git a/rma/views/res_config_settings_views.xml b/rma/views/res_config_settings_views.xml new file mode 100644 index 00000000..a749333a --- /dev/null +++ b/rma/views/res_config_settings_views.xml @@ -0,0 +1,52 @@ + + + + + + res.config.settings.view.form.inherit.rma + res.config.settings + + + + +
+

Return Merchandise Authorization

+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ + + Settings + ir.actions.act_window + res.config.settings + form + inline + {'module' : 'rma'} + + +
diff --git a/rma/views/res_partner_view.xml b/rma/views/res_partner_view.xml new file mode 100644 index 00000000..64ad3fa6 --- /dev/null +++ b/rma/views/res_partner_view.xml @@ -0,0 +1,23 @@ + + + + + res.partner.form - rma + res.partner + + +
+ +
+
+
+ +
diff --git a/rma/views/rma_menu.xml b/rma/views/rma_menu.xml new file mode 100644 index 00000000..b95f3e97 --- /dev/null +++ b/rma/views/rma_menu.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rma/views/rma_operation_view.xml b/rma/views/rma_operation_view.xml new file mode 100644 index 00000000..8b98317e --- /dev/null +++ b/rma/views/rma_operation_view.xml @@ -0,0 +1,81 @@ + + + + rma.operation.tree + rma.operation + + + + + + + + + + + + + rma.operation.form + rma.operation + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + Customer Operations + rma.operation + tree,form + {'default_type': "customer"} + [('type','=', 'customer')] + + + + + Supplier Operations + rma.operation + tree,form + {'default_type': "supplier"} + [('type','=', 'supplier')] + + + +
diff --git a/rma/views/rma_order_line_view.xml b/rma/views/rma_order_line_view.xml new file mode 100644 index 00000000..fc203ba5 --- /dev/null +++ b/rma/views/rma_order_line_view.xml @@ -0,0 +1,459 @@ + + + + + rma.order.line.tree + rma.order.line + + + + + + + + + + + + + + + + + + + + rma.order.line.supplier.tree + rma.order.line + + + + + + + + + + + + + + + + + + + rma.order.line.supplier.form + rma.order.line + +
+
+
+ +
+ + + +
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+
+ + + rma.order.line.form + rma.order.line + +
+
+
+ +
+ + + + + +
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+
+ + + rma.order.line.select + rma.order.line + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Customer RMA + rma.order.line + [('type','=', 'customer')] + {"search_default_assigned_to":uid} + tree,form + + + + + Supplier RMA + rma.order.line + [('type','=', 'supplier')] + {"search_default_assigned_to":uid, "supplier":1} + tree,form + + + + + + tree + + + + + + form + + + + +
+
diff --git a/rma/views/rma_order_view.xml b/rma/views/rma_order_view.xml new file mode 100644 index 00000000..982908fe --- /dev/null +++ b/rma/views/rma_order_view.xml @@ -0,0 +1,292 @@ + + + + rma.order.tree + rma.order + + + + + + + + + + + + rma.order.supplier.tree + rma.order + + + + + + + + + + + + rma.order.form + rma.order + +
+
+ +
+ + + + +
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + + + + + rma.order.supplier.form + rma.order + +
+
+ +
+ + + +
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + + + + + rma.order.select + rma.order + + + + + + + + + + + + + + + + Customer RMA Group + rma.order + [('type','=', 'customer')] + {'customer':1} + tree,form + + + + + Supplier RMA Group + rma.order + [('type','=', 'supplier')] + {'supplier':1} + tree,form + + + + + + tree + + + + + + form + + + + + diff --git a/rma/views/stock_view.xml b/rma/views/stock_view.xml new file mode 100644 index 00000000..e8d4655f --- /dev/null +++ b/rma/views/stock_view.xml @@ -0,0 +1,26 @@ + + + + rma.move.form + stock.move + + + + + + + + + + + + stock.location.route.form + + stock.location.route + + + + + + + diff --git a/rma/views/stock_warehouse.xml b/rma/views/stock_warehouse.xml new file mode 100644 index 00000000..617e2cab --- /dev/null +++ b/rma/views/stock_warehouse.xml @@ -0,0 +1,22 @@ + + + + view_warehouse_form + stock.warehouse + + + + + + + + + + + + + + + + + diff --git a/rma/wizards/__init__.py b/rma/wizards/__init__.py new file mode 100644 index 00000000..6d5e9355 --- /dev/null +++ b/rma/wizards/__init__.py @@ -0,0 +1,6 @@ +# Copyright (C) 2017-20 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from . import rma_add_stock_move +from . import rma_make_picking +from . import rma_order_line_make_supplier_rma diff --git a/rma/wizards/rma_add_stock_move.py b/rma/wizards/rma_add_stock_move.py new file mode 100644 index 00000000..8b1a3607 --- /dev/null +++ b/rma/wizards/rma_add_stock_move.py @@ -0,0 +1,125 @@ +# Copyright (C) 2017-20 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class RmaAddStockMove(models.TransientModel): + _name = "rma_add_stock_move" + _description = "Wizard to add rma lines from pickings" + + @api.model + def default_get(self, fields_list): + res = super(RmaAddStockMove, self).default_get(fields_list) + rma_obj = self.env["rma.order"] + rma_id = self.env.context["active_ids"] or [] + active_model = self.env.context["active_model"] + if not rma_id: + return res + assert active_model == "rma.order", "Bad context propagation" + + rma = rma_obj.browse(rma_id) + res["rma_id"] = rma.id + res["partner_id"] = rma.partner_id.id + res["move_ids"] = False + return res + + rma_id = fields.Many2one( + comodel_name="rma.order", string="RMA Order", readonly=True, ondelete="cascade" + ) + partner_id = fields.Many2one( + comodel_name="res.partner", string="Partner", readonly=True + ) + move_ids = fields.Many2many( + comodel_name="stock.move", + string="Stock Moves", + domain="[('state', '=', 'done')]", + ) + + def _prepare_rma_line_from_stock_move(self, sm, lot=False): + if self.env.context.get("customer"): + operation = ( + sm.product_id.rma_customer_operation_id + or sm.product_id.categ_id.rma_customer_operation_id + ) + else: + operation = ( + sm.product_id.rma_supplier_operation_id + or sm.product_id.categ_id.rma_supplier_operation_id + ) + if not operation: + operation = self.env["rma.operation"].search( + [("type", "=", self.rma_id.type)], limit=1 + ) + if not operation: + raise ValidationError(_("Please define an operation first")) + + if not operation.in_route_id or not operation.out_route_id: + route = self.env["stock.location.route"].search( + [("rma_selectable", "=", True)], limit=1 + ) + if not route: + raise ValidationError(_("Please define an RMA route")) + + if not operation.in_warehouse_id or not operation.out_warehouse_id: + warehouse = self.env["stock.warehouse"].search( + [ + ("company_id", "=", self.rma_id.company_id.id), + ("lot_rma_id", "!=", False), + ], + limit=1, + ) + if not warehouse: + raise ValidationError( + _("Please define a warehouse with a default RMA location") + ) + data = { + "partner_id": self.partner_id.id, + "reference_move_id": sm.id, + "product_id": sm.product_id.id, + "lot_id": lot and lot.id or False, + "origin": sm.picking_id.name or sm.name, + "uom_id": sm.product_uom.id, + "operation_id": operation.id, + "product_qty": sm.product_uom_qty, + "delivery_address_id": sm.picking_id.partner_id.id, + "rma_id": self.rma_id.id, + "receipt_policy": operation.receipt_policy, + "delivery_policy": operation.delivery_policy, + "in_warehouse_id": operation.in_warehouse_id.id or warehouse.id, + "out_warehouse_id": operation.out_warehouse_id.id or warehouse.id, + "in_route_id": operation.in_route_id.id or route.id, + "out_route_id": operation.out_route_id.id or route.id, + "location_id": ( + operation.location_id.id + or operation.in_warehouse_id.lot_rma_id.id + or warehouse.lot_rma_id.id + ), + } + return data + + @api.model + def _get_existing_stock_moves(self): + existing_move_lines = [] + for rma_line in self.rma_id.rma_line_ids: + existing_move_lines.append(rma_line.reference_move_id) + return existing_move_lines + + def add_lines(self): + rma_line_obj = self.env["rma.order.line"] + existing_stock_moves = self._get_existing_stock_moves() + for sm in self.move_ids: + if sm not in existing_stock_moves: + if sm.product_id.tracking == "none": + data = self._prepare_rma_line_from_stock_move(sm, lot=False) + rma_line_obj.with_context(default_rma_id=self.rma_id.id).create( + data + ) + else: + lot_ids = [x.lot_id.id for x in sm.move_line_ids if x.lot_id] + data = self._prepare_rma_line_from_stock_move(sm, lot=lot_ids[0]) + rma_line_obj.with_context(default_rma_id=self.rma_id.id).create( + data + ) + return {"type": "ir.actions.act_window_close"} diff --git a/rma/wizards/rma_add_stock_move_view.xml b/rma/wizards/rma_add_stock_move_view.xml new file mode 100644 index 00000000..f7512c4a --- /dev/null +++ b/rma/wizards/rma_add_stock_move_view.xml @@ -0,0 +1,121 @@ + + + + + rma.add.stock.move.customer + rma_add_stock_move + +
+ + + + + + + + + + + + + + + + + + +
+
+ +
+
+ + + Add from Stock Move + ir.actions.act_window + rma_add_stock_move + form + new + + + + + + + rma.order.form - stock.move wizard + rma.order + + + +
+
+
+ + + rma.order.line.supplier.form + rma.order.line + + +
+
+
+
+ + diff --git a/rma/wizards/rma_order_line_make_supplier_rma.py b/rma/wizards/rma_order_line_make_supplier_rma.py new file mode 100644 index 00000000..eba6ea0c --- /dev/null +++ b/rma/wizards/rma_order_line_make_supplier_rma.py @@ -0,0 +1,211 @@ +# Copyright 2017 ForgeFlow +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class RmaLineMakeSupplierRma(models.TransientModel): + _name = "rma.order.line.make.supplier.rma" + _description = "RMA Line Make Supplier RMA" + + partner_id = fields.Many2one( + comodel_name="res.partner", + string="Supplier", + domain=[("supplier", "=", True)], + required=True, + ) + item_ids = fields.One2many( + comodel_name="rma.order.line.make.supplier.rma.item", + inverse_name="wiz_id", + string="Items", + ) + supplier_rma_id = fields.Many2one( + comodel_name="rma.order", string="Supplier RMA Order Group" + ) + + @api.model + def _get_default_operation(self): + """Dropshipping is the most common use case of this wizard, thus + trying to default to a dropshipping operation first.""" + operation = self.env["rma.operation"].search( + [("type", "=", "supplier"), ("supplier_to_customer", "=", True)], limit=1 + ) + if not operation: + operation = self.env["rma.operation"].search( + [("type", "=", "supplier")], limit=1 + ) + return operation + + @api.model + def _prepare_item(self, line): + operation = self._get_default_operation() + return { + "line_id": line.id, + "product_id": line.product_id.id, + "name": line.name, + "product_qty": line.qty_to_supplier_rma, + "uom_id": line.uom_id.id, + "operation_id": operation.id if operation else False, + } + + @api.model + def default_get(self, fields_list): + res = super(RmaLineMakeSupplierRma, self).default_get(fields_list) + rma_line_obj = self.env["rma.order.line"] + rma_line_ids = self.env.context["active_ids"] or [] + active_model = self.env.context["active_model"] + + if not rma_line_ids: + return res + assert active_model == "rma.order.line", "Bad context propagation" + + items = [] + lines = rma_line_obj.browse(rma_line_ids) + for line in lines: + items.append([0, 0, self._prepare_item(line)]) + suppliers = lines.mapped( + lambda r: r.supplier_address_id.parent_id or r.supplier_address_id + ) + if len(suppliers) > 1: + raise ValidationError( + _( + "Only RMA lines from the same supplier can be " + "processed at the same time" + ) + ) + res["partner_id"] = suppliers.id + res["item_ids"] = items + return res + + @api.model + def _prepare_supplier_rma(self, company): + if not self.partner_id: + raise ValidationError(_("Enter a supplier.")) + return { + "partner_id": self.partner_id.id, + "type": "supplier", + "company_id": company.id, + } + + @api.model + def _prepare_supplier_rma_line(self, rma, item): + if item.operation_id: + operation = item.operation_id + else: + operation = self._get_default_operation() + if not operation.in_route_id or not operation.out_route_id: + route = self.env["stock.location.route"].search( + [("rma_selectable", "=", True)], limit=1 + ) + if not route: + raise ValidationError(_("Please define an RMA route")) + if not operation.in_warehouse_id or not operation.out_warehouse_id: + warehouse = self.env["stock.warehouse"].search( + [ + ("company_id", "=", self.rma_id.company_id.id), + ("lot_rma_id", "!=", False), + ], + limit=1, + ) + if not warehouse: + raise ValidationError( + _("Please define a warehouse with a default RMA location") + ) + data = { + "partner_id": self.partner_id.id, + "type": "supplier", + "origin": item.line_id.rma_id.name, + "customer_address_id": item.line_id.delivery_address_id.id + or item.line_id.partner_id.id, + "delivery_address_id": self.partner_id.id, + "product_id": item.line_id.product_id.id, + "customer_rma_id": item.line_id.id, + "product_qty": item.product_qty, + "rma_id": rma.id if rma else False, + "uom_id": item.line_id.uom_id.id, + "operation_id": operation.id, + "receipt_policy": operation.receipt_policy, + "delivery_policy": operation.delivery_policy, + "supplier_to_customer": operation.supplier_to_customer, + "in_warehouse_id": operation.in_warehouse_id.id or warehouse.id, + "out_warehouse_id": operation.out_warehouse_id.id or warehouse.id, + "in_route_id": operation.in_route_id.id or route.id, + "out_route_id": operation.out_route_id.id or route.id, + "location_id": ( + operation.location_id.id + or operation.in_warehouse_id.lot_rma_id.id + or warehouse.lot_rma_id.id + ), + } + return data + + def make_supplier_rma(self): + self = self.with_context(supplier=True, customer=False) + rma_obj = self.env["rma.order"] + rma_line_obj = self.env["rma.order.line"] + rma = False + + for item in self.item_ids: + line = item.line_id + if item.product_qty <= 0.0: + raise ValidationError(_("Enter a positive quantity.")) + + if self.supplier_rma_id: + rma = self.supplier_rma_id + if not rma and len(self.item_ids) > 1: + rma_data = self._prepare_supplier_rma(line.company_id) + rma = rma_obj.create(rma_data) + + rma_line_data = self._prepare_supplier_rma_line(rma, item) + rma_line = rma_line_obj.create(rma_line_data) + if rma: + return { + "name": _("Supplier RMA"), + "view_mode": "form", + "res_model": "rma.order", + "view_id": False, + "res_id": rma.id, + "context": {"supplier": True, "customer": False}, + "type": "ir.actions.act_window", + } + else: + return { + "name": _("Supplier RMA Line"), + "view_mode": "form", + "res_model": "rma.order.line", + "view_id": False, + "res_id": rma_line.id, + "context": {"supplier": True, "customer": False}, + "type": "ir.actions.act_window", + } + + +class RmaLineMakeRmaOrderItem(models.TransientModel): + _name = "rma.order.line.make.supplier.rma.item" + _description = "RMA Line Make Supplier RMA Item" + + wiz_id = fields.Many2one( + "rma.order.line.make.supplier.rma", + string="Wizard", + required=True, + ondelete="cascade", + readonly=True, + ) + line_id = fields.Many2one( + "rma.order.line", string="RMA Line", required=True, ondelete="cascade" + ) + rma_id = fields.Many2one( + "rma.order", related="line_id.rma_id", string="RMA Order", readonly=True + ) + product_id = fields.Many2one( + "product.product", related="line_id.product_id", readony=True + ) + name = fields.Char(related="line_id.name", readonly=True) + uom_id = fields.Many2one("uom.uom", string="UoM", readonly=True) + product_qty = fields.Float(string="Quantity", digits="Product UoS") + operation_id = fields.Many2one( + comodel_name="rma.operation", + string="Operation", + domain=[("type", "=", "supplier")], + ) diff --git a/rma/wizards/rma_order_line_make_supplier_rma_view.xml b/rma/wizards/rma_order_line_make_supplier_rma_view.xml new file mode 100644 index 00000000..bc9cc547 --- /dev/null +++ b/rma/wizards/rma_order_line_make_supplier_rma_view.xml @@ -0,0 +1,75 @@ + + + + + RMA Line Make Supplier RMA + rma.order.line.make.supplier.rma + form + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ + + Create Supplier RMA + ir.actions.act_window + rma.order.line.make.supplier.rma + form + + new + + + + + rma.order.line.supplier.rma.form + rma.order.line + + +
+
+
+
+