diff --git a/addons/delivery/models/stock_picking.py b/addons/delivery/models/stock_picking.py
index e8d37aa0cec..0f27e75c639 100644
--- a/addons/delivery/models/stock_picking.py
+++ b/addons/delivery/models/stock_picking.py
@@ -98,6 +98,7 @@ class StockPicking(models.Model):
@api.multi
def do_transfer(self):
+ # TDE FIXME: should work in batch
self.ensure_one()
res = super(StockPicking, self).do_transfer()
@@ -111,6 +112,7 @@ class StockPicking(models.Model):
@api.multi
def put_in_pack(self):
+ # TDE FIXME: work in batch, please
self.ensure_one()
package_id = super(StockPicking, self).put_in_pack()
package = self.env['stock.quant.package'].browse(package_id)
diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py
index 467fac5dbb7..c5287e7c556 100644
--- a/addons/stock/models/stock_picking.py
+++ b/addons/stock/models/stock_picking.py
@@ -1,42 +1,76 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from collections import namedtuple
import json
import time
-from openerp.osv import fields, osv
-from openerp.tools.float_utils import float_compare
-from openerp.tools.translate import _
-from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
-from openerp import api, models
-from openerp.addons.procurement import procurement
-from openerp.exceptions import UserError
+from odoo import api, fields, models, _
+from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
+from odoo.tools.float_utils import float_compare
+from odoo.addons.procurement import procurement
+from odoo.exceptions import UserError
-class stock_picking_type(osv.osv):
+class PickingType(models.Model):
_name = "stock.picking.type"
_description = "The picking type determines the picking view"
_order = 'sequence'
- def _get_tristate_values(self, cr, uid, ids, field_name, arg, context=None):
- picking_obj = self.pool.get('stock.picking')
- res = {}
- for picking_type_id in ids:
- #get last 10 pickings of this type
- picking_ids = picking_obj.search(cr, uid, [('picking_type_id', '=', picking_type_id), ('state', '=', 'done')], order='date_done desc', limit=10, context=context)
- tristates = []
- for picking in picking_obj.browse(cr, uid, picking_ids, context=context):
- if picking.date_done > picking.date:
- tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('Late'), 'value': -1})
- elif picking.backorder_id:
- tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('Backorder exists'), 'value': 0})
- else:
- tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('OK'), 'value': 1})
- res[picking_type_id] = json.dumps(tristates)
- return res
+ name = fields.Char('Picking Type Name', required=True, translate=True)
+ color = fields.Integer('Color')
+ sequence = fields.Integer('Sequence', help="Used to order the 'All Operations' kanban view")
+ sequence_id = fields.Many2one('ir.sequence', 'Reference Sequence', required=True)
+ default_location_src_id = fields.Many2one(
+ 'stock.location', 'Default Source Location',
+ help="This is the default source location when you create a picking manually with this picking type. It is possible however to change it or that the routes put another location. If it is empty, it will check for the supplier location on the partner. ")
+ default_location_dest_id = fields.Many2one(
+ 'stock.location', 'Default Destination Location',
+ help="This is the default destination location when you create a picking manually with this picking type. It is possible however to change it or that the routes put another location. If it is empty, it will check for the customer location on the partner. ")
+ code = fields.Selection([('incoming', 'Vendors'), ('outgoing', 'Customers'), ('internal', 'Internal')], 'Type of Operation', required=True)
+ return_picking_type_id = fields.Many2one('stock.picking.type', 'Picking Type for Returns')
+ show_entire_packs = fields.Boolean('Allow moving packs', help="If checked, this shows the packs to be moved as a whole in the Operations tab all the time, even if there was no entire pack reserved.")
+ warehouse_id = fields.Many2one(
+ 'stock.warehouse', 'Warehouse', ondelete='cascade',
+ default=lambda self: self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1))
+ active = fields.Boolean('Active', default=True)
+ use_create_lots = fields.Boolean(
+ 'Create New Lots', default=True,
+ help="If this is checked only, it will suppose you want to create new Serial Numbers / Lots, so you can provide them in a text field. ")
+ use_existing_lots = fields.Boolean(
+ 'Use Existing Lots', default=True,
+ help="If this is checked, you will be able to choose the Serial Number / Lots. You can also decide to not put lots in this picking type. This means it will create stock with no lot or not put a restriction on the lot taken. ")
- def _get_picking_count(self, cr, uid, ids, field_names, arg, context=None):
- obj = self.pool.get('stock.picking')
+ # Statistics for the kanban view
+ last_done_picking = fields.Char('Last 10 Done Pickings', compute='_compute_last_done_picking')
+ count_picking_draft = fields.Integer(compute='_compute_picking_count')
+ count_picking_ready = fields.Integer(compute='_compute_picking_count')
+ count_picking = fields.Integer(compute='_compute_picking_count')
+ count_picking_waiting = fields.Integer(compute='_compute_picking_count')
+ count_picking_late = fields.Integer(compute='_compute_picking_count')
+ count_picking_backorders = fields.Integer(compute='_compute_picking_count')
+ rate_picking_late = fields.Integer(compute='_compute_picking_count')
+ rate_picking_backorders = fields.Integer(compute='_compute_picking_count')
+
+ barcode_nomenclature_id = fields.Many2one(
+ 'barcode.nomenclature', 'Barcode Nomenclature')
+
+ @api.one
+ def _compute_last_done_picking(self):
+ # TDE TODO: true multi
+ tristates = []
+ for picking in self.env['stock.picking'].search([('picking_type_id', '=', self.id), ('state', '=', 'done')], order='date_done desc', limit=10):
+ if picking.date_done > picking.date:
+ tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('Late'), 'value': -1})
+ elif picking.backorder_id:
+ tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('Backorder exists'), 'value': 0})
+ else:
+ tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('OK'), 'value': 1})
+ self.last_done_picking = json.dumps(tristates)
+
+ @api.multi
+ def _compute_picking_count(self):
+ # TDE TODO count picking can be done using previous two
domains = {
'count_picking_draft': [('state', '=', 'draft')],
'count_picking_waiting': [('state', 'in', ('confirmed', 'waiting'))],
@@ -45,192 +79,320 @@ class stock_picking_type(osv.osv):
'count_picking_late': [('min_date', '<', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)), ('state', 'in', ('assigned', 'waiting', 'confirmed', 'partially_available'))],
'count_picking_backorders': [('backorder_id', '!=', False), ('state', 'in', ('confirmed', 'assigned', 'waiting', 'partially_available'))],
}
- result = {}
for field in domains:
- data = obj.read_group(cr, uid, domains[field] +
- [('state', 'not in', ('done', 'cancel')), ('picking_type_id', 'in', ids)],
- ['picking_type_id'], ['picking_type_id'], context=context)
+ data = self.env['stock.picking'].read_group(domains[field] +
+ [('state', 'not in', ('done', 'cancel')), ('picking_type_id', 'in', self.ids)],
+ ['picking_type_id'], ['picking_type_id'])
count = dict(map(lambda x: (x['picking_type_id'] and x['picking_type_id'][0], x['picking_type_id_count']), data))
- for tid in ids:
- result.setdefault(tid, {})[field] = count.get(tid, 0)
- for tid in ids:
- if result[tid]['count_picking']:
- result[tid]['rate_picking_late'] = result[tid]['count_picking_late'] * 100 / result[tid]['count_picking']
- result[tid]['rate_picking_backorders'] = result[tid]['count_picking_backorders'] * 100 / result[tid]['count_picking']
- else:
- result[tid]['rate_picking_late'] = 0
- result[tid]['rate_picking_backorders'] = 0
- return result
+ for record in self:
+ record[field] = count.get(record.id, 0)
+ for record in self:
+ record.rate_picking_late = record.count_picking and record.count_picking_late * 100 / record.count_picking or 0
+ record.rate_picking_backorders = record.count_picking and record.count_picking_backorders * 100 / record.count_picking or 0
- def _get_action(self, cr, uid, ids, action, context=None):
- mod_obj = self.pool.get('ir.model.data')
- act_obj = self.pool.get('ir.actions.act_window')
- result = mod_obj.xmlid_to_res_id(cr, uid, action, raise_if_not_found=True)
- result = act_obj.read(cr, uid, [result], context=context)[0]
- if ids:
- picking_type = self.browse(cr, uid, ids[0], context=context)
- result['display_name'] = picking_type.display_name
- return result
-
- def get_action_picking_tree_late(self, cr, uid, ids, context=None):
- return self._get_action(cr, uid, ids, 'stock.action_picking_tree_late', context=context)
-
- def get_action_picking_tree_backorder(self, cr, uid, ids, context=None):
- return self._get_action(cr, uid, ids, 'stock.action_picking_tree_backorder', context=context)
-
- def get_action_picking_tree_waiting(self, cr, uid, ids, context=None):
- return self._get_action(cr, uid, ids, 'stock.action_picking_tree_waiting', context=context)
-
- def get_action_picking_tree_ready(self, cr, uid, ids, context=None):
- return self._get_action(cr, uid, ids, 'stock.action_picking_tree_ready', context=context)
-
- def get_stock_picking_action_picking_type(self, cr, uid, ids, context=None):
- return self._get_action(cr, uid, ids, 'stock.stock_picking_action_picking_type', context=context)
-
-
- def onchange_picking_code(self, cr, uid, ids, picking_code=False):
- if not picking_code:
- return False
-
- obj_data = self.pool.get('ir.model.data')
- stock_loc = obj_data.xmlid_to_res_id(cr, uid, 'stock.stock_location_stock')
-
- result = {
- 'default_location_src_id': stock_loc,
- 'default_location_dest_id': stock_loc,
- }
- if picking_code == 'incoming':
- result['default_location_src_id'] = obj_data.xmlid_to_res_id(cr, uid, 'stock.stock_location_suppliers')
- elif picking_code == 'outgoing':
- result['default_location_dest_id'] = obj_data.xmlid_to_res_id(cr, uid, 'stock.stock_location_customers')
- return {'value': result}
-
- def name_get(self, cr, uid, ids, context=None):
- """Overides orm name_get method to display 'Warehouse_name: PickingType_name' """
- if context is None:
- context = {}
- if not isinstance(ids, list):
- ids = [ids]
+ @api.multi
+ def name_get(self):
+ """ Display 'Warehouse_name: PickingType_name' """
+ # TDE TODO remove context key support + update purchase
res = []
- if not ids:
- return res
- for record in self.browse(cr, uid, ids, context=context):
- name = record.name
- if record.warehouse_id:
- name = record.warehouse_id.name + ': ' +name
- if context.get('special_shortened_wh_name'):
- if record.warehouse_id:
- name = record.warehouse_id.name
+ for picking_type in self:
+ if self.env.context.get('special_shortened_wh_name'):
+ if picking_type.warehouse_id:
+ name = picking_type.warehouse_id.name
else:
- name = _('Customer') + ' (' + record.name + ')'
- res.append((record.id, name))
+ name = _('Customer') + ' (' + picking_type.name + ')'
+ elif picking_type.warehouse_id:
+ name = picking_type.warehouse_id.name + ': ' + picking_type.name
+ else:
+ name = picking_type.name
+ res.append((picking_type.id, name))
return res
- def _default_warehouse(self, cr, uid, context=None):
- user = self.pool.get('res.users').browse(cr, uid, uid, context)
- res = self.pool.get('stock.warehouse').search(cr, uid, [('company_id', '=', user.company_id.id)], limit=1, context=context)
- return res and res[0] or False
+ @api.onchange('code')
+ def onchange_picking_code(self):
+ if self.code == 'incoming':
+ self.default_location_src_id = self.env.ref('stock_location_suppliers').id
+ self.default_location_dest_id = self.env.ref('stock_location_stock').id
+ elif self.code == 'outgoing':
+ self.default_location_src_id = self.env.ref('stock_location_stock').id
+ self.default_location_dest_id = self.env.ref('stock_location_customers').id
- _columns = {
- 'name': fields.char('Picking Type Name', translate=True, required=True),
- 'color': fields.integer('Color'),
- 'sequence': fields.integer('Sequence', help="Used to order the 'All Operations' kanban view"),
- 'sequence_id': fields.many2one('ir.sequence', 'Reference Sequence', required=True),
- 'default_location_src_id': fields.many2one('stock.location', 'Default Source Location', help="This is the default source location when you create a picking manually with this picking type. It is possible however to change it or that the routes put another location. If it is empty, it will check for the supplier location on the partner. "),
- 'default_location_dest_id': fields.many2one('stock.location', 'Default Destination Location', help="This is the default destination location when you create a picking manually with this picking type. It is possible however to change it or that the routes put another location. If it is empty, it will check for the customer location on the partner. "),
- 'code': fields.selection([('incoming', 'Vendors'), ('outgoing', 'Customers'), ('internal', 'Internal')], 'Type of Operation', required=True),
- 'return_picking_type_id': fields.many2one('stock.picking.type', 'Picking Type for Returns'),
- 'show_entire_packs': fields.boolean('Allow moving packs', help="If checked, this shows the packs to be moved as a whole in the Operations tab all the time, even if there was no entire pack reserved."),
- 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', ondelete='cascade'),
- 'active': fields.boolean('Active'),
- 'use_create_lots': fields.boolean('Create New Lots', help="If this is checked only, it will suppose you want to create new Serial Numbers / Lots, so you can provide them in a text field. "),
- 'use_existing_lots': fields.boolean('Use Existing Lots', help="If this is checked, you will be able to choose the Serial Number / Lots. You can also decide to not put lots in this picking type. This means it will create stock with no lot or not put a restriction on the lot taken. "),
+ @api.multi
+ def _get_action(self, action_xmlid):
+ # TDE TODO check to have one view + custo in methods
+ action = self.env.ref(action_xmlid).read()[0]
+ if self:
+ action['display_name'] = self.display_name
+ return action
- # Statistics for the kanban view
- 'last_done_picking': fields.function(_get_tristate_values,
- type='char',
- string='Last 10 Done Pickings'),
+ @api.multi
+ def get_action_picking_tree_late(self):
+ return self._get_action('stock.action_picking_tree_late')
- 'count_picking_draft': fields.function(_get_picking_count,
- type='integer', multi='_get_picking_count'),
- 'count_picking_ready': fields.function(_get_picking_count,
- type='integer', multi='_get_picking_count'),
- 'count_picking': fields.function(_get_picking_count,
- type='integer', multi='_get_picking_count'),
- 'count_picking_waiting': fields.function(_get_picking_count,
- type='integer', multi='_get_picking_count'),
- 'count_picking_late': fields.function(_get_picking_count,
- type='integer', multi='_get_picking_count'),
- 'count_picking_backorders': fields.function(_get_picking_count,
- type='integer', multi='_get_picking_count'),
+ @api.multi
+ def get_action_picking_tree_backorder(self):
+ return self._get_action('stock.action_picking_tree_backorder')
- 'rate_picking_late': fields.function(_get_picking_count,
- type='integer', multi='_get_picking_count'),
- 'rate_picking_backorders': fields.function(_get_picking_count,
- type='integer', multi='_get_picking_count'),
+ @api.multi
+ def get_action_picking_tree_waiting(self):
+ return self._get_action('stock.action_picking_tree_waiting')
- # Barcode nomenclature
- 'barcode_nomenclature_id': fields.many2one('barcode.nomenclature','Barcode Nomenclature', help='A barcode nomenclature'),
- }
+ @api.multi
+ def get_action_picking_tree_ready(self):
+ return self._get_action('stock.action_picking_tree_ready')
- _defaults = {
- 'warehouse_id': _default_warehouse,
- 'active': True,
- 'use_existing_lots': True,
- 'use_create_lots': True,
- }
+ @api.multi
+ def get_stock_picking_action_picking_type(self):
+ return self._get_action('stock.stock_picking_action_picking_type')
-class stock_picking(models.Model):
+class Picking(models.Model):
_name = "stock.picking"
_inherit = ['mail.thread']
_description = "Transfer"
_order = "priority desc, date asc, id desc"
- def _set_min_date(self, cr, uid, id, field, value, arg, context=None):
- move_obj = self.pool.get("stock.move")
- if value:
- move_ids = [move.id for move in self.browse(cr, uid, id, context=context).move_lines]
- move_obj.write(cr, uid, move_ids, {'date_expected': value}, context=context)
+ name = fields.Char(
+ 'Reference', default='/',
+ copy=False, index=True,
+ states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
+ origin = fields.Char(
+ 'Source Document', index=True,
+ states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
+ help="Reference of the document")
+ note = fields.Text('Notes')
- def _set_priority(self, cr, uid, id, field, value, arg, context=None):
- move_obj = self.pool.get("stock.move")
- if value:
- move_ids = [move.id for move in self.browse(cr, uid, id, context=context).move_lines]
- move_obj.write(cr, uid, move_ids, {'priority': value}, context=context)
+ backorder_id = fields.Many2one(
+ 'stock.picking', 'Back Order of',
+ copy=False, index=True,
+ states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
+ help="If this shipment was split, then this field links to the shipment which contains the already processed part.")
- def get_min_max_date(self, cr, uid, ids, field_name, arg, context=None):
- """ Finds minimum and maximum dates for picking.
- @return: Dictionary of values
- """
- res = {}
- for id in ids:
- res[id] = {'min_date': False, 'max_date': False, 'priority': '1'}
- if not ids:
- return res
- cr.execute("""select
- picking_id,
- min(date_expected),
- max(date_expected),
- max(priority)
- from
- stock_move
- where
- picking_id IN %s
- group by
- picking_id""", (tuple(ids),))
- for pick, dt1, dt2, prio in cr.fetchall():
- res[pick]['min_date'] = dt1
- res[pick]['max_date'] = dt2
- res[pick]['priority'] = prio
- return res
+ move_type = fields.Selection([
+ ('direct', 'Partial'), ('one', 'All at once')], 'Delivery Type',
+ default='direct', required=True,
+ states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
+ help="It specifies goods to be deliver partially or all at once")
- def create(self, cr, user, vals, context=None):
- context = context or {}
- if ('name' not in vals) or (vals.get('name') in ('/', False)):
- ptype_id = vals.get('picking_type_id', context.get('default_picking_type_id', False))
- sequence_id = self.pool.get('stock.picking.type').browse(cr, user, ptype_id, context=context).sequence_id.id
- vals['name'] = self.pool.get('ir.sequence').next_by_id(cr, user, sequence_id, context=context)
+ state = fields.Selection([
+ ('draft', 'Draft'), ('cancel', 'Cancelled'),
+ ('waiting', 'Waiting Another Operation'),
+ ('confirmed', 'Waiting Availability'),
+ ('partially_available', 'Partially Available'),
+ ('assigned', 'Available'), ('done', 'Done')], string='Status', compute='_compute_state',
+ copy=False, index=True, readonly=True, store=True, track_visibility='onchange',
+ help=" * Draft: not confirmed yet and will not be scheduled until confirmed\n"
+ " * Waiting Another Operation: waiting for another move to proceed before it becomes automatically available (e.g. in Make-To-Order flows)\n"
+ " * Waiting Availability: still waiting for the availability of products\n"
+ " * Partially Available: some products are available and reserved\n"
+ " * Ready to Transfer: products reserved, simply waiting for confirmation.\n"
+ " * Transferred: has been processed, can't be modified or cancelled anymore\n"
+ " * Cancelled: has been cancelled, can't be confirmed anymore")
+
+ group_id = fields.Many2one(
+ 'procurement.group', 'Procurement Group',
+ readonly=True, related='move_lines.group_id', store=True)
+
+ priority = fields.Selection(
+ procurement.PROCUREMENT_PRIORITIES, string='Priority',
+ compute='_compute_priority', inverse='_set_priority', store=True,
+ # default='1', required=True, # TDE: required, depending on moves ? strange
+ index=True, track_visibility='onchange',
+ states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
+ help="Priority for this picking. Setting manually a value here would set it as priority for all the moves")
+ min_date = fields.Datetime(
+ 'Scheduled Date', compute='_compute_dates', inverse='_set_min_date', store=True,
+ index=True, track_visibility='onchange',
+ states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
+ help="Scheduled time for the first part of the shipment to be processed. Setting manually a value here would set it as expected date for all the stock moves.")
+ max_date = fields.Datetime(
+ 'Max. Expected Date', compute='_compute_dates', store=True,
+ index=True,
+ help="Scheduled time for the last part of the shipment to be processed")
+ date = fields.Datetime(
+ 'Creation Date',
+ default=fields.Datetime.now, index=True, track_visibility='onchange',
+ states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
+ help="Creation Date, usually the time of the order")
+ date_done = fields.Datetime('Date of Transfer', copy=False, readonly=True, help="Completion Date of Transfer")
+
+ location_id = fields.Many2one(
+ 'stock.location', "Source Location Zone",
+ default=lambda self: self.env['stock.picking.type'].browse(self._context.get('default_picking_type_id')).default_location_src_id,
+ readonly=True, required=True,
+ states={'draft': [('readonly', False)]})
+ location_dest_id = fields.Many2one(
+ 'stock.location', "Destination Location Zone",
+ default=lambda self: self.env['stock.picking.type'].browse(self._context.get('default_picking_type_id')).default_location_dest_id,
+ readonly=True, required=True,
+ states={'draft': [('readonly', False)]})
+ move_lines = fields.One2many('stock.move', 'picking_id', string="Stock Moves", copy=True)
+ picking_type_id = fields.Many2one(
+ 'stock.picking.type', 'Picking Type',
+ required=True,
+ states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
+ picking_type_code = fields.Selection([
+ ('incoming', 'Vendors'),
+ ('outgoing', 'Customers'),
+ ('internal', 'Internal')], related='picking_type_id.code')
+ picking_type_entire_packs = fields.Boolean(related='picking_type_id.show_entire_packs')
+
+ quant_reserved_exist = fields.Boolean(
+ 'Has quants already reserved', compute='_compute_quant_reserved_exist',
+ help='Check the existance of quants linked to this picking')
+
+ partner_id = fields.Many2one(
+ 'res.partner', 'Partner',
+ states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
+ company_id = fields.Many2one(
+ 'res.company', 'Company',
+ default=lambda self: self.env['res.company']._company_default_get('stock.picking'),
+ index=True, required=True,
+ states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
+ # TDE FIXME: separate those two kind of pack operations
+ pack_operation_ids = fields.One2many(
+ 'stock.pack.operation', 'picking_id', 'Related Packing Operations',
+ states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
+ pack_operation_product_ids = fields.One2many(
+ 'stock.pack.operation', 'picking_id', 'Non pack',
+ domain=[('product_id', '!=', False)],
+ states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
+ pack_operation_pack_ids = fields.One2many(
+ 'stock.pack.operation', 'picking_id', 'Pack',
+ domain=[('product_id', '=', False)],
+ states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
+
+ pack_operation_exist = fields.Boolean(
+ 'Has Pack Operations', compute='_compute_pack_operation_exist',
+ help='Check the existence of pack operation on the picking')
+
+ owner_id = fields.Many2one(
+ 'res.partner', 'Owner',
+ states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
+ help="Default Owner")
+ printed = fields.Boolean('Printed')
+ # Used to search on pickings
+ product_id = fields.Many2one('product.product', 'Product', related='move_lines.product_id')
+ recompute_pack_op = fields.Boolean(
+ 'Recompute pack operation?', copy=False,
+ help='True if reserved quants changed, which mean we might need to recompute the package operations')
+ launch_pack_operations = fields.Boolean("Launch Pack Operations", copy=False)
+
+ _sql_constraints = [
+ ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per company!'),
+ ]
+
+ @api.depends('move_type', 'launch_pack_operations', 'move_lines.state', 'move_lines.picking_id', 'move_lines.partially_available')
+ @api.one
+ def _compute_state(self):
+ ''' State of a picking depends on the state of its related stock.move
+ - no moves: draft or assigned (launch_pack_operations)
+ - all moves canceled: cancel
+ - all moves done (including possible canceled): done
+ - All at once picking: least of confirmed / waiting / assigned
+ - Partial picking
+ - all moves assigned: assigned
+ - one of the move is assigned or partially available: partially available
+ - otherwise in waiting or confirmed state
+ '''
+ if not self.move_lines and self.launch_pack_operations:
+ self.state = 'assigned'
+ elif not self.move_lines:
+ self.state = 'draft'
+ elif any(move.state == 'draft' for move in self.move_lines): # TDE FIXME: should be all ?
+ self.state = 'draft'
+ elif all(move.state == 'cancel' for move in self.move_lines):
+ self.state = 'cancel'
+ elif all(move.state in ['cancel', 'done'] for move in self.move_lines):
+ self.state = 'done'
+ elif self.move_type == 'one':
+ ordered_moves = self.move_lines.filtered(
+ lambda move: move.state not in ['cancel', 'done']
+ ).sorted(
+ key=lambda move: (move.state == 'assigned' and 2) or (move.state == 'waiting' and 1) or 0, reverse=False
+ )
+ self.state = ordered_moves[0].state
+ else:
+ filtered_moves = self.move_lines.filtered(lambda move: move.state not in ['cancel', 'done'])
+ if not all(move.state == 'assigned' for move in filtered_moves) and any(move.state == 'assigned' for move in filtered_moves):
+ self.state = 'partially_available'
+ elif any(move.partially_available for move in filtered_moves):
+ self.state = 'partially_available'
+ else:
+ ordered_moves = filtered_moves.sorted(key=lambda move: (move.state == 'assigned' and 2) or (move.state == 'waiting' and 1) or 0, reverse=True)
+ self.state = ordered_moves[0].state
+
+ @api.one
+ @api.depends('move_lines.priority')
+ def _compute_priority(self):
+ self.priority = self.mapped('move_lines') and max(self.mapped('move_lines').mapped('priority')) or '1'
+
+ @api.one
+ def _set_priority(self):
+ self.move_lines.write({'priority': self.priority})
+
+ @api.one
+ @api.depends('move_lines.date_expected')
+ def _compute_dates(self):
+ self.min_date = min(self.move_lines.mapped('date_expected') or [False])
+ self.max_date = max(self.move_lines.mapped('date_expected') or [False])
+
+ @api.one
+ def _set_min_date(self):
+ self.move_lines.write({'date_expected': self.min_date})
+
+ @api.one
+ def _compute_quant_reserved_exist(self):
+ # TDE TODO: chould probably be cleaned with a search in quants
+ self.quant_reserved_exist = any(move.reserved_quant_ids for move in self.mapped('move_lines'))
+
+ @api.one
+ def _compute_pack_operation_exist(self):
+ self.pack_operation_exist = bool(self.pack_operation_ids)
+
+ @api.onchange('picking_type_id', 'partner_id')
+ def onchange_picking_type(self):
+ if self.picking_type_id:
+ if self.picking_type_id.default_location_src_id:
+ location_id = self.picking_type_id.default_location_src_id.id
+ elif self.partner_id:
+ location_id = self.partner_id.property_stock_supplier.id
+ else:
+ customerloc, location_id = self.env['stock.warehouse']._get_partner_locations()
+
+ if self.picking_type_id.default_location_dest_id:
+ location_dest_id = self.picking_type_id.default_location_dest_id.id
+ elif self.partner_id:
+ location_dest_id = self.partner_id.property_stock_customer.id
+ else:
+ customerloc, location_dest_id = self.env['stock.warehouse']._get_partner_locations()
+
+ self.location_id = location_id
+ self.location_dest_id = location_dest_id
+ # TDE CLEANME move into onchange_partner_id
+ if self.partner_id:
+ if self.partner_id.picking_warn == 'no-message' and self.partner_id.parent_id:
+ partner = self.partner_id.parent_id
+ elif self.partner_id.picking_warn not in ('no-message', 'block') and partner.parent_id.picking_warn == 'block':
+ partner = self.partner_id.parent_id
+ else:
+ partner = self.partner_id
+ if partner.picking_warn != 'no-message':
+ if partner.picking_warn == 'block':
+ self.partner_id = False
+ return {'warning': {
+ 'title': ("Warning for %s") % partner.name,
+ 'message': partner.picking_warn_msg
+ }}
+
+ @api.model
+ def create(self, vals):
+ # TDE FIXME: clean that brol
+ defaults = self.default_get(['name', 'picking_type_id'])
+ if vals.get('name', '/') == '/' and defaults.get('name', '/') == '/' and vals.get('picking_type_id', defaults.get('picking_type_id')):
+ vals['name'] = self.env['stock.picking.type'].browse(vals.get('picking_type_id', defaults.get('picking_type_id'))).sequence_id.next_by_id()
+
+ # TDE FIXME: what ?
# As the on_change in one2many list is WIP, we will overwrite the locations on the stock moves here
# As it is a create the format will be a list of (0, 0, dict)
if vals.get('move_lines') and vals.get('location_id') and vals.get('location_dest_id'):
@@ -238,381 +400,103 @@ class stock_picking(models.Model):
if len(move) == 3:
move[2]['location_id'] = vals['location_id']
move[2]['location_dest_id'] = vals['location_dest_id']
- return super(stock_picking, self).create(cr, user, vals, context)
+ return super(Picking, self).create(vals)
- def write(self, cr, uid, ids, vals, context=None):
- res = super(stock_picking, self).write(cr, uid, ids, vals, context=context)
+ @api.multi
+ def write(self, vals):
+ res = super(Picking, self).write(vals)
+ # Change locations of moves if those of the picking change
after_vals = {}
if vals.get('location_id'):
after_vals['location_id'] = vals['location_id']
if vals.get('location_dest_id'):
after_vals['location_dest_id'] = vals['location_dest_id']
- # Change locations of moves if those of the picking change
if after_vals:
- moves = []
- for pick in self.browse(cr, uid, ids, context=context):
- moves += [x.id for x in pick.move_lines if not x.scrapped]
- if moves:
- self.pool['stock.move'].write(cr, uid, moves, after_vals, context=context)
+ self.mapped('move_lines').filtered(lambda move: not move.scrapped).write(after_vals)
return res
- def _state_get(self, cr, uid, ids, field_name, arg, context=None):
- '''The state of a picking depends on the state of its related stock.move
- draft: the picking has no line or any one of the lines is draft
- done, draft, cancel: all lines are done / draft / cancel
- confirmed, waiting, assigned, partially_available depends on move_type (all at once or partial)
- '''
- res = {}
- for pick in self.browse(cr, uid, ids, context=context):
- if not pick.move_lines:
- res[pick.id] = pick.launch_pack_operations and 'assigned' or 'draft'
- continue
- if any([x.state == 'draft' for x in pick.move_lines]):
- res[pick.id] = 'draft'
- continue
- if all([x.state == 'cancel' for x in pick.move_lines]):
- res[pick.id] = 'cancel'
- continue
- if all([x.state in ('cancel', 'done') for x in pick.move_lines]):
- res[pick.id] = 'done'
- continue
+ @api.multi
+ def unlink(self):
+ self.mapped('move_lines').action_cancel().unlink()
+ return super(Picking, self).unlink()
- order = {'confirmed': 0, 'waiting': 1, 'assigned': 2}
- order_inv = {0: 'confirmed', 1: 'waiting', 2: 'assigned'}
- lst = [order[x.state] for x in pick.move_lines if x.state not in ('cancel', 'done')]
- if pick.move_type == 'one':
- res[pick.id] = order_inv[min(lst)]
- else:
- #we are in the case of partial delivery, so if all move are assigned, picking
- #should be assign too, else if one of the move is assigned, or partially available, picking should be
- #in partially available state, otherwise, picking is in waiting or confirmed state
- res[pick.id] = order_inv[max(lst)]
- if not all(x == 2 for x in lst):
- if any(x == 2 for x in lst):
- res[pick.id] = 'partially_available'
- else:
- #if all moves aren't assigned, check if we have one product partially available
- for move in pick.move_lines:
- if move.partially_available:
- res[pick.id] = 'partially_available'
- break
- return res
+ # Actions
+ # ----------------------------------------
- def _get_pickings(self, cr, uid, ids, context=None):
- res = set()
- for move in self.browse(cr, uid, ids, context=context):
- if move.picking_id:
- res.add(move.picking_id.id)
- return list(res)
+ @api.one
+ def action_assign_owner(self):
+ self.pack_operation_ids.write({'owner_id': self.owner_id.id})
- def _get_pickings_dates_priority(self, cr, uid, ids, context=None):
- res = set()
- for move in self.browse(cr, uid, ids, context=context):
- if move.picking_id and (not (move.picking_id.min_date < move.date_expected < move.picking_id.max_date) or move.priority > move.picking_id.priority):
- res.add(move.picking_id.id)
- return list(res)
+ @api.multi
+ def do_print_picking(self):
+ self.write({'printed': True})
+ return self.env["report"].get_action(self, 'stock.report_picking')
- def _get_pack_operation_exist(self, cr, uid, ids, field_name, arg, context=None):
- res = {}
- for pick in self.browse(cr, uid, ids, context=context):
- res[pick.id] = False
- if pick.pack_operation_ids:
- res[pick.id] = True
- return res
-
- def _get_quant_reserved_exist(self, cr, uid, ids, field_name, arg, context=None):
- res = {}
- for pick in self.browse(cr, uid, ids, context=context):
- res[pick.id] = False
- for move in pick.move_lines:
- if move.reserved_quant_ids:
- res[pick.id] = True
- continue
- return res
-
- def action_assign_owner(self, cr, uid, ids, context=None):
- for picking in self.browse(cr, uid, ids, context=context):
- packop_ids = [op.id for op in picking.pack_operation_ids]
- self.pool.get('stock.pack.operation').write(cr, uid, packop_ids, {'owner_id': picking.owner_id.id}, context=context)
-
- def onchange_picking_type(self, cr, uid, ids, picking_type_id, partner_id, context=None):
- res = {}
- if picking_type_id:
- picking_type = self.pool['stock.picking.type'].browse(cr, uid, picking_type_id, context=context)
- if not picking_type.default_location_src_id:
- if partner_id:
- partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
- location_id = partner.property_stock_supplier.id
- else:
- customerloc, supplierloc = self.pool['stock.warehouse']._get_partner_locations(cr, uid, [], context=context)
- location_id = supplierloc.id
- else:
- location_id = picking_type.default_location_src_id.id
-
- if not picking_type.default_location_dest_id:
- if partner_id:
- partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
- location_dest_id = partner.property_stock_customer.id
- else:
- customerloc, supplierloc = self.pool['stock.warehouse']._get_partner_locations(cr, uid, [], context=context)
- location_dest_id = customerloc.id
- else:
- location_dest_id = picking_type.default_location_dest_id.id
-
- res['value'] = {'location_id': location_id,
- 'location_dest_id': location_dest_id}
-
- if partner_id:
- warning = {}
- title = False
- message = False
- partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
-
- # If partner has no warning, check its company
- if partner.picking_warn == 'no-message' and partner.parent_id:
- partner = partner.parent_id
-
- if partner.picking_warn != 'no-message':
- # Block if partner only has warning but parent company is blocked
- if partner.picking_warn != 'block' and partner.parent_id and partner.parent_id.picking_warn == 'block':
- partner = partner.parent_id
- title = _("Warning for %s") % partner.name
- message = partner.picking_warn_msg
- warning = {
- 'title': title,
- 'message': message
- }
- if partner.picking_warn == 'block':
- return {'value': {'partner_id': False}, 'warning': warning}
- return {'warning': warning}
-
- return res
-
-
-
- def _default_location_destination(self):
- # retrieve picking type from context; if none this returns an empty recordset
- picking_type_id = self._context.get('default_picking_type_id')
- picking_type = self.env['stock.picking.type'].browse(picking_type_id)
- return picking_type.default_location_dest_id
-
- def _default_location_source(self):
- # retrieve picking type from context; if none this returns an empty recordset
- picking_type_id = self._context.get('default_picking_type_id')
- picking_type = self.env['stock.picking.type'].browse(picking_type_id)
- return picking_type.default_location_src_id
-
- _columns = {
- 'name': fields.char('Reference', select=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, copy=False),
- 'origin': fields.char('Source Document', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Reference of the document", select=True),
- 'backorder_id': fields.many2one('stock.picking', 'Back Order of', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="If this shipment was split, then this field links to the shipment which contains the already processed part.", select=True, copy=False),
- 'note': fields.text('Notes'),
- 'move_type': fields.selection([('direct', 'Partial'), ('one', 'All at once')], 'Delivery Type', required=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="It specifies goods to be deliver partially or all at once"),
- 'state': fields.function(_state_get, type="selection", copy=False,
- store={
- 'stock.picking': (lambda self, cr, uid, ids, ctx: ids, ['move_type', 'launch_pack_operations'], 20),
- 'stock.move': (_get_pickings, ['state', 'picking_id', 'partially_available'], 20)},
- selection=[
- ('draft', 'Draft'),
- ('cancel', 'Cancelled'),
- ('waiting', 'Waiting Another Operation'),
- ('confirmed', 'Waiting Availability'),
- ('partially_available', 'Partially Available'),
- ('assigned', 'Available'),
- ('done', 'Done'),
- ], string='Status', readonly=True, select=True, track_visibility='onchange',
- help="""
- * Draft: not confirmed yet and will not be scheduled until confirmed\n
- * Waiting Another Operation: waiting for another move to proceed before it becomes automatically available (e.g. in Make-To-Order flows)\n
- * Waiting Availability: still waiting for the availability of products\n
- * Partially Available: some products are available and reserved\n
- * Ready to Transfer: products reserved, simply waiting for confirmation.\n
- * Transferred: has been processed, can't be modified or cancelled anymore\n
- * Cancelled: has been cancelled, can't be confirmed anymore"""
- ),
- 'location_id': fields.many2one('stock.location', required=True, string="Source Location Zone",
- default=_default_location_source, readonly=True, states={'draft': [('readonly', False)]}),
- 'location_dest_id': fields.many2one('stock.location', required=True,string="Destination Location Zone",
- default=_default_location_destination, readonly=True, states={'draft': [('readonly', False)]}),
- 'move_lines': fields.one2many('stock.move', 'picking_id', string="Stock Moves", copy=True),
- 'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, required=True),
- 'picking_type_code': fields.related('picking_type_id', 'code', type='selection', selection=[('incoming', 'Vendors'), ('outgoing', 'Customers'), ('internal', 'Internal')]),
- 'picking_type_entire_packs': fields.related('picking_type_id', 'show_entire_packs', type='boolean'),
- 'priority': fields.function(get_min_max_date, multi="min_max_date", fnct_inv=_set_priority, type='selection', selection=procurement.PROCUREMENT_PRIORITIES, string='Priority',
- store={'stock.move': (_get_pickings_dates_priority, ['priority', 'picking_id'], 20)}, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, select=1, help="Priority for this picking. Setting manually a value here would set it as priority for all the moves",
- track_visibility='onchange', required=True),
- 'min_date': fields.function(get_min_max_date, multi="min_max_date", fnct_inv=_set_min_date,
- store={'stock.move': (_get_pickings_dates_priority, ['date_expected', 'picking_id'], 20)}, type='datetime', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, string='Scheduled Date', select=1, help="Scheduled time for the first part of the shipment to be processed. Setting manually a value here would set it as expected date for all the stock moves.", track_visibility='onchange'),
- 'max_date': fields.function(get_min_max_date, multi="min_max_date",
- store={'stock.move': (_get_pickings_dates_priority, ['date_expected', 'picking_id'], 20)}, type='datetime', string='Max. Expected Date', select=2, help="Scheduled time for the last part of the shipment to be processed"),
- 'date': fields.datetime('Creation Date', help="Creation Date, usually the time of the order", select=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, track_visibility='onchange'),
- 'date_done': fields.datetime('Date of Transfer', help="Completion Date of Transfer", readonly=True, copy=False),
- 'quant_reserved_exist': fields.function(_get_quant_reserved_exist, type='boolean', string='Has quants already reserved', help='Check the existance of quants linked to this picking'),
- 'partner_id': fields.many2one('res.partner', 'Partner', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
- 'company_id': fields.many2one('res.company', 'Company', required=True, select=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
- 'pack_operation_ids': fields.one2many('stock.pack.operation', 'picking_id', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, string='Related Packing Operations'),
- 'pack_operation_product_ids': fields.one2many('stock.pack.operation', 'picking_id', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, domain=[('product_id', '!=', False)], string='Non pack'),
- 'pack_operation_pack_ids': fields.one2many('stock.pack.operation', 'picking_id', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, domain=[('product_id', '=', False)], string='Pack'),
- # technical field for attrs in view
- 'pack_operation_exist': fields.function(_get_pack_operation_exist, type='boolean', string='Has Pack Operations', help='Check the existance of pack operation on the picking'),
- 'owner_id': fields.many2one('res.partner', 'Owner', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Default Owner"),
- 'printed': fields.boolean('Printed'),
- # Used to search on pickings
- 'product_id': fields.related('move_lines', 'product_id', type='many2one', relation='product.product', string='Product'),
- 'recompute_pack_op': fields.boolean('Recompute pack operation?', help='True if reserved quants changed, which mean we might need to recompute the package operations', copy=False),
- 'group_id': fields.related('move_lines', 'group_id', type='many2one', relation='procurement.group', string='Procurement Group', readonly=True,
- store={
- 'stock.picking': (lambda self, cr, uid, ids, ctx: ids, ['move_lines'], 10),
- 'stock.move': (_get_pickings, ['group_id', 'picking_id'], 10),
- }),
- 'launch_pack_operations': fields.boolean("Launch Pack Operations", copy=False),
- }
-
- _defaults = {
- 'name': '/',
- 'state': 'draft',
- 'move_type': 'direct',
- 'printed': False,
- 'priority': '1', # normal
- 'date': fields.datetime.now,
- 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.picking', context=c),
- 'recompute_pack_op': False,
- 'launch_pack_operations': False,
- }
- _sql_constraints = [
- ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per company!'),
- ]
-
- def do_print_picking(self, cr, uid, ids, context=None):
- '''This function prints the picking list'''
- context = dict(context or {}, active_ids=ids)
- self.write(cr, uid, ids, {'printed': True}, context=context)
- return self.pool.get("report").get_action(cr, uid, ids, 'stock.report_picking', context=context)
-
- def launch_packops(self, cr, uid, ids, context=None):
- self.write(cr, uid, ids, {'launch_pack_operations': True}, context=context)
-
- def action_confirm(self, cr, uid, ids, context=None):
- todo = []
- todo_force_assign = []
- for picking in self.browse(cr, uid, ids, context=context):
- if not picking.move_lines:
- self.launch_packops(cr, uid, [picking.id], context=context)
- if picking.location_id.usage in ('supplier', 'inventory', 'production'):
- todo_force_assign.append(picking.id)
- for r in picking.move_lines:
- if r.state == 'draft':
- todo.append(r.id)
- if len(todo):
- self.pool.get('stock.move').action_confirm(cr, uid, todo, context=context)
-
- if todo_force_assign:
- self.force_assign(cr, uid, todo_force_assign, context=context)
+ @api.multi
+ def action_confirm(self):
+ self.filtered(lambda picking: not picking.move_lines).write({'launch_pack_operations': True})
+ # TDE CLEANME: use of launch pack operation, really useful ?
+ self.mapped('move_lines').filtered(lambda move: move.state == 'draft').action_confirm()
+ self.filtered(lambda picking: picking.location_id.usage in ('supplier', 'inventory', 'production')).force_assign()
return True
- def action_assign(self, cr, uid, ids, context=None):
+ @api.multi
+ def action_assign(self):
""" Check availability of picking moves.
This has the effect of changing the state and reserve quants on available moves, and may
also impact the state of the picking as it is computed based on move's states.
@return: True
"""
- for pick in self.browse(cr, uid, ids, context=context):
- if pick.state == 'draft':
- self.action_confirm(cr, uid, [pick.id], context=context)
- #skip the moves that don't need to be checked
- move_ids = [x.id for x in pick.move_lines if x.state not in ('draft', 'cancel', 'done')]
- if not move_ids:
- raise UserError(_('Nothing to check the availability for.'))
- self.pool.get('stock.move').action_assign(cr, uid, move_ids, context=context)
+ self.filtered(lambda picking: picking.state == 'draft').action_confirm()
+ moves = self.mapped('move_lines').filtered(lambda move: move.state not in ('draft', 'cancel', 'done'))
+ if not moves:
+ raise UserError(_('Nothing to check the availability for.'))
+ moves.action_assign()
return True
- def force_assign(self, cr, uid, ids, context=None):
+ @api.multi
+ def force_assign(self):
""" Changes state of picking to available if moves are confirmed or waiting.
@return: True
"""
- pickings = self.browse(cr, uid, ids, context=context)
- for pick in pickings:
- move_ids = [x.id for x in pick.move_lines if x.state in ['confirmed', 'waiting']]
- self.pool.get('stock.move').force_assign(cr, uid, move_ids, context=context)
+ self.mapped('move_lines').filtered(lambda move: move.state in ['confirmed', 'waiting']).force_assign()
return True
- def action_cancel(self, cr, uid, ids, context=None):
- for pick in self.browse(cr, uid, ids, context=context):
- ids2 = [move.id for move in pick.move_lines]
- self.pool.get('stock.move').action_cancel(cr, uid, ids2, context)
+ @api.multi
+ def action_cancel(self):
+ self.mapped('move_lines').action_cancel()
return True
- def action_done(self, cr, uid, ids, context=None):
+ @api.multi
+ def action_done(self):
"""Changes picking state to done by processing the Stock Moves of the Picking
Normally that happens when the button "Done" is pressed on a Picking view.
@return: True
"""
- for pick in self.browse(cr, uid, ids, context=context):
- todo = []
- for move in pick.move_lines:
- if move.state == 'draft':
- todo.extend(self.pool.get('stock.move').action_confirm(cr, uid, [move.id], context=context))
- elif move.state in ('assigned', 'confirmed'):
- todo.append(move.id)
- if len(todo):
- self.pool.get('stock.move').action_done(cr, uid, todo, context=context)
+ # TDE FIXME: remove decorator when migration the remaining
+ # TDE FIXME: draft -> automatically done, if waiting ?? CLEAR ME
+ draft_moves = self.mapped('move_lines').filtered(lambda self: self.state == 'draft')
+ todo_moves = self.mapped('move_lines').filtered(lambda self: self.state in ['draft', 'assigned', 'confirmed'])
+ draft_moves.action_confirm()
+ todo_moves.action_done()
return True
- def unlink(self, cr, uid, ids, context=None):
- #on picking deletion, cancel its move then unlink them too
- move_obj = self.pool.get('stock.move')
- context = context or {}
- for pick in self.browse(cr, uid, ids, context=context):
- move_ids = [move.id for move in pick.move_lines]
- move_obj.action_cancel(cr, uid, move_ids, context=context)
- move_obj.unlink(cr, uid, move_ids, context=context)
- return super(stock_picking, self).unlink(cr, uid, ids, context=context)
+ @api.multi
+ def recheck_availability(self):
+ self.action_assign()
+ self.do_prepare_partial()
- def _create_backorder(self, cr, uid, ids, backorder_moves=[], context=None):
- """ Move all non-done lines into a new backorder picking. If the key 'do_only_split' is given in the context, then move all lines not in context.get('split', []) instead of all non-done lines.
- """
- picking = self.browse(cr, uid, ids[0], context=context)
- if not backorder_moves:
- backorder_moves = picking.move_lines
- backorder_move_ids = [x.id for x in backorder_moves if x.state not in ('done', 'cancel')]
- if 'do_only_split' in context and context['do_only_split']:
- backorder_move_ids = [x.id for x in backorder_moves if x.id not in context.get('split', [])]
-
- if backorder_move_ids:
- backorder_id = self.copy(cr, uid, picking.id, {
- 'name': '/',
- 'move_lines': [],
- 'pack_operation_ids': [],
- 'backorder_id': picking.id,
- })
- backorder = self.browse(cr, uid, backorder_id, context=context)
- self.message_post(cr, uid, picking.id, body=_("Back order %s created.") % (backorder.name), context=context)
- move_obj = self.pool.get("stock.move")
- move_obj.write(cr, uid, backorder_move_ids, {'picking_id': backorder_id}, context=context)
-
- if not picking.date_done:
- self.write(cr, uid, [picking.id], {'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
- self.action_confirm(cr, uid, [backorder_id], context=context)
- self.action_assign(cr, uid, [backorder_id], context=context)
- return backorder_id
- return False
-
- @api.cr_uid_ids_context
- def recheck_availability(self, cr, uid, picking_ids, context=None):
- self.action_assign(cr, uid, picking_ids, context=context)
- self.do_prepare_partial(cr, uid, picking_ids, context=context)
-
- def _get_top_level_packages(self, cr, uid, quants_suggested_locations, context=None):
+ @api.model
+ def _get_top_level_packages(self, quants, quant_to_locations):
"""This method searches for the higher level packages that can be moved as a single operation, given a list of quants
to move and their suggested destination, and returns the list of matching packages.
"""
+ # TDE FIXME: this method uses shitty arguments
+ # TDE CLEAME while loop could be simpliied with pack.parent_id + all_in check
# Try to find as much as possible top-level packages that can be moved
- pack_obj = self.pool.get("stock.quant.package")
- quant_obj = self.pool.get("stock.quant")
- top_lvl_packages = set()
- quants_to_compare = quants_suggested_locations.keys()
- for pack in list(set([x.package_id for x in quants_suggested_locations.keys() if x and x.package_id])):
+ top_lvl_packages = self.env['stock.quant.package']
+ for pack in set(quant.package_id for quant in quants if quant.package_id):
loop = True
test_pack = pack
good_pack = False
@@ -621,14 +505,14 @@ class stock_picking(models.Model):
all_in = True
for quant in test_pack.get_content():
# If the quant is not in the quants to compare and not in the common location
- if not quant in quants_to_compare:
+ if not quant in quants:
all_in = False
break
else:
#if putaway strat apply, the destination location of each quant may be different (and thus the package should not be taken as a single operation)
if not pack_destination:
- pack_destination = quants_suggested_locations[quant]
- elif pack_destination != quants_suggested_locations[quant]:
+ pack_destination = quant_to_locations[quant]
+ elif pack_destination != quant_to_locations[quant]:
all_in = False
break
if all_in:
@@ -643,78 +527,61 @@ class stock_picking(models.Model):
#(some quants may be reserved for other picking or not reserved at all)
loop = False
if good_pack:
- top_lvl_packages.add(good_pack)
- return list(top_lvl_packages)
+ top_lvl_packages |= good_pack
+ return top_lvl_packages
- def _prepare_pack_ops(self, cr, uid, ids, quants, forced_qties, context=None):
- """ returns a list of dict, ready to be used in create() of stock.pack.operation.
+ def _prepare_pack_ops(self, quants, forced_qties):
+ """ Prepare pack_operations, returns a list of dict to give at create """
+ # TDE CLEANME: oh dear ...
+ _Mapping = namedtuple('Mapping', ('product', 'package', 'owner', 'location', 'location_dst_id'))
- :param picking: browse record (stock.picking)
- :param quants: browse record list (stock.quant). List of quants associated to the picking
- :param forced_qties: dictionary showing for each product (keys) its corresponding quantity (value) that is not covered by the quants associated to the picking
- """
- picking = self.browse(cr, uid, ids[0], context=context)
- def _picking_putaway_apply(product):
- location = False
- # Search putaway strategy
- if product_putaway_strats.get(product.id):
- location = product_putaway_strats[product.id]
- else:
- location = self.pool.get('stock.location').get_putaway_strategy(cr, uid, [picking.location_dest_id.id], product, context=context)
- product_putaway_strats[product.id] = location
- return location or picking.location_dest_id.id
+ all_products = quants.mapped('product_id') | self.env['product.product'].browse(p.id for p in forced_qties.keys()) | self.move_lines.mapped('product_id')
+ computed_putaway_locations = dict(
+ (product, self.location_dest_id.get_putaway_strategy(product) or self.location_dest_id.id) for product in all_products)
- # If we encounter an UoM that is smaller than the default UoM or the one already chosen, use the new one instead.
- product_uom = {} # Determines UoM used in pack operations
- location_dest_id = None
- location_id = None
- for move in [x for x in picking.move_lines if x.state not in ('done', 'cancel')]:
- if not product_uom.get(move.product_id.id):
- product_uom[move.product_id.id] = move.product_id.uom_id
- if move.product_uom.id != move.product_id.uom_id.id and move.product_uom.factor > product_uom[move.product_id.id].factor:
- product_uom[move.product_id.id] = move.product_uom
- if not move.scrapped:
- if location_dest_id and move.location_dest_id.id != location_dest_id:
- raise UserError(_('The destination location must be the same for all the moves of the picking.'))
- location_dest_id = move.location_dest_id.id
- if location_id and move.location_id.id != location_id:
- raise UserError(_('The source location must be the same for all the moves of the picking.'))
- location_id = move.location_id.id
+ product_to_uom = dict((product.id, product.uom_id) for product in all_products)
+ picking_moves = self.move_lines.filtered(lambda move: move.state not in ('done', 'cancel'))
+ for move in picking_moves:
+ # If we encounter an UoM that is smaller than the default UoM or the one already chosen, use the new one instead.
+ if move.product_uom != product_to_uom[move.product_id.id] and move.product_uom.factor > product_to_uom[move.product_id.id].factor:
+ product_to_uom[move.product_id.id] = move.product_uom
+ if len(picking_moves.mapped('location_id')) > 1:
+ raise UserError(_('The source location must be the same for all the moves of the picking.'))
+ if len(picking_moves.mapped('location_dest_id')) > 1:
+ raise UserError(_('The destination location must be the same for all the moves of the picking.'))
- pack_obj = self.pool.get("stock.quant.package")
- quant_obj = self.pool.get("stock.quant")
vals = []
qtys_grouped = {}
lots_grouped = {}
- #for each quant of the picking, find the suggested location
+ # for each quant of the picking, find the suggested location
quants_suggested_locations = {}
- product_putaway_strats = {}
+ # product_putaway_strats = {}
for quant in quants:
if quant.qty <= 0:
continue
- suggested_location_id = _picking_putaway_apply(quant.product_id)
+ suggested_location_id = computed_putaway_locations[quant.product_id]
quants_suggested_locations[quant] = suggested_location_id
- #find the packages we can movei as a whole
- top_lvl_packages = self._get_top_level_packages(cr, uid, quants_suggested_locations, context=context)
+ # find the packages we can movei as a whole
+ top_lvl_packages = self._get_top_level_packages(quants_suggested_locations.keys(), quants_suggested_locations)
# and then create pack operations for the top-level packages found
for pack in top_lvl_packages:
pack_quants = pack.get_content()
vals.append({
- 'picking_id': picking.id,
- 'package_id': pack.id,
- 'product_qty': 1.0,
- 'location_id': pack.location_id.id,
- 'location_dest_id': quants_suggested_locations[pack_quants[0]],
- 'owner_id': pack.owner_id.id,
- })
- #remove the quants inside the package so that they are excluded from the rest of the computation
+ 'picking_id': self.id,
+ 'package_id': pack.id,
+ 'product_qty': 1.0,
+ 'location_id': pack.location_id.id,
+ 'location_dest_id': quants_suggested_locations[pack_quants[0]],
+ 'owner_id': pack.owner_id.id,
+ })
+ # remove the quants inside the package so that they are excluded from the rest of the computation
for quant in pack_quants:
del quants_suggested_locations[quant]
# Go through all remaining reserved quants and group by product, package, owner, source location and dest location
# Lots will go into pack operation lot object
for quant, dest_location_id in quants_suggested_locations.items():
- key = (quant.product_id.id, quant.package_id.id, quant.owner_id.id, quant.location_id.id, dest_location_id)
+ key = _Mapping(quant.product_id, quant.package_id, quant.owner_id, quant.location_id, dest_location_id)
if qtys_grouped.get(key):
qtys_grouped[key] += quant.qty
else:
@@ -727,103 +594,97 @@ class stock_picking(models.Model):
for product, qty in forced_qties.items():
if qty <= 0:
continue
- suggested_location_id = _picking_putaway_apply(product)
- key = (product.id, False, picking.owner_id.id, picking.location_id.id, suggested_location_id)
+ suggested_location_id = computed_putaway_locations[product]
+ key = _Mapping(product, self.env['stock.quant.package'], self.owner_id, self.location_id, suggested_location_id)
if qtys_grouped.get(key):
qtys_grouped[key] += qty
else:
qtys_grouped[key] = qty
# Create the necessary operations for the grouped quants and remaining qtys
- uom_obj = self.pool.get('product.uom')
+ Uom = self.env['product.uom']
prevals = {}
- for key, qty in qtys_grouped.items():
- product = self.pool.get("product.product").browse(cr, uid, key[0], context=context)
- uom_id = product.uom_id.id
- qty_uom = qty
- if product_uom.get(key[0]):
- uom_id = product_uom[key[0]].id
- qty_uom = uom_obj._compute_qty(cr, uid, product.uom_id.id, qty, uom_id)
+ for mapping, qty in qtys_grouped.items():
+ # product = self.env["product.product"].browse(key[0])
+ uom = product_to_uom[mapping.product.id]
+ qty_uom = Uom._compute_qty_obj(mapping.product.uom_id, qty, uom)
pack_lot_ids = []
- if lots_grouped.get(key):
- for lot in lots_grouped[key].keys():
- pack_lot_ids += [(0, 0, {'lot_id': lot, 'qty': 0.0, 'qty_todo': lots_grouped[key][lot]})]
+ if lots_grouped.get(mapping):
+ for lot in lots_grouped[mapping].keys():
+ pack_lot_ids += [(0, 0, {'lot_id': lot, 'qty': 0.0, 'qty_todo': lots_grouped[mapping][lot]})]
val_dict = {
- 'picking_id': picking.id,
+ 'picking_id': self.id,
'product_qty': qty_uom,
- 'product_id': key[0],
- 'package_id': key[1],
- 'owner_id': key[2],
- 'location_id': key[3],
- 'location_dest_id': key[4],
- 'product_uom_id': uom_id,
+ 'product_id': mapping.product.id,
+ 'package_id': mapping.package.id,
+ 'owner_id': mapping.owner.id,
+ 'location_id': mapping.location.id,
+ 'location_dest_id': mapping.location_dst_id,
+ 'product_uom_id': uom.id,
'pack_lot_ids': pack_lot_ids,
}
- if key[0] in prevals:
- prevals[key[0]].append(val_dict)
+ if mapping.product.id in prevals:
+ prevals[mapping.product.id].append(val_dict)
else:
- prevals[key[0]] = [val_dict]
+ prevals[mapping.product.id] = [val_dict]
# prevals var holds the operations in order to create them in the same order than the picking stock moves if possible
processed_products = set()
- for move in [x for x in picking.move_lines if x.state not in ('done', 'cancel')]:
+ for move in [x for x in self.move_lines if x.state not in ('done', 'cancel')]:
if move.product_id.id not in processed_products:
vals += prevals.get(move.product_id.id, [])
processed_products.add(move.product_id.id)
return vals
- @api.cr_uid_ids_context
- def do_prepare_partial(self, cr, uid, picking_ids, context=None):
- context = context or {}
- pack_operation_obj = self.pool.get('stock.pack.operation')
+ @api.multi
+ def do_prepare_partial(self):
+ # TDE CLEANME: oh dear ...
+ PackOperation = self.env['stock.pack.operation']
- #get list of existing operations and delete them
- existing_package_ids = pack_operation_obj.search(cr, uid, [('picking_id', 'in', picking_ids)], context=context)
- if existing_package_ids:
- pack_operation_obj.unlink(cr, uid, existing_package_ids, context)
- for picking in self.browse(cr, uid, picking_ids, context=context):
+ # get list of existing operations and delete them
+ existing_packages = PackOperation.search([('picking_id', 'in', self.ids)]) # TDE FIXME: o2m / m2o ?
+ if existing_packages:
+ existing_packages.unlink()
+ for picking in self:
forced_qties = {} # Quantity remaining after calculating reserved quants
- picking_quants = []
- #Calculate packages, reserved quants, qtys of this picking's moves
+ picking_quants = self.env['stock.quant']
+ # Calculate packages, reserved quants, qtys of this picking's moves
for move in picking.move_lines:
if move.state not in ('assigned', 'confirmed', 'waiting'):
continue
move_quants = move.reserved_quant_ids
picking_quants += move_quants
forced_qty = (move.state == 'assigned') and move.product_qty - sum([x.qty for x in move_quants]) or 0
- #if we used force_assign() on the move, or if the move is incoming, forced_qty > 0
+ # if we used force_assign() on the move, or if the move is incoming, forced_qty > 0
if float_compare(forced_qty, 0, precision_rounding=move.product_id.uom_id.rounding) > 0:
if forced_qties.get(move.product_id):
forced_qties[move.product_id] += forced_qty
else:
forced_qties[move.product_id] = forced_qty
- for vals in self._prepare_pack_ops(cr, uid, [picking.id], picking_quants, forced_qties, context=context):
+ for vals in picking._prepare_pack_ops(picking_quants, forced_qties):
vals['fresh_record'] = False
- pack_operation_obj.create(cr, uid, vals, context=context)
- #recompute the remaining quantities all at once
- self.do_recompute_remaining_quantities(cr, uid, picking_ids, context=context)
- self.write(cr, uid, picking_ids, {'recompute_pack_op': False}, context=context)
+ PackOperation.create(vals)
+ # recompute the remaining quantities all at once
+ self.do_recompute_remaining_quantities()
+ self.write({'recompute_pack_op': False})
- @api.cr_uid_ids_context
- def do_unreserve(self, cr, uid, picking_ids, context=None):
+ @api.multi
+ def do_unreserve(self):
"""
Will remove all quants for picking in picking_ids
"""
- moves_to_unreserve = []
- pack_line_to_unreserve = []
- for picking in self.browse(cr, uid, picking_ids, context=context):
- moves_to_unreserve += [m.id for m in picking.move_lines if m.state not in ('done', 'cancel')]
- pack_line_to_unreserve += [p.id for p in picking.pack_operation_ids]
+ moves_to_unreserve = self.mapped('move_lines').filtered(lambda move: move.state not in ('done', 'cancel'))
+ pack_line_to_unreserve = self.mapped('pack_operation_ids')
if moves_to_unreserve:
if pack_line_to_unreserve:
- self.pool.get('stock.pack.operation').unlink(cr, uid, pack_line_to_unreserve, context=context)
- self.pool.get('stock.move').do_unreserve(cr, uid, moves_to_unreserve, context=context)
+ pack_line_to_unreserve.unlink()
+ moves_to_unreserve.do_unreserve()
+
+ def recompute_remaining_qty(self, done_qtys=False):
- def recompute_remaining_qty(self, cr, uid, ids, done_qtys=False, context=None):
- picking = self.browse(cr, uid, ids[0], context=context)
def _create_link_for_index(operation_id, index, product_id, qty_to_assign, quant_id=False):
move_dict = prod2move_ids[product_id][index]
qty_on_link = min(move_dict['remaining_qty'], qty_to_assign)
- self.pool.get('stock.move.operation.link').create(cr, uid, {'move_id': move_dict['move'].id, 'operation_id': operation_id, 'qty': qty_on_link, 'reserved_quant_id': quant_id}, context=context)
+ self.env['stock.move.operation.link'].create({'move_id': move_dict['move'].id, 'operation_id': operation_id, 'qty': qty_on_link, 'reserved_quant_id': quant_id})
if move_dict['remaining_qty'] == qty_on_link:
prod2move_ids[product_id].pop(index)
else:
@@ -846,8 +707,8 @@ class stock_picking(models.Model):
'''method that creates the link between a given operation and move(s) of given product, for the given quantity.
Returns True if it was possible to create links for the requested quantity (False if there was not enough quantity on stock moves)'''
qty_to_assign = qty
- prod_obj = self.pool.get("product.product")
- product = prod_obj.browse(cr, uid, product_id)
+ Product = self.env["product.product"]
+ product = Product.browse(product_id)
rounding = product.uom_id.rounding
qtyassign_cmp = float_compare(qty_to_assign, 0.0, precision_rounding=rounding)
if prod2move_ids.get(product_id):
@@ -857,15 +718,16 @@ class stock_picking(models.Model):
qtyassign_cmp = float_compare(qty_to_assign, 0.0, precision_rounding=rounding)
return qtyassign_cmp == 0
- uom_obj = self.pool.get('product.uom')
- package_obj = self.pool.get('stock.quant.package')
- quant_obj = self.pool.get('stock.quant')
- link_obj = self.pool.get('stock.move.operation.link')
+ # TDE CLEANME: oh dear ...
+ Uom = self.env['product.uom']
+ QuantPackage = self.env['stock.quant.package']
+ OperationLink = self.env['stock.move.operation.link']
+
quants_in_package_done = set()
prod2move_ids = {}
still_to_do = []
- #make a dictionary giving for each product, the moves and related quantity that can be used in operation links
- moves = sorted([x for x in picking.move_lines if x.state not in ('done', 'cancel')], key=lambda x: (((x.state == 'assigned') and -2 or 0) + (x.partially_available and -1 or 0)))
+ # make a dictionary giving for each product, the moves and related quantity that can be used in operation links
+ moves = sorted([x for x in self.move_lines if x.state not in ('done', 'cancel')], key=lambda x: (((x.state == 'assigned') and -2 or 0) + (x.partially_available and -1 or 0)))
for move in moves:
if not prod2move_ids.get(move.product_id.id):
prod2move_ids[move.product_id.id] = [{'move': move, 'remaining_qty': move.product_qty}]
@@ -873,26 +735,26 @@ class stock_picking(models.Model):
prod2move_ids[move.product_id.id].append({'move': move, 'remaining_qty': move.product_qty})
need_rereserve = False
- #sort the operations in order to give higher priority to those with a package, then a serial number
- operations = picking.pack_operation_ids
+ # sort the operations in order to give higher priority to those with a package, then a serial number
+ operations = self.pack_operation_ids
operations = sorted(operations, key=lambda x: ((x.package_id and not x.product_id) and -4 or 0) + (x.package_id and -2 or 0) + (x.pack_lot_ids and -1 or 0))
- #delete existing operations to start again from scratch
- links = link_obj.search(cr, uid, [('operation_id', 'in', [x.id for x in operations])], context=context)
+ # delete existing operations to start again from scratch
+ links = OperationLink.search([('operation_id', 'in', [x.id for x in operations])])
if links:
- link_obj.unlink(cr, uid, links, context=context)
- #1) first, try to create links when quants can be identified without any doubt
+ links.unlink()
+ # 1) first, try to create links when quants can be identified without any doubt
for ops in operations:
lot_qty = {}
for packlot in ops.pack_lot_ids:
- lot_qty[packlot.lot_id.id] = uom_obj._compute_qty(cr, uid, ops.product_uom_id.id, packlot.qty, ops.product_id.uom_id.id)
- #for each operation, create the links with the stock move by seeking on the matching reserved quants,
- #and deffer the operation if there is some ambiguity on the move to select
+ lot_qty[packlot.lot_id.id] = Uom._compute_qty_obj(ops.product_uom_id, packlot.qty, ops.product_id.uom_id)
+ # for each operation, create the links with the stock move by seeking on the matching reserved quants,
+ # and deffer the operation if there is some ambiguity on the move to select
if ops.package_id and not ops.product_id and (not done_qtys or ops.qty_done):
- #entire package
+ # entire package
for quant in ops.package_id.get_content():
remaining_qty_on_quant = quant.qty
if quant.reservation_id:
- #avoid quants being counted twice
+ # avoid quants being counted twice
quants_in_package_done.add(quant.id)
qty_on_link = _create_link_for_quant(ops.id, quant, quant.qty)
remaining_qty_on_quant -= qty_on_link
@@ -900,9 +762,9 @@ class stock_picking(models.Model):
still_to_do.append((ops, quant.product_id.id, remaining_qty_on_quant))
need_rereserve = True
elif ops.product_id.id:
- #Check moves with same product
+ # Check moves with same product
product_qty = ops.qty_done if done_qtys else ops.product_qty
- qty_to_assign = uom_obj._compute_qty_obj(cr, uid, ops.product_uom_id, product_qty, ops.product_id.uom_id, context=context)
+ qty_to_assign = Uom._compute_qty_obj(ops.product_uom_id, product_qty, ops.product_id.uom_id)
for move_dict in prod2move_ids.get(ops.product_id.id, []):
move = move_dict['move']
for quant in move.reserved_quant_ids:
@@ -911,9 +773,9 @@ class stock_picking(models.Model):
if quant.id in quants_in_package_done:
continue
- #check if the quant is matching the operation details
+ # check if the quant is matching the operation details
if ops.package_id:
- flag = quant.package_id and bool(package_obj.search(cr, uid, [('id', 'child_of', [ops.package_id.id])], context=context)) or False
+ flag = quant.package_id and bool(QuantPackage.search([('id', 'child_of', [ops.package_id.id])])) or False
else:
flag = not quant.package_id.id
flag = flag and (ops.owner_id.id == quant.owner_id.id)
@@ -923,7 +785,7 @@ class stock_picking(models.Model):
qty_on_link = _create_link_for_quant(ops.id, quant, max_qty_on_link)
qty_to_assign -= qty_on_link
else:
- if lot_qty.get(quant.lot_id.id): #if there is still some qty left
+ if lot_qty.get(quant.lot_id.id): # if there is still some qty left
max_qty_on_link = min(quant.qty, qty_to_assign, lot_qty[quant.lot_id.id])
qty_on_link = _create_link_for_quant(ops.id, quant, max_qty_on_link)
qty_to_assign -= qty_on_link
@@ -931,43 +793,198 @@ class stock_picking(models.Model):
qty_assign_cmp = float_compare(qty_to_assign, 0, precision_rounding=ops.product_id.uom_id.rounding)
if qty_assign_cmp > 0:
- #qty reserved is less than qty put in operations. We need to create a link but it's deferred after we processed
- #all the quants (because they leave no choice on their related move and needs to be processed with higher priority)
+ # qty reserved is less than qty put in operations. We need to create a link but it's deferred after we processed
+ # all the quants (because they leave no choice on their related move and needs to be processed with higher priority)
still_to_do += [(ops, ops.product_id.id, qty_to_assign)]
need_rereserve = True
- #2) then, process the remaining part
+ # 2) then, process the remaining part
all_op_processed = True
for ops, product_id, remaining_qty in still_to_do:
all_op_processed = _create_link_for_product(ops.id, product_id, remaining_qty) and all_op_processed
return (need_rereserve, all_op_processed)
- def picking_recompute_remaining_quantities(self, cr, uid, ids, done_qtys=False, context=None):
- picking = self.browse(cr, uid, ids[0], context=context)
+ def picking_recompute_remaining_quantities(self, done_qtys=False):
need_rereserve = False
all_op_processed = True
- if picking.pack_operation_ids:
- need_rereserve, all_op_processed = self.recompute_remaining_qty(cr, uid, [picking.id], done_qtys=done_qtys, context=context)
+ if self.pack_operation_ids:
+ need_rereserve, all_op_processed = self.recompute_remaining_qty(done_qtys=done_qtys)
return need_rereserve, all_op_processed
- @api.cr_uid_ids_context
- def do_recompute_remaining_quantities(self, cr, uid, picking_ids, done_qtys=False, context=None):
- for picking in self.browse(cr, uid, picking_ids, context=context):
- if picking.pack_operation_ids:
- self.recompute_remaining_qty(cr, uid, [picking.id], done_qtys=done_qtys, context=context)
+ def do_recompute_remaining_quantities(self, done_qtys=False):
+ # TDE FIXME
+ tmp = self.filtered(lambda picking: picking.pack_operation_ids)
+ if tmp:
+ for pick in tmp:
+ pick.recompute_remaining_qty(done_qtys=done_qtys)
- def _prepare_values_extra_move(self, cr, uid, op, product, remaining_qty, context=None):
+ def rereserve_quants(self, move_ids=[]):
+ """ Unreserve quants then try to reassign quants."""
+ if not move_ids:
+ self.do_unreserve()
+ self.action_assign()
+ else:
+ moves = self.env['stock.move'].browse(move_ids)
+ moves.do_unreserve()
+ moves.action_assign(no_prepare=True)
+
+ @api.multi
+ def do_new_transfer(self):
+ for pick in self:
+ pack_operations_delete = self.env['stock.pack.operation']
+ if not pick.move_lines and not pick.pack_operation_ids:
+ raise UserError(_('Please create some Initial Demand or Mark as Todo and create some Operations. '))
+ # In draft or with no pack operations edited yet, ask if we can just do everything
+ if pick.state == 'draft' or all([x.qty_done == 0.0 for x in pick.pack_operation_ids]):
+ # If no lots when needed, raise error
+ picking_type = pick.picking_type_id
+ if (picking_type.use_create_lots or picking_type.use_existing_lots):
+ for pack in pick.pack_operation_ids:
+ if pack.product_id and pack.product_id.tracking != 'none':
+ raise UserError(_('Some products require lots, so you need to specify those first!'))
+ view = self.env.ref('stock.view_immediate_transfer')
+ wiz = self.env['stock.immediate.transfer'].create({'pick_id': pick.id})
+ # TDE FIXME: a return in a loop, what a good idea. Really.
+ return {
+ 'name': _('Immediate Transfer?'),
+ 'type': 'ir.actions.act_window',
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_model': 'stock.immediate.transfer',
+ 'views': [(view.id, 'form')],
+ 'view_id': view.id,
+ 'target': 'new',
+ 'res_id': wiz.id,
+ 'context': self.env.context,
+ }
+
+ # Check backorder should check for other barcodes
+ if pick.check_backorder():
+ view = self.env.ref('stock.view_backorder_confirmation')
+ wiz = self.env['stock.backorder.confirmation'].create({'pick_id': pick.id})
+ # TDE FIXME: same reamrk as above actually
+ return {
+ 'name': _('Create Backorder?'),
+ 'type': 'ir.actions.act_window',
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_model': 'stock.backorder.confirmation',
+ 'views': [(view.id, 'form')],
+ 'view_id': view.id,
+ 'target': 'new',
+ 'res_id': wiz.id,
+ 'context': self.env.context,
+ }
+ for operation in pick.pack_operation_ids:
+ if operation.qty_done < 0:
+ raise UserError(_('No negative quantities allowed'))
+ if operation.qty_done > 0:
+ operation.write({'product_qty': operation.qty_done})
+ else:
+ pack_operations_delete |= operation
+ if pack_operations_delete:
+ pack_operations_delete.unlink()
+ self.do_transfer()
+ return
+
+ def check_backorder(self):
+ need_rereserve, all_op_processed = self.picking_recompute_remaining_quantities(done_qtys=True)
+ for move in self.move_lines:
+ if float_compare(move.remaining_qty, 0, precision_rounding=move.product_id.uom_id.rounding) != 0:
+ return True
+ return False
+
+ @api.multi
+ def do_transfer(self):
+ """ If no pack operation, we do simple action_done of the picking.
+ Otherwise, do the pack operations. """
+ # TDE CLEAN ME: reclean me, please
+ self._create_lots_for_picking()
+
+ no_pack_op_pickings = self.filtered(lambda picking: not picking.pack_operation_ids)
+ no_pack_op_pickings.action_done()
+ other_pickings = self - no_pack_op_pickings
+ for picking in other_pickings:
+ need_rereserve, all_op_processed = picking.picking_recompute_remaining_quantities()
+ todo_moves = self.env['stock.move']
+ toassign_moves = self.env['stock.move']
+
+ # create extra moves in the picking (unexpected product moves coming from pack operations)
+ if not all_op_processed:
+ todo_moves |= picking._create_extra_moves()
+
+ # split move lines if needed
+ for move in picking.move_lines:
+ rounding = move.product_id.uom_id.rounding
+ remaining_qty = move.remaining_qty
+ if move.state in ('done', 'cancel'):
+ # ignore stock moves cancelled or already done
+ continue
+ elif move.state == 'draft':
+ toassign_moves |= move
+ if float_compare(remaining_qty, 0, precision_rounding=rounding) == 0:
+ if move.state in ('draft', 'assigned', 'confirmed'):
+ todo_moves |= move
+ elif float_compare(remaining_qty, 0, precision_rounding=rounding) > 0 and float_compare(remaining_qty, move.product_qty, precision_rounding=rounding) < 0:
+ # TDE FIXME: shoudl probably return a move - check for no track key, by the way
+ new_move_id = move.split(remaining_qty)
+ new_move = self.env['stock.move'].with_context(mail_notrack=True).browse(new_move_id)
+ todo_moves |= move
+ # Assign move as it was assigned before
+ toassign_moves |= new_move
+
+ if need_rereserve or not all_op_processed:
+ if picking.location_id.usage not in ("supplier", "production", "inventory"):
+ picking.rereserve_quants(move_ids=todo_moves.ids)
+ picking.do_recompute_remaining_quantities()
+
+ # TDE FIXME: do_only_split does not seem used anymore
+ if todo_moves and not self.env.context.get('do_only_split'):
+ todo_moves.with_context(mail_notrack=True).action_done()
+ elif self.env.context.get('do_only_split'):
+ picking = picking.with_context(split=todo_moves.ids)
+
+ picking._create_backorder()
+ return True
+
+ def _create_lots_for_picking(self):
+ Lot = self.env['stock.production.lot']
+ for pack_op_lot in self.mapped('pack_operation_ids').mapped('pack_lot_ids'):
+ if not pack_op_lot.lot_id:
+ lot = Lot.create({'name': pack_op_lot.lot_name, 'product_id': pack_op_lot.operation_id.product_id.id})
+ pack_op_lot.write({'lot_id': lot.id})
+ # TDE FIXME: this should not be done here
+ self.mapped('pack_operation_ids').mapped('pack_lot_ids').filtered(lambda op_lot: op_lot.qty == 0.0).unlink()
+ create_lots_for_picking = _create_lots_for_picking
+
+ def _create_extra_moves(self):
+ '''This function creates move lines on a picking, at the time of do_transfer, based on
+ unexpected product transfers (or exceeding quantities) found in the pack operations.
+ '''
+ # TDE FIXME: move to batch
+ self.ensure_one()
+ moves = self.env['stock.move']
+ for pack_operation in self.pack_operation_ids:
+ for product, remaining_qty in pack_operation._get_remaining_prod_quantities().items():
+ if float_compare(remaining_qty, 0, precision_rounding=product.uom_id.rounding) > 0:
+ vals = self._prepare_values_extra_move(pack_operation, product, remaining_qty)
+ moves |= moves.create(vals)
+ if moves:
+ moves.action_confirm()
+ return moves
+
+ def _prepare_values_extra_move(self, op, product, remaining_qty):
"""
Creates an extra move when there is no corresponding original move to be copied
"""
- uom_obj = self.pool.get("product.uom")
+ Uom = self.env["product.uom"]
uom_id = product.uom_id.id
qty = remaining_qty
if op.product_id and op.product_uom_id and op.product_uom_id.id != product.uom_id.id:
- if op.product_uom_id.factor > product.uom_id.factor: #If the pack operation's is a smaller unit
+ if op.product_uom_id.factor > product.uom_id.factor: # If the pack operation's is a smaller unit
uom_id = op.product_uom_id.id
- #HALF-UP rounding as only rounding errors will be because of propagation of error from default UoM
- qty = uom_obj._compute_qty_obj(cr, uid, product.uom_id, remaining_qty, op.product_uom_id, rounding_method='HALF-UP')
+ # HALF-UP rounding as only rounding errors will be because of propagation of error from default UoM
+ qty = Uom._compute_qty_obj(product.uom_id, remaining_qty, op.product_uom_id, rounding_method='HALF-UP')
picking = op.picking_id
ref = product.default_code
name = '[' + ref + ']' + ' ' + product.name if ref else product.name
@@ -976,7 +993,7 @@ class stock_picking(models.Model):
if m.move_id.procurement_id:
proc_id = m.move_id.procurement_id.id
break
- res = {
+ return {
'picking_id': picking.id,
'location_id': picking.location_id.id,
'location_dest_id': picking.location_dest_id.id,
@@ -988,192 +1005,60 @@ class stock_picking(models.Model):
'state': 'draft',
'restrict_partner_id': op.owner_id,
'group_id': picking.group_id.id,
- }
- return res
+ }
- def _create_extra_moves(self, cr, uid, ids, context=None):
- '''This function creates move lines on a picking, at the time of do_transfer, based on
- unexpected product transfers (or exceeding quantities) found in the pack operations.
- '''
- picking = self.browse(cr, uid, ids[0], context=context)
- move_obj = self.pool.get('stock.move')
- operation_obj = self.pool.get('stock.pack.operation')
- moves = []
- for op in picking.pack_operation_ids:
- for product, remaining_qty in operation_obj._get_remaining_prod_quantities(cr, uid, [op.id], context=context).items():
- if float_compare(remaining_qty, 0, precision_rounding=product.uom_id.rounding) > 0:
- vals = self._prepare_values_extra_move(cr, uid, op, product, remaining_qty, context=context)
- moves.append(move_obj.create(cr, uid, vals, context=context))
- if moves:
- move_obj.action_confirm(cr, uid, moves, context=context)
- return moves
-
- def rereserve_quants(self, cr, uid, ids, move_ids=[], context=None):
- """ Unreserve quants then try to reassign quants."""
- picking = self.browse(cr, uid, ids[0], context=context)
- stock_move_obj = self.pool.get('stock.move')
- if not move_ids:
- self.do_unreserve(cr, uid, [picking.id], context=context)
- self.action_assign(cr, uid, [picking.id], context=context)
- else:
- stock_move_obj.do_unreserve(cr, uid, move_ids, context=context)
- stock_move_obj.action_assign(cr, uid, move_ids, no_prepare=True, context=context)
-
- def do_new_transfer(self, cr, uid, ids, context=None):
- pack_op_obj = self.pool['stock.pack.operation']
- data_obj = self.pool['ir.model.data']
- for pick in self.browse(cr, uid, ids, context=context):
- to_delete = []
- if not pick.move_lines and not pick.pack_operation_ids:
- raise UserError(_('Please create some Initial Demand or Mark as Todo and create some Operations. '))
- # In draft or with no pack operations edited yet, ask if we can just do everything
- if pick.state == 'draft' or all([x.qty_done == 0.0 for x in pick.pack_operation_ids]):
- # If no lots when needed, raise error
- picking_type = pick.picking_type_id
- if (picking_type.use_create_lots or picking_type.use_existing_lots):
- for pack in pick.pack_operation_ids:
- if pack.product_id and pack.product_id.tracking != 'none':
- raise UserError(_('Some products require lots, so you need to specify those first!'))
- view = data_obj.xmlid_to_res_id(cr, uid, 'stock.view_immediate_transfer')
- wiz_id = self.pool['stock.immediate.transfer'].create(cr, uid, {'pick_id': pick.id}, context=context)
- return {
- 'name': _('Immediate Transfer?'),
- 'type': 'ir.actions.act_window',
- 'view_type': 'form',
- 'view_mode': 'form',
- 'res_model': 'stock.immediate.transfer',
- 'views': [(view, 'form')],
- 'view_id': view,
- 'target': 'new',
- 'res_id': wiz_id,
- 'context': context,
- }
-
- # Check backorder should check for other barcodes
- if self.check_backorder(cr, uid, [pick.id], context=context):
- view = data_obj.xmlid_to_res_id(cr, uid, 'stock.view_backorder_confirmation')
- wiz_id = self.pool['stock.backorder.confirmation'].create(cr, uid, {'pick_id': pick.id}, context=context)
- return {
- 'name': _('Create Backorder?'),
- 'type': 'ir.actions.act_window',
- 'view_type': 'form',
- 'view_mode': 'form',
- 'res_model': 'stock.backorder.confirmation',
- 'views': [(view, 'form')],
- 'view_id': view,
- 'target': 'new',
- 'res_id': wiz_id,
- 'context': context,
- }
- for operation in pick.pack_operation_ids:
- if operation.qty_done < 0:
- raise UserError(_('No negative quantities allowed'))
- if operation.qty_done > 0:
- pack_op_obj.write(cr, uid, operation.id, {'product_qty': operation.qty_done}, context=context)
- else:
- to_delete.append(operation.id)
- if to_delete:
- pack_op_obj.unlink(cr, uid, to_delete, context=context)
- self.do_transfer(cr, uid, ids, context=context)
- return
-
- def check_backorder(self, cr, uid, ids, context=None):
- picking = self.browse(cr, uid, ids[0], context=context)
- need_rereserve, all_op_processed = self.picking_recompute_remaining_quantities(cr, uid, [picking.id], done_qtys=True, context=context)
- for move in picking.move_lines:
- if float_compare(move.remaining_qty, 0, precision_rounding = move.product_id.uom_id.rounding) != 0:
- return True
- return False
-
- def create_lots_for_picking(self, cr, uid, ids, context=None):
- lot_obj = self.pool['stock.production.lot']
- opslot_obj = self.pool['stock.pack.operation.lot']
- to_unlink = []
- for picking in self.browse(cr, uid, ids, context=context):
- for ops in picking.pack_operation_ids:
- for opslot in ops.pack_lot_ids:
- if not opslot.lot_id:
- lot_id = lot_obj.create(cr, uid, {'name': opslot.lot_name, 'product_id': ops.product_id.id}, context=context)
- opslot_obj.write(cr, uid, [opslot.id], {'lot_id':lot_id}, context=context)
- #Unlink pack operations where qty = 0
- to_unlink += [x.id for x in ops.pack_lot_ids if x.qty == 0.0]
- opslot_obj.unlink(cr, uid, to_unlink, context=context)
-
- def do_transfer(self, cr, uid, ids, context=None):
+ @api.multi
+ def _create_backorder(self, backorder_moves=[]):
+ """ Move all non-done lines into a new backorder picking. If the key 'do_only_split' is given in the context, then move all lines not in context.get('split', []) instead of all non-done lines.
"""
- If no pack operation, we do simple action_done of the picking
- Otherwise, do the pack operations
- """
- if not context:
- context = {}
- notrack_context = dict(context, mail_notrack=True)
- stock_move_obj = self.pool.get('stock.move')
- self.create_lots_for_picking(cr, uid, ids, context=context)
- for picking in self.browse(cr, uid, ids, context=context):
- if not picking.pack_operation_ids:
- self.action_done(cr, uid, [picking.id], context=context)
- continue
+ # TDE note: o2o conversion, todo multi
+ for picking in self:
+ backorder_moves = backorder_moves or picking.move_lines
+ if self._context.get('do_only_split'):
+ not_done_bo_moves = backorder_moves.filtered(lambda move: move.id not in self._context.get('split', []))
else:
- need_rereserve, all_op_processed = self.picking_recompute_remaining_quantities(cr, uid, [picking.id], context=context)
- #create extra moves in the picking (unexpected product moves coming from pack operations)
- todo_move_ids = []
- if not all_op_processed:
- todo_move_ids += self._create_extra_moves(cr, uid, [picking.id], context=context)
-
- #split move lines if needed
- toassign_move_ids = []
- for move in picking.move_lines:
- remaining_qty = move.remaining_qty
- if move.state in ('done', 'cancel'):
- #ignore stock moves cancelled or already done
- continue
- elif move.state == 'draft':
- toassign_move_ids.append(move.id)
- if float_compare(remaining_qty, 0, precision_rounding = move.product_id.uom_id.rounding) == 0:
- if move.state in ('draft', 'assigned', 'confirmed'):
- todo_move_ids.append(move.id)
- elif float_compare(remaining_qty,0, precision_rounding = move.product_id.uom_id.rounding) > 0 and \
- float_compare(remaining_qty, move.product_qty, precision_rounding = move.product_id.uom_id.rounding) < 0:
- new_move = stock_move_obj.split(cr, uid, [move.id], remaining_qty, context=notrack_context)
- todo_move_ids.append(move.id)
- #Assign move as it was assigned before
- toassign_move_ids.append(new_move)
- todo_move_ids = list(set(todo_move_ids))
- if need_rereserve or not all_op_processed:
- if not picking.location_id.usage in ("supplier", "production", "inventory"):
- self.rereserve_quants(cr, uid, [picking.id], move_ids=todo_move_ids, context=context)
- self.do_recompute_remaining_quantities(cr, uid, [picking.id], context=context)
- if todo_move_ids and not context.get('do_only_split'):
- self.pool.get('stock.move').action_done(cr, uid, todo_move_ids, context=notrack_context)
- elif context.get('do_only_split'):
- context = dict(context, split=todo_move_ids)
- self._create_backorder(cr, uid, [picking.id], context=context)
+ not_done_bo_moves = backorder_moves.filtered(lambda move: move.state not in ('done', 'cancel'))
+ if not not_done_bo_moves:
+ continue
+ backorder_picking = picking.copy({
+ 'name': '/',
+ 'move_lines': [],
+ 'pack_operation_ids': [],
+ 'backorder_id': picking.id
+ })
+ picking.message_post(body=_("Back order %s created.") % (backorder_picking.name))
+ not_done_bo_moves.write({'picking_id': backorder_picking.id})
+ if not picking.date_done:
+ picking.write({'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
+ backorder_picking.action_confirm()
+ backorder_picking.action_assign()
return True
- def put_in_pack(self, cr, uid, ids, context=None):
- stock_operation_obj = self.pool["stock.pack.operation"]
- package_obj = self.pool["stock.quant.package"]
+ @api.multi
+ def put_in_pack(self):
+ # TDE FIXME: reclean me
+ QuantPackage = self.env["stock.quant.package"]
package_id = False
- for pick in self.browse(cr, uid, ids, context=context):
+ for pick in self:
operations = [x for x in pick.pack_operation_ids if x.qty_done > 0 and (not x.result_package_id)]
- pack_operation_ids = []
+ pack_operation_ids = self.env['stock.pack.operation']
for operation in operations:
- #If we haven't done all qty in operation, we have to split into 2 operation
+ # If we haven't done all qty in operation, we have to split into 2 operation
op = operation
if operation.qty_done < operation.product_qty:
- new_operation = stock_operation_obj.copy(cr, uid, operation.id, {'product_qty': operation.qty_done,'qty_done': operation.qty_done}, context=context)
+ new_operation = operation.copy({'product_qty': operation.qty_done,'qty_done': operation.qty_done})
- stock_operation_obj.write(cr, uid, operation.id, {'product_qty': operation.product_qty - operation.qty_done,'qty_done': 0}, context=context)
+ operation.write({'product_qty': operation.product_qty - operation.qty_done,'qty_done': 0})
if operation.pack_lot_ids:
packlots_transfer = [(4, x.id) for x in operation.pack_lot_ids]
- stock_operation_obj.write(cr, uid, [new_operation], {'pack_lot_ids': packlots_transfer}, context=context)
+ new_operation.write({'pack_lot_ids': packlots_transfer})
- op = stock_operation_obj.browse(cr, uid, new_operation, context=context)
- pack_operation_ids.append(op.id)
+ op = new_operation
+ pack_operation_ids |= op
if operations:
- stock_operation_obj.check_tracking(cr, uid, pack_operation_ids, context=context)
- package_id = package_obj.create(cr, uid, {}, context=context)
- stock_operation_obj.write(cr, uid, pack_operation_ids, {'result_package_id': package_id}, context=context)
+ pack_operation_ids.check_tracking()
+ package_id = QuantPackage.create({})
+ pack_operation_ids.write({'result_package_id': package_id})
else:
raise UserError(_('Please process some quantities to put in the pack first!'))
return package_id
diff --git a/addons/stock/tests/test_stock_flow.py b/addons/stock/tests/test_stock_flow.py
index 0be7f7bfabc..61a14101443 100644
--- a/addons/stock/tests/test_stock_flow.py
+++ b/addons/stock/tests/test_stock_flow.py
@@ -309,7 +309,7 @@ class TestStockFlow(TestStockCommon):
# Confirm back order of incoming shipment.
back_order_in.action_confirm()
- self.assertEqual(back_order_in.state, 'assigned', 'Wrong state of incoming shipment back order.')
+ self.assertEqual(back_order_in.state, 'assigned', 'Wrong state of incoming shipment back order: %s instead of %s' % (back_order_in.state, 'assigned'))
for move in back_order_in.move_lines:
self.assertEqual(move.state, 'assigned', 'Wrong state of move line.')
@@ -917,7 +917,7 @@ class TestStockFlow(TestStockCommon):
#-----------------------------------------------------------------------
# Check incoming shipment state.
- self.assertEqual(picking_in.state, 'done', 'Incoming shipment state should be done.')
+ self.assertEqual(picking_in.state, 'done', 'Incoming shipment state: %s instead of %s' % (picking_in.state, 'done'))
# Check incoming shipment move lines state.
for move in picking_in.move_lines:
self.assertEqual(move.state, 'done', 'Wrong state of move lines.')
diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml
index cfb52f7200e..2cebbb77c95 100644
--- a/addons/stock/views/stock_picking_views.xml
+++ b/addons/stock/views/stock_picking_views.xml
@@ -90,7 +90,7 @@
-
+
@@ -121,7 +121,7 @@
name="Dashboard"/>
-
+
stock.picking.calendar
stock.picking
@@ -222,7 +222,7 @@
-
+
@@ -367,7 +367,7 @@
-
+