1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
|
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.tools.float_utils import float_round
class ReturnPickingLine(models.TransientModel):
_name = "stock.return.picking.line"
_rec_name = 'product_id'
_description = 'Return Picking Line'
product_id = fields.Many2one('product.product', string="Product", required=True, domain="[('id', '=', product_id)]")
quantity = fields.Float("Quantity", digits='Product Unit of Measure', required=True)
uom_id = fields.Many2one('uom.uom', string='Unit of Measure', related='move_id.product_uom', readonly=False)
wizard_id = fields.Many2one('stock.return.picking', string="Wizard")
move_id = fields.Many2one('stock.move', "Move")
class ReturnPicking(models.TransientModel):
_name = 'stock.return.picking'
_description = 'Return Picking'
@api.model
def default_get(self, fields):
if len(self.env.context.get('active_ids', list())) > 1:
raise UserError(_("You may only return one picking at a time."))
res = super(ReturnPicking, self).default_get(fields)
if self.env.context.get('active_id') and self.env.context.get('active_model') == 'stock.picking':
picking = self.env['stock.picking'].browse(self.env.context.get('active_id'))
if picking.exists():
res.update({'picking_id': picking.id})
return res
picking_id = fields.Many2one('stock.picking')
product_return_moves = fields.One2many('stock.return.picking.line', 'wizard_id', 'Moves')
move_dest_exists = fields.Boolean('Chained Move Exists', readonly=True)
original_location_id = fields.Many2one('stock.location')
parent_location_id = fields.Many2one('stock.location')
company_id = fields.Many2one(related='picking_id.company_id')
location_id = fields.Many2one(
'stock.location', 'Return Location',
domain="['|', ('id', '=', original_location_id), '|', '&', ('return_location', '=', True), ('company_id', '=', False), '&', ('return_location', '=', True), ('company_id', '=', company_id)]")
@api.onchange('picking_id')
def _onchange_picking_id(self):
move_dest_exists = False
product_return_moves = [(5,)]
if self.picking_id and self.picking_id.state != 'done':
raise UserError(_("You may only return Done pickings."))
# In case we want to set specific default values (e.g. 'to_refund'), we must fetch the
# default values for creation.
line_fields = [f for f in self.env['stock.return.picking.line']._fields.keys()]
product_return_moves_data_tmpl = self.env['stock.return.picking.line'].default_get(line_fields)
for move in self.picking_id.move_lines:
if move.state == 'cancel':
continue
if move.scrapped:
continue
if move.move_dest_ids:
move_dest_exists = True
product_return_moves_data = dict(product_return_moves_data_tmpl)
product_return_moves_data.update(self._prepare_stock_return_picking_line_vals_from_move(move))
product_return_moves.append((0, 0, product_return_moves_data))
if self.picking_id and not product_return_moves:
raise UserError(_("No products to return (only lines in Done state and not fully returned yet can be returned)."))
if self.picking_id:
self.product_return_moves = product_return_moves
self.move_dest_exists = move_dest_exists
self.parent_location_id = self.picking_id.picking_type_id.warehouse_id and self.picking_id.picking_type_id.warehouse_id.view_location_id.id or self.picking_id.location_id.location_id.id
self.original_location_id = self.picking_id.location_id.id
location_id = self.picking_id.location_id.id
if self.picking_id.picking_type_id.return_picking_type_id.default_location_dest_id.return_location:
location_id = self.picking_id.picking_type_id.return_picking_type_id.default_location_dest_id.id
self.location_id = location_id
@api.model
def _prepare_stock_return_picking_line_vals_from_move(self, stock_move):
quantity = stock_move.product_qty
for move in stock_move.move_dest_ids:
if move.origin_returned_move_id and move.origin_returned_move_id != stock_move:
continue
if move.state in ('partially_available', 'assigned'):
quantity -= sum(move.move_line_ids.mapped('product_qty'))
elif move.state in ('done'):
quantity -= move.product_qty
quantity = float_round(quantity, precision_rounding=stock_move.product_id.uom_id.rounding)
return {
'product_id': stock_move.product_id.id,
'quantity': quantity,
'move_id': stock_move.id,
'uom_id': stock_move.product_id.uom_id.id,
}
def _prepare_move_default_values(self, return_line, new_picking):
vals = {
'product_id': return_line.product_id.id,
'product_uom_qty': return_line.quantity,
'product_uom': return_line.product_id.uom_id.id,
'picking_id': new_picking.id,
'state': 'draft',
'date': fields.Datetime.now(),
'location_id': return_line.move_id.location_dest_id.id,
'location_dest_id': self.location_id.id or return_line.move_id.location_id.id,
'picking_type_id': new_picking.picking_type_id.id,
'warehouse_id': self.picking_id.picking_type_id.warehouse_id.id,
'origin_returned_move_id': return_line.move_id.id,
'procure_method': 'make_to_stock',
}
return vals
def _create_returns(self):
# TODO sle: the unreserve of the next moves could be less brutal
for return_move in self.product_return_moves.mapped('move_id'):
return_move.move_dest_ids.filtered(lambda m: m.state not in ('done', 'cancel'))._do_unreserve()
# create new picking for returned products
picking_type_id = self.picking_id.picking_type_id.return_picking_type_id.id or self.picking_id.picking_type_id.id
new_picking = self.picking_id.copy({
'move_lines': [],
'picking_type_id': picking_type_id,
'state': 'draft',
'origin': _("Return of %s", self.picking_id.name),
'location_id': self.picking_id.location_dest_id.id,
'location_dest_id': self.location_id.id})
new_picking.message_post_with_view('mail.message_origin_link',
values={'self': new_picking, 'origin': self.picking_id},
subtype_id=self.env.ref('mail.mt_note').id)
returned_lines = 0
for return_line in self.product_return_moves:
if not return_line.move_id:
raise UserError(_("You have manually created product lines, please delete them to proceed."))
# TODO sle: float_is_zero?
if return_line.quantity:
returned_lines += 1
vals = self._prepare_move_default_values(return_line, new_picking)
r = return_line.move_id.copy(vals)
vals = {}
# +--------------------------------------------------------------------------------------------------------+
# | picking_pick <--Move Orig-- picking_pack --Move Dest--> picking_ship
# | | returned_move_ids ↑ | returned_move_ids
# | ↓ | return_line.move_id ↓
# | return pick(Add as dest) return toLink return ship(Add as orig)
# +--------------------------------------------------------------------------------------------------------+
move_orig_to_link = return_line.move_id.move_dest_ids.mapped('returned_move_ids')
# link to original move
move_orig_to_link |= return_line.move_id
# link to siblings of original move, if any
move_orig_to_link |= return_line.move_id\
.mapped('move_dest_ids').filtered(lambda m: m.state not in ('cancel'))\
.mapped('move_orig_ids').filtered(lambda m: m.state not in ('cancel'))
move_dest_to_link = return_line.move_id.move_orig_ids.mapped('returned_move_ids')
# link to children of originally returned moves, if any. Note that the use of
# 'return_line.move_id.move_orig_ids.returned_move_ids.move_orig_ids.move_dest_ids'
# instead of 'return_line.move_id.move_orig_ids.move_dest_ids' prevents linking a
# return directly to the destination moves of its parents. However, the return of
# the return will be linked to the destination moves.
move_dest_to_link |= return_line.move_id.move_orig_ids.mapped('returned_move_ids')\
.mapped('move_orig_ids').filtered(lambda m: m.state not in ('cancel'))\
.mapped('move_dest_ids').filtered(lambda m: m.state not in ('cancel'))
vals['move_orig_ids'] = [(4, m.id) for m in move_orig_to_link]
vals['move_dest_ids'] = [(4, m.id) for m in move_dest_to_link]
r.write(vals)
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.id, picking_type_id
def create_returns(self):
for wizard in self:
new_picking_id, pick_type_id = wizard._create_returns()
# Override the context to disable all the potential filters that could have been set previously
ctx = dict(self.env.context)
ctx.update({
'default_partner_id': self.picking_id.partner_id.id,
'search_default_picking_type_id': pick_type_id,
'search_default_draft': False,
'search_default_assigned': False,
'search_default_confirmed': False,
'search_default_ready': False,
'search_default_planning_issues': False,
'search_default_available': False,
})
return {
'name': _('Returned Picking'),
'view_mode': 'form,tree,calendar',
'res_model': 'stock.picking',
'res_id': new_picking_id,
'type': 'ir.actions.act_window',
'context': ctx,
}
|