1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from osv import fields
28 from datetime import datetime
29 from dateutil.relativedelta import relativedelta
31 from tools import config
32 from tools.translate import _
34 import decimal_precision as dp
35 from osv.orm import browse_record, browse_null
40 class purchase_order(osv.osv):
41 def _calc_amount(self, cr, uid, ids, prop, unknow_none, unknow_dict):
43 for order in self.browse(cr, uid, ids):
45 for oline in order.order_line:
46 res[order.id] += oline.price_unit * oline.product_qty
49 def _amount_all(self, cr, uid, ids, field_name, arg, context):
51 cur_obj=self.pool.get('res.currency')
52 for order in self.browse(cr, uid, ids):
54 'amount_untaxed': 0.0,
59 cur=order.pricelist_id.currency_id
60 for line in order.order_line:
61 for c in self.pool.get('account.tax').compute(cr, uid, line.taxes_id, line.price_unit, line.product_qty, order.partner_address_id.id, line.product_id, order.partner_id):
63 val1 += line.price_subtotal
64 res[order.id]['amount_tax']=cur_obj.round(cr, uid, cur, val)
65 res[order.id]['amount_untaxed']=cur_obj.round(cr, uid, cur, val1)
66 res[order.id]['amount_total']=res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
69 def _set_minimum_planned_date(self, cr, uid, ids, name, value, arg, context):
70 if not value: return False
71 if type(ids)!=type([]):
73 for po in self.browse(cr, uid, ids, context):
74 cr.execute("""update purchase_order_line set
78 (date_planned=%s or date_planned<%s)""", (value,po.id,po.minimum_planned_date,value))
81 def _minimum_planned_date(self, cr, uid, ids, field_name, arg, context):
83 purchase_obj=self.browse(cr, uid, ids, context=context)
84 for purchase in purchase_obj:
85 res[purchase.id] = False
86 if purchase.order_line:
87 min_date=purchase.order_line[0].date_planned
88 for line in purchase.order_line:
89 if line.date_planned < min_date:
90 min_date=line.date_planned
91 res[purchase.id]=min_date
94 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
96 for purchase in self.browse(cursor, user, ids, context=context):
98 if purchase.invoice_id and purchase.invoice_id.state not in ('draft','cancel'):
99 tot += purchase.invoice_id.amount_untaxed
100 if purchase.amount_untaxed:
101 res[purchase.id] = tot * 100.0 / purchase.amount_untaxed
103 res[purchase.id] = 0.0
106 def _shipped_rate(self, cr, uid, ids, name, arg, context=None):
107 if not ids: return {}
112 p.purchase_id,sum(m.product_qty), m.state
116 stock_picking p on (p.id=m.picking_id)
118 p.purchase_id = ANY(%s) GROUP BY m.state, p.purchase_id''',(ids,))
119 for oid,nbr,state in cr.fetchall():
123 res[oid][0] += nbr or 0.0
124 res[oid][1] += nbr or 0.0
126 res[oid][1] += nbr or 0.0
131 res[r] = 100.0 * res[r][0] / res[r][1]
134 def _get_order(self, cr, uid, ids, context={}):
136 for line in self.pool.get('purchase.order.line').browse(cr, uid, ids, context=context):
137 result[line.order_id.id] = True
140 def _invoiced(self, cursor, user, ids, name, arg, context=None):
142 for purchase in self.browse(cursor, user, ids, context=context):
143 if purchase.invoice_id.reconciled:
144 res[purchase.id] = purchase.invoice_id.reconciled
146 res[purchase.id] = False
150 'name': fields.char('Order Reference', size=64, required=True, select=True),
151 'origin': fields.char('Source Document', size=64,
152 help="Reference of the document that generated this purchase order request."
154 'partner_ref': fields.char('Supplier Reference', size=64),
155 'date_order':fields.date('Date Ordered', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}, help="Date on which this document has been created."),
156 'date_approve':fields.date('Date Approved', readonly=1),
157 'partner_id':fields.many2one('res.partner', 'Supplier', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, change_default=True),
158 'partner_address_id':fields.many2one('res.partner.address', 'Address', required=True, states={'posted':[('readonly',True)]}),
159 'dest_address_id':fields.many2one('res.partner.address', 'Destination Address', states={'posted':[('readonly',True)]},
160 help="Put an address if you want to deliver directly from the supplier to the customer." \
161 "In this case, it will remove the warehouse link and set the customer location."
163 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', states={'posted':[('readonly',True)]}),
164 'location_id': fields.many2one('stock.location', 'Destination', required=True),
165 'pricelist_id':fields.many2one('product.pricelist', 'Pricelist', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, help="The pricelist sets the currency used for this purchase order. It also computes the supplier price for the selected products/quantities."),
166 'state': fields.selection([('draft', 'Request for Quotation'), ('wait', 'Waiting'), ('confirmed', 'Waiting Supplier Ack'), ('approved', 'Approved'),('except_picking', 'Shipping Exception'), ('except_invoice', 'Invoice Exception'), ('done', 'Done'), ('cancel', 'Cancelled')], '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 'Done'. If a cancel action occurs in the invoice or in the reception of goods, the state becomes in exception.", select=True),
167 'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order Lines', states={'approved':[('readonly',True)],'done':[('readonly',True)]}),
168 'validator' : fields.many2one('res.users', 'Validated by', readonly=True),
169 'notes': fields.text('Notes', translate=True),
170 'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True),
171 'picking_ids': fields.one2many('stock.picking', 'purchase_id', 'Picking List', readonly=True, help="This is the list of picking list that have been generated for this purchase"),
172 'shipped':fields.boolean('Received', readonly=True, select=True),
173 'shipped_rate': fields.function(_shipped_rate, method=True, string='Received', type='float'),
174 'invoiced': fields.function(_invoiced, method=True, string='Invoiced & Paid', type='boolean'),
175 'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
176 'invoice_method': fields.selection([('manual','Manual'),('order','From Order'),('picking','From Picking')], 'Invoicing Control', required=True,
177 help="From Order: a draft invoice will be pre-generated based on the purchase order. The accountant " \
178 "will just have to validate this invoice for control.\n" \
179 "From Picking: a draft invoice will be pre-generated based on validated receptions.\n" \
180 "Manual: no invoice will be pre-generated. The accountant will have to encode manually."
182 'minimum_planned_date':fields.function(_minimum_planned_date, fnct_inv=_set_minimum_planned_date, method=True,store=True, string='Expected Date', type='datetime', help="This is computed as the minimum scheduled date of all purchase order lines' products."),
183 'amount_untaxed': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Purchase Price'), string='Untaxed Amount',
185 'purchase.order.line': (_get_order, None, 10),
187 'amount_tax': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Purchase Price'), string='Taxes',
189 'purchase.order.line': (_get_order, None, 10),
191 'amount_total': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Purchase Price'), string='Total',
193 'purchase.order.line': (_get_order, None, 10),
195 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
196 'product_id': fields.related('order_line','product_id', type='many2one', relation='product.product', string='Product'),
197 'create_uid': fields.many2one('res.users', 'Responsible'),
198 'company_id': fields.many2one('res.company','Company',required=True,select=1),
201 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
202 'state': lambda *a: 'draft',
203 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
204 'shipped': lambda *a: 0,
205 'invoice_method': lambda *a: 'order',
206 'invoiced': lambda *a: 0,
207 'partner_address_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['default'])['default'],
208 'pricelist_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').browse(cr, uid, context['partner_id']).property_product_pricelist_purchase.id,
209 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.order', context=c),
211 _name = "purchase.order"
212 _description = "Purchase order"
215 def unlink(self, cr, uid, ids, context=None):
216 purchase_orders = self.read(cr, uid, ids, ['state'])
218 for s in purchase_orders:
219 if s['state'] in ['draft','cancel']:
220 unlink_ids.append(s['id'])
222 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Purchase Order(s) which are in %s State!' % s['state']))
224 # TODO: temporary fix in 5.0, to remove in 5.2 when subflows support
225 # automatically sending subflow.delete upon deletion
226 wf_service = netsvc.LocalService("workflow")
227 for id in unlink_ids:
228 wf_service.trg_validate(uid, 'purchase.order', id, 'purchase_cancel', cr)
230 return super(purchase_order, self).unlink(cr, uid, unlink_ids, context=context)
232 def button_dummy(self, cr, uid, ids, context={}):
235 def onchange_dest_address_id(self, cr, uid, ids, adr_id):
238 part_id = self.pool.get('res.partner.address').read(cr, uid, [adr_id], ['partner_id'])[0]['partner_id'][0]
239 loc_id = self.pool.get('res.partner').browse(cr, uid, part_id).property_stock_customer.id
240 return {'value':{'location_id': loc_id, 'warehouse_id': False}}
242 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id):
245 res = self.pool.get('stock.warehouse').read(cr, uid, [warehouse_id], ['lot_input_id'])[0]['lot_input_id'][0]
246 return {'value':{'location_id': res, 'dest_address_id': False}}
248 def onchange_partner_id(self, cr, uid, ids, part):
250 return {'value':{'partner_address_id': False, 'fiscal_position': False}}
251 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['default'])
252 part = self.pool.get('res.partner').browse(cr, uid, part)
253 pricelist = part.property_product_pricelist_purchase.id
254 fiscal_position = part.property_account_position and part.property_account_position.id or False
255 return {'value':{'partner_address_id': addr['default'], 'pricelist_id': pricelist, 'fiscal_position': fiscal_position}}
257 def wkf_approve_order(self, cr, uid, ids, context={}):
258 self.write(cr, uid, ids, {'state': 'approved', 'date_approve': time.strftime('%Y-%m-%d')})
261 def wkf_confirm_order(self, cr, uid, ids, context={}):
262 for po in self.browse(cr, uid, ids):
263 if self.pool.get('res.partner.event.type').check(cr, uid, 'purchase_open'):
264 self.pool.get('res.partner.event').create(cr, uid, {'name':'Purchase Order: '+po.name, 'partner_id':po.partner_id.id, 'date':time.strftime('%Y-%m-%d %H:%M:%S'), 'user_id':uid, 'partner_type':'retailer', 'probability': 1.0, 'planned_cost':po.amount_untaxed})
265 if not po.order_line:
266 raise osv.except_osv(_('Error !'),_('You can not confirm purchase order without Purchase Order Lines.'))
267 current_name = self.name_get(cr, uid, ids)[0][1]
269 self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
272 def wkf_warn_buyer(self, cr, uid, ids):
273 self.write(cr, uid, ids, {'state' : 'wait', 'validator' : uid})
274 request = pooler.get_pool(cr.dbname).get('res.request')
275 for po in self.browse(cr, uid, ids):
277 for oline in po.order_line:
278 manager = oline.product_id.product_manager
279 if manager and not (manager.id in managers):
280 managers.append(manager.id)
281 for manager_id in managers:
282 request.create(cr, uid,
283 {'name' : "Purchase amount over the limit",
285 'act_to' : manager_id,
286 'body': 'Somebody has just confirmed a purchase with an amount over the defined limit',
287 'ref_partner_id': po.partner_id.id,
288 'ref_doc1': 'purchase.order,%d' % (po.id,),
290 def inv_line_create(self, cr, uid, a, ol):
294 'price_unit': ol.price_unit or 0.0,
295 'quantity': ol.product_qty,
296 'product_id': ol.product_id.id or False,
297 'uos_id': ol.product_uom.id or False,
298 'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])],
299 'account_analytic_id': ol.account_analytic_id.id,
302 def action_cancel_draft(self, cr, uid, ids, *args):
305 self.write(cr, uid, ids, {'state':'draft','shipped':0})
306 wf_service = netsvc.LocalService("workflow")
308 # Deleting the existing instance of workflow for PO
309 wf_service.trg_delete(uid, 'purchase.order', p_id, cr)
310 wf_service.trg_create(uid, 'purchase.order', p_id, cr)
313 def action_invoice_create(self, cr, uid, ids, *args):
315 journal_obj = self.pool.get('account.journal')
316 for o in self.browse(cr, uid, ids):
318 for ol in o.order_line:
321 a = ol.product_id.product_tmpl_id.property_account_expense.id
323 a = ol.product_id.categ_id.property_account_expense_categ.id
325 raise osv.except_osv(_('Error !'), _('There is no expense account defined for this product: "%s" (id:%d)') % (ol.product_id.name, ol.product_id.id,))
327 a = self.pool.get('ir.property').get(cr, uid, 'property_account_expense_categ', 'product.category')
328 fpos = o.fiscal_position or False
329 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
330 il.append(self.inv_line_create(cr, uid, a, ol))
332 a = o.partner_id.property_account_payable.id
333 journal_ids = journal_obj.search(cr, uid, [('type', '=','purchase'),('company_id', '=', o.company_id.id)], limit=1)
335 raise osv.except_osv(_('Error !'),
336 _('There is no purchase journal defined for this company: "%s" (id:%d)') % (o.company_id.name, o.company_id.id))
338 'name': o.partner_ref or o.name,
339 'reference': "P%dPO%d" % (o.partner_id.id, o.id),
341 'type': 'in_invoice',
342 'partner_id': o.partner_id.id,
343 'currency_id': o.pricelist_id.currency_id.id,
344 'address_invoice_id': o.partner_address_id.id,
345 'address_contact_id': o.partner_address_id.id,
346 'journal_id': len(journal_ids) and journal_ids[0] or False,
349 'fiscal_position': o.partner_id.property_account_position.id,
350 'payment_term': o.partner_id.property_payment_term and o.partner_id.property_payment_term.id or False,
351 'company_id': o.company_id.id,
353 inv_id = self.pool.get('account.invoice').create(cr, uid, inv, {'type':'in_invoice'})
354 self.pool.get('account.invoice').button_compute(cr, uid, [inv_id], {'type':'in_invoice'}, set_total=True)
356 self.write(cr, uid, [o.id], {'invoice_id': inv_id})
360 def has_stockable_product(self,cr, uid, ids, *args):
361 for order in self.browse(cr, uid, ids):
362 for order_line in order.order_line:
363 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
367 def action_cancel(self, cr, uid, ids, context={}):
369 purchase_order_line_obj = self.pool.get('purchase.order.line')
370 for purchase in self.browse(cr, uid, ids):
371 for pick in purchase.picking_ids:
372 if pick.state not in ('draft','cancel'):
373 raise osv.except_osv(
374 _('Could not cancel purchase order !'),
375 _('You must first cancel all picking attached to this purchase order.'))
376 for pick in purchase.picking_ids:
377 wf_service = netsvc.LocalService("workflow")
378 wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_cancel', cr)
379 inv = purchase.invoice_id
380 if inv and inv.state not in ('cancel','draft'):
381 raise osv.except_osv(
382 _('Could not cancel this purchase order !'),
383 _('You must first cancel all invoices attached to this purchase order.'))
385 wf_service = netsvc.LocalService("workflow")
386 wf_service.trg_validate(uid, 'account.invoice', inv.id, 'invoice_cancel', cr)
387 self.write(cr,uid,ids,{'state':'cancel'})
390 def action_picking_create(self,cr, uid, ids, *args):
392 for order in self.browse(cr, uid, ids):
393 loc_id = order.partner_id.property_stock_supplier.id
395 if order.invoice_method=='picking':
396 istate = '2binvoiced'
397 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.in')
398 picking_id = self.pool.get('stock.picking').create(cr, uid, {
400 'origin': order.name+((order.origin and (':'+order.origin)) or ''),
402 'address_id': order.dest_address_id.id or order.partner_address_id.id,
403 'invoice_state': istate,
404 'purchase_id': order.id,
405 'company_id': order.company_id.id,
407 for order_line in order.order_line:
408 if not order_line.product_id:
410 if order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
411 dest = order.location_id.id
412 move = self.pool.get('stock.move').create(cr, uid, {
413 'name': 'PO:'+order_line.name,
414 'product_id': order_line.product_id.id,
415 'product_qty': order_line.product_qty,
416 'product_uos_qty': order_line.product_qty,
417 'product_uom': order_line.product_uom.id,
418 'product_uos': order_line.product_uom.id,
419 'date_planned': order_line.date_planned,
420 'location_id': loc_id,
421 'location_dest_id': dest,
422 'picking_id': picking_id,
423 'move_dest_id': order_line.move_dest_id.id,
425 'purchase_line_id': order_line.id,
426 'company_id': order.company_id.id,
428 if order_line.move_dest_id:
429 self.pool.get('stock.move').write(cr, uid, [order_line.move_dest_id.id], {'location_id':order.location_id.id})
430 self.pool.get('stock.move').action_confirm(cr, uid, [move])
431 self.pool.get('stock.move').force_assign(cr,uid, [move])
432 wf_service = netsvc.LocalService("workflow")
433 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
436 def copy(self, cr, uid, id, default=None,context={}):
445 'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
447 return super(purchase_order, self).copy(cr, uid, id, default, context)
450 def do_merge(self, cr, uid, ids, context):
452 To merge similar type of purchase orders.
453 Orders will only be merged if:
454 * Purchase Orders are in draft
455 * Purchase Orders belong to the same partner
456 * Purchase Orders are have same stock location, same pricelist
457 Lines will only be merged if:
458 * Order lines are exactly the same except for the quantity and unit
460 @param self: The object pointer.
461 @param cr: A database cursor
462 @param uid: ID of the user currently logged in
463 @param ids: the ID or list of IDs
464 @param context: A standard dictionary
466 @return: new purchase order id
469 wf_service = netsvc.LocalService("workflow")
470 def make_key(br, fields):
473 field_val = getattr(br, field)
474 if field in ('product_id', 'move_dest_id', 'account_analytic_id'):
477 if isinstance(field_val, browse_record):
478 field_val = field_val.id
479 elif isinstance(field_val, browse_null):
481 elif isinstance(field_val, list):
482 field_val = ((6, 0, tuple([v.id for v in field_val])),)
483 list_key.append((field, field_val))
485 return tuple(list_key)
487 # compute what the new orders should contain
491 for porder in [order for order in self.browse(cr, uid, ids) if order.state == 'draft']:
492 order_key = make_key(porder, ('partner_id', 'location_id', 'pricelist_id'))
493 new_order = new_orders.setdefault(order_key, ({}, []))
494 new_order[1].append(porder.id)
495 order_infos = new_order[0]
498 'origin': porder.origin,
499 'date_order': time.strftime('%Y-%m-%d'),
500 'partner_id': porder.partner_id.id,
501 'partner_address_id': porder.partner_address_id.id,
502 'dest_address_id': porder.dest_address_id.id,
503 'warehouse_id': porder.warehouse_id.id,
504 'location_id': porder.location_id.id,
505 'pricelist_id': porder.pricelist_id.id,
508 'notes': '%s' % (porder.notes or '',),
511 #order_infos['name'] += ', %s' % porder.name
513 order_infos['notes'] = (order_infos['notes'] or '') + ('\n%s' % (porder.notes,))
515 order_infos['origin'] = (order_infos['origin'] or '') + ' ' + porder.origin
517 for order_line in porder.order_line:
518 line_key = make_key(order_line, ('name', 'date_planned', 'taxes_id', 'price_unit', 'notes', 'product_id', 'move_dest_id', 'account_analytic_id'))
519 o_line = order_infos['order_line'].setdefault(line_key, {})
521 # merge the line with an existing line
522 o_line['product_qty'] += order_line.product_qty * order_line.product_uom.factor / o_line['uom_factor']
524 # append a new "standalone" line
525 for field in ('product_qty', 'product_uom'):
526 field_val = getattr(order_line, field)
527 if isinstance(field_val, browse_record):
528 field_val = field_val.id
529 o_line[field] = field_val
530 o_line['uom_factor'] = order_line.product_uom and order_line.product_uom.factor or 1.0
535 for order_key, (order_data, old_ids) in new_orders.iteritems():
536 # skip merges with only one order
538 allorders += (old_ids or [])
541 # cleanup order line data
542 for key, value in order_data['order_line'].iteritems():
543 del value['uom_factor']
544 value.update(dict(key))
545 order_data['order_line'] = [(0, 0, value) for value in order_data['order_line'].itervalues()]
547 # create the new order
548 neworder_id = self.create(cr, uid, order_data)
549 allorders.append(neworder_id)
551 # make triggers pointing to the old orders point to the new order
552 for old_id in old_ids:
553 wf_service.trg_redirect(uid, 'purchase.order', old_id, neworder_id, cr)
554 wf_service.trg_validate(uid, 'purchase.order', old_id, 'purchase_cancel', cr)
559 class purchase_order_line(osv.osv):
560 def _amount_line(self, cr, uid, ids, prop, unknow_none,unknow_dict):
562 cur_obj=self.pool.get('res.currency')
563 for line in self.browse(cr, uid, ids):
564 cur = line.order_id.pricelist_id.currency_id
565 res[line.id] = cur_obj.round(cr, uid, cur, line.price_unit * line.product_qty)
569 'name': fields.char('Description', size=256, required=True),
570 'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
571 'date_planned': fields.datetime('Scheduled date', required=True),
572 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
573 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
574 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
575 'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
576 'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
577 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Purchase Price')),
578 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal', digits_compute= dp.get_precision('Purchase Price')),
579 'notes': fields.text('Notes', translate=True),
580 'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
581 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
582 'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company')
585 'product_qty': lambda *a: 1.0
587 _table = 'purchase_order_line'
588 _name = 'purchase.order.line'
589 _description = 'Purchase Order lines'
590 def copy_data(self, cr, uid, id, default=None,context={}):
593 default.update({'state':'draft', 'move_ids':[]})
594 return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
596 def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
597 partner_id, date_order=False, fiscal_position=False, date_planned=False,
598 name=False, price_unit=False, notes=False):
600 raise osv.except_osv(_('No Pricelist !'), _('You have to select a pricelist in the purchase form !\nPlease set one before choosing a product.'))
602 raise osv.except_osv(_('No Partner!'), _('You have to select a partner in the purchase form !\nPlease set one partner before choosing a product.'))
604 return {'value': {'price_unit': price_unit or 0.0, 'name': name or '',
605 'notes': notes or'', 'product_uom' : uom or False}, 'domain':{'product_uom':[]}}
606 prod= self.pool.get('product.product').browse(cr, uid,product)
609 lang=self.pool.get('res.partner').read(cr, uid, partner_id)['lang']
610 context={'lang':lang}
611 context['partner_id'] = partner_id
613 prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
614 prod_uom_po = prod.uom_po_id.id
618 date_order = time.strftime('%Y-%m-%d')
621 for s in prod.seller_ids:
622 if s.name.id == partner_id:
623 seller_delay = s.delay
624 temp_qty = s.qty # supplier _qty assigned to temp
625 if qty < temp_qty: # If the supplier quantity is greater than entered from user, set minimal.
630 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
631 product, qty or 1.0, partner_id, {
635 dt = (datetime.now() + relativedelta(days=int(seller_delay) or 0.0)).strftime('%Y-%m-%d %H:%M:%S')
636 prod_name = self.pool.get('product.product').name_get(cr, uid, [prod.id])[0][1]
639 res = {'value': {'price_unit': price, 'name': name or prod_name,
640 'taxes_id':map(lambda x: x.id, prod.supplier_taxes_id),
641 'date_planned': date_planned or dt,'notes': notes or prod.description_purchase,
646 partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
647 taxes = self.pool.get('account.tax').browse(cr, uid,map(lambda x: x.id, prod.supplier_taxes_id))
648 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
649 res['value']['taxes_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
651 res2 = self.pool.get('product.uom').read(cr, uid, [uom], ['category_id'])
652 res3 = prod.uom_id.category_id.id
653 domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
654 if res2[0]['category_id'][0] != res3:
655 raise osv.except_osv(_('Wrong Product UOM !'), _('You have to select a product UOM in the same category than the purchase UOM of the product'))
657 res['domain'] = domain
660 def product_uom_change(self, cr, uid, ids, pricelist, product, qty, uom,
661 partner_id, date_order=False):
662 res = self.product_id_change(cr, uid, ids, pricelist, product, qty, uom,
663 partner_id, date_order=date_order)
664 if 'product_uom' in res['value']:
665 del res['value']['product_uom']
667 res['value']['price_unit'] = 0.0
669 purchase_order_line()
671 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: