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