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 ##############################################################################
23 from openerp.osv import fields, osv
24 from openerp.tools.translate import _
25 import openerp.addons.decimal_precision as dp
27 class purchase_requisition(osv.osv):
28 _name = "purchase.requisition"
29 _description = "Purchase Requisition"
30 _inherit = ['mail.thread', 'ir.needaction_mixin']
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]
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),
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]
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,
74 def copy(self, cr, uid, id, default=None, context=None):
75 default = default or {}
79 'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order.requisition'),
81 return super(purchase_requisition, self).copy(cr, uid, id, default, context)
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'})
92 def tender_in_progress(self, cr, uid, ids, context=None):
93 return self.write(cr, uid, ids, {'state': 'in_progress'}, context=context)
95 def tender_open(self, cr, uid, ids, context=None):
96 return self.write(cr, uid, ids, {'state': 'open'}, context=context)
98 def tender_reset(self, cr, uid, ids, context=None):
99 self.write(cr, uid, ids, {'state': 'draft'})
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])
106 def tender_done(self, cr, uid, ids, context=None):
107 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
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
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
120 'search_default_groupby_product': True,
121 'search_default_hide_cancelled': True,
123 res['domain'] = [('id', 'in', [line.id for line in po_lines])]
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
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)]
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)
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,
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']
169 'order_id': purchase_id,
170 'product_id': product.id,
171 'account_analytic_id': requisition_line.account_analytic_id.id,
175 def make_purchase_order(self, cr, uid, ids, partner_id, context=None):
177 Create New RFQ for Supplier
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)
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)
198 def check_valid_quotation(self, cr, uid, quotation, context=None):
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
203 args : 'quotation' must be a browse record
205 for line in quotation.order_line:
206 if line.state != 'confirmed' or line.product_qty != line.quantity_bid:
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.
214 :param tender: the source tender from which we generate a purchase order
216 return {'order_line': [],
217 'requisition_id': tender.id,
218 'origin': tender.name}
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.
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
228 return {'product_qty': line.quantity_bid,
229 'order_id': purchase_id}
231 def generate_po(self, cr, uid, ids, context=None):
233 Generate all purchase order based on selected lines, should only be called on one tender at a time
235 po = self.pool.get('purchase.order')
236 poline = self.pool.get('purchase.order.line')
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).'))
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':
249 raise osv.except_osv(_('Warning!'), _('You have no line selected for buying.'))
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])
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)
264 id_per_supplier[po_line.partner_id.id] = [po_line]
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])
281 self.cancel_unconfirmed_quotations(cr, uid, tender, context=context)
283 #set tender to state done
284 self.signal_done(cr, uid, [tender.id])
287 def cancel_unconfirmed_quotations(self, cr, uid, tender, context=None):
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)
297 class purchase_requisition_line(osv.osv):
298 _name = "purchase.requisition.line"
299 _description = "Purchase Requisition Line"
300 _rec_name = 'product_id'
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'),
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
318 value = {'product_uom_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})
325 value.update({'schedule_date': parent_date})
326 return {'value': value}
329 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.requisition.line', context=c),
332 class purchase_order(osv.osv):
333 _inherit = "purchase.order"
336 'requisition_id': fields.many2one('purchase.requisition', 'Call for Bids'),
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)
353 def copy(self, cr, uid, id, default=None, context=None):
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)
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
369 class purchase_order_line(osv.osv):
370 _inherit = 'purchase.order.line'
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"),
376 def action_draft(self, cr, uid, ids, context=None):
377 self.write(cr, uid, ids, {'state': 'draft'}, context=context)
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)
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)
391 class product_template(osv.osv):
392 _inherit = 'product.template'
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.")
399 class procurement_order(osv.osv):
400 _inherit = 'procurement.order'
402 'requisition_id': fields.many2one('purchase.requisition', 'Latest Requisition')
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
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)
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]):
433 return super(procurement_order, self)._check(cr, uid, procurement, context=context)