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 ##############################################################################
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 _
29 import decimal_precision as dp
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 Reference',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,
92 help=' * The \'Draft\' state is used when a user is encoding a new and unconfirmed repair order. \
93 \n* The \'Confirmed\' state is used when a user confirms the repair order. \
94 \n* The \'Ready to Repair\' state is used to start to repairing, user can start repairing only after repair order is confirmed. \
95 \n* The \'To be Invoiced\' state is used to generate the invoice before or after repairing done. \
96 \n* The \'Done\' state is set when repairing is completed.\
97 \n* The \'Cancelled\' state is used when user cancel repair order.'),
98 'location_id': fields.many2one('stock.location', 'Current Location', select=True, readonly=True, states={'draft':[('readonly',False)]}),
99 'location_dest_id': fields.many2one('stock.location', 'Delivery Location', readonly=True, states={'draft':[('readonly',False)]}),
100 'move_id': fields.many2one('stock.move', 'Move',required=True, domain="[('product_id','=',product_id)]", readonly=True, states={'draft':[('readonly',False)]}),
101 'guarantee_limit': fields.date('Guarantee limit', help="The guarantee limit is computed as: last move date + warranty defined on selected product. If the current date is below the guarantee limit, each operation and fee you will add will be set as 'not to invoiced' by default. Note that you can change manually afterwards."),
102 'operations' : fields.one2many('mrp.repair.line', 'repair_id', 'Operation Lines', readonly=True, states={'draft':[('readonly',False)]}),
103 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', help='The pricelist comes from the selected partner, by default.'),
104 'partner_invoice_id':fields.many2one('res.partner.address', 'Invoicing Address', domain="[('partner_id','=',partner_id)]"),
105 'invoice_method':fields.selection([
106 ("none","No Invoice"),
107 ("b4repair","Before Repair"),
108 ("after_repair","After Repair")
110 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.'),
111 'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True),
112 'picking_id': fields.many2one('stock.picking', 'Picking',readonly=True),
113 'fees_lines' : fields.one2many('mrp.repair.fee', 'repair_id', 'Fees Lines', readonly=True, states={'draft':[('readonly',False)]}),
114 'internal_notes' : fields.text('Internal Notes'),
115 'quotation_notes' : fields.text('Quotation Notes'),
116 '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."),
117 'invoiced': fields.boolean('Invoiced', readonly=True),
118 'repaired' : fields.boolean('Repaired', readonly=True),
119 'amount_untaxed': fields.function(_amount_untaxed, method=True, string='Untaxed Amount'),
120 'amount_tax': fields.function(_amount_tax, method=True, string='Taxes'),
121 'amount_total': fields.function(_amount_total, method=True, string='Total'),
125 'state': lambda *a: 'draft',
126 'deliver_bool': lambda *a: True,
127 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'mrp.repair'),
128 'invoice_method': lambda *a: 'none',
129 'pricelist_id': lambda self, cr, uid,context : self.pool.get('product.pricelist').search(cr,uid,[('type','=','sale')])[0]
132 def copy(self, cr, uid, id, default=None, context=None):
141 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.repair'),
143 return super(mrp_repair, self).copy(cr, uid, id, default, context)
146 def onchange_product_id(self, cr, uid, ids, product_id=None):
150 'guarantee_limit' :False,
151 'location_id': False,
152 'location_dest_id': False,
156 def onchange_move_id(self, cr, uid, ids, prod_id=False, move_id=False):
162 move = self.pool.get('stock.move').browse(cr, uid, move_id)
163 product = self.pool.get('product.product').browse(cr, uid, prod_id)
164 date = move.date_planned
165 limit = mx.DateTime.strptime(date, '%Y-%m-%d %H:%M:%S') + RelativeDateTime(months=product.warranty)
166 data['value']['guarantee_limit'] = limit.strftime('%Y-%m-%d')
167 data['value']['location_id'] = move.location_dest_id.id
168 data['value']['location_dest_id'] = move.location_dest_id.id
170 data['value']['partner_id'] = move.address_id.partner_id and move.address_id.partner_id.id
172 data['value']['partner_id'] = False
173 data['value']['address_id'] = move.address_id and move.address_id.id
174 d = self.onchange_partner_id(cr, uid, ids, data['value']['partner_id'], data['value']['address_id'])
175 data['value'].update(d['value'])
178 def button_dummy(self, cr, uid, ids, context=None):
181 def onchange_partner_id(self, cr, uid, ids, part, address_id):
185 'partner_invoice_id': False,
186 'pricelist_id': self.pool.get('product.pricelist').search(cr,uid,[('type','=','sale')])[0]
189 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'default'])
190 partner = self.pool.get('res.partner').browse(cr, uid, part)
191 pricelist = partner.property_product_pricelist and partner.property_product_pricelist.id or False
193 'address_id': address_id or addr['delivery'],
194 'partner_invoice_id': addr['invoice'],
195 'pricelist_id': pricelist
199 def onchange_lot_id(self, cr, uid, ids, lot, product_id):
202 'location_id': False,
203 'location_dest_id': False,
205 'guarantee_limit': False
210 lot_info = self.pool.get('stock.production.lot').browse(cr, uid, lot)
211 move_ids = self.pool.get('stock.move').search(cr, uid, [('prodlot_id', '=', lot)])
213 if not len(move_ids):
216 def get_last_move(lst_move):
217 while lst_move.move_dest_id and lst_move.move_dest_id.state == 'done':
218 lst_move = lst_move.move_dest_id
221 move_id = move_ids[0]
222 move = get_last_move(self.pool.get('stock.move').browse(cr, uid, move_id))
223 data['value']['move_id'] = move.id
224 d = self.onchange_move_id(cr, uid, ids, product_id, move.id)
225 data['value'].update(d['value'])
228 def action_cancel_draft(self, cr, uid, ids, *args):
231 mrp_line_obj = self.pool.get('mrp.repair.line')
232 for repair in self.browse(cr, uid, ids):
233 mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'draft'})
234 self.write(cr, uid, ids, {'state':'draft'})
235 wf_service = netsvc.LocalService("workflow")
237 wf_service.trg_create(uid, 'mrp.repair', id, cr)
240 def action_confirm(self, cr, uid, ids, *args):
241 mrp_line_obj = self.pool.get('mrp.repair.line')
242 for o in self.browse(cr, uid, ids):
243 if (o.invoice_method == 'b4repair'):
244 self.write(cr, uid, [o.id], {'state': '2binvoiced'})
246 self.write(cr, uid, [o.id], {'state': 'confirmed'})
247 mrp_line_obj.write(cr, uid, [l.id for l in o.operations], {'state': 'confirmed'})
250 def action_cancel(self, cr, uid, ids, context=None):
252 mrp_line_obj = self.pool.get('mrp.repair.line')
253 for repair in self.browse(cr, uid, ids):
254 mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'cancel'})
255 self.write(cr,uid,ids,{'state':'cancel'})
258 def wkf_invoice_create(self, cr, uid, ids, *args):
259 return self.action_invoice_create(cr, uid, ids)
261 def action_invoice_create(self, cr, uid, ids, group=False, context=None):
264 for repair in self.browse(cr, uid, ids, context=context):
266 if repair.state in ('draft','cancel') or repair.invoice_id:
268 if not (repair.partner_id.id and repair.partner_invoice_id.id):
269 raise osv.except_osv(_('No partner !'),_('You have to select a Partner Invoice Address in the repair form !'))
270 comment=repair.quotation_notes
271 if (repair.invoice_method != 'none'):
272 if group and repair.partner_invoice_id.id in invoices_group:
273 inv_id= invoices_group[repair.partner_invoice_id.id]
274 invoice=invoice_obj.browse(cr, uid,inv_id)
276 'name': invoice.name +', '+repair.name,
277 'origin': invoice.origin+', '+repair.name,
278 'comment':(comment and (invoice.comment and invoice.comment+"\n"+comment or comment)) or (invoice.comment and invoice.comment or ''),
280 invoice_obj.write(cr, uid, [inv_id],invoice_vals,context=context)
282 a = repair.partner_id.property_account_receivable.id
285 'origin':repair.name,
286 'type': 'out_invoice',
288 'partner_id': repair.partner_id.id,
289 'address_invoice_id': repair.address_id.id,
290 'currency_id': repair.pricelist_id.currency_id.id,
291 'comment': repair.quotation_notes,
292 'fiscal_position': repair.partner_id.property_account_position.id
294 inv_obj = self.pool.get('account.invoice')
295 inv_id = inv_obj.create(cr, uid, inv)
296 invoices_group[repair.partner_invoice_id.id] = inv_id
297 self.write(cr, uid, repair.id , {'invoiced':True,'invoice_id' : inv_id})
299 for operation in repair.operations:
300 if operation.to_invoice == True:
302 name = repair.name + '-' + operation.name
304 name = operation.name
305 invoice_line_id=self.pool.get('account.invoice.line').create(cr, uid, {
306 'invoice_id': inv_id,
308 'origin':repair.name,
309 'account_id': operation.product_id and operation.product_id.property_account_income and operation.product_id.property_account_income.id,
310 'quantity' : operation.product_uom_qty,
311 'invoice_line_tax_id': [(6,0,[x.id for x in operation.tax_id])],
312 'uos_id' : operation.product_uom.id,
313 'price_unit' : operation.price_unit,
314 'price_subtotal' : operation.product_uom_qty*operation.price_unit,
315 'product_id' : operation.product_id and operation.product_id.id or False
317 self.pool.get('mrp.repair.line').write(cr, uid, [operation.id], {'invoiced':True,'invoice_line_id':invoice_line_id})
318 for fee in repair.fees_lines:
319 if fee.to_invoice == True:
321 name = repair.name + '-' + fee.name
324 invoice_fee_id=self.pool.get('account.invoice.line').create(cr, uid, {
325 'invoice_id': inv_id,
327 'origin':repair.name,
329 'quantity': fee.product_uom_qty,
330 'invoice_line_tax_id': [(6,0,[x.id for x in fee.tax_id])],
331 'uos_id': fee.product_uom.id,
332 'product_id': fee.product_id and fee.product_id.id or False,
333 'price_unit': fee.price_unit,
334 'price_subtotal': fee.product_uom_qty*fee.price_unit
336 self.pool.get('mrp.repair.fee').write(cr, uid, [fee.id], {'invoiced':True,'invoice_line_id':invoice_fee_id})
337 res[repair.id]=inv_id
338 #self.action_invoice_end(cr, uid, ids)
341 def action_repair_ready(self, cr, uid, ids, context=None):
342 self.write(cr, uid, ids, {'state':'ready'})
345 def action_invoice_cancel(self, cr, uid, ids, context=None):
346 self.write(cr, uid, ids, {'state':'invoice_except'})
349 def action_repair_start(self, cr, uid, ids, context=None):
350 self.write(cr, uid, ids, {'state':'under_repair'})
353 def action_invoice_end(self, cr, uid, ids, context=None):
354 for order in self.browse(cr, uid, ids):
356 if (order.invoice_method=='b4repair'):
357 val['state'] = 'ready'
359 #val['state'] = 'done'
361 self.write(cr, uid, [order.id], val)
364 def action_repair_end(self, cr, uid, ids, context=None):
365 for order in self.browse(cr, uid, ids):
368 if (not order.invoiced and order.invoice_method=='after_repair'):
369 val['state'] = '2binvoiced'
370 elif (not order.invoiced and order.invoice_method=='b4repair'):
371 val['state'] = 'ready'
373 #val['state'] = 'done'
375 self.write(cr, uid, [order.id], val)
378 def wkf_repair_done(self, cr, uid, ids, *args):
379 res=self.action_repair_done(cr,uid,ids)
382 def action_repair_done(self, cr, uid, ids, context=None):
384 company = self.pool.get('res.users').browse(cr, uid, uid).company_id
385 for repair in self.browse(cr, uid, ids, context=context):
386 for move in repair.operations:
387 move_id = self.pool.get('stock.move').create(cr, uid, {
389 'product_id': move.product_id.id,
390 'product_qty': move.product_uom_qty,
391 'product_uom': move.product_uom.id,
392 'address_id': repair.address_id and repair.address_id.id or False,
393 'location_id': move.location_id.id,
394 'location_dest_id': move.location_dest_id.id,
395 'tracking_id': False,
398 self.pool.get('mrp.repair.line').write(cr, uid, [move.id], {'move_id': move_id})
400 if repair.deliver_bool:
401 picking = self.pool.get('stock.picking').create(cr, uid, {
402 'origin': repair.name,
405 'address_id': repair.address_id and repair.address_id.id or False,
406 'note': repair.internal_notes,
407 'invoice_state': 'none',
410 wf_service = netsvc.LocalService("workflow")
411 wf_service.trg_validate(uid, 'stock.picking', picking, 'button_confirm', cr)
413 move_id = self.pool.get('stock.move').create(cr, uid, {
415 'picking_id': picking,
416 'product_id': repair.product_id.id,
418 'product_uom': repair.product_id.uom_id.id,
419 #'product_uos_qty': line.product_uom_qty,
420 #'product_uos': line.product_uom.id,
421 'prodlot_id': repair.prodlot_id and repair.prodlot_id.id or False,
422 'address_id': repair.address_id and repair.address_id.id or False,
423 'location_id': repair.location_id.id,
424 'location_dest_id': repair.location_dest_id.id,
425 'tracking_id': False,
426 'state': 'assigned', # FIXME done ?
428 self.write(cr, uid, [repair.id], {'state':'done', 'picking_id':picking})
429 res[repair.id] = picking
431 self.write(cr, uid, [repair.id], {'state':'done'})
438 class ProductChangeMixin(object):
439 def product_id_change(self, cr, uid, ids, pricelist, product, uom=False, product_uom_qty=0, partner_id=False, guarantee_limit=False):
443 if not product_uom_qty:
445 result['product_uom_qty'] = product_uom_qty
448 product_obj = self.pool.get('product.product').browse(cr, uid, product)
450 partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
451 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, partner.property_account_position, product_obj.taxes_id)
453 result['name'] = product_obj.partner_ref
454 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id or False
457 'title':'No Pricelist !',
459 'You have to select a pricelist in the Repair form !\n'
460 'Please set one before choosing a product.'
463 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
464 product, product_uom_qty, partner_id, {'uom': uom,})[pricelist]
468 'title':'No valid pricelist line found !',
470 "Couldn't find a pricelist line matching this product and quantity.\n"
471 "You have to change either the product, the quantity or the pricelist."
474 result.update({'price_unit': price, 'price_subtotal' :price*product_uom_qty})
476 return {'value': result, 'warning': warning}
479 class mrp_repair_line(osv.osv, ProductChangeMixin):
480 _name = 'mrp.repair.line'
481 _description = 'Repair Operations Lines'
483 def copy_data(self, cr, uid, id, default=None, context=None):
484 if not default: default = {}
485 default.update( {'invoice_line_id':False,'move_id':False,'invoiced':False,'state':'draft'})
486 return super(mrp_repair_line, self).copy_data(cr, uid, id, default, context)
488 def _amount_line(self, cr, uid, ids, field_name, arg, context):
490 cur_obj=self.pool.get('res.currency')
491 for line in self.browse(cr, uid, ids):
492 res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0
493 cur = line.repair_id.pricelist_id.currency_id
494 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
498 'name' : fields.char('Description',size=64,required=True),
499 'repair_id': fields.many2one('mrp.repair', 'Repair Order Reference',ondelete='cascade', select=True),
500 'type': fields.selection([('add','Add'),('remove','Remove')],'Type', required=True),
501 'to_invoice': fields.boolean('To Invoice'),
502 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok','=',True)], required=True),
503 'invoiced': fields.boolean('Invoiced',readonly=True),
504 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price')),
505 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal',digits_compute= dp.get_precision('Sale Price')),
506 'tax_id': fields.many2many('account.tax', 'repair_operation_line_tax', 'repair_operation_line_id', 'tax_id', 'Taxes'),
507 'product_uom_qty': fields.float('Quantity (UoM)', digits=(16,2), required=True),
508 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
509 'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
510 'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True),
511 'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True, select=True),
512 'move_id': fields.many2one('stock.move', 'Inventory Move', readonly=True),
513 'state': fields.selection([
515 ('confirmed','Confirmed'),
517 ('cancel','Canceled')], 'State', required=True, readonly=True,
518 help=' * The \'Draft\' state is set automatically as draft when repair order in draft state. \
519 \n* The \'Confirmed\' state is set automatically as confirm when repair order in confirm state. \
520 \n* The \'Done\' state is set automatically when repair order is completed.\
521 \n* The \'Cancelled\' state is set automatically when user cancel repair order.'),
524 'state': lambda *a: 'draft',
525 'product_uom_qty':lambda *a:1,
528 def onchange_operation_type(self, cr, uid, ids, type, guarantee_limit):
531 'location_id': False,
532 'location_dest_id': False
535 produc_id = self.pool.get('stock.location').search(cr, uid, [('name','=','Production')])[0]
537 stock_id = self.pool.get('stock.location').search(cr, uid, [('name','=','Stock')])[0]
539 if guarantee_limit and today() > mx.DateTime.strptime(guarantee_limit, '%Y-%m-%d'):
542 'to_invoice': to_invoice,
543 'location_id': stock_id,
544 'location_dest_id' : produc_id
549 'location_id': produc_id,
550 'location_dest_id':False
556 class mrp_repair_fee(osv.osv, ProductChangeMixin):
557 _name = 'mrp.repair.fee'
558 _description = 'Repair Fees line'
559 def copy_data(self, cr, uid, id, default=None, context=None):
560 if not default: default = {}
561 default.update( {'invoice_line_id':False,'invoiced':False})
562 return super(mrp_repair_fee, self).copy_data(cr, uid, id, default, context)
563 def _amount_line(self, cr, uid, ids, field_name, arg, context):
565 cur_obj=self.pool.get('res.currency')
566 for line in self.browse(cr, uid, ids):
567 res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0
568 cur = line.repair_id.pricelist_id.currency_id
569 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
573 'repair_id': fields.many2one('mrp.repair', 'Repair Order Reference', required=True, ondelete='cascade', select=True),
574 'name': fields.char('Description', size=64, select=True,required=True),
575 'product_id': fields.many2one('product.product', 'Product'),
576 'product_uom_qty': fields.float('Quantity', digits=(16,2), required=True),
577 'price_unit': fields.float('Unit Price', required=True),
578 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
579 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal',digits_compute= dp.get_precision('Sale Price')),
580 'tax_id': fields.many2many('account.tax', 'repair_fee_line_tax', 'repair_fee_line_id', 'tax_id', 'Taxes'),
581 'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
582 'to_invoice': fields.boolean('To Invoice'),
583 'invoiced': fields.boolean('Invoiced',readonly=True),
586 'to_invoice': lambda *a: True,
590 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: