diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py index 979acce6a25..25e9738fd00 100644 --- a/addons/sale_stock/sale_stock.py +++ b/addons/sale_stock/sale_stock.py @@ -265,7 +265,6 @@ class AccountInvoice(models.Model): class ProcurementOrder(models.Model): _inherit = "procurement.order" - @api.multi def _run_move_create(self): vals = super(ProcurementOrder, self)._run_move_create() if self.sale_line_id: diff --git a/addons/stock/models/procurement.py b/addons/stock/models/procurement.py index 3ffc77bc223..2747db6a776 100644 --- a/addons/stock/models/procurement.py +++ b/addons/stock/models/procurement.py @@ -1,362 +1,316 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import fields, osv -from openerp.tools.translate import _ - -from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, float_compare, float_round -from openerp import SUPERUSER_ID - from collections import defaultdict -from dateutil.relativedelta import relativedelta from datetime import datetime +from dateutil.relativedelta import relativedelta from psycopg2 import OperationalError + +from odoo import api, fields, models, registry, _ +from odoo.osv import expression +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, float_compare, float_round + import openerp -class procurement_group(osv.osv): - _inherit = 'procurement.group' - _columns = { - 'partner_id': fields.many2one('res.partner', 'Partner') - } -class procurement_rule(osv.osv): +class ProcurementGroup(models.Model): + _inherit = 'procurement.group' + + partner_id = fields.Many2one('res.partner', 'Partner') + + +class ProcurementRule(models.Model): + """ Pull rules """ _inherit = 'procurement.rule' - def _get_action(self, cr, uid, context=None): - result = super(procurement_rule, self)._get_action(cr, uid, context=context) + location_id = fields.Many2one('stock.location', 'Procurement Location') + location_src_id = fields.Many2one('stock.location', 'Source Location', help="Source location is action=move") + route_id = fields.Many2one('stock.location.route', 'Route', help="If route_id is False, the rule is global") + procure_method = fields.Selection([ + ('make_to_stock', 'Take From Stock'), + ('make_to_order', 'Create Procurement')], string='Move Supply Method', + default='make_to_stock', required=True, + help="""Determines the procurement method of the stock move that will be generated: whether it will need to 'take from the available stock' in its source location or needs to ignore its stock and create a procurement over there.""") + route_sequence = fields.Integer('Route Sequence', related='route_id.sequence', store=True) + picking_type_id = fields.Many2one('stock.picking.type', 'Picking Type', help="Picking Type determines the way the picking should be shown in the view, reports, ...") + delay = fields.Integer('Number of Days', default=0) + partner_address_id = fields.Many2one('res.partner', 'Partner Address') + propagate = fields.Boolean( + 'Propagate cancel and split', default=True, + help='If checked, when the previous move of the move (which was generated by a next procurement) is cancelled or split, the move generated by this move will too') + warehouse_id = fields.Many2one('stock.warehouse', 'Served Warehouse', help='The warehouse this rule is for') + propagate_warehouse_id = fields.Many2one( + 'stock.warehouse', 'Warehouse to Propagate', + help="The warehouse to propagate on the created move/procurement, which can be different of the warehouse this rule is for (e.g for resupplying rules from another warehouse)") + + @api.model + def _get_action(self): + result = super(ProcurementRule, self)._get_action() return result + [('move', _('Move From Another Location'))] - def _get_rules(self, cr, uid, ids, context=None): - res = [] - for route in self.browse(cr, uid, ids): - res += [x.id for x in route.pull_ids] - return res - _columns = { - 'location_id': fields.many2one('stock.location', 'Procurement Location'), - 'location_src_id': fields.many2one('stock.location', 'Source Location', - help="Source location is action=move"), - 'route_id': fields.many2one('stock.location.route', 'Route', - help="If route_id is False, the rule is global"), - 'procure_method': fields.selection([('make_to_stock', 'Take From Stock'), ('make_to_order', 'Create Procurement')], 'Move Supply Method', required=True, - help="""Determines the procurement method of the stock move that will be generated: whether it will need to 'take from the available stock' in its source location or needs to ignore its stock and create a procurement over there."""), - 'route_sequence': fields.related('route_id', 'sequence', string='Route Sequence', - store={ - 'stock.location.route': (_get_rules, ['sequence'], 10), - 'procurement.rule': (lambda self, cr, uid, ids, c={}: ids, ['route_id'], 10), - }), - 'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type', - help="Picking Type determines the way the picking should be shown in the view, reports, ..."), - 'delay': fields.integer('Number of Days'), - 'partner_address_id': fields.many2one('res.partner', 'Partner Address'), - 'propagate': fields.boolean('Propagate cancel and split', help='If checked, when the previous move of the move (which was generated by a next procurement) is cancelled or split, the move generated by this move will too'), - 'warehouse_id': fields.many2one('stock.warehouse', 'Served Warehouse', help='The warehouse this rule is for'), - 'propagate_warehouse_id': fields.many2one('stock.warehouse', 'Warehouse to Propagate', help="The warehouse to propagate on the created move/procurement, which can be different of the warehouse this rule is for (e.g for resupplying rules from another warehouse)"), - } - - _defaults = { - 'procure_method': 'make_to_stock', - 'propagate': True, - 'delay': 0, - } - -class procurement_order(osv.osv): +class ProcurementOrder(models.Model): _inherit = "procurement.order" - _columns = { - 'location_id': fields.many2one('stock.location', 'Procurement Location'), # not required because task may create procurements that aren't linked to a location with sale_service - 'partner_dest_id': fields.many2one('res.partner', 'Customer Address', help="In case of dropshipping, we need to know the destination address more precisely"), - 'move_ids': fields.one2many('stock.move', 'procurement_id', 'Moves', help="Moves created by the procurement"), - 'move_dest_id': fields.many2one('stock.move', 'Destination Move', help="Move which caused (created) the procurement"), - 'route_ids': fields.many2many('stock.location.route', 'stock_location_route_procurement', 'procurement_id', 'route_id', 'Preferred Routes', help="Preferred route to be followed by the procurement order. Usually copied from the generating document (SO) but could be set up manually."), - 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', help="Warehouse to consider for the route selection"), - 'orderpoint_id': fields.many2one('stock.warehouse.orderpoint', 'Minimum Stock Rule'), - } - def propagate_cancels(self, cr, uid, ids, context=None): - move_cancel = [] - for procurement in self.browse(cr, uid, ids, context=context): - if procurement.rule_id.action == 'move' and procurement.move_ids: - move_cancel += [m.id for m in procurement.move_ids] - if move_cancel: - self.pool.get('stock.move').action_cancel(cr, uid, move_cancel, context=context) - return True + location_id = fields.Many2one('stock.location', 'Procurement Location') # not required because task may create procurements that aren't linked to a location with sale_service + partner_dest_id = fields.Many2one('res.partner', 'Customer Address', help="In case of dropshipping, we need to know the destination address more precisely") + move_ids = fields.One2many('stock.move', 'procurement_id', 'Moves', help="Moves created by the procurement") + move_dest_id = fields.Many2one('stock.move', 'Destination Move', help="Move which caused (created) the procurement") + route_ids = fields.Many2many( + 'stock.location.route', 'stock_location_route_procurement', 'procurement_id', 'route_id', 'Preferred Routes', + help="Preferred route to be followed by the procurement order. Usually copied from the generating document (SO) but could be set up manually.") + warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse', help="Warehouse to consider for the route selection") + orderpoint_id = fields.Many2one('stock.warehouse.orderpoint', 'Minimum Stock Rule') - def cancel(self, cr, uid, ids, context=None): - if context is None: - context = {} - to_cancel_ids = self.get_cancel_ids(cr, uid, ids, context=context) - ctx = context.copy() - #set the context for the propagation of the procurement cancellation - ctx['cancel_procurement'] = True - self.propagate_cancels(cr, uid, to_cancel_ids, context=ctx) - return super(procurement_order, self).cancel(cr, uid, to_cancel_ids, context=ctx) + @api.onchange('warehouse_id') + def onchange_warehouse_id(self): + if self.warehouse_id: + self.location_id = self.warehouse_id.lot_stock_id.id - def _find_parent_locations(self, cr, uid, ids, context=None): - procurement = self.browse(cr, uid, ids[0], context=context) - location = procurement.location_id - res = [location.id] - while location.location_id: + @api.multi + def propagate_cancels(self): + # set the context for the propagation of the procurement cancellation + # TDE FIXME: was in cancel, moved here for consistency + cancel_moves = self.with_context(cancel_procurement=True).filtered(lambda order: order.rule_id.action == 'move').mapped('move_ids') + if cancel_moves: + cancel_moves.action_cancel() + return self.search([('move_dest_id', 'in', cancel_moves.filtered(lambda move: move.propagate).ids)]) + + @api.multi + def cancel(self): + propagated_procurements = self.filtered(lambda order: order.state != 'done').propagate_cancels() + if propagated_procurements: + propagated_procurements.cancel() + return super(ProcurementOrder, self).cancel() + + @api.multi + def do_view_pickings(self): + """ Return an action to display the pickings belonging to the same + procurement group of given ids. """ + action = self.env.ref('stock.do_view_pickings').read()[0] + action['domain'] = [('group_id', 'in', self.mapped('group_id').ids)] + return action + + @api.multi + @api.returns('procurement.rule', lambda value: value.id if value else False) + def _find_suitable_rule(self): + rule = super(ProcurementOrder, self)._find_suitable_rule() + if not rule: + # a rule defined on 'Stock' is suitable for a procurement in 'Stock\Bin A' + all_parent_location_ids = self._find_parent_locations() + rule = self._search_suitable_rule([('location_id', 'in', all_parent_location_ids.ids)]) + return rule + + def _find_parent_locations(self): + parent_locations = self.env['stock.location'] + location = self.location_id + while location: + parent_locations |= location location = location.location_id - res.append(location.id) - return res + return parent_locations - def change_warehouse_id(self, cr, uid, ids, warehouse_id, context=None): - if warehouse_id: - warehouse = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context) - return {'value': {'location_id': warehouse.lot_stock_id.id}} - return {} - - def _search_suitable_rule(self, cr, uid, ids, domain, context=None): - '''we try to first find a rule among the ones defined on the procurement order group and if none is found, we try on the routes defined for the product, and finally we fallback on the default behavior''' - procurement = self.browse(cr, uid, ids[0], context=context) - pull_obj = self.pool.get('procurement.rule') - warehouse_route_ids = [] - if procurement.warehouse_id: - domain += ['|', ('warehouse_id', '=', procurement.warehouse_id.id), ('warehouse_id', '=', False)] - warehouse_route_ids = [x.id for x in procurement.warehouse_id.route_ids] - product_route_ids = [x.id for x in procurement.product_id.route_ids + procurement.product_id.categ_id.total_route_ids] - procurement_route_ids = [x.id for x in procurement.route_ids] - res = pull_obj.search(cr, uid, domain + [('route_id', 'in', procurement_route_ids)], order='route_sequence, sequence', context=context) + def _search_suitable_rule(self, domain): + """ First find a rule among the ones defined on the procurement order + group; then try on the routes defined for the product; finally fallback + on the default behavior """ + if self.warehouse_id: + domain = expression.AND([['|', ('warehouse_id', '=', self.warehouse_id.id), ('warehouse_id', '=', False)], domain]) + Pull = self.env['procurement.rule'] + res = self.env['procurement.rule'] + if self.route_ids: + res = Pull.search(expression.AND([[('route_id', 'in', self.route_ids.ids)], domain]), order='route_sequence, sequence', limit=1) if not res: - res = pull_obj.search(cr, uid, domain + [('route_id', 'in', product_route_ids)], order='route_sequence, sequence', context=context) - if not res: - res = warehouse_route_ids and pull_obj.search(cr, uid, domain + [('route_id', 'in', warehouse_route_ids)], order='route_sequence, sequence', context=context) or [] - if not res: - res = pull_obj.search(cr, uid, domain + [('route_id', '=', False)], order='sequence', context=context) + product_routes = self.product_id.route_ids | self.product_id.categ_id.total_route_ids + if product_routes: + res = Pull.search(expression.AND([[('route_id', 'in', product_routes.ids)], domain]), order='route_sequence, sequence', limit=1) + if not res: + warehouse_routes = self.warehouse_id.route_ids + if warehouse_routes: + res = Pull.search(expression.AND([[('route_id', 'in', warehouse_routes.ids)], domain]), order='route_sequence, sequence', limit=1) + if not res: + res = Pull.search(expression.AND([[('route_id', '=', False)], domain]), order='sequence', limit=1) return res - def _find_suitable_rule(self, cr, uid, ids, context=None): - procurement = self.browse(cr, uid, ids[0], context=context) - rule_id = super(procurement_order, self)._find_suitable_rule(cr, uid, ids, context=context) - if not rule_id: - #a rule defined on 'Stock' is suitable for a procurement in 'Stock\Bin A' - all_parent_location_ids = procurement._find_parent_locations() - rule_id = procurement._search_suitable_rule([('location_id', 'in', all_parent_location_ids)]) - rule_id = rule_id and rule_id[0] or False - return rule_id - - def _run_move_create(self, cr, uid, ids, context=None): + def _get_stock_move_values(self): ''' Returns a dictionary of values that will be used to create a stock move from a procurement. This function assumes that the given procurement has a rule (action == 'move') set on it. :param procurement: browse record :rtype: dictionary ''' - procurement = self.browse(cr, uid, ids[0], context=context) - newdate = (datetime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S') - relativedelta(days=procurement.rule_id.delay or 0)).strftime('%Y-%m-%d %H:%M:%S') group_id = False - if procurement.rule_id.group_propagation_option == 'propagate': - group_id = procurement.group_id and procurement.group_id.id or False - elif procurement.rule_id.group_propagation_option == 'fixed': - group_id = procurement.rule_id.group_id and procurement.rule_id.group_id.id or False - #it is possible that we've already got some move done, so check for the done qty and create - #a new move with the correct qty - already_done_qty = 0 - for move in procurement.move_ids: - already_done_qty += move.product_uom_qty if move.state == 'done' else 0 - qty_left = max(procurement.product_qty - already_done_qty, 0) - vals = { - 'name': procurement.name, - 'company_id': procurement.rule_id.company_id.id or procurement.rule_id.location_src_id.company_id.id or procurement.rule_id.location_id.company_id.id or procurement.company_id.id, - 'product_id': procurement.product_id.id, - 'product_uom': procurement.product_uom.id, + if self.rule_id.group_propagation_option == 'propagate': + group_id = self.group_id.id + elif self.rule_id.group_propagation_option == 'fixed': + group_id = self.rule_id.group_id.id + date_expected = (datetime.strptime(self.date_planned, DEFAULT_SERVER_DATETIME_FORMAT) - relativedelta(days=self.rule_id.delay or 0)).strftime(DEFAULT_SERVER_DATETIME_FORMAT) + # it is possible that we've already got some move done, so check for the done qty and create + # a new move with the correct qty + qty_done = sum(self.move_ids.filtered(lambda move: move.state == 'done').mapped('product_uom_qty')) + qty_left = max(self.product_qty - qty_done, 0) + return { + 'name': self.name, + 'company_id': self.rule_id.company_id.id or self.rule_id.location_src_id.company_id.id or self.rule_id.location_id.company_id.id or self.company_id.id, + 'product_id': self.product_id.id, + 'product_uom': self.product_uom.id, 'product_uom_qty': qty_left, - 'partner_id': procurement.rule_id.partner_address_id.id or (procurement.group_id and procurement.group_id.partner_id.id) or False, - 'location_id': procurement.rule_id.location_src_id.id, - 'location_dest_id': procurement.location_id.id, - 'move_dest_id': procurement.move_dest_id and procurement.move_dest_id.id or False, - 'procurement_id': procurement.id, - 'rule_id': procurement.rule_id.id, - 'procure_method': procurement.rule_id.procure_method, - 'origin': procurement.origin, - 'picking_type_id': procurement.rule_id.picking_type_id.id, + 'partner_id': self.rule_id.partner_address_id.id or (self.group_id and self.group_id.partner_id.id) or False, + 'location_id': self.rule_id.location_src_id.id, + 'location_dest_id': self.location_id.id, + 'move_dest_id': self.move_dest_id and self.move_dest_id.id or False, + 'procurement_id': self.id, + 'rule_id': self.rule_id.id, + 'procure_method': self.rule_id.procure_method, + 'origin': self.origin, + 'picking_type_id': self.rule_id.picking_type_id.id, 'group_id': group_id, - 'route_ids': [(4, x.id) for x in procurement.route_ids], - 'warehouse_id': procurement.rule_id.propagate_warehouse_id.id or procurement.rule_id.warehouse_id.id, - 'date': newdate, - 'date_expected': newdate, - 'propagate': procurement.rule_id.propagate, - 'priority': procurement.priority, + 'route_ids': [(4, route.id) for route in self.route_ids], + 'warehouse_id': self.rule_id.propagate_warehouse_id.id or self.rule_id.warehouse_id.id, + 'date': date_expected, + 'date_expected': date_expected, + 'propagate': self.rule_id.propagate, + 'priority': self.priority, } - return vals + # compatibility + _run_move_create = _get_stock_move_values - def _run(self, cr, uid, ids, context=None): - procurement = self.browse(cr, uid, ids[0], context=context) - if procurement.rule_id and procurement.rule_id.action == 'move': - if not procurement.rule_id.location_src_id: - self.message_post(cr, uid, [procurement.id], body=_('No source location defined!'), context=context) + @api.multi + def _run(self): + if self.rule_id.action == 'move': + if not self.rule_id.location_src_id: + self.message_post(body=_('No source location defined!')) return False - move_obj = self.pool.get('stock.move') - move_dict = procurement._run_move_create() - #create the move as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example) - move_obj.create(cr, SUPERUSER_ID, move_dict, context=context) + # create the move as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example) + self.env['stock.move'].sudo().create(self._get_stock_move_values()) return True - return super(procurement_order, self)._run(cr, uid, ids, context=context) + return super(ProcurementOrder, self)._run() - def run(self, cr, uid, ids, autocommit=False, context=None): - new_ids = [x.id for x in self.browse(cr, uid, ids, context=context) if x.state not in ('running', 'done', 'cancel')] - context = dict(context or {}, procurement_auto_defer=True) #When creating - res = super(procurement_order, self).run(cr, uid, new_ids, autocommit=autocommit, context=context) + @api.multi + def run(self, autocommit=False): + # TDE CLEANME: unused context key procurement_auto_defer remove + new_self = self.filtered(lambda order: order.state not in ['running', 'done', 'cancel']) + res = super(ProcurementOrder, new_self).run(autocommit=autocommit) - #after all the procurements are run, check if some created a draft stock move that needs to be confirmed - #(we do that in batch because it fasts the picking assignation and the picking state computation) - move_to_confirm_ids = [] - for procurement in self.browse(cr, uid, new_ids, context=context): - if procurement.state == "running" and procurement.rule_id and procurement.rule_id.action == "move": - move_to_confirm_ids += [m.id for m in procurement.move_ids if m.state == 'draft'] - if move_to_confirm_ids: - self.pool.get('stock.move').action_confirm(cr, uid, move_to_confirm_ids, context=context) + # after all the procurements are run, check if some created a draft stock move that needs to be confirmed + # (we do that in batch because it fasts the picking assignation and the picking state computation) + new_self.filtered(lambda order: order.state == 'running' and order.rule_id.action == 'move').mapped('move_ids').filtered(lambda move: move.state == 'draft').action_confirm() + + # TDE FIXME: action_confirm in stock_move already call run() ... necessary ?? # If procurements created other procurements, run the created in batch - procurement_ids = self.search(cr, uid, [('move_dest_id.procurement_id', 'in', new_ids)], order='id', context=context) - if procurement_ids: - res = res and self.run(cr, uid, procurement_ids, autocommit=autocommit, context=context) + new_procurements = self.search([('move_dest_id.procurement_id', 'in', new_self.ids)], order='id') + if new_procurements: + res = new_procurements.run(autocommit=autocommit) return res - def _check(self, cr, uid, ids, context=None): - ''' Implement the procurement checking for rules of type 'move'. The procurement will be satisfied only if all related - moves are done/cancel and if the requested quantity is moved. - ''' - procurement = self.browse(cr, uid, ids[0], context=context) - if procurement.rule_id and procurement.rule_id.action == 'move': - uom_obj = self.pool.get('product.uom') + @api.multi + def _check(self): + """ Checking rules of type 'move': satisfied only if all related moves + are done/cancel and if the requested quantity is moved. """ + if self.rule_id.action == 'move': # In case Phantom BoM splits only into procurements - if not procurement.move_ids: + if not self.move_ids: return True - cancel_test_list = [x.state == 'cancel' for x in procurement.move_ids] - done_cancel_test_list = [x.state in ('done', 'cancel') for x in procurement.move_ids] - at_least_one_cancel = any(cancel_test_list) - all_done_or_cancel = all(done_cancel_test_list) - all_cancel = all(cancel_test_list) - if not all_done_or_cancel: + move_all_done_or_cancel = all(move.state in ['done', 'cancel'] for move in self.move_ids) + move_all_cancel = all(move.state == 'cancel' for move in self.move_ids) + if not move_all_done_or_cancel: return False - elif all_done_or_cancel and not all_cancel: + elif move_all_done_or_cancel and not move_all_cancel: return True - elif all_cancel: - self.message_post(cr, uid, [procurement.id], body=_('All stock moves have been cancelled for this procurement.'), context=context) - self.write(cr, uid, [procurement.id], {'state': 'cancel'}, context=context) - return False + else: + self.message_post(body=_('All stock moves have been cancelled for this procurement.')) + # TDE FIXME: strange that a check method actually modified the procurement... + self.write({'state': 'cancel'}) + return False + return super(ProcurementOrder, self)._check() - return super(procurement_order, self)._check(cr, uid, procurement, context) - - def do_view_pickings(self, cr, uid, ids, context=None): - ''' - This function returns an action that display the pickings of the procurements belonging - to the same procurement group of given ids. - ''' - mod_obj = self.pool.get('ir.model.data') - act_obj = self.pool.get('ir.actions.act_window') - result = mod_obj.get_object_reference(cr, uid, 'stock', 'do_view_pickings') - id = result and result[1] or False - result = act_obj.read(cr, uid, [id], context=context)[0] - group_ids = set([proc.group_id.id for proc in self.browse(cr, uid, ids, context=context) if proc.group_id]) - result['domain'] = "[('group_id','in',[" + ','.join(map(str, list(group_ids))) + "])]" - return result - - def run_scheduler(self, cr, uid, use_new_cursor=False, company_id=False, context=None): - ''' - Call the scheduler in order to check the running procurements (super method), to check the minimum stock rules + @api.model + def run_scheduler(self, use_new_cursor=False, company_id=False): + ''' Call the scheduler in order to check the running procurements (super method), to check the minimum stock rules and the availability of moves. This function is intended to be run for all the companies at the same time, so - we run functions as SUPERUSER to avoid intercompanies and access rights issues. - - @param self: The object pointer - @param cr: The current row, from the database cursor, - @param uid: The current user ID for security checks - @param ids: List of selected IDs - @param use_new_cursor: if set, use a dedicated cursor and auto-commit after processing each procurement. - This is appropriate for batch jobs only. - @param context: A standard dictionary for contextual values - @return: Dictionary of values - ''' - super(procurement_order, self).run_scheduler(cr, uid, use_new_cursor=use_new_cursor, company_id=company_id, context=context) - if context is None: - context = {} + we run functions as SUPERUSER to avoid intercompanies and access rights issues. ''' + super(ProcurementOrder, self).run_scheduler(use_new_cursor=use_new_cursor, company_id=company_id) try: if use_new_cursor: - cr = openerp.registry(cr.dbname).cursor() + cr = registry(self._cr.dbname).cursor() + self = self.with_env(self.env(cr=cr)) # TDE FIXME - move_obj = self.pool.get('stock.move') + # Minimum stock rules + self.sudo()._procure_orderpoint_confirm(use_new_cursor=use_new_cursor, company_id=company_id) - #Minimum stock rules - self._procure_orderpoint_confirm(cr, SUPERUSER_ID, use_new_cursor=use_new_cursor, company_id=company_id, context=context) - - #Search all confirmed stock_moves and try to assign them - confirmed_ids = move_obj.search(cr, uid, [('state', '=', 'confirmed')], limit=None, order='priority desc, date_expected asc', context=context) - for x in xrange(0, len(confirmed_ids), 100): - move_obj.action_assign(cr, uid, confirmed_ids[x:x + 100], context=context) + # Search all confirmed stock_moves and try to assign them + confirmed_moves = self.env['stock.move'].search([('state', '=', 'confirmed')], limit=None, order='priority desc, date_expected asc') + for x in xrange(0, len(confirmed_moves.ids), 100): + # TDE CLEANME: muf muf + self.env['stock.move'].browse(confirmed_moves.ids[x:x + 100]).action_assign() if use_new_cursor: - cr.commit() - + self._cr.commit() if use_new_cursor: - cr.commit() + self._cr.commit() finally: if use_new_cursor: try: - cr.close() + self._cr.close() except Exception: pass return {} - def _procurement_from_orderpoint_get_order(self, cr, uid, context=None): + @api.model + def _procurement_from_orderpoint_get_order(self): return 'location_id' - def _procurement_from_orderpoint_get_grouping_key(self, cr, uid, orderpoint_ids, context=None): - orderpoint = self.pool['stock.warehouse.orderpoint'].browse(cr, uid, orderpoint_ids[0], context=context) - return orderpoint.location_id.id + @api.model + def _procurement_from_orderpoint_get_grouping_key(self, orderpoint_ids): + orderpoints = self.env['stock.warehouse.orderpoint'].browse(orderpoint_ids) + return orderpoints.location_id.id - def _procurement_from_orderpoint_get_groups(self, cr, uid, orderpoint_ids, context=None): + @api.model + def _procurement_from_orderpoint_get_groups(self, orderpoint_ids): """ Make groups for a given orderpoint; by default schedule all operations in one without date """ return [{'to_date': False, 'procurement_values': dict()}] - def _procurement_from_orderpoint_post_process(self, cr, uid, orderpoint_ids, context=None): + @api.model + def _procurement_from_orderpoint_post_process(self, orderpoint_ids): return True - def _procure_orderpoint_confirm(self, cr, uid, use_new_cursor=False, company_id=False, context=None): - ''' - Create procurement based on Orderpoint - + @api.model + def _procure_orderpoint_confirm(self, use_new_cursor=False, company_id=False): + """ Create procurements based on orderpoints. :param bool use_new_cursor: if set, use a dedicated cursor and auto-commit after processing each procurement. This is appropriate for batch jobs only. - ''' - if context is None: - context = {} - procurement_context = dict(context, procurement_autorun_defer=True) + """ if use_new_cursor: - cr = openerp.registry(cr.dbname).cursor() - orderpoint_obj = self.pool.get('stock.warehouse.orderpoint') - procurement_obj = self.pool.get('procurement.order') - product_obj = self.pool.get('product.product') + cr = registry(self._cr.dbname).cursor() + self = self.with_env(self.env(cr=cr)) - orderpoint_ids = orderpoint_obj.search( - cr, uid, + OrderPoint = self.env['stock.warehouse.orderpoint'] + Procurement = self.env['procurement.order'] + ProcurementAutorundefer = Procurement.with_context(procurement_autorun_defer=True) + procurement_list = [] + + orderpoints_noprefetch = OrderPoint.with_context(prefetch_fields=False).search( company_id and [('company_id', '=', company_id)] or [], - order=self._procurement_from_orderpoint_get_order(cr, uid, context=context)) - procurement_ids = [] - while orderpoint_ids: - current_ids = orderpoint_ids[:1000] - orderpoint_ids = orderpoint_ids[1000:] - orderpoints = orderpoint_obj.browse(cr, uid, current_ids, context=context) + order=self._procurement_from_orderpoint_get_order()) + while orderpoints_noprefetch: + orderpoints = OrderPoint.browse(orderpoints_noprefetch[:1000].ids) + orderpoints_noprefetch = orderpoints_noprefetch[1000:] # Calculate groups that can be executed together - location_data = defaultdict(lambda: dict(products=list(), orderpoints=list(), groups=list())) - + location_data = defaultdict(lambda: dict(products=self.env['product.product'], orderpoints=self.env['stock.warehouse.orderpoint'], groups=list())) for orderpoint in orderpoints: - key = self._procurement_from_orderpoint_get_grouping_key(cr, uid, [orderpoint.id], context=context) + key = self._procurement_from_orderpoint_get_grouping_key([orderpoint.id]) location_data[key]['products'] += orderpoint.product_id location_data[key]['orderpoints'] += orderpoint - location_data[key]['groups'] = self._procurement_from_orderpoint_get_groups(cr, uid, [orderpoint.id], context=context) + location_data[key]['groups'] = self._procurement_from_orderpoint_get_groups([orderpoint.id]) for location_id, location_data in location_data.iteritems(): location_orderpoints = location_data['orderpoints'] - product_context = dict(context, location=location_orderpoints[0].location_id.id) - substract_quantity = self.pool['stock.warehouse.orderpoint'].subtract_procurements_from_orderpoints( - cr, uid, - [op.id for op in location_orderpoints], - context=context) + product_context = dict(self._context, location=location_orderpoints[0].location_id.id) + substract_quantity = location_orderpoints.subtract_procurements_from_orderpoints() for group in location_data['groups']: if group['to_date']: product_context['to_date'] = group['to_date'].strftime(DEFAULT_SERVER_DATETIME_FORMAT) - product_quantity = product_obj._product_available(cr, uid, [prod.id for prod in location_data['products']], context=product_context) + product_quantity = location_data['products'].with_context(product_context)._product_available() for orderpoint in location_orderpoints: try: op_product_virtual = product_quantity[orderpoint.product_id.id]['virtual_available'] @@ -375,27 +329,28 @@ class procurement_order(osv.osv): qty -= substract_quantity[orderpoint.id] qty_rounded = float_round(qty, precision_rounding=orderpoint.product_uom.rounding) if qty_rounded > 0: - new_procurement_id = procurement_obj.create( - cr, uid, - orderpoint._prepare_procurement_values(qty_rounded, **group['procurement_values']), - context=procurement_context) - procurement_ids.append(new_procurement_id) - self._procurement_from_orderpoint_post_process(cr, uid, [orderpoint.id], context=context) + new_procurement = ProcurementAutorundefer.create( + orderpoint._prepare_procurement_values(qty_rounded, **group['procurement_values'])) + procurement_list.append(new_procurement) + self._procurement_from_orderpoint_post_process([orderpoint.id]) if use_new_cursor: cr.commit() except OperationalError: if use_new_cursor: - orderpoint_ids += orderpoint.id + orderpoints_noprefetch += orderpoint.id cr.rollback() continue else: raise try: - procurement_ids.reverse() - self.run(cr, uid, procurement_ids, context=context) - procurement_ids = [] + # TDE CLEANME: use record set ? + procurement_list.reverse() + procurements = self.env['procurement.order'] + for p in procurement_list: + procurements += p + procurements.run() if use_new_cursor: cr.commit() except OperationalError: diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 33237711e09..aab7cb1ef36 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -652,7 +652,8 @@ class StockMove(models.Model): move.quants_unreserve() if self.env.context.get('cancel_procurement'): if move.propagate: - procurements.search([('move_dest_id', '=', move.id)]).cancel() + pass + # procurements.search([('move_dest_id', '=', move.id)]).cancel() else: if move.move_dest_id: if move.propagate: diff --git a/addons/stock/views/procurement_views.xml b/addons/stock/views/procurement_views.xml index 241b0a296d8..3d5de1a7d37 100644 --- a/addons/stock/views/procurement_views.xml +++ b/addons/stock/views/procurement_views.xml @@ -20,7 +20,7 @@ - +