Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ADD] stock_mass_return: add mass return functionality for done transfer #640

Draft
wants to merge 1 commit into
base: 18.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions stock_mass_return/__init__.py
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions stock_mass_return/__manifest__.py
Original file line number Diff line number Diff line change
@@ -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",
}
5 changes: 5 additions & 0 deletions stock_mass_return/models/__init__.py
Original file line number Diff line number Diff line change
@@ -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
27 changes: 27 additions & 0 deletions stock_mass_return/models/sale_order.py
Original file line number Diff line number Diff line change
@@ -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)],
}
43 changes: 43 additions & 0 deletions stock_mass_return/models/stock_picking.py
Original file line number Diff line number Diff line change
@@ -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)

5 changes: 5 additions & 0 deletions stock_mass_return/wizard/__init__.py
Original file line number Diff line number Diff line change
@@ -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
164 changes: 164 additions & 0 deletions stock_mass_return/wizard/stock_picking_return.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# -*- 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
)
],
}
)

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)]})
last_message = self.env["mail.message"].search(
[("res_id", "=", new_picking.id), ("model", "=", "stock.picking")],
order="id desc",
limit=1,
)
if last_message:
last_message.unlink()
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",
)

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
80 changes: 80 additions & 0 deletions stock_mass_return/wizard/stock_picking_return_line.py
Original file line number Diff line number Diff line change
@@ -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]})
35 changes: 35 additions & 0 deletions stock_mass_return/wizard/stock_picking_return_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_stock_return_picking_form_inherited" model="ir.ui.view">
<field name="name">stock.return.picking.inherit.form</field>
<field name="model">stock.return.picking</field>
<field name="inherit_id" ref="stock.view_stock_return_picking_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='product_id']" position="after">
<field name="allowed_sale_order_ids" column_invisible="1" />
<field name="allowed_purchase_order_ids" column_invisible="1" />
<field name="allowed_picking_ids" column_invisible="1" />
<field name="move_id" column_invisible="1"/>
<field name="picking_id" column_invisible="1"/>
<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'"/>
<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'"/>
</xpath>
<xpath expr="//button[@name='action_create_returns']" position="replace">
<button name="action_create_returns" string="Return" type="object" class="btn-primary"/>
</xpath>
<xpath expr="//button[@name='action_create_exchanges']" position="replace">
<button name="create_exchanges" string="Return for Exchange" type="object" class="btn-primary"/>
</xpath>
</field>
</record>

<record id="action_stock_mass_return_wizard" model="ir.actions.act_window">
<field name="name">Mass Return</field>
<field name="res_model">stock.return.picking</field>
<field name="view_mode">form</field>
<field name="view_id" ref="view_stock_return_picking_form_inherited"/>
<field name="target">new</field>
<field name="binding_model_id" ref="stock.model_stock_picking"/>
<field name="binding_view_types">list</field>
</record>
</odoo>