[FIX] website_sale: the order of the attribute list doesn't matter
[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 openerp.tools.translate import _
25 import openerp.addons.decimal_precision as dp
26
27 class mrp_repair(osv.osv):
28     _name = 'mrp.repair'
29     _inherit = 'mail.thread'
30     _description = 'Repair Order'
31
32     def _amount_untaxed(self, cr, uid, ids, field_name, arg, context=None):
33         """ Calculates untaxed amount.
34         @param self: The object pointer
35         @param cr: The current row, from the database cursor,
36         @param uid: The current user ID for security checks
37         @param ids: List of selected IDs
38         @param field_name: Name of field.
39         @param arg: Argument
40         @param context: A standard dictionary for contextual values
41         @return: Dictionary of values.
42         """
43         res = {}
44         cur_obj = self.pool.get('res.currency')
45
46         for repair in self.browse(cr, uid, ids, context=context):
47             res[repair.id] = 0.0
48             for line in repair.operations:
49                 res[repair.id] += line.price_subtotal
50             for line in repair.fees_lines:
51                 res[repair.id] += line.price_subtotal
52             cur = repair.pricelist_id.currency_id
53             res[repair.id] = cur_obj.round(cr, uid, cur, res[repair.id])
54         return res
55
56     def _amount_tax(self, cr, uid, ids, field_name, arg, context=None):
57         """ Calculates taxed amount.
58         @param field_name: Name of field.
59         @param arg: Argument
60         @return: Dictionary of values.
61         """
62         res = {}
63         #return {}.fromkeys(ids, 0)
64         cur_obj = self.pool.get('res.currency')
65         tax_obj = self.pool.get('account.tax')
66         for repair in self.browse(cr, uid, ids, context=context):
67             val = 0.0
68             cur = repair.pricelist_id.currency_id
69             for line in repair.operations:
70                 #manage prices with tax included use compute_all instead of compute
71                 if line.to_invoice:
72                     tax_calculate = tax_obj.compute_all(cr, uid, line.tax_id, line.price_unit, line.product_uom_qty, line.product_id, repair.partner_id)
73                     for c in tax_calculate['taxes']:
74                         val += c['amount']
75             for line in repair.fees_lines:
76                 if line.to_invoice:
77                     tax_calculate = tax_obj.compute_all(cr, uid, line.tax_id, line.price_unit, line.product_uom_qty, line.product_id, repair.partner_id)
78                     for c in tax_calculate['taxes']:
79                         val += c['amount']
80             res[repair.id] = cur_obj.round(cr, uid, cur, val)
81         return res
82
83     def _amount_total(self, cr, uid, ids, field_name, arg, context=None):
84         """ Calculates total amount.
85         @param field_name: Name of field.
86         @param arg: Argument
87         @return: Dictionary of values.
88         """
89         res = {}
90         untax = self._amount_untaxed(cr, uid, ids, field_name, arg, context=context)
91         tax = self._amount_tax(cr, uid, ids, field_name, arg, context=context)
92         cur_obj = self.pool.get('res.currency')
93         for id in ids:
94             repair = self.browse(cr, uid, id, context=context)
95             cur = repair.pricelist_id.currency_id
96             res[id] = cur_obj.round(cr, uid, cur, untax.get(id, 0.0) + tax.get(id, 0.0))
97         return res
98
99     def _get_default_address(self, cr, uid, ids, field_name, arg, context=None):
100         res = {}
101         partner_obj = self.pool.get('res.partner')
102         for data in self.browse(cr, uid, ids, context=context):
103             adr_id = False
104             if data.partner_id:
105                 adr_id = partner_obj.address_get(cr, uid, [data.partner_id.id], ['default'])['default']
106             res[data.id] = adr_id
107         return res
108
109     def _get_lines(self, cr, uid, ids, context=None):
110         return self.pool['mrp.repair'].search(cr, uid, [('operations', 'in', ids)], context=context)
111
112     def _get_fee_lines(self, cr, uid, ids, context=None):
113         return self.pool['mrp.repair'].search(cr, uid, [('fees_lines', 'in', ids)], context=context)
114
115     _columns = {
116         'name': fields.char('Repair Reference', size=24, required=True, states={'confirmed': [('readonly', True)]}),
117         'product_id': fields.many2one('product.product', string='Product to Repair', required=True, readonly=True, states={'draft': [('readonly', False)]}),
118         'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'),
119                                     required=True, readonly=True, states={'draft': [('readonly', False)]}),
120         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)]}),
121         '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)]}),
122         'address_id': fields.many2one('res.partner', 'Delivery Address', domain="[('parent_id','=',partner_id)]", states={'confirmed': [('readonly', True)]}),
123         'default_address_id': fields.function(_get_default_address, type="many2one", relation="res.partner"),
124         'state': fields.selection([
125             ('draft', 'Quotation'),
126             ('cancel', 'Cancelled'),
127             ('confirmed', 'Confirmed'),
128             ('under_repair', 'Under Repair'),
129             ('ready', 'Ready to Repair'),
130             ('2binvoiced', 'To be Invoiced'),
131             ('invoice_except', 'Invoice Exception'),
132             ('done', 'Repaired')
133             ], 'Status', readonly=True, track_visibility='onchange',
134             help=' * The \'Draft\' status is used when a user is encoding a new and unconfirmed repair order. \
135             \n* The \'Confirmed\' status is used when a user confirms the repair order. \
136             \n* The \'Ready to Repair\' status is used to start to repairing, user can start repairing only after repair order is confirmed. \
137             \n* The \'To be Invoiced\' status is used to generate the invoice before or after repairing done. \
138             \n* The \'Done\' status is set when repairing is completed.\
139             \n* The \'Cancelled\' status is used when user cancel repair order.'),
140         'location_id': fields.many2one('stock.location', 'Current Location', select=True, required=True, readonly=True, states={'draft': [('readonly', False)], 'confirmed': [('readonly', True)]}),
141         'location_dest_id': fields.many2one('stock.location', 'Delivery Location', readonly=True, required=True, states={'draft': [('readonly', False)], 'confirmed': [('readonly', True)]}),
142         'lot_id': fields.many2one('stock.production.lot', 'Repaired Lot', domain="[('product_id','=', product_id)]", help="Products repaired are all belonging to this lot"),
143         'guarantee_limit': fields.date('Warranty Expiration', help="The warranty expiration limit is computed as: last move date + warranty defined on selected product. If the current date is below the warranty expiration limit, each operation and fee you will add will be set as 'not to invoiced' by default. Note that you can change manually afterwards.", states={'confirmed': [('readonly', True)]}),
144         'operations': fields.one2many('mrp.repair.line', 'repair_id', 'Operation Lines', readonly=True, states={'draft': [('readonly', False)]}),
145         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', help='Pricelist of the selected partner.'),
146         'partner_invoice_id': fields.many2one('res.partner', 'Invoicing Address'),
147         'invoice_method': fields.selection([
148             ("none", "No Invoice"),
149             ("b4repair", "Before Repair"),
150             ("after_repair", "After Repair")
151            ], "Invoice Method",
152             select=True, required=True, states={'draft': [('readonly', False)]}, readonly=True, help='Selecting \'Before Repair\' or \'After Repair\' will allow you to generate invoice before or after the repair is done respectively. \'No invoice\' means you don\'t want to generate invoice for this repair order.'),
153         'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True, track_visibility="onchange"),
154         'move_id': fields.many2one('stock.move', 'Move', readonly=True, help="Move created by the repair order", track_visibility="onchange"),
155         'fees_lines': fields.one2many('mrp.repair.fee', 'repair_id', 'Fees', 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         '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', 'fees_lines'], 10),
164                 'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
165                 'mrp.repair.fee': (_get_fee_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', 'fees_lines'], 10),
170                 'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
171                 'mrp.repair.fee': (_get_fee_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
172             }),
173         'amount_total': fields.function(_amount_total, string='Total',
174             store={
175                 'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations', 'fees_lines'], 10),
176                 'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
177                 'mrp.repair.fee': (_get_fee_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
178             }),
179     }
180
181     def _default_stock_location(self, cr, uid, context=None):
182         try:
183             warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0')
184             return warehouse.lot_stock_id.id
185         except:
186             return False
187
188     _defaults = {
189         'state': lambda *a: 'draft',
190         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'mrp.repair'),
191         'invoice_method': lambda *a: 'none',
192         'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.repair', context=context),
193         'pricelist_id': lambda self, cr, uid, context: self.pool.get('product.pricelist').search(cr, uid, [('type', '=', 'sale')])[0],
194         'product_qty': 1.0,
195         'location_id': _default_stock_location,
196     }
197
198     _sql_constraints = [
199         ('name', 'unique (name)', 'The name of the Repair Order must be unique!'),
200     ]
201
202     def copy(self, cr, uid, id, default=None, context=None):
203         if not default:
204             default = {}
205         default.update({
206             'state': 'draft',
207             'repaired': False,
208             'invoiced': False,
209             'invoice_id': False,
210             'move_id': False,
211             'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.repair'),
212         })
213         return super(mrp_repair, self).copy(cr, uid, id, default, context)
214
215     def onchange_product_id(self, cr, uid, ids, product_id=None):
216         """ On change of product sets some values.
217         @param product_id: Changed product
218         @return: Dictionary of values.
219         """
220         product = False
221         if product_id:
222             product = self.pool.get("product.product").browse(cr, uid, product_id)
223         return {'value': {
224                     'guarantee_limit': False,
225                     'lot_id': False,
226                     'product_uom': product and product.uom_id.id or False,
227                 }
228         }
229
230     def onchange_product_uom(self, cr, uid, ids, product_id, product_uom, context=None):
231         res = {'value': {}}
232         if not product_uom or not product_id:
233             return res
234         product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
235         uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
236         if uom.category_id.id != product.uom_id.category_id.id:
237             res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
238             res['value'].update({'product_uom': product.uom_id.id})
239         return res
240
241     def onchange_location_id(self, cr, uid, ids, location_id=None):
242         """ On change of location
243         """
244         return {'value': {'location_dest_id': location_id}}
245
246     def button_dummy(self, cr, uid, ids, context=None):
247         return True
248
249     def onchange_partner_id(self, cr, uid, ids, part, address_id):
250         """ On change of partner sets the values of partner address,
251         partner invoice address and pricelist.
252         @param part: Changed id of partner.
253         @param address_id: Address id from current record.
254         @return: Dictionary of values.
255         """
256         part_obj = self.pool.get('res.partner')
257         pricelist_obj = self.pool.get('product.pricelist')
258         if not part:
259             return {'value': {
260                         'address_id': False,
261                         'partner_invoice_id': False,
262                         'pricelist_id': pricelist_obj.search(cr, uid, [('type', '=', 'sale')])[0]
263                     }
264             }
265         addr = part_obj.address_get(cr, uid, [part], ['delivery', 'invoice', 'default'])
266         partner = part_obj.browse(cr, uid, part)
267         pricelist = partner.property_product_pricelist and partner.property_product_pricelist.id or False
268         return {'value': {
269                     'address_id': addr['delivery'] or addr['default'],
270                     'partner_invoice_id': addr['invoice'],
271                     'pricelist_id': pricelist
272                 }
273         }
274
275     def action_cancel_draft(self, cr, uid, ids, *args):
276         """ Cancels repair order when it is in 'Draft' state.
277         @param *arg: Arguments
278         @return: True
279         """
280         if not len(ids):
281             return False
282         mrp_line_obj = self.pool.get('mrp.repair.line')
283         for repair in self.browse(cr, uid, ids):
284             mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'draft'})
285         self.write(cr, uid, ids, {'state': 'draft'})
286         return self.create_workflow(cr, uid, ids)
287
288     def action_confirm(self, cr, uid, ids, *args):
289         """ Repair order state is set to 'To be invoiced' when invoice method
290         is 'Before repair' else state becomes 'Confirmed'.
291         @param *arg: Arguments
292         @return: True
293         """
294         mrp_line_obj = self.pool.get('mrp.repair.line')
295         for o in self.browse(cr, uid, ids):
296             if (o.invoice_method == 'b4repair'):
297                 self.write(cr, uid, [o.id], {'state': '2binvoiced'})
298             else:
299                 self.write(cr, uid, [o.id], {'state': 'confirmed'})
300                 for line in o.operations:
301                     if line.product_id.track_production:
302                         raise osv.except_osv(_('Warning!'), _("Serial number is required for operation line with product '%s'") % (line.product_id.name))
303                 mrp_line_obj.write(cr, uid, [l.id for l in o.operations], {'state': 'confirmed'})
304         return True
305
306     def action_cancel(self, cr, uid, ids, context=None):
307         """ Cancels repair order.
308         @return: True
309         """
310         mrp_line_obj = self.pool.get('mrp.repair.line')
311         for repair in self.browse(cr, uid, ids, context=context):
312             if not repair.invoiced:
313                 mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'cancel'}, context=context)
314             else:
315                 raise osv.except_osv(_('Warning!'), _('Repair order is already invoiced.'))
316         return self.write(cr, uid, ids, {'state': 'cancel'})
317
318     def wkf_invoice_create(self, cr, uid, ids, *args):
319         self.action_invoice_create(cr, uid, ids)
320         return True
321
322     def action_invoice_create(self, cr, uid, ids, group=False, context=None):
323         """ Creates invoice(s) for repair order.
324         @param group: It is set to true when group invoice is to be generated.
325         @return: Invoice Ids.
326         """
327         res = {}
328         invoices_group = {}
329         inv_line_obj = self.pool.get('account.invoice.line')
330         inv_obj = self.pool.get('account.invoice')
331         repair_line_obj = self.pool.get('mrp.repair.line')
332         repair_fee_obj = self.pool.get('mrp.repair.fee')
333         for repair in self.browse(cr, uid, ids, context=context):
334             res[repair.id] = False
335             if repair.state in ('draft', 'cancel') or repair.invoice_id:
336                 continue
337             if not (repair.partner_id.id and repair.partner_invoice_id.id):
338                 raise osv.except_osv(_('No partner!'), _('You have to select a Partner Invoice Address in the repair form!'))
339             comment = repair.quotation_notes
340             if (repair.invoice_method != 'none'):
341                 if group and repair.partner_invoice_id.id in invoices_group:
342                     inv_id = invoices_group[repair.partner_invoice_id.id]
343                     invoice = inv_obj.browse(cr, uid, inv_id)
344                     invoice_vals = {
345                         'name': invoice.name + ', ' + repair.name,
346                         'origin': invoice.origin + ', ' + repair.name,
347                         'comment': (comment and (invoice.comment and invoice.comment + "\n" + comment or comment)) or (invoice.comment and invoice.comment or ''),
348                     }
349                     inv_obj.write(cr, uid, [inv_id], invoice_vals, context=context)
350                 else:
351                     if not repair.partner_id.property_account_receivable:
352                         raise osv.except_osv(_('Error!'), _('No account defined for partner "%s".') % repair.partner_id.name)
353                     account_id = repair.partner_id.property_account_receivable.id
354                     inv = {
355                         'name': repair.name,
356                         'origin': repair.name,
357                         'type': 'out_invoice',
358                         'account_id': account_id,
359                         'partner_id': repair.partner_id.id,
360                         'currency_id': repair.pricelist_id.currency_id.id,
361                         'comment': repair.quotation_notes,
362                         'fiscal_position': repair.partner_id.property_account_position.id
363                     }
364                     inv_id = inv_obj.create(cr, uid, inv)
365                     invoices_group[repair.partner_invoice_id.id] = inv_id
366                 self.write(cr, uid, repair.id, {'invoiced': True, 'invoice_id': inv_id})
367
368                 for operation in repair.operations:
369                     if operation.to_invoice:
370                         if group:
371                             name = repair.name + '-' + operation.name
372                         else:
373                             name = operation.name
374
375                         if operation.product_id.property_account_income:
376                             account_id = operation.product_id.property_account_income.id
377                         elif operation.product_id.categ_id.property_account_income_categ:
378                             account_id = operation.product_id.categ_id.property_account_income_categ.id
379                         else:
380                             raise osv.except_osv(_('Error!'), _('No account defined for product "%s".') % operation.product_id.name)
381
382                         invoice_line_id = inv_line_obj.create(cr, uid, {
383                             'invoice_id': inv_id,
384                             'name': name,
385                             'origin': repair.name,
386                             'account_id': account_id,
387                             'quantity': operation.product_uom_qty,
388                             'invoice_line_tax_id': [(6, 0, [x.id for x in operation.tax_id])],
389                             'uos_id': operation.product_uom.id,
390                             'price_unit': operation.price_unit,
391                             'price_subtotal': operation.product_uom_qty * operation.price_unit,
392                             'product_id': operation.product_id and operation.product_id.id or False
393                         })
394                         repair_line_obj.write(cr, uid, [operation.id], {'invoiced': True, 'invoice_line_id': invoice_line_id})
395                 for fee in repair.fees_lines:
396                     if fee.to_invoice:
397                         if group:
398                             name = repair.name + '-' + fee.name
399                         else:
400                             name = fee.name
401                         if not fee.product_id:
402                             raise osv.except_osv(_('Warning!'), _('No product defined on Fees!'))
403
404                         if fee.product_id.property_account_income:
405                             account_id = fee.product_id.property_account_income.id
406                         elif fee.product_id.categ_id.property_account_income_categ:
407                             account_id = fee.product_id.categ_id.property_account_income_categ.id
408                         else:
409                             raise osv.except_osv(_('Error!'), _('No account defined for product "%s".') % fee.product_id.name)
410
411                         invoice_fee_id = inv_line_obj.create(cr, uid, {
412                             'invoice_id': inv_id,
413                             'name': name,
414                             'origin': repair.name,
415                             'account_id': account_id,
416                             'quantity': fee.product_uom_qty,
417                             'invoice_line_tax_id': [(6, 0, [x.id for x in fee.tax_id])],
418                             'uos_id': fee.product_uom.id,
419                             'product_id': fee.product_id and fee.product_id.id or False,
420                             'price_unit': fee.price_unit,
421                             'price_subtotal': fee.product_uom_qty * fee.price_unit
422                         })
423                         repair_fee_obj.write(cr, uid, [fee.id], {'invoiced': True, 'invoice_line_id': invoice_fee_id})
424                 res[repair.id] = inv_id
425         return res
426
427     def action_repair_ready(self, cr, uid, ids, context=None):
428         """ Writes repair order state to 'Ready'
429         @return: True
430         """
431         for repair in self.browse(cr, uid, ids, context=context):
432             self.pool.get('mrp.repair.line').write(cr, uid, [l.id for
433                     l in repair.operations], {'state': 'confirmed'}, context=context)
434             self.write(cr, uid, [repair.id], {'state': 'ready'})
435         return True
436
437     def action_repair_start(self, cr, uid, ids, context=None):
438         """ Writes repair order state to 'Under Repair'
439         @return: True
440         """
441         repair_line = self.pool.get('mrp.repair.line')
442         for repair in self.browse(cr, uid, ids, context=context):
443             repair_line.write(cr, uid, [l.id for
444                     l in repair.operations], {'state': 'confirmed'}, context=context)
445             repair.write({'state': 'under_repair'})
446         return True
447
448     def action_repair_end(self, cr, uid, ids, context=None):
449         """ Writes repair order state to 'To be invoiced' if invoice method is
450         After repair else state is set to 'Ready'.
451         @return: True
452         """
453         for order in self.browse(cr, uid, ids, context=context):
454             val = {}
455             val['repaired'] = True
456             if (not order.invoiced and order.invoice_method == 'after_repair'):
457                 val['state'] = '2binvoiced'
458             elif (not order.invoiced and order.invoice_method == 'b4repair'):
459                 val['state'] = 'ready'
460             else:
461                 pass
462             self.write(cr, uid, [order.id], val)
463         return True
464
465     def wkf_repair_done(self, cr, uid, ids, *args):
466         self.action_repair_done(cr, uid, ids)
467         return True
468
469     def action_repair_done(self, cr, uid, ids, context=None):
470         """ Creates stock move for operation and stock move for final product of repair order.
471         @return: Move ids of final products
472         """
473         res = {}
474         move_obj = self.pool.get('stock.move')
475         repair_line_obj = self.pool.get('mrp.repair.line')
476         for repair in self.browse(cr, uid, ids, context=context):
477             move_ids = []
478             for move in repair.operations:
479                 move_id = move_obj.create(cr, uid, {
480                     'name': move.name,
481                     'product_id': move.product_id.id,
482                     'restrict_lot_id': move.lot_id.id,
483                     'product_uom_qty': move.product_uom_qty,
484                     'product_uom': move.product_uom.id,
485                     'partner_id': repair.address_id and repair.address_id.id or False,
486                     'location_id': move.location_id.id,
487                     'location_dest_id': move.location_dest_id.id,
488                     'state': 'assigned',
489                 })
490                 move_ids.append(move_id)
491                 repair_line_obj.write(cr, uid, [move.id], {'move_id': move_id, 'state': 'done'}, context=context)
492             move_id = move_obj.create(cr, uid, {
493                 'name': repair.name,
494                 'product_id': repair.product_id.id,
495                 'product_uom': repair.product_uom.id or repair.product_id.uom_id.id,
496                 'product_uom_qty': repair.product_qty,
497                 'partner_id': repair.address_id and repair.address_id.id or False,
498                 'location_id': repair.location_id.id,
499                 'location_dest_id': repair.location_dest_id.id,
500                 'restrict_lot_id': repair.lot_id.id,
501             })
502             move_ids.append(move_id)
503             move_obj.action_done(cr, uid, move_ids, context=context)
504             self.write(cr, uid, [repair.id], {'state': 'done', 'move_id': move_id}, context=context)
505             res[repair.id] = move_id
506         return res
507
508
509 class ProductChangeMixin(object):
510     def product_id_change(self, cr, uid, ids, pricelist, product, uom=False,
511                           product_uom_qty=0, partner_id=False, guarantee_limit=False):
512         """ On change of product it sets product quantity, tax account, name,
513         uom of product, unit price and price subtotal.
514         @param pricelist: Pricelist of current record.
515         @param product: Changed id of product.
516         @param uom: UoM of current record.
517         @param product_uom_qty: Quantity of current record.
518         @param partner_id: Partner of current record.
519         @param guarantee_limit: Guarantee limit of current record.
520         @return: Dictionary of values and warning message.
521         """
522         result = {}
523         warning = {}
524
525         if not product_uom_qty:
526             product_uom_qty = 1
527         result['product_uom_qty'] = product_uom_qty
528
529         if product:
530             product_obj = self.pool.get('product.product').browse(cr, uid, product)
531             if partner_id:
532                 partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
533                 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, partner.property_account_position, product_obj.taxes_id)
534
535             result['name'] = product_obj.partner_ref
536             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id or False
537             if not pricelist:
538                 warning = {
539                     'title': _('No Pricelist!'),
540                     'message':
541                         _('You have to select a pricelist in the Repair form !\n'
542                         'Please set one before choosing a product.')
543                 }
544             else:
545                 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
546                             product, product_uom_qty, partner_id, {'uom': uom})[pricelist]
547
548                 if price is False:
549                     warning = {
550                         'title': _('No valid pricelist line found !'),
551                         'message':
552                             _("Couldn't find a pricelist line matching this product and quantity.\n"
553                             "You have to change either the product, the quantity or the pricelist.")
554                      }
555                 else:
556                     result.update({'price_unit': price, 'price_subtotal': price * product_uom_qty})
557
558         return {'value': result, 'warning': warning}
559
560
561 class mrp_repair_line(osv.osv, ProductChangeMixin):
562     _name = 'mrp.repair.line'
563     _description = 'Repair Line'
564
565     def copy_data(self, cr, uid, id, default=None, context=None):
566         if not default:
567             default = {}
568         default.update({'invoice_line_id': False, 'move_id': False, 'invoiced': False, 'state': 'draft'})
569         return super(mrp_repair_line, self).copy_data(cr, uid, id, default, context)
570
571     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
572         """ Calculates amount.
573         @param field_name: Name of field.
574         @param arg: Argument
575         @return: Dictionary of values.
576         """
577         res = {}
578         cur_obj = self.pool.get('res.currency')
579         for line in self.browse(cr, uid, ids, context=context):
580             res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0
581             cur = line.repair_id.pricelist_id.currency_id
582             res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
583         return res
584
585     _columns = {
586         'name': fields.char('Description', size=64, required=True),
587         'repair_id': fields.many2one('mrp.repair', 'Repair Order Reference', ondelete='cascade', select=True),
588         'type': fields.selection([('add', 'Add'), ('remove', 'Remove')], 'Type', required=True),
589         'to_invoice': fields.boolean('To Invoice'),
590         'product_id': fields.many2one('product.product', 'Product', required=True),
591         'invoiced': fields.boolean('Invoiced', readonly=True),
592         'price_unit': fields.float('Unit Price', required=True, digits_compute=dp.get_precision('Product Price')),
593         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute=dp.get_precision('Account')),
594         'tax_id': fields.many2many('account.tax', 'repair_operation_line_tax', 'repair_operation_line_id', 'tax_id', 'Taxes'),
595         'product_uom_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
596         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
597         'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
598         'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True),
599         'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True, select=True),
600         'move_id': fields.many2one('stock.move', 'Inventory Move', readonly=True),
601         'lot_id': fields.many2one('stock.production.lot', 'Lot'),
602         'state': fields.selection([
603                     ('draft', 'Draft'),
604                     ('confirmed', 'Confirmed'),
605                     ('done', 'Done'),
606                     ('cancel', 'Cancelled')], 'Status', required=True, readonly=True,
607                     help=' * The \'Draft\' status is set automatically as draft when repair order in draft status. \
608                         \n* The \'Confirmed\' status is set automatically as confirm when repair order in confirm status. \
609                         \n* The \'Done\' status is set automatically when repair order is completed.\
610                         \n* The \'Cancelled\' status is set automatically when user cancel repair order.'),
611     }
612     _defaults = {
613         'state': lambda *a: 'draft',
614         'product_uom_qty': lambda *a: 1,
615     }
616
617     def onchange_operation_type(self, cr, uid, ids, type, guarantee_limit, company_id=False, context=None):
618         """ On change of operation type it sets source location, destination location
619         and to invoice field.
620         @param product: Changed operation type.
621         @param guarantee_limit: Guarantee limit of current record.
622         @return: Dictionary of values.
623         """
624         if not type:
625             return {'value': {
626                 'location_id': False,
627                 'location_dest_id': False
628                 }}
629         location_obj = self.pool.get('stock.location')
630         warehouse_obj = self.pool.get('stock.warehouse')
631         location_id = location_obj.search(cr, uid, [('usage', '=', 'production')], context=context)
632         location_id = location_id and location_id[0] or False
633
634         if type == 'add':
635             # TOCHECK: Find stock location for user's company warehouse or
636             # repair order's company's warehouse (company_id field is added in fix of lp:831583)
637             args = company_id and [('company_id', '=', company_id)] or []
638             warehouse_ids = warehouse_obj.search(cr, uid, args, context=context)
639             stock_id = False
640             if warehouse_ids:
641                 stock_id = warehouse_obj.browse(cr, uid, warehouse_ids[0], context=context).lot_stock_id.id
642             to_invoice = (guarantee_limit and datetime.strptime(guarantee_limit, '%Y-%m-%d') < datetime.now())
643
644             return {'value': {
645                 'to_invoice': to_invoice,
646                 'location_id': stock_id,
647                 'location_dest_id': location_id
648                 }}
649         scrap_location_ids = location_obj.search(cr, uid, [('scrap_location', '=', True)], context=context)
650
651         return {'value': {
652                 'to_invoice': False,
653                 'location_id': location_id,
654                 'location_dest_id': scrap_location_ids and scrap_location_ids[0] or False,
655                 }}
656
657
658 class mrp_repair_fee(osv.osv, ProductChangeMixin):
659     _name = 'mrp.repair.fee'
660     _description = 'Repair Fees Line'
661
662     def copy_data(self, cr, uid, id, default=None, context=None):
663         if not default:
664             default = {}
665         default.update({'invoice_line_id': False, 'invoiced': False})
666         return super(mrp_repair_fee, self).copy_data(cr, uid, id, default, context)
667
668     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
669         """ Calculates amount.
670         @param field_name: Name of field.
671         @param arg: Argument
672         @return: Dictionary of values.
673         """
674         res = {}
675         cur_obj = self.pool.get('res.currency')
676         for line in self.browse(cr, uid, ids, context=context):
677             res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0
678             cur = line.repair_id.pricelist_id.currency_id
679             res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
680         return res
681
682     _columns = {
683         'repair_id': fields.many2one('mrp.repair', 'Repair Order Reference', required=True, ondelete='cascade', select=True),
684         'name': fields.char('Description', size=64, select=True, required=True),
685         'product_id': fields.many2one('product.product', 'Product'),
686         'product_uom_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
687         'price_unit': fields.float('Unit Price', required=True),
688         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
689         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute=dp.get_precision('Account')),
690         'tax_id': fields.many2many('account.tax', 'repair_fee_line_tax', 'repair_fee_line_id', 'tax_id', 'Taxes'),
691         'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
692         'to_invoice': fields.boolean('To Invoice'),
693         'invoiced': fields.boolean('Invoiced', readonly=True),
694     }
695
696     _defaults = {
697         'to_invoice': lambda *a: True,
698     }
699
700 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: