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 ##############################################################################
24 from openerp import SUPERUSER_ID
25 from openerp.osv import fields, osv
26 import openerp.addons.decimal_precision as dp
27 from openerp.tools.translate import _
30 class procurement_group(osv.osv):
32 The procurement group class is used to group products together
33 when computing procurements. (tasks, physical products, ...)
35 The goal is that when you have one sale order of several products
36 and the products are pulled from the same or several location(s), to keep
37 having the moves grouped into pickings that represent the sale order.
39 Used in: sales order (to group delivery order lines like the so), pull/push
40 rules (to pack like the delivery order), on orderpoints (e.g. for wave picking
41 all the similar products together).
43 Grouping is made only if the source and the destination is the same.
44 Suppose you have 4 lines on a picking from Output where 2 lines will need
45 to come from Input (crossdock) and 2 lines coming from Stock -> Output As
46 the four procurement orders will have the same group ids from the SO, the
47 move from input will have a stock.picking with 2 grouped lines and the move
48 from stock will have 2 grouped lines also.
50 The name is usually the name of the original document (sale order) or a
51 sequence computed if created manually.
53 _name = 'procurement.group'
54 _description = 'Procurement Requisition'
57 'name': fields.char('Reference', required=True),
58 'move_type': fields.selection([
59 ('direct', 'Partial'), ('one', 'All at once')],
60 'Delivery Method', required=True),
61 'procurement_ids': fields.one2many('procurement.order', 'group_id', 'Procurements'),
64 'name': lambda self, cr, uid, c: self.pool.get('ir.sequence').get(cr, uid, 'procurement.group') or '',
65 'move_type': lambda self, cr, uid, c: 'one'
68 class procurement_rule(osv.osv):
70 A rule describe what a procurement should do; produce, buy, move, ...
72 _name = 'procurement.rule'
73 _description = "Procurement Rule"
76 def _get_action(self, cr, uid, context=None):
80 'name': fields.char('Name', required=True,
81 help="This field will fill the packing origin and the name of its moves"),
82 'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the rule without removing it."),
83 'group_propagation_option': fields.selection([('none', 'Leave Empty'), ('propagate', 'Propagate'), ('fixed', 'Fixed')], string="Propagation of Procurement Group"),
84 'group_id': fields.many2one('procurement.group', 'Fixed Procurement Group'),
85 'action': fields.selection(selection=lambda s, cr, uid, context=None: s._get_action(cr, uid, context=context),
86 string='Action', required=True),
87 'sequence': fields.integer('Sequence'),
88 'company_id': fields.many2one('res.company', 'Company'),
92 'group_propagation_option': 'propagate',
98 class procurement_order(osv.osv):
102 _name = "procurement.order"
103 _description = "Procurement"
104 _order = 'priority desc, date_planned, id asc'
105 _inherit = ['mail.thread']
108 'name': fields.text('Description', required=True),
110 'origin': fields.char('Source Document', size=64,
111 help="Reference of the document that created this Procurement.\n"
112 "This is automatically completed by OpenERP."),
113 'company_id': fields.many2one('res.company', 'Company', required=True),
115 # These two fields are used for shceduling
116 'priority': fields.selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority', required=True, select=True, track_visibility='onchange'),
117 'date_planned': fields.datetime('Scheduled Date', required=True, select=True, track_visibility='onchange'),
119 'group_id': fields.many2one('procurement.group', 'Procurement Group'),
120 '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."),
122 'product_id': fields.many2one('product.product', 'Product', required=True, states={'confirmed': [('readonly', False)]}, readonly=True),
123 'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, states={'confirmed': [('readonly', False)]}, readonly=True),
124 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, states={'confirmed': [('readonly', False)]}, readonly=True),
126 'product_uos_qty': fields.float('UoS Quantity', states={'confirmed': [('readonly', False)]}, readonly=True),
127 'product_uos': fields.many2one('product.uom', 'Product UoS', states={'confirmed': [('readonly', False)]}, readonly=True),
129 'state': fields.selection([
130 ('cancel', 'Cancelled'),
131 ('confirmed', 'Confirmed'),
132 ('exception', 'Exception'),
133 ('running', 'Running'),
135 ], 'Status', required=True, track_visibility='onchange'),
139 'state': 'confirmed',
141 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
142 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'procurement.order', context=c)
145 def unlink(self, cr, uid, ids, context=None):
146 procurements = self.read(cr, uid, ids, ['state'], context=context)
148 for s in procurements:
149 if s['state'] == 'cancel':
150 unlink_ids.append(s['id'])
152 raise osv.except_osv(_('Invalid Action!'),
153 _('Cannot delete Procurement Order(s) which are in %s state.') % s['state'])
154 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
156 def do_view_procurements(self, cr, uid, ids, context=None):
158 This function returns an action that display existing procurement orders
159 of same procurement group of given ids.
161 mod_obj = self.pool.get('ir.model.data')
162 act_obj = self.pool.get('ir.actions.act_window')
163 result = mod_obj.get_object_reference(cr, uid, 'procurement', 'do_view_procurements')
164 id = result and result[1] or False
165 result = act_obj.read(cr, uid, [id], context=context)[0]
166 group_ids = set([proc.group_id.id for proc in self.browse(cr, uid, ids, context=context) if proc.group_id])
167 result['domain'] = "[('group_id','in',[" + ','.join(map(str, list(group_ids))) + "])]"
170 def onchange_product_id(self, cr, uid, ids, product_id, context=None):
171 """ Finds UoM and UoS of changed product.
172 @param product_id: Changed id of product.
173 @return: Dictionary of values.
176 w = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
178 'product_uom': w.uom_id.id,
179 'product_uos': w.uos_id and w.uos_id.id or w.uom_id.id
184 def get_cancel_ids(self, cr, uid, ids, context=None):
185 return [proc.id for proc in self.browse(cr, uid, ids, context=context) if proc.state != 'done']
187 def cancel(self, cr, uid, ids, context=None):
188 #cancel only the procurements that aren't done already
189 to_cancel_ids = self.get_cancel_ids(cr, uid, ids, context=context)
191 return self.write(cr, uid, to_cancel_ids, {'state': 'cancel'}, context=context)
193 def reset_to_confirmed(self, cr, uid, ids, context=None):
194 return self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
196 def run(self, cr, uid, ids, context=None):
197 for procurement_id in ids:
198 #we intentionnaly do the browse under the for loop to avoid caching all ids which would be ressource greedy
199 #and useless as we'll make a refresh later that will invalidate all the cache (and thus the next iteration
200 #will fetch all the ids again)
201 procurement = self.browse(cr, uid, procurement_id, context=context)
202 if procurement.state not in ("running", "done"):
203 if self._assign(cr, uid, procurement, context=context):
204 procurement.refresh()
205 res = self._run(cr, uid, procurement, context=context or {})
207 self.write(cr, uid, [procurement.id], {'state': 'running'}, context=context)
209 self.write(cr, uid, [procurement.id], {'state': 'exception'}, context=context)
211 self.message_post(cr, uid, [procurement.id], body=_('No rule matching this procurement'), context=context)
212 self.write(cr, uid, [procurement.id], {'state': 'exception'}, context=context)
215 def check(self, cr, uid, ids, context=None):
217 for procurement in self.browse(cr, uid, ids, context=context):
218 result = self._check(cr, uid, procurement, context=context)
220 done_ids.append(procurement.id)
222 self.write(cr, uid, done_ids, {'state': 'done'}, context=context)
226 # Method to overwrite in different procurement modules
228 def _find_suitable_rule(self, cr, uid, procurement, context=None):
229 '''This method returns a procurement.rule that depicts what to do with the given procurement
230 in order to complete its needs. It returns False if no suiting rule is found.
231 :param procurement: browse record
236 def _assign(self, cr, uid, procurement, context=None):
237 '''This method check what to do with the given procurement in order to complete its needs.
238 It returns False if no solution is found, otherwise it stores the matching rule (if any) and
240 :param procurement: browse record
243 #if the procurement already has a rule assigned, we keep it (it has a higher priority as it may have been chosen manually)
244 if procurement.rule_id:
246 elif procurement.product_id.type != 'service':
247 rule_id = self._find_suitable_rule(cr, uid, procurement, context=context)
249 self.write(cr, uid, [procurement.id], {'rule_id': rule_id}, context=context)
253 def _run(self, cr, uid, procurement, context=None):
254 '''This method implements the resolution of the given procurement
255 :param procurement: browse record
256 :returns: True if the resolution of the procurement was a success, False otherwise to set it in exception
260 def _check(self, cr, uid, procurement, context=None):
261 '''Returns True if the given procurement is fulfilled, False otherwise
262 :param procurement: browse record
270 def run_scheduler(self, cr, uid, use_new_cursor=False, context=None):
272 Call the scheduler to check the procurement order. This is intented to be done for all existing companies at
273 the same time, so we're running all the methods as SUPERUSER to avoid intercompany and access rights issues.
275 @param self: The object pointer
276 @param cr: The current row, from the database cursor,
277 @param uid: The current user ID for security checks
278 @param ids: List of selected IDs
279 @param use_new_cursor: False or the dbname
280 @param context: A standard dictionary for contextual values
281 @return: Dictionary of values
287 cr = openerp.registry(use_new_cursor).cursor()
289 # Run confirmed procurements
291 ids = self.search(cr, SUPERUSER_ID, [('state', '=', 'confirmed')], context=context)
294 self.run(cr, SUPERUSER_ID, ids, context=context)
298 # Check if running procurements are done
301 ids = self.search(cr, SUPERUSER_ID, [('state', '=', 'running')], offset=offset, context=context)
304 done = self.check(cr, SUPERUSER_ID, ids, context=context)
305 offset += len(ids) - len(done)
316 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: