[MERGE] forward port of branch saas-4 up to f68c835
[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         procurement_ids = self.pool['procurement.order'].search(cr, uid, [('requisition_id', 'in', ids)], context=context)
95         self.pool['procurement.order'].action_done(cr, uid, procurement_ids)
96         return self.write(cr, uid, ids, {'state': 'cancel'})
97
98     def tender_in_progress(self, cr, uid, ids, context=None):
99         return self.write(cr, uid, ids, {'state': 'in_progress'}, context=context)
100
101     def tender_open(self, cr, uid, ids, context=None):
102         return self.write(cr, uid, ids, {'state': 'open'}, context=context)
103
104     def tender_reset(self, cr, uid, ids, context=None):
105         self.write(cr, uid, ids, {'state': 'draft'})
106         for p_id in ids:
107             # Deleting the existing instance of workflow for PO
108             self.delete_workflow(cr, uid, [p_id])
109             self.create_workflow(cr, uid, [p_id])
110         return True
111
112     def tender_done(self, cr, uid, ids, context=None):
113         procurement_ids = self.pool['procurement.order'].search(cr, uid, [('requisition_id', 'in', ids)], context=context)
114         self.pool['procurement.order'].action_done(cr, uid, procurement_ids)
115         return self.write(cr, uid, ids, {'state': 'done'}, context=context)
116
117     def open_product_line(self, cr, uid, ids, context=None):
118         """ This opens product line view to view all lines from the different quotations, groupby default by product and partner to show comparaison
119             between supplier price
120             @return: the product line tree view
121         """
122         if context is None:
123             context = {}
124         res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'purchase_requisition', 'purchase_line_tree', context=context)
125         res['context'] = context
126         po_lines = self.browse(cr, uid, ids, context=context)[0].po_line_ids
127         res['context'] = {
128             'search_default_groupby_product': True,
129             'search_default_hide_cancelled': True,
130         }
131         res['domain'] = [('id', 'in', [line.id for line in po_lines])]
132         return res
133
134     def open_rfq(self, cr, uid, ids, context=None):
135         """ This opens rfq view to view all quotations associated to the call for bids
136             @return: the RFQ tree view
137         """
138         if context is None:
139             context = {}
140         res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'purchase', 'purchase_rfq', context=context)
141         res['context'] = context
142         po_ids = [po.id for po in self.browse(cr, uid, ids, context=context)[0].purchase_ids]
143         res['domain'] = [('id', 'in', po_ids)]
144         return res
145
146     def _prepare_purchase_order(self, cr, uid, requisition, supplier, context=None):
147         supplier_pricelist = supplier.property_product_pricelist_purchase and supplier.property_product_pricelist_purchase.id or False
148         picking_type_in = self.pool.get("purchase.order")._get_picking_in(cr, uid, context=context)
149         return {
150             'origin': requisition.name,
151             'date_order': requisition.date_end or fields.date.context_today(self, cr, uid, context=context),
152             'partner_id': supplier.id,
153             'pricelist_id': supplier_pricelist,
154             'location_id': requisition.picking_type_id.default_location_dest_id.id,
155             'company_id': requisition.company_id.id,
156             'fiscal_position': supplier.property_account_position and supplier.property_account_position.id or False,
157             'requisition_id': requisition.id,
158             'notes': requisition.description,
159             'picking_type_id': picking_type_in,
160         }
161
162     def _prepare_purchase_order_line(self, cr, uid, requisition, requisition_line, purchase_id, supplier, context=None):
163         po_line_obj = self.pool.get('purchase.order.line')
164         product_uom = self.pool.get('product.uom')
165         product = requisition_line.product_id
166         default_uom_po_id = product.uom_po_id.id
167         date_order = requisition.ordering_date or fields.date.context_today(self, cr, uid, context=context)
168         qty = product_uom._compute_qty(cr, uid, requisition_line.product_uom_id.id, requisition_line.product_qty, default_uom_po_id)
169         supplier_pricelist = supplier.property_product_pricelist_purchase and supplier.property_product_pricelist_purchase.id or False
170         vals = po_line_obj.onchange_product_id(cr, uid, [], supplier_pricelist, product.id, qty, default_uom_po_id,
171             supplier.id, date_order=date_order, fiscal_position_id=supplier.property_account_position, date_planned=requisition_line.schedule_date,
172             name=False, price_unit=False, state='draft', context=context)['value']
173         vals.update({
174             'order_id': purchase_id,
175             'product_id': product.id,
176             'account_analytic_id': requisition_line.account_analytic_id.id,
177         })
178         return vals
179
180     def make_purchase_order(self, cr, uid, ids, partner_id, context=None):
181         """
182         Create New RFQ for Supplier
183         """
184         if context is None:
185             context = {}
186         assert partner_id, 'Supplier should be specified'
187         purchase_order = self.pool.get('purchase.order')
188         purchase_order_line = self.pool.get('purchase.order.line')
189         res_partner = self.pool.get('res.partner')
190         supplier = res_partner.browse(cr, uid, partner_id, context=context)
191         res = {}
192         for requisition in self.browse(cr, uid, ids, context=context):
193             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]):
194                 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)
195             context.update({'mail_create_nolog': True})
196             purchase_id = purchase_order.create(cr, uid, self._prepare_purchase_order(cr, uid, requisition, supplier, context=context), context=context)
197             purchase_order.message_post(cr, uid, [purchase_id], body=_("RFQ created"), context=context)
198             res[requisition.id] = purchase_id
199             for line in requisition.line_ids:
200                 purchase_order_line.create(cr, uid, self._prepare_purchase_order_line(cr, uid, requisition, line, purchase_id, supplier, context=context), context=context)
201         return res
202
203     def check_valid_quotation(self, cr, uid, quotation, context=None):
204         """
205         Check if a quotation has all his order lines bid in order to confirm it if its the case
206         return True if all order line have been selected during bidding process, else return False
207
208         args : 'quotation' must be a browse record
209         """
210         for line in quotation.order_line:
211             if line.state != 'confirmed' or line.product_qty != line.quantity_bid:
212                 return False
213         return True
214
215     def _prepare_po_from_tender(self, cr, uid, tender, context=None):
216         """ Prepare the values to write in the purchase order
217         created from a tender.
218
219         :param tender: the source tender from which we generate a purchase order
220         """
221         return {'order_line': [],
222                 'requisition_id': tender.id,
223                 'origin': tender.name}
224
225     def _prepare_po_line_from_tender(self, cr, uid, tender, line, purchase_id, context=None):
226         """ Prepare the values to write in the purchase order line
227         created from a line of the tender.
228
229         :param tender: the source tender from which we generate a purchase order
230         :param line: the source tender's line from which we generate a line
231         :param purchase_id: the id of the new purchase
232         """
233         return {'product_qty': line.quantity_bid,
234                 'order_id': purchase_id}
235
236     def generate_po(self, cr, uid, ids, context=None):
237         """
238         Generate all purchase order based on selected lines, should only be called on one tender at a time
239         """
240         if context is None:
241             contex = {}
242         po = self.pool.get('purchase.order')
243         poline = self.pool.get('purchase.order.line')
244         id_per_supplier = {}
245         for tender in self.browse(cr, uid, ids, context=context):
246             if tender.state == 'done':
247                 raise osv.except_osv(_('Warning!'), _('You have already generate the purchase order(s).'))
248
249             confirm = False
250             #check that we have at least confirm one line
251             for po_line in tender.po_line_ids:
252                 if po_line.state == 'confirmed':
253                     confirm = True
254                     break
255             if not confirm:
256                 raise osv.except_osv(_('Warning!'), _('You have no line selected for buying.'))
257
258             #check for complete RFQ
259             for quotation in tender.purchase_ids:
260                 if (self.check_valid_quotation(cr, uid, quotation, context=context)):
261                     #use workflow to set PO state to confirm
262                     po.signal_purchase_confirm(cr, uid, [quotation.id])
263
264             #get other confirmed lines per supplier
265             for po_line in tender.po_line_ids:
266                 #only take into account confirmed line that does not belong to already confirmed purchase order
267                 if po_line.state == 'confirmed' and po_line.order_id.state in ['draft', 'sent', 'bid']:
268                     if id_per_supplier.get(po_line.partner_id.id):
269                         id_per_supplier[po_line.partner_id.id].append(po_line)
270                     else:
271                         id_per_supplier[po_line.partner_id.id] = [po_line]
272
273             #generate po based on supplier and cancel all previous RFQ
274             ctx = context.copy()
275             ctx['force_requisition_id'] = True
276             for supplier, product_line in id_per_supplier.items():
277                 #copy a quotation for this supplier and change order_line then validate it
278                 quotation_id = po.search(cr, uid, [('requisition_id', '=', tender.id), ('partner_id', '=', supplier)], limit=1)[0]
279                 vals = self._prepare_po_from_tender(cr, uid, tender, context=context)
280                 new_po = po.copy(cr, uid, quotation_id, default=vals, context=ctx)
281                 #duplicate po_line and change product_qty if needed and associate them to newly created PO
282                 for line in product_line:
283                     vals = self._prepare_po_line_from_tender(cr, uid, tender, line, new_po, context=context)
284                     poline.copy(cr, uid, line.id, default=vals, context=context)
285                 #use workflow to set new PO state to confirm
286                 po.signal_purchase_confirm(cr, uid, [new_po])
287
288             #cancel other orders
289             self.cancel_unconfirmed_quotations(cr, uid, tender, context=context)
290
291             #set tender to state done
292             self.signal_done(cr, uid, [tender.id])
293         return True
294
295     def cancel_unconfirmed_quotations(self, cr, uid, tender, context=None):
296         #cancel other orders
297         po = self.pool.get('purchase.order')
298         for quotation in tender.purchase_ids:
299             if quotation.state in ['draft', 'sent', 'bid']:
300                 self.pool.get('purchase.order').signal_purchase_cancel(cr, uid, [quotation.id])
301                 po.message_post(cr, uid, [quotation.id], body=_('Cancelled by the call for bids associated to this request for quotation.'), context=context)
302         return True
303
304
305 class purchase_requisition_line(osv.osv):
306     _name = "purchase.requisition.line"
307     _description = "Purchase Requisition Line"
308     _rec_name = 'product_id'
309
310     _columns = {
311         'product_id': fields.many2one('product.product', 'Product'),
312         'product_uom_id': fields.many2one('product.uom', 'Product Unit of Measure'),
313         'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure')),
314         'requisition_id': fields.many2one('purchase.requisition', 'Call for Bids', ondelete='cascade'),
315         'company_id': fields.related('requisition_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
316         'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic Account',),
317         'schedule_date': fields.date('Scheduled Date'),
318     }
319
320     def onchange_product_id(self, cr, uid, ids, product_id, product_uom_id, parent_analytic_account, analytic_account, parent_date, date, context=None):
321         """ Changes UoM and name if product_id changes.
322         @param name: Name of the field
323         @param product_id: Changed product_id
324         @return:  Dictionary of changed values
325         """
326         value = {'product_uom_id': ''}
327         if product_id:
328             prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
329             value = {'product_uom_id': prod.uom_id.id, 'product_qty': 1.0}
330         if not analytic_account:
331             value.update({'account_analytic_id': parent_analytic_account})
332         if not date:
333             value.update({'schedule_date': parent_date})
334         return {'value': value}
335
336     _defaults = {
337         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.requisition.line', context=c),
338     }
339
340 class purchase_order(osv.osv):
341     _inherit = "purchase.order"
342
343     _columns = {
344         'requisition_id': fields.many2one('purchase.requisition', 'Call for Bids'),
345     }
346
347     def wkf_confirm_order(self, cr, uid, ids, context=None):
348         res = super(purchase_order, self).wkf_confirm_order(cr, uid, ids, context=context)
349         proc_obj = self.pool.get('procurement.order')
350         for po in self.browse(cr, uid, ids, context=context):
351             if po.requisition_id and (po.requisition_id.exclusive == 'exclusive'):
352                 for order in po.requisition_id.purchase_ids:
353                     if order.id != po.id:
354                         proc_ids = proc_obj.search(cr, uid, [('purchase_id', '=', order.id)])
355                         if proc_ids and po.state == 'confirmed':
356                             proc_obj.write(cr, uid, proc_ids, {'purchase_id': po.id})
357                         self.signal_purchase_cancel(cr, uid, [order.id])
358                     po.requisition_id.tender_done(context=context)
359             if po.requisition_id and all(purchase_id.state in ['draft', 'cancel'] for purchase_id in po.requisition_id.purchase_ids if purchase_id.id != po.id):
360                 procurement_ids = self.pool['procurement.order'].search(cr, uid, [('requisition_id', '=', po.requisition_id.id)], context=context)
361                 for procurement in proc_obj.browse(cr, uid, procurement_ids, context=context):
362                     procurement.move_id.write({'location_id': procurement.move_id.location_dest_id.id})
363         return res
364
365     def copy(self, cr, uid, id, default=None, context=None):
366         if context is None:
367             context = {}
368         if not context.get('force_requisition_id'):
369             default = default or {}
370             default.update({'requisition_id': False})
371         return super(purchase_order, self).copy(cr, uid, id, default=default, context=context)
372
373     def _prepare_order_line_move(self, cr, uid, order, order_line, picking_id, group_id, context=None):
374         stock_move_lines = super(purchase_order, self)._prepare_order_line_move(cr, uid, order, order_line, picking_id, group_id, context=context)
375         if order.requisition_id and order.requisition_id.procurement_id and order.requisition_id.procurement_id.move_dest_id:
376             for i in range(0, len(stock_move_lines)):
377                 stock_move_lines[i]['move_dest_id'] = order.requisition_id.procurement_id.move_dest_id.id
378         return stock_move_lines
379
380
381 class purchase_order_line(osv.osv):
382     _inherit = 'purchase.order.line'
383
384     _columns = {
385         '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"),
386     }
387
388     def action_draft(self, cr, uid, ids, context=None):
389         self.write(cr, uid, ids, {'state': 'draft'}, context=context)
390
391     def action_confirm(self, cr, uid, ids, context=None):
392         super(purchase_order_line, self).action_confirm(cr, uid, ids, context=context)
393         for element in self.browse(cr, uid, ids, context=context):
394             if not element.quantity_bid:
395                 self.write(cr, uid, ids, {'quantity_bid': element.product_qty}, context=context)
396         return True
397
398     def generate_po(self, cr, uid, tender_id, context=None):
399         #call generate_po from tender with active_id. Called from js widget
400         return self.pool.get('purchase.requisition').generate_po(cr, uid, [tender_id], context=context)
401
402
403 class product_template(osv.osv):
404     _inherit = 'product.template'
405
406     _columns = {
407         'purchase_requisition': fields.boolean('Call for Bids', help="Check this box to generate Call for Bids instead of generating requests for quotation from procurement.")
408     }
409
410
411 class procurement_order(osv.osv):
412     _inherit = 'procurement.order'
413     _columns = {
414         'requisition_id': fields.many2one('purchase.requisition', 'Latest Requisition')
415     }
416
417     def _run(self, cr, uid, procurement, context=None):
418         requisition_obj = self.pool.get('purchase.requisition')
419         warehouse_obj = self.pool.get('stock.warehouse')
420         if procurement.rule_id and procurement.rule_id.action == 'buy' and procurement.product_id.purchase_requisition:
421             warehouse_id = warehouse_obj.search(cr, uid, [('company_id', '=', procurement.company_id.id)], context=context)
422             requisition_id = requisition_obj.create(cr, uid, {
423                 'origin': procurement.origin,
424                 'date_end': procurement.date_planned,
425                 'warehouse_id': warehouse_id and warehouse_id[0] or False,
426                 'company_id': procurement.company_id.id,
427                 'procurement_id': procurement.id,
428                 'line_ids': [(0, 0, {
429                     'product_id': procurement.product_id.id,
430                     'product_uom_id': procurement.product_uom.id,
431                     'product_qty': procurement.product_qty
432
433                 })],
434             })
435             self.message_post(cr, uid, [procurement.id], body=_("Purchase Requisition created"), context=context)
436             return self.write(cr, uid, [procurement.id], {'requisition_id': requisition_id}, context=context)
437         return super(procurement_order, self)._run(cr, uid, procurement, context=context)
438
439     def _check(self, cr, uid, procurement, context=None):
440         requisition_obj = self.pool.get('purchase.requisition')
441         if procurement.rule_id and procurement.rule_id.action == 'buy' and procurement.product_id.purchase_requisition:
442             if procurement.requisition_id.state == 'done':
443                 if any([purchase.shipped for purchase in procurement.requisition_id.purchase_ids]):
444                     return True
445             return False
446         return super(procurement_order, self)._check(cr, uid, procurement, context=context)
447
448 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: