Skip to content

Commit b78493d

Browse files
committed
[ADD] stock_mass_return: add mass return functionality for done transfer
In this commit: - Inherited `stock.return.picking` wizard (`stock_picking_return.py`) to add new fields and functions for handling multiple pickings simultaneously. - Added validation to ensure all selected pickings are in the `Done` state, have the same operation type, and are linked to either a sale or purchase order. - Inherited `stock.return.picking.line` wizard (`stock_picking_return_line.py`) to add new fields and set dynamic domains for fields like `product_id`, `sale_order_id`, `purchase_order_id`, and `picking_id`. - Updated the view (`stock_picking_return_views.xml`)
1 parent 4c650f3 commit b78493d

9 files changed

+380
-0
lines changed

stock_mass_return/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# -*- coding: utf-8 -*-
2+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
3+
4+
from . import wizard
5+
from . import models

stock_mass_return/__manifest__.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# -*- coding: utf-8 -*-
2+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
3+
4+
{
5+
"name": "Stock Mass Return",
6+
"version": "1.0",
7+
"summary": "Handle mass return functionality for done transfers",
8+
"depends": [
9+
"stock",
10+
"stock_accountant",
11+
"sale_management",
12+
"purchase"
13+
],
14+
"data": ["wizard/stock_picking_return_views.xml"],
15+
"license": "LGPL-3",
16+
}

stock_mass_return/models/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# -*- coding: utf-8 -*-
2+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
3+
4+
from . import sale_order
5+
from . import stock_picking
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# -*- coding: utf-8 -*-
2+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
3+
4+
from odoo import models
5+
6+
7+
class SaleOrder(models.Model):
8+
_inherit = "sale.order"
9+
10+
def _compute_picking_ids(self):
11+
for order in self:
12+
order.delivery_count = self.env["stock.picking"].search_count(
13+
["|", ("sale_ids", "in", order.ids), ("sale_id", "=", order.id)]
14+
)
15+
16+
def action_view_delivery(self):
17+
self.ensure_one()
18+
pickings = self.env["stock.picking"].search(
19+
["|", ("sale_ids", "in", self.ids), ("sale_id", "=", self.id)],
20+
)
21+
return {
22+
"type": "ir.actions.act_window",
23+
"name": "Delivery Orders",
24+
"res_model": "stock.picking",
25+
"view_mode": "list,form",
26+
"domain": [("id", "in", pickings.ids)],
27+
}
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# -*- coding: utf-8 -*-
2+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
3+
4+
from odoo import _, api, fields, models
5+
6+
7+
class StockPicking(models.Model):
8+
_inherit = "stock.picking"
9+
10+
sale_ids = fields.Many2many(
11+
"sale.order",
12+
"stock_picking_sale_order_rel",
13+
"picking_id"
14+
"sale_id",
15+
string="Sale Orders",
16+
)
17+
18+
new_return_ids = fields.Many2many("stock.picking",
19+
"stock_picking_new_return_rel",
20+
"picking_id",
21+
"return_id",
22+
string="New Return Pickings",
23+
)
24+
25+
@api.depends('return_ids', 'new_return_ids')
26+
def _compute_return_count(self):
27+
for picking in self:
28+
picking.return_count = len(set(picking.return_ids.ids) | set(picking.new_return_ids.ids))
29+
30+
def action_see_returns(self):
31+
returns = self.return_ids | self.new_return_ids
32+
return {
33+
"name": _('Returns'),
34+
"type": "ir.actions.act_window",
35+
"res_model": "stock.picking",
36+
"view_mode": "list,form",
37+
"domain": [("id", "in", returns.ids)],
38+
}
39+
40+
def _get_next_transfers(self):
41+
next_pickings = super()._get_next_transfers()
42+
return next_pickings.filtered(lambda p: p not in self.new_return_ids)
43+

stock_mass_return/wizard/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# -*- coding: utf-8 -*-
2+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
3+
4+
from . import stock_picking_return
5+
from . import stock_picking_return_line
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# -*- coding: utf-8 -*-
2+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
3+
4+
from odoo import _, api, fields, models, Command
5+
from odoo.exceptions import UserError
6+
7+
8+
class ReturnPicking(models.TransientModel):
9+
_inherit = "stock.return.picking"
10+
11+
picking_ids = fields.Many2many("stock.picking", string="Pickings")
12+
picking_type_id = fields.Many2one("stock.picking.type", string="Operation Type")
13+
allowed_product_ids = fields.Many2many(
14+
"product.product", string="Allowed Products"
15+
)
16+
17+
@api.model
18+
def default_get(self, fields):
19+
active_ids = self.get_active_ids()
20+
if len(active_ids) == 1:
21+
return super().default_get(fields)
22+
res = {}
23+
pickings = self.env["stock.picking"].browse(active_ids)
24+
if any(p.state != "done" for p in pickings):
25+
raise UserError(_("All selected pickings must be in 'Done' state."))
26+
27+
picking_type = pickings[0].picking_type_id
28+
if any(p.picking_type_id != picking_type for p in pickings):
29+
raise UserError(
30+
_("All selected pickings must have the same operation type.")
31+
)
32+
if picking_type.code == "outgoing":
33+
if any(not p.sale_id for p in pickings):
34+
raise UserError(_("All selected pickings must have a sale order."))
35+
else:
36+
for picking in pickings:
37+
purchase_order = self.env["purchase.order"].search(
38+
[("picking_ids", "in", picking.ids)], limit=1
39+
)
40+
if not purchase_order:
41+
raise UserError(
42+
_("All selected pickings must have a purchase order.")
43+
)
44+
45+
res = {
46+
"picking_ids": [Command.set(active_ids)],
47+
"picking_id": pickings[0].id,
48+
"picking_type_id": picking_type.id,
49+
"allowed_product_ids": [Command.set(pickings.mapped("move_ids.product_id").ids)],
50+
"product_return_moves": [Command.clear()],
51+
}
52+
53+
return res
54+
55+
def get_active_ids(self):
56+
active_ids = self.env.context.get("active_ids", [])
57+
if isinstance(active_ids, int):
58+
active_ids = [active_ids]
59+
return active_ids
60+
61+
def _get_picking_ids(self):
62+
return self.product_return_moves.filtered(
63+
lambda line: line.quantity > 0
64+
).mapped("picking_id")
65+
66+
def _get_origin(self):
67+
new_origin = "Return of "
68+
for picking in self._get_picking_ids():
69+
new_origin += picking.origin + ", "
70+
return new_origin[:-2]
71+
72+
def create_picking_wizard(self, picking_id):
73+
return self.env["stock.return.picking"].create(
74+
{
75+
"picking_id": picking_id.id,
76+
"product_return_moves": [
77+
Command.create(
78+
{
79+
"product_id": line.product_id.id,
80+
"quantity": line.quantity,
81+
"to_refund": line.to_refund,
82+
"move_id": line.move_id.id,
83+
},
84+
)
85+
for line in self.product_return_moves.filtered(
86+
lambda l: l.picking_id == picking_id
87+
)
88+
],
89+
}
90+
)
91+
92+
def action_create_returns_all(self):
93+
if len(self.get_active_ids()) == 1:
94+
return super().action_create_returns_all()
95+
new_picking = self._create_return()
96+
97+
for picking in self._get_picking_ids():
98+
picking.write({"new_return_ids": [Command.link(new_picking.id)]})
99+
last_message = self.env["mail.message"].search(
100+
[("res_id", "=", new_picking.id), ("model", "=", "stock.picking")],
101+
order="id desc",
102+
limit=1,
103+
)
104+
if last_message:
105+
last_message.unlink()
106+
new_picking.message_post_with_source(
107+
"mail.message_origin_link",
108+
render_values={"self": new_picking, "origin": self._get_picking_ids()},
109+
subtype_xmlid="mail.mt_note",
110+
)
111+
112+
vals = {
113+
"sale_id": self._get_picking_ids()[0].sale_id.id,
114+
"sale_ids": self._get_picking_ids().mapped("sale_id").ids,
115+
"origin": self._get_origin(),
116+
"return_id": self._get_picking_ids()[0].id
117+
}
118+
119+
new_picking.write(vals)
120+
return {
121+
"name": _("Returned Picking"),
122+
"view_mode": "form",
123+
"res_model": "stock.picking",
124+
"res_id": new_picking.id,
125+
"type": "ir.actions.act_window",
126+
"context": self.env.context,
127+
}
128+
129+
def action_create_returns(self):
130+
new_pickings = None
131+
active_ids = self.get_active_ids()
132+
if len(active_ids) == 1:
133+
return super().action_create_returns()
134+
135+
for picking_id in self._get_picking_ids():
136+
picking_return_wizard = self.create_picking_wizard(picking_id)
137+
138+
new_pickings =picking_return_wizard.with_context(
139+
active_ids=picking_id.id
140+
)._create_return()
141+
new_pickings.sale_id = picking_id.sale_id.id
142+
new_pickings.return_id = picking_id.id
143+
144+
return {
145+
"name": _("Returned Pickings"),
146+
"view_mode": "form",
147+
"res_model": "stock.picking",
148+
"res_id": new_pickings.id,
149+
"type": "ir.actions.act_window",
150+
"context": self.env.context,
151+
}
152+
153+
def create_exchanges(self):
154+
active_ids = self.get_active_ids()
155+
if len(active_ids) == 1:
156+
return super().action_create_exchanges()
157+
else:
158+
action = None
159+
for picking_id in self.picking_ids:
160+
picking_return_wizard = self.create_picking_wizard(picking_id)
161+
action = picking_return_wizard.with_context(
162+
active_ids=picking_id.id
163+
).action_create_exchanges()
164+
return action
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# -*- coding: utf-8 -*-
2+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
3+
4+
from odoo import api, fields, models
5+
6+
7+
class ReturnPickingLine(models.TransientModel):
8+
_inherit = "stock.return.picking.line"
9+
10+
product_id = fields.Many2one(
11+
"product.product",
12+
string="Product",
13+
required=True,
14+
domain="[('id', 'in', allowed_product_ids)]",
15+
)
16+
allowed_product_ids = fields.Many2many(
17+
"product.product",
18+
string="Allowed Products",
19+
related="wizard_id.allowed_product_ids",
20+
store=False,
21+
)
22+
allowed_picking_ids = fields.Many2many("stock.picking")
23+
picking_id = fields.Many2one(
24+
"stock.picking",
25+
string="Sale Order",
26+
domain="[('id', 'in', allowed_picking_ids)]",
27+
)
28+
allowed_sale_order_ids = fields.Many2many("sale.order")
29+
sale_order_id = fields.Many2one("sale.order", string="Sale Order", domain="[('id', 'in', allowed_sale_order_ids)]",)
30+
allowed_purchase_order_ids = fields.Many2many("purchase.order")
31+
purchase_order_id = fields.Many2one("purchase.order", string="Purchase Order", domain="[('id', 'in', allowed_purchase_order_ids)]",)
32+
33+
@api.onchange("purchase_order_id")
34+
def _onchange_purchase_order_id(self):
35+
if self.purchase_order_id:
36+
self.picking_id = self.env["stock.picking"].search(
37+
[("id", "in", self.purchase_order_id.picking_ids.ids)], limit=1
38+
)
39+
40+
@api.onchange("sale_order_id")
41+
def _onchange_sale_order_id(self):
42+
if self.sale_order_id:
43+
self.picking_id = self.wizard_id.picking_ids.filtered(
44+
lambda p: p.sale_id == self.sale_order_id._origin
45+
)
46+
47+
@api.onchange("product_id","picking_id")
48+
def _onchange_product_id_and_picking_id(self):
49+
if self.product_id:
50+
move = self.picking_id.move_ids.filtered(lambda m: m.product_id == self.product_id)
51+
self.move_id = move.id
52+
53+
self.allowed_picking_ids = self.wizard_id.picking_ids.filtered(
54+
lambda p: p.move_ids.filtered(
55+
lambda m: m.product_id == self.product_id
56+
)
57+
).ids
58+
59+
if self.wizard_id.picking_type_id.code == "outgoing":
60+
allowed_sales = [
61+
picking.sale_id.id
62+
for picking in self.allowed_picking_ids
63+
if picking.sale_id
64+
]
65+
self.allowed_sale_order_ids = [(6, 0, allowed_sales)]
66+
else:
67+
allowed_purchase = self.env["purchase.order"].search([
68+
("picking_ids", "in", self.allowed_picking_ids.ids),
69+
("order_line.product_id", "=", self.product_id.id),
70+
])
71+
self.allowed_purchase_order_ids = [(6, 0, allowed_purchase.ids)]
72+
73+
if (self.allowed_purchase_order_ids and len(self.allowed_purchase_order_ids) == 1):
74+
self.purchase_order_id = self.allowed_purchase_order_ids[0].id
75+
76+
if self.allowed_sale_order_ids and len(self.allowed_sale_order_ids) == 1:
77+
self.sale_order_id = self.allowed_sale_order_ids[0].id
78+
79+
if len(self.allowed_picking_ids) == 1:
80+
self.update({"picking_id": self.allowed_picking_ids[0]})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<record id="view_stock_return_picking_form_inherited" model="ir.ui.view">
4+
<field name="name">stock.return.picking.inherit.form</field>
5+
<field name="model">stock.return.picking</field>
6+
<field name="inherit_id" ref="stock.view_stock_return_picking_form"/>
7+
<field name="arch" type="xml">
8+
<xpath expr="//field[@name='product_id']" position="after">
9+
<field name="allowed_sale_order_ids" column_invisible="1" />
10+
<field name="allowed_purchase_order_ids" column_invisible="1" />
11+
<field name="allowed_picking_ids" column_invisible="1" />
12+
<field name="move_id" column_invisible="1"/>
13+
<field name="picking_id" column_invisible="1"/>
14+
<field name="sale_order_id" required="context.get('active_ids').length > 1 and allowed_sale_order_ids.length >= 1" column_invisible="context.get('active_ids').length == 1 or context.get('active_domain')[0][2] == 'incoming'"/>
15+
<field name="purchase_order_id" required="context.get('active_ids').length > 1 and allowed_purchase_order_ids.length >= 1" column_invisible="context.get('active_ids').length == 1 or context.get('active_domain')[0][2] == 'outgoing'"/>
16+
</xpath>
17+
<xpath expr="//button[@name='action_create_returns']" position="replace">
18+
<button name="action_create_returns" string="Return" type="object" class="btn-primary"/>
19+
</xpath>
20+
<xpath expr="//button[@name='action_create_exchanges']" position="replace">
21+
<button name="create_exchanges" string="Return for Exchange" type="object" class="btn-primary"/>
22+
</xpath>
23+
</field>
24+
</record>
25+
26+
<record id="action_stock_mass_return_wizard" model="ir.actions.act_window">
27+
<field name="name">Mass Return</field>
28+
<field name="res_model">stock.return.picking</field>
29+
<field name="view_mode">form</field>
30+
<field name="view_id" ref="view_stock_return_picking_form_inherited"/>
31+
<field name="target">new</field>
32+
<field name="binding_model_id" ref="stock.model_stock_picking"/>
33+
<field name="binding_view_types">list</field>
34+
</record>
35+
</odoo>

0 commit comments

Comments
 (0)