[IMP] routing and procurements
[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
30 class procurement_group(osv.osv):
31     '''
32     The procurement requirement class is used to group products together
33     when computing procurements. (tasks, physical products, ...)
34
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.
38
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).
42
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.
49
50     The name is usually the name of the original document (sale order) or a
51     sequence computed if created manually.
52     '''
53     _name = 'procurement.group'
54     _description = 'Procurement Requisition'
55     _order = "id desc"
56     _columns = {
57         'name': fields.char('Reference'), 
58     }
59     _defaults = {
60         'name': lambda self,cr,uid,c: self.pool.get('ir.sequence').get(cr,uid,'procurement.group') or ''
61     }
62
63 class procurement_rule(osv.osv):
64     '''
65     A rule describe what a procurement should do; produce, buy, move, ...
66     '''
67     _name = 'procurement.rule'
68     _description = "Procurement Rule"
69     _columns = {
70         'name': fields.char('Name', required=True,
71             help="This field will fill the packing origin and the name of its moves"),
72         'group_id': fields.many2one('procurement.group', 'Procurement Group'),
73         'action': fields.selection(selection=lambda s, c, u, ctx: s._get_action(c, u, context=ctx),
74             string='Action', required=True)
75     }
76     def _get_action(self, cr, uid, context=None):
77         return []
78
79
80 class procurement_order(osv.osv):
81     """
82     Procurement Orders
83     """
84     _name = "procurement.order"
85     _description = "Procurement"
86     _order = 'priority desc,date_planned'
87     _inherit = ['mail.thread']
88     _log_create = False
89     _columns = {
90         'name': fields.text('Description', required=True),
91
92         'origin': fields.char('Source Document', size=64,
93             help="Reference of the document that created this Procurement.\n"
94             "This is automatically completed by OpenERP."),
95         'company_id': fields.many2one('res.company','Company',required=True),
96
97         # These two fields are used for shceduling
98         'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority', required=True, select=True),
99         'date_planned': fields.datetime('Scheduled date', required=True, select=True),
100
101         'group_id':fields.many2one('procurement.group', 'Procurement Requisition'), 
102         'rule_id': fields.many2one('procurement.rule', 'Rule'),
103
104         'product_id': fields.many2one('product.product', 'Product', required=True, states={'draft':[('readonly',False)]}, readonly=True),
105         'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, states={'draft':[('readonly',False)]}, readonly=True),
106         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, states={'draft':[('readonly',False)]}, readonly=True),
107
108         'product_uos_qty': fields.float('UoS Quantity', states={'draft':[('readonly',False)]}, readonly=True),
109         'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
110
111         'procure_method': fields.selection([('make_to_stock','Make to Stock'),('make_to_order','Make to Order')],
112             'Procurement Method', states={'draft':[('readonly',False)], 'confirmed':[('readonly',False)]},
113             readonly=True, required=True, help="If you encode manually a Procurement, you probably want to use" \
114             " a make to order method."),
115
116         'state': fields.selection([
117             ('cancel','Cancelled'),
118             ('confirmed','Confirmed'),
119             ('exception','Exception'),
120             ('running','Running'),
121             ('done','Done')
122         ], 'Status', required=True, track_visibility='onchange'),
123
124     }
125     _defaults = {
126         'state': 'confirmed',
127         'priority': '1',
128         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
129         'procure_method': 'make_to_order',
130         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'procurement.order', context=c)
131     }
132     def onchange_product_id(self, cr, uid, ids, product_id, context=None):
133         """ Finds UoM and UoS of changed product.
134         @param product_id: Changed id of product.
135         @return: Dictionary of values.
136         """
137         if product_id:
138             w = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
139             v = {
140                 'product_uom': w.uom_id.id,
141                 'product_uos': w.uos_id and w.uos_id.id or w.uom_id.id
142             }
143             return {'value': v}
144         return {}
145
146     def run(self, cr, uid, ids, context=None):
147         for procurement in self.browse(cr, uid, ids, context=context or {}):
148             if procurement.procure_method=='make_to_order':
149                 rule = self._assign(cr, uid, procurement, context=context)
150                 if rule:
151                     self.write(cr, uid, [procurement.id], {'rule_id', rule.id}, context=context)
152                     procurement.refresh()
153                     self._run(cr, uid, procurement, context=context or {})
154                 else:
155                     self.message_post(cr, uid, [procurement.id], body=_('No rule matching this procurement'), context=context)
156             else:
157                 result = True
158             if result:
159                 self.write(cr, uid, [procurement.id], {'state': 'running'}, context=context)
160             else:
161                 self.write(cr, uid, [procurement.id], {'state': 'exception'}, context=context)
162         return True
163
164     def check(self, cr, uid, ids, context=None):
165         done = []
166         for procurement in self.browse(cr, uid, ids, context=context or {}):
167             result = self._check(cr, uid, procurement.id, context=context or {})
168             if result:
169                 self.write(cr, uid, [procurement.id], {'state': 'done'}, context=context)
170                 done.append(procurement.id)
171         return done
172
173     #
174     # Method to overwrite in different procurement modules
175     #
176     def _assign(self, cr, uid, procurement, context=None):
177         return False
178
179     def _run(self, cr, uid, procurement, context=None):
180         return True
181
182     def _check(self, cr, uid, procurement, context=None):
183         return True
184
185     #
186     # Scheduler
187     #
188     def run_scheduler(self, cr, uid, use_new_cursor=False, context=None):
189         '''
190         Call the scheduler to check the procurement order
191
192         @param self: The object pointer
193         @param cr: The current row, from the database cursor,
194         @param uid: The current user ID for security checks
195         @param ids: List of selected IDs
196         @param use_new_cursor: False or the dbname
197         @param context: A standard dictionary for contextual values
198         @return:  Dictionary of values
199         '''
200         if context is None:
201             context = {}
202         try:
203             if use_new_cursor:
204                 cr = openerp.registry(use_new_cursor).db.cursor()
205
206             company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
207             maxdate = (datetime.today() + relativedelta(days=company.schedule_range)).strftime('%Y-%m-%d %H:%M:%S')
208
209             # Run confirmed procurements
210             while True:
211                 ids = self.search(cr, uid, [('state', '=', 'confirmed'),('date_planned','<=', maxdate)], context=context)
212                 if not ids: break
213                 self.run(cr, uid, ids, context=context)
214                 if use_new_cursor:
215                     cr.commit()
216
217             # Check if running procurements are done
218             offset = 0
219             while True:
220                 ids = self.search(cr, uid, [('state', '=', 'running'),('date_planned','<=', maxdate)], offset=offset, context=context)
221                 if not ids: break
222                 done = self.check(cr, uid, ids, context=context)
223                 offset += len(ids) - len(done)
224                 if use_new_cursor and len(done):
225                     cr.commit()
226
227         finally:
228             if use_new_cursor:
229                 try:
230                     cr.close()
231                 except Exception:
232                     pass
233         return {}
234