1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 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 ##############################################################################
23 from osv import fields,osv
26 from mx.DateTime import RelativeDateTime, today, DateTime, localtime
27 from tools import config
28 from tools.translate import _
30 class mrp_repair(osv.osv):
32 _description = 'Repairs Order'
34 def _amount_untaxed(self, cr, uid, ids, field_name, arg, context):
36 cur_obj=self.pool.get('res.currency')
37 for repair in self.browse(cr, uid, ids):
39 for line in repair.operations:
40 res[repair.id] += line.price_subtotal
41 for line in repair.fees_lines:
42 res[repair.id] += line.price_subtotal
43 cur = repair.pricelist_id.currency_id
44 res[repair.id] = cur_obj.round(cr, uid, cur, res[repair.id])
47 def _amount_tax(self, cr, uid, ids, field_name, arg, context):
49 cur_obj=self.pool.get('res.currency')
50 for repair in self.browse(cr, uid, ids):
52 cur=repair.pricelist_id.currency_id
53 for line in repair.operations:
55 for c in self.pool.get('account.tax').compute(cr, uid, line.tax_id, line.price_unit, line.product_uom_qty, repair.partner_invoice_id.id, line.product_id, repair.partner_id):
57 for line in repair.fees_lines:
59 for c in self.pool.get('account.tax').compute(cr, uid, line.tax_id, line.price_unit, line.product_uom_qty, repair.partner_invoice_id.id, line.product_id, repair.partner_id):
61 res[repair.id]=cur_obj.round(cr, uid, cur, val)
64 def _amount_total(self, cr, uid, ids, field_name, arg, context):
66 untax = self._amount_untaxed(cr, uid, ids, field_name, arg, context)
67 tax = self._amount_tax(cr, uid, ids, field_name, arg, context)
68 cur_obj=self.pool.get('res.currency')
70 repair=self.browse(cr, uid, [id])[0]
71 cur=repair.pricelist_id.currency_id
72 res[id] = cur_obj.round(cr, uid, cur, untax.get(id, 0.0) + tax.get(id, 0.0))
76 'name' : fields.char('Repair Ref',size=24, required=True),
77 'product_id': fields.many2one('product.product', string='Product to Repair', required=True, readonly=True, states={'draft':[('readonly',False)]}),
78 'partner_id' : fields.many2one('res.partner', 'Partner', select=True, help='This field allow you to choose the parner that will be invoiced and delivered'),
79 'address_id': fields.many2one('res.partner.address', 'Delivery Address', domain="[('partner_id','=',partner_id)]"),
80 'prodlot_id': fields.many2one('stock.production.lot', 'Lot Number', select=True, domain="[('product_id','=',product_id)]"),
81 'state': fields.selection([
82 ('draft','Quotation'),
83 ('confirmed','Confirmed'),
84 ('ready','Ready to Repair'),
85 ('under_repair','Under Repair'),
86 ('2binvoiced','To be Invoiced'),
87 ('invoice_except','Invoice Exception'),
90 ], 'Repair State', readonly=True, help="Gives the state of the Repair Order"),
91 'location_id': fields.many2one('stock.location', 'Current Location', required=True, select=True, readonly=True, states={'draft':[('readonly',False)]}),
92 'location_dest_id': fields.many2one('stock.location', 'Delivery Location', readonly=True, states={'draft':[('readonly',False)]}),
93 'move_id': fields.many2one('stock.move', 'Move',required=True, domain="[('product_id','=',product_id)]", readonly=True, states={'draft':[('readonly',False)]}),
94 'guarantee_limit': fields.date('Guarantee limit', help="The garantee limit is computed as: last move date + warranty defined on selected product. If the current date is below the garantee limit, each operation and fee you will add will be set as 'not to invoiced' by default. Note that you can change manually afterwards."),
95 'operations' : fields.one2many('mrp.repair.line', 'repair_id', 'Operation Lines', readonly=True, states={'draft':[('readonly',False)]}),
96 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', help='The pricelist comes from the selected partner, by default.'),
97 'partner_invoice_id':fields.many2one('res.partner.address', 'Invoicing Address', domain="[('partner_id','=',partner_id)]"),
98 'invoice_method':fields.selection([
99 ("none","No Invoice"),
100 ("b4repair","Before Repair"),
101 ("after_repair","After Repair")
103 select=True, required=True, states={'draft':[('readonly',False)]}, readonly=True, help='This field allow you to change the workflow of the repair order. If value selected is different from \'No Invoice\', it also allow you to select the pricelist and invoicing address.'),
104 'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True),
105 'picking_id': fields.many2one('stock.picking', 'Picking',readonly=True),
106 'fees_lines' : fields.one2many('mrp.repair.fee', 'repair_id', 'Fees Lines', readonly=True, states={'draft':[('readonly',False)]}),
107 'internal_notes' : fields.text('Internal Notes'),
108 'quotation_notes' : fields.text('Quotation Notes'),
109 'deliver_bool': fields.boolean('Deliver', help="Check this box if you want to manage the delivery once the product is repaired. If cheked, it will create a picking with selected product. Note that you can select the locations in the Info tab, if you have the extended view."),
110 'invoiced': fields.boolean('Invoiced', readonly=True),
111 'repaired' : fields.boolean('Repaired', readonly=True),
112 'amount_untaxed': fields.function(_amount_untaxed, method=True, string='Untaxed Amount'),
113 'amount_tax': fields.function(_amount_tax, method=True, string='Taxes'),
114 'amount_total': fields.function(_amount_total, method=True, string='Total'),
118 'state': lambda *a: 'draft',
119 'deliver_bool': lambda *a: True,
120 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'mrp.repair'),
121 'invoice_method': lambda *a: 'none',
122 'pricelist_id': lambda self, cr, uid,context : self.pool.get('product.pricelist').search(cr,uid,[('type','=','sale')])[0]
125 def copy(self, cr, uid, id, default=None, context=None):
134 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.repair'),
136 return super(mrp_repair, self).copy(cr, uid, id, default, context)
139 def onchange_product_id(self, cr, uid, ids, product_id=None):
143 'guarantee_limit' :False,
144 'location_id': False,
145 'location_dest_id': False,
149 def onchange_move_id(self, cr, uid, ids, prod_id=False, move_id=False):
155 move = self.pool.get('stock.move').browse(cr, uid, move_id)
156 product = self.pool.get('product.product').browse(cr, uid, prod_id)
157 date = move.date_planned
158 limit = mx.DateTime.strptime(date, '%Y-%m-%d %H:%M:%S') + RelativeDateTime(months=product.warranty)
159 data['value']['guarantee_limit'] = limit.strftime('%Y-%m-%d')
160 data['value']['location_id'] = move.location_dest_id.id
161 data['value']['location_dest_id'] = move.location_dest_id.id
163 data['value']['partner_id'] = move.address_id.partner_id and move.address_id.partner_id.id
165 data['value']['partner_id'] = False
166 data['value']['address_id'] = move.address_id and move.address_id.id
167 d = self.onchange_partner_id(cr, uid, ids, data['value']['partner_id'], data['value']['address_id'])
168 data['value'].update(d['value'])
171 def button_dummy(self, cr, uid, ids, context=None):
174 def onchange_partner_id(self, cr, uid, ids, part, address_id):
178 'partner_invoice_id': False,
179 'pricelist_id': self.pool.get('product.pricelist').search(cr,uid,[('type','=','sale')])[0]
182 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'default'])
183 partner = self.pool.get('res.partner').browse(cr, uid, part)
184 pricelist = partner.property_product_pricelist and partner.property_product_pricelist.id or False
186 'address_id': address_id or addr['delivery'],
187 'partner_invoice_id': addr['invoice'],
188 'pricelist_id': pricelist
192 def onchange_lot_id(self, cr, uid, ids, lot, product_id):
195 'location_id': False,
196 'location_dest_id': False,
198 'guarantee_limit': False
203 lot_info = self.pool.get('stock.production.lot').browse(cr, uid, lot)
204 move_ids = self.pool.get('stock.move').search(cr, uid, [('prodlot_id', '=', lot)])
206 if not len(move_ids):
209 def get_last_move(lst_move):
210 while lst_move.move_dest_id and lst_move.move_dest_id.state == 'done':
211 lst_move = lst_move.move_dest_id
214 move_id = move_ids[0]
215 move = get_last_move(self.pool.get('stock.move').browse(cr, uid, move_id))
216 data['value']['move_id'] = move.id
217 d = self.onchange_move_id(cr, uid, ids, product_id, move.id)
218 data['value'].update(d['value'])
221 def action_cancel_draft(self, cr, uid, ids, *args):
224 mrp_line_obj = self.pool.get('mrp.repair.line')
225 for repair in self.browse(cr, uid, ids):
226 mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'draft'})
227 self.write(cr, uid, ids, {'state':'draft'})
228 wf_service = netsvc.LocalService("workflow")
230 wf_service.trg_create(uid, 'mrp.repair', id, cr)
233 def action_confirm(self, cr, uid, ids, *args):
234 mrp_line_obj = self.pool.get('mrp.repair.line')
235 for o in self.browse(cr, uid, ids):
236 if (o.invoice_method == 'b4repair'):
237 self.write(cr, uid, [o.id], {'state': '2binvoiced'})
239 self.write(cr, uid, [o.id], {'state': 'confirmed'})
240 mrp_line_obj.write(cr, uid, [l.id for l in o.operations], {'state': 'confirmed'})
243 def action_cancel(self, cr, uid, ids, context=None):
245 mrp_line_obj = self.pool.get('mrp.repair.line')
246 for repair in self.browse(cr, uid, ids):
247 mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'cancel'})
248 self.write(cr,uid,ids,{'state':'cancel'})
251 def wkf_invoice_create(self, cr, uid, ids, *args):
252 return self.action_invoice_create(cr, uid, ids)
254 def action_invoice_create(self, cr, uid, ids, group=False, context=None):
257 for repair in self.browse(cr, uid, ids, context=context):
259 if repair.state in ('draft','cancel') or repair.invoice_id:
261 if not (repair.partner_id.id and repair.partner_invoice_id.id):
262 raise osv.except_osv(_('No partner !'),_('You have to select a partner in the repair form !'))
263 comment=repair.quotation_notes
264 if (repair.invoice_method != 'none'):
265 if group and repair.partner_invoice_id.id in invoices_group:
266 inv_id= invoices_group[repair.partner_invoice_id.id]
267 invoice=invoice_obj.browse(cr, uid,inv_id)
269 'name': invoice.name +', '+repair.name,
270 'origin': invoice.origin+', '+repair.name,
271 'comment':(comment and (invoice.comment and invoice.comment+"\n"+comment or comment)) or (invoice.comment and invoice.comment or ''),
273 invoice_obj.write(cr, uid, [inv_id],invoice_vals,context=context)
275 a = repair.partner_id.property_account_receivable.id
278 'origin':repair.name,
279 'type': 'out_invoice',
281 'partner_id': repair.partner_id.id,
282 'address_invoice_id': repair.address_id.id,
283 'currency_id': repair.pricelist_id.currency_id.id,
284 'comment': repair.quotation_notes,
285 'fiscal_position': repair.partner_id.property_account_position.id
287 inv_obj = self.pool.get('account.invoice')
288 inv_id = inv_obj.create(cr, uid, inv)
289 invoices_group[repair.partner_invoice_id.id] = inv_id
290 self.write(cr, uid, repair.id , {'invoiced':True,'invoice_id' : inv_id})
292 for operation in repair.operations:
293 if operation.to_invoice == True:
295 name = repair.name + '-' + operation.name
297 name = operation.name
298 invoice_line_id=self.pool.get('account.invoice.line').create(cr, uid, {
299 'invoice_id': inv_id,
301 'origin':repair.name,
302 'account_id': operation.product_id and operation.product_id.property_account_income and operation.product_id.property_account_income.id,
303 'quantity' : operation.product_uom_qty,
304 'invoice_line_tax_id': [(6,0,[x.id for x in operation.tax_id])],
305 'uos_id' : operation.product_uom.id,
306 'price_unit' : operation.price_unit,
307 'price_subtotal' : operation.product_uom_qty*operation.price_unit,
308 'product_id' : operation.product_id and operation.product_id.id or False
310 self.pool.get('mrp.repair.line').write(cr, uid, [operation.id], {'invoiced':True,'invoice_line_id':invoice_line_id})
311 for fee in repair.fees_lines:
312 if fee.to_invoice == True:
314 name = repair.name + '-' + fee.name
317 invoice_fee_id=self.pool.get('account.invoice.line').create(cr, uid, {
318 'invoice_id': inv_id,
320 'origin':repair.name,
322 'quantity': fee.product_uom_qty,
323 'invoice_line_tax_id': [(6,0,[x.id for x in fee.tax_id])],
324 'uos_id': fee.product_uom.id,
325 'product_id': fee.product_id and fee.product_id.id or False,
326 'price_unit': fee.price_unit,
327 'price_subtotal': fee.product_uom_qty*fee.price_unit
329 self.pool.get('mrp.repair.fee').write(cr, uid, [fee.id], {'invoiced':True,'invoice_line_id':invoice_fee_id})
330 res[repair.id]=inv_id
331 #self.action_invoice_end(cr, uid, ids)
334 def action_repair_ready(self, cr, uid, ids, context=None):
335 self.write(cr, uid, ids, {'state':'ready'})
338 def action_invoice_cancel(self, cr, uid, ids, context=None):
339 self.write(cr, uid, ids, {'state':'invoice_except'})
342 def action_repair_start(self, cr, uid, ids, context=None):
343 self.write(cr, uid, ids, {'state':'under_repair'})
346 def action_invoice_end(self, cr, uid, ids, context=None):
347 for order in self.browse(cr, uid, ids):
349 if (order.invoice_method=='b4repair'):
350 val['state'] = 'ready'
352 #val['state'] = 'done'
354 self.write(cr, uid, [order.id], val)
357 def action_repair_end(self, cr, uid, ids, context=None):
358 for order in self.browse(cr, uid, ids):
361 if (not order.invoiced and order.invoice_method=='after_repair'):
362 val['state'] = '2binvoiced'
363 elif (not order.invoiced and order.invoice_method=='b4repair'):
364 val['state'] = 'ready'
366 #val['state'] = 'done'
368 self.write(cr, uid, [order.id], val)
371 def wkf_repair_done(self, cr, uid, ids, *args):
372 res=self.action_repair_done(cr,uid,ids)
375 def action_repair_done(self, cr, uid, ids, context=None):
377 company = self.pool.get('res.users').browse(cr, uid, uid).company_id
378 for repair in self.browse(cr, uid, ids, context=context):
379 for move in repair.operations:
380 move_id = self.pool.get('stock.move').create(cr, uid, {
382 'product_id': move.product_id.id,
383 'product_qty': move.product_uom_qty,
384 'product_uom': move.product_uom.id,
385 'address_id': repair.address_id and repair.address_id.id or False,
386 'location_id': move.location_id.id,
387 'location_dest_id': move.location_dest_id.id,
388 'tracking_id': False,
391 self.pool.get('mrp.repair.line').write(cr, uid, [move.id], {'move_id': move_id})
393 if repair.deliver_bool:
394 picking = self.pool.get('stock.picking').create(cr, uid, {
395 'origin': repair.name,
398 'address_id': repair.address_id and repair.address_id.id or False,
399 'note': repair.internal_notes,
400 'invoice_state': 'none',
403 wf_service = netsvc.LocalService("workflow")
404 wf_service.trg_validate(uid, 'stock.picking', picking, 'button_confirm', cr)
406 move_id = self.pool.get('stock.move').create(cr, uid, {
408 'picking_id': picking,
409 'product_id': repair.product_id.id,
411 'product_uom': repair.product_id.uom_id.id,
412 #'product_uos_qty': line.product_uom_qty,
413 #'product_uos': line.product_uom.id,
414 'prodlot_id': repair.prodlot_id and repair.prodlot_id.id or False,
415 'address_id': repair.address_id and repair.address_id.id or False,
416 'location_id': repair.location_id.id,
417 'location_dest_id': repair.location_dest_id.id,
418 'tracking_id': False,
419 'state': 'assigned', # FIXME done ?
421 self.write(cr, uid, [repair.id], {'state':'done', 'picking_id':picking})
422 res[repair.id] = picking
424 self.write(cr, uid, [repair.id], {'state':'done'})
431 class ProductChangeMixin(object):
432 def product_id_change(self, cr, uid, ids, pricelist, product, uom=False, product_uom_qty=0, partner_id=False, guarantee_limit=False):
436 if not product_uom_qty:
438 result['product_uom_qty'] = product_uom_qty
441 product_obj = self.pool.get('product.product').browse(cr, uid, product)
443 partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
444 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, partner.property_account_position, product_obj.taxes_id)
446 result['name'] = product_obj.partner_ref
447 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id or False
450 'title':'No Pricelist !',
452 'You have to select a pricelist in the Repair form !\n'
453 'Please set one before choosing a product.'
456 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
457 product, product_uom_qty, partner_id, {'uom': uom,})[pricelist]
461 'title':'No valid pricelist line found !',
463 "Couldn't find a pricelist line matching this product and quantity.\n"
464 "You have to change either the product, the quantity or the pricelist."
467 result.update({'price_unit': price, 'price_subtotal' :price*product_uom_qty})
469 return {'value': result, 'warning': warning}
472 class mrp_repair_line(osv.osv, ProductChangeMixin):
473 _name = 'mrp.repair.line'
474 _description = 'Repair Operations Lines'
476 def copy_data(self, cr, uid, id, default=None, context=None):
477 if not default: default = {}
478 default.update( {'invoice_line_id':False,'move_id':False,'invoiced':False,'state':'draft'})
479 return super(mrp_repair_line, self).copy_data(cr, uid, id, default, context)
481 def _amount_line(self, cr, uid, ids, field_name, arg, context):
483 cur_obj=self.pool.get('res.currency')
484 for line in self.browse(cr, uid, ids):
485 res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0
486 cur = line.repair_id.pricelist_id.currency_id
487 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
491 'name' : fields.char('Description',size=64,required=True),
492 'repair_id': fields.many2one('mrp.repair', 'Repair Order Ref',ondelete='cascade', select=True),
493 'type': fields.selection([('add','Add'),('remove','Remove')],'Type', required=True),
494 'to_invoice': fields.boolean('To Invoice'),
495 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok','=',True)], required=True),
496 'invoiced': fields.boolean('Invoiced',readonly=True),
497 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
498 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal',digits=(16, int(config['price_accuracy']))),
499 'tax_id': fields.many2many('account.tax', 'repair_operation_line_tax', 'repair_operation_line_id', 'tax_id', 'Taxes'),
500 'product_uom_qty': fields.float('Quantity (UoM)', digits=(16,2), required=True),
501 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
502 'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
503 'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True),
504 'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True, select=True),
505 'move_id': fields.many2one('stock.move', 'Inventory Move', readonly=True),
506 'state': fields.selection([('draft','Draft'),('confirmed','Confirmed'),('done','Done'),('cancel','Canceled')], 'Status', required=True, readonly=True),
509 'state': lambda *a: 'draft',
510 'product_uom_qty':lambda *a:1,
513 def onchange_operation_type(self, cr, uid, ids, type, guarantee_limit):
516 'location_id': False,
517 'location_dest_id': False
520 produc_id = self.pool.get('stock.location').search(cr, uid, [('name','=','Production')])[0]
522 stock_id = self.pool.get('stock.location').search(cr, uid, [('name','=','Stock')])[0]
524 if guarantee_limit and today() > mx.DateTime.strptime(guarantee_limit, '%Y-%m-%d'):
527 'to_invoice': to_invoice,
528 'location_id': stock_id,
529 'location_dest_id' : produc_id
534 'location_id': produc_id,
535 'location_dest_id':False
541 class mrp_repair_fee(osv.osv, ProductChangeMixin):
542 _name = 'mrp.repair.fee'
543 _description = 'Repair Fees line'
544 def copy_data(self, cr, uid, id, default=None, context=None):
545 if not default: default = {}
546 default.update( {'invoice_line_id':False,'invoiced':False})
547 return super(mrp_repair_fee, self).copy_data(cr, uid, id, default, context)
548 def _amount_line(self, cr, uid, ids, field_name, arg, context):
550 cur_obj=self.pool.get('res.currency')
551 for line in self.browse(cr, uid, ids):
552 res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0
553 cur = line.repair_id.pricelist_id.currency_id
554 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
558 'repair_id': fields.many2one('mrp.repair', 'Repair Order Ref', required=True, ondelete='cascade', select=True),
559 'name': fields.char('Description', size=64, select=True,required=True),
560 'product_id': fields.many2one('product.product', 'Product'),
561 'product_uom_qty': fields.float('Quantity', digits=(16,2), required=True),
562 'price_unit': fields.float('Unit Price', required=True),
563 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
564 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal',digits=(16, int(config['price_accuracy']))),
565 'tax_id': fields.many2many('account.tax', 'repair_fee_line_tax', 'repair_fee_line_id', 'tax_id', 'Taxes'),
566 'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
567 'to_invoice': fields.boolean('To Invoice'),
568 'invoiced': fields.boolean('Invoiced',readonly=True),
571 'to_invoice': lambda *a: True,
575 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: