[IMP] pocurement, stock, purchase, sale_stock, stock_account: misc changes in usabilt...
[odoo/odoo.git] / addons / procurement / procurement.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 import time
23
24 from datetime import datetime
25 from dateutil.relativedelta import relativedelta
26
27 from openerp.osv import fields, osv
28 import openerp.addons.decimal_precision as dp
29 from openerp.tools.translate import _
30 import openerp
31
32 class procurement_group(osv.osv):
33     '''
34     The procurement requirement class is used to group products together
35     when computing procurements. (tasks, physical products, ...)
36
37     The goal is that when you have one sale order of several products
38     and the products are pulled from the same or several location(s), to keep
39     having the moves grouped into pickings that represent the sale order.
40
41     Used in: sales order (to group delivery order lines like the so), pull/push
42     rules (to pack like the delivery order), on orderpoints (e.g. for wave picking
43     all the similar products together).
44
45     Grouping is made only if the source and the destination is the same.
46     Suppose you have 4 lines on a picking from Output where 2 lines will need
47     to come from Input (crossdock) and 2 lines coming from Stock -> Output As
48     the four procurement orders will have the same group ids from the SO, the
49     move from input will have a stock.picking with 2 grouped lines and the move
50     from stock will have 2 grouped lines also.
51
52     The name is usually the name of the original document (sale order) or a
53     sequence computed if created manually.
54     '''
55     _name = 'procurement.group'
56     _description = 'Procurement Requisition'
57     _order = "id desc"
58     _columns = {
59         'name': fields.char('Reference', required=True), 
60         'move_type': fields.selection([
61             ('direct', 'Partial'), ('one', 'All at once')],
62             'Delivery Method', required=True),
63         'partner_id': fields.many2one('res.partner', string = 'Partner'), #Sale should pass it here 
64         'procurement_ids': fields.one2many('procurement.order', 'group_id', 'Procurements'), 
65     }
66     _defaults = {
67         'name': lambda self, cr, uid, c: self.pool.get('ir.sequence').get(cr, uid, 'procurement.group') or '',
68         'move_type': lambda self, cr, uid, c: 'one'
69     }
70
71 class procurement_rule(osv.osv):
72     '''
73     A rule describe what a procurement should do; produce, buy, move, ...
74     '''
75     _name = 'procurement.rule'
76     _description = "Procurement Rule"
77     _order = "name"
78
79     def _get_action(self, cr, uid, context=None):
80         return []
81
82     _columns = {
83         'name': fields.char('Name', required=True,
84             help="This field will fill the packing origin and the name of its moves"),
85         'group_propagation_option': fields.selection([('none', 'Leave Empty'), ('propagate', 'Propagate'), ('fixed', 'Fixed')], string="Propagation of Procurement Group"),
86         'group_id': fields.many2one('procurement.group', 'Fixed Procurement Group'),
87         'action': fields.selection(selection=lambda s, cr, uid, context=None: s._get_action(cr, uid, context=context),
88             string='Action', required=True),
89         'sequence': fields.integer('Sequence'),
90         'company_id': fields.many2one('res.company', 'Company'),
91     }
92
93     _defaults = {
94         'group_propagation_option': 'propagate',
95         'sequence': 20,
96     }
97
98
99 class procurement_order(osv.osv):
100     """
101     Procurement Orders
102     """
103     _name = "procurement.order"
104     _description = "Procurement"
105     _order = 'priority desc, date_planned, id asc'
106     _inherit = ['mail.thread']
107     _log_create = False
108     _columns = {
109         'name': fields.text('Description', required=True),
110
111         'origin': fields.char('Source Document', size=64,
112             help="Reference of the document that created this Procurement.\n"
113             "This is automatically completed by OpenERP."),
114         'company_id': fields.many2one('res.company', 'Company', required=True),
115
116         # These two fields are used for shceduling
117         'priority': fields.selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority', required=True, select=True, track_visibility='onchange'),
118         'date_planned': fields.datetime('Scheduled Date', required=True, select=True, track_visibility='onchange'),
119
120         'group_id': fields.many2one('procurement.group', 'Procurement Group'),
121         'rule_id': fields.many2one('procurement.rule', 'Rule', track_visibility='onchange'),
122
123         'product_id': fields.many2one('product.product', 'Product', required=True, states={'confirmed': [('readonly', False)]}, readonly=True),
124         'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, states={'confirmed': [('readonly', False)]}, readonly=True),
125         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, states={'confirmed': [('readonly', False)]}, readonly=True),
126
127         'product_uos_qty': fields.float('UoS Quantity', states={'confirmed': [('readonly', False)]}, readonly=True),
128         'product_uos': fields.many2one('product.uom', 'Product UoS', states={'confirmed': [('readonly', False)]}, readonly=True),
129
130         'state': fields.selection([
131             ('cancel', 'Cancelled'),
132             ('confirmed', 'Confirmed'),
133             ('exception', 'Exception'),
134             ('running', 'Running'),
135             ('done', 'Done')
136         ], 'Status', required=True, track_visibility='onchange'),
137         'message': fields.text('Latest error', help="Exception occurred while computing procurement orders.", track_visibility='onchange'),
138     }
139
140     _defaults = {
141         'state': 'confirmed',
142         'priority': '1',
143         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
144         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'procurement.order', context=c)
145     }
146
147     def unlink(self, cr, uid, ids, context=None):
148         procurements = self.read(cr, uid, ids, ['state'], context=context)
149         unlink_ids = []
150         for s in procurements:
151             if s['state'] in ['draft', 'cancel']:
152                 unlink_ids.append(s['id'])
153             else:
154                 raise osv.except_osv(_('Invalid Action!'),
155                         _('Cannot delete Procurement Order(s) which are in %s state.') % s['state'])
156         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
157
158     def do_view_procurements(self, cr, uid, ids, context=None):
159         '''
160         This function returns an action that display existing procurement orders
161         of same procurement group of given ids.
162         '''
163         mod_obj = self.pool.get('ir.model.data')
164         act_obj = self.pool.get('ir.actions.act_window')
165         result = mod_obj.get_object_reference(cr, uid, 'procurement', 'do_view_procurements')
166         id = result and result[1] or False
167         result = act_obj.read(cr, uid, [id], context=context)[0]
168         group_ids = set([proc.group_id.id for proc in self.browse(cr, uid, ids, context=context) if proc.group_id])
169         result['domain'] = "[('group_id','in',[" + ','.join(map(str, list(group_ids))) + "])]"
170         return result
171
172     def onchange_product_id(self, cr, uid, ids, product_id, context=None):
173         """ Finds UoM and UoS of changed product.
174         @param product_id: Changed id of product.
175         @return: Dictionary of values.
176         """
177         if product_id:
178             w = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
179             v = {
180                 'product_uom': w.uom_id.id,
181                 'product_uos': w.uos_id and w.uos_id.id or w.uom_id.id
182             }
183             return {'value': v}
184         return {}
185
186     def cancel(self, cr, uid, ids, context=None):
187         return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
188
189     def reset_to_confirmed(self, cr, uid, ids, context=None):
190         return self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
191
192     def run(self, cr, uid, ids, context=None):
193         for procurement in self.browse(cr, uid, ids, context=context):
194             if procurement.state not in ("running", "done"):
195                 if self._assign(cr, uid, procurement, context=context):
196                     procurement.refresh()
197                     res = self._run(cr, uid, procurement, context=context or {})
198                     if res:
199                         self.write(cr, uid, [procurement.id], {'state': 'running'}, context=context)
200                     else:
201                         self.write(cr, uid, [procurement.id], {'state': 'exception'}, context=context)
202                 else:
203                     self.message_post(cr, uid, [procurement.id], body=_('No rule matching this procurement'), context=context)
204                     self.write(cr, uid, [procurement.id], {'state': 'exception'}, context=context)
205         return True
206
207     def check(self, cr, uid, ids, context=None):
208         done_ids = []
209         for procurement in self.browse(cr, uid, ids, context=context):
210             result = self._check(cr, uid, procurement, context=context)
211             if result:
212                 done_ids.append(procurement.id)
213         if done_ids:
214             self.write(cr, uid, done_ids, {'state': 'done'}, context=context)
215         return done_ids
216
217     #
218     # Method to overwrite in different procurement modules
219     #
220     def _find_suitable_rule(self, cr, uid, procurement, context=None):
221         '''This method returns a procurement.rule that depicts what to do with the given procurement
222         in order to complete its needs. It returns False if no suiting rule is found.
223             :param procurement: browse record
224             :rtype: int or False
225         '''
226         return False
227
228     def _assign(self, cr, uid, procurement, context=None):
229         '''This method check what to do with the given procurement in order to complete its needs.
230         It returns False if no solution is found, otherwise it stores the matching rule (if any) and
231         returns True.
232             :param procurement: browse record
233             :rtype: boolean
234         '''
235         if procurement.product_id.type != 'service':
236             rule_id = self._find_suitable_rule(cr, uid, procurement, context=context)
237             if rule_id:
238                 self.write(cr, uid, [procurement.id], {'rule_id': rule_id}, context=context)
239                 return True
240         return False
241
242     def _run(self, cr, uid, procurement, context=None):
243         '''This method implements the resolution of the given procurement
244             :param procurement: browse record
245         '''
246         return True
247
248     def _check(self, cr, uid, procurement, context=None):
249         '''Returns True if the given procurement is fulfilled, False otherwise
250             :param procurement: browse record
251             :rtype: boolean
252         '''
253         return False
254
255     #
256     # Scheduler
257     #
258     def run_scheduler(self, cr, uid, use_new_cursor=False, context=None):
259         '''
260         Call the scheduler to check the procurement order
261
262         @param self: The object pointer
263         @param cr: The current row, from the database cursor,
264         @param uid: The current user ID for security checks
265         @param ids: List of selected IDs
266         @param use_new_cursor: False or the dbname
267         @param context: A standard dictionary for contextual values
268         @return:  Dictionary of values
269         '''
270         if context is None:
271             context = {}
272         try:
273             if use_new_cursor:
274                 cr = openerp.registry(use_new_cursor).db.cursor()
275
276             company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
277             maxdate = (datetime.today() + relativedelta(days=company.schedule_range)).strftime('%Y-%m-%d %H:%M:%S')
278
279             # Run confirmed procurements
280             while True:
281                 ids = self.search(cr, uid, [('state', '=', 'confirmed'), ('date_planned', '<=', maxdate)], context=context)
282                 if not ids:
283                     break
284                 self.run(cr, uid, ids, context=context)
285                 if use_new_cursor:
286                     cr.commit()
287
288             # Check if running procurements are done
289             offset = 0
290             while True:
291                 ids = self.search(cr, uid, [('state', '=', 'running'), ('date_planned', '<=', maxdate)], offset=offset, context=context)
292                 if not ids:
293                     break
294                 done = self.check(cr, uid, ids, context=context)
295                 offset += len(ids) - len(done)
296                 if use_new_cursor and len(done):
297                     cr.commit()
298
299         finally:
300             if use_new_cursor:
301                 try:
302                     cr.close()
303                 except Exception:
304                     pass
305         return {}
306 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: