[MERGE]: Merged with trunk-addons.
[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 from datetime import datetime
25 from dateutil.relativedelta import relativedelta
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=None):
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, context=context):
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=None):
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, context=context):
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=None):
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=context)
89         tax = self._amount_tax(cr, uid, ids, field_name, arg, context=context)
90         cur_obj = self.pool.get('res.currency')
91         for id in ids:
92             repair = self.browse(cr, uid, [id], context=context)[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=None):
98         res = {}
99         partner_obj = self.pool.get('res.partner')
100         for data in self.browse(cr, uid, ids, context=context):
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             limit = datetime.strptime(move.date_expected, '%Y-%m-%d %H:%M:%S') + relativedelta(months=product.warranty)
226             data['value']['guarantee_limit'] = limit.strftime('%Y-%m-%d')
227             data['value']['location_id'] = move.location_dest_id.id
228             data['value']['location_dest_id'] = move.location_dest_id.id
229             if move.address_id:
230                 data['value']['partner_id'] = move.address_id.partner_id and move.address_id.partner_id.id
231             else:
232                 data['value']['partner_id'] = False
233             data['value']['address_id'] = move.address_id and move.address_id.id
234             d = self.onchange_partner_id(cr, uid, ids, data['value']['partner_id'], data['value']['address_id'])
235             data['value'].update(d['value'])
236         return data
237
238     def button_dummy(self, cr, uid, ids, context=None):
239         return True
240
241     def onchange_partner_id(self, cr, uid, ids, part, address_id):
242         """ On change of partner sets the values of partner address,
243         partner invoice address and pricelist.
244         @param part: Changed id of partner.
245         @param address_id: Address id from current record.
246         @return: Dictionary of values.
247         """
248         part_obj = self.pool.get('res.partner')
249         pricelist_obj = self.pool.get('product.pricelist')
250         if not part:
251             return {'value': {
252                         'address_id': False,
253                         'partner_invoice_id': False,
254                         'pricelist_id': pricelist_obj.search(cr, uid, [('type','=','sale')])[0]
255                     }
256             }
257         addr = part_obj.address_get(cr, uid, [part], ['delivery', 'invoice', 'default'])
258         partner = part_obj.browse(cr, uid, part)
259         pricelist = partner.property_product_pricelist and partner.property_product_pricelist.id or False
260         return {'value': {
261                     'address_id': address_id or addr['delivery'],
262                     'partner_invoice_id': addr['invoice'],
263                     'pricelist_id': pricelist
264                 }
265         }
266
267     def onchange_lot_id(self, cr, uid, ids, lot, product_id):
268         """ On change of production lot sets the values of source location,
269         destination location, move and guarantee limit.
270         @param lot: Changed id of production lot.
271         @param product_id: Product id from current record.
272         @return: Dictionary of values.
273         """
274         move_obj = self.pool.get('stock.move')
275         data = {}
276         data['value'] = {
277             'location_id': False,
278             'location_dest_id': False,
279             'move_id': False,
280             'guarantee_limit': False
281         }
282
283         if not lot:
284             return data
285         move_ids = move_obj.search(cr, uid, [('prodlot_id', '=', lot)])
286
287         if not len(move_ids):
288             return data
289
290         def get_last_move(lst_move):
291             while lst_move.move_dest_id and lst_move.move_dest_id.state == 'done':
292                 lst_move = lst_move.move_dest_id
293             return lst_move
294
295         move_id = move_ids[0]
296         move = get_last_move(move_obj.browse(cr, uid, move_id))
297         data['value']['move_id'] = move.id
298         d = self.onchange_move_id(cr, uid, ids, product_id, move.id)
299         data['value'].update(d['value'])
300         return data
301
302     def action_cancel_draft(self, cr, uid, ids, *args):
303         """ Cancels repair order when it is in 'Draft' state.
304         @param *arg: Arguments
305         @return: True
306         """
307         if not len(ids):
308             return False
309         mrp_line_obj = self.pool.get('mrp.repair.line')
310         for repair in self.browse(cr, uid, ids):
311             mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'draft'})
312         self.write(cr, uid, ids, {'state':'draft'})
313         wf_service = netsvc.LocalService("workflow")
314         for id in ids:
315             wf_service.trg_create(uid, 'mrp.repair', id, cr)
316         return True
317
318     def action_confirm(self, cr, uid, ids, *args):
319         """ Repair order state is set to 'To be invoiced' when invoice method
320         is 'Before repair' else state becomes 'Confirmed'.
321         @param *arg: Arguments
322         @return: True
323         """
324         mrp_line_obj = self.pool.get('mrp.repair.line')
325         for o in self.browse(cr, uid, ids):
326             if (o.invoice_method == 'b4repair'):
327                 self.write(cr, uid, [o.id], {'state': '2binvoiced'})
328             else:
329                 self.write(cr, uid, [o.id], {'state': 'confirmed'})
330                 mrp_line_obj.write(cr, uid, [l.id for l in o.operations], {'state': 'confirmed'})
331         return True
332
333     def action_cancel(self, cr, uid, ids, context=None):
334         """ Cancels repair order.
335         @return: True
336         """
337         mrp_line_obj = self.pool.get('mrp.repair.line')
338         for repair in self.browse(cr, uid, ids, context=context):
339             mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'cancel'})
340         self.write(cr,uid,ids,{'state':'cancel'})
341         return True
342
343     def wkf_invoice_create(self, cr, uid, ids, *args):
344         return self.action_invoice_create(cr, uid, ids)
345
346     def action_invoice_create(self, cr, uid, ids, group=False, context=None):
347         """ Creates invoice(s) for repair order.
348         @param group: It is set to true when group invoice is to be generated.
349         @return: Invoice Ids.
350         """
351         res = {}
352         invoices_group = {}
353         inv_line_obj = self.pool.get('account.invoice.line')
354         inv_obj = self.pool.get('account.invoice')
355         repair_line_obj = self.pool.get('mrp.repair.line')
356         repair_fee_obj = self.pool.get('mrp.repair.fee')
357         for repair in self.browse(cr, uid, ids, context=context):
358             res[repair.id] = False
359             if repair.state in ('draft','cancel') or repair.invoice_id:
360                 continue
361             if not (repair.partner_id.id and repair.partner_invoice_id.id):
362                 raise osv.except_osv(_('No partner !'),_('You have to select a Partner Invoice Address in the repair form !'))
363             comment = repair.quotation_notes
364             if (repair.invoice_method != 'none'):
365                 if group and repair.partner_invoice_id.id in invoices_group:
366                     inv_id = invoices_group[repair.partner_invoice_id.id]
367                     invoice = inv_obj.browse(cr, uid, inv_id)
368                     invoice_vals = {
369                         'name': invoice.name +', '+repair.name,
370                         'origin': invoice.origin+', '+repair.name,
371                         'comment':(comment and (invoice.comment and invoice.comment+"\n"+comment or comment)) or (invoice.comment and invoice.comment or ''),
372                     }
373                     inv_obj.write(cr, uid, [inv_id], invoice_vals, context=context)
374                 else:
375                     if not repair.partner_id.property_account_receivable:
376                         raise osv.except_osv(_('Error !'), _('No account defined for partner "%s".') % repair.partner_id.name )
377                     account_id = repair.partner_id.property_account_receivable.id
378                     inv = {
379                         'name': repair.name,
380                         'origin':repair.name,
381                         'type': 'out_invoice',
382                         'account_id': account_id,
383                         'partner_id': repair.partner_id.id,
384                         'address_invoice_id': repair.address_id.id,
385                         'currency_id': repair.pricelist_id.currency_id.id,
386                         'comment': repair.quotation_notes,
387                         'fiscal_position': repair.partner_id.property_account_position.id
388                     }
389                     inv_id = inv_obj.create(cr, uid, inv)
390                     invoices_group[repair.partner_invoice_id.id] = inv_id
391                 self.write(cr, uid, repair.id, {'invoiced': True, 'invoice_id': inv_id})
392
393                 for operation in repair.operations:
394                     if operation.to_invoice == True:
395                         if group:
396                             name = repair.name + '-' + operation.name
397                         else:
398                             name = operation.name
399
400                         if operation.product_id.property_account_income:
401                             account_id = operation.product_id.property_account_income.id
402                         elif operation.product_id.categ_id.property_account_income_categ:
403                             account_id = operation.product_id.categ_id.property_account_income_categ.id
404                         else:
405                             raise osv.except_osv(_('Error !'), _('No account defined for product "%s".') % operation.product_id.name )
406
407                         invoice_line_id = inv_line_obj.create(cr, uid, {
408                             'invoice_id': inv_id,
409                             'name': name,
410                             'origin': repair.name,
411                             'account_id': account_id,
412                             'quantity': operation.product_uom_qty,
413                             'invoice_line_tax_id': [(6,0,[x.id for x in operation.tax_id])],
414                             'uos_id': operation.product_uom.id,
415                             'price_unit': operation.price_unit,
416                             'price_subtotal': operation.product_uom_qty*operation.price_unit,
417                             'product_id': operation.product_id and operation.product_id.id or False
418                         })
419                         repair_line_obj.write(cr, uid, [operation.id], {'invoiced': True, 'invoice_line_id': invoice_line_id})
420                 for fee in repair.fees_lines:
421                     if fee.to_invoice == True:
422                         if group:
423                             name = repair.name + '-' + fee.name
424                         else:
425                             name = fee.name
426                         if not fee.product_id:
427                             raise osv.except_osv(_('Warning !'), _('No product defined on Fees!'))
428                         
429                         if fee.product_id.property_account_income:
430                             account_id = fee.product_id.property_account_income.id
431                         elif fee.product_id.categ_id.property_account_income_categ:
432                             account_id = fee.product_id.categ_id.property_account_income_categ.id
433                         else:
434                             raise osv.except_osv(_('Error !'), _('No account defined for product "%s".') % fee.product_id.name)
435                         
436                         invoice_fee_id = inv_line_obj.create(cr, uid, {
437                             'invoice_id': inv_id,
438                             'name': name,
439                             'origin': repair.name,
440                             'account_id': account_id,
441                             'quantity': fee.product_uom_qty,
442                             'invoice_line_tax_id': [(6,0,[x.id for x in fee.tax_id])],
443                             'uos_id': fee.product_uom.id,
444                             'product_id': fee.product_id and fee.product_id.id or False,
445                             'price_unit': fee.price_unit,
446                             'price_subtotal': fee.product_uom_qty*fee.price_unit
447                         })
448                         repair_fee_obj.write(cr, uid, [fee.id], {'invoiced': True, 'invoice_line_id': invoice_fee_id})
449                 res[repair.id] = inv_id
450         return res
451
452     def action_repair_ready(self, cr, uid, ids, context=None):
453         """ Writes repair order state to 'Ready'
454         @return: True
455         """
456         self.write(cr, uid, ids, {'state': 'ready'})
457         return True
458
459     def action_invoice_cancel(self, cr, uid, ids, context=None):
460         """ Writes repair order state to 'Exception in invoice'
461         @return: True
462         """
463         self.write(cr, uid, ids, {'state': 'invoice_except'})
464         return True
465
466     def action_repair_start(self, cr, uid, ids, context=None):
467         """ Writes repair order state to 'Under Repair'
468         @return: True
469         """
470         self.write(cr, uid, ids, {'state': 'under_repair'})
471         return True
472
473     def action_invoice_end(self, cr, uid, ids, context=None):
474         """ Writes repair order state to 'Ready' if invoice method is Before repair.
475         @return: True
476         """
477         for order in self.browse(cr, uid, ids):
478             val = {}
479             if (order.invoice_method == 'b4repair'):
480                 val['state'] = 'ready'
481             else:
482                 pass
483             self.write(cr, uid, [order.id], val)
484         return True
485
486     def action_repair_end(self, cr, uid, ids, context=None):
487         """ Writes repair order state to 'To be invoiced' if invoice method is
488         After repair else state is set to 'Ready'.
489         @return: True
490         """
491         for order in self.browse(cr, uid, ids):
492             val = {}
493             val['repaired'] = True
494             if (not order.invoiced and order.invoice_method=='after_repair'):
495                 val['state'] = '2binvoiced'
496             elif (not order.invoiced and order.invoice_method=='b4repair'):
497                 val['state'] = 'ready'
498             else:
499                 pass
500             self.write(cr, uid, [order.id], val)
501         return True
502
503     def wkf_repair_done(self, cr, uid, ids, *args):
504         self.action_repair_done(cr, uid, ids)
505         return True
506
507     def action_repair_done(self, cr, uid, ids, context=None):
508         """ Creates stock move and picking for repair order.
509         @return: Picking ids.
510         """
511         res = {}
512         move_obj = self.pool.get('stock.move')
513         wf_service = netsvc.LocalService("workflow")
514         repair_line_obj = self.pool.get('mrp.repair.line')
515         seq_obj = self.pool.get('ir.sequence')
516         pick_obj = self.pool.get('stock.picking')
517         for repair in self.browse(cr, uid, ids, context=context):
518             for move in repair.operations:
519                 move_id = move_obj.create(cr, uid, {
520                     'name': move.name,
521                     'product_id': move.product_id.id,
522                     'product_qty': move.product_uom_qty,
523                     'product_uom': move.product_uom.id,
524                     'address_id': repair.address_id and repair.address_id.id or False,
525                     'location_id': move.location_id.id,
526                     'location_dest_id': move.location_dest_id.id,
527                     'tracking_id': False,
528                     'state': 'done',
529                 })
530                 repair_line_obj.write(cr, uid, [move.id], {'move_id': move_id})
531             if repair.deliver_bool:
532                 pick_name = seq_obj.get(cr, uid, 'stock.picking.out')
533                 picking = pick_obj.create(cr, uid, {
534                     'name': pick_name,
535                     'origin': repair.name,
536                     'state': 'draft',
537                     'move_type': 'one',
538                     'address_id': repair.address_id and repair.address_id.id or False,
539                     'note': repair.internal_notes,
540                     'invoice_state': 'none',
541                     'type': 'out',
542                 })
543                 move_id = move_obj.create(cr, uid, {
544                     'name': repair.name,
545                     'picking_id': picking,
546                     'product_id': repair.product_id.id,
547                     'product_qty': 1.0,
548                     'product_uom': repair.product_id.uom_id.id,
549                     'prodlot_id': repair.prodlot_id and repair.prodlot_id.id or False,
550                     'address_id': repair.address_id and repair.address_id.id or False,
551                     'location_id': repair.location_id.id,
552                     'location_dest_id': repair.location_dest_id.id,
553                     'tracking_id': False,
554                     'state': 'assigned',
555                 })
556                 wf_service.trg_validate(uid, 'stock.picking', picking, 'button_confirm', cr)
557                 self.write(cr, uid, [repair.id], {'state': 'done', 'picking_id': picking})
558                 res[repair.id] = picking
559             else:
560                 self.write(cr, uid, [repair.id], {'state': 'done'})
561         return res
562
563
564 mrp_repair()
565
566
567 class ProductChangeMixin(object):
568     def product_id_change(self, cr, uid, ids, pricelist, product, uom=False,
569                           product_uom_qty=0, partner_id=False, guarantee_limit=False):
570         """ On change of product it sets product quantity, tax account, name,
571         uom of product, unit price and price subtotal.
572         @param pricelist: Pricelist of current record.
573         @param product: Changed id of product.
574         @param uom: UoM of current record.
575         @param product_uom_qty: Quantity of current record.
576         @param partner_id: Partner of current record.
577         @param guarantee_limit: Guarantee limit of current record.
578         @return: Dictionary of values and warning message.
579         """
580         result = {}
581         warning = {}
582
583         if not product_uom_qty:
584             product_uom_qty = 1
585         result['product_uom_qty'] = product_uom_qty
586
587         if product:
588             product_obj = self.pool.get('product.product').browse(cr, uid, product)
589             if partner_id:
590                 partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
591                 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, partner.property_account_position, product_obj.taxes_id)
592
593             result['name'] = product_obj.partner_ref
594             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id or False
595             if not pricelist:
596                 warning = {
597                     'title':'No Pricelist !',
598                     'message':
599                         'You have to select a pricelist in the Repair form !\n'
600                         'Please set one before choosing a product.'
601                 }
602             else:
603                 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
604                             product, product_uom_qty, partner_id, {'uom': uom,})[pricelist]
605
606                 if price is False:
607                      warning = {
608                         'title':'No valid pricelist line found !',
609                         'message':
610                             "Couldn't find a pricelist line matching this product and quantity.\n"
611                             "You have to change either the product, the quantity or the pricelist."
612                      }
613                 else:
614                     result.update({'price_unit': price, 'price_subtotal': price*product_uom_qty})
615
616         return {'value': result, 'warning': warning}
617
618
619 class mrp_repair_line(osv.osv, ProductChangeMixin):
620     _name = 'mrp.repair.line'
621     _description = 'Repair Line'
622
623     def copy_data(self, cr, uid, id, default=None, context=None):
624         if not default: default = {}
625         default.update( {'invoice_line_id': False, 'move_id': False, 'invoiced': False, 'state': 'draft'})
626         return super(mrp_repair_line, self).copy_data(cr, uid, id, default, context)
627
628     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
629         """ Calculates amount.
630         @param field_name: Name of field.
631         @param arg: Argument
632         @return: Dictionary of values.
633         """
634         res = {}
635         cur_obj=self.pool.get('res.currency')
636         for line in self.browse(cr, uid, ids, context=context):
637             res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0
638             cur = line.repair_id.pricelist_id.currency_id
639             res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
640         return res
641
642     _columns = {
643         'name' : fields.char('Description',size=64,required=True),
644         'repair_id': fields.many2one('mrp.repair', 'Repair Order Reference',ondelete='cascade', select=True),
645         'type': fields.selection([('add','Add'),('remove','Remove')],'Type', required=True),
646         'to_invoice': fields.boolean('To Invoice'),
647         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok','=',True)], required=True),
648         'invoiced': fields.boolean('Invoiced',readonly=True),
649         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price')),
650         'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal',digits_compute= dp.get_precision('Sale Price')),
651         'tax_id': fields.many2many('account.tax', 'repair_operation_line_tax', 'repair_operation_line_id', 'tax_id', 'Taxes'),
652         'product_uom_qty': fields.float('Quantity (UoM)', digits=(16,2), required=True),
653         'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
654         'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
655         'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True),
656         'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True, select=True),
657         'move_id': fields.many2one('stock.move', 'Inventory Move', readonly=True),
658         'state': fields.selection([
659                     ('draft','Draft'),
660                     ('confirmed','Confirmed'),
661                     ('done','Done'),
662                     ('cancel','Canceled')], 'State', required=True, readonly=True,
663                     help=' * The \'Draft\' state is set automatically as draft when repair order in draft state. \
664                         \n* The \'Confirmed\' state is set automatically as confirm when repair order in confirm state. \
665                         \n* The \'Done\' state is set automatically when repair order is completed.\
666                         \n* The \'Cancelled\' state is set automatically when user cancel repair order.'),
667     }
668     _defaults = {
669      'state': lambda *a: 'draft',
670      'product_uom_qty': lambda *a: 1,
671     }
672
673     def onchange_operation_type(self, cr, uid, ids, type, guarantee_limit):
674         """ On change of operation type it sets source location, destination location
675         and to invoice field.
676         @param product: Changed operation type.
677         @param guarantee_limit: Guarantee limit of current record.
678         @return: Dictionary of values.
679         """
680         if not type:
681             return {'value': {
682                 'location_id': False,
683                 'location_dest_id': False
684                 }
685             }
686
687         product_id = self.pool.get('stock.location').search(cr, uid, [('name','=','Production')])[0]
688         if type != 'add':
689             return {'value': {
690                 'to_invoice': False,
691                 'location_id': product_id,
692                 'location_dest_id': False
693                 }
694             }
695
696         stock_id = self.pool.get('stock.location').search(cr, uid, [('name','=','Stock')])[0]
697         to_invoice = (guarantee_limit and
698                       datetime.strptime(guarantee_limit, '%Y-%m-%d') < datetime.now())
699         return {'value': {
700             'to_invoice': to_invoice,
701             'location_id': stock_id,
702             'location_dest_id': product_id
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=None):
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, context=context):
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: