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