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