7bab1c04c99b0d9c171a59cd3c4d45d4491ea8fb
[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 openerp.osv import fields,osv
23 from openerp import netsvc
24 from datetime import datetime
25 from dateutil.relativedelta import relativedelta
26 from openerp.tools.translate import _
27 import openerp.addons.decimal_precision as dp
28
29 class mrp_repair(osv.osv):
30     _name = 'mrp.repair'
31     _inherit = 'mail.thread'
32     _description = 'Repair Order'
33
34     def _amount_untaxed(self, cr, uid, ids, field_name, arg, context=None):
35         """ Calculates untaxed amount.
36         @param self: The object pointer
37         @param cr: The current row, from the database cursor,
38         @param uid: The current user ID for security checks
39         @param ids: List of selected IDs
40         @param field_name: Name of field.
41         @param arg: Argument
42         @param context: A standard dictionary for contextual values
43         @return: Dictionary of values.
44         """
45         res = {}
46         cur_obj = self.pool.get('res.currency')
47
48         for repair in self.browse(cr, uid, ids, context=context):
49             res[repair.id] = 0.0
50             for line in repair.operations:
51                 res[repair.id] += line.price_subtotal
52             for line in repair.fees_lines:
53                 res[repair.id] += line.price_subtotal
54             cur = repair.pricelist_id.currency_id
55             res[repair.id] = cur_obj.round(cr, uid, cur, res[repair.id])
56         return res
57
58     def _amount_tax(self, cr, uid, ids, field_name, arg, context=None):
59         """ Calculates taxed amount.
60         @param field_name: Name of field.
61         @param arg: Argument
62         @return: Dictionary of values.
63         """
64         res = {}
65         #return {}.fromkeys(ids, 0)
66         cur_obj = self.pool.get('res.currency')
67         tax_obj = self.pool.get('account.tax')
68         for repair in self.browse(cr, uid, ids, context=context):
69             val = 0.0
70             cur = repair.pricelist_id.currency_id
71             for line in repair.operations:
72                 #manage prices with tax included use compute_all instead of compute
73                 if line.to_invoice:
74                     tax_calculate = tax_obj.compute_all(cr, uid, line.tax_id, line.price_unit, line.product_uom_qty, line.product_id, repair.partner_id)
75                     for c in tax_calculate['taxes']:
76                         val += c['amount']
77             for line in repair.fees_lines:
78                 if line.to_invoice:
79                     tax_calculate = tax_obj.compute_all(cr, uid, line.tax_id, line.price_unit, line.product_uom_qty,  line.product_id, repair.partner_id)
80                     for c in tax_calculate['taxes']:
81                         val += c['amount']
82             res[repair.id] = cur_obj.round(cr, uid, cur, val)
83         return res
84
85     def _amount_total(self, cr, uid, ids, field_name, arg, context=None):
86         """ Calculates total amount.
87         @param field_name: Name of field.
88         @param arg: Argument
89         @return: Dictionary of values.
90         """
91         res = {}
92         untax = self._amount_untaxed(cr, uid, ids, field_name, arg, context=context)
93         tax = self._amount_tax(cr, uid, ids, field_name, arg, context=context)
94         cur_obj = self.pool.get('res.currency')
95         for id in ids:
96             repair = self.browse(cr, uid, id, context=context)
97             cur = repair.pricelist_id.currency_id
98             res[id] = cur_obj.round(cr, uid, cur, untax.get(id, 0.0) + tax.get(id, 0.0))
99         return res
100
101     def _get_default_address(self, cr, uid, ids, field_name, arg, context=None):
102         res = {}
103         partner_obj = self.pool.get('res.partner')
104         for data in self.browse(cr, uid, ids, context=context):
105             adr_id = False
106             if data.partner_id:
107                 adr_id = partner_obj.address_get(cr, uid, [data.partner_id.id], ['default'])['default']
108             res[data.id] = adr_id
109         return res
110
111     def _get_lines(self, cr, uid, ids, context=None):
112         result = {}
113         for line in self.pool.get('mrp.repair.line').browse(cr, uid, ids, context=context):
114             result[line.repair_id.id] = True
115         return result.keys()
116
117     _columns = {
118         'name': fields.char('Repair Reference',size=24, required=True, states={'confirmed':[('readonly',True)]}),
119         'product_id': fields.many2one('product.product', string='Product to Repair', required=True, readonly=True, states={'draft':[('readonly',False)]}),
120         'partner_id' : fields.many2one('res.partner', 'Partner', select=True, help='Choose partner for whom the order will be invoiced and delivered.', states={'confirmed':[('readonly',True)]}),
121         'address_id': fields.many2one('res.partner', 'Delivery Address', domain="[('parent_id','=',partner_id)]", states={'confirmed':[('readonly',True)]}),
122         'default_address_id': fields.function(_get_default_address, type="many2one", relation="res.partner"),
123         'prodlot_id': fields.many2one('stock.production.lot', 'Lot Number', select=True, states={'draft':[('readonly',False)]},domain="[('product_id','=',product_id)]"),
124         'state': fields.selection([
125             ('draft','Quotation'),
126             ('cancel','Cancelled'),
127             ('confirmed','Confirmed'),
128             ('under_repair','Under Repair'),
129             ('ready','Ready to Repair'),
130             ('2binvoiced','To be Invoiced'),
131             ('invoice_except','Invoice Exception'),
132             ('done','Repaired')
133             ], 'Status', readonly=True, track_visibility='onchange',
134             help=' * The \'Draft\' status is used when a user is encoding a new and unconfirmed repair order. \
135             \n* The \'Confirmed\' status is used when a user confirms the repair order. \
136             \n* The \'Ready to Repair\' status is used to start to repairing, user can start repairing only after repair order is confirmed. \
137             \n* The \'To be Invoiced\' status is used to generate the invoice before or after repairing done. \
138             \n* The \'Done\' status is set when repairing is completed.\
139             \n* The \'Cancelled\' status is used when user cancel repair order.'),
140         'location_id': fields.many2one('stock.location', 'Current Location', select=True, readonly=True, states={'draft':[('readonly',False)], 'confirmed':[('readonly',True)]}),
141         'location_dest_id': fields.many2one('stock.location', 'Delivery Location', readonly=True, states={'draft':[('readonly',False)], 'confirmed':[('readonly',True)]}),
142         'move_id': fields.many2one('stock.move', 'Move',required=True, domain="[('product_id','=',product_id)]", readonly=True, states={'draft':[('readonly',False)]}),
143         'guarantee_limit': fields.date('Warranty Expiration', help="The warranty expiration limit is computed as: last move date + warranty defined on selected product. If the current date is below the warranty expiration limit, each operation and fee you will add will be set as 'not to invoiced' by default. Note that you can change manually afterwards.", states={'confirmed':[('readonly',True)]}),
144         'operations' : fields.one2many('mrp.repair.line', 'repair_id', 'Operation Lines', readonly=True, states={'draft':[('readonly',False)]}),
145         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', help='Pricelist of the selected partner.'),
146         'partner_invoice_id':fields.many2one('res.partner', 'Invoicing Address'),
147         'invoice_method':fields.selection([
148             ("none","No Invoice"),
149             ("b4repair","Before Repair"),
150             ("after_repair","After Repair")
151            ], "Invoice Method",
152             select=True, required=True, states={'draft':[('readonly',False)]}, readonly=True, help='Selecting \'Before Repair\' or \'After Repair\' will allow you to generate invoice before or after the repair is done respectively. \'No invoice\' means you don\'t want to generate invoice for this repair order.'),
153         'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True),
154         'picking_id': fields.many2one('stock.picking', 'Picking',readonly=True),
155         'fees_lines': fields.one2many('mrp.repair.fee', 'repair_id', 'Fees Lines', readonly=True, states={'draft':[('readonly',False)]}),
156         'internal_notes': fields.text('Internal Notes'),
157         'quotation_notes': fields.text('Quotation Notes'),
158         'company_id': fields.many2one('res.company', 'Company'),
159         'deliver_bool': fields.boolean('Deliver', help="Check this box if you want to manage the delivery once the product is repaired and create a picking with selected product. Note that you can select the locations in the Info tab, if you have the extended view.", states={'confirmed':[('readonly',True)]}),
160         'invoiced': fields.boolean('Invoiced', readonly=True),
161         'repaired': fields.boolean('Repaired', readonly=True),
162         'amount_untaxed': fields.function(_amount_untaxed, string='Untaxed Amount',
163             store={
164                 'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations'], 10),
165                 'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
166             }),
167         'amount_tax': fields.function(_amount_tax, string='Taxes',
168             store={
169                 'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations'], 10),
170                 'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
171             }),
172         'amount_total': fields.function(_amount_total, string='Total',
173             store={
174                 'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations'], 10),
175                 'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
176             }),
177     }
178
179     _defaults = {
180         'state': lambda *a: 'draft',
181         'deliver_bool': lambda *a: True,
182         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'mrp.repair'),
183         'invoice_method': lambda *a: 'none',
184         'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.repair', context=context),
185         'pricelist_id': lambda self, cr, uid,context : self.pool.get('product.pricelist').search(cr, uid, [('type','=','sale')])[0]
186     }
187
188     def copy(self, cr, uid, id, default=None, context=None):
189         if not default:
190             default = {}
191         default.update({
192             'state':'draft',
193             'repaired':False,
194             'invoiced':False,
195             'invoice_id': False,
196             'picking_id': False,
197             'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.repair'),
198         })
199         return super(mrp_repair, self).copy(cr, uid, id, default, context)
200
201     def onchange_product_id(self, cr, uid, ids, product_id=None):
202         """ On change of product sets some values.
203         @param product_id: Changed product
204         @return: Dictionary of values.
205         """
206         return {'value': {
207                     'prodlot_id': False,
208                     'move_id': False,
209                     'guarantee_limit' :False,
210                     'location_id':  False,
211                     'location_dest_id': False,
212                 }
213         }
214
215     def onchange_move_id(self, cr, uid, ids, prod_id=False, move_id=False):
216         """ On change of move id sets values of guarantee limit, source location,
217         destination location, partner and partner address.
218         @param prod_id: Id of product in current record.
219         @param move_id: Changed move.
220         @return: Dictionary of values.
221         """
222         data = {}
223         data['value'] = {'guarantee_limit': False, 'location_id': False, 'prodlot_id': False, 'partner_id': False}
224         if not prod_id:
225             return data
226         if move_id:
227             move =  self.pool.get('stock.move').browse(cr, uid, move_id)
228             product = self.pool.get('product.product').browse(cr, uid, prod_id)
229             limit = datetime.strptime(move.date_expected, '%Y-%m-%d %H:%M:%S') + relativedelta(months=int(product.warranty))
230             data['value']['guarantee_limit'] = limit.strftime('%Y-%m-%d')
231             data['value']['location_id'] = move.location_dest_id.id
232             data['value']['location_dest_id'] = move.location_dest_id.id
233             data['value']['prodlot_id'] = move.prodlot_id.id
234             if move.partner_id:
235                 data['value']['partner_id'] = move.partner_id.id
236             else:
237                 data['value']['partner_id'] = False
238             d = self.onchange_partner_id(cr, uid, ids, data['value']['partner_id'], data['value']['partner_id'])
239             data['value'].update(d['value'])
240         return data
241
242     def button_dummy(self, cr, uid, ids, context=None):
243         return True
244
245     def onchange_partner_id(self, cr, uid, ids, part, address_id):
246         """ On change of partner sets the values of partner address,
247         partner invoice address and pricelist.
248         @param part: Changed id of partner.
249         @param address_id: Address id from current record.
250         @return: Dictionary of values.
251         """
252         part_obj = self.pool.get('res.partner')
253         pricelist_obj = self.pool.get('product.pricelist')
254         if not part:
255             return {'value': {
256                         'address_id': False,
257                         'partner_invoice_id': False,
258                         'pricelist_id': pricelist_obj.search(cr, uid, [('type','=','sale')])[0]
259                     }
260             }
261         addr = part_obj.address_get(cr, uid, [part], ['delivery', 'invoice', 'default'])
262         partner = part_obj.browse(cr, uid, part)
263         pricelist = partner.property_product_pricelist and partner.property_product_pricelist.id or False
264         return {'value': {
265                     'address_id': addr['delivery'] or addr['default'],
266                     'partner_invoice_id': addr['invoice'],
267                     'pricelist_id': pricelist
268                 }
269         }
270
271     def onchange_lot_id(self, cr, uid, ids, lot, product_id):
272         """ On change of Serial Number sets the values of source location,
273         destination location, move and guarantee limit.
274         @param lot: Changed id of Serial Number.
275         @param product_id: Product id from current record.
276         @return: Dictionary of values.
277         """
278         move_obj = self.pool.get('stock.move')
279         data = {}
280         data['value'] = {
281             'location_id': False,
282             'location_dest_id': False,
283             'move_id': False,
284             'guarantee_limit': False
285         }
286
287         if not lot:
288             return data
289         move_ids = move_obj.search(cr, uid, [('prodlot_id', '=', lot)])
290
291         if not len(move_ids):
292             return data
293
294         def get_last_move(lst_move):
295             while lst_move.move_dest_id and lst_move.move_dest_id.state == 'done':
296                 lst_move = lst_move.move_dest_id
297             return lst_move
298
299         move_id = move_ids[0]
300         move = get_last_move(move_obj.browse(cr, uid, move_id))
301         data['value']['move_id'] = move.id
302         d = self.onchange_move_id(cr, uid, ids, product_id, move.id)
303         data['value'].update(d['value'])
304         return data
305
306     def action_cancel_draft(self, cr, uid, ids, *args):
307         """ Cancels repair order when it is in 'Draft' state.
308         @param *arg: Arguments
309         @return: True
310         """
311         if not len(ids):
312             return False
313         mrp_line_obj = self.pool.get('mrp.repair.line')
314         for repair in self.browse(cr, uid, ids):
315             mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'draft'})
316         self.write(cr, uid, ids, {'state':'draft'})
317         wf_service = netsvc.LocalService("workflow")
318         for id in ids:
319             wf_service.trg_create(uid, 'mrp.repair', id, cr)
320         return True
321
322     def action_confirm(self, cr, uid, ids, *args):
323         """ Repair order state is set to 'To be invoiced' when invoice method
324         is 'Before repair' else state becomes 'Confirmed'.
325         @param *arg: Arguments
326         @return: True
327         """
328         mrp_line_obj = self.pool.get('mrp.repair.line')
329         for o in self.browse(cr, uid, ids):
330             if (o.invoice_method == 'b4repair'):
331                 self.write(cr, uid, [o.id], {'state': '2binvoiced'})
332             else:
333                 self.write(cr, uid, [o.id], {'state': 'confirmed'})
334                 for line in o.operations:
335                     if line.product_id.track_production and not line.prodlot_id:
336                         raise osv.except_osv(_('Warning!'), _("Serial number is required for operation line with product '%s'") % (line.product_id.name))
337                 mrp_line_obj.write(cr, uid, [l.id for l in o.operations], {'state': 'confirmed'})
338         return True
339
340     def action_cancel(self, cr, uid, ids, context=None):
341         """ Cancels repair order.
342         @return: True
343         """
344         mrp_line_obj = self.pool.get('mrp.repair.line')
345         for repair in self.browse(cr, uid, ids, context=context):
346             if not repair.invoiced:
347                 mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'cancel'}, context=context)
348             else:
349                 raise osv.except_osv(_('Warning!'),_('Repair order is already invoiced.'))
350         return self.write(cr,uid,ids,{'state':'cancel'})
351
352     def wkf_invoice_create(self, cr, uid, ids, *args):
353         return self.action_invoice_create(cr, uid, ids)
354
355     def action_invoice_create(self, cr, uid, ids, group=False, context=None):
356         """ Creates invoice(s) for repair order.
357         @param group: It is set to true when group invoice is to be generated.
358         @return: Invoice Ids.
359         """
360         res = {}
361         invoices_group = {}
362         inv_line_obj = self.pool.get('account.invoice.line')
363         inv_obj = self.pool.get('account.invoice')
364         repair_line_obj = self.pool.get('mrp.repair.line')
365         repair_fee_obj = self.pool.get('mrp.repair.fee')
366         for repair in self.browse(cr, uid, ids, context=context):
367             res[repair.id] = False
368             if repair.state in ('draft','cancel') or repair.invoice_id:
369                 continue
370             if not (repair.partner_id.id and repair.partner_invoice_id.id):
371                 raise osv.except_osv(_('No partner!'),_('You have to select a Partner Invoice Address in the repair form!'))
372             comment = repair.quotation_notes
373             if (repair.invoice_method != 'none'):
374                 if group and repair.partner_invoice_id.id in invoices_group:
375                     inv_id = invoices_group[repair.partner_invoice_id.id]
376                     invoice = inv_obj.browse(cr, uid, inv_id)
377                     invoice_vals = {
378                         'name': invoice.name +', '+repair.name,
379                         'origin': invoice.origin+', '+repair.name,
380                         'comment':(comment and (invoice.comment and invoice.comment+"\n"+comment or comment)) or (invoice.comment and invoice.comment or ''),
381                     }
382                     inv_obj.write(cr, uid, [inv_id], invoice_vals, context=context)
383                 else:
384                     if not repair.partner_id.property_account_receivable:
385                         raise osv.except_osv(_('Error!'), _('No account defined for partner "%s".') % repair.partner_id.name )
386                     account_id = repair.partner_id.property_account_receivable.id
387                     inv = {
388                         'name': repair.name,
389                         'origin':repair.name,
390                         'type': 'out_invoice',
391                         'account_id': account_id,
392                         'partner_id': repair.partner_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                     'partner_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                     'partner_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_uom': repair.product_id.uom_id.id,
544                     'prodlot_id': repair.prodlot_id and repair.prodlot_id.id or False,
545                     'partner_id': repair.address_id and repair.address_id.id or False,
546                     'location_id': repair.location_id.id,
547                     'location_dest_id': repair.location_dest_id.id,
548                     'tracking_id': False,
549                     'state': 'assigned',
550                 })
551                 wf_service.trg_validate(uid, 'stock.picking', picking, 'button_confirm', cr)
552                 self.write(cr, uid, [repair.id], {'state': 'done', 'picking_id': picking})
553                 res[repair.id] = picking
554             else:
555                 self.write(cr, uid, [repair.id], {'state': 'done'})
556         return res
557
558
559 class ProductChangeMixin(object):
560     def product_id_change(self, cr, uid, ids, pricelist, product, uom=False,
561                           product_uom_qty=0, partner_id=False, guarantee_limit=False):
562         """ On change of product it sets product quantity, tax account, name,
563         uom of product, unit price and price subtotal.
564         @param pricelist: Pricelist of current record.
565         @param product: Changed id of product.
566         @param uom: UoM of current record.
567         @param product_uom_qty: Quantity of current record.
568         @param partner_id: Partner of current record.
569         @param guarantee_limit: Guarantee limit of current record.
570         @return: Dictionary of values and warning message.
571         """
572         result = {}
573         warning = {}
574
575         if not product_uom_qty:
576             product_uom_qty = 1
577         result['product_uom_qty'] = product_uom_qty
578
579         if product:
580             product_obj = self.pool.get('product.product').browse(cr, uid, product)
581             if partner_id:
582                 partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
583                 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, partner.property_account_position, product_obj.taxes_id)
584
585             result['name'] = product_obj.partner_ref
586             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id or False
587             if not pricelist:
588                 warning = {
589                     'title':'No Pricelist!',
590                     'message':
591                         'You have to select a pricelist in the Repair form !\n'
592                         'Please set one before choosing a product.'
593                 }
594             else:
595                 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
596                             product, product_uom_qty, partner_id, {'uom': uom,})[pricelist]
597
598                 if price is False:
599                      warning = {
600                         'title':'No valid pricelist line found !',
601                         'message':
602                             "Couldn't find a pricelist line matching this product and quantity.\n"
603                             "You have to change either the product, the quantity or the pricelist."
604                      }
605                 else:
606                     result.update({'price_unit': price, 'price_subtotal': price*product_uom_qty})
607
608         return {'value': result, 'warning': warning}
609
610
611 class mrp_repair_line(osv.osv, ProductChangeMixin):
612     _name = 'mrp.repair.line'
613     _description = 'Repair Line'
614
615     def copy_data(self, cr, uid, id, default=None, context=None):
616         if not default: default = {}
617         default.update( {'invoice_line_id': False, 'move_id': False, 'invoiced': False, 'state': 'draft'})
618         return super(mrp_repair_line, self).copy_data(cr, uid, id, default, context)
619
620     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
621         """ Calculates amount.
622         @param field_name: Name of field.
623         @param arg: Argument
624         @return: Dictionary of values.
625         """
626         res = {}
627         cur_obj=self.pool.get('res.currency')
628         for line in self.browse(cr, uid, ids, context=context):
629             res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0
630             cur = line.repair_id.pricelist_id.currency_id
631             res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
632         return res
633
634     _columns = {
635         'name' : fields.char('Description',size=64,required=True),
636         'repair_id': fields.many2one('mrp.repair', 'Repair Order Reference',ondelete='cascade', select=True),
637         'type': fields.selection([('add','Add'),('remove','Remove')],'Type', required=True),
638         'to_invoice': fields.boolean('To Invoice'),
639         'product_id': fields.many2one('product.product', 'Product', required=True),
640         'invoiced': fields.boolean('Invoiced',readonly=True),
641         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price')),
642         'price_subtotal': fields.function(_amount_line, string='Subtotal',digits_compute= dp.get_precision('Account')),
643         'tax_id': fields.many2many('account.tax', 'repair_operation_line_tax', 'repair_operation_line_id', 'tax_id', 'Taxes'),
644         'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product Unit of Measure'), required=True),
645         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
646         'prodlot_id': fields.many2one('stock.production.lot', 'Lot Number',domain="[('product_id','=',product_id)]"),
647         'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
648         'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True),
649         'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True, select=True),
650         'move_id': fields.many2one('stock.move', 'Inventory Move', readonly=True),
651         'state': fields.selection([
652                     ('draft','Draft'),
653                     ('confirmed','Confirmed'),
654                     ('done','Done'),
655                     ('cancel','Cancelled')], 'Status', required=True, readonly=True,
656                     help=' * The \'Draft\' status is set automatically as draft when repair order in draft status. \
657                         \n* The \'Confirmed\' status is set automatically as confirm when repair order in confirm status. \
658                         \n* The \'Done\' status is set automatically when repair order is completed.\
659                         \n* The \'Cancelled\' status is set automatically when user cancel repair order.'),
660     }
661     _defaults = {
662      'state': lambda *a: 'draft',
663      'product_uom_qty': lambda *a: 1,
664     }
665
666     def onchange_operation_type(self, cr, uid, ids, type, guarantee_limit, company_id=False, context=None):
667         """ On change of operation type it sets source location, destination location
668         and to invoice field.
669         @param product: Changed operation type.
670         @param guarantee_limit: Guarantee limit of current record.
671         @return: Dictionary of values.
672         """
673         if not type:
674             return {'value': {
675                 'location_id': False,
676                 'location_dest_id': False
677                 }}
678         location_obj = self.pool.get('stock.location')
679         warehouse_obj = self.pool.get('stock.warehouse')
680         location_id = location_obj.search(cr, uid, [('usage','=','production')], context=context)
681         location_id = location_id and location_id[0] or False
682
683         if type == 'add':
684             # TOCHECK: Find stock location for user's company warehouse or
685             # repair order's company's warehouse (company_id field is added in fix of lp:831583)
686             args = company_id and [('company_id', '=', company_id)] or []
687             warehouse_ids = warehouse_obj.search(cr, uid, args, context=context)
688             stock_id = False
689             if warehouse_ids:
690                 stock_id = warehouse_obj.browse(cr, uid, warehouse_ids[0], context=context).lot_stock_id.id
691             to_invoice = (guarantee_limit and datetime.strptime(guarantee_limit, '%Y-%m-%d') < datetime.now())
692
693             return {'value': {
694                 'to_invoice': to_invoice,
695                 'location_id': stock_id,
696                 'location_dest_id': location_id
697                 }}
698         scrap_location_ids = location_obj.search(cr, uid, [('scrap_location', '=', True)], context=context)
699
700         return {'value': {
701                 'to_invoice': False,
702                 'location_id': location_id,
703                 'location_dest_id': scrap_location_ids and scrap_location_ids[0] or False,
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_compute= dp.get_precision('Product Unit of Measure'), required=True),
736         'price_unit': fields.float('Unit Price', required=True),
737         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
738         'price_subtotal': fields.function(_amount_line, string='Subtotal',digits_compute= dp.get_precision('Account')),
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: