[TYPO] Set the right category for the Point Of Sale
[odoo/odoo.git] / addons / mrp_repair / mrp_repair.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from osv import fields,osv
23 import netsvc
24 from datetime import datetime
25 from dateutil.relativedelta import relativedelta
26 from tools.translate import _
27 import decimal_precision as dp
28
29 class mrp_repair(osv.osv):
30     _name = 'mrp.repair'
31     _description = 'Repair Order'
32
33     def _amount_untaxed(self, cr, uid, ids, field_name, arg, context=None):
34         """ Calculates untaxed amount.
35         @param self: The object pointer
36         @param cr: The current row, from the database cursor,
37         @param uid: The current user ID for security checks
38         @param ids: List of selected IDs
39         @param field_name: Name of field.
40         @param arg: Argument
41         @param context: A standard dictionary for contextual values
42         @return: Dictionary of values.
43         """
44         res = {}
45         cur_obj = self.pool.get('res.currency')
46
47         for repair in self.browse(cr, uid, ids, context=context):
48             res[repair.id] = 0.0
49             for line in repair.operations:
50                 res[repair.id] += line.price_subtotal
51             for line in repair.fees_lines:
52                 res[repair.id] += line.price_subtotal
53             cur = repair.pricelist_id.currency_id
54             res[repair.id] = cur_obj.round(cr, uid, cur, res[repair.id])
55         return res
56
57     def _amount_tax(self, cr, uid, ids, field_name, arg, context=None):
58         """ Calculates taxed amount.
59         @param field_name: Name of field.
60         @param arg: Argument
61         @return: Dictionary of values.
62         """
63         res = {}
64         #return {}.fromkeys(ids, 0)
65         cur_obj = self.pool.get('res.currency')
66         tax_obj = self.pool.get('account.tax')
67         for repair in self.browse(cr, uid, ids, context=context):
68             val = 0.0
69             cur = repair.pricelist_id.currency_id
70             for line in repair.operations:
71                 #manage prices with tax included use compute_all instead of compute
72                 if line.to_invoice:
73                     tax_calculate = tax_obj.compute_all(cr, uid, line.tax_id, line.price_unit, line.product_uom_qty, 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),
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.'),
120         'address_id': fields.many2one('res.partner', 'Delivery Address', domain="[('parent_id','=',partner_id)]"),
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, domain="[('product_id','=',product_id)]"),
123         'state': fields.selection([
124             ('draft','Quotation'),
125             ('cancel','Cancel'),
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','Done')
132             ], 'Status', readonly=True,
133             help=' * The \'Draft\' state is used when a user is encoding a new and unconfirmed repair order. \
134             \n* The \'Confirmed\' state is used when a user confirms the repair order. \
135             \n* The \'Ready to Repair\' state is used to start to repairing, user can start repairing only after repair order is confirmed. \
136             \n* The \'To be Invoiced\' state is used to generate the invoice before or after repairing done. \
137             \n* The \'Done\' state is set when repairing is completed.\
138             \n* The \'Cancelled\' state is used when user cancel repair order.'),
139         'location_id': fields.many2one('stock.location', 'Current Location', select=True, readonly=True, states={'draft':[('readonly',False)]}),
140         'location_dest_id': fields.many2one('stock.location', 'Delivery Location', readonly=True, states={'draft':[('readonly',False)]}),
141         'move_id': fields.many2one('stock.move', 'Move',required=True, domain="[('product_id','=',product_id)]", readonly=True, states={'draft':[('readonly',False)]}),
142         'guarantee_limit': fields.date('Guarantee limit', help="The guarantee limit is computed as: last move date + warranty defined on selected product. If the current date is below the guarantee limit, each operation and fee you will add will be set as 'not to invoiced' by default. Note that you can change manually afterwards."),
143         'operations' : fields.one2many('mrp.repair.line', 'repair_id', 'Operation Lines', readonly=True, states={'draft':[('readonly',False)]}),
144         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', help='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."),
159         'invoiced': fields.boolean('Invoiced', readonly=True),
160         'repaired': fields.boolean('Repaired', readonly=True),
161         'amount_untaxed': fields.function(_amount_untaxed, string='Untaxed Amount',
162             store={
163                 'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations'], 10),
164                 'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
165             }),
166         'amount_tax': fields.function(_amount_tax, string='Taxes',
167             store={
168                 'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations'], 10),
169                 'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
170             }),
171         'amount_total': fields.function(_amount_total, string='Total',
172             store={
173                 'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations'], 10),
174                 'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
175             }),
176     }
177
178     _defaults = {
179         'state': lambda *a: 'draft',
180         'deliver_bool': lambda *a: True,
181         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'mrp.repair'),
182         'invoice_method': lambda *a: 'none',
183         'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.repair', context=context),
184         'pricelist_id': lambda self, cr, uid,context : self.pool.get('product.pricelist').search(cr, uid, [('type','=','sale')])[0]
185     }
186
187     def copy(self, cr, uid, id, default=None, context=None):
188         if not default:
189             default = {}
190         default.update({
191             'state':'draft',
192             'repaired':False,
193             'invoiced':False,
194             'invoice_id': False,
195             'picking_id': False,
196             'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.repair'),
197         })
198         return super(mrp_repair, self).copy(cr, uid, id, default, context)
199
200     def onchange_product_id(self, cr, uid, ids, product_id=None):
201         """ On change of product sets some values.
202         @param product_id: Changed product
203         @return: Dictionary of values.
204         """
205         return {'value': {
206                     'prodlot_id': False,
207                     'move_id': False,
208                     'guarantee_limit' :False,
209                     'location_id':  False,
210                     'location_dest_id': False,
211                 }
212         }
213
214     def onchange_move_id(self, cr, uid, ids, prod_id=False, move_id=False):
215         """ On change of move id sets values of guarantee limit, source location,
216         destination location, partner and partner address.
217         @param prod_id: Id of product in current record.
218         @param move_id: Changed move.
219         @return: Dictionary of values.
220         """
221         data = {}
222         data['value'] = {}
223         if not prod_id:
224             return data
225         if move_id:
226             move =  self.pool.get('stock.move').browse(cr, uid, move_id)
227             product = self.pool.get('product.product').browse(cr, uid, prod_id)
228             limit = datetime.strptime(move.date_expected, '%Y-%m-%d %H:%M:%S') + relativedelta(months=int(product.warranty))
229             data['value']['guarantee_limit'] = limit.strftime('%Y-%m-%d')
230             data['value']['location_id'] = move.location_dest_id.id
231             data['value']['location_dest_id'] = move.location_dest_id.id
232             if move.partner_id:
233                 data['value']['partner_id'] = move.partner_id.id
234             else:
235                 data['value']['partner_id'] = False
236             d = self.onchange_partner_id(cr, uid, ids, data['value']['partner_id'], data['value']['partner_id'])
237             data['value'].update(d['value'])
238         return data
239
240     def button_dummy(self, cr, uid, ids, context=None):
241         return True
242
243     def onchange_partner_id(self, cr, uid, ids, part, address_id):
244         """ On change of partner sets the values of partner address,
245         partner invoice address and pricelist.
246         @param part: Changed id of partner.
247         @param address_id: Address id from current record.
248         @return: Dictionary of values.
249         """
250         part_obj = self.pool.get('res.partner')
251         pricelist_obj = self.pool.get('product.pricelist')
252         if not part:
253             return {'value': {
254                         'address_id': False,
255                         'partner_invoice_id': False,
256                         'pricelist_id': pricelist_obj.search(cr, uid, [('type','=','sale')])[0]
257                     }
258             }
259         addr = part_obj.address_get(cr, uid, [part], ['delivery', 'invoice', 'default'])
260         partner = part_obj.browse(cr, uid, part)
261         pricelist = partner.property_product_pricelist and partner.property_product_pricelist.id or False
262         return {'value': {
263                     'address_id': addr['delivery'] or addr['default'],
264                     'partner_invoice_id': addr['invoice'],
265                     'pricelist_id': pricelist
266                 }
267         }
268
269     def onchange_lot_id(self, cr, uid, ids, lot, product_id):
270         """ On change of Serial Number sets the values of source location,
271         destination location, move and guarantee limit.
272         @param lot: Changed id of Serial Number.
273         @param product_id: Product id from current record.
274         @return: Dictionary of values.
275         """
276         move_obj = self.pool.get('stock.move')
277         data = {}
278         data['value'] = {
279             'location_id': False,
280             'location_dest_id': False,
281             'move_id': False,
282             'guarantee_limit': False
283         }
284
285         if not lot:
286             return data
287         move_ids = move_obj.search(cr, uid, [('prodlot_id', '=', lot)])
288
289         if not len(move_ids):
290             return data
291
292         def get_last_move(lst_move):
293             while lst_move.move_dest_id and lst_move.move_dest_id.state == 'done':
294                 lst_move = lst_move.move_dest_id
295             return lst_move
296
297         move_id = move_ids[0]
298         move = get_last_move(move_obj.browse(cr, uid, move_id))
299         data['value']['move_id'] = move.id
300         d = self.onchange_move_id(cr, uid, ids, product_id, move.id)
301         data['value'].update(d['value'])
302         return data
303
304     def action_cancel_draft(self, cr, uid, ids, *args):
305         """ Cancels repair order when it is in 'Draft' state.
306         @param *arg: Arguments
307         @return: True
308         """
309         if not len(ids):
310             return False
311         mrp_line_obj = self.pool.get('mrp.repair.line')
312         for repair in self.browse(cr, uid, ids):
313             mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'draft'})
314         self.write(cr, uid, ids, {'state':'draft'})
315         wf_service = netsvc.LocalService("workflow")
316         for id in ids:
317             wf_service.trg_create(uid, 'mrp.repair', id, cr)
318         return True
319
320     def action_confirm(self, cr, uid, ids, *args):
321         """ Repair order state is set to 'To be invoiced' when invoice method
322         is 'Before repair' else state becomes 'Confirmed'.
323         @param *arg: Arguments
324         @return: True
325         """
326         mrp_line_obj = self.pool.get('mrp.repair.line')
327         for o in self.browse(cr, uid, ids):
328             if (o.invoice_method == 'b4repair'):
329                 self.write(cr, uid, [o.id], {'state': '2binvoiced'})
330             else:
331                 self.write(cr, uid, [o.id], {'state': 'confirmed'})
332                 if not o.operations:
333                     raise osv.except_osv(_('Error !'),_('You cannot confirm a repair order which has no line.'))
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             mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'cancel'}, context=context)
347         self.write(cr,uid,ids,{'state':'cancel'})
348         return True
349
350     def wkf_invoice_create(self, cr, uid, ids, *args):
351         return self.action_invoice_create(cr, uid, ids)
352
353     def action_invoice_create(self, cr, uid, ids, group=False, context=None):
354         """ Creates invoice(s) for repair order.
355         @param group: It is set to true when group invoice is to be generated.
356         @return: Invoice Ids.
357         """
358         res = {}
359         invoices_group = {}
360         inv_line_obj = self.pool.get('account.invoice.line')
361         inv_obj = self.pool.get('account.invoice')
362         repair_line_obj = self.pool.get('mrp.repair.line')
363         repair_fee_obj = self.pool.get('mrp.repair.fee')
364         for repair in self.browse(cr, uid, ids, context=context):
365             res[repair.id] = False
366             if repair.state in ('draft','cancel') or repair.invoice_id:
367                 continue
368             if not (repair.partner_id.id and repair.partner_invoice_id.id):
369                 raise osv.except_osv(_('No partner !'),_('You have to select a Partner Invoice Address in the repair form !'))
370             comment = repair.quotation_notes
371             if (repair.invoice_method != 'none'):
372                 if group and repair.partner_invoice_id.id in invoices_group:
373                     inv_id = invoices_group[repair.partner_invoice_id.id]
374                     invoice = inv_obj.browse(cr, uid, inv_id)
375                     invoice_vals = {
376                         'name': invoice.name +', '+repair.name,
377                         'origin': invoice.origin+', '+repair.name,
378                         'comment':(comment and (invoice.comment and invoice.comment+"\n"+comment or comment)) or (invoice.comment and invoice.comment or ''),
379                     }
380                     inv_obj.write(cr, uid, [inv_id], invoice_vals, context=context)
381                 else:
382                     if not repair.partner_id.property_account_receivable:
383                         raise osv.except_osv(_('Error !'), _('No account defined for partner "%s".') % repair.partner_id.name )
384                     account_id = repair.partner_id.property_account_receivable.id
385                     inv = {
386                         'name': repair.name,
387                         'origin':repair.name,
388                         'type': 'out_invoice',
389                         'account_id': account_id,
390                         'partner_id': repair.partner_id.id,
391                         'currency_id': repair.pricelist_id.currency_id.id,
392                         'comment': repair.quotation_notes,
393                         'fiscal_position': repair.partner_id.property_account_position.id
394                     }
395                     inv_id = inv_obj.create(cr, uid, inv)
396                     invoices_group[repair.partner_invoice_id.id] = inv_id
397                 self.write(cr, uid, repair.id, {'invoiced': True, 'invoice_id': inv_id})
398
399                 for operation in repair.operations:
400                     if operation.to_invoice == True:
401                         if group:
402                             name = repair.name + '-' + operation.name
403                         else:
404                             name = operation.name
405
406                         if operation.product_id.property_account_income:
407                             account_id = operation.product_id.property_account_income.id
408                         elif operation.product_id.categ_id.property_account_income_categ:
409                             account_id = operation.product_id.categ_id.property_account_income_categ.id
410                         else:
411                             raise osv.except_osv(_('Error !'), _('No account defined for product "%s".') % operation.product_id.name )
412
413                         invoice_line_id = inv_line_obj.create(cr, uid, {
414                             'invoice_id': inv_id,
415                             'name': name,
416                             'origin': repair.name,
417                             'account_id': account_id,
418                             'quantity': operation.product_uom_qty,
419                             'invoice_line_tax_id': [(6,0,[x.id for x in operation.tax_id])],
420                             'uos_id': operation.product_uom.id,
421                             'price_unit': operation.price_unit,
422                             'price_subtotal': operation.product_uom_qty*operation.price_unit,
423                             'product_id': operation.product_id and operation.product_id.id or False
424                         })
425                         repair_line_obj.write(cr, uid, [operation.id], {'invoiced': True, 'invoice_line_id': invoice_line_id})
426                 for fee in repair.fees_lines:
427                     if fee.to_invoice == True:
428                         if group:
429                             name = repair.name + '-' + fee.name
430                         else:
431                             name = fee.name
432                         if not fee.product_id:
433                             raise osv.except_osv(_('Warning !'), _('No product defined on Fees!'))
434
435                         if fee.product_id.property_account_income:
436                             account_id = fee.product_id.property_account_income.id
437                         elif fee.product_id.categ_id.property_account_income_categ:
438                             account_id = fee.product_id.categ_id.property_account_income_categ.id
439                         else:
440                             raise osv.except_osv(_('Error !'), _('No account defined for product "%s".') % fee.product_id.name)
441
442                         invoice_fee_id = inv_line_obj.create(cr, uid, {
443                             'invoice_id': inv_id,
444                             'name': name,
445                             'origin': repair.name,
446                             'account_id': account_id,
447                             'quantity': fee.product_uom_qty,
448                             'invoice_line_tax_id': [(6,0,[x.id for x in fee.tax_id])],
449                             'uos_id': fee.product_uom.id,
450                             'product_id': fee.product_id and fee.product_id.id or False,
451                             'price_unit': fee.price_unit,
452                             'price_subtotal': fee.product_uom_qty*fee.price_unit
453                         })
454                         repair_fee_obj.write(cr, uid, [fee.id], {'invoiced': True, 'invoice_line_id': invoice_fee_id})
455                 res[repair.id] = inv_id
456         return res
457
458     def action_repair_ready(self, cr, uid, ids, context=None):
459         """ Writes repair order state to 'Ready'
460         @return: True
461         """
462         for repair in self.browse(cr, uid, ids, context=context):
463             self.pool.get('mrp.repair.line').write(cr, uid, [l.id for
464                     l in repair.operations], {'state': 'confirmed'}, context=context)
465             self.write(cr, uid, [repair.id], {'state': 'ready'})
466         return True
467
468     def action_repair_start(self, cr, uid, ids, context=None):
469         """ Writes repair order state to 'Under Repair'
470         @return: True
471         """
472         repair_line = self.pool.get('mrp.repair.line')
473         for repair in self.browse(cr, uid, ids, context=context):
474             repair_line.write(cr, uid, [l.id for
475                     l in repair.operations], {'state': 'confirmed'}, context=context)
476             repair.write({'state': 'under_repair'})
477         return True
478
479     def action_repair_end(self, cr, uid, ids, context=None):
480         """ Writes repair order state to 'To be invoiced' if invoice method is
481         After repair else state is set to 'Ready'.
482         @return: True
483         """
484         for order in self.browse(cr, uid, ids, context=context):
485             val = {}
486             val['repaired'] = True
487             if (not order.invoiced and order.invoice_method=='after_repair'):
488                 val['state'] = '2binvoiced'
489             elif (not order.invoiced and order.invoice_method=='b4repair'):
490                 val['state'] = 'ready'
491             else:
492                 pass
493             self.write(cr, uid, [order.id], val)
494         return True
495
496     def wkf_repair_done(self, cr, uid, ids, *args):
497         self.action_repair_done(cr, uid, ids)
498         return True
499
500     def action_repair_done(self, cr, uid, ids, context=None):
501         """ Creates stock move and picking for repair order.
502         @return: Picking ids.
503         """
504         res = {}
505         move_obj = self.pool.get('stock.move')
506         wf_service = netsvc.LocalService("workflow")
507         repair_line_obj = self.pool.get('mrp.repair.line')
508         seq_obj = self.pool.get('ir.sequence')
509         pick_obj = self.pool.get('stock.picking')
510         for repair in self.browse(cr, uid, ids, context=context):
511             for move in repair.operations:
512                 move_id = move_obj.create(cr, uid, {
513                     'name': move.name,
514                     'product_id': move.product_id.id,
515                     'product_qty': move.product_uom_qty,
516                     'product_uom': move.product_uom.id,
517                     'partner_id': repair.address_id and repair.address_id.id or False,
518                     'location_id': move.location_id.id,
519                     'location_dest_id': move.location_dest_id.id,
520                     'tracking_id': False,
521                     'prodlot_id': move.prodlot_id and move.prodlot_id.id or False,
522                     'state': 'done',
523                 })
524                 repair_line_obj.write(cr, uid, [move.id], {'move_id': move_id, 'state': 'done'}, context=context)
525             if repair.deliver_bool:
526                 pick_name = seq_obj.get(cr, uid, 'stock.picking.out')
527                 picking = pick_obj.create(cr, uid, {
528                     'name': pick_name,
529                     'origin': repair.name,
530                     'state': 'draft',
531                     'move_type': 'one',
532                     'partner_id': repair.address_id and repair.address_id.id or False,
533                     'note': repair.internal_notes,
534                     'invoice_state': 'none',
535                     'type': 'out',
536                 })
537                 move_id = move_obj.create(cr, uid, {
538                     'name': repair.name,
539                     'picking_id': picking,
540                     'product_id': repair.product_id.id,
541                     'product_qty': move.product_uom_qty or 1.0,
542                     'product_uom': repair.product_id.uom_id.id,
543                     'prodlot_id': repair.prodlot_id and repair.prodlot_id.id or False,
544                     'partner_id': repair.address_id and repair.address_id.id or False,
545                     'location_id': repair.location_id.id,
546                     'location_dest_id': repair.location_dest_id.id,
547                     'tracking_id': False,
548                     'state': 'assigned',
549                 })
550                 wf_service.trg_validate(uid, 'stock.picking', picking, 'button_confirm', cr)
551                 self.write(cr, uid, [repair.id], {'state': 'done', 'picking_id': picking})
552                 res[repair.id] = picking
553             else:
554                 self.write(cr, uid, [repair.id], {'state': 'done'})
555         return res
556
557
558 mrp_repair()
559
560
561 class ProductChangeMixin(object):
562     def product_id_change(self, cr, uid, ids, pricelist, product, uom=False,
563                           product_uom_qty=0, partner_id=False, guarantee_limit=False):
564         """ On change of product it sets product quantity, tax account, name,
565         uom of product, unit price and price subtotal.
566         @param pricelist: Pricelist of current record.
567         @param product: Changed id of product.
568         @param uom: UoM of current record.
569         @param product_uom_qty: Quantity of current record.
570         @param partner_id: Partner of current record.
571         @param guarantee_limit: Guarantee limit of current record.
572         @return: Dictionary of values and warning message.
573         """
574         result = {}
575         warning = {}
576
577         if not product_uom_qty:
578             product_uom_qty = 1
579         result['product_uom_qty'] = product_uom_qty
580
581         if product:
582             product_obj = self.pool.get('product.product').browse(cr, uid, product)
583             if partner_id:
584                 partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
585                 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, partner.property_account_position, product_obj.taxes_id)
586
587             result['name'] = product_obj.partner_ref
588             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id or False
589             if not pricelist:
590                 warning = {
591                     'title':'No Pricelist !',
592                     'message':
593                         'You have to select a pricelist in the Repair form !\n'
594                         'Please set one before choosing a product.'
595                 }
596             else:
597                 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
598                             product, product_uom_qty, partner_id, {'uom': uom,})[pricelist]
599
600                 if price is False:
601                      warning = {
602                         'title':'No valid pricelist line found !',
603                         'message':
604                             "Couldn't find a pricelist line matching this product and quantity.\n"
605                             "You have to change either the product, the quantity or the pricelist."
606                      }
607                 else:
608                     result.update({'price_unit': price, 'price_subtotal': price*product_uom_qty})
609
610         return {'value': result, 'warning': warning}
611
612
613 class mrp_repair_line(osv.osv, ProductChangeMixin):
614     _name = 'mrp.repair.line'
615     _description = 'Repair Line'
616
617     def copy_data(self, cr, uid, id, default=None, context=None):
618         if not default: default = {}
619         default.update( {'invoice_line_id': False, 'move_id': False, 'invoiced': False, 'state': 'draft'})
620         return super(mrp_repair_line, self).copy_data(cr, uid, id, default, context)
621
622     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
623         """ Calculates amount.
624         @param field_name: Name of field.
625         @param arg: Argument
626         @return: Dictionary of values.
627         """
628         res = {}
629         cur_obj=self.pool.get('res.currency')
630         for line in self.browse(cr, uid, ids, context=context):
631             res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0
632             cur = line.repair_id.pricelist_id.currency_id
633             res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
634         return res
635
636     _columns = {
637         'name' : fields.char('Description',size=64,required=True),
638         'repair_id': fields.many2one('mrp.repair', 'Repair Order Reference',ondelete='cascade', select=True),
639         'type': fields.selection([('add','Add'),('remove','Remove')],'Type', required=True),
640         'to_invoice': fields.boolean('To Invoice'),
641         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok','=',True)], required=True),
642         'invoiced': fields.boolean('Invoiced',readonly=True),
643         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price')),
644         'price_subtotal': fields.function(_amount_line, string='Subtotal',digits_compute= dp.get_precision('Sale Price')),
645         'tax_id': fields.many2many('account.tax', 'repair_operation_line_tax', 'repair_operation_line_id', 'tax_id', 'Taxes'),
646         'product_uom_qty': fields.float('Quantity (Unit of Measure)', digits=(16,2), required=True),
647         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
648         'prodlot_id': fields.many2one('stock.production.lot', 'Lot Number',domain="[('product_id','=',product_id)]"),
649         'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
650         'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True),
651         'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True, select=True),
652         'move_id': fields.many2one('stock.move', 'Inventory Move', readonly=True),
653         'state': fields.selection([
654                     ('draft','Draft'),
655                     ('confirmed','Confirmed'),
656                     ('done','Done'),
657                     ('cancel','Canceled')], 'Status', required=True, readonly=True,
658                     help=' * The \'Draft\' state is set automatically as draft when repair order in draft state. \
659                         \n* The \'Confirmed\' state is set automatically as confirm when repair order in confirm state. \
660                         \n* The \'Done\' state is set automatically when repair order is completed.\
661                         \n* The \'Cancelled\' state is set automatically when user cancel repair order.'),
662     }
663     _defaults = {
664      'state': lambda *a: 'draft',
665      'product_uom_qty': lambda *a: 1,
666     }
667
668     def onchange_operation_type(self, cr, uid, ids, type, guarantee_limit, company_id=False, context=None):
669         """ On change of operation type it sets source location, destination location
670         and to invoice field.
671         @param product: Changed operation type.
672         @param guarantee_limit: Guarantee limit of current record.
673         @return: Dictionary of values.
674         """
675         if not type:
676             return {'value': {
677                 'location_id': False,
678                 'location_dest_id': False
679                 }}
680         warehouse_obj = self.pool.get('stock.warehouse')
681         location_id = self.pool.get('stock.location').search(cr, uid, [('usage','=','production')], context=context)
682         location_id = location_id and location_id[0] or False
683
684         if type == 'add':
685             # TOCHECK: Find stock location for user's company warehouse or
686             # repair order's company's warehouse (company_id field is added in fix of lp:831583)
687             args = company_id and [('company_id', '=', company_id)] or []
688             warehouse_ids = warehouse_obj.search(cr, uid, args, context=context)
689             stock_id = False
690             if warehouse_ids:
691                 stock_id = warehouse_obj.browse(cr, uid, warehouse_ids[0], context=context).lot_stock_id.id
692             to_invoice = (guarantee_limit and datetime.strptime(guarantee_limit, '%Y-%m-%d') < datetime.now())
693
694             return {'value': {
695                 'to_invoice': to_invoice,
696                 'location_id': stock_id,
697                 'location_dest_id': location_id
698                 }}
699
700         return {'value': {
701                 'to_invoice': False,
702                 'location_id': location_id,
703                 'location_dest_id': 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=(16,2), 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('Sale Price')),
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: