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
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
|
# -*- 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_compare, float_is_zero, float_round
class StockPickingBatch(models.Model):
_inherit = ['mail.thread', 'mail.activity.mixin']
_name = "stock.picking.batch"
_description = "Batch Transfer"
_order = "name desc"
name = fields.Char(
string='Batch Transfer', default='New',
copy=False, required=True, readonly=True,
help='Name of the batch transfer')
user_id = fields.Many2one(
'res.users', string='Responsible', tracking=True, check_company=True,
readonly=True, states={'draft': [('readonly', False)], 'in_progress': [('readonly', False)]},
help='Person responsible for this batch transfer')
company_id = fields.Many2one(
'res.company', string="Company", required=True, readonly=True,
index=True, default=lambda self: self.env.company)
picking_ids = fields.One2many(
'stock.picking', 'batch_id', string='Transfers', readonly=True,
domain="[('id', 'in', allowed_picking_ids)]", check_company=True,
states={'draft': [('readonly', False)], 'in_progress': [('readonly', False)]},
help='List of transfers associated to this batch')
show_check_availability = fields.Boolean(
compute='_compute_move_ids',
help='Technical field used to compute whether the check availability button should be shown.')
allowed_picking_ids = fields.One2many('stock.picking', compute='_compute_allowed_picking_ids')
move_ids = fields.One2many(
'stock.move', string="Stock moves", compute='_compute_move_ids')
move_line_ids = fields.One2many(
'stock.move.line', string='Stock move lines',
compute='_compute_move_ids', inverse='_set_move_line_ids', readonly=True,
states={'draft': [('readonly', False)], 'in_progress': [('readonly', False)]})
state = fields.Selection([
('draft', 'Draft'),
('in_progress', 'In progress'),
('done', 'Done'),
('cancel', 'Cancelled')], default='draft',
store=True, compute='_compute_state',
copy=False, tracking=True, required=True, readonly=True)
picking_type_id = fields.Many2one(
'stock.picking.type', 'Operation Type', check_company=True, copy=False,
readonly=True, states={'draft': [('readonly', False)]})
scheduled_date = fields.Datetime(
'Scheduled Date', copy=False, store=True, readonly=False, compute="_compute_scheduled_date",
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
help="""Scheduled date for the transfers to be processed.
- If manually set then scheduled date for all transfers in batch will automatically update to this date.
- If not manually changed and transfers are added/removed/updated then this will be their earliest scheduled date
but this scheduled date will not be set for all transfers in batch.""")
@api.depends('company_id', 'picking_type_id', 'state')
def _compute_allowed_picking_ids(self):
allowed_picking_states = ['waiting', 'confirmed', 'assigned']
cancelled_batchs = self.env['stock.picking.batch'].search_read(
[('state', '=', 'cancel')], ['id']
)
cancelled_batch_ids = [batch['id'] for batch in cancelled_batchs]
for batch in self:
domain_states = list(allowed_picking_states)
# Allows to add draft pickings only if batch is in draft as well.
if batch.state == 'draft':
domain_states.append('draft')
domain = [
('company_id', '=', batch.company_id.id),
('immediate_transfer', '=', False),
('state', 'in', domain_states),
'|',
'|',
('batch_id', '=', False),
('batch_id', '=', batch.id),
('batch_id', 'in', cancelled_batch_ids),
]
if batch.picking_type_id:
domain += [('picking_type_id', '=', batch.picking_type_id.id)]
batch.allowed_picking_ids = self.env['stock.picking'].search(domain)
@api.depends('picking_ids', 'picking_ids.move_line_ids', 'picking_ids.move_lines', 'picking_ids.move_lines.state')
def _compute_move_ids(self):
for batch in self:
batch.move_ids = batch.picking_ids.move_lines
batch.move_line_ids = batch.picking_ids.move_line_ids
batch.show_check_availability = any(m.state not in ['assigned', 'done'] for m in batch.move_ids)
@api.depends('picking_ids', 'picking_ids.state')
def _compute_state(self):
batchs = self.filtered(lambda batch: batch.state not in ['cancel', 'done'])
for batch in batchs:
if not batch.picking_ids:
return
# Cancels automatically the batch picking if all its transfers are cancelled.
if all(picking.state == 'cancel' for picking in batch.picking_ids):
batch.state = 'cancel'
# Batch picking is marked as done if all its not canceled transfers are done.
elif all(picking.state in ['cancel', 'done'] for picking in batch.picking_ids):
batch.state = 'done'
@api.depends('picking_ids', 'picking_ids.scheduled_date')
def _compute_scheduled_date(self):
self.scheduled_date = min(self.picking_ids.filtered('scheduled_date').mapped('scheduled_date'), default=False)
@api.onchange('scheduled_date')
def onchange_scheduled_date(self):
if self.scheduled_date:
self.picking_ids.scheduled_date = self.scheduled_date
def _set_move_line_ids(self):
new_move_lines = self[0].move_line_ids
for picking in self.picking_ids:
old_move_lines = picking.move_line_ids
picking.move_line_ids = new_move_lines.filtered(lambda ml: ml.picking_id.id == picking.id)
move_lines_to_unlink = old_move_lines - new_move_lines
if move_lines_to_unlink:
move_lines_to_unlink.unlink()
# -------------------------------------------------------------------------
# CRUD
# -------------------------------------------------------------------------
@api.model
def create(self, vals):
if vals.get('name', '/') == '/':
vals['name'] = self.env['ir.sequence'].next_by_code('picking.batch') or '/'
return super().create(vals)
def write(self, vals):
res = super().write(vals)
if vals.get('picking_type_id'):
self._sanity_check()
if vals.get('picking_ids'):
batch_without_picking_type = self.filtered(lambda batch: not batch.picking_type_id)
if batch_without_picking_type:
picking = self.picking_ids and self.picking_ids[0]
batch_without_picking_type.picking_type_id = picking.picking_type_id.id
return res
def unlink(self):
if any(batch.state != 'draft' for batch in self):
raise UserError(_("You can only delete draft batch transfers."))
return super().unlink()
def onchange(self, values, field_name, field_onchange):
"""Override onchange to NOT to update all scheduled_date on pickings when
scheduled_date on batch is updated by the change of scheduled_date on pickings.
"""
result = super().onchange(values, field_name, field_onchange)
if field_name == 'picking_ids' and 'value' in result:
for line in result['value'].get('picking_ids', []):
if line[0] < 2 and 'scheduled_date' in line[2]:
del line[2]['scheduled_date']
return result
# -------------------------------------------------------------------------
# Action methods
# -------------------------------------------------------------------------
def action_confirm(self):
"""Sanity checks, confirm the pickings and mark the batch as confirmed."""
self.ensure_one()
if not self.picking_ids:
raise UserError(_("You have to set some pickings to batch."))
self.picking_ids.action_confirm()
self._check_company()
self.state = 'in_progress'
return True
def action_cancel(self):
self.ensure_one()
self.state = 'cancel'
return True
def action_print(self):
self.ensure_one()
return self.env.ref('stock_picking_batch.action_report_picking_batch').report_action(self)
def action_done(self):
self.ensure_one()
self._check_company()
pickings = self.mapped('picking_ids').filtered(lambda picking: picking.state not in ('cancel', 'done'))
if any(picking.state not in ('assigned', 'confirmed') for picking in pickings):
raise UserError(_('Some transfers are still waiting for goods. Please check or force their availability before setting this batch to done.'))
for picking in pickings:
picking.message_post(
body="<b>%s:</b> %s <a href=#id=%s&view_type=form&model=stock.picking.batch>%s</a>" % (
_("Transferred by"),
_("Batch Transfer"),
picking.batch_id.id,
picking.batch_id.name))
return pickings.button_validate()
def action_assign(self):
self.ensure_one()
self.picking_ids.action_assign()
def action_put_in_pack(self):
""" Action to put move lines with 'Done' quantities into a new pack
This method follows same logic to stock.picking.
"""
self.ensure_one()
if self.state not in ('done', 'cancel'):
picking_move_lines = self.move_line_ids
move_line_ids = picking_move_lines.filtered(lambda ml:
float_compare(ml.qty_done, 0.0, precision_rounding=ml.product_uom_id.rounding) > 0
and not ml.result_package_id
)
if not move_line_ids:
move_line_ids = picking_move_lines.filtered(lambda ml: float_compare(ml.product_uom_qty, 0.0,
precision_rounding=ml.product_uom_id.rounding) > 0 and float_compare(ml.qty_done, 0.0,
precision_rounding=ml.product_uom_id.rounding) == 0)
if move_line_ids:
res = self.picking_ids[0]._pre_put_in_pack_hook(move_line_ids)
if not res:
res = self.picking_ids[0]._put_in_pack(move_line_ids, False)
return res
else:
raise UserError(_("Please add 'Done' quantities to the batch picking to create a new pack."))
# -------------------------------------------------------------------------
# Miscellaneous
# -------------------------------------------------------------------------
def _sanity_check(self):
for batch in self:
if not batch.picking_ids <= batch.allowed_picking_ids:
erroneous_pickings = batch.picking_ids - batch.allowed_picking_ids
raise UserError(_(
"The following transfers cannot be added to batch transfer %s. "
"Please check their states and operation types, if they aren't immediate "
"transfers or if they're not already part of another batch transfer.\n\n"
"Incompatibilities: %s", batch.name, ', '.join(erroneous_pickings.mapped('name'))))
def _track_subtype(self, init_values):
if 'state' in init_values:
return self.env.ref('stock_picking_batch.mt_batch_state')
return super()._track_subtype(init_values)
|