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 ##############################################################################
22 from osv import fields,osv
24 from datetime import datetime
25 from dateutil.relativedelta import relativedelta
26 from tools.translate import _
27 import decimal_precision as dp
29 class mrp_repair(osv.osv):
31 _inherit = 'mail.thread'
32 _description = 'Repair Order'
34 def _amount_untaxed(self, cr, uid, ids, field_name, arg, context=None):
35 """ Calculates untaxed amount.
36 @param self: The object pointer
37 @param cr: The current row, from the database cursor,
38 @param uid: The current user ID for security checks
39 @param ids: List of selected IDs
40 @param field_name: Name of field.
42 @param context: A standard dictionary for contextual values
43 @return: Dictionary of values.
46 cur_obj = self.pool.get('res.currency')
48 for repair in self.browse(cr, uid, ids, context=context):
50 for line in repair.operations:
51 res[repair.id] += line.price_subtotal
52 for line in repair.fees_lines:
53 res[repair.id] += line.price_subtotal
54 cur = repair.pricelist_id.currency_id
55 res[repair.id] = cur_obj.round(cr, uid, cur, res[repair.id])
58 def _amount_tax(self, cr, uid, ids, field_name, arg, context=None):
59 """ Calculates taxed amount.
60 @param field_name: Name of field.
62 @return: Dictionary of values.
65 #return {}.fromkeys(ids, 0)
66 cur_obj = self.pool.get('res.currency')
67 tax_obj = self.pool.get('account.tax')
68 for repair in self.browse(cr, uid, ids, context=context):
70 cur = repair.pricelist_id.currency_id
71 for line in repair.operations:
72 #manage prices with tax included use compute_all instead of compute
74 tax_calculate = tax_obj.compute_all(cr, uid, line.tax_id, line.price_unit, line.product_uom_qty, line.product_id, repair.partner_id)
75 for c in tax_calculate['taxes']:
77 for line in repair.fees_lines:
79 tax_calculate = tax_obj.compute_all(cr, uid, line.tax_id, line.price_unit, line.product_uom_qty, line.product_id, repair.partner_id)
80 for c in tax_calculate['taxes']:
82 res[repair.id] = cur_obj.round(cr, uid, cur, val)
85 def _amount_total(self, cr, uid, ids, field_name, arg, context=None):
86 """ Calculates total amount.
87 @param field_name: Name of field.
89 @return: Dictionary of values.
92 untax = self._amount_untaxed(cr, uid, ids, field_name, arg, context=context)
93 tax = self._amount_tax(cr, uid, ids, field_name, arg, context=context)
94 cur_obj = self.pool.get('res.currency')
96 repair = self.browse(cr, uid, id, context=context)
97 cur = repair.pricelist_id.currency_id
98 res[id] = cur_obj.round(cr, uid, cur, untax.get(id, 0.0) + tax.get(id, 0.0))
101 def _get_default_address(self, cr, uid, ids, field_name, arg, context=None):
103 partner_obj = self.pool.get('res.partner')
104 for data in self.browse(cr, uid, ids, context=context):
107 adr_id = partner_obj.address_get(cr, uid, [data.partner_id.id], ['default'])['default']
108 res[data.id] = adr_id
111 def _get_lines(self, cr, uid, ids, context=None):
113 for line in self.pool.get('mrp.repair.line').browse(cr, uid, ids, context=context):
114 result[line.repair_id.id] = True
118 'name': fields.char('Repair Reference',size=24, required=True),
119 'product_id': fields.many2one('product.product', string='Product to Repair', required=True, readonly=True, states={'draft':[('readonly',False)]}),
120 'partner_id' : fields.many2one('res.partner', 'Partner', select=True, help='Choose partner for whom the order will be invoiced and delivered.'),
121 'address_id': fields.many2one('res.partner', 'Delivery Address', domain="[('parent_id','=',partner_id)]"),
122 'default_address_id': fields.function(_get_default_address, type="many2one", relation="res.partner"),
123 'prodlot_id': fields.many2one('stock.production.lot', 'Lot Number', select=True, states={'draft':[('readonly',False)]},domain="[('product_id','=',product_id)]"),
124 'state': fields.selection([
125 ('draft','Quotation'),
126 ('cancel','Cancelled'),
127 ('confirmed','Confirmed'),
128 ('under_repair','Under Repair'),
129 ('ready','Ready to Repair'),
130 ('2binvoiced','To be Invoiced'),
131 ('invoice_except','Invoice Exception'),
133 ], 'Status', readonly=True,
134 help=' * The \'Draft\' state is used when a user is encoding a new and unconfirmed repair order. \
135 \n* The \'Confirmed\' state is used when a user confirms the repair order. \
136 \n* The \'Ready to Repair\' state is used to start to repairing, user can start repairing only after repair order is confirmed. \
137 \n* The \'To be Invoiced\' state is used to generate the invoice before or after repairing done. \
138 \n* The \'Done\' state is set when repairing is completed.\
139 \n* The \'Cancelled\' state is used when user cancel repair order.'),
140 'location_id': fields.many2one('stock.location', 'Current Location', select=True, readonly=True, states={'draft':[('readonly',False)]}),
141 'location_dest_id': fields.many2one('stock.location', 'Delivery Location', readonly=True, states={'draft':[('readonly',False)]}),
142 'move_id': fields.many2one('stock.move', 'Move',required=True, domain="[('product_id','=',product_id)]", readonly=True, states={'draft':[('readonly',False)]}),
143 '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."),
144 'operations' : fields.one2many('mrp.repair.line', 'repair_id', 'Operation Lines', readonly=True, states={'draft':[('readonly',False)]}),
145 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', help='Pricelist of the selected partner.'),
146 'partner_invoice_id':fields.many2one('res.partner', 'Invoicing Address'),
147 'invoice_method':fields.selection([
148 ("none","No Invoice"),
149 ("b4repair","Before Repair"),
150 ("after_repair","After Repair")
152 select=True, required=True, states={'draft':[('readonly',False)]}, readonly=True, help='Selecting \'Before Repair\' or \'After Repair\' will allow you to generate invoice before or after the repair is done respectively. \'No invoice\' means you don\'t want to generate invoice for this repair order.'),
153 'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True),
154 'picking_id': fields.many2one('stock.picking', 'Picking',readonly=True),
155 'fees_lines': fields.one2many('mrp.repair.fee', 'repair_id', 'Fees Lines', readonly=True, states={'draft':[('readonly',False)]}),
156 'internal_notes': fields.text('Internal Notes'),
157 'quotation_notes': fields.text('Quotation Notes'),
158 'company_id': fields.many2one('res.company', 'Company'),
159 'deliver_bool': fields.boolean('Deliver', help="Check this box if you want to manage the delivery once the product is repaired and create a picking with selected product. Note that you can select the locations in the Info tab, if you have the extended view."),
160 'invoiced': fields.boolean('Invoiced', readonly=True),
161 'repaired': fields.boolean('Repaired', readonly=True),
162 'amount_untaxed': fields.function(_amount_untaxed, string='Untaxed Amount',
164 'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations'], 10),
165 'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
167 'amount_tax': fields.function(_amount_tax, string='Taxes',
169 'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations'], 10),
170 'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
172 'amount_total': fields.function(_amount_total, string='Total',
174 'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations'], 10),
175 'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
180 'state': lambda *a: 'draft',
181 'deliver_bool': lambda *a: True,
182 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'mrp.repair'),
183 'invoice_method': lambda *a: 'none',
184 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.repair', context=context),
185 'pricelist_id': lambda self, cr, uid,context : self.pool.get('product.pricelist').search(cr, uid, [('type','=','sale')])[0]
188 def copy(self, cr, uid, id, default=None, context=None):
197 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.repair'),
199 return super(mrp_repair, self).copy(cr, uid, id, default, context)
201 def onchange_product_id(self, cr, uid, ids, product_id=None):
202 """ On change of product sets some values.
203 @param product_id: Changed product
204 @return: Dictionary of values.
209 'guarantee_limit' :False,
210 'location_id': False,
211 'location_dest_id': False,
215 def onchange_move_id(self, cr, uid, ids, prod_id=False, move_id=False):
216 """ On change of move id sets values of guarantee limit, source location,
217 destination location, partner and partner address.
218 @param prod_id: Id of product in current record.
219 @param move_id: Changed move.
220 @return: Dictionary of values.
223 data['value'] = {'guarantee_limit': False, 'location_id': False, 'prodlot_id': False, 'partner_id': False}
227 move = self.pool.get('stock.move').browse(cr, uid, move_id)
228 product = self.pool.get('product.product').browse(cr, uid, prod_id)
229 limit = datetime.strptime(move.date_expected, '%Y-%m-%d %H:%M:%S') + relativedelta(months=int(product.warranty))
230 data['value']['guarantee_limit'] = limit.strftime('%Y-%m-%d')
231 data['value']['location_id'] = move.location_dest_id.id
232 data['value']['location_dest_id'] = move.location_dest_id.id
233 data['value']['prodlot_id'] = move.prodlot_id.id
235 data['value']['partner_id'] = move.partner_id.id
237 data['value']['partner_id'] = False
238 d = self.onchange_partner_id(cr, uid, ids, data['value']['partner_id'], data['value']['partner_id'])
239 data['value'].update(d['value'])
242 def button_dummy(self, cr, uid, ids, context=None):
245 def onchange_partner_id(self, cr, uid, ids, part, address_id):
246 """ On change of partner sets the values of partner address,
247 partner invoice address and pricelist.
248 @param part: Changed id of partner.
249 @param address_id: Address id from current record.
250 @return: Dictionary of values.
252 part_obj = self.pool.get('res.partner')
253 pricelist_obj = self.pool.get('product.pricelist')
257 'partner_invoice_id': False,
258 'pricelist_id': pricelist_obj.search(cr, uid, [('type','=','sale')])[0]
261 addr = part_obj.address_get(cr, uid, [part], ['delivery', 'invoice', 'default'])
262 partner = part_obj.browse(cr, uid, part)
263 pricelist = partner.property_product_pricelist and partner.property_product_pricelist.id or False
265 'address_id': addr['delivery'] or addr['default'],
266 'partner_invoice_id': addr['invoice'],
267 'pricelist_id': pricelist
271 def onchange_lot_id(self, cr, uid, ids, lot, product_id):
272 """ On change of Serial Number sets the values of source location,
273 destination location, move and guarantee limit.
274 @param lot: Changed id of Serial Number.
275 @param product_id: Product id from current record.
276 @return: Dictionary of values.
278 move_obj = self.pool.get('stock.move')
281 'location_id': False,
282 'location_dest_id': False,
284 'guarantee_limit': False
289 move_ids = move_obj.search(cr, uid, [('prodlot_id', '=', lot)])
291 if not len(move_ids):
294 def get_last_move(lst_move):
295 while lst_move.move_dest_id and lst_move.move_dest_id.state == 'done':
296 lst_move = lst_move.move_dest_id
299 move_id = move_ids[0]
300 move = get_last_move(move_obj.browse(cr, uid, move_id))
301 data['value']['move_id'] = move.id
302 d = self.onchange_move_id(cr, uid, ids, product_id, move.id)
303 data['value'].update(d['value'])
306 def action_cancel_draft(self, cr, uid, ids, *args):
307 """ Cancels repair order when it is in 'Draft' state.
308 @param *arg: Arguments
313 mrp_line_obj = self.pool.get('mrp.repair.line')
314 for repair in self.browse(cr, uid, ids):
315 mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'draft'})
316 self.write(cr, uid, ids, {'state':'draft'})
317 wf_service = netsvc.LocalService("workflow")
319 wf_service.trg_create(uid, 'mrp.repair', id, cr)
322 def action_confirm(self, cr, uid, ids, *args):
323 """ Repair order state is set to 'To be invoiced' when invoice method
324 is 'Before repair' else state becomes 'Confirmed'.
325 @param *arg: Arguments
328 mrp_line_obj = self.pool.get('mrp.repair.line')
329 for o in self.browse(cr, uid, ids):
330 if (o.invoice_method == 'b4repair'):
331 self.write(cr, uid, [o.id], {'state': '2binvoiced'})
333 self.write(cr, uid, [o.id], {'state': 'confirmed'})
334 for line in o.operations:
335 if line.product_id.track_production and not line.prodlot_id:
336 raise osv.except_osv(_('Warning!'), _("Serial number is required for operation line with product '%s'") % (line.product_id.name))
337 mrp_line_obj.write(cr, uid, [l.id for l in o.operations], {'state': 'confirmed'})
338 self.set_confirm_send_note(cr, uid, ids)
341 def action_cancel(self, cr, uid, ids, context=None):
342 """ Cancels repair order.
345 mrp_line_obj = self.pool.get('mrp.repair.line')
346 for repair in self.browse(cr, uid, ids, context=context):
347 if not repair.invoiced:
348 mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'cancel'}, context=context)
350 raise osv.except_osv(_('Warning!'),_('Repair order is already invoiced.'))
351 self.write(cr,uid,ids,{'state':'cancel'})
352 self.set_cancel_send_note(cr, uid, ids, context)
355 def wkf_invoice_create(self, cr, uid, ids, *args):
356 self.action_invoice_create(cr, uid, ids)
357 self.set_toinvoiced_send_note(cr, uid, ids)
360 def action_invoice_create(self, cr, uid, ids, group=False, context=None):
361 """ Creates invoice(s) for repair order.
362 @param group: It is set to true when group invoice is to be generated.
363 @return: Invoice Ids.
367 inv_line_obj = self.pool.get('account.invoice.line')
368 inv_obj = self.pool.get('account.invoice')
369 repair_line_obj = self.pool.get('mrp.repair.line')
370 repair_fee_obj = self.pool.get('mrp.repair.fee')
371 for repair in self.browse(cr, uid, ids, context=context):
372 res[repair.id] = False
373 if repair.state in ('draft','cancel') or repair.invoice_id:
375 if not (repair.partner_id.id and repair.partner_invoice_id.id):
376 raise osv.except_osv(_('No partner !'),_('You have to select a Partner Invoice Address in the repair form !'))
377 comment = repair.quotation_notes
378 if (repair.invoice_method != 'none'):
379 if group and repair.partner_invoice_id.id in invoices_group:
380 inv_id = invoices_group[repair.partner_invoice_id.id]
381 invoice = inv_obj.browse(cr, uid, inv_id)
383 'name': invoice.name +', '+repair.name,
384 'origin': invoice.origin+', '+repair.name,
385 'comment':(comment and (invoice.comment and invoice.comment+"\n"+comment or comment)) or (invoice.comment and invoice.comment or ''),
387 inv_obj.write(cr, uid, [inv_id], invoice_vals, context=context)
389 if not repair.partner_id.property_account_receivable:
390 raise osv.except_osv(_('Error!'), _('No account defined for partner "%s".') % repair.partner_id.name )
391 account_id = repair.partner_id.property_account_receivable.id
394 'origin':repair.name,
395 'type': 'out_invoice',
396 'account_id': account_id,
397 'partner_id': repair.partner_id.id,
398 'currency_id': repair.pricelist_id.currency_id.id,
399 'comment': repair.quotation_notes,
400 'fiscal_position': repair.partner_id.property_account_position.id
402 inv_id = inv_obj.create(cr, uid, inv)
403 invoices_group[repair.partner_invoice_id.id] = inv_id
404 self.write(cr, uid, repair.id, {'invoiced': True, 'invoice_id': inv_id})
406 for operation in repair.operations:
407 if operation.to_invoice == True:
409 name = repair.name + '-' + operation.name
411 name = operation.name
413 if operation.product_id.property_account_income:
414 account_id = operation.product_id.property_account_income.id
415 elif operation.product_id.categ_id.property_account_income_categ:
416 account_id = operation.product_id.categ_id.property_account_income_categ.id
418 raise osv.except_osv(_('Error!'), _('No account defined for product "%s".') % operation.product_id.name )
420 invoice_line_id = inv_line_obj.create(cr, uid, {
421 'invoice_id': inv_id,
423 'origin': repair.name,
424 'account_id': account_id,
425 'quantity': operation.product_uom_qty,
426 'invoice_line_tax_id': [(6,0,[x.id for x in operation.tax_id])],
427 'uos_id': operation.product_uom.id,
428 'price_unit': operation.price_unit,
429 'price_subtotal': operation.product_uom_qty*operation.price_unit,
430 'product_id': operation.product_id and operation.product_id.id or False
432 repair_line_obj.write(cr, uid, [operation.id], {'invoiced': True, 'invoice_line_id': invoice_line_id})
433 for fee in repair.fees_lines:
434 if fee.to_invoice == True:
436 name = repair.name + '-' + fee.name
439 if not fee.product_id:
440 raise osv.except_osv(_('Warning!'), _('No product defined on Fees!'))
442 if fee.product_id.property_account_income:
443 account_id = fee.product_id.property_account_income.id
444 elif fee.product_id.categ_id.property_account_income_categ:
445 account_id = fee.product_id.categ_id.property_account_income_categ.id
447 raise osv.except_osv(_('Error!'), _('No account defined for product "%s".') % fee.product_id.name)
449 invoice_fee_id = inv_line_obj.create(cr, uid, {
450 'invoice_id': inv_id,
452 'origin': repair.name,
453 'account_id': account_id,
454 'quantity': fee.product_uom_qty,
455 'invoice_line_tax_id': [(6,0,[x.id for x in fee.tax_id])],
456 'uos_id': fee.product_uom.id,
457 'product_id': fee.product_id and fee.product_id.id or False,
458 'price_unit': fee.price_unit,
459 'price_subtotal': fee.product_uom_qty*fee.price_unit
461 repair_fee_obj.write(cr, uid, [fee.id], {'invoiced': True, 'invoice_line_id': invoice_fee_id})
462 res[repair.id] = inv_id
465 def action_repair_ready(self, cr, uid, ids, context=None):
466 """ Writes repair order state to 'Ready'
469 for repair in self.browse(cr, uid, ids, context=context):
470 self.pool.get('mrp.repair.line').write(cr, uid, [l.id for
471 l in repair.operations], {'state': 'confirmed'}, context=context)
472 self.write(cr, uid, [repair.id], {'state': 'ready'})
473 self.set_ready_send_note(cr, uid, ids, context)
476 def action_repair_start(self, cr, uid, ids, context=None):
477 """ Writes repair order state to 'Under Repair'
480 repair_line = self.pool.get('mrp.repair.line')
481 for repair in self.browse(cr, uid, ids, context=context):
482 repair_line.write(cr, uid, [l.id for
483 l in repair.operations], {'state': 'confirmed'}, context=context)
484 repair.write({'state': 'under_repair'})
485 self.set_start_send_note(cr, uid, ids, context)
488 def action_repair_end(self, cr, uid, ids, context=None):
489 """ Writes repair order state to 'To be invoiced' if invoice method is
490 After repair else state is set to 'Ready'.
493 for order in self.browse(cr, uid, ids, context=context):
495 val['repaired'] = True
496 if (not order.invoiced and order.invoice_method=='after_repair'):
497 val['state'] = '2binvoiced'
498 elif (not order.invoiced and order.invoice_method=='b4repair'):
499 val['state'] = 'ready'
502 self.write(cr, uid, [order.id], val)
505 def wkf_repair_done(self, cr, uid, ids, *args):
506 self.action_repair_done(cr, uid, ids)
509 def action_repair_done(self, cr, uid, ids, context=None):
510 """ Creates stock move and picking for repair order.
511 @return: Picking ids.
514 move_obj = self.pool.get('stock.move')
515 wf_service = netsvc.LocalService("workflow")
516 repair_line_obj = self.pool.get('mrp.repair.line')
517 seq_obj = self.pool.get('ir.sequence')
518 pick_obj = self.pool.get('stock.picking')
519 for repair in self.browse(cr, uid, ids, context=context):
520 for move in repair.operations:
521 move_id = move_obj.create(cr, uid, {
523 'product_id': move.product_id.id,
524 'product_qty': move.product_uom_qty,
525 'product_uom': move.product_uom.id,
526 'partner_id': repair.address_id and repair.address_id.id or False,
527 'location_id': move.location_id.id,
528 'location_dest_id': move.location_dest_id.id,
529 'tracking_id': False,
530 'prodlot_id': move.prodlot_id and move.prodlot_id.id or False,
533 repair_line_obj.write(cr, uid, [move.id], {'move_id': move_id, 'state': 'done'}, context=context)
534 if repair.deliver_bool:
535 pick_name = seq_obj.get(cr, uid, 'stock.picking.out')
536 picking = pick_obj.create(cr, uid, {
538 'origin': repair.name,
541 'partner_id': repair.address_id and repair.address_id.id or False,
542 'note': repair.internal_notes,
543 'invoice_state': 'none',
546 move_id = move_obj.create(cr, uid, {
548 'picking_id': picking,
549 'product_id': repair.product_id.id,
550 'product_uom': repair.product_id.uom_id.id,
551 'prodlot_id': repair.prodlot_id and repair.prodlot_id.id or False,
552 'partner_id': repair.address_id and repair.address_id.id or False,
553 'location_id': repair.location_id.id,
554 'location_dest_id': repair.location_dest_id.id,
555 'tracking_id': False,
558 wf_service.trg_validate(uid, 'stock.picking', picking, 'button_confirm', cr)
559 self.write(cr, uid, [repair.id], {'state': 'done', 'picking_id': picking})
560 res[repair.id] = picking
562 self.write(cr, uid, [repair.id], {'state': 'done'})
563 self.set_done_send_note(cr, uid, [repair.id], context)
566 def create(self, cr, uid, vals, context=None):
567 repair_id = super(mrp_repair, self).create(cr, uid, vals, context=context)
568 self.create_send_note(cr, uid, [repair_id], context=context)
571 def create_send_note(self, cr, uid, ids, context=None):
572 for repair in self.browse(cr, uid, ids, context):
573 message = _("Repair Order for <em>%s</em> has been <b>created</b>." % (repair.product_id.name))
574 self.message_append_note(cr, uid, [repair.id], body=message, context=context)
577 def set_start_send_note(self, cr, uid, ids, context=None):
578 for repair in self.browse(cr, uid, ids, context):
579 message = _("Repair Order for <em>%s</em> has been <b>started</b>." % (repair.product_id.name))
580 self.message_append_note(cr, uid, [repair.id], body=message, context=context)
583 def set_toinvoiced_send_note(self, cr, uid, ids, context=None):
584 for repair in self.browse(cr, uid, ids, context):
585 message = _("Draft Invoice of %s %s <b>waiting for validation</b>.") % (repair.invoice_id.amount_total, repair.invoice_id.currency_id.symbol)
586 self.message_append_note(cr, uid, [repair.id], body=message, context=context)
589 def set_confirm_send_note(self, cr, uid, ids, context=None):
590 for repair in self.browse(cr, uid, ids, context):
591 message = _( "Repair Order for <em>%s</em> has been <b>accepted</b>." % (repair.product_id.name))
592 self.message_append_note(cr, uid, [repair.id], body=message, context=context)
595 def set_cancel_send_note(self, cr, uid, ids, context=None):
596 message = _("Repair has been <b>cancelled</b>.")
597 self.message_append_note(cr, uid, ids, body=message, context=context)
600 def set_ready_send_note(self, cr, uid, ids, context=None):
601 message = _("Repair Order is now <b>ready</b> to repair.")
602 self.message_append_note(cr, uid, ids, body=message, context=context)
605 def set_done_send_note(self, cr, uid, ids, context=None):
606 message = _("Repair Order is <b>closed</b>.")
607 self.message_append_note(cr, uid, ids, body=message, context=context)
613 class ProductChangeMixin(object):
614 def product_id_change(self, cr, uid, ids, pricelist, product, uom=False,
615 product_uom_qty=0, partner_id=False, guarantee_limit=False):
616 """ On change of product it sets product quantity, tax account, name,
617 uom of product, unit price and price subtotal.
618 @param pricelist: Pricelist of current record.
619 @param product: Changed id of product.
620 @param uom: UoM of current record.
621 @param product_uom_qty: Quantity of current record.
622 @param partner_id: Partner of current record.
623 @param guarantee_limit: Guarantee limit of current record.
624 @return: Dictionary of values and warning message.
629 if not product_uom_qty:
631 result['product_uom_qty'] = product_uom_qty
634 product_obj = self.pool.get('product.product').browse(cr, uid, product)
636 partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
637 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, partner.property_account_position, product_obj.taxes_id)
639 result['name'] = product_obj.partner_ref
640 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id or False
643 'title':'No Pricelist !',
645 'You have to select a pricelist in the Repair form !\n'
646 'Please set one before choosing a product.'
649 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
650 product, product_uom_qty, partner_id, {'uom': uom,})[pricelist]
654 'title':'No valid pricelist line found !',
656 "Couldn't find a pricelist line matching this product and quantity.\n"
657 "You have to change either the product, the quantity or the pricelist."
660 result.update({'price_unit': price, 'price_subtotal': price*product_uom_qty})
662 return {'value': result, 'warning': warning}
665 class mrp_repair_line(osv.osv, ProductChangeMixin):
666 _name = 'mrp.repair.line'
667 _description = 'Repair Line'
669 def copy_data(self, cr, uid, id, default=None, context=None):
670 if not default: default = {}
671 default.update( {'invoice_line_id': False, 'move_id': False, 'invoiced': False, 'state': 'draft'})
672 return super(mrp_repair_line, self).copy_data(cr, uid, id, default, context)
674 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
675 """ Calculates amount.
676 @param field_name: Name of field.
678 @return: Dictionary of values.
681 cur_obj=self.pool.get('res.currency')
682 for line in self.browse(cr, uid, ids, context=context):
683 res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0
684 cur = line.repair_id.pricelist_id.currency_id
685 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
689 'name' : fields.char('Description',size=64,required=True),
690 'repair_id': fields.many2one('mrp.repair', 'Repair Order Reference',ondelete='cascade', select=True),
691 'type': fields.selection([('add','Add'),('remove','Remove')],'Type', required=True),
692 'to_invoice': fields.boolean('To Invoice'),
693 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok','=',True)], required=True),
694 'invoiced': fields.boolean('Invoiced',readonly=True),
695 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price')),
696 'price_subtotal': fields.function(_amount_line, string='Subtotal',digits_compute= dp.get_precision('Account')),
697 'tax_id': fields.many2many('account.tax', 'repair_operation_line_tax', 'repair_operation_line_id', 'tax_id', 'Taxes'),
698 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product Unit of Measure'), required=True),
699 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
700 'prodlot_id': fields.many2one('stock.production.lot', 'Lot Number',domain="[('product_id','=',product_id)]"),
701 'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
702 'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True),
703 'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True, select=True),
704 'move_id': fields.many2one('stock.move', 'Inventory Move', readonly=True),
705 'state': fields.selection([
707 ('confirmed','Confirmed'),
709 ('cancel','Cancelled')], 'Status', required=True, readonly=True,
710 help=' * The \'Draft\' state is set automatically as draft when repair order in draft state. \
711 \n* The \'Confirmed\' state is set automatically as confirm when repair order in confirm state. \
712 \n* The \'Done\' state is set automatically when repair order is completed.\
713 \n* The \'Cancelled\' state is set automatically when user cancel repair order.'),
716 'state': lambda *a: 'draft',
717 'product_uom_qty': lambda *a: 1,
720 def onchange_operation_type(self, cr, uid, ids, type, guarantee_limit, company_id=False, context=None):
721 """ On change of operation type it sets source location, destination location
722 and to invoice field.
723 @param product: Changed operation type.
724 @param guarantee_limit: Guarantee limit of current record.
725 @return: Dictionary of values.
729 'location_id': False,
730 'location_dest_id': False
732 warehouse_obj = self.pool.get('stock.warehouse')
733 location_id = self.pool.get('stock.location').search(cr, uid, [('usage','=','production')], context=context)
734 location_id = location_id and location_id[0] or False
737 # TOCHECK: Find stock location for user's company warehouse or
738 # repair order's company's warehouse (company_id field is added in fix of lp:831583)
739 args = company_id and [('company_id', '=', company_id)] or []
740 warehouse_ids = warehouse_obj.search(cr, uid, args, context=context)
743 stock_id = warehouse_obj.browse(cr, uid, warehouse_ids[0], context=context).lot_stock_id.id
744 to_invoice = (guarantee_limit and datetime.strptime(guarantee_limit, '%Y-%m-%d') < datetime.now())
747 'to_invoice': to_invoice,
748 'location_id': stock_id,
749 'location_dest_id': location_id
754 'location_id': location_id,
755 'location_dest_id': False
760 class mrp_repair_fee(osv.osv, ProductChangeMixin):
761 _name = 'mrp.repair.fee'
762 _description = 'Repair Fees Line'
764 def copy_data(self, cr, uid, id, default=None, context=None):
765 if not default: default = {}
766 default.update({'invoice_line_id': False, 'invoiced': False})
767 return super(mrp_repair_fee, self).copy_data(cr, uid, id, default, context)
769 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
770 """ Calculates amount.
771 @param field_name: Name of field.
773 @return: Dictionary of values.
776 cur_obj = self.pool.get('res.currency')
777 for line in self.browse(cr, uid, ids, context=context):
778 res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0
779 cur = line.repair_id.pricelist_id.currency_id
780 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
784 'repair_id': fields.many2one('mrp.repair', 'Repair Order Reference', required=True, ondelete='cascade', select=True),
785 'name': fields.char('Description', size=64, select=True,required=True),
786 'product_id': fields.many2one('product.product', 'Product'),
787 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product Unit of Measure'), required=True),
788 'price_unit': fields.float('Unit Price', required=True),
789 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
790 'price_subtotal': fields.function(_amount_line, string='Subtotal',digits_compute= dp.get_precision('Account')),
791 'tax_id': fields.many2many('account.tax', 'repair_fee_line_tax', 'repair_fee_line_id', 'tax_id', 'Taxes'),
792 'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
793 'to_invoice': fields.boolean('To Invoice'),
794 'invoiced': fields.boolean('Invoiced',readonly=True),
797 'to_invoice': lambda *a: True,
801 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: