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 return self.write(cr, uid, ids, {'state': 'cancel'})
96 def tender_in_progress(self, cr, uid, ids, context=None):
97 return self.write(cr, uid, ids, {'state': 'in_progress'}, context=context)
99 def tender_open(self, cr, uid, ids, context=None):
100 return self.write(cr, uid, ids, {'state': 'open'}, context=context)
102 def tender_reset(self, cr, uid, ids, context=None):
103 self.write(cr, uid, ids, {'state': 'draft'})
105 # Deleting the existing instance of workflow for PO
106 self.delete_workflow(cr, uid, [p_id])
107 self.create_workflow(cr, uid, [p_id])
110 def tender_done(self, cr, uid, ids, context=None):
111 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
113 def open_product_line(self, cr, uid, ids, context=None):
114 """ This opens product line view to view all lines from the different quotations, groupby default by product and partner to show comparaison
115 between supplier price
116 @return: the product line tree view
120 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'purchase_requisition', 'purchase_line_tree', context=context)
121 res['context'] = context
122 po_lines = self.browse(cr, uid, ids, context=context)[0].po_line_ids
124 'search_default_groupby_product': True,
125 'search_default_hide_cancelled': True,
127 res['domain'] = [('id', 'in', [line.id for line in po_lines])]
130 def open_rfq(self, cr, uid, ids, context=None):
131 """ This opens rfq view to view all quotations associated to the call for bids
132 @return: the RFQ tree view
136 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'purchase', 'purchase_rfq', context=context)
137 res['context'] = context
138 po_ids = [po.id for po in self.browse(cr, uid, ids, context=context)[0].purchase_ids]
139 res['domain'] = [('id', 'in', po_ids)]
142 def _prepare_purchase_order(self, cr, uid, requisition, supplier, context=None):
143 supplier_pricelist = supplier.property_product_pricelist_purchase and supplier.property_product_pricelist_purchase.id or False
144 picking_type_in = self.pool.get("purchase.order")._get_picking_in(cr, uid, context=context)
146 'origin': requisition.name,
147 'date_order': requisition.date_end or fields.date.context_today(self, cr, uid, context=context),
148 'partner_id': supplier.id,
149 'pricelist_id': supplier_pricelist,
150 'location_id': requisition.picking_type_id.default_location_dest_id.id,
151 'company_id': requisition.company_id.id,
152 'fiscal_position': supplier.property_account_position and supplier.property_account_position.id or False,
153 'requisition_id': requisition.id,
154 'notes': requisition.description,
155 'picking_type_id': picking_type_in,
158 def _prepare_purchase_order_line(self, cr, uid, requisition, requisition_line, purchase_id, supplier, context=None):
159 po_line_obj = self.pool.get('purchase.order.line')
160 product_uom = self.pool.get('product.uom')
161 product = requisition_line.product_id
162 default_uom_po_id = product.uom_po_id.id
163 date_order = requisition.ordering_date or fields.date.context_today(self, cr, uid, context=context)
164 qty = product_uom._compute_qty(cr, uid, requisition_line.product_uom_id.id, requisition_line.product_qty, default_uom_po_id)
165 supplier_pricelist = supplier.property_product_pricelist_purchase and supplier.property_product_pricelist_purchase.id or False
166 vals = po_line_obj.onchange_product_id(cr, uid, [], supplier_pricelist, product.id, qty, default_uom_po_id,
167 supplier.id, date_order=date_order, fiscal_position_id=supplier.property_account_position, date_planned=requisition_line.schedule_date,
168 name=False, price_unit=False, state='draft', context=context)['value']
170 'order_id': purchase_id,
171 'product_id': product.id,
172 'account_analytic_id': requisition_line.account_analytic_id.id,
176 def make_purchase_order(self, cr, uid, ids, partner_id, context=None):
178 Create New RFQ for Supplier
182 assert partner_id, 'Supplier should be specified'
183 purchase_order = self.pool.get('purchase.order')
184 purchase_order_line = self.pool.get('purchase.order.line')
185 res_partner = self.pool.get('res.partner')
186 supplier = res_partner.browse(cr, uid, partner_id, context=context)
188 for requisition in self.browse(cr, uid, ids, context=context):
189 if not requisition.multiple_rfq_per_supplier and supplier.id in filter(lambda x: x, [rfq.state != 'cancel' and rfq.partner_id.id or None for rfq in requisition.purchase_ids]):
190 raise osv.except_osv(_('Warning!'), _('You have already one %s purchase order for this partner, you must cancel this purchase order to create a new quotation.') % rfq.state)
191 context.update({'mail_create_nolog': True})
192 purchase_id = purchase_order.create(cr, uid, self._prepare_purchase_order(cr, uid, requisition, supplier, context=context), context=context)
193 purchase_order.message_post(cr, uid, [purchase_id], body=_("RFQ created"), context=context)
194 res[requisition.id] = purchase_id
195 for line in requisition.line_ids:
196 purchase_order_line.create(cr, uid, self._prepare_purchase_order_line(cr, uid, requisition, line, purchase_id, supplier, context=context), context=context)
199 def check_valid_quotation(self, cr, uid, quotation, context=None):
201 Check if a quotation has all his order lines bid in order to confirm it if its the case
202 return True if all order line have been selected during bidding process, else return False
204 args : 'quotation' must be a browse record
206 for line in quotation.order_line:
207 if line.state != 'confirmed' or line.product_qty != line.quantity_bid:
211 def _prepare_po_from_tender(self, cr, uid, tender, context=None):
212 """ Prepare the values to write in the purchase order
213 created from a tender.
215 :param tender: the source tender from which we generate a purchase order
217 return {'order_line': [],
218 'requisition_id': tender.id,
219 'origin': tender.name}
221 def _prepare_po_line_from_tender(self, cr, uid, tender, line, purchase_id, context=None):
222 """ Prepare the values to write in the purchase order line
223 created from a line of the tender.
225 :param tender: the source tender from which we generate a purchase order
226 :param line: the source tender's line from which we generate a line
227 :param purchase_id: the id of the new purchase
229 return {'product_qty': line.quantity_bid,
230 'order_id': purchase_id}
232 def generate_po(self, cr, uid, ids, context=None):
234 Generate all purchase order based on selected lines, should only be called on one tender at a time
238 po = self.pool.get('purchase.order')
239 poline = self.pool.get('purchase.order.line')
241 for tender in self.browse(cr, uid, ids, context=context):
242 if tender.state == 'done':
243 raise osv.except_osv(_('Warning!'), _('You have already generate the purchase order(s).'))
246 #check that we have at least confirm one line
247 for po_line in tender.po_line_ids:
248 if po_line.state == 'confirmed':
252 raise osv.except_osv(_('Warning!'), _('You have no line selected for buying.'))
254 #check for complete RFQ
255 for quotation in tender.purchase_ids:
256 if (self.check_valid_quotation(cr, uid, quotation, context=context)):
257 #use workflow to set PO state to confirm
258 po.signal_purchase_confirm(cr, uid, [quotation.id])
260 #get other confirmed lines per supplier
261 for po_line in tender.po_line_ids:
262 #only take into account confirmed line that does not belong to already confirmed purchase order
263 if po_line.state == 'confirmed' and po_line.order_id.state in ['draft', 'sent', 'bid']:
264 if id_per_supplier.get(po_line.partner_id.id):
265 id_per_supplier[po_line.partner_id.id].append(po_line)
267 id_per_supplier[po_line.partner_id.id] = [po_line]
269 #generate po based on supplier and cancel all previous RFQ
271 ctx['force_requisition_id'] = True
272 for supplier, product_line in id_per_supplier.items():
273 #copy a quotation for this supplier and change order_line then validate it
274 quotation_id = po.search(cr, uid, [('requisition_id', '=', tender.id), ('partner_id', '=', supplier)], limit=1)[0]
275 vals = self._prepare_po_from_tender(cr, uid, tender, context=context)
276 new_po = po.copy(cr, uid, quotation_id, default=vals, context=ctx)
277 #duplicate po_line and change product_qty if needed and associate them to newly created PO
278 for line in product_line:
279 vals = self._prepare_po_line_from_tender(cr, uid, tender, line, new_po, context=context)
280 poline.copy(cr, uid, line.id, default=vals, context=context)
281 #use workflow to set new PO state to confirm
282 po.signal_purchase_confirm(cr, uid, [new_po])
285 self.cancel_unconfirmed_quotations(cr, uid, tender, context=context)
287 #set tender to state done
288 self.signal_done(cr, uid, [tender.id])
291 def cancel_unconfirmed_quotations(self, cr, uid, tender, context=None):
293 po = self.pool.get('purchase.order')
294 for quotation in tender.purchase_ids:
295 if quotation.state in ['draft', 'sent', 'bid']:
296 self.pool.get('purchase.order').signal_purchase_cancel(cr, uid, [quotation.id])
297 po.message_post(cr, uid, [quotation.id], body=_('Cancelled by the call for bids associated to this request for quotation.'), context=context)
301 class purchase_requisition_line(osv.osv):
302 _name = "purchase.requisition.line"
303 _description = "Purchase Requisition Line"
304 _rec_name = 'product_id'
307 'product_id': fields.many2one('product.product', 'Product'),
308 'product_uom_id': fields.many2one('product.uom', 'Product Unit of Measure'),
309 'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure')),
310 'requisition_id': fields.many2one('purchase.requisition', 'Call for Bids', ondelete='cascade'),
311 'company_id': fields.related('requisition_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
312 'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic Account',),
313 'schedule_date': fields.date('Scheduled Date'),
316 def onchange_product_id(self, cr, uid, ids, product_id, product_uom_id, parent_analytic_account, analytic_account, parent_date, date, context=None):
317 """ Changes UoM and name if product_id changes.
318 @param name: Name of the field
319 @param product_id: Changed product_id
320 @return: Dictionary of changed values
322 value = {'product_uom_id': ''}
324 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
325 value = {'product_uom_id': prod.uom_id.id, 'product_qty': 1.0}
326 if not analytic_account:
327 value.update({'account_analytic_id': parent_analytic_account})
329 value.update({'schedule_date': parent_date})
330 return {'value': value}
333 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.requisition.line', context=c),
336 class purchase_order(osv.osv):
337 _inherit = "purchase.order"
340 'requisition_id': fields.many2one('purchase.requisition', 'Call for Bids'),
343 def wkf_confirm_order(self, cr, uid, ids, context=None):
344 res = super(purchase_order, self).wkf_confirm_order(cr, uid, ids, context=context)
345 proc_obj = self.pool.get('procurement.order')
346 for po in self.browse(cr, uid, ids, context=context):
347 if po.requisition_id and (po.requisition_id.exclusive == 'exclusive'):
348 for order in po.requisition_id.purchase_ids:
349 if order.id != po.id:
350 proc_ids = proc_obj.search(cr, uid, [('purchase_id', '=', order.id)])
351 if proc_ids and po.state == 'confirmed':
352 proc_obj.write(cr, uid, proc_ids, {'purchase_id': po.id})
353 self.signal_purchase_cancel(cr, uid, [order.id])
354 po.requisition_id.tender_done(context=context)
357 def copy(self, cr, uid, id, default=None, context=None):
360 if not context.get('force_requisition_id'):
361 default = default or {}
362 default.update({'requisition_id': False})
363 return super(purchase_order, self).copy(cr, uid, id, default=default, context=context)
365 def _prepare_order_line_move(self, cr, uid, order, order_line, picking_id, group_id, context=None):
366 stock_move_lines = super(purchase_order, self)._prepare_order_line_move(cr, uid, order, order_line, picking_id, group_id, context=context)
367 if order.requisition_id and order.requisition_id.procurement_id and order.requisition_id.procurement_id.move_dest_id:
368 for i in range(0, len(stock_move_lines)):
369 stock_move_lines[i]['move_dest_id'] = order.requisition_id.procurement_id.move_dest_id.id
370 return stock_move_lines
373 class purchase_order_line(osv.osv):
374 _inherit = 'purchase.order.line'
377 'quantity_bid': fields.float('Quantity Bid', digits_compute=dp.get_precision('Product Unit of Measure'), help="Technical field for not loosing the initial information about the quantity proposed in the bid"),
380 def action_draft(self, cr, uid, ids, context=None):
381 self.write(cr, uid, ids, {'state': 'draft'}, context=context)
383 def action_confirm(self, cr, uid, ids, context=None):
384 super(purchase_order_line, self).action_confirm(cr, uid, ids, context=context)
385 for element in self.browse(cr, uid, ids, context=context):
386 if not element.quantity_bid:
387 self.write(cr, uid, ids, {'quantity_bid': element.product_qty}, context=context)
390 def generate_po(self, cr, uid, tender_id, context=None):
391 #call generate_po from tender with active_id. Called from js widget
392 return self.pool.get('purchase.requisition').generate_po(cr, uid, [tender_id], context=context)
395 class product_product(osv.osv):
396 _inherit = 'product.product'
399 'purchase_requisition': fields.boolean('Call for Bids', help="Check this box to generate Call for Bids instead of generating requests for quotation from procurement.")
403 class procurement_order(osv.osv):
404 _inherit = 'procurement.order'
406 'requisition_id': fields.many2one('purchase.requisition', 'Latest Requisition')
409 def _run(self, cr, uid, procurement, context=None):
410 requisition_obj = self.pool.get('purchase.requisition')
411 warehouse_obj = self.pool.get('stock.warehouse')
412 if procurement.rule_id and procurement.rule_id.action == 'buy' and procurement.product_id.purchase_requisition:
413 warehouse_id = warehouse_obj.search(cr, uid, [('company_id', '=', procurement.company_id.id)], context=context)
414 requisition_id = requisition_obj.create(cr, uid, {
415 'origin': procurement.origin,
416 'date_end': procurement.date_planned,
417 'warehouse_id': warehouse_id and warehouse_id[0] or False,
418 'company_id': procurement.company_id.id,
419 'procurement_id': procurement.id,
420 'line_ids': [(0, 0, {
421 'product_id': procurement.product_id.id,
422 'product_uom_id': procurement.product_uom.id,
423 'product_qty': procurement.product_qty
427 self.message_post(cr, uid, [procurement.id], body=_("Purchase Requisition created"), context=context)
428 return self.write(cr, uid, [procurement.id], {'requisition_id': requisition_id}, context=context)
429 return super(procurement_order, self)._run(cr, uid, procurement, context=context)
431 def _check(self, cr, uid, procurement, context=None):
432 requisition_obj = self.pool.get('purchase.requisition')
433 if procurement.rule_id and procurement.rule_id.action == 'buy' and procurement.product_id.purchase_requisition:
434 if procurement.requisition_id.state == 'done':
435 if any([purchase.shipped for purchase in procurement.requisition_id.purchase_ids]):
438 return super(procurement_order, self)._check(cr, uid, procurement, context=context)
440 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: