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