1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>). All Rights Reserved
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.
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.
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/>.
21 ##############################################################################
22 from datetime import datetime
23 from dateutil.relativedelta import relativedelta
26 from openerp.osv import fields, osv
27 from openerp.tools.translate import _
28 import openerp.addons.decimal_precision as dp
30 class purchase_requisition(osv.osv):
31 _name = "purchase.requisition"
32 _description = "Purchase Requisition"
33 _inherit = ['mail.thread', 'ir.needaction_mixin']
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]
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),
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]
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,
77 def copy(self, cr, uid, id, default=None, context=None):
78 default = default or {}
82 'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order.requisition'),
84 return super(purchase_requisition, self).copy(cr, uid, id, default, context)
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
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'})
98 def tender_in_progress(self, cr, uid, ids, context=None):
99 return self.write(cr, uid, ids, {'state': 'in_progress'}, context=context)
101 def tender_open(self, cr, uid, ids, context=None):
102 return self.write(cr, uid, ids, {'state': 'open'}, context=context)
104 def tender_reset(self, cr, uid, ids, context=None):
105 self.write(cr, uid, ids, {'state': 'draft'})
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])
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)
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
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
128 'search_default_groupby_product': True,
129 'search_default_hide_cancelled': True,
131 res['domain'] = [('id', 'in', [line.id for line in po_lines])]
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
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)]
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)
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,
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']
174 'order_id': purchase_id,
175 'product_id': product.id,
176 'account_analytic_id': requisition_line.account_analytic_id.id,
180 def make_purchase_order(self, cr, uid, ids, partner_id, context=None):
182 Create New RFQ for Supplier
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)
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)
203 def check_valid_quotation(self, cr, uid, quotation, context=None):
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
208 args : 'quotation' must be a browse record
210 for line in quotation.order_line:
211 if line.state != 'confirmed' or line.product_qty != line.quantity_bid:
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.
219 :param tender: the source tender from which we generate a purchase order
221 return {'order_line': [],
222 'requisition_id': tender.id,
223 'origin': tender.name}
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.
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
233 return {'product_qty': line.quantity_bid,
234 'order_id': purchase_id}
236 def generate_po(self, cr, uid, ids, context=None):
238 Generate all purchase order based on selected lines, should only be called on one tender at a time
242 po = self.pool.get('purchase.order')
243 poline = self.pool.get('purchase.order.line')
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).'))
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':
256 raise osv.except_osv(_('Warning!'), _('You have no line selected for buying.'))
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])
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)
271 id_per_supplier[po_line.partner_id.id] = [po_line]
273 #generate po based on supplier and cancel all previous RFQ
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])
289 self.cancel_unconfirmed_quotations(cr, uid, tender, context=context)
291 #set tender to state done
292 self.signal_done(cr, uid, [tender.id])
295 def cancel_unconfirmed_quotations(self, cr, uid, tender, context=None):
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)
305 class purchase_requisition_line(osv.osv):
306 _name = "purchase.requisition.line"
307 _description = "Purchase Requisition Line"
308 _rec_name = 'product_id'
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'),
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
326 value = {'product_uom_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})
333 value.update({'schedule_date': parent_date})
334 return {'value': value}
337 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.requisition.line', context=c),
340 class purchase_order(osv.osv):
341 _inherit = "purchase.order"
344 'requisition_id': fields.many2one('purchase.requisition', 'Call for Bids'),
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})
365 def copy(self, cr, uid, id, default=None, context=None):
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)
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
381 class purchase_order_line(osv.osv):
382 _inherit = 'purchase.order.line'
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"),
388 def action_draft(self, cr, uid, ids, context=None):
389 self.write(cr, uid, ids, {'state': 'draft'}, context=context)
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)
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)
403 class product_template(osv.osv):
404 _inherit = 'product.template'
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.")
411 class procurement_order(osv.osv):
412 _inherit = 'procurement.order'
414 'requisition_id': fields.many2one('purchase.requisition', 'Latest Requisition')
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
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)
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]):
446 return super(procurement_order, self)._check(cr, uid, procurement, context=context)
448 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: