[FIX] stock_picking_wave: fixed picking wave report
[odoo/odoo.git] / addons / purchase_requisition / purchase_requisition.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>). All Rights Reserved
6 #    $Id$
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU Affero General Public License as published by
10 #    the Free Software Foundation, either version 3 of the License, or
11 #    (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU Affero General Public License for more details.
17 #
18 #    You should have received a copy of the GNU Affero General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22 from datetime import datetime
23 from dateutil.relativedelta import relativedelta
24 import time
25
26 from openerp.osv import fields, osv
27 from openerp.tools.translate import _
28 import openerp.addons.decimal_precision as dp
29
30 class purchase_requisition(osv.osv):
31     _name = "purchase.requisition"
32     _description = "Purchase Requisition"
33     _inherit = ['mail.thread', 'ir.needaction_mixin']
34
35     def _get_po_line(self, cr, uid, ids, field_names, arg=None, context=None):
36         result = {}.fromkeys(ids, [])
37         for element in self.browse(cr, uid, ids, context=context):
38             for po in element.purchase_ids:
39                 result[element.id] += [po_line.id for po_line in po.order_line]
40         return result
41
42     _columns = {
43         'name': fields.char('Call for Bids Reference', size=32, required=True),
44         'origin': fields.char('Source Document', size=32),
45         'ordering_date': fields.date('Scheduled Ordering Date'),
46         'date_end': fields.datetime('Bid Submission Deadline'),
47         'schedule_date': fields.date('Scheduled Date', select=True, help="The expected and scheduled date where all the products are received"),
48         'user_id': fields.many2one('res.users', 'Responsible'),
49         'exclusive': fields.selection([('exclusive', 'Select only one RFQ (exclusive)'), ('multiple', 'Select multiple RFQ')], 'Bid Selection Type', required=True, help="Select only one RFQ (exclusive):  On the confirmation of a purchase order, it cancels the remaining purchase order.\nSelect multiple RFQ:  It allows to have multiple purchase orders.On confirmation of a purchase order it does not cancel the remaining orders"""),
50         'description': fields.text('Description'),
51         'company_id': fields.many2one('res.company', 'Company', required=True),
52         'purchase_ids': fields.one2many('purchase.order', 'requisition_id', 'Purchase Orders', states={'done': [('readonly', True)]}),
53         'po_line_ids': fields.function(_get_po_line, method=True, type='one2many', relation='purchase.order.line', string='Products by supplier'),
54         'line_ids': fields.one2many('purchase.requisition.line', 'requisition_id', 'Products to Purchase', states={'done': [('readonly', True)]}),
55         'procurement_id': fields.many2one('procurement.order', 'Procurement', ondelete='set null'),
56         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
57         'state': fields.selection([('draft', 'Draft'), ('in_progress', 'Confirmed'), ('open', 'Bid Selection'), ('done', 'PO Created'), ('cancel', 'Cancelled')],
58             'Status', track_visibility='onchange', required=True),
59         'multiple_rfq_per_supplier': fields.boolean('Multiple RFQ per supplier'),
60         'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic Account'),
61         'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type', required=True),
62     }
63
64     def _get_picking_in(self, cr, uid, context=None):
65         obj_data = self.pool.get('ir.model.data')
66         return obj_data.get_object_reference(cr, uid, 'stock','picking_type_in')[1]
67
68     _defaults = {
69         'state': 'draft',
70         'exclusive': 'multiple',
71         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.requisition', context=c),
72         'user_id': lambda self, cr, uid, c: self.pool.get('res.users').browse(cr, uid, uid, c).id,
73         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'purchase.order.requisition'),
74         'picking_type_id': _get_picking_in,
75     }
76
77     def copy(self, cr, uid, id, default=None, context=None):
78         default = default or {}
79         default.update({
80             'state': 'draft',
81             'purchase_ids': [],
82             'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order.requisition'),
83         })
84         return super(purchase_requisition, self).copy(cr, uid, id, default, context)
85
86     def tender_cancel(self, cr, uid, ids, context=None):
87         purchase_order_obj = self.pool.get('purchase.order')
88         #try to set all associated quotations to cancel state
89         purchase_ids = []
90         for tender in self.browse(cr, uid, ids, context=context):
91             for purchase_order in tender.purchase_ids:
92                 purchase_order_obj.action_cancel(cr, uid, [purchase_order.id], context=context)
93                 purchase_order_obj.message_post(cr, uid, [purchase_order.id], body=_('Cancelled by the tender associated to this quotation.'), context=context)
94         return self.write(cr, uid, ids, {'state': 'cancel'})
95
96     def tender_in_progress(self, cr, uid, ids, context=None):
97         return self.write(cr, uid, ids, {'state': 'in_progress'}, context=context)
98
99     def tender_open(self, cr, uid, ids, context=None):
100         return self.write(cr, uid, ids, {'state': 'open'}, context=context)
101
102     def tender_reset(self, cr, uid, ids, context=None):
103         self.write(cr, uid, ids, {'state': 'draft'})
104         for p_id in ids:
105             # Deleting the existing instance of workflow for PO
106             self.delete_workflow(cr, uid, [p_id])
107             self.create_workflow(cr, uid, [p_id])
108         return True
109
110     def tender_done(self, cr, uid, ids, context=None):
111         return self.write(cr, uid, ids, {'state': 'done'}, context=context)
112
113     def open_product_line(self, cr, uid, ids, context=None):
114         """ This opens product line view to view all lines from the different quotations, groupby default by product and partner to show comparaison
115             between supplier price
116             @return: the product line tree view
117         """
118         if context is None:
119             context = {}
120         res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'purchase_requisition', 'purchase_line_tree', context=context)
121         res['context'] = context
122         po_lines = self.browse(cr, uid, ids, context=context)[0].po_line_ids
123         res['context'] = {
124             'search_default_groupby_product': True,
125             'search_default_hide_cancelled': True,
126         }
127         res['domain'] = [('id', 'in', [line.id for line in po_lines])]
128         return res
129
130     def open_rfq(self, cr, uid, ids, context=None):
131         """ This opens rfq view to view all quotations associated to the call for bids
132             @return: the RFQ tree view
133         """
134         if context is None:
135             context = {}
136         res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'purchase', 'purchase_rfq', context=context)
137         res['context'] = context
138         po_ids = [po.id for po in self.browse(cr, uid, ids, context=context)[0].purchase_ids]
139         res['domain'] = [('id', 'in', po_ids)]
140         return res
141
142     def _prepare_purchase_order(self, cr, uid, requisition, supplier, context=None):
143         supplier_pricelist = supplier.property_product_pricelist_purchase and supplier.property_product_pricelist_purchase.id or False
144         picking_type_in = self.pool.get("purchase.order")._get_picking_in(cr, uid, context=context)
145         return {
146             'origin': requisition.name,
147             'date_order': requisition.date_end or fields.date.context_today(self, cr, uid, context=context),
148             'partner_id': supplier.id,
149             'pricelist_id': supplier_pricelist,
150             'location_id': requisition.picking_type_id.default_location_dest_id.id,
151             'company_id': requisition.company_id.id,
152             'fiscal_position': supplier.property_account_position and supplier.property_account_position.id or False,
153             'requisition_id': requisition.id,
154             'notes': requisition.description,
155             'picking_type_id': picking_type_in,
156         }
157
158     def _prepare_purchase_order_line(self, cr, uid, requisition, requisition_line, purchase_id, supplier, context=None):
159         po_line_obj = self.pool.get('purchase.order.line')
160         product_uom = self.pool.get('product.uom')
161         product = requisition_line.product_id
162         default_uom_po_id = product.uom_po_id.id
163         date_order = requisition.ordering_date or fields.date.context_today(self, cr, uid, context=context)
164         qty = product_uom._compute_qty(cr, uid, requisition_line.product_uom_id.id, requisition_line.product_qty, default_uom_po_id)
165         supplier_pricelist = supplier.property_product_pricelist_purchase and supplier.property_product_pricelist_purchase.id or False
166         vals = po_line_obj.onchange_product_id(cr, uid, [], supplier_pricelist, product.id, qty, default_uom_po_id,
167             supplier.id, date_order=date_order, fiscal_position_id=supplier.property_account_position, date_planned=requisition_line.schedule_date,
168             name=False, price_unit=False, state='draft', context=context)['value']
169         vals.update({
170             'order_id': purchase_id,
171             'product_id': product.id,
172             'account_analytic_id': requisition_line.account_analytic_id.id,
173         })
174         return vals
175
176     def make_purchase_order(self, cr, uid, ids, partner_id, context=None):
177         """
178         Create New RFQ for Supplier
179         """
180         if context is None:
181             context = {}
182         assert partner_id, 'Supplier should be specified'
183         purchase_order = self.pool.get('purchase.order')
184         purchase_order_line = self.pool.get('purchase.order.line')
185         res_partner = self.pool.get('res.partner')
186         supplier = res_partner.browse(cr, uid, partner_id, context=context)
187         res = {}
188         for requisition in self.browse(cr, uid, ids, context=context):
189             if not requisition.multiple_rfq_per_supplier and supplier.id in filter(lambda x: x, [rfq.state != 'cancel' and rfq.partner_id.id or None for rfq in requisition.purchase_ids]):
190                 raise osv.except_osv(_('Warning!'), _('You have already one %s purchase order for this partner, you must cancel this purchase order to create a new quotation.') % rfq.state)
191             context.update({'mail_create_nolog': True})
192             purchase_id = purchase_order.create(cr, uid, self._prepare_purchase_order(cr, uid, requisition, supplier, context=context), context=context)
193             purchase_order.message_post(cr, uid, [purchase_id], body=_("RFQ created"), context=context)
194             res[requisition.id] = purchase_id
195             for line in requisition.line_ids:
196                 purchase_order_line.create(cr, uid, self._prepare_purchase_order_line(cr, uid, requisition, line, purchase_id, supplier, context=context), context=context)
197         return res
198
199     def check_valid_quotation(self, cr, uid, quotation, context=None):
200         """
201         Check if a quotation has all his order lines bid in order to confirm it if its the case
202         return True if all order line have been selected during bidding process, else return False
203
204         args : 'quotation' must be a browse record
205         """
206         for line in quotation.order_line:
207             if line.state != 'confirmed' or line.product_qty != line.quantity_bid:
208                 return False
209         return True
210
211     def _prepare_po_from_tender(self, cr, uid, tender, context=None):
212         """ Prepare the values to write in the purchase order
213         created from a tender.
214
215         :param tender: the source tender from which we generate a purchase order
216         """
217         return {'order_line': [],
218                 'requisition_id': tender.id,
219                 'origin': tender.name}
220
221     def _prepare_po_line_from_tender(self, cr, uid, tender, line, purchase_id, context=None):
222         """ Prepare the values to write in the purchase order line
223         created from a line of the tender.
224
225         :param tender: the source tender from which we generate a purchase order
226         :param line: the source tender's line from which we generate a line
227         :param purchase_id: the id of the new purchase
228         """
229         return {'product_qty': line.quantity_bid,
230                 'order_id': purchase_id}
231
232     def generate_po(self, cr, uid, ids, context=None):
233         """
234         Generate all purchase order based on selected lines, should only be called on one tender at a time
235         """
236         if context is None:
237             contex = {}
238         po = self.pool.get('purchase.order')
239         poline = self.pool.get('purchase.order.line')
240         id_per_supplier = {}
241         for tender in self.browse(cr, uid, ids, context=context):
242             if tender.state == 'done':
243                 raise osv.except_osv(_('Warning!'), _('You have already generate the purchase order(s).'))
244
245             confirm = False
246             #check that we have at least confirm one line
247             for po_line in tender.po_line_ids:
248                 if po_line.state == 'confirmed':
249                     confirm = True
250                     break
251             if not confirm:
252                 raise osv.except_osv(_('Warning!'), _('You have no line selected for buying.'))
253
254             #check for complete RFQ
255             for quotation in tender.purchase_ids:
256                 if (self.check_valid_quotation(cr, uid, quotation, context=context)):
257                     #use workflow to set PO state to confirm
258                     po.signal_purchase_confirm(cr, uid, [quotation.id])
259
260             #get other confirmed lines per supplier
261             for po_line in tender.po_line_ids:
262                 #only take into account confirmed line that does not belong to already confirmed purchase order
263                 if po_line.state == 'confirmed' and po_line.order_id.state in ['draft', 'sent', 'bid']:
264                     if id_per_supplier.get(po_line.partner_id.id):
265                         id_per_supplier[po_line.partner_id.id].append(po_line)
266                     else:
267                         id_per_supplier[po_line.partner_id.id] = [po_line]
268
269             #generate po based on supplier and cancel all previous RFQ
270             ctx = context.copy()
271             ctx['force_requisition_id'] = True
272             for supplier, product_line in id_per_supplier.items():
273                 #copy a quotation for this supplier and change order_line then validate it
274                 quotation_id = po.search(cr, uid, [('requisition_id', '=', tender.id), ('partner_id', '=', supplier)], limit=1)[0]
275                 vals = self._prepare_po_from_tender(cr, uid, tender, context=context)
276                 new_po = po.copy(cr, uid, quotation_id, default=vals, context=ctx)
277                 #duplicate po_line and change product_qty if needed and associate them to newly created PO
278                 for line in product_line:
279                     vals = self._prepare_po_line_from_tender(cr, uid, tender, line, new_po, context=context)
280                     poline.copy(cr, uid, line.id, default=vals, context=context)
281                 #use workflow to set new PO state to confirm
282                 po.signal_purchase_confirm(cr, uid, [new_po])
283
284             #cancel other orders
285             self.cancel_unconfirmed_quotations(cr, uid, tender, context=context)
286
287             #set tender to state done
288             self.signal_done(cr, uid, [tender.id])
289         return True
290
291     def cancel_unconfirmed_quotations(self, cr, uid, tender, context=None):
292         #cancel other orders
293         po = self.pool.get('purchase.order')
294         for quotation in tender.purchase_ids:
295             if quotation.state in ['draft', 'sent', 'bid']:
296                 self.pool.get('purchase.order').signal_purchase_cancel(cr, uid, [quotation.id])
297                 po.message_post(cr, uid, [quotation.id], body=_('Cancelled by the call for bids associated to this request for quotation.'), context=context)
298         return True
299
300
301 class purchase_requisition_line(osv.osv):
302     _name = "purchase.requisition.line"
303     _description = "Purchase Requisition Line"
304     _rec_name = 'product_id'
305
306     _columns = {
307         'product_id': fields.many2one('product.product', 'Product'),
308         'product_uom_id': fields.many2one('product.uom', 'Product Unit of Measure'),
309         'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure')),
310         'requisition_id': fields.many2one('purchase.requisition', 'Call for Bids', ondelete='cascade'),
311         'company_id': fields.related('requisition_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
312         'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic Account',),
313         'schedule_date': fields.date('Scheduled Date'),
314     }
315
316     def onchange_product_id(self, cr, uid, ids, product_id, product_uom_id, parent_analytic_account, analytic_account, parent_date, date, context=None):
317         """ Changes UoM and name if product_id changes.
318         @param name: Name of the field
319         @param product_id: Changed product_id
320         @return:  Dictionary of changed values
321         """
322         value = {'product_uom_id': ''}
323         if product_id:
324             prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
325             value = {'product_uom_id': prod.uom_id.id, 'product_qty': 1.0}
326         if not analytic_account:
327             value.update({'account_analytic_id': parent_analytic_account})
328         if not date:
329             value.update({'schedule_date': parent_date})
330         return {'value': value}
331
332     _defaults = {
333         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.requisition.line', context=c),
334     }
335
336 class purchase_order(osv.osv):
337     _inherit = "purchase.order"
338
339     _columns = {
340         'requisition_id': fields.many2one('purchase.requisition', 'Call for Bids'),
341     }
342
343     def wkf_confirm_order(self, cr, uid, ids, context=None):
344         res = super(purchase_order, self).wkf_confirm_order(cr, uid, ids, context=context)
345         proc_obj = self.pool.get('procurement.order')
346         for po in self.browse(cr, uid, ids, context=context):
347             if po.requisition_id and (po.requisition_id.exclusive == 'exclusive'):
348                 for order in po.requisition_id.purchase_ids:
349                     if order.id != po.id:
350                         proc_ids = proc_obj.search(cr, uid, [('purchase_id', '=', order.id)])
351                         if proc_ids and po.state == 'confirmed':
352                             proc_obj.write(cr, uid, proc_ids, {'purchase_id': po.id})
353                         self.signal_purchase_cancel(cr, uid, [order.id])
354                     po.requisition_id.tender_done(context=context)
355         return res
356
357     def copy(self, cr, uid, id, default=None, context=None):
358         if context is None:
359             context = {}
360         if not context.get('force_requisition_id'):
361             default = default or {}
362             default.update({'requisition_id': False})
363         return super(purchase_order, self).copy(cr, uid, id, default=default, context=context)
364
365     def _prepare_order_line_move(self, cr, uid, order, order_line, picking_id, group_id, context=None):
366         stock_move_lines = super(purchase_order, self)._prepare_order_line_move(cr, uid, order, order_line, picking_id, group_id, context=context)
367         if order.requisition_id and order.requisition_id.procurement_id and order.requisition_id.procurement_id.move_dest_id:
368             for i in range(0, len(stock_move_lines)):
369                 stock_move_lines[i]['move_dest_id'] = order.requisition_id.procurement_id.move_dest_id.id
370         return stock_move_lines
371
372
373 class purchase_order_line(osv.osv):
374     _inherit = 'purchase.order.line'
375
376     _columns = {
377         'quantity_bid': fields.float('Quantity Bid', digits_compute=dp.get_precision('Product Unit of Measure'), help="Technical field for not loosing the initial information about the quantity proposed in the bid"),
378     }
379
380     def action_draft(self, cr, uid, ids, context=None):
381         self.write(cr, uid, ids, {'state': 'draft'}, context=context)
382
383     def action_confirm(self, cr, uid, ids, context=None):
384         super(purchase_order_line, self).action_confirm(cr, uid, ids, context=context)
385         for element in self.browse(cr, uid, ids, context=context):
386             if not element.quantity_bid:
387                 self.write(cr, uid, ids, {'quantity_bid': element.product_qty}, context=context)
388         return True
389
390     def generate_po(self, cr, uid, tender_id, context=None):
391         #call generate_po from tender with active_id. Called from js widget
392         return self.pool.get('purchase.requisition').generate_po(cr, uid, [tender_id], context=context)
393
394
395 class product_product(osv.osv):
396     _inherit = 'product.product'
397
398     _columns = {
399         'purchase_requisition': fields.boolean('Call for Bids', help="Check this box to generate Call for Bids instead of generating requests for quotation from procurement.")
400     }
401
402
403 class procurement_order(osv.osv):
404     _inherit = 'procurement.order'
405     _columns = {
406         'requisition_id': fields.many2one('purchase.requisition', 'Latest Requisition')
407     }
408
409     def _run(self, cr, uid, procurement, context=None):
410         requisition_obj = self.pool.get('purchase.requisition')
411         warehouse_obj = self.pool.get('stock.warehouse')
412         if procurement.rule_id and procurement.rule_id.action == 'buy' and procurement.product_id.purchase_requisition:
413             warehouse_id = warehouse_obj.search(cr, uid, [('company_id', '=', procurement.company_id.id)], context=context)
414             requisition_id = requisition_obj.create(cr, uid, {
415                 'origin': procurement.origin,
416                 'date_end': procurement.date_planned,
417                 'warehouse_id': warehouse_id and warehouse_id[0] or False,
418                 'company_id': procurement.company_id.id,
419                 'procurement_id': procurement.id,
420                 'line_ids': [(0, 0, {
421                     'product_id': procurement.product_id.id,
422                     'product_uom_id': procurement.product_uom.id,
423                     'product_qty': procurement.product_qty
424
425                 })],
426             })
427             self.message_post(cr, uid, [procurement.id], body=_("Purchase Requisition created"), context=context)
428             return self.write(cr, uid, [procurement.id], {'requisition_id': requisition_id}, context=context)
429         return super(procurement_order, self)._run(cr, uid, procurement, context=context)
430
431     def _check(self, cr, uid, procurement, context=None):
432         requisition_obj = self.pool.get('purchase.requisition')
433         if procurement.rule_id and procurement.rule_id.action == 'buy' and procurement.product_id.purchase_requisition:
434             if procurement.requisition_id.state == 'done':
435                 if any([purchase.shipped for purchase in procurement.requisition_id.purchase_ids]):
436                     return True
437             return False
438         return super(procurement_order, self)._check(cr, uid, procurement, context=context)
439
440 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: