1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
23 from psycopg2 import OperationalError
25 from openerp import SUPERUSER_ID
26 from openerp.osv import fields, osv
27 import openerp.addons.decimal_precision as dp
28 from openerp.tools.translate import _
31 PROCUREMENT_PRIORITIES = [('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')]
33 class procurement_group(osv.osv):
35 The procurement group class is used to group products together
36 when computing procurements. (tasks, physical products, ...)
38 The goal is that when you have one sale order of several products
39 and the products are pulled from the same or several location(s), to keep
40 having the moves grouped into pickings that represent the sale order.
42 Used in: sales order (to group delivery order lines like the so), pull/push
43 rules (to pack like the delivery order), on orderpoints (e.g. for wave picking
44 all the similar products together).
46 Grouping is made only if the source and the destination is the same.
47 Suppose you have 4 lines on a picking from Output where 2 lines will need
48 to come from Input (crossdock) and 2 lines coming from Stock -> Output As
49 the four procurement orders will have the same group ids from the SO, the
50 move from input will have a stock.picking with 2 grouped lines and the move
51 from stock will have 2 grouped lines also.
53 The name is usually the name of the original document (sale order) or a
54 sequence computed if created manually.
56 _name = 'procurement.group'
57 _description = 'Procurement Requisition'
60 'name': fields.char('Reference', required=True),
61 'move_type': fields.selection([
62 ('direct', 'Partial'), ('one', 'All at once')],
63 'Delivery Method', required=True),
64 'procurement_ids': fields.one2many('procurement.order', 'group_id', 'Procurements'),
67 'name': lambda self, cr, uid, c: self.pool.get('ir.sequence').next_by_code(cr, uid, 'procurement.group') or '',
68 'move_type': lambda self, cr, uid, c: 'direct'
71 class procurement_rule(osv.osv):
73 A rule describe what a procurement should do; produce, buy, move, ...
75 _name = 'procurement.rule'
76 _description = "Procurement Rule"
79 def _get_action(self, cr, uid, context=None):
83 'name': fields.char('Name', required=True,
84 help="This field will fill the packing origin and the name of its moves"),
85 'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the rule without removing it."),
86 'group_propagation_option': fields.selection([('none', 'Leave Empty'), ('propagate', 'Propagate'), ('fixed', 'Fixed')], string="Propagation of Procurement Group"),
87 'group_id': fields.many2one('procurement.group', 'Fixed Procurement Group'),
88 'action': fields.selection(selection=lambda s, cr, uid, context=None: s._get_action(cr, uid, context=context),
89 string='Action', required=True),
90 'sequence': fields.integer('Sequence'),
91 'company_id': fields.many2one('res.company', 'Company'),
95 'group_propagation_option': 'propagate',
101 class procurement_order(osv.osv):
105 _name = "procurement.order"
106 _description = "Procurement"
107 _order = 'priority desc, date_planned, id asc'
108 _inherit = ['mail.thread']
111 'name': fields.text('Description', required=True),
113 'origin': fields.char('Source Document',
114 help="Reference of the document that created this Procurement.\n"
115 "This is automatically completed by Odoo."),
116 'company_id': fields.many2one('res.company', 'Company', required=True),
118 # These two fields are used for shceduling
119 'priority': fields.selection(PROCUREMENT_PRIORITIES, 'Priority', required=True, select=True, track_visibility='onchange'),
120 'date_planned': fields.datetime('Scheduled Date', required=True, select=True, track_visibility='onchange'),
122 'group_id': fields.many2one('procurement.group', 'Procurement Group'),
123 '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."),
125 'product_id': fields.many2one('product.product', 'Product', required=True, states={'confirmed': [('readonly', False)]}, readonly=True),
126 'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, states={'confirmed': [('readonly', False)]}, readonly=True),
127 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, states={'confirmed': [('readonly', False)]}, readonly=True),
129 'product_uos_qty': fields.float('UoS Quantity', states={'confirmed': [('readonly', False)]}, readonly=True),
130 'product_uos': fields.many2one('product.uom', 'Product UoS', states={'confirmed': [('readonly', False)]}, readonly=True),
132 'state': fields.selection([
133 ('cancel', 'Cancelled'),
134 ('confirmed', 'Confirmed'),
135 ('exception', 'Exception'),
136 ('running', 'Running'),
138 ], 'Status', required=True, track_visibility='onchange', copy=False),
142 'state': 'confirmed',
144 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
145 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'procurement.order', context=c)
148 def unlink(self, cr, uid, ids, context=None):
149 procurements = self.read(cr, uid, ids, ['state'], context=context)
151 for s in procurements:
152 if s['state'] == 'cancel':
153 unlink_ids.append(s['id'])
155 raise osv.except_osv(_('Invalid Action!'),
156 _('Cannot delete Procurement Order(s) which are in %s state.') % s['state'])
157 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
159 def do_view_procurements(self, cr, uid, ids, context=None):
161 This function returns an action that display existing procurement orders
162 of same procurement group of given ids.
164 act_obj = self.pool.get('ir.actions.act_window')
165 action_id = self.pool.get('ir.model.data').xmlid_to_res_id(cr, uid, 'procurement.do_view_procurements', raise_if_not_found=True)
166 result = act_obj.read(cr, uid, [action_id], context=context)[0]
167 group_ids = set([proc.group_id.id for proc in self.browse(cr, uid, ids, context=context) if proc.group_id])
168 result['domain'] = "[('group_id','in',[" + ','.join(map(str, list(group_ids))) + "])]"
171 def onchange_product_id(self, cr, uid, ids, product_id, context=None):
172 """ Finds UoM and UoS of changed product.
173 @param product_id: Changed id of product.
174 @return: Dictionary of values.
177 w = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
179 'product_uom': w.uom_id.id,
180 'product_uos': w.uos_id and w.uos_id.id or w.uom_id.id
185 def get_cancel_ids(self, cr, uid, ids, context=None):
186 return [proc.id for proc in self.browse(cr, uid, ids, context=context) if proc.state != 'done']
188 def cancel(self, cr, uid, ids, context=None):
189 #cancel only the procurements that aren't done already
190 to_cancel_ids = self.get_cancel_ids(cr, uid, ids, context=context)
192 return self.write(cr, uid, to_cancel_ids, {'state': 'cancel'}, context=context)
194 def reset_to_confirmed(self, cr, uid, ids, context=None):
195 return self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
197 def run(self, cr, uid, ids, autocommit=False, context=None):
198 for procurement_id in ids:
199 #we intentionnaly do the browse under the for loop to avoid caching all ids which would be resource greedy
200 #and useless as we'll make a refresh later that will invalidate all the cache (and thus the next iteration
201 #will fetch all the ids again)
202 procurement = self.browse(cr, uid, procurement_id, context=context)
203 if procurement.state not in ("running", "done"):
205 if self._assign(cr, uid, procurement, context=context):
206 res = self._run(cr, uid, procurement, context=context or {})
208 self.write(cr, uid, [procurement.id], {'state': 'running'}, context=context)
210 self.write(cr, uid, [procurement.id], {'state': 'exception'}, context=context)
212 self.message_post(cr, uid, [procurement.id], body=_('No rule matching this procurement'), context=context)
213 self.write(cr, uid, [procurement.id], {'state': 'exception'}, context=context)
216 except OperationalError:
224 def check(self, cr, uid, ids, autocommit=False, context=None):
226 for procurement in self.browse(cr, uid, ids, context=context):
228 result = self._check(cr, uid, procurement, context=context)
230 done_ids.append(procurement.id)
233 except OperationalError:
240 self.write(cr, uid, done_ids, {'state': 'done'}, context=context)
244 # Method to overwrite in different procurement modules
246 def _find_suitable_rule(self, cr, uid, procurement, context=None):
247 '''This method returns a procurement.rule that depicts what to do with the given procurement
248 in order to complete its needs. It returns False if no suiting rule is found.
249 :param procurement: browse record
254 def _assign(self, cr, uid, procurement, context=None):
255 '''This method check what to do with the given procurement in order to complete its needs.
256 It returns False if no solution is found, otherwise it stores the matching rule (if any) and
258 :param procurement: browse record
261 #if the procurement already has a rule assigned, we keep it (it has a higher priority as it may have been chosen manually)
262 if procurement.rule_id:
264 elif procurement.product_id.type != 'service':
265 rule_id = self._find_suitable_rule(cr, uid, procurement, context=context)
267 self.write(cr, uid, [procurement.id], {'rule_id': rule_id}, context=context)
271 def _run(self, cr, uid, procurement, context=None):
272 '''This method implements the resolution of the given procurement
273 :param procurement: browse record
274 :returns: True if the resolution of the procurement was a success, False otherwise to set it in exception
278 def _check(self, cr, uid, procurement, context=None):
279 '''Returns True if the given procurement is fulfilled, False otherwise
280 :param procurement: browse record
288 def run_scheduler(self, cr, uid, use_new_cursor=False, company_id = False, context=None):
290 Call the scheduler to check the procurement order. This is intented to be done for all existing companies at
291 the same time, so we're running all the methods as SUPERUSER to avoid intercompany and access rights issues.
293 @param self: The object pointer
294 @param cr: The current row, from the database cursor,
295 @param uid: The current user ID for security checks
296 @param ids: List of selected IDs
297 @param use_new_cursor: if set, use a dedicated cursor and auto-commit after processing each procurement.
298 This is appropriate for batch jobs only.
299 @param context: A standard dictionary for contextual values
300 @return: Dictionary of values
306 cr = openerp.registry(cr.dbname).cursor()
308 # Run confirmed procurements
309 dom = [('state', '=', 'confirmed')]
311 dom += [('company_id', '=', company_id)]
314 ids = self.search(cr, SUPERUSER_ID, dom, context=context)
315 if not ids or prev_ids == ids:
319 self.run(cr, SUPERUSER_ID, ids, autocommit=use_new_cursor, context=context)
323 # Check if running procurements are done
325 dom = [('state', '=', 'running')]
327 dom += [('company_id', '=', company_id)]
330 ids = self.search(cr, SUPERUSER_ID, dom, offset=offset, context=context)
331 if not ids or prev_ids == ids:
335 self.check(cr, SUPERUSER_ID, ids, autocommit=use_new_cursor, context=context)
347 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: