[Merge]with: lp:openobject-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)
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, 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, to repair'),
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, 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, 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, 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=int(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                 for line in o.operations:
331                     if line.product_id.track_production and not line.prodlot_id:
332                         raise osv.except_osv(_('Warning'), _("Production lot is required for opration line with product '%s'") % (line.product_id.name))
333                 mrp_line_obj.write(cr, uid, [l.id for l in o.operations], {'state': 'confirmed'})
334         return True
335
336     def action_cancel(self, cr, uid, ids, context=None):
337         """ Cancels repair order.
338         @return: True
339         """
340         mrp_line_obj = self.pool.get('mrp.repair.line')
341         for repair in self.browse(cr, uid, ids, context=context):
342             mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'cancel'}, context=context)
343         self.write(cr,uid,ids,{'state':'cancel'})
344         return True
345
346     def wkf_invoice_create(self, cr, uid, ids, *args):
347         return self.action_invoice_create(cr, uid, ids)
348
349     def action_invoice_create(self, cr, uid, ids, group=False, context=None):
350         """ Creates invoice(s) for repair order.
351         @param group: It is set to true when group invoice is to be generated.
352         @return: Invoice Ids.
353         """
354         res = {}
355         invoices_group = {}
356         inv_line_obj = self.pool.get('account.invoice.line')
357         inv_obj = self.pool.get('account.invoice')
358         repair_line_obj = self.pool.get('mrp.repair.line')
359         repair_fee_obj = self.pool.get('mrp.repair.fee')
360         for repair in self.browse(cr, uid, ids, context=context):
361             res[repair.id] = False
362             if repair.state in ('draft','cancel') or repair.invoice_id:
363                 continue
364             if not (repair.partner_id.id and repair.partner_invoice_id.id):
365                 raise osv.except_osv(_('No partner !'),_('You have to select a Partner Invoice Address in the repair form !'))
366             comment = repair.quotation_notes
367             if (repair.invoice_method != 'none'):
368                 if group and repair.partner_invoice_id.id in invoices_group:
369                     inv_id = invoices_group[repair.partner_invoice_id.id]
370                     invoice = inv_obj.browse(cr, uid, inv_id)
371                     invoice_vals = {
372                         'name': invoice.name +', '+repair.name,
373                         'origin': invoice.origin+', '+repair.name,
374                         'comment':(comment and (invoice.comment and invoice.comment+"\n"+comment or comment)) or (invoice.comment and invoice.comment or ''),
375                     }
376                     inv_obj.write(cr, uid, [inv_id], invoice_vals, context=context)
377                 else:
378                     if not repair.partner_id.property_account_receivable:
379                         raise osv.except_osv(_('Error !'), _('No account defined for partner "%s".') % repair.partner_id.name )
380                     account_id = repair.partner_id.property_account_receivable.id
381                     inv = {
382                         'name': repair.name,
383                         'origin':repair.name,
384                         'type': 'out_invoice',
385                         'account_id': account_id,
386                         'partner_id': repair.partner_id.id,
387                         'address_invoice_id': repair.address_id.id,
388                         'currency_id': repair.pricelist_id.currency_id.id,
389                         'comment': repair.quotation_notes,
390                         'fiscal_position': repair.partner_id.property_account_position.id
391                     }
392                     inv_id = inv_obj.create(cr, uid, inv)
393                     invoices_group[repair.partner_invoice_id.id] = inv_id
394                 self.write(cr, uid, repair.id, {'invoiced': True, 'invoice_id': inv_id})
395
396                 for operation in repair.operations:
397                     if operation.to_invoice == True:
398                         if group:
399                             name = repair.name + '-' + operation.name
400                         else:
401                             name = operation.name
402
403                         if operation.product_id.property_account_income:
404                             account_id = operation.product_id.property_account_income.id
405                         elif operation.product_id.categ_id.property_account_income_categ:
406                             account_id = operation.product_id.categ_id.property_account_income_categ.id
407                         else:
408                             raise osv.except_osv(_('Error !'), _('No account defined for product "%s".') % operation.product_id.name )
409
410                         invoice_line_id = inv_line_obj.create(cr, uid, {
411                             'invoice_id': inv_id,
412                             'name': name,
413                             'origin': repair.name,
414                             'account_id': account_id,
415                             'quantity': operation.product_uom_qty,
416                             'invoice_line_tax_id': [(6,0,[x.id for x in operation.tax_id])],
417                             'uos_id': operation.product_uom.id,
418                             'price_unit': operation.price_unit,
419                             'price_subtotal': operation.product_uom_qty*operation.price_unit,
420                             'product_id': operation.product_id and operation.product_id.id or False
421                         })
422                         repair_line_obj.write(cr, uid, [operation.id], {'invoiced': True, 'invoice_line_id': invoice_line_id})
423                 for fee in repair.fees_lines:
424                     if fee.to_invoice == True:
425                         if group:
426                             name = repair.name + '-' + fee.name
427                         else:
428                             name = fee.name
429                         if not fee.product_id:
430                             raise osv.except_osv(_('Warning !'), _('No product defined on Fees!'))
431
432                         if fee.product_id.property_account_income:
433                             account_id = fee.product_id.property_account_income.id
434                         elif fee.product_id.categ_id.property_account_income_categ:
435                             account_id = fee.product_id.categ_id.property_account_income_categ.id
436                         else:
437                             raise osv.except_osv(_('Error !'), _('No account defined for product "%s".') % fee.product_id.name)
438
439                         invoice_fee_id = inv_line_obj.create(cr, uid, {
440                             'invoice_id': inv_id,
441                             'name': name,
442                             'origin': repair.name,
443                             'account_id': account_id,
444                             'quantity': fee.product_uom_qty,
445                             'invoice_line_tax_id': [(6,0,[x.id for x in fee.tax_id])],
446                             'uos_id': fee.product_uom.id,
447                             'product_id': fee.product_id and fee.product_id.id or False,
448                             'price_unit': fee.price_unit,
449                             'price_subtotal': fee.product_uom_qty*fee.price_unit
450                         })
451                         repair_fee_obj.write(cr, uid, [fee.id], {'invoiced': True, 'invoice_line_id': invoice_fee_id})
452                 res[repair.id] = inv_id
453         return res
454
455     def action_repair_ready(self, cr, uid, ids, context=None):
456         """ Writes repair order state to 'Ready'
457         @return: True
458         """
459         for repair in self.browse(cr, uid, ids, context=context):
460             self.pool.get('mrp.repair.line').write(cr, uid, [l.id for
461                     l in repair.operations], {'state': 'confirmed'}, context=context)
462             self.write(cr, uid, [repair.id], {'state': 'ready'})
463         return True
464
465     def action_invoice_cancel(self, cr, uid, ids, context=None):
466         """ Writes repair order state to 'Exception in invoice'
467         @return: True
468         """
469         self.write(cr, uid, ids, {'state': 'invoice_except'})
470         return True
471
472     def action_repair_start(self, cr, uid, ids, context=None):
473         """ Writes repair order state to 'Under Repair'
474         @return: True
475         """
476         repair_line = self.pool.get('mrp.repair.line')
477         for repair in self.browse(cr, uid, ids, context=context):
478             repair_line.write(cr, uid, [l.id for
479                     l in repair.operations], {'state': 'confirmed'}, context=context)
480             repair.write({'state': 'under_repair'})
481         return True
482
483     def action_invoice_end(self, cr, uid, ids, context=None):
484         """ Writes repair order state to 'Ready' if invoice method is Before repair.
485         @return: True
486         """
487         repair_line = self.pool.get('mrp.repair.line')
488         for order in self.browse(cr, uid, ids, context=context):
489             val = {}
490             if (order.invoice_method == 'b4repair'):
491                 val['state'] = 'ready'
492                 repair_line.write(cr, uid, [l.id for
493                         l in order.operations], {'state': 'confirmed'}, context=context)
494             else:
495                 pass
496             self.write(cr, uid, [order.id], val, context=context)
497         return True
498
499     def action_repair_end(self, cr, uid, ids, context=None):
500         """ Writes repair order state to 'To be invoiced' if invoice method is
501         After repair else state is set to 'Ready'.
502         @return: True
503         """
504         for order in self.browse(cr, uid, ids, context=context):
505             val = {}
506             val['repaired'] = True
507             if (not order.invoiced and order.invoice_method=='after_repair'):
508                 val['state'] = '2binvoiced'
509             elif (not order.invoiced and order.invoice_method=='b4repair'):
510                 val['state'] = 'ready'
511             else:
512                 pass
513             self.write(cr, uid, [order.id], val)
514         return True
515
516     def wkf_repair_done(self, cr, uid, ids, *args):
517         self.action_repair_done(cr, uid, ids)
518         return True
519
520     def action_repair_done(self, cr, uid, ids, context=None):
521         """ Creates stock move and picking for repair order.
522         @return: Picking ids.
523         """
524         res = {}
525         move_obj = self.pool.get('stock.move')
526         wf_service = netsvc.LocalService("workflow")
527         repair_line_obj = self.pool.get('mrp.repair.line')
528         seq_obj = self.pool.get('ir.sequence')
529         pick_obj = self.pool.get('stock.picking')
530         for repair in self.browse(cr, uid, ids, context=context):
531             for move in repair.operations:
532                 move_id = move_obj.create(cr, uid, {
533                     'name': move.name,
534                     'product_id': move.product_id.id,
535                     'product_qty': move.product_uom_qty,
536                     'product_uom': move.product_uom.id,
537                     'address_id': repair.address_id and repair.address_id.id or False,
538                     'location_id': move.location_id.id,
539                     'location_dest_id': move.location_dest_id.id,
540                     'tracking_id': False,
541                     'prodlot_id': move.prodlot_id and move.prodlot_id.id or False,
542                     'state': 'done',
543                 })
544                 repair_line_obj.write(cr, uid, [move.id], {'move_id': move_id, 'state': 'done'}, context=context)
545             if repair.deliver_bool:
546                 pick_name = seq_obj.get(cr, uid, 'stock.picking.out')
547                 picking = pick_obj.create(cr, uid, {
548                     'name': pick_name,
549                     'origin': repair.name,
550                     'state': 'draft',
551                     'move_type': 'one',
552                     'address_id': repair.address_id and repair.address_id.id or False,
553                     'note': repair.internal_notes,
554                     'invoice_state': 'none',
555                     'type': 'out',
556                 })
557                 move_id = move_obj.create(cr, uid, {
558                     'name': repair.name,
559                     'picking_id': picking,
560                     'product_id': repair.product_id.id,
561                     'product_qty': move.product_uom_qty or 1.0,
562                     'product_uom': repair.product_id.uom_id.id,
563                     'prodlot_id': repair.prodlot_id and repair.prodlot_id.id or False,
564                     'address_id': repair.address_id and repair.address_id.id or False,
565                     'location_id': repair.location_id.id,
566                     'location_dest_id': repair.location_dest_id.id,
567                     'tracking_id': False,
568                     'state': 'assigned',
569                 })
570                 wf_service.trg_validate(uid, 'stock.picking', picking, 'button_confirm', cr)
571                 self.write(cr, uid, [repair.id], {'state': 'done', 'picking_id': picking})
572                 res[repair.id] = picking
573             else:
574                 self.write(cr, uid, [repair.id], {'state': 'done'})
575         return res
576
577
578 mrp_repair()
579
580
581 class ProductChangeMixin(object):
582     def product_id_change(self, cr, uid, ids, pricelist, product, uom=False,
583                           product_uom_qty=0, partner_id=False, guarantee_limit=False):
584         """ On change of product it sets product quantity, tax account, name,
585         uom of product, unit price and price subtotal.
586         @param pricelist: Pricelist of current record.
587         @param product: Changed id of product.
588         @param uom: UoM of current record.
589         @param product_uom_qty: Quantity of current record.
590         @param partner_id: Partner of current record.
591         @param guarantee_limit: Guarantee limit of current record.
592         @return: Dictionary of values and warning message.
593         """
594         result = {}
595         warning = {}
596
597         if not product_uom_qty:
598             product_uom_qty = 1
599         result['product_uom_qty'] = product_uom_qty
600
601         if product:
602             product_obj = self.pool.get('product.product').browse(cr, uid, product)
603             if partner_id:
604                 partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
605                 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, partner.property_account_position, product_obj.taxes_id)
606
607             result['name'] = product_obj.partner_ref
608             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id or False
609             if not pricelist:
610                 warning = {
611                     'title':'No Pricelist !',
612                     'message':
613                         'You have to select a pricelist in the Repair form !\n'
614                         'Please set one before choosing a product.'
615                 }
616             else:
617                 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
618                             product, product_uom_qty, partner_id, {'uom': uom,})[pricelist]
619
620                 if price is False:
621                      warning = {
622                         'title':'No valid pricelist line found !',
623                         'message':
624                             "Couldn't find a pricelist line matching this product and quantity.\n"
625                             "You have to change either the product, the quantity or the pricelist."
626                      }
627                 else:
628                     result.update({'price_unit': price, 'price_subtotal': price*product_uom_qty})
629
630         return {'value': result, 'warning': warning}
631
632
633 class mrp_repair_line(osv.osv, ProductChangeMixin):
634     _name = 'mrp.repair.line'
635     _description = 'Repair Line'
636
637     def copy_data(self, cr, uid, id, default=None, context=None):
638         if not default: default = {}
639         default.update( {'invoice_line_id': False, 'move_id': False, 'invoiced': False, 'state': 'draft'})
640         return super(mrp_repair_line, self).copy_data(cr, uid, id, default, context)
641
642     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
643         """ Calculates amount.
644         @param field_name: Name of field.
645         @param arg: Argument
646         @return: Dictionary of values.
647         """
648         res = {}
649         cur_obj=self.pool.get('res.currency')
650         for line in self.browse(cr, uid, ids, context=context):
651             res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0
652             cur = line.repair_id.pricelist_id.currency_id
653             res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
654         return res
655
656     _columns = {
657         'name' : fields.char('Description',size=64,required=True),
658         'repair_id': fields.many2one('mrp.repair', 'Repair Order Reference',ondelete='cascade', select=True),
659         'type': fields.selection([('add','Add'),('remove','Remove')],'Type', required=True),
660         'to_invoice': fields.boolean('To Invoice'),
661         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok','=',True)], required=True),
662         'invoiced': fields.boolean('Invoiced',readonly=True),
663         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price')),
664         'price_subtotal': fields.function(_amount_line, string='Subtotal',digits_compute= dp.get_precision('Sale Price')),
665         'tax_id': fields.many2many('account.tax', 'repair_operation_line_tax', 'repair_operation_line_id', 'tax_id', 'Taxes'),
666         'product_uom_qty': fields.float('Quantity (UoM)', digits=(16,2), required=True),
667         'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
668         'prodlot_id': fields.many2one('stock.production.lot', 'Lot Number',domain="[('product_id','=',product_id)]"),
669         'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
670         'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True),
671         'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True, select=True),
672         'move_id': fields.many2one('stock.move', 'Inventory Move', readonly=True),
673         'state': fields.selection([
674                     ('draft','Draft'),
675                     ('confirmed','Confirmed'),
676                     ('done','Done'),
677                     ('cancel','Canceled')], 'State', required=True, readonly=True,
678                     help=' * The \'Draft\' state is set automatically as draft when repair order in draft state. \
679                         \n* The \'Confirmed\' state is set automatically as confirm when repair order in confirm state. \
680                         \n* The \'Done\' state is set automatically when repair order is completed.\
681                         \n* The \'Cancelled\' state is set automatically when user cancel repair order.'),
682     }
683     _defaults = {
684      'state': lambda *a: 'draft',
685      'product_uom_qty': lambda *a: 1,
686     }
687
688     def onchange_operation_type(self, cr, uid, ids, type, guarantee_limit):
689         """ On change of operation type it sets source location, destination location
690         and to invoice field.
691         @param product: Changed operation type.
692         @param guarantee_limit: Guarantee limit of current record.
693         @return: Dictionary of values.
694         """
695         if not type:
696             return {'value': {
697                 'location_id': False,
698                 'location_dest_id': False
699                 }
700             }
701
702         product_id = self.pool.get('stock.location').search(cr, uid, [('name','=','Production')])[0]
703         if type != 'add':
704             return {'value': {
705                 'to_invoice': False,
706                 'location_id': product_id,
707                 'location_dest_id': False
708                 }
709             }
710
711         stock_id = self.pool.get('stock.location').search(cr, uid, [('name','=','Stock')])[0]
712         to_invoice = (guarantee_limit and
713                       datetime.strptime(guarantee_limit, '%Y-%m-%d') < datetime.now())
714         return {'value': {
715             'to_invoice': to_invoice,
716             'location_id': stock_id,
717             'location_dest_id': product_id
718             }
719         }
720
721 mrp_repair_line()
722
723 class mrp_repair_fee(osv.osv, ProductChangeMixin):
724     _name = 'mrp.repair.fee'
725     _description = 'Repair Fees Line'
726
727     def copy_data(self, cr, uid, id, default=None, context=None):
728         if not default: default = {}
729         default.update({'invoice_line_id': False, 'invoiced': False})
730         return super(mrp_repair_fee, self).copy_data(cr, uid, id, default, context)
731
732     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
733         """ Calculates amount.
734         @param field_name: Name of field.
735         @param arg: Argument
736         @return: Dictionary of values.
737         """
738         res = {}
739         cur_obj = self.pool.get('res.currency')
740         for line in self.browse(cr, uid, ids, context=context):
741             res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0
742             cur = line.repair_id.pricelist_id.currency_id
743             res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
744         return res
745
746     _columns = {
747         'repair_id': fields.many2one('mrp.repair', 'Repair Order Reference', required=True, ondelete='cascade', select=True),
748         'name': fields.char('Description', size=64, select=True,required=True),
749         'product_id': fields.many2one('product.product', 'Product'),
750         'product_uom_qty': fields.float('Quantity', digits=(16,2), required=True),
751         'price_unit': fields.float('Unit Price', required=True),
752         'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
753         'price_subtotal': fields.function(_amount_line, string='Subtotal',digits_compute= dp.get_precision('Sale Price')),
754         'tax_id': fields.many2many('account.tax', 'repair_fee_line_tax', 'repair_fee_line_id', 'tax_id', 'Taxes'),
755         'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
756         'to_invoice': fields.boolean('To Invoice'),
757         'invoiced': fields.boolean('Invoiced',readonly=True),
758     }
759     _defaults = {
760         'to_invoice': lambda *a: True,
761     }
762
763 mrp_repair_fee()
764 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: