1
# -*- coding: utf-8 -*-
2
##############################################################################
4
# Copyright (C) 2011 MSF, TeMPO Consulting
6
# This program is free software: you can redistribute it and/or modify
7
# it under the terms of the GNU Affero General Public License as
8
# published by the Free Software Foundation, either version 3 of the
9
# License, or (at your option) any later version.
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU Affero General Public License for more details.
16
# You should have received a copy of the GNU Affero General Public License
17
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
##############################################################################
21
from datetime import datetime, timedelta, date
22
from dateutil.relativedelta import relativedelta, relativedelta
23
from order_types import ORDER_PRIORITY, ORDER_CATEGORY
24
from osv import osv, fields
25
from osv.orm import browse_record, browse_null
26
from tools.translate import _
27
from lxml import etree
29
import decimal_precision as dp
35
from lxml import etree
37
from purchase_override import PURCHASE_ORDER_STATE_SELECTION
39
class tender(osv.osv):
44
_description = 'Tender'
46
def copy(self, cr, uid, id, default=None, context=None, done_list=[], local=False):
49
default['internal_state'] = 'draft' # UF-733: Reset the internal_state
50
return super(osv.osv, self).copy(cr, uid, id, default, context=context)
52
def unlink(self, cr, uid, ids, context=None):
54
cannot delete tender not draft
58
if isinstance(ids, (int, long)):
61
for obj in self.browse(cr, uid, ids, context=context):
62
if obj.state != 'draft':
63
raise osv.except_osv(_('Warning !'), _("Cannot delete Tenders not in 'draft' state."))
64
return super(tender, self).unlink(cr, uid, ids, context=context)
66
def _vals_get(self, cr, uid, ids, fields, arg, context=None):
68
return function values
71
for obj in self.browse(cr, uid, ids, context=context):
72
result[obj.id] = {'rfq_name_list': '',
75
for rfq in obj.rfq_ids:
76
rfq_names.append(rfq.name)
79
result[obj.id]['rfq_name_list'] = ','.join(rfq_names)
83
def _is_tender_from_fo(self, cr, uid, ids, field_name, args, context=None):
85
for tender in self.browse(cr, uid, ids, context=context):
87
ids_proc = self.pool.get('procurement.order').search(cr,uid,[('tender_id','=',tender.id)])
88
ids_sol = self.pool.get('sale.order.line').search(cr,uid,[('procurement_id','in',ids_proc),('order_id.procurement_request','=',False)])
91
res[tender.id] = retour
94
_columns = {'name': fields.char('Tender Reference', size=64, required=True, select=True, readonly=True),
95
'sale_order_id': fields.many2one('sale.order', string="Sale Order", readonly=True),
96
'state': fields.selection([('draft', 'Draft'),('comparison', 'Comparison'), ('done', 'Closed'), ('cancel', 'Cancelled'),], string="State", readonly=True),
97
'supplier_ids': fields.many2many('res.partner', 'tender_supplier_rel', 'tender_id', 'supplier_id', string="Suppliers", domain="[('id', '!=', company_id)]",
98
states={'draft':[('readonly',False)]}, readonly=True,
99
context={'search_default_supplier': 1,}),
100
'location_id': fields.many2one('stock.location', 'Location', required=True, states={'draft':[('readonly',False)]}, readonly=True, domain=[('usage', '=', 'internal')]),
101
'company_id': fields.many2one('res.company','Company',required=True, states={'draft':[('readonly',False)]}, readonly=True),
102
'rfq_ids': fields.one2many('purchase.order', 'tender_id', string="RfQs", readonly=True),
103
'priority': fields.selection(ORDER_PRIORITY, string='Tender Priority', states={'draft':[('readonly',False)],}, readonly=True,),
104
'categ': fields.selection(ORDER_CATEGORY, string='Tender Category', required=True, states={'draft':[('readonly',False)],}, readonly=True),
105
'creator': fields.many2one('res.users', string="Creator", readonly=True, required=True,),
106
'warehouse_id': fields.many2one('stock.warehouse', string="Warehouse", required=True, states={'draft':[('readonly',False)],}, readonly=True),
107
'creation_date': fields.date(string="Creation Date", readonly=True, states={'draft':[('readonly',False)]}),
108
'details': fields.char(size=30, string="Details", states={'draft':[('readonly',False)],}, readonly=True),
109
'requested_date': fields.date(string="Requested Date", required=True, states={'draft':[('readonly',False)],}, readonly=True),
110
'notes': fields.text('Notes'),
111
'internal_state': fields.selection([('draft', 'Draft'),('updated', 'Rfq Updated'), ], string="Internal State", readonly=True),
112
'rfq_name_list': fields.function(_vals_get, method=True, string='RfQs Ref', type='char', readonly=True, store=False, multi='get_vals',),
113
'product_id': fields.related('tender_line_ids', 'product_id', type='many2one', relation='product.product', string='Product'),
114
'tender_from_fo': fields.function(_is_tender_from_fo, method=True, type='boolean', string='Is tender from FO ?',),
117
_defaults = {'categ': 'other',
119
'internal_state': 'draft',
120
'company_id': lambda obj, cr, uid, context: obj.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id,
121
'creator': lambda obj, cr, uid, context: uid,
122
'creation_date': lambda *a: time.strftime('%Y-%m-%d'),
123
'requested_date': lambda *a: time.strftime('%Y-%m-%d'),
124
'priority': 'normal',
125
'warehouse_id': lambda obj, cr, uid, context: len(obj.pool.get('stock.warehouse').search(cr, uid, [])) and obj.pool.get('stock.warehouse').search(cr, uid, [])[0],
130
def _check_tender_from_fo(self, cr, uid, ids, context=None):
134
for tender in self.browse(cr, uid, ids, context=context):
135
if not tender.tender_from_fo:
137
for sup in tender.supplier_ids:
138
if sup.partner_type == 'internal' :
143
(_check_tender_from_fo, 'You cannot choose an internal supplier for this tender', []),
146
def create(self, cr, uid, vals, context=None):
148
Set the reference of the tender at this time
150
if not vals.get('name', False):
151
vals.update({'name': self.pool.get('ir.sequence').get(cr, uid, 'tender')})
152
return super(tender, self).create(cr, uid, vals, context=context)
154
def onchange_warehouse(self, cr, uid, ids, warehouse_id, context=None):
156
on_change function for the warehouse
158
result = {'value':{},}
160
input_loc_id = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context).lot_input_id.id
161
result['value'].update(location_id=input_loc_id)
165
def wkf_generate_rfq(self, cr, uid, ids, context=None):
167
generate the rfqs for each specified supplier
171
po_obj = self.pool.get('purchase.order')
172
pol_obj = self.pool.get('purchase.order.line')
173
partner_obj = self.pool.get('res.partner')
174
pricelist_obj = self.pool.get('product.pricelist')
175
obj_data = self.pool.get('ir.model.data')
176
# no suppliers -> raise error
177
for tender in self.browse(cr, uid, ids, context=context):
178
# check some supplier have been selected
179
if not tender.supplier_ids:
180
raise osv.except_osv(_('Warning !'), _('You must select at least one supplier!'))
181
#utp-315: check that the suppliers are not inactive (I use a SQL request because the inactive partner are ignored with the browse)
183
select tsr.supplier_id, rp.name, rp.active
184
from tender_supplier_rel tsr
185
left join res_partner rp
186
on tsr.supplier_id = rp.id
187
where tsr.tender_id=%s
190
cr.execute(sql, (ids[0],))
191
inactive_supplier_ids = cr.dictfetchall()
192
if any(inactive_supplier_ids):
193
raise osv.except_osv(_('Warning !'), _("You can't have inactive supplier! Please remove: %s"
194
) % ' ,'.join([partner['name'] for partner in inactive_supplier_ids]))
195
# check some products have been selected
196
tender_line_ids = self.pool.get('tender.line').search(cr, uid, [('tender_id', '=', tender.id)], context=context)
197
if not tender_line_ids:
198
raise osv.except_osv(_('Warning !'), _('You must select at least one product!'))
199
for supplier in tender.supplier_ids:
200
# create a purchase order for each supplier
201
address_id = partner_obj.address_get(cr, uid, [supplier.id], ['delivery'])['delivery']
203
raise osv.except_osv(_('Warning !'), _('The supplier "%s" has no address defined!')%(supplier.name,))
204
pricelist_id = supplier.property_product_pricelist_purchase.id
205
values = {'origin': tender.sale_order_id and tender.sale_order_id.name + ';' + tender.name or tender.name,
207
'partner_id': supplier.id,
208
'partner_address_id': address_id,
209
'location_id': tender.location_id.id,
210
'pricelist_id': pricelist_id,
211
'company_id': tender.company_id.id,
212
'fiscal_position': supplier.property_account_position and supplier.property_account_position.id or False,
213
'tender_id': tender.id,
214
'warehouse_id': tender.warehouse_id.id,
215
'categ': tender.categ,
216
'priority': tender.priority,
217
'details': tender.details,
218
'delivery_requested_date': tender.requested_date,
220
# create the rfq - dic is udpated for default partner_address_id at purchase.order level
221
po_id = po_obj.create(cr, uid, values, context=dict(context, partner_id=supplier.id, rfq_ok=True))
223
for line in tender.tender_line_ids:
224
if line.product_id.id == obj_data.get_object_reference(cr, uid,'msf_doc_import', 'product_tbd')[1]:
225
raise osv.except_osv(_('Warning !'), _('You can\'t have "To Be Defined" for the product. Please select an existing product.'))
226
# create an order line for each tender line
227
price = pricelist_obj.price_get(cr, uid, [pricelist_id], line.product_id.id, line.qty, supplier.id, {'uom': line.product_uom.id})[pricelist_id]
228
newdate = datetime.strptime(line.date_planned, '%Y-%m-%d')
229
#newdate = (newdate - relativedelta(days=tender.company_id.po_lead)) - relativedelta(days=int(supplier.default_delay)) # requested by Magali uf-489
230
values = {'name': line.product_id.partner_ref,
231
'product_qty': line.qty,
232
'product_id': line.product_id.id,
233
'product_uom': line.product_uom.id,
234
'price_unit': 0.0, # was price variable - uf-607
235
'date_planned': newdate.strftime('%Y-%m-%d'),
236
'notes': line.product_id.description_purchase,
239
# create purchase order line
240
pol_id = pol_obj.create(cr, uid, values, context=context)
241
message = "Request for Quotation '%s' has been created."%po_obj.browse(cr, uid, po_id, context=context).name
242
# create the log message
243
self.pool.get('res.log').create(cr, uid,
245
'res_model': po_obj._name,
248
'domain': [('rfq_ok', '=', True)],
249
}, context={'rfq_ok': True})
251
self.write(cr, uid, ids, {'state':'comparison'}, context=context)
254
def wkf_action_done(self, cr, uid, ids, context=None):
258
# done all related rfqs
259
wf_service = netsvc.LocalService("workflow")
260
for tender in self.browse(cr, uid, ids, context=context):
262
for rfq in tender.rfq_ids:
263
if rfq.state not in ('rfq_updated', 'cancel',):
264
rfq_list.append(rfq.id)
266
wf_service.trg_validate(uid, 'purchase.order', rfq.id, 'rfq_done', cr)
268
# if some rfq have wrong state, we display a message
270
raise osv.except_osv(_('Warning !'), _("Generated RfQs must be Updated or Cancelled."))
272
# integrity check, all lines must have purchase_order_line_id
273
if not all([line.purchase_order_line_id.id for line in tender.tender_line_ids]):
274
raise osv.except_osv(_('Error !'), _('All tender lines must have been compared!'))
276
# update product supplierinfo and pricelist
277
self.update_supplier_info(cr, uid, ids, context=context, integrity_test=False,)
278
# change tender state
279
self.write(cr, uid, ids, {'state':'done'}, context=context)
282
def tender_integrity(self, cr, uid, tender, context=None):
284
check the state of corresponding RfQs
286
po_obj = self.pool.get('purchase.order')
287
# no rfq in done state
288
rfq_ids = po_obj.search(cr, uid, [('tender_id', '=', tender.id),
289
('state', 'in', ('done',)),], context=context)
291
raise osv.except_osv(_('Error !'), _("Some RfQ are already Closed. Integrity failure."))
292
# all rfqs must have been treated
293
rfq_ids = po_obj.search(cr, uid, [('tender_id', '=', tender.id),
294
('state', 'in', ('draft', 'rfq_sent',)),], context=context)
296
raise osv.except_osv(_('Warning !'), _("Generated RfQs must be Updated or Cancelled."))
297
# at least one rfq must be updated and not canceled
298
rfq_ids = po_obj.search(cr, uid, [('tender_id', '=', tender.id),
299
('state', 'in', ('rfq_updated',)),], context=context)
301
raise osv.except_osv(_('Warning !'), _("At least one RfQ must be in state Updated."))
305
def compare_rfqs(self, cr, uid, ids, context=None):
310
raise osv.except_osv(_('Warning !'), _('Cannot compare rfqs of more than one tender at a time!'))
311
po_obj = self.pool.get('purchase.order')
312
wiz_obj = self.pool.get('wizard.compare.rfq')
313
for tender in self.browse(cr, uid, ids, context=context):
314
# check if corresponding rfqs are in the good state
315
rfq_ids = self.tender_integrity(cr, uid, tender, context=context)
316
# gather the product_id -> supplier_id relationship to display it back in the compare wizard
318
for line in tender.tender_line_ids:
319
if line.product_id and line.supplier_id:
320
suppliers.update({line.product_id.id:line.supplier_id.id,})
321
# rfq corresponding to this tender with done state (has been updated and not canceled)
322
# the list of rfq which will be compared
323
c = dict(context, active_ids=rfq_ids, tender_id=tender.id, end_wizard=False, suppliers=suppliers,)
325
action = wiz_obj.start_compare_rfq(cr, uid, ids, context=c)
328
def update_supplier_info(self, cr, uid, ids, context=None, *args, **kwargs):
330
update the supplier info of corresponding products
332
info_obj = self.pool.get('product.supplierinfo')
333
pricelist_info_obj = self.pool.get('pricelist.partnerinfo')
334
# integrity check flag
335
integrity_test = kwargs.get('integrity_test', False)
336
for tender in self.browse(cr, uid, ids, context=context):
337
# flag if at least one update
338
updated = tender.tender_line_ids and False or True
339
# check if corresponding rfqs are in the good state
341
self.tender_integrity(cr, uid, tender, context=context)
342
for line in tender.tender_line_ids:
343
# if a supplier has been selected
344
if line.purchase_order_line_id:
348
product = line.product_id
349
# find the corresponding suppinfo with sequence -99
350
info_99_list = info_obj.search(cr, uid, [('product_id', '=', product.product_tmpl_id.id),
351
('sequence', '=', -99),], context=context)
355
info_obj.unlink(cr, uid, info_99_list, context=context)
358
values = {'name': line.supplier_id.id,
359
'product_name': False,
360
'product_code': False,
362
#'product_uom': line.product_uom.id,
365
'product_id' : product.product_tmpl_id.id,
366
'delay' : int(line.supplier_id.default_delay),
367
#'pricelist_ids': created just after
368
#'company_id': default value
371
new_info_id = info_obj.create(cr, uid, values, context=context)
372
# price lists creation - 'pricelist.partnerinfo
373
values = {'suppinfo_id': new_info_id,
374
'min_quantity': 1.00,
375
'price': line.price_unit,
376
'uom_id': line.product_uom.id,
377
'currency_id': line.purchase_order_line_id.currency_id.id,
378
'valid_till': line.purchase_order_id.valid_till,
379
'purchase_order_line_id': line.purchase_order_line_id.id,
380
'comment': 'RfQ original quantity for price : %s' % line.qty,
382
new_pricelist_id = pricelist_info_obj.create(cr, uid, values, context=context)
384
# warn the user if no update has been performed
386
raise osv.except_osv(_('Warning !'), _('No information available for update!'))
390
def done(self, cr, uid, ids, context=None):
392
method to perform checks before call to workflow
394
po_obj = self.pool.get('purchase.order')
395
wf_service = netsvc.LocalService("workflow")
396
for tender in self.browse(cr, uid, ids, context=context):
397
# check if corresponding rfqs are in the good state
398
self.tender_integrity(cr, uid, tender, context=context)
399
wf_service.trg_validate(uid, 'tender', tender.id, 'button_done', cr)
400
# trigger all related rfqs
401
rfq_ids = po_obj.search(cr, uid, [('tender_id', '=', tender.id),], context=context)
402
for rfq_id in rfq_ids:
403
wf_service.trg_validate(uid, 'purchase.order', rfq_id, 'rfq_done', cr)
407
def create_po(self, cr, uid, ids, context=None):
409
create a po from the updated RfQs
411
if isinstance(ids, (int, long)):
414
partner_obj = self.pool.get('res.partner')
415
po_obj = self.pool.get('purchase.order')
416
wf_service = netsvc.LocalService("workflow")
418
for tender in self.browse(cr, uid, ids, context=context):
419
# check if corresponding rfqs are in the good state
420
self.tender_integrity(cr, uid, tender, context=context)
421
# integrity check, all lines must have purchase_order_line_id
422
if not all([line.purchase_order_line_id.id for line in tender.tender_line_ids]):
423
raise osv.except_osv(_('Error !'), _('All tender lines must have been compared!'))
425
for line in tender.tender_line_ids:
426
data.setdefault(line.supplier_id.id, {}) \
427
.setdefault('order_line', []).append((0,0,{'name': line.product_id.partner_ref,
428
'product_qty': line.qty,
429
'product_id': line.product_id.id,
430
'product_uom': line.product_uom.id,
431
'change_price_manually': 'True',
432
'price_unit': line.price_unit,
433
'date_planned': line.date_planned,
434
'move_dest_id': False,
435
'notes': line.product_id.description_purchase,
438
# fill data corresponding to po creation
439
address_id = partner_obj.address_get(cr, uid, [line.supplier_id.id], ['delivery'])['delivery']
440
pricelist = line.supplier_id.property_product_pricelist_purchase.id,
442
price_ids = self.pool.get('product.pricelist').search(cr, uid, [('type', '=', 'purchase'), ('currency_id', '=', line.currency_id.id)], context=context)
444
pricelist = price_ids[0]
445
po_values = {'origin': (tender.sale_order_id and tender.sale_order_id.name or "") + ';' + tender.name,
446
'partner_id': line.supplier_id.id,
447
'partner_address_id': address_id,
448
'location_id': tender.location_id.id,
449
'pricelist_id': pricelist,
450
'company_id': tender.company_id.id,
451
'fiscal_position': line.supplier_id.property_account_position and line.supplier_id.property_account_position.id or False,
452
'categ': tender.categ,
453
'priority': tender.priority,
454
'origin_tender_id': tender.id,
455
#'tender_id': tender.id, # not for now, because tender_id is the flag for a po to be considered as RfQ
456
'warehouse_id': tender.warehouse_id.id,
457
'details': tender.details,
458
'delivery_requested_date': tender.requested_date,
460
data[line.supplier_id.id].update(po_values)
462
# create the pos, one for each selected supplier
463
for po_data in data.values():
464
po_id = po_obj.create(cr, uid, po_data, context=context)
465
po = po_obj.browse(cr, uid, po_id, context=context)
466
po_obj.log(cr, uid, po_id, 'The Purchase order %s for supplier %s has been created.'%(po.name, po.partner_id.name))
467
#UF-802: the PO created must be in draft state, and not validated!
468
#wf_service.trg_validate(uid, 'purchase.order', po_id, 'purchase_confirm', cr)
470
# when the po is generated, the tender is done - no more modification or comparison
471
self.done(cr, uid, [tender.id], context=context)
475
def wkf_action_cancel(self, cr, uid, ids, context=None):
477
cancel all corresponding rfqs
479
po_obj = self.pool.get('purchase.order')
480
wf_service = netsvc.LocalService("workflow")
482
self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
483
for tender in self.browse(cr, uid, ids, context=context):
484
# trigger all related rfqs
485
rfq_ids = po_obj.search(cr, uid, [('tender_id', '=', tender.id),], context=context)
486
for rfq_id in rfq_ids:
487
wf_service.trg_validate(uid, 'purchase.order', rfq_id, 'purchase_cancel', cr)
491
def set_manually_done(self, cr, uid, ids, all_doc=True, context=None):
493
Set the tender and all related documents to done state
495
if isinstance(ids, (int, long)):
498
wf_service = netsvc.LocalService("workflow")
500
for tender in self.browse(cr, uid, ids, context=context):
502
if tender.state not in ('done', 'cancel'):
503
for line in tender.tender_line_ids:
504
if line.purchase_order_line_id:
506
# Cancel or done all RfQ related to the tender
507
for rfq in tender.rfq_ids:
508
if rfq.state not in ('done', 'cancel'):
509
if rfq.state == 'draft' or not line_updated:
510
wf_service.trg_validate(uid, 'purchase.order', rfq.id, 'purchase_cancel', cr)
512
wf_service.trg_validate(uid, 'purchase.order', rfq.id, 'rfq_sent', cr)
513
if not rfq.valid_till:
514
self.pool.get('purchase.order').write(cr, uid, [rfq.id], {'valid_till': time.strftime('%Y-%m-%d')}, context=context)
515
wf_service.trg_validate(uid, 'purchase.order', rfq.id, 'rfq_updated', cr)
518
if tender.state == 'draft' or not tender.tender_line_ids or not line_updated:
519
# Call the cancel method of the tender
520
wf_service.trg_validate(uid, 'tender', tender.id, 'tender_cancel', cr)
522
# Call the cancel method of the tender
523
wf_service.trg_validate(uid, 'tender', tender.id, 'button_done', cr)
530
class tender_line(osv.osv):
534
_name = 'tender.line'
535
_description= 'Tender Line'
537
_SELECTION_TENDER_STATE = [('draft', 'Draft'),('comparison', 'Comparison'), ('done', 'Closed'),]
539
def on_product_change(self, cr, uid, id, product_id, context=None):
541
product is changed, we update the UoM
543
prod_obj = self.pool.get('product.product')
544
result = {'value': {}}
546
result['value']['product_uom'] = prod_obj.browse(cr, uid, product_id, context=context).uom_po_id.id
550
def _get_total_price(self, cr, uid, ids, field_name, arg, context=None):
552
return the total price
555
for line in self.browse(cr, uid, ids, context=context):
557
if line.price_unit and line.qty:
558
result[line.id]['total_price'] = line.price_unit * line.qty
560
result[line.id]['total_price'] = 0.0
562
result[line.id]['func_currency_id'] = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
563
if line.purchase_order_line_id:
564
result[line.id]['currency_id'] = line.purchase_order_line_id.order_id.pricelist_id.currency_id.id
566
result[line.id]['currency_id'] = result[line.id]['func_currency_id']
568
result[line.id]['func_total_price'] = self.pool.get('res.currency').compute(cr, uid, result[line.id]['currency_id'],
569
result[line.id]['func_currency_id'],
570
result[line.id]['total_price'],
571
round=True, context=context)
575
def name_get(self, cr, user, ids, context=None):
576
result = self.browse(cr, user, ids, context=context)
579
code = rs.product_id and rs.product_id.name or ''
580
res += [(rs.id, code)]
583
_columns = {'product_id': fields.many2one('product.product', string="Product", required=True),
584
'qty': fields.float(string="Qty", required=True),
585
'tender_id': fields.many2one('tender', string="Tender", required=True, ondelete='cascade'),
586
'purchase_order_line_id': fields.many2one('purchase.order.line', string="Related RfQ line", readonly=True),
587
'sale_order_line_id': fields.many2one('sale.order.line', string="Sale Order Line"),
588
'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
589
'date_planned': fields.related('tender_id', 'requested_date', type='date', string='Requested Date', store=False,),
591
'supplier_id': fields.related('purchase_order_line_id', 'order_id', 'partner_id', type='many2one', relation='res.partner', string="Supplier", readonly=True),
592
'price_unit': fields.related('purchase_order_line_id', 'price_unit', type="float", string="Price unit", digits_compute=dp.get_precision('Purchase Price Computation'), readonly=True), # same precision as related field!
593
'total_price': fields.function(_get_total_price, method=True, type='float', string="Total Price", digits_compute=dp.get_precision('Purchase Price'), multi='total'),
594
'currency_id': fields.function(_get_total_price, method=True, type='many2one', relation='res.currency', string='Cur.', multi='total'),
595
'func_total_price': fields.function(_get_total_price, method=True, type='float', string="Func. Total Price", digits_compute=dp.get_precision('Purchase Price'), multi='total'),
596
'func_currency_id': fields.function(_get_total_price, method=True, type='many2one', relation='res.currency', string='Func. Cur.', multi='total'),
597
'purchase_order_id': fields.related('purchase_order_line_id', 'order_id', type='many2one', relation='purchase.order', string="Related RfQ", readonly=True,),
598
'purchase_order_line_number': fields.related('purchase_order_line_id', 'line_number', type="integer", string="Related Line Number", readonly=True,),
599
'state': fields.related('tender_id', 'state', type="selection", selection=_SELECTION_TENDER_STATE, string="State",),
600
'comment': fields.char(size=128, string='Comment'),
602
_defaults = {'qty': lambda *a: 1.0,
603
'state': lambda *a: 'draft',
607
('product_qty_check', 'CHECK( qty > 0 )', 'Product Quantity must be greater than zero.'),
613
class tender2(osv.osv):
618
_columns = {'tender_line_ids': fields.one2many('tender.line', 'tender_id', string="Tender lines", states={'draft':[('readonly',False)]}, readonly=True),
621
def copy(self, cr, uid, id, default=None, context=None):
623
reset the name to get new sequence number
625
the copy method is here because upwards it goes in infinite loop
627
line_obj = self.pool.get('tender.line')
631
default.update(name=self.pool.get('ir.sequence').get(cr, uid, 'tender'),
633
sale_order_line_id=False,)
635
result = super(tender2, self).copy(cr, uid, id, default, context)
639
def copy_data(self, cr, uid, id, default=None, context=None):
641
reset the tender line
643
result = super(tender, self).copy_data(cr, uid, id, default=default, context=context)
644
# reset the tender line
645
for line in result['tender_line_ids']:
646
line[2].update(sale_order_line_id=False,
647
purchase_order_line_id=False,)
653
class procurement_order(osv.osv):
657
_inherit = 'procurement.order'
659
def _is_tender(self, cr, uid, ids, field_name, arg, context=None):
661
tell if the corresponding sale order line is tender sourcing or not
667
for proc in self.browse(cr, uid, ids, context=context):
668
for line in proc.sale_order_line_ids:
669
result[proc.id] = line.po_cft == 'cft'
673
_columns = {'is_tender': fields.function(_is_tender, method=True, type='boolean', string='Is Tender', readonly=True,),
674
'sale_order_line_ids': fields.one2many('sale.order.line', 'procurement_id', string="Sale Order Lines"),
675
'tender_id': fields.many2one('tender', string='Tender', readonly=True),
676
'is_tender_done': fields.boolean(string="Tender Closed"),
677
'state': fields.selection([('draft','Draft'),
678
('confirmed','Confirmed'),
679
('exception','Exception'),
680
('running','Converted'),
681
('cancel','Cancelled'),
684
('tender', 'Tender'),
685
('waiting','Waiting'),], 'State', required=True,
686
help='When a procurement is created the state is set to \'Draft\'.\n If the procurement is confirmed, the state is set to \'Confirmed\'.\
687
\nAfter confirming the state is set to \'Running\'.\n If any exception arises in the order then the state is set to \'Exception\'.\n Once the exception is removed the state becomes \'Ready\'.\n It is in \'Waiting\'. state when the procurement is waiting for another one to finish.'),
688
'price_unit': fields.float('Unit Price from Tender', digits_compute=dp.get_precision('Purchase Price Computation')),
690
_defaults = {'is_tender_done': False,}
692
def wkf_action_tender_create(self, cr, uid, ids, context=None):
694
creation of tender from procurement workflow
696
tender_obj = self.pool.get('tender')
697
tender_line_obj = self.pool.get('tender.line')
698
# find the corresponding sale order id for tender
699
for proc in self.browse(cr, uid, ids, context=context):
701
sale_order_line = False
702
for sol in proc.sale_order_line_ids:
703
sale_order = sol.order_id
704
sale_order_line = sol
707
tender_ids = tender_obj.search(cr, uid, [('sale_order_id', '=', sale_order.id),('state', '=', 'draft'),], context=context)
709
tender_id = tender_ids[0]
710
# create if not found
712
tender_id = tender_obj.create(cr, uid, {'sale_order_id': sale_order.id,
713
'location_id': proc.location_id.id,
714
'categ': sale_order.categ,
715
'priority': sale_order.priority,
716
'warehouse_id': sale_order.shop_id.warehouse_id.id,
717
'requested_date': proc.date_planned,
719
# add a line to the tender
720
tender_line_obj.create(cr, uid, {'product_id': proc.product_id.id,
721
'comment': sale_order_line.comment,
722
'qty': proc.product_qty,
723
'tender_id': tender_id,
724
'sale_order_line_id': sale_order_line.id,
725
'location_id': proc.location_id.id,
726
'product_uom': proc.product_uom.id,
727
#'date_planned': proc.date_planned, # function at line level
730
self.write(cr, uid, ids, {'tender_id': tender_id}, context=context)
732
# log message concerning tender creation
733
tender_obj.log(cr, uid, tender_id, "The tender '%s' has been created and must be completed before purchase order creation."%tender_obj.browse(cr, uid, tender_id, context=context).name)
734
# state of procurement is Tender
735
self.write(cr, uid, ids, {'state': 'tender'}, context=context)
739
def wkf_action_tender_done(self, cr, uid, ids, context=None):
741
set is_tender_done value
743
self.write(cr, uid, ids, {'is_tender_done': True, 'state': 'exception',}, context=context)
746
def action_po_assign(self, cr, uid, ids, context=None):
748
- convert the created rfq by the tender to a po
749
- add message at po creation during on_order workflow
751
po_obj = self.pool.get('purchase.order')
752
result = super(procurement_order, self).action_po_assign(cr, uid, ids, context=context)
753
# The quotation 'SO001' has been converted to a sales order.
755
# do not display a log if we come from po update backward update of so
756
data = self.read(cr, uid, ids, ['so_back_update_dest_po_id_procurement_order'], context=context)
757
if not data[0]['so_back_update_dest_po_id_procurement_order']:
758
po_obj.log(cr, uid, result, "The Purchase Order '%s' has been created following 'on order' sourcing."%po_obj.browse(cr, uid, result, context=context).name)
761
def po_values_hook(self, cr, uid, ids, context=None, *args, **kwargs):
763
data for the purchase order creation
765
values = super(procurement_order, self).po_values_hook(cr, uid, ids, context=context, *args, **kwargs)
766
procurement = kwargs['procurement']
768
# set tender link in purchase order
769
if procurement.tender_id:
770
values['origin_tender_id'] = procurement.tender_id.id
772
values['date_planned'] = procurement.date_planned
774
if procurement.product_id:
775
if procurement.product_id.type == 'consu':
776
values['location_id'] = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock_override', 'stock_location_non_stockable')[1]
777
elif procurement.product_id.type == 'service_recep':
778
values['location_id'] = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'msf_config_locations', 'stock_location_service')[1]
780
wh_ids = self.pool.get('stock.warehouse').search(cr, uid, [])
782
values['location_id'] = self.pool.get('stock.warehouse').browse(cr, uid, wh_ids[0]).lot_input_id.id
784
values['location_id'] = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'msf_config_locations', 'stock_location_service')[1]
791
class purchase_order(osv.osv):
795
_inherit = 'purchase.order'
797
def _check_valid_till(self, cr, uid, ids, context=None):
798
""" Checks if valid till has been completed
800
for obj in self.browse(cr, uid, ids, context=context):
801
if obj.state == 'rfq_updated' and not obj.valid_till:
804
_columns = {'tender_id': fields.many2one('tender', string="Tender", readonly=True),
805
'origin_tender_id': fields.many2one('tender', string='Tender', readonly=True),
806
'rfq_ok': fields.boolean(string='Is RfQ ?'),
807
'state': fields.selection(PURCHASE_ORDER_STATE_SELECTION, 'State', readonly=True, help="The state of the purchase order or the quotation request. A quotation is a purchase order in a 'Draft' state. Then the order has to be confirmed by the user, the state switch to 'Confirmed'. Then the supplier must confirm the order to change the state to 'Approved'. When the purchase order is paid and received, the state becomes 'Closed'. If a cancel action occurs in the invoice or in the reception of goods, the state becomes in exception.", select=True),
808
'valid_till': fields.date(string='Valid Till', states={'rfq_updated': [('required', True), ('readonly', True)], 'rfq_sent':[('required',False), ('readonly', False),]}, readonly=True,),
809
# add readonly when state is Done
813
'rfq_ok': lambda self, cr, uid, c: c.get('rfq_ok', False),
818
'You must specify a Valid Till date.',
821
def create(self, cr, uid, vals, context=None):
823
Set the reference at this step
827
if context.get('rfq_ok', False) and not vals.get('name', False):
828
vals.update({'name': self.pool.get('ir.sequence').get(cr, uid, 'rfq')})
829
elif not vals.get('name', False):
830
vals.update({'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order')})
832
return super(purchase_order, self).create(cr, uid, vals, context=context)
834
def unlink(self, cr, uid, ids, context=None):
836
Display an error message if the PO has associated IN
838
in_ids = self.pool.get('stock.picking').search(cr, uid, [('purchase_id', 'in', ids)], context=context)
840
raise osv.except_osv(_('Error !'), _('Cannot delete a document if its associated ' \
841
'document remains open. Please delete it (associated IN) first.'))
843
# Copy a part of purchase_order standard unlink method to fix the bad state on error message
844
purchase_orders = self.read(cr, uid, ids, ['state'], context=context)
846
for s in purchase_orders:
847
if s['state'] in ['draft','cancel']:
848
unlink_ids.append(s['id'])
850
raise osv.except_osv(_('Invalid action !'), _('Cannot delete Purchase Order(s) which are in %s State!') % _(dict(PURCHASE_ORDER_STATE_SELECTION).get(s['state'])))
852
return super(purchase_order, self).unlink(cr, uid, ids, context=context)
854
def _hook_copy_name(self, cr, uid, ids, context=None, *args, **kwargs):
856
HOOK from purchase>purchase.py for COPY function. Modification of default copy values
857
define which name value will be used
859
# default values from copy function
860
default = kwargs.get('default', False)
861
# flag defining if the new object will be a rfq
863
# calling super function
864
result = super(purchase_order, self)._hook_copy_name(cr, uid, ids, context=context, *args, **kwargs)
865
if default.get('rfq_ok', False):
867
elif 'rfq_ok' not in default:
868
for obj in self.browse(cr, uid, ids, context=context):
869
# if rfq_ok is specified as default value for new object, we base our decision on this value
873
result.update(name=self.pool.get('ir.sequence').get(cr, uid, 'rfq'))
876
def hook_rfq_sent_check_lines(self, cr, uid, ids, context=None):
878
Please copy this to your module's method also.
879
This hook belongs to the rfq_sent method from tender_flow>tender_flow.py
880
- check lines after import
886
def rfq_sent(self, cr, uid, ids, context=None):
887
self.hook_rfq_sent_check_lines(cr, uid, ids, context=context)
888
for rfq in self.browse(cr, uid, ids, context=context):
889
wf_service = netsvc.LocalService("workflow")
890
wf_service.trg_validate(uid, 'purchase.order', rfq.id, 'rfq_sent', cr)
892
self.write(cr, uid, ids, {'date_confirm': time.strftime('%Y-%m-%d')}, context=context)
896
return {'type': 'ir.actions.report.xml',
897
'report_name': 'msf.purchase.quotation',
900
def check_rfq_updated(self, cr, uid, ids, context=None):
901
if isinstance(ids, (int, long)):
904
wf_service = netsvc.LocalService("workflow")
905
for rfq in self.browse(cr, uid, ids, context=context):
906
if not rfq.valid_till:
907
raise osv.except_osv(_('Error'), _('You must specify a Valid Till date.'))
909
wf_service.trg_validate(uid, 'purchase.order', rfq.id, 'rfq_updated', cr)
912
'type': 'ir.actions.act_window',
913
'res_model': 'purchase.order',
914
'view_mode': 'form,tree,graph,calendar',
917
'context': {'rfq_ok': True, 'search_default_draft_rfq': 1},
918
'domain': [('rfq_ok', '=', True)],
922
def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
928
# the search view depends on the type we want to display
929
if view_type == 'search':
930
if context.get('rfq_ok', False):
932
view = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'tender_flow', 'view_rfq_filter')
935
if view_type == 'tree':
936
# the view depends on po type
937
if context.get('rfq_ok', False):
939
view = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'tender_flow', 'view_rfq_tree')
944
result = super(purchase_order, self).fields_view_get(cr, uid, view_id, view_type, context=context, toolbar=toolbar, submenu=submenu)
945
if view_type == 'form':
946
if context.get('rfq_ok', False):
947
# the title of the screen depends on po type
948
form = etree.fromstring(result['arch'])
950
fields = form.xpath('//form[@string="%s"]' % _('Purchase Order'))
952
field.set('string', _("Request for Quotation"))
954
fields2 = form.xpath('//page[@string="%s"]' % _('Purchase Order'))
955
for field2 in fields2:
956
field2.set('string', _("Request for Quotation"))
958
result['arch'] = etree.tostring(form)
965
class purchase_order_line(osv.osv):
967
add a tender_id related field
969
_inherit = 'purchase.order.line'
970
_columns = {'tender_id': fields.related('order_id', 'tender_id', type='many2one', relation='tender', string='Tender',),
971
'rfq_ok': fields.related('order_id', 'rfq_ok', type='boolean', string='RfQ ?'),
974
def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
982
result = super(purchase_order_line, self).fields_view_get(cr, uid, view_id, view_type, context=context, toolbar=toolbar, submenu=submenu)
983
if view_type == 'form':
984
if context.get('rfq_ok', False):
985
# the title of the screen depends on po type
986
form = etree.fromstring(result['arch'])
987
fields = form.xpath('//form[@string="%s"]' % _('Purchase Order Line'))
989
field.set('string', _("Request for Quotation Line"))
990
result['arch'] = etree.tostring(form)
994
purchase_order_line()
997
class sale_order_line(osv.osv):
999
add link one2many to tender.line
1001
_inherit = 'sale.order.line'
1003
_columns = {'tender_line_ids': fields.one2many('tender.line', 'sale_order_line_id', string="Tender Lines", readonly=True),}
1008
class pricelist_partnerinfo(osv.osv):
1010
add new information from specifications
1012
def _get_line_number(self, cr, uid, ids, field_name, args, context=None):
1014
for price in self.browse(cr, uid, ids, context=context):
1016
if price.purchase_order_line_id:
1017
res[price.id] = price.purchase_order_line_id.line_number
1021
_inherit = 'pricelist.partnerinfo'
1022
_columns = {'price': fields.float('Unit Price', required=True, digits_compute=dp.get_precision('Purchase Price Computation'), help="This price will be considered as a price for the supplier UoM if any or the default Unit of Measure of the product otherwise"),
1023
'currency_id': fields.many2one('res.currency', string='Currency', required=True, domain="[('partner_currency', '=', partner_id)]"),
1024
'valid_till': fields.date(string="Valid Till",),
1025
'comment': fields.char(size=128, string='Comment'),
1026
'purchase_order_id': fields.related('purchase_order_line_id', 'order_id', type='many2one', relation='purchase.order', string="Related RfQ", readonly=True,),
1027
'purchase_order_line_id': fields.many2one('purchase.order.line', string="RfQ Line Ref",),
1028
#'purchase_order_line_number': fields.related('purchase_order_line_id', 'line_number', type="integer", string="Related Line Number", ),
1029
'purchase_order_line_number': fields.function(_get_line_number, method=True, type="integer", string="Related Line Number", readonly=True),
1031
pricelist_partnerinfo()
1033
class ir_values(osv.osv):
1035
_inherit = 'ir.values'
1037
def get(self, cr, uid, key, key2, models, meta=False, context=None, res_id_req=False, without_user=True, key2_req=True):
1040
values = super(ir_values, self).get(cr, uid, key, key2, models, meta, context, res_id_req, without_user, key2_req)
1043
po_accepted_values = {'client_action_multi': ['Order Follow Up',
1044
'action_view_purchase_order_group'],
1045
'client_print_multi': ['Purchase Order (Merged)',
1047
'Allocation report',
1048
'Order impact vs. Budget'],
1049
'client_action_relate': ['ir_open_product_list_export_view',
1050
'View_log_purchase.order',
1051
'Allocation report'],
1052
'tree_but_action': [],
1053
'tree_but_open': []}
1055
rfq_accepted_values = {'client_action_multi': [],
1056
'client_print_multi': ['Request for Quotation'],
1057
'client_action_relate': [],
1058
'tree_but_action': [],
1059
'tree_but_open': []}
1060
if context.get('purchase_order', False) and 'purchase.order' in [x[0] for x in models]:
1063
if key == 'action' and v[1] in po_accepted_values[key2] \
1064
or v[1] == 'Purchase Order Excel Export' \
1065
or v[1] == 'Purchase Order' \
1066
or v[1] == 'Purchase Order (Merged)' \
1067
or v[1] == 'Allocation report' \
1068
or v[1] == 'Order impact vs. Budget' :
1069
new_values.append(v)
1070
elif context.get('request_for_quotation', False) and 'purchase.order' in [x[0] for x in models]:
1073
if key == 'action' and v[1] in rfq_accepted_values[key2] \
1074
or v[1] == 'Request for Quotation' \
1075
or v[1] == 'Request For Quotation Excel Export' :
1076
new_values.append(v)