##############################################################################
import time
+from psycopg2 import OperationalError
-from datetime import datetime
-from dateutil.relativedelta import relativedelta
-
+from openerp import SUPERUSER_ID
from openerp.osv import fields, osv
import openerp.addons.decimal_precision as dp
from openerp.tools.translate import _
import openerp
+PROCUREMENT_PRIORITIES = [('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')]
+
class procurement_group(osv.osv):
'''
- The procurement requirement class is used to group products together
+ The procurement group class is used to group products together
when computing procurements. (tasks, physical products, ...)
The goal is that when you have one sale order of several products
_description = 'Procurement Requisition'
_order = "id desc"
_columns = {
- 'name': fields.char('Reference', required=True),
+ 'name': fields.char('Reference', required=True),
'move_type': fields.selection([
('direct', 'Partial'), ('one', 'All at once')],
'Delivery Method', required=True),
- 'partner_id': fields.many2one('res.partner', string = 'Partner'), #Sale should pass it here
- 'procurement_ids': fields.one2many('procurement.order', 'group_id', 'Procurements'),
+ 'procurement_ids': fields.one2many('procurement.order', 'group_id', 'Procurements'),
}
_defaults = {
'name': lambda self, cr, uid, c: self.pool.get('ir.sequence').get(cr, uid, 'procurement.group') or '',
- 'move_type': lambda self, cr, uid, c: 'one'
+ 'move_type': lambda self, cr, uid, c: 'direct'
}
class procurement_rule(osv.osv):
'''
_name = 'procurement.rule'
_description = "Procurement Rule"
+ _order = "name"
def _get_action(self, cr, uid, context=None):
return []
_columns = {
'name': fields.char('Name', required=True,
help="This field will fill the packing origin and the name of its moves"),
- 'group_id': fields.many2one('procurement.group', 'Procurement Group'),
+ 'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the rule without removing it."),
+ 'group_propagation_option': fields.selection([('none', 'Leave Empty'), ('propagate', 'Propagate'), ('fixed', 'Fixed')], string="Propagation of Procurement Group"),
+ 'group_id': fields.many2one('procurement.group', 'Fixed Procurement Group'),
'action': fields.selection(selection=lambda s, cr, uid, context=None: s._get_action(cr, uid, context=context),
string='Action', required=True),
+ 'sequence': fields.integer('Sequence'),
+ 'company_id': fields.many2one('res.company', 'Company'),
+ }
+
+ _defaults = {
+ 'group_propagation_option': 'propagate',
+ 'sequence': 20,
+ 'active': True,
}
"""
_name = "procurement.order"
_description = "Procurement"
- _order = 'priority desc,date_planned'
+ _order = 'priority desc, date_planned, id asc'
_inherit = ['mail.thread']
_log_create = False
_columns = {
'name': fields.text('Description', required=True),
- 'origin': fields.char('Source Document', size=64,
+ 'origin': fields.char('Source Document',
help="Reference of the document that created this Procurement.\n"
- "This is automatically completed by OpenERP."),
+ "This is automatically completed by Odoo."),
'company_id': fields.many2one('res.company', 'Company', required=True),
# These two fields are used for shceduling
- 'priority': fields.selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority', required=True, select=True),
- 'date_planned': fields.datetime('Scheduled date', required=True, select=True),
+ 'priority': fields.selection(PROCUREMENT_PRIORITIES, 'Priority', required=True, select=True, track_visibility='onchange'),
+ 'date_planned': fields.datetime('Scheduled Date', required=True, select=True, track_visibility='onchange'),
- 'group_id': fields.many2one('procurement.group', 'Procurement Requisition'),
- 'rule_id': fields.many2one('procurement.rule', 'Rule'),
+ 'group_id': fields.many2one('procurement.group', 'Procurement Group'),
+ 'rule_id': fields.many2one('procurement.rule', 'Rule', track_visibility='onchange', help="Chosen rule for the procurement resolution. Usually chosen by the system but can be manually set by the procurement manager to force an unusual behavior."),
'product_id': fields.many2one('product.product', 'Product', required=True, states={'confirmed': [('readonly', False)]}, readonly=True),
'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, states={'confirmed': [('readonly', False)]}, readonly=True),
('exception', 'Exception'),
('running', 'Running'),
('done', 'Done')
- ], 'Status', required=True, track_visibility='onchange'),
- 'message': fields.text('Latest error', help="Exception occurred while computing procurement orders.", track_visibility='onchange'),
+ ], 'Status', required=True, track_visibility='onchange', copy=False),
}
_defaults = {
'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'procurement.order', context=c)
}
+
+ def unlink(self, cr, uid, ids, context=None):
+ procurements = self.read(cr, uid, ids, ['state'], context=context)
+ unlink_ids = []
+ for s in procurements:
+ if s['state'] == 'cancel':
+ unlink_ids.append(s['id'])
+ else:
+ raise osv.except_osv(_('Invalid Action!'),
+ _('Cannot delete Procurement Order(s) which are in %s state.') % s['state'])
+ return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
+
+ def do_view_procurements(self, cr, uid, ids, context=None):
+ '''
+ This function returns an action that display existing procurement orders
+ of same procurement group of given ids.
+ '''
+ act_obj = self.pool.get('ir.actions.act_window')
+ action_id = self.pool.get('ir.model.data').xmlid_to_res_id(cr, uid, 'procurement.do_view_procurements', raise_if_not_found=True)
+ result = act_obj.read(cr, uid, [action_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 onchange_product_id(self, cr, uid, ids, product_id, context=None):
""" Finds UoM and UoS of changed product.
@param product_id: Changed id of product.
return {'value': v}
return {}
- def cancel(self, cr, uid, ids, context=None):
- self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
- return True
+ def get_cancel_ids(self, cr, uid, ids, context=None):
+ return [proc.id for proc in self.browse(cr, uid, ids, context=context) if proc.state != 'done']
- def run(self, cr, uid, ids, context=None):
- for procurement in self.browse(cr, uid, ids, context=context):
+ def cancel(self, cr, uid, ids, context=None):
+ #cancel only the procurements that aren't done already
+ to_cancel_ids = self.get_cancel_ids(cr, uid, ids, context=context)
+ if to_cancel_ids:
+ return self.write(cr, uid, to_cancel_ids, {'state': 'cancel'}, context=context)
+
+ def reset_to_confirmed(self, cr, uid, ids, context=None):
+ return self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
+
+ def run(self, cr, uid, ids, autocommit=False, context=None):
+ for procurement_id in ids:
+ #we intentionnaly do the browse under the for loop to avoid caching all ids which would be resource greedy
+ #and useless as we'll make a refresh later that will invalidate all the cache (and thus the next iteration
+ #will fetch all the ids again)
+ procurement = self.browse(cr, uid, procurement_id, context=context)
if procurement.state not in ("running", "done"):
- if self._assign(cr, uid, procurement, context=context):
- procurement.refresh()
- self._run(cr, uid, procurement, context=context or {})
- self.write(cr, uid, [procurement.id], {'state': 'running'}, context=context)
- else:
- self.message_post(cr, uid, [procurement.id], body=_('No rule matching this procurement'), context=context)
- self.write(cr, uid, [procurement.id], {'state': 'exception'}, context=context)
+ try:
+ if self._assign(cr, uid, procurement, context=context):
+ res = self._run(cr, uid, procurement, context=context or {})
+ if res:
+ self.write(cr, uid, [procurement.id], {'state': 'running'}, context=context)
+ else:
+ self.write(cr, uid, [procurement.id], {'state': 'exception'}, context=context)
+ else:
+ self.message_post(cr, uid, [procurement.id], body=_('No rule matching this procurement'), context=context)
+ self.write(cr, uid, [procurement.id], {'state': 'exception'}, context=context)
+ if autocommit:
+ cr.commit()
+ except OperationalError:
+ if autocommit:
+ cr.rollback()
+ continue
+ else:
+ raise
return True
- def check(self, cr, uid, ids, context=None):
- done = []
+ def check(self, cr, uid, ids, autocommit=False, context=None):
+ done_ids = []
for procurement in self.browse(cr, uid, ids, context=context):
- result = self._check(cr, uid, procurement, context=context)
- if result:
- self.write(cr, uid, [procurement.id], {'state': 'done'}, context=context)
- done.append(procurement.id)
- return done
+ try:
+ result = self._check(cr, uid, procurement, context=context)
+ if result:
+ done_ids.append(procurement.id)
+ if autocommit:
+ cr.commit()
+ except OperationalError:
+ if autocommit:
+ cr.rollback()
+ continue
+ else:
+ raise
+ if done_ids:
+ self.write(cr, uid, done_ids, {'state': 'done'}, context=context)
+ return done_ids
#
# Method to overwrite in different procurement modules
:param procurement: browse record
:rtype: boolean
'''
- rule_id = self._find_suitable_rule(cr, uid, procurement, context=context)
- if rule_id:
- self.write(cr, uid, [procurement.id], {'rule_id': rule_id}, context=context)
+ #if the procurement already has a rule assigned, we keep it (it has a higher priority as it may have been chosen manually)
+ if procurement.rule_id:
return True
+ elif procurement.product_id.type != 'service':
+ rule_id = self._find_suitable_rule(cr, uid, procurement, context=context)
+ if rule_id:
+ self.write(cr, uid, [procurement.id], {'rule_id': rule_id}, context=context)
+ return True
return False
def _run(self, cr, uid, procurement, context=None):
'''This method implements the resolution of the given procurement
:param procurement: browse record
+ :returns: True if the resolution of the procurement was a success, False otherwise to set it in exception
'''
return True
#
# Scheduler
#
- def run_scheduler(self, cr, uid, use_new_cursor=False, context=None):
+ def run_scheduler(self, cr, uid, use_new_cursor=False, company_id = False, context=None):
'''
- Call the scheduler to check the procurement order
+ Call the scheduler to check the procurement order. This is intented to be done for all existing companies at
+ the same time, so we're running all the methods as SUPERUSER to avoid intercompany 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: False or the dbname
+ @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
'''
context = {}
try:
if use_new_cursor:
- cr = openerp.registry(use_new_cursor).db.cursor()
-
- company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
- maxdate = (datetime.today() + relativedelta(days=company.schedule_range)).strftime('%Y-%m-%d %H:%M:%S')
+ cr = openerp.registry(cr.dbname).cursor()
# Run confirmed procurements
+ dom = [('state', '=', 'confirmed')]
+ if company_id:
+ dom += [('company_id', '=', company_id)]
+ prev_ids = []
while True:
- ids = self.search(cr, uid, [('state', '=', 'confirmed'), ('date_planned', '<=', maxdate)], context=context)
- if not ids:
+ ids = self.search(cr, SUPERUSER_ID, dom, context=context)
+ if not ids or prev_ids == ids:
break
- self.run(cr, uid, ids, context=context)
+ else:
+ prev_ids = ids
+ self.run(cr, SUPERUSER_ID, ids, autocommit=use_new_cursor, context=context)
if use_new_cursor:
cr.commit()
# Check if running procurements are done
offset = 0
+ dom = [('state', '=', 'running')]
+ if company_id:
+ dom += [('company_id', '=', company_id)]
+ prev_ids = []
while True:
- ids = self.search(cr, uid, [('state', '=', 'running'), ('date_planned', '<=', maxdate)], offset=offset, context=context)
- if not ids:
+ ids = self.search(cr, SUPERUSER_ID, dom, offset=offset, context=context)
+ if not ids or prev_ids == ids:
break
- done = self.check(cr, uid, ids, context=context)
- offset += len(ids) - len(done)
- if use_new_cursor and len(done):
+ else:
+ prev_ids = ids
+ self.check(cr, SUPERUSER_ID, ids, autocommit=use_new_cursor, context=context)
+ if use_new_cursor:
cr.commit()
finally:
cr.close()
except Exception:
pass
+
return {}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: