1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
24 from osv import fields,osv
27 from mx.DateTime import RelativeDateTime, today, DateTime, localtime
28 from tools import config
29 from tools.translate import _
31 class mrp_repair(osv.osv):
33 _description = 'Repairs Order'
35 def _amount_untaxed(self, cr, uid, ids, field_name, arg, context):
37 cur_obj=self.pool.get('res.currency')
38 for repair in self.browse(cr, uid, ids):
40 for line in repair.operations:
41 res[repair.id] += line.price_subtotal
42 for line in repair.fees_lines:
43 res[repair.id] += line.price_subtotal
44 cur = repair.pricelist_id.currency_id
45 res[repair.id] = cur_obj.round(cr, uid, cur, res[repair.id])
48 def _amount_tax(self, cr, uid, ids, field_name, arg, context):
50 cur_obj=self.pool.get('res.currency')
51 for repair in self.browse(cr, uid, ids):
53 cur=repair.pricelist_id.currency_id
54 for line in repair.operations:
56 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):
58 for line in repair.fees_lines:
60 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):
62 res[repair.id]=cur_obj.round(cr, uid, cur, val)
65 def _amount_total(self, cr, uid, ids, field_name, arg, context):
67 untax = self._amount_untaxed(cr, uid, ids, field_name, arg, context)
68 tax = self._amount_tax(cr, uid, ids, field_name, arg, context)
69 cur_obj=self.pool.get('res.currency')
71 repair=self.browse(cr, uid, [id])[0]
72 cur=repair.pricelist_id.currency_id
73 res[id] = cur_obj.round(cr, uid, cur, untax.get(id, 0.0) + tax.get(id, 0.0))
77 'name' : fields.char('Repair Ref',size=24, required=True),
78 'product_id': fields.many2one('product.product', string='Product to Repair', required=True, readonly=True, states={'draft':[('readonly',False)]}),
79 'partner_id' : fields.many2one('res.partner', 'Partner', select=True, help='This field allow you to choose the parner that will be invoiced and delivered'),
80 'address_id': fields.many2one('res.partner.address', 'Delivery Address', domain="[('partner_id','=',partner_id)]"),
81 'prodlot_id': fields.many2one('stock.production.lot', 'Lot Number', select=True, domain="[('product_id','=',product_id)]"),
82 'state': fields.selection([
83 ('draft','Quotation'),
84 ('confirmed','Confirmed'),
85 ('ready','Ready to Repair'),
86 ('under_repair','Under Repair'),
87 ('2binvoiced','To be Invoiced'),
88 ('invoice_except','Invoice Exception'),
91 ], 'Repair State', readonly=True, help="Gives the state of the Repair Order"),
92 'location_id': fields.many2one('stock.location', 'Current Location', required=True, select=True, readonly=True, states={'draft':[('readonly',False)]}),
93 'location_dest_id': fields.many2one('stock.location', 'Delivery Location', readonly=True, states={'draft':[('readonly',False)]}),
94 'move_id': fields.many2one('stock.move', 'Move',required=True, domain="[('product_id','=',product_id)]", readonly=True, states={'draft':[('readonly',False)]}),
95 '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."),
96 'operations' : fields.one2many('mrp.repair.line', 'repair_id', 'Operation Lines', readonly=True, states={'draft':[('readonly',False)]}),
97 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', help='The pricelist comes from the selected partner, by default.'),
98 'partner_invoice_id':fields.many2one('res.partner.address', 'Invoicing Address', domain="[('partner_id','=',partner_id)]"),
99 'invoice_method':fields.selection([
100 ("none","No Invoice"),
101 ("b4repair","Before Repair"),
102 ("after_repair","After Repair")
104 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.'),
105 'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True),
106 'picking_id': fields.many2one('stock.picking', 'Packing',readonly=True),
107 'fees_lines' : fields.one2many('mrp.repair.fee', 'repair_id', 'Fees Lines', readonly=True, states={'draft':[('readonly',False)]}),
108 'internal_notes' : fields.text('Internal Notes'),
109 'quotation_notes' : fields.text('Quotation Notes'),
110 '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 packing with selected product. Note that you can select the locations in the Info tab, if you have the extended view."),
111 'invoiced': fields.boolean('Invoiced', readonly=True),
112 'repaired' : fields.boolean('Repaired', readonly=True),
113 'amount_untaxed': fields.function(_amount_untaxed, method=True, string='Untaxed Amount'),
114 'amount_tax': fields.function(_amount_tax, method=True, string='Taxes'),
115 'amount_total': fields.function(_amount_total, method=True, string='Total'),
119 'state': lambda *a: 'draft',
120 'deliver_bool': lambda *a: True,
121 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'mrp.repair'),
122 'invoice_method': lambda *a: 'none',
123 'pricelist_id': lambda self, cr, uid,context : self.pool.get('product.pricelist').search(cr,uid,[('type','=','sale')])[0]
126 def copy(self, cr, uid, id, default=None, context=None):
135 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.repair'),
137 return super(mrp_repair, self).copy(cr, uid, id, default, context)
140 def onchange_product_id(self, cr, uid, ids, product_id=None):
144 'guarantee_limit' :False,
145 'location_id': False,
146 'location_dest_id': False,
150 def onchange_move_id(self, cr, uid, ids, prod_id=False, move_id=False):
156 move = self.pool.get('stock.move').browse(cr, uid, move_id)
157 product = self.pool.get('product.product').browse(cr, uid, prod_id)
158 date = move.date_planned
159 limit = mx.DateTime.strptime(date, '%Y-%m-%d %H:%M:%S') + RelativeDateTime(months=product.warranty)
160 data['value']['guarantee_limit'] = limit.strftime('%Y-%m-%d')
161 data['value']['location_id'] = move.location_dest_id.id
162 data['value']['location_dest_id'] = move.location_dest_id.id
164 data['value']['partner_id'] = move.address_id.partner_id and move.address_id.partner_id.id
166 data['value']['partner_id'] = False
167 data['value']['address_id'] = move.address_id and move.address_id.id
168 d = self.onchange_partner_id(cr, uid, ids, data['value']['partner_id'], data['value']['address_id'])
169 data['value'].update(d['value'])
172 def button_dummy(self, cr, uid, ids, context=None):
175 def onchange_partner_id(self, cr, uid, ids, part, address_id):
179 'partner_invoice_id': False,
180 'pricelist_id': self.pool.get('product.pricelist').search(cr,uid,[('type','=','sale')])[0]
183 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'default'])
184 partner = self.pool.get('res.partner').browse(cr, uid, part)
185 pricelist = partner.property_product_pricelist and partner.property_product_pricelist.id or False
187 'address_id': address_id or addr['delivery'],
188 'partner_invoice_id': addr['invoice'],
189 'pricelist_id': pricelist
193 def onchange_lot_id(self, cr, uid, ids, lot, product_id):
196 'location_id': False,
197 'location_dest_id': False,
199 'guarantee_limit': False
204 lot_info = self.pool.get('stock.production.lot').browse(cr, uid, lot)
205 move_ids = self.pool.get('stock.move').search(cr, uid, [('prodlot_id', '=', lot)])
207 if not len(move_ids):
210 def get_last_move(lst_move):
211 while lst_move.move_dest_id and lst_move.move_dest_id.state == 'done':
212 lst_move = lst_move.move_dest_id
215 move_id = move_ids[0]
216 move = get_last_move(self.pool.get('stock.move').browse(cr, uid, move_id))
217 data['value']['move_id'] = move.id
218 d = self.onchange_move_id(cr, uid, ids, product_id, move.id)
219 data['value'].update(d['value'])
222 def action_cancel_draft(self, cr, uid, ids, *args):
225 mrp_line_obj = self.pool.get('mrp.repair.line')
226 for repair in self.browse(cr, uid, ids):
227 mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'draft'})
228 self.write(cr, uid, ids, {'state':'draft'})
229 wf_service = netsvc.LocalService("workflow")
231 wf_service.trg_create(uid, 'mrp.repair', id, cr)
234 def action_confirm(self, cr, uid, ids, *args):
235 mrp_line_obj = self.pool.get('mrp.repair.line')
236 for o in self.browse(cr, uid, ids):
237 if (o.invoice_method == 'b4repair'):
238 self.write(cr, uid, [o.id], {'state': '2binvoiced'})
240 self.write(cr, uid, [o.id], {'state': 'confirmed'})
241 mrp_line_obj.write(cr, uid, [l.id for l in o.operations], {'state': 'confirmed'})
244 def action_cancel(self, cr, uid, ids, context=None):
246 mrp_line_obj = self.pool.get('mrp.repair.line')
247 for repair in self.browse(cr, uid, ids):
248 mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'cancel'})
249 self.write(cr,uid,ids,{'state':'cancel'})
252 def wkf_invoice_create(self, cr, uid, ids, *args):
253 return self.action_invoice_create(cr, uid, ids)
255 def action_invoice_create(self, cr, uid, ids, group=False, context=None):
258 for repair in self.browse(cr, uid, ids, context=context):
260 if repair.state in ('draft','cancel') or repair.invoice_id:
262 if not (repair.partner_id.id and repair.partner_invoice_id.id):
263 raise osv.except_osv(_('No partner !'),_('You have to select a partner and invoicing address in the repair form !'))
264 comment=repair.quotation_notes
265 if (repair.invoice_method != 'none'):
266 if group and repair.partner_invoice_id.id in invoices_group:
267 inv_id= invoices_group[repair.partner_invoice_id.id]
268 invoice=invoice_obj.browse(cr, uid,inv_id)
270 'name': invoice.name +', '+repair.name,
271 'origin': invoice.origin+', '+repair.name,
272 'comment':(comment and (invoice.comment and invoice.comment+"\n"+comment or comment)) or (invoice.comment and invoice.comment or ''),
274 invoice_obj.write(cr, uid, [inv_id],invoice_vals,context=context)
276 if not repair.partner_id.property_account_receivable:
277 raise osv.except_osv(_('Error !'), _('No account defined for partner "%s".') % repair.partner_id.name )
278 account_id = repair.partner_id.property_account_receivable.id
281 'origin':repair.name,
282 'type': 'out_invoice',
283 'account_id': account_id,
284 'partner_id': repair.partner_id.id,
285 'address_invoice_id': repair.address_id.id,
286 'currency_id': repair.pricelist_id.currency_id.id,
287 'comment': repair.quotation_notes,
288 'fiscal_position': repair.partner_id.property_account_position.id
290 inv_obj = self.pool.get('account.invoice')
291 inv_id = inv_obj.create(cr, uid, inv)
292 invoices_group[repair.partner_invoice_id.id] = inv_id
293 self.write(cr, uid, repair.id , {'invoiced':True,'invoice_id' : inv_id})
295 for operation in repair.operations:
296 if operation.to_invoice == True:
298 name = repair.name + '-' + operation.name
300 name = operation.name
302 if operation.product_id.property_account_income:
303 account_id = operation.product_id.property_account_income.id
304 elif operation.product_id.categ_id.property_account_income_categ:
305 account_id = operation.product_id.categ_id.property_account_income_categ.id
307 raise osv.except_osv(_('Error !'), _('No account defined for product "%s".') % operation.product_id.name )
309 invoice_line_id = self.pool.get('account.invoice.line').create(cr, uid, {
310 'invoice_id': inv_id,
312 'origin':repair.name,
313 'account_id': account_id,
314 'quantity' : operation.product_uom_qty,
315 'invoice_line_tax_id': [(6,0,[x.id for x in operation.tax_id])],
316 'uos_id' : operation.product_uom.id,
317 'price_unit' : operation.price_unit,
318 'price_subtotal' : operation.product_uom_qty*operation.price_unit,
319 'product_id' : operation.product_id and operation.product_id.id or False
321 self.pool.get('mrp.repair.line').write(cr, uid, [operation.id], {'invoiced':True,'invoice_line_id':invoice_line_id})
322 for fee in repair.fees_lines:
323 if fee.to_invoice == True:
325 name = repair.name + '-' + fee.name
328 if not fee.product_id:
329 raise osv.except_osv(_('Warning !'), _('No product defined on Fees!'))
330 if fee.product_id.property_account_income:
331 account_id = fee.product_id.property_account_income.id
332 elif fee.product_id.categ_id.property_account_income_categ:
333 account_id = fee.product_id.categ_id.property_account_income_categ.id
335 raise osv.except_osv(_('Error !'), _('No account defined for product "%s".') % fee.product_id.name)
336 invoice_fee_id = self.pool.get('account.invoice.line').create(cr, uid, {
337 'invoice_id': inv_id,
339 'origin':repair.name,
340 'account_id': account_id,
341 'quantity': fee.product_uom_qty,
342 'invoice_line_tax_id': [(6,0,[x.id for x in fee.tax_id])],
343 'uos_id': fee.product_uom.id,
344 'product_id': fee.product_id and fee.product_id.id or False,
345 'price_unit': fee.price_unit,
346 'price_subtotal': fee.product_uom_qty*fee.price_unit
348 self.pool.get('mrp.repair.fee').write(cr, uid, [fee.id], {'invoiced':True,'invoice_line_id':invoice_fee_id})
349 res[repair.id]=inv_id
350 #self.action_invoice_end(cr, uid, ids)
353 def action_repair_ready(self, cr, uid, ids, context=None):
354 self.write(cr, uid, ids, {'state':'ready'})
357 def action_invoice_cancel(self, cr, uid, ids, context=None):
358 self.write(cr, uid, ids, {'state':'invoice_except'})
361 def action_repair_start(self, cr, uid, ids, context=None):
362 self.write(cr, uid, ids, {'state':'under_repair'})
365 def action_invoice_end(self, cr, uid, ids, context=None):
366 for order in self.browse(cr, uid, ids):
368 if (order.invoice_method=='b4repair'):
369 val['state'] = 'ready'
371 #val['state'] = 'done'
373 self.write(cr, uid, [order.id], val)
376 def action_repair_end(self, cr, uid, ids, context=None):
377 for order in self.browse(cr, uid, ids):
380 if (not order.invoiced and order.invoice_method=='after_repair'):
381 val['state'] = '2binvoiced'
382 elif (not order.invoiced and order.invoice_method=='b4repair'):
383 val['state'] = 'ready'
385 #val['state'] = 'done'
387 self.write(cr, uid, [order.id], val)
390 def wkf_repair_done(self, cr, uid, ids, *args):
391 res=self.action_repair_done(cr,uid,ids)
394 def action_repair_done(self, cr, uid, ids, context=None):
396 company = self.pool.get('res.users').browse(cr, uid, uid).company_id
397 for repair in self.browse(cr, uid, ids, context=context):
398 for move in repair.operations:
399 move_id = self.pool.get('stock.move').create(cr, uid, {
401 'product_id': move.product_id.id,
402 'product_qty': move.product_uom_qty,
403 'product_uom': move.product_uom.id,
404 'address_id': repair.address_id and repair.address_id.id or False,
405 'location_id': move.location_id.id,
406 'location_dest_id': move.location_dest_id.id,
407 'tracking_id': False,
410 self.pool.get('mrp.repair.line').write(cr, uid, [move.id], {'move_id': move_id})
412 if repair.deliver_bool:
413 picking = self.pool.get('stock.picking').create(cr, uid, {
414 'origin': repair.name,
417 'address_id': repair.address_id and repair.address_id.id or False,
418 'note': repair.internal_notes,
419 'invoice_state': 'none',
422 wf_service = netsvc.LocalService("workflow")
423 wf_service.trg_validate(uid, 'stock.picking', picking, 'button_confirm', cr)
425 move_id = self.pool.get('stock.move').create(cr, uid, {
427 'picking_id': picking,
428 'product_id': repair.product_id.id,
430 'product_uom': repair.product_id.uom_id.id,
431 #'product_uos_qty': line.product_uom_qty,
432 #'product_uos': line.product_uom.id,
433 'prodlot_id': repair.prodlot_id and repair.prodlot_id.id or False,
434 'address_id': repair.address_id and repair.address_id.id or False,
435 'location_id': repair.location_id.id,
436 'location_dest_id': repair.location_dest_id.id,
437 'tracking_id': False,
438 'state': 'assigned', # FIXME done ?
440 self.write(cr, uid, [repair.id], {'state':'done', 'picking_id':picking})
441 res[repair.id] = picking
443 self.write(cr, uid, [repair.id], {'state':'done'})
450 class ProductChangeMixin(object):
451 def product_id_change(self, cr, uid, ids, pricelist, product, uom=False, product_uom_qty=0, partner_id=False, guarantee_limit=False):
455 if not product_uom_qty:
457 result['product_uom_qty'] = product_uom_qty
460 product_obj = self.pool.get('product.product').browse(cr, uid, product)
462 partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
463 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, partner.property_account_position, product_obj.taxes_id)
465 result['name'] = product_obj.partner_ref
466 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id or False
469 'title':'No Pricelist !',
471 'You have to select a pricelist in the Repair form !\n'
472 'Please set one before choosing a product.'
475 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
476 product, product_uom_qty, partner_id, {'uom': uom,})[pricelist]
480 'title':'No valid pricelist line found !',
482 "Couldn't find a pricelist line matching this product and quantity.\n"
483 "You have to change either the product, the quantity or the pricelist."
486 result.update({'price_unit': price, 'price_subtotal' :price*product_uom_qty})
488 return {'value': result, 'warning': warning}
491 class mrp_repair_line(osv.osv, ProductChangeMixin):
492 _name = 'mrp.repair.line'
493 _description = 'Repair Operations Lines'
495 def copy_data(self, cr, uid, id, default=None, context=None):
496 if not default: default = {}
497 default.update( {'invoice_line_id':False,'move_id':False,'invoiced':False,'state':'draft'})
498 return super(mrp_repair_line, self).copy_data(cr, uid, id, default, context)
500 def _amount_line(self, cr, uid, ids, field_name, arg, context):
502 cur_obj=self.pool.get('res.currency')
503 for line in self.browse(cr, uid, ids):
504 res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0
505 cur = line.repair_id.pricelist_id.currency_id
506 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
510 'name' : fields.char('Description',size=64,required=True),
511 'repair_id': fields.many2one('mrp.repair', 'Repair Order Ref',ondelete='cascade', select=True),
512 'type': fields.selection([('add','Add'),('remove','Remove')],'Type', required=True),
513 'to_invoice': fields.boolean('To Invoice'),
514 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok','=',True)], required=True),
515 'invoiced': fields.boolean('Invoiced',readonly=True),
516 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
517 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal',digits=(16, int(config['price_accuracy']))),
518 'tax_id': fields.many2many('account.tax', 'repair_operation_line_tax', 'repair_operation_line_id', 'tax_id', 'Taxes'),
519 'product_uom_qty': fields.float('Quantity (UoM)', digits=(16,2), required=True),
520 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
521 'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
522 'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True),
523 'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True, select=True),
524 'move_id': fields.many2one('stock.move', 'Inventory Move', readonly=True),
525 'state': fields.selection([('draft','Draft'),('confirmed','Confirmed'),('done','Done'),('cancel','Canceled')], 'Status', required=True, readonly=True),
528 'state': lambda *a: 'draft',
529 'product_uom_qty':lambda *a:1,
532 def onchange_operation_type(self, cr, uid, ids, type, guarantee_limit):
535 'location_id': False,
536 'location_dest_id': False
539 produc_id = self.pool.get('stock.location').search(cr, uid, [('name','=','Production')])[0]
541 stock_id = self.pool.get('stock.location').search(cr, uid, [('name','=','Stock')])[0]
543 if guarantee_limit and today() > mx.DateTime.strptime(guarantee_limit, '%Y-%m-%d'):
546 'to_invoice': to_invoice,
547 'location_id': stock_id,
548 'location_dest_id' : produc_id
553 'location_id': produc_id,
554 'location_dest_id':False
560 class mrp_repair_fee(osv.osv, ProductChangeMixin):
561 _name = 'mrp.repair.fee'
562 _description = 'Repair Fees line'
563 def copy_data(self, cr, uid, id, default=None, context=None):
564 if not default: default = {}
565 default.update( {'invoice_line_id':False,'invoiced':False})
566 return super(mrp_repair_fee, self).copy_data(cr, uid, id, default, context)
567 def _amount_line(self, cr, uid, ids, field_name, arg, context):
569 cur_obj=self.pool.get('res.currency')
570 for line in self.browse(cr, uid, ids):
571 res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0
572 cur = line.repair_id.pricelist_id.currency_id
573 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
577 'repair_id': fields.many2one('mrp.repair', 'Repair Order Ref', required=True, ondelete='cascade', select=True),
578 'name': fields.char('Description', size=64, select=True,required=True),
579 'product_id': fields.many2one('product.product', 'Product'),
580 'product_uom_qty': fields.float('Quantity', digits=(16,2), required=True),
581 'price_unit': fields.float('Unit Price', required=True),
582 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
583 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal',digits=(16, int(config['price_accuracy']))),
584 'tax_id': fields.many2many('account.tax', 'repair_fee_line_tax', 'repair_fee_line_id', 'tax_id', 'Taxes'),
585 'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
586 'to_invoice': fields.boolean('To Invoice'),
587 'invoiced': fields.boolean('Invoiced',readonly=True),
590 'to_invoice': lambda *a: True,
594 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: