7aafee1a4471b9c5e4592cf436f94660be9351d2
[odoo/odoo.git] / addons / mrp_repair / mrp_repair.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 from osv import fields,osv
23 import netsvc
24 import mx.DateTime
25 from mx.DateTime import RelativeDateTime, today
26 from tools.translate import _
27 import decimal_precision as dp
28
29 class mrp_repair(osv.osv):
30     _name = 'mrp.repair'
31     _description = 'Repair Order'
32
33     def _amount_untaxed(self, cr, uid, ids, field_name, arg, context):
34         """ Calculates untaxed amount.
35         @param self: The object pointer
36         @param cr: The current row, from the database cursor,
37         @param uid: The current user ID for security checks
38         @param ids: List of selected IDs
39         @param field_name: Name of field.
40         @param arg: Argument
41         @param context: A standard dictionary for contextual values
42         @return: Dictionary of values.
43         """
44         res = {}
45         cur_obj = self.pool.get('res.currency')
46
47         for repair in self.browse(cr, uid, ids):
48             res[repair.id] = 0.0
49             for line in repair.operations:
50                 res[repair.id] += line.price_subtotal
51             for line in repair.fees_lines:
52                 res[repair.id] += line.price_subtotal
53             cur = repair.pricelist_id.currency_id
54             res[repair.id] = cur_obj.round(cr, uid, cur, res[repair.id])
55         return res
56
57     def _amount_tax(self, cr, uid, ids, field_name, arg, context):
58         """ Calculates taxed amount.
59         @param field_name: Name of field.
60         @param arg: Argument
61         @return: Dictionary of values.
62         """
63         res = {}
64         #return {}.fromkeys(ids, 0)
65         cur_obj = self.pool.get('res.currency')
66         tax_obj = self.pool.get('account.tax')
67         for repair in self.browse(cr, uid, ids):
68             val = 0.0
69             cur = repair.pricelist_id.currency_id
70             for line in repair.operations:
71                 if line.to_invoice:
72                     for c in tax_obj.compute(cr, uid, line.tax_id, line.price_unit, line.product_uom_qty, repair.partner_invoice_id.id, line.product_id, repair.partner_id):
73                         val += c['amount']
74             for line in repair.fees_lines:
75                 if line.to_invoice:
76                     for c in tax_obj.compute(cr, uid, line.tax_id, line.price_unit, line.product_uom_qty, repair.partner_invoice_id.id, line.product_id, repair.partner_id):
77                         val += c['amount']
78             res[repair.id] = cur_obj.round(cr, uid, cur, val)
79         return res
80
81     def _amount_total(self, cr, uid, ids, field_name, arg, context):
82         """ Calculates total amount.
83         @param field_name: Name of field.
84         @param arg: Argument
85         @return: Dictionary of values.
86         """
87         res = {}
88         untax = self._amount_untaxed(cr, uid, ids, field_name, arg, context)
89         tax = self._amount_tax(cr, uid, ids, field_name, arg, context)
90         cur_obj = self.pool.get('res.currency')
91         for id in ids:
92             repair = self.browse(cr, uid, [id])[0]
93             cur = repair.pricelist_id.currency_id
94             res[id] = cur_obj.round(cr, uid, cur, untax.get(id, 0.0) + tax.get(id, 0.0))
95         return res
96     
97     def _get_default_address(self, cr, uid, ids, field_name, arg, context):
98         res = {}
99         partner_obj = self.pool.get('res.partner')
100         for data in self.browse(cr, uid, ids):
101             adr_id = False
102             if data.partner_id:
103                 adr_id = partner_obj.address_get(cr, uid, [data.partner_id.id], ['default'])['default']
104             res[data.id] = adr_id
105         return res
106
107     def _get_lines(self, cr, uid, ids, context=None):
108         if context is None:
109             context = {}
110         result = {}
111         for line in self.pool.get('mrp.repair.line').browse(cr, uid, ids, context=context):
112             result[line.repair_id.id] = True
113         return result.keys()
114
115     _columns = {
116         'name': fields.char('Repair Reference',size=24, required=True),
117         'product_id': fields.many2one('product.product', string='Product to Repair', required=True, readonly=True, states={'draft':[('readonly',False)]}),
118         'partner_id' : fields.many2one('res.partner', 'Partner', select=True, help='This field allow you to choose the parner that will be invoiced and delivered'),
119         'address_id': fields.many2one('res.partner.address', 'Delivery Address', domain="[('partner_id','=',partner_id)]"),
120         'default_address_id': fields.function(_get_default_address, method=True, type="many2one", relation="res.partner.address"),
121         'prodlot_id': fields.many2one('stock.production.lot', 'Lot Number', select=True, domain="[('product_id','=',product_id)]"),
122         'state': fields.selection([
123             ('draft','Quotation'),
124             ('confirmed','Confirmed'),
125             ('ready','Ready to Repair'),
126             ('under_repair','Under Repair'),
127             ('2binvoiced','To be Invoiced'),
128             ('invoice_except','Invoice Exception'),
129             ('done','Done'),
130             ('cancel','Cancel')
131             ], 'State', readonly=True,
132             help=' * The \'Draft\' state is used when a user is encoding a new and unconfirmed repair order. \
133             \n* The \'Confirmed\' state is used when a user confirms the repair order. \
134             \n* The \'Ready to Repair\' state is used to start to repairing, user can start repairing only after repair order is confirmed. \
135             \n* The \'To be Invoiced\' state is used to generate the invoice before or after repairing done. \
136             \n* The \'Done\' state is set when repairing is completed.\
137             \n* The \'Cancelled\' state is used when user cancel repair order.'),
138         'location_id': fields.many2one('stock.location', 'Current Location', select=True, readonly=True, states={'draft':[('readonly',False)]}),
139         'location_dest_id': fields.many2one('stock.location', 'Delivery Location', readonly=True, states={'draft':[('readonly',False)]}),
140         'move_id': fields.many2one('stock.move', 'Move',required=True, domain="[('product_id','=',product_id)]", readonly=True, states={'draft':[('readonly',False)]}),
141         '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."),
142         'operations' : fields.one2many('mrp.repair.line', 'repair_id', 'Operation Lines', readonly=True, states={'draft':[('readonly',False)]}),
143         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', help='The pricelist comes from the selected partner, by default.'),
144         'partner_invoice_id':fields.many2one('res.partner.address', 'Invoicing Address',  domain="[('partner_id','=',partner_id)]"),
145         'invoice_method':fields.selection([
146             ("none","No Invoice"),
147             ("b4repair","Before Repair"),
148             ("after_repair","After Repair")
149            ], "Invoice Method",
150             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.'),
151         'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True),
152         'picking_id': fields.many2one('stock.picking', 'Picking',readonly=True),
153         'fees_lines': fields.one2many('mrp.repair.fee', 'repair_id', 'Fees Lines', readonly=True, states={'draft':[('readonly',False)]}),
154         'internal_notes': fields.text('Internal Notes'),
155         'quotation_notes': fields.text('Quotation Notes'),
156         '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."),
157         'invoiced': fields.boolean('Invoiced', readonly=True),
158         'repaired': fields.boolean('Repaired', readonly=True),
159         'amount_untaxed': fields.function(_amount_untaxed, method=True, string='Untaxed Amount',
160             store={
161                 'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations'], 10),
162                 'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
163             }),
164         'amount_tax': fields.function(_amount_tax, method=True, string='Taxes',
165             store={
166                 'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations'], 10),
167                 'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
168             }),
169         'amount_total': fields.function(_amount_total, method=True, string='Total',
170             store={
171                 'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations'], 10),
172                 'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
173             }),
174     }
175
176     _defaults = {
177         'state': lambda *a: 'draft',
178         'deliver_bool': lambda *a: True,
179         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'mrp.repair'),
180         'invoice_method': lambda *a: 'none',
181         'pricelist_id': lambda self, cr, uid,context : self.pool.get('product.pricelist').search(cr, uid, [('type','=','sale')])[0]
182     }
183
184     def copy(self, cr, uid, id, default=None, context=None):
185         if not default:
186             default = {}
187         default.update({
188             'state':'draft',
189             'repaired':False,
190             'invoiced':False,
191             'invoice_id': False,
192             'picking_id': False,
193             'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.repair'),
194         })
195         return super(mrp_repair, self).copy(cr, uid, id, default, context)
196
197     def onchange_product_id(self, cr, uid, ids, product_id=None):
198         """ On change of product sets some values.
199         @param product_id: Changed product
200         @return: Dictionary of values.
201         """
202         return {'value': {
203                     'prodlot_id': False,
204                     'move_id': False,
205                     'guarantee_limit' :False,
206                     'location_id':  False,
207                     'location_dest_id': False,
208                 }
209         }
210
211     def onchange_move_id(self, cr, uid, ids, prod_id=False, move_id=False):
212         """ On change of move id sets values of guarantee limit, source location,
213         destination location, partner and partner address.
214         @param prod_id: Id of product in current record.
215         @param move_id: Changed move.
216         @return: Dictionary of values.
217         """
218         data = {}
219         data['value'] = {}
220         if not prod_id:
221             return data
222         if move_id:
223             move =  self.pool.get('stock.move').browse(cr, uid, move_id)
224             product = self.pool.get('product.product').browse(cr, uid, prod_id)
225             date = move.date_planned
226             limit = mx.DateTime.strptime(date, '%Y-%m-%d %H:%M:%S') + RelativeDateTime(months=product.warranty)
227             data['value']['guarantee_limit'] = limit.strftime('%Y-%m-%d')
228             data['value']['location_id'] = move.location_dest_id.id
229             data['value']['location_dest_id'] = move.location_dest_id.id
230             if move.address_id:
231                 data['value']['partner_id'] = move.address_id.partner_id and move.address_id.partner_id.id
232             else:
233                 data['value']['partner_id'] = False
234             data['value']['address_id'] = move.address_id and move.address_id.id
235             d = self.onchange_partner_id(cr, uid, ids, data['value']['partner_id'], data['value']['address_id'])
236             data['value'].update(d['value'])
237         return data
238
239     def button_dummy(self, cr, uid, ids, context=None):
240         return True
241
242     def onchange_partner_id(self, cr, uid, ids, part, address_id):
243         """ On change of partner sets the values of partner address,
244         partner invoice address and pricelist.
245         @param part: Changed id of partner.
246         @param address_id: Address id from current record.
247         @return: Dictionary of values.
248         """
249         part_obj = self.pool.get('res.partner')
250         pricelist_obj = self.pool.get('product.pricelist')
251         if not part:
252             return {'value': {
253                         'address_id': False,
254                         'partner_invoice_id': False,
255                         'pricelist_id': pricelist_obj.search(cr, uid, [('type','=','sale')])[0]
256                     }
257             }
258         addr = part_obj.address_get(cr, uid, [part], ['delivery', 'invoice', 'default'])
259         partner = part_obj.browse(cr, uid, part)
260         pricelist = partner.property_product_pricelist and partner.property_product_pricelist.id or False
261         return {'value': {
262                     'address_id': address_id or addr['delivery'],
263                     'partner_invoice_id': addr['invoice'],
264                     'pricelist_id': pricelist
265                 }
266         }
267
268     def onchange_lot_id(self, cr, uid, ids, lot, product_id):
269         """ On change of production lot sets the values of source location,
270         destination location, move and guarantee limit.
271         @param lot: Changed id of production lot.
272         @param product_id: Product id from current record.
273         @return: Dictionary of values.
274         """
275         move_obj = self.pool.get('stock.move')
276         data = {}
277         data['value'] = {
278             'location_id': False,
279             'location_dest_id': False,
280             'move_id': False,
281             'guarantee_limit': False
282         }
283
284         if not lot:
285             return data
286         move_ids = move_obj.search(cr, uid, [('prodlot_id', '=', lot)])
287
288         if not len(move_ids):
289             return data
290
291         def get_last_move(lst_move):
292             while lst_move.move_dest_id and lst_move.move_dest_id.state == 'done':
293                 lst_move = lst_move.move_dest_id
294             return lst_move
295
296         move_id = move_ids[0]
297         move = get_last_move(move_obj.browse(cr, uid, move_id))
298         data['value']['move_id'] = move.id
299         d = self.onchange_move_id(cr, uid, ids, product_id, move.id)
300         data['value'].update(d['value'])
301         return data
302
303     def action_cancel_draft(self, cr, uid, ids, *args):
304         """ Cancels repair order when it is in 'Draft' state.
305         @param *arg: Arguments
306         @return: True
307         """
308         if not len(ids):
309             return False
310         mrp_line_obj = self.pool.get('mrp.repair.line')
311         for repair in self.browse(cr, uid, ids):
312             mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'draft'})
313         self.write(cr, uid, ids, {'state':'draft'})
314         wf_service = netsvc.LocalService("workflow")
315         for id in ids:
316             wf_service.trg_create(uid, 'mrp.repair', id, cr)
317         return True
318
319     def action_confirm(self, cr, uid, ids, *args):
320         """ Repair order state is set to 'To be invoiced' when invoice method
321         is 'Before repair' else state becomes 'Confirmed'.
322         @param *arg: Arguments
323         @return: True
324         """
325         mrp_line_obj = self.pool.get('mrp.repair.line')
326         for o in self.browse(cr, uid, ids):
327             if (o.invoice_method == 'b4repair'):
328                 self.write(cr, uid, [o.id], {'state': '2binvoiced'})
329             else:
330                 self.write(cr, uid, [o.id], {'state': 'confirmed'})
331                 mrp_line_obj.write(cr, uid, [l.id for l in o.operations], {'state': 'confirmed'})
332         return True
333
334     def action_cancel(self, cr, uid, ids, context=None):
335         """ Cancels repair order.
336         @return: True
337         """
338         mrp_line_obj = self.pool.get('mrp.repair.line')
339         for repair in self.browse(cr, uid, ids):
340             mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'cancel'})
341         self.write(cr,uid,ids,{'state':'cancel'})
342         return True
343
344     def wkf_invoice_create(self, cr, uid, ids, *args):
345         return self.action_invoice_create(cr, uid, ids)
346
347     def action_invoice_create(self, cr, uid, ids, group=False, context=None):
348         """ Creates invoice(s) for repair order.
349         @param group: It is set to true when group invoice is to be generated.
350         @return: Invoice Ids.
351         """
352         res = {}
353         invoices_group = {}
354         inv_line_obj = self.pool.get('account.invoice.line')
355         inv_obj = self.pool.get('account.invoice')
356         repair_line_obj = self.pool.get('mrp.repair.line')
357         repair_fee_obj = self.pool.get('mrp.repair.fee')
358         for repair in self.browse(cr, uid, ids, context=context):
359             res[repair.id] = False
360             if repair.state in ('draft','cancel') or repair.invoice_id:
361                 continue
362             if not (repair.partner_id.id and repair.partner_invoice_id.id):
363                 raise osv.except_osv(_('No partner !'),_('You have to select a Partner Invoice Address in the repair form !'))
364             comment = repair.quotation_notes
365             if (repair.invoice_method != 'none'):
366                 if group and repair.partner_invoice_id.id in invoices_group:
367                     inv_id = invoices_group[repair.partner_invoice_id.id]
368                     invoice = inv_obj.browse(cr, uid, inv_id)
369                     invoice_vals = {
370                         'name': invoice.name +', '+repair.name,
371                         'origin': invoice.origin+', '+repair.name,
372                         'comment':(comment and (invoice.comment and invoice.comment+"\n"+comment or comment)) or (invoice.comment and invoice.comment or ''),
373                     }
374                     inv_obj.write(cr, uid, [inv_id], invoice_vals, context=context)
375                 else:
376                     if not repair.partner_id.property_account_receivable:
377                         raise osv.except_osv(_('Error !'), _('No account defined for partner "%s".') % repair.partner_id.name )
378                     account_id = repair.partner_id.property_account_receivable.id
379                     inv = {
380                         'name': repair.name,
381                         'origin':repair.name,
382                         'type': 'out_invoice',
383                         'account_id': account_id,
384                         'partner_id': repair.partner_id.id,
385                         'address_invoice_id': repair.address_id.id,
386                         'currency_id': repair.pricelist_id.currency_id.id,
387                         'comment': repair.quotation_notes,
388                         'fiscal_position': repair.partner_id.property_account_position.id
389                     }
390                     inv_id = inv_obj.create(cr, uid, inv)
391                     invoices_group[repair.partner_invoice_id.id] = inv_id
392                 self.write(cr, uid, repair.id, {'invoiced': True, 'invoice_id': inv_id})
393
394                 for operation in repair.operations:
395                     if operation.to_invoice == True:
396                         if group:
397                             name = repair.name + '-' + operation.name
398                         else:
399                             name = operation.name
400
401                         if operation.product_id.property_account_income:
402                             account_id = operation.product_id.property_account_income.id
403                         elif operation.product_id.categ_id.property_account_income_categ:
404                             account_id = operation.product_id.categ_id.property_account_income_categ.id
405                         else:
406                             raise osv.except_osv(_('Error !'), _('No account defined for product "%s".') % operation.product_id.name )
407
408                         invoice_line_id = inv_line_obj.create(cr, uid, {
409                             'invoice_id': inv_id,
410                             'name': name,
411                             'origin': repair.name,
412                             'account_id': account_id,
413                             'quantity': operation.product_uom_qty,
414                             'invoice_line_tax_id': [(6,0,[x.id for x in operation.tax_id])],
415                             'uos_id': operation.product_uom.id,
416                             'price_unit': operation.price_unit,
417                             'price_subtotal': operation.product_uom_qty*operation.price_unit,
418                             'product_id': operation.product_id and operation.product_id.id or False
419                         })
420                         repair_line_obj.write(cr, uid, [operation.id], {'invoiced': True, 'invoice_line_id': invoice_line_id})
421                 for fee in repair.fees_lines:
422                     if fee.to_invoice == True:
423                         if group:
424                             name = repair.name + '-' + fee.name
425                         else:
426                             name = fee.name
427                         if not fee.product_id:
428                             raise osv.except_osv(_('Warning !'), _('No product defined on Fees!'))
429                         
430                         if fee.product_id.property_account_income:
431                             account_id = fee.product_id.property_account_income.id
432                         elif fee.product_id.categ_id.property_account_income_categ:
433                             account_id = fee.product_id.categ_id.property_account_income_categ.id
434                         else:
435                             raise osv.except_osv(_('Error !'), _('No account defined for product "%s".') % fee.product_id.name)
436                         
437                         invoice_fee_id = inv_line_obj.create(cr, uid, {
438                             'invoice_id': inv_id,
439                             'name': name,
440                             'origin': repair.name,
441                             'account_id': account_id,
442                             'quantity': fee.product_uom_qty,
443                             'invoice_line_tax_id': [(6,0,[x.id for x in fee.tax_id])],
444                             'uos_id': fee.product_uom.id,
445                             'product_id': fee.product_id and fee.product_id.id or False,
446                             'price_unit': fee.price_unit,
447                             'price_subtotal': fee.product_uom_qty*fee.price_unit
448                         })
449                         repair_fee_obj.write(cr, uid, [fee.id], {'invoiced': True, 'invoice_line_id': invoice_fee_id})
450                 res[repair.id] = inv_id
451         return res
452
453     def action_repair_ready(self, cr, uid, ids, context=None):
454         """ Writes repair order state to 'Ready'
455         @return: True
456         """
457         self.write(cr, uid, ids, {'state': 'ready'})
458         return True
459
460     def action_invoice_cancel(self, cr, uid, ids, context=None):
461         """ Writes repair order state to 'Exception in invoice'
462         @return: True
463         """
464         self.write(cr, uid, ids, {'state': 'invoice_except'})
465         return True
466
467     def action_repair_start(self, cr, uid, ids, context=None):
468         """ Writes repair order state to 'Under Repair'
469         @return: True
470         """
471         self.write(cr, uid, ids, {'state': 'under_repair'})
472         return True
473
474     def action_invoice_end(self, cr, uid, ids, context=None):
475         """ Writes repair order state to 'Ready' if invoice method is Before repair.
476         @return: True
477         """
478         for order in self.browse(cr, uid, ids):
479             val = {}
480             if (order.invoice_method == 'b4repair'):
481                 val['state'] = 'ready'
482             else:
483                 pass
484             self.write(cr, uid, [order.id], val)
485         return True
486
487     def action_repair_end(self, cr, uid, ids, context=None):
488         """ Writes repair order state to 'To be invoiced' if invoice method is
489         After repair else state is set to 'Ready'.
490         @return: True
491         """
492         for order in self.browse(cr, uid, ids):
493             val = {}
494             val['repaired'] = True
495             if (not order.invoiced and order.invoice_method=='after_repair'):
496                 val['state'] = '2binvoiced'
497             elif (not order.invoiced and order.invoice_method=='b4repair'):
498                 val['state'] = 'ready'
499             else:
500                 pass
501             self.write(cr, uid, [order.id], val)
502         return True
503
504     def wkf_repair_done(self, cr, uid, ids, *args):
505         self.action_repair_done(cr, uid, ids)
506         return True
507
508     def action_repair_done(self, cr, uid, ids, context=None):
509         """ Creates stock move and picking for repair order.
510         @return: Picking ids.
511         """
512         res = {}
513         move_obj = self.pool.get('stock.move')
514         wf_service = netsvc.LocalService("workflow")
515         repair_line_obj = self.pool.get('mrp.repair.line')
516         seq_obj = self.pool.get('ir.sequence')
517         pick_obj = self.pool.get('stock.picking')
518         for repair in self.browse(cr, uid, ids, context=context):
519             for move in repair.operations:
520                 move_id = move_obj.create(cr, uid, {
521                     'name': move.name,
522                     'product_id': move.product_id.id,
523                     'product_qty': move.product_uom_qty,
524                     'product_uom': move.product_uom.id,
525                     'address_id': repair.address_id and repair.address_id.id or False,
526                     'location_id': move.location_id.id,
527                     'location_dest_id': move.location_dest_id.id,
528                     'tracking_id': False,
529                     'state': 'done',
530                 })
531                 repair_line_obj.write(cr, uid, [move.id], {'move_id': move_id})
532             if repair.deliver_bool:
533                 pick_name = seq_obj.get(cr, uid, 'stock.picking.out')
534                 picking = pick_obj.create(cr, uid, {
535                     'name': pick_name,
536                     'origin': repair.name,
537                     'state': 'draft',
538                     'move_type': 'one',
539                     'address_id': repair.address_id and repair.address_id.id or False,
540                     'note': repair.internal_notes,
541                     'invoice_state': 'none',
542                     'type': 'out',
543                 })
544                 move_id = move_obj.create(cr, uid, {
545                     'name': repair.name,
546                     'picking_id': picking,
547                     'product_id': repair.product_id.id,
548                     'product_qty': 1.0,
549                     'product_uom': repair.product_id.uom_id.id,
550                     'prodlot_id': repair.prodlot_id and repair.prodlot_id.id or False,
551                     'address_id': repair.address_id and repair.address_id.id or False,
552                     'location_id': repair.location_id.id,
553                     'location_dest_id': repair.location_dest_id.id,
554                     'tracking_id': False,
555                     'state': 'assigned',
556                 })
557                 wf_service.trg_validate(uid, 'stock.picking', picking, 'button_confirm', cr)
558                 self.write(cr, uid, [repair.id], {'state': 'done', 'picking_id': picking})
559                 res[repair.id] = picking
560             else:
561                 self.write(cr, uid, [repair.id], {'state': 'done'})
562         return res
563
564
565 mrp_repair()
566
567
568 class ProductChangeMixin(object):
569     def product_id_change(self, cr, uid, ids, pricelist, product, uom=False,
570                           product_uom_qty=0, partner_id=False, guarantee_limit=False):
571         """ On change of product it sets product quantity, tax account, name,
572         uom of product, unit price and price subtotal.
573         @param pricelist: Pricelist of current record.
574         @param product: Changed id of product.
575         @param uom: UoM of current record.
576         @param product_uom_qty: Quantity of current record.
577         @param partner_id: Partner of current record.
578         @param guarantee_limit: Guarantee limit of current record.
579         @return: Dictionary of values and warning message.
580         """
581         result = {}
582         warning = {}
583
584         if not product_uom_qty:
585             product_uom_qty = 1
586         result['product_uom_qty'] = product_uom_qty
587
588         if product:
589             product_obj = self.pool.get('product.product').browse(cr, uid, product)
590             if partner_id:
591                 partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
592                 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, partner.property_account_position, product_obj.taxes_id)
593
594             result['name'] = product_obj.partner_ref
595             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id or False
596             if not pricelist:
597                 warning = {
598                     'title':'No Pricelist !',
599                     'message':
600                         'You have to select a pricelist in the Repair form !\n'
601                         'Please set one before choosing a product.'
602                 }
603             else:
604                 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
605                             product, product_uom_qty, partner_id, {'uom': uom,})[pricelist]
606
607                 if price is False:
608                      warning = {
609                         'title':'No valid pricelist line found !',
610                         'message':
611                             "Couldn't find a pricelist line matching this product and quantity.\n"
612                             "You have to change either the product, the quantity or the pricelist."
613                      }
614                 else:
615                     result.update({'price_unit': price, 'price_subtotal': price*product_uom_qty})
616
617         return {'value': result, 'warning': warning}
618
619
620 class mrp_repair_line(osv.osv, ProductChangeMixin):
621     _name = 'mrp.repair.line'
622     _description = 'Repair Line'
623
624     def copy_data(self, cr, uid, id, default=None, context=None):
625         if not default: default = {}
626         default.update( {'invoice_line_id': False, 'move_id': False, 'invoiced': False, 'state': 'draft'})
627         return super(mrp_repair_line, self).copy_data(cr, uid, id, default, context)
628
629     def _amount_line(self, cr, uid, ids, field_name, arg, context):
630         """ Calculates amount.
631         @param field_name: Name of field.
632         @param arg: Argument
633         @return: Dictionary of values.
634         """
635         res = {}
636         cur_obj=self.pool.get('res.currency')
637         for line in self.browse(cr, uid, ids):
638             res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0
639             cur = line.repair_id.pricelist_id.currency_id
640             res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
641         return res
642
643     _columns = {
644         'name' : fields.char('Description',size=64,required=True),
645         'repair_id': fields.many2one('mrp.repair', 'Repair Order Reference',ondelete='cascade', select=True),
646         'type': fields.selection([('add','Add'),('remove','Remove')],'Type', required=True),
647         'to_invoice': fields.boolean('To Invoice'),
648         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok','=',True)], required=True),
649         'invoiced': fields.boolean('Invoiced',readonly=True),
650         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price')),
651         'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal',digits_compute= dp.get_precision('Sale Price')),
652         'tax_id': fields.many2many('account.tax', 'repair_operation_line_tax', 'repair_operation_line_id', 'tax_id', 'Taxes'),
653         'product_uom_qty': fields.float('Quantity (UoM)', digits=(16,2), required=True),
654         'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
655         'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
656         'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True),
657         'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True, select=True),
658         'move_id': fields.many2one('stock.move', 'Inventory Move', readonly=True),
659         'state': fields.selection([
660                     ('draft','Draft'),
661                     ('confirmed','Confirmed'),
662                     ('done','Done'),
663                     ('cancel','Canceled')], 'State', required=True, readonly=True,
664                     help=' * The \'Draft\' state is set automatically as draft when repair order in draft state. \
665                         \n* The \'Confirmed\' state is set automatically as confirm when repair order in confirm state. \
666                         \n* The \'Done\' state is set automatically when repair order is completed.\
667                         \n* The \'Cancelled\' state is set automatically when user cancel repair order.'),
668     }
669     _defaults = {
670      'state': lambda *a: 'draft',
671      'product_uom_qty': lambda *a: 1,
672     }
673
674     def onchange_operation_type(self, cr, uid, ids, type, guarantee_limit):
675         """ On change of operation type it sets source location, destination location
676         and to invoice field.
677         @param product: Changed operation type.
678         @param guarantee_limit: Guarantee limit of current record.
679         @return: Dictionary of values.
680         """
681         if not type:
682             return {'value': {
683                         'location_id': False,
684                         'location_dest_id': False
685                     }
686             }
687         produc_id = self.pool.get('stock.location').search(cr, uid, [('name','=','Production')])[0]
688         if type == 'add':
689             stock_id = self.pool.get('stock.location').search(cr, uid, [('name','=','Stock')])[0]
690             to_invoice = False
691             if guarantee_limit and today() > mx.DateTime.strptime(guarantee_limit, '%Y-%m-%d'):
692                 to_invoice=True
693             return {'value': {
694                         'to_invoice': to_invoice,
695                         'location_id': stock_id,
696                         'location_dest_id': produc_id
697                     }
698             }
699         return {'value': {
700                 'to_invoice': False,
701                 'location_id': produc_id,
702                 'location_dest_id': False
703             }
704         }
705
706 mrp_repair_line()
707
708 class mrp_repair_fee(osv.osv, ProductChangeMixin):
709     _name = 'mrp.repair.fee'
710     _description = 'Repair Fees Line'
711
712     def copy_data(self, cr, uid, id, default=None, context=None):
713         if not default: default = {}
714         default.update({'invoice_line_id': False, 'invoiced': False})
715         return super(mrp_repair_fee, self).copy_data(cr, uid, id, default, context)
716
717     def _amount_line(self, cr, uid, ids, field_name, arg, context):
718         """ Calculates amount.
719         @param field_name: Name of field.
720         @param arg: Argument
721         @return: Dictionary of values.
722         """
723         res = {}
724         cur_obj = self.pool.get('res.currency')
725         for line in self.browse(cr, uid, ids):
726             res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0
727             cur = line.repair_id.pricelist_id.currency_id
728             res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
729         return res
730
731     _columns = {
732         'repair_id': fields.many2one('mrp.repair', 'Repair Order Reference', required=True, ondelete='cascade', select=True),
733         'name': fields.char('Description', size=64, select=True,required=True),
734         'product_id': fields.many2one('product.product', 'Product'),
735         'product_uom_qty': fields.float('Quantity', digits=(16,2), required=True),
736         'price_unit': fields.float('Unit Price', required=True),
737         'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
738         'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal',digits_compute= dp.get_precision('Sale Price')),
739         'tax_id': fields.many2many('account.tax', 'repair_fee_line_tax', 'repair_fee_line_id', 'tax_id', 'Taxes'),
740         'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
741         'to_invoice': fields.boolean('To Invoice'),
742         'invoiced': fields.boolean('Invoiced',readonly=True),
743     }
744     _defaults = {
745         'to_invoice': lambda *a: True,
746     }
747
748 mrp_repair_fee()
749 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: