diff --git a/stock_mass_return/__init__.py b/stock_mass_return/__init__.py new file mode 100644 index 00000000000..b6f07433380 --- /dev/null +++ b/stock_mass_return/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import wizard +from . import models diff --git a/stock_mass_return/__manifest__.py b/stock_mass_return/__manifest__.py new file mode 100644 index 00000000000..0ac6db255fd --- /dev/null +++ b/stock_mass_return/__manifest__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + "name": "Stock Mass Return", + "version": "1.0", + "summary": "Handle mass return functionality for done transfers", + "depends": [ + "stock", + "stock_accountant", + "sale_management", + "purchase" + ], + "data": ["wizard/stock_picking_return_views.xml"], + "license": "LGPL-3", +} diff --git a/stock_mass_return/models/__init__.py b/stock_mass_return/models/__init__.py new file mode 100644 index 00000000000..63e83203356 --- /dev/null +++ b/stock_mass_return/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import sale_order +from . import stock_picking diff --git a/stock_mass_return/models/sale_order.py b/stock_mass_return/models/sale_order.py new file mode 100644 index 00000000000..5ae8a130ed0 --- /dev/null +++ b/stock_mass_return/models/sale_order.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def _compute_picking_ids(self): + for order in self: + order.delivery_count = self.env["stock.picking"].search_count( + ["|", ("sale_ids", "in", order.ids), ("sale_id", "=", order.id)] + ) + + def action_view_delivery(self): + self.ensure_one() + pickings = self.env["stock.picking"].search( + ["|", ("sale_ids", "in", self.ids), ("sale_id", "=", self.id)], + ) + return { + "type": "ir.actions.act_window", + "name": "Delivery Orders", + "res_model": "stock.picking", + "view_mode": "list,form", + "domain": [("id", "in", pickings.ids)], + } diff --git a/stock_mass_return/models/stock_picking.py b/stock_mass_return/models/stock_picking.py new file mode 100644 index 00000000000..770cefed163 --- /dev/null +++ b/stock_mass_return/models/stock_picking.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import _, api, fields, models + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + sale_ids = fields.Many2many( + "sale.order", + "stock_picking_sale_order_rel", + "picking_id" + "sale_id", + string="Sale Orders", + ) + + new_return_ids = fields.Many2many("stock.picking", + "stock_picking_new_return_rel", + "picking_id", + "return_id", + string="New Return Pickings", + ) + + @api.depends('return_ids', 'new_return_ids') + def _compute_return_count(self): + for picking in self: + picking.return_count = len(set(picking.return_ids.ids) | set(picking.new_return_ids.ids)) + + def action_see_returns(self): + returns = self.return_ids | self.new_return_ids + return { + "name": _('Returns'), + "type": "ir.actions.act_window", + "res_model": "stock.picking", + "view_mode": "list,form", + "domain": [("id", "in", returns.ids)], + } + + def _get_next_transfers(self): + next_pickings = super()._get_next_transfers() + return next_pickings.filtered(lambda p: p not in self.new_return_ids) + diff --git a/stock_mass_return/wizard/__init__.py b/stock_mass_return/wizard/__init__.py new file mode 100644 index 00000000000..de8142564fd --- /dev/null +++ b/stock_mass_return/wizard/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import stock_picking_return +from . import stock_picking_return_line diff --git a/stock_mass_return/wizard/stock_picking_return.py b/stock_mass_return/wizard/stock_picking_return.py new file mode 100644 index 00000000000..f65f66912e0 --- /dev/null +++ b/stock_mass_return/wizard/stock_picking_return.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import _, api, fields, models, Command +from odoo.exceptions import UserError + + +class ReturnPicking(models.TransientModel): + _inherit = "stock.return.picking" + + picking_ids = fields.Many2many("stock.picking", string="Pickings") + picking_type_id = fields.Many2one("stock.picking.type", string="Operation Type") + allowed_product_ids = fields.Many2many( + "product.product", string="Allowed Products" + ) + + @api.model + def default_get(self, fields): + active_ids = self.get_active_ids() + if len(active_ids) == 1: + return super().default_get(fields) + res = {} + pickings = self.env["stock.picking"].browse(active_ids) + if any(p.state != "done" for p in pickings): + raise UserError(_("All selected pickings must be in 'Done' state.")) + + picking_type = pickings[0].picking_type_id + if any(p.picking_type_id != picking_type for p in pickings): + raise UserError( + _("All selected pickings must have the same operation type.") + ) + if picking_type.code == "outgoing": + if any(not p.sale_id for p in pickings): + raise UserError(_("All selected pickings must have a sale order.")) + else: + for picking in pickings: + purchase_order = self.env["purchase.order"].search( + [("picking_ids", "in", picking.ids)], limit=1 + ) + if not purchase_order: + raise UserError( + _("All selected pickings must have a purchase order.") + ) + + res = { + "picking_ids": [Command.set(active_ids)], + "picking_id": pickings[0].id, + "picking_type_id": picking_type.id, + "allowed_product_ids": [Command.set(pickings.mapped("move_ids.product_id").ids)], + "product_return_moves": [Command.clear()], + } + + return res + + def get_active_ids(self): + active_ids = self.env.context.get("active_ids", []) + if isinstance(active_ids, int): + active_ids = [active_ids] + return active_ids + + def _get_picking_ids(self): + return self.product_return_moves.filtered( + lambda line: line.quantity > 0 + ).mapped("picking_id") + + def _get_origin(self): + new_origin = "Return of " + for picking in self._get_picking_ids(): + new_origin += picking.origin + ", " + return new_origin[:-2] + + def create_picking_wizard(self, picking_id): + return self.env["stock.return.picking"].create( + { + "picking_id": picking_id.id, + "product_return_moves": [ + Command.create( + { + "product_id": line.product_id.id, + "quantity": line.quantity, + "to_refund": line.to_refund, + "move_id": line.move_id.id, + }, + ) + for line in self.product_return_moves.filtered( + lambda l: l.picking_id == picking_id + ) + ], + } + ) + + # Override _create_return + def _create_return(self): + for return_move in self.product_return_moves.move_id: + return_move.move_dest_ids.filtered(lambda m: m.state not in ('done', 'cancel'))._do_unreserve() + + # create new picking for returned products + new_picking = self.picking_id.copy(self._prepare_picking_default_values()) + new_picking.user_id = False + new_picking.message_post_with_source( + "mail.message_origin_link", + render_values={"self": new_picking, "origin": self._get_picking_ids()}, + subtype_xmlid="mail.mt_note", + ) + returned_lines = False + for return_line in self.product_return_moves: + if return_line._process_line(new_picking): + returned_lines = True + if not returned_lines: + raise UserError(_("Please specify at least one non-zero quantity.")) + + new_picking.action_confirm() + new_picking.action_assign() + return new_picking + + def action_create_returns_all(self): + if len(self.get_active_ids()) == 1: + return super().action_create_returns_all() + new_picking = self._create_return() + + for picking in self._get_picking_ids(): + picking.write({"new_return_ids": [Command.link(new_picking.id)]}) + + vals = { + "sale_id": self._get_picking_ids()[0].sale_id.id, + "sale_ids": self._get_picking_ids().mapped("sale_id").ids, + "origin": self._get_origin(), + "return_id": self._get_picking_ids()[0].id + } + + new_picking.write(vals) + return { + "name": _("Returned Picking"), + "view_mode": "form", + "res_model": "stock.picking", + "res_id": new_picking.id, + "type": "ir.actions.act_window", + "context": self.env.context, + } + + def action_create_returns(self): + new_pickings = None + active_ids = self.get_active_ids() + if len(active_ids) == 1: + return super().action_create_returns() + + for picking_id in self._get_picking_ids(): + picking_return_wizard = self.create_picking_wizard(picking_id) + + new_pickings =picking_return_wizard.with_context( + active_ids=picking_id.id + )._create_return() + new_pickings.sale_id = picking_id.sale_id.id + new_pickings.return_id = picking_id.id + + return { + "name": _("Returned Pickings"), + "view_mode": "form", + "res_model": "stock.picking", + "res_id": new_pickings.id, + "type": "ir.actions.act_window", + "context": self.env.context, + } + + def create_exchanges(self): + active_ids = self.get_active_ids() + if len(active_ids) == 1: + return super().action_create_exchanges() + else: + action = None + for picking_id in self.picking_ids: + picking_return_wizard = self.create_picking_wizard(picking_id) + action = picking_return_wizard.with_context( + active_ids=picking_id.id + ).action_create_exchanges() + return action diff --git a/stock_mass_return/wizard/stock_picking_return_line.py b/stock_mass_return/wizard/stock_picking_return_line.py new file mode 100644 index 00000000000..7080bcbd196 --- /dev/null +++ b/stock_mass_return/wizard/stock_picking_return_line.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class ReturnPickingLine(models.TransientModel): + _inherit = "stock.return.picking.line" + + product_id = fields.Many2one( + "product.product", + string="Product", + required=True, + domain="[('id', 'in', allowed_product_ids)]", + ) + allowed_product_ids = fields.Many2many( + "product.product", + string="Allowed Products", + related="wizard_id.allowed_product_ids", + store=False, + ) + allowed_picking_ids = fields.Many2many("stock.picking") + picking_id = fields.Many2one( + "stock.picking", + string="Sale Order", + domain="[('id', 'in', allowed_picking_ids)]", + ) + allowed_sale_order_ids = fields.Many2many("sale.order") + sale_order_id = fields.Many2one("sale.order", string="Sale Order", domain="[('id', 'in', allowed_sale_order_ids)]",) + allowed_purchase_order_ids = fields.Many2many("purchase.order") + purchase_order_id = fields.Many2one("purchase.order", string="Purchase Order", domain="[('id', 'in', allowed_purchase_order_ids)]",) + + @api.onchange("purchase_order_id") + def _onchange_purchase_order_id(self): + if self.purchase_order_id: + self.picking_id = self.env["stock.picking"].search( + [("id", "in", self.purchase_order_id.picking_ids.ids)], limit=1 + ) + + @api.onchange("sale_order_id") + def _onchange_sale_order_id(self): + if self.sale_order_id: + self.picking_id = self.wizard_id.picking_ids.filtered( + lambda p: p.sale_id == self.sale_order_id._origin + ) + + @api.onchange("product_id","picking_id") + def _onchange_product_id_and_picking_id(self): + if self.product_id: + move = self.picking_id.move_ids.filtered(lambda m: m.product_id == self.product_id) + self.move_id = move.id + + self.allowed_picking_ids = self.wizard_id.picking_ids.filtered( + lambda p: p.move_ids.filtered( + lambda m: m.product_id == self.product_id + ) + ).ids + + if self.wizard_id.picking_type_id.code == "outgoing": + allowed_sales = [ + picking.sale_id.id + for picking in self.allowed_picking_ids + if picking.sale_id + ] + self.allowed_sale_order_ids = [(6, 0, allowed_sales)] + else: + allowed_purchase = self.env["purchase.order"].search([ + ("picking_ids", "in", self.allowed_picking_ids.ids), + ("order_line.product_id", "=", self.product_id.id), + ]) + self.allowed_purchase_order_ids = [(6, 0, allowed_purchase.ids)] + + if (self.allowed_purchase_order_ids and len(self.allowed_purchase_order_ids) == 1): + self.purchase_order_id = self.allowed_purchase_order_ids[0].id + + if self.allowed_sale_order_ids and len(self.allowed_sale_order_ids) == 1: + self.sale_order_id = self.allowed_sale_order_ids[0].id + + if len(self.allowed_picking_ids) == 1: + self.update({"picking_id": self.allowed_picking_ids[0]}) diff --git a/stock_mass_return/wizard/stock_picking_return_views.xml b/stock_mass_return/wizard/stock_picking_return_views.xml new file mode 100644 index 00000000000..069fcc361ae --- /dev/null +++ b/stock_mass_return/wizard/stock_picking_return_views.xml @@ -0,0 +1,35 @@ + + + + stock.return.picking.inherit.form + stock.return.picking + + + + + + + + + + + + +