1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2008 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 class mrp_repair(osv.osv):
31 _description = 'Repairs Order'
33 def _amount_untaxed(self, cr, uid, ids, field_name, arg, context):
35 cur_obj=self.pool.get('res.currency')
36 for repair in self.browse(cr, uid, ids):
38 for line in repair.operations:
39 res[repair.id] += line.price_subtotal
40 for line in repair.fees_lines:
41 res[repair.id] += line.price_subtotal
42 cur = repair.pricelist_id.currency_id
43 res[repair.id] = cur_obj.round(cr, uid, cur, res[repair.id])
46 def _amount_tax(self, cr, uid, ids, field_name, arg, context):
48 cur_obj=self.pool.get('res.currency')
49 for repair in self.browse(cr, uid, ids):
51 cur=repair.pricelist_id.currency_id
52 for line in repair.operations:
54 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):
56 for line in repair.fees_lines:
58 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):
60 res[repair.id]=cur_obj.round(cr, uid, cur, val)
63 def _amount_total(self, cr, uid, ids, field_name, arg, context):
65 untax = self._amount_untaxed(cr, uid, ids, field_name, arg, context)
66 tax = self._amount_tax(cr, uid, ids, field_name, arg, context)
67 cur_obj=self.pool.get('res.currency')
69 repair=self.browse(cr, uid, [id])[0]
70 cur=repair.pricelist_id.currency_id
71 res[id] = cur_obj.round(cr, uid, cur, untax.get(id, 0.0) + tax.get(id, 0.0))
75 'name' : fields.char('Repair Ref',size=24, required=True),
76 'product_id': fields.many2one('product.product', string='Product to Repair', required=True, readonly=True, states={'draft':[('readonly',False)]}),
77 'partner_id' : fields.many2one('res.partner', 'Partner', select=True, help='This field allow you to choose the parner that will be invoiced and delivered'),
78 'address_id': fields.many2one('res.partner.address', 'Delivery Address', domain="[('partner_id','=',partner_id)]"),
79 'prodlot_id': fields.many2one('stock.production.lot', 'Lot Number', select=True, domain="[('product_id','=',product_id)]"),
80 'state': fields.selection([
81 ('draft','Quotation'),
82 ('confirmed','Confirmed'),
83 ('ready','Ready to Repair'),
84 ('under_repair','Under Repair'),
85 ('2binvoiced','To be Invoiced'),
86 ('invoice_except','Invoice Exception'),
89 ], 'Repair State', readonly=True, help="Gives the state of the Repair Order"),
90 'location_id': fields.many2one('stock.location', 'Current Location', required=True, select=True, readonly=True, states={'draft':[('readonly',False)]}),
91 'location_dest_id': fields.many2one('stock.location', 'Delivery Location', readonly=True, states={'draft':[('readonly',False)]}),
92 'move_id': fields.many2one('stock.move', 'Move',required=True, domain="[('product_id','=',product_id)]", readonly=True, states={'draft':[('readonly',False)]}),
93 '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."),
94 'operations' : fields.one2many('mrp.repair.line', 'repair_id', 'Operation Lines', readonly=True, states={'draft':[('readonly',False)]}),
95 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', help='The pricelist comes from the selected partner, by default.'),
96 'partner_invoice_id':fields.many2one('res.partner.address', 'Invoice to', domain="[('partner_id','=',partner_id)]"),
97 'invoice_method':fields.selection([
98 ("none","No Invoice"),
99 ("b4repair","Before Repair"),
100 ("after_repair","After Repair")
102 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.'),
103 'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True),
104 'picking_id': fields.many2one('stock.picking', 'Packing',readonly=True),
105 'fees_lines' : fields.one2many('mrp.repair.fee', 'repair_id', 'Fees Lines', readonly=True, states={'draft':[('readonly',False)]}),
106 'internal_notes' : fields.text('Internal Notes'),
107 'quotation_notes' : fields.text('Quotation Notes'),
108 '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."),
109 'invoiced': fields.boolean('Invoiced', readonly=True),
110 'repaired' : fields.boolean('Repaired', readonly=True),
111 'amount_untaxed': fields.function(_amount_untaxed, method=True, string='Untaxed Amount'),
112 'amount_tax': fields.function(_amount_tax, method=True, string='Taxes'),
113 'amount_total': fields.function(_amount_total, method=True, string='Total'),
117 'state': lambda *a: 'draft',
118 'deliver_bool': lambda *a: True,
119 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'mrp.repair'),
120 'invoice_method': lambda *a: 'none',
121 'pricelist_id': lambda self, cr, uid,context : self.pool.get('product.pricelist').search(cr,uid,[('type','=','sale')])[0]
124 def copy(self, cr, uid, id, default=None, context=None):
133 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.repair'),
135 return super(mrp_repair, self).copy(cr, uid, id, default, context)
138 def onchange_product_id(self, cr, uid, ids, product_id=None):
142 'guarantee_limit' :False,
143 'location_id': False,
144 'location_dest_id': False,
148 def onchange_move_id(self, cr, uid, ids, prod_id=False, move_id=False):
154 move = self.pool.get('stock.move').browse(cr, uid, move_id)
155 product = self.pool.get('product.product').browse(cr, uid, prod_id)
156 date = move.date_planned
157 limit = mx.DateTime.strptime(date, '%Y-%m-%d %H:%M:%S') + RelativeDateTime(months=product.warranty)
158 data['value']['guarantee_limit'] = limit.strftime('%Y-%m-%d')
159 data['value']['location_id'] = move.location_dest_id.id
160 data['value']['location_dest_id'] = move.location_dest_id.id
162 data['value']['partner_id'] = move.address_id.partner_id and move.address_id.partner_id.id
164 data['value']['partner_id'] = False
165 data['value']['address_id'] = move.address_id and move.address_id.id
166 d = self.onchange_partner_id(cr, uid, ids, data['value']['partner_id'], data['value']['address_id'])
167 data['value'].update(d['value'])
170 def button_dummy(self, cr, uid, ids, context=None):
173 def onchange_partner_id(self, cr, uid, ids, part, address_id):
177 'partner_invoice_id': False,
178 'pricelist_id': self.pool.get('product.pricelist').search(cr,uid,[('type','=','sale')])[0]
181 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'default'])
182 partner = self.pool.get('res.partner').browse(cr, uid, part)
183 pricelist = partner.property_product_pricelist and partner.property_product_pricelist.id or False
185 'address_id': address_id or addr['delivery'],
186 'partner_invoice_id': addr['invoice'],
187 'pricelist_id': pricelist
191 def onchange_lot_id(self, cr, uid, ids, lot, product_id):
194 'location_id': False,
195 'location_dest_id': False,
197 'guarantee_limit': False
202 lot_info = self.pool.get('stock.production.lot').browse(cr, uid, lot)
203 move_ids = self.pool.get('stock.move').search(cr, uid, [('prodlot_id', '=', lot)])
205 if not len(move_ids):
208 def get_last_move(lst_move):
209 while lst_move.move_dest_id and lst_move.move_dest_id.state == 'done':
210 lst_move = lst_move.move_dest_id
213 move_id = move_ids[0]
214 move = get_last_move(self.pool.get('stock.move').browse(cr, uid, move_id))
215 data['value']['move_id'] = move.id
216 d = self.onchange_move_id(cr, uid, ids, product_id, move.id)
217 data['value'].update(d['value'])
220 def action_cancel_draft(self, cr, uid, ids, *args):
223 mrp_line_obj = self.pool.get('mrp.repair.line')
224 for repair in self.browse(cr, uid, ids):
225 mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'draft'})
226 self.write(cr, uid, ids, {'state':'draft'})
227 wf_service = netsvc.LocalService("workflow")
229 wf_service.trg_create(uid, 'mrp.repair', id, cr)
232 def action_confirm(self, cr, uid, ids, *args):
233 mrp_line_obj = self.pool.get('mrp.repair.line')
234 for o in self.browse(cr, uid, ids):
235 if (o.invoice_method == 'b4repair'):
236 self.write(cr, uid, [o.id], {'state': '2binvoiced'})
238 self.write(cr, uid, [o.id], {'state': 'confirmed'})
239 mrp_line_obj.write(cr, uid, [l.id for l in o.operations], {'state': 'confirmed'})
242 def action_cancel(self, cr, uid, ids, context=None):
244 mrp_line_obj = self.pool.get('mrp.repair.line')
245 for repair in self.browse(cr, uid, ids):
246 mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'cancel'})
247 self.write(cr,uid,ids,{'state':'cancel'})
250 def wkf_invoice_create(self, cr, uid, ids, *args):
251 return self.action_invoice_create(cr, uid, ids)
253 def action_invoice_create(self, cr, uid, ids, group=False, context=None):
256 for repair in self.browse(cr, uid, ids, context=context):
258 if repair.state in ('draft','cancel') or repair.invoice_id:
260 if not (repair.partner_id.id and repair.partner_invoice_id.id):
261 raise osv.except_osv('No partner !','You have to select a partner in the repair form ! ')
262 comment=repair.quotation_notes
263 if (repair.invoice_method != 'none'):
264 if group and repair.partner_invoice_id.id in invoices_group:
265 inv_id= invoices_group[repair.partner_invoice_id.id]
266 invoice=invoice_obj.browse(cr, uid,inv_id)
268 'name': invoice.name +', '+repair.name,
269 'origin': invoice.origin+', '+repair.name,
270 'comment':(comment and (invoice.comment and invoice.comment+"\n"+comment or comment)) or (invoice.comment and invoice.comment or ''),
272 invoice_obj.write(cr, uid, [inv_id],invoice_vals,context=context)
274 a = repair.partner_id.property_account_receivable.id
277 'origin':repair.name,
278 'type': 'out_invoice',
280 'partner_id': repair.partner_id.id,
281 'address_invoice_id': repair.address_id.id,
282 'currency_id': repair.pricelist_id.currency_id.id,
283 'comment': repair.quotation_notes,
285 inv_obj = self.pool.get('account.invoice')
286 inv_id = inv_obj.create(cr, uid, inv)
287 invoices_group[repair.partner_invoice_id.id] = inv_id
288 self.write(cr, uid, repair.id , {'invoiced':True,'invoice_id' : inv_id})
290 for operation in repair.operations:
291 if operation.to_invoice == True:
293 name = repair.name + '-' + operation.name
295 name = operation.name
296 invoice_line_id=self.pool.get('account.invoice.line').create(cr, uid, {
297 'invoice_id': inv_id,
299 'origin':repair.name,
301 'quantity' : operation.product_uom_qty,
302 'invoice_line_tax_id': [(6,0,[x.id for x in operation.tax_id])],
303 'uos_id' : operation.product_uom.id,
304 'price_unit' : operation.price_unit,
305 'price_subtotal' : operation.product_uom_qty*operation.price_unit,
306 'product_id' : operation.product_id and operation.product_id.id or False
308 self.pool.get('mrp.repair.line').write(cr, uid, [operation.id], {'invoiced':True,'invoice_line_id':invoice_line_id})
309 for fee in repair.fees_lines:
310 if fee.to_invoice == True:
312 name = repair.name + '-' + fee.name
315 invoice_fee_id=self.pool.get('account.invoice.line').create(cr, uid, {
316 'invoice_id': inv_id,
318 'origin':repair.name,
320 'quantity': fee.product_uom_qty,
321 'invoice_line_tax_id': [(6,0,[x.id for x in fee.tax_id])],
322 'uos_id': fee.product_uom.id,
323 'product_id': fee.product_id and fee.product_id.id or False,
324 'price_unit': fee.price_unit,
325 'price_subtotal': fee.product_uom_qty*fee.price_unit
327 self.pool.get('mrp.repair.fee').write(cr, uid, [fee.id], {'invoiced':True,'invoice_line_id':invoice_fee_id})
328 res[repair.id]=inv_id
329 #self.action_invoice_end(cr, uid, ids)
332 def action_repair_ready(self, cr, uid, ids, context=None):
333 self.write(cr, uid, ids, {'state':'ready'})
336 def action_invoice_cancel(self, cr, uid, ids, context=None):
337 self.write(cr, uid, ids, {'state':'invoice_except'})
340 def action_repair_start(self, cr, uid, ids, context=None):
341 self.write(cr, uid, ids, {'state':'under_repair'})
344 def action_invoice_end(self, cr, uid, ids, context=None):
345 for order in self.browse(cr, uid, ids):
347 if (order.invoice_method=='b4repair'):
348 val['state'] = 'ready'
350 #val['state'] = 'done'
352 self.write(cr, uid, [order.id], val)
355 def action_repair_end(self, cr, uid, ids, context=None):
356 for order in self.browse(cr, uid, ids):
359 if (not order.invoiced and order.invoice_method=='after_repair'):
360 val['state'] = '2binvoiced'
361 elif (not order.invoiced and order.invoice_method=='b4repair'):
362 val['state'] = 'ready'
364 #val['state'] = 'done'
366 self.write(cr, uid, [order.id], val)
369 def wkf_repair_done(self, cr, uid, ids, *args):
370 res=self.action_repair_done(cr,uid,ids)
373 def action_repair_done(self, cr, uid, ids, context=None):
375 company = self.pool.get('res.users').browse(cr, uid, uid).company_id
376 for repair in self.browse(cr, uid, ids, context=context):
377 #TODO: create the moves add/remove
378 for move in repair.operations:
379 move_id = self.pool.get('stock.move').create(cr, uid, {
381 'product_id': move.product_id.id,
382 'product_qty': move.product_uom_qty,
383 'product_uom': move.product_uom.id,
384 'address_id': repair.address_id and repair.address_id.id or False,
385 'location_id': move.location_id.id,
386 'location_dest_id': move.location_dest_id.id,
387 'tracking_id': False,
390 self.pool.get('mrp.repair.line').write(cr, uid, [move.id], {'move_id': move_id})
392 if repair.deliver_bool:
393 picking = self.pool.get('stock.picking').create(cr, uid, {
394 'origin': repair.name,
397 'address_id': repair.address_id and repair.address_id.id or False,
398 'note': repair.internal_notes,
399 'invoice_state': 'none',
400 'type': 'out', # FIXME delivery ?
402 wf_service = netsvc.LocalService("workflow")
403 wf_service.trg_validate(uid, 'stock.picking', picking, 'button_confirm', cr)
405 move_id = self.pool.get('stock.move').create(cr, uid, {
407 'picking_id': picking,
408 'product_id': repair.product_id.id,
410 'product_uom': repair.product_id.uom_id.id,
411 #'product_uos_qty': line.product_uom_qty,
412 #'product_uos': line.product_uom.id,
413 'prodlot_id': repair.prodlot_id and repair.prodlot_id.id or False,
414 'address_id': repair.address_id and repair.address_id.id or False,
415 'location_id': repair.location_id.id,
416 'location_dest_id': repair.location_dest_id.id,
417 'tracking_id': False,
418 'state': 'assigned', # FIXME done ?
420 self.write(cr, uid, [repair.id], {'state':'done', 'picking_id':picking})
421 res[repair.id] = picking
423 self.write(cr, uid, [repair.id], {'state':'done'})
430 class ProductChangeMixin(object):
431 def product_id_change(self, cr, uid, ids, pricelist, product, uom=False, product_uom_qty=0, partner_id=False, guarantee_limit=False):
435 if not product_uom_qty:
437 result['product_uom_qty'] = product_uom_qty
440 product_obj = self.pool.get('product.product').browse(cr, uid, product)
442 partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
443 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, partner, product_obj.taxes_id)
445 result['name'] = product_obj.partner_ref
446 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id or False
449 'title':'No Pricelist !',
451 'You have to select a pricelist in the Repair form !\n'
452 'Please set one before choosing a product.'
455 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
456 product, product_uom_qty, partner_id, {'uom': uom,})[pricelist]
460 'title':'No valid pricelist line found !',
462 "Couldn't find a pricelist line matching this product and quantity.\n"
463 "You have to change either the product, the quantity or the pricelist."
466 result.update({'price_unit': price, 'price_subtotal' :price*product_uom_qty})
468 return {'value': result, 'warning': warning}
471 class mrp_repair_line(osv.osv, ProductChangeMixin):
472 _name = 'mrp.repair.line'
473 _description = 'Repair Operations Lines'
475 def copy(self, cr, uid, id, default=None, context=None):
476 if not default: default = {}
477 default.update( {'invoice_line_id':False,'move_id':False,'invoiced':False,'state':'draft'})
478 return super(mrp_repair_line, self).copy(cr, uid, id, default, context)
480 def _amount_line(self, cr, uid, ids, field_name, arg, context):
482 cur_obj=self.pool.get('res.currency')
483 for line in self.browse(cr, uid, ids):
484 res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0
485 cur = line.repair_id.pricelist_id.currency_id
486 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
490 'name' : fields.char('Description',size=64,required=True),
491 'repair_id': fields.many2one('mrp.repair', 'Repair Order Ref',ondelete='cascade', select=True),
492 'type': fields.selection([('add','Add'),('remove','Remove')],'Type', required=True),
493 'to_invoice': fields.boolean('To Invoice'),
494 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok','=',True)], required=True),
495 'invoiced': fields.boolean('Invoiced',readonly=True),
496 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
497 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal',digits=(16, int(config['price_accuracy']))),
498 'tax_id': fields.many2many('account.tax', 'repair_operation_line_tax', 'repair_operation_line_id', 'tax_id', 'Taxes'),
499 'product_uom_qty': fields.float('Quantity (UoM)', digits=(16,2), required=True),
500 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
501 'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
502 'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True),
503 'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True, select=True),
504 'move_id': fields.many2one('stock.move', 'Inventory Move', readonly=True),
505 'state': fields.selection([('draft','Draft'),('confirmed','Confirmed'),('done','Done'),('cancel','Canceled')], 'Status', required=True, readonly=True),
508 'state': lambda *a: 'draft',
509 'product_uom_qty':lambda *a:1,
512 def onchange_operation_type(self, cr, uid, ids, type, guarantee_limit):
515 'location_id': False,
516 'location_dest_id': False
519 produc_id = self.pool.get('stock.location').search(cr, uid, [('name','=','Production')])[0]
521 stock_id = self.pool.get('stock.location').search(cr, uid, [('name','=','Stock')])[0]
523 if guarantee_limit and today() > mx.DateTime.strptime(guarantee_limit, '%Y-%m-%d'):
526 'to_invoice': to_invoice,
527 'location_id': stock_id,
528 'location_dest_id' : produc_id
533 'location_id': produc_id,
534 'location_dest_id':False
540 class mrp_repair_fee(osv.osv, ProductChangeMixin):
541 _name = 'mrp.repair.fee'
542 _description = 'Repair Fees line'
543 def copy(self, cr, uid, id, default=None, context=None):
544 if not default: default = {}
545 default.update( {'invoice_line_id':False,'invoiced':False})
546 return super(mrp_repair_fee, self).copy(cr, uid, id, default, context)
547 def _amount_line(self, cr, uid, ids, field_name, arg, context):
549 cur_obj=self.pool.get('res.currency')
550 for line in self.browse(cr, uid, ids):
551 res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0
552 cur = line.repair_id.pricelist_id.currency_id
553 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
557 'repair_id': fields.many2one('mrp.repair', 'Repair Order Ref', required=True, ondelete='cascade', select=True),
558 'name': fields.char('Description', size=64, select=True,required=True),
559 'product_id': fields.many2one('product.product', 'Product'),
560 'product_uom_qty': fields.float('Quantity', digits=(16,2), required=True),
561 'price_unit': fields.float('Unit Price', required=True),
562 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
563 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal',digits=(16, int(config['price_accuracy']))),
564 'tax_id': fields.many2many('account.tax', 'repair_fee_line_tax', 'repair_fee_line_id', 'tax_id', 'Taxes'),
565 'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
566 'to_invoice': fields.boolean('To Invoice'),
567 'invoiced': fields.boolean('Invoiced',readonly=True),
570 'to_invoice': lambda *a: True,
574 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: