1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # Copyright (c) 2004-2008 TINY SPRL. (http://tiny.be) All Rights Reserved.
8 # WARNING: This program as such is intended to be used by professional
9 # programmers who take the whole responsability of assessing all potential
10 # consequences resulting from its eventual inadequacies and bugs
11 # End users who are looking for a ready-to-use solution with commercial
12 # garantees and support are strongly adviced to contract a Free Software
15 # This program is Free Software; you can redistribute it and/or
16 # modify it under the terms of the GNU General Public License
17 # as published by the Free Software Foundation; either version 2
18 # of the License, or (at your option) any later version.
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
25 # You should have received a copy of the GNU General Public License
26 # along with this program; if not, write to the Free Software
27 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
29 ##############################################################################
31 from osv import fields
37 from mx import DateTime
39 from tools import config
40 from tools.translate import _
45 class purchase_order(osv.osv):
46 def _calc_amount(self, cr, uid, ids, prop, unknow_none, unknow_dict):
48 for order in self.browse(cr, uid, ids):
50 for oline in order.order_line:
51 res[order.id] += oline.price_unit * oline.product_qty
54 def _amount_untaxed(self, cr, uid, ids, field_name, arg, context):
56 cur_obj=self.pool.get('res.currency')
57 for purchase in self.browse(cr, uid, ids):
58 res[purchase.id] = 0.0
59 for line in purchase.order_line:
60 res[purchase.id] += line.price_subtotal
61 cur = purchase.pricelist_id.currency_id
62 res[purchase.id] = cur_obj.round(cr, uid, cur, res[purchase.id])
66 def _amount_tax(self, cr, uid, ids, field_name, arg, context):
68 cur_obj=self.pool.get('res.currency')
69 for order in self.browse(cr, uid, ids):
71 cur=order.pricelist_id.currency_id
72 for line in order.order_line:
73 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):
74 val+= cur_obj.round(cr, uid, cur, c['amount'])
75 res[order.id]=cur_obj.round(cr, uid, cur, val)
78 def _amount_total(self, cr, uid, ids, field_name, arg, context):
80 untax = self._amount_untaxed(cr, uid, ids, field_name, arg, context)
81 tax = self._amount_tax(cr, uid, ids, field_name, arg, context)
82 cur_obj=self.pool.get('res.currency')
84 order=self.browse(cr, uid, [id])[0]
85 cur=order.pricelist_id.currency_id
86 res[id] = cur_obj.round(cr, uid, cur, untax.get(id, 0.0) + tax.get(id, 0.0))
90 'name': fields.char('Order Reference', size=64, required=True, select=True),
91 'origin': fields.char('Origin', size=64),
92 'partner_ref': fields.char('Partner Ref.', size=64),
93 'date_order':fields.date('Date Ordered', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}),
94 'date_approve':fields.date('Date Approved'),
95 'partner_id':fields.many2one('res.partner', 'Supplier', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}, change_default=True),
96 'partner_address_id':fields.many2one('res.partner.address', 'Address', required=True, states={'posted':[('readonly',True)]}),
98 'dest_address_id':fields.many2one('res.partner.address', 'Destination Address', states={'posted':[('readonly',True)]}),
99 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', states={'posted':[('readonly',True)]}),
100 'location_id': fields.many2one('stock.location', 'Delivery destination', required=True),
102 'pricelist_id':fields.many2one('product.pricelist', 'Pricelist', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}),
104 'state': fields.selection([('draft', 'Request for Quotation'), ('wait', 'Waiting'), ('confirmed', 'Confirmed'), ('approved', 'Approved'),('except_picking', 'Shipping Exception'), ('except_invoice', 'Invoice Exception'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Order 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),
105 'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order State', states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}),
106 'validator' : fields.many2one('res.users', 'Validated by', readonly=True),
107 'notes': fields.text('Notes'),
108 'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True),
109 '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"),
110 'shipped':fields.boolean('Received', readonly=True, select=True),
111 'invoiced':fields.boolean('Invoiced & Paid', readonly=True, select=True),
112 'invoice_method': fields.selection([('manual','Manual'),('order','From order'),('picking','From picking')], 'Invoicing Control', required=True),
114 'amount_untaxed': fields.function(_amount_untaxed, method=True, string='Untaxed Amount'),
115 'amount_tax': fields.function(_amount_tax, method=True, string='Taxes'),
116 'amount_total': fields.function(_amount_total, method=True, string='Total'),
119 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
120 'state': lambda *a: 'draft',
121 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
122 'shipped': lambda *a: 0,
123 'invoice_method': lambda *a: 'order',
124 'invoiced': lambda *a: 0,
125 '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'],
126 '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,
128 _name = "purchase.order"
129 _description = "Purchase order"
132 def button_dummy(self, cr, uid, ids, context={}):
135 def onchange_dest_address_id(self, cr, uid, ids, adr_id):
138 part_id = self.pool.get('res.partner.address').read(cr, uid, [adr_id], ['partner_id'])[0]['partner_id'][0]
139 loc_id = self.pool.get('res.partner').browse(cr, uid, part_id).property_stock_customer.id
140 return {'value':{'location_id': loc_id, 'warehouse_id': False}}
142 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id):
145 res = self.pool.get('stock.warehouse').read(cr, uid, [warehouse_id], ['lot_input_id'])[0]['lot_input_id'][0]
146 return {'value':{'location_id': res, 'dest_address_id': False}}
148 def onchange_partner_id(self, cr, uid, ids, part):
150 return {'value':{'partner_address_id': False}}
151 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['default'])
152 pricelist = self.pool.get('res.partner').browse(cr, uid, part).property_product_pricelist_purchase.id
153 return {'value':{'partner_address_id': addr['default'], 'pricelist_id': pricelist}}
155 def wkf_approve_order(self, cr, uid, ids):
156 self.write(cr, uid, ids, {'state': 'approved', 'date_approve': time.strftime('%Y-%m-%d')})
159 def wkf_confirm_order(self, cr, uid, ids, context={}):
160 for po in self.browse(cr, uid, ids):
161 if self.pool.get('res.partner.event.type').check(cr, uid, 'purchase_open'):
162 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})
163 current_name = self.name_get(cr, uid, ids)[0][1]
165 self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
168 def wkf_warn_buyer(self, cr, uid, ids):
169 self.write(cr, uid, ids, {'state' : 'wait', 'validator' : uid})
170 request = pooler.get_pool(cr.dbname).get('res.request')
171 for po in self.browse(cr, uid, ids):
173 for oline in po.order_line:
174 manager = oline.product_id.product_manager
175 if manager and not (manager.id in managers):
176 managers.append(manager.id)
177 for manager_id in managers:
178 request.create(cr, uid,
179 {'name' : "Purchase amount over the limit",
181 'act_to' : manager_id,
182 'body': 'Somebody has just confirmed a purchase with an amount over the defined limit',
183 'ref_partner_id': po.partner_id.id,
184 'ref_doc1': 'purchase.order,%d' % (po.id,),
186 def inv_line_create(self,a,ol):
190 'price_unit': ol.price_unit or 0.0,
191 'quantity': ol.product_qty,
192 'product_id': ol.product_id.id or False,
193 'uos_id': ol.product_uom.id or False,
194 'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])],
195 'account_analytic_id': ol.account_analytic_id.id,
198 def action_invoice_create(self, cr, uid, ids, *args):
200 for o in self.browse(cr, uid, ids):
202 for ol in o.order_line:
205 a = ol.product_id.product_tmpl_id.property_account_expense.id
207 a = ol.product_id.categ_id.property_account_expense_categ.id
209 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,))
211 a = self.pool.get('ir.property').get(cr, uid, 'property_account_expense_categ', 'product.category')
212 il.append(self.inv_line_create(a,ol))
213 # il.append((0, False, {
216 # 'price_unit': ol.price_unit or 0.0,
217 # 'quantity': ol.product_qty,
218 # 'product_id': ol.product_id.id or False,
219 # 'uos_id': ol.product_uom.id or False,
220 # 'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])],
221 # 'account_analytic_id': ol.account_analytic_id.id,
224 a = o.partner_id.property_account_payable.id
226 'name': o.partner_ref or o.name,
227 'reference': "P%dPO%d" % (o.partner_id.id, o.id),
229 'type': 'in_invoice',
230 'partner_id': o.partner_id.id,
231 'currency_id': o.pricelist_id.currency_id.id,
232 'address_invoice_id': o.partner_address_id.id,
233 'address_contact_id': o.partner_address_id.id,
237 inv_id = self.pool.get('account.invoice').create(cr, uid, inv, {'type':'in_invoice'})
238 self.pool.get('account.invoice').button_compute(cr, uid, [inv_id], {'type':'in_invoice'}, set_total=True)
240 self.write(cr, uid, [o.id], {'invoice_id': inv_id})
244 def has_stockable_product(self,cr, uid, ids, *args):
245 for order in self.browse(cr, uid, ids):
246 for order_line in order.order_line:
247 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
251 def action_picking_create(self,cr, uid, ids, *args):
253 for order in self.browse(cr, uid, ids):
254 loc_id = order.partner_id.property_stock_supplier.id
256 if order.invoice_method=='picking':
257 istate = '2binvoiced'
258 picking_id = self.pool.get('stock.picking').create(cr, uid, {
259 'origin': order.name+((order.origin and (':'+order.origin)) or ''),
261 'address_id': order.dest_address_id.id or order.partner_address_id.id,
262 'invoice_state': istate,
263 'purchase_id': order.id,
265 for order_line in order.order_line:
266 if not order_line.product_id:
268 if order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
269 dest = order.location_id.id
270 self.pool.get('stock.move').create(cr, uid, {
271 'name': 'PO:'+order_line.name,
272 'product_id': order_line.product_id.id,
273 'product_qty': order_line.product_qty,
274 'product_uos_qty': order_line.product_qty,
275 'product_uom': order_line.product_uom.id,
276 'product_uos': order_line.product_uom.id,
277 'date_planned': order_line.date_planned,
278 'location_id': loc_id,
279 'location_dest_id': dest,
280 'picking_id': picking_id,
281 'move_dest_id': order_line.move_dest_id.id,
283 'purchase_line_id': order_line.id,
285 if order_line.move_dest_id:
286 self.pool.get('stock.move').write(cr, uid, [order_line.move_dest_id.id], {'location_id':order.location_id.id})
287 wf_service = netsvc.LocalService("workflow")
288 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
290 def copy(self, cr, uid, id, default=None,context={}):
299 'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
301 return super(purchase_order, self).copy(cr, uid, id, default, context)
305 class purchase_order_line(osv.osv):
306 def _amount_line(self, cr, uid, ids, prop, unknow_none,unknow_dict):
308 cur_obj=self.pool.get('res.currency')
309 for line in self.browse(cr, uid, ids):
310 cur = line.order_id.pricelist_id.currency_id
311 res[line.id] = cur_obj.round(cr, uid, cur, line.price_unit * line.product_qty)
315 'name': fields.char('Description', size=64, required=True),
316 'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
317 'date_planned': fields.date('Scheduled date', required=True),
318 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
319 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
320 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
321 'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
322 'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
323 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
324 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'),
325 'notes': fields.text('Notes'),
326 'order_id': fields.many2one('purchase.order', 'Order Ref', select=True, required=True, ondelete='cascade'),
327 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
330 'product_qty': lambda *a: 1.0
332 _table = 'purchase_order_line'
333 _name = 'purchase.order.line'
334 _description = 'Purchase Order line'
335 def copy(self, cr, uid, id, default=None,context={}):
338 default.update({'state':'draft', 'move_id':False})
339 return super(purchase_order_line, self).copy(cr, uid, id, default, context)
341 def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
342 partner_id, date_order=False):
344 raise osv.except_osv(_('No Pricelist !'), _('You have to select a pricelist in the purchase form !\nPlease set one before choosing a product.'))
346 return {'value': {'price_unit': 0.0, 'name':'','notes':''}, 'domain':{'product_uom':[]}}
349 lang=self.pool.get('res.partner').read(cr, uid, [partner_id])[0]['lang']
350 context={'lang':lang}
352 prod = self.pool.get('product.product').read(cr, uid, [product], ['supplier_taxes_id','name','seller_delay','uom_po_id','description_purchase'])[0]
353 prod_uom_po = prod['uom_po_id'][0]
357 date_order = time.strftime('%Y-%m-%d')
358 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
359 product, qty or 1.0, partner_id, {
363 dt = (DateTime.now() + DateTime.RelativeDateTime(days=prod['seller_delay'] or 0.0)).strftime('%Y-%m-%d')
364 prod_name = self.pool.get('product.product').name_get(cr, uid, [product], context=context)[0][1]
366 res = {'value': {'price_unit': price, 'name':prod_name, 'taxes_id':prod['supplier_taxes_id'], 'date_planned': dt,'notes':prod['description_purchase'], 'product_uom': uom}}
369 if res['value']['taxes_id']:
370 taxes = self.pool.get('account.tax').browse(cr, uid,
371 [x.id for x in product.supplier_taxes_id])
374 taxep = self.pool.get('res.partner').browse(cr, uid,
375 partner_id).property_account_supplier_tax
376 if not taxep or not taxep.id:
377 res['value']['taxes_id'] = [x.id for x in product.taxes_id]
381 if not t.tax_group==taxep.tax_group:
383 res['value']['taxes_id'] = res5
385 res2 = self.pool.get('product.uom').read(cr, uid, [uom], ['category_id'])
386 res3 = self.pool.get('product.uom').read(cr, uid, [prod_uom_po], ['category_id'])
387 domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
388 if res2[0]['category_id'] != res3[0]['category_id']:
389 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'))
391 res['domain'] = domain
394 def product_uom_change(self, cr, uid, ids, pricelist, product, qty, uom,
395 partner_id, date_order=False):
396 res = self.product_id_change(cr, uid, ids, pricelist, product, qty, uom,
397 partner_id, date_order=date_order)
398 if 'product_uom' in res['value']:
399 del res['value']['product_uom']
401 res['value']['price_unit'] = 0.0
403 purchase_order_line()
405 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: