[FIX]account_asset:Fixes the expresion problem in
[odoo/odoo.git] / addons / account_asset / account_asset.py
1 # -*- encoding: 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 import time
23 from datetime import datetime
24 from dateutil.relativedelta import relativedelta
25
26 from osv import osv, fields
27 import decimal_precision as dp
28
29 class account_asset_category(osv.osv):
30     _name = 'account.asset.category'
31     _description = 'Asset category'
32
33     _columns = {
34         'name': fields.char('Name', size=64, required=True, select=1),
35         'note': fields.text('Note'),
36         'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic account'),
37         'account_asset_id': fields.many2one('account.account', 'Asset Account', required=True),
38         'account_depreciation_id': fields.many2one('account.account', 'Depreciation Account', required=True),
39         'account_expense_depreciation_id': fields.many2one('account.account', 'Depr. Expense Account', required=True),
40         'journal_id': fields.many2one('account.journal', 'Journal', required=True),
41         'company_id': fields.many2one('res.company', 'Company', required=True),
42         'method': fields.selection([('linear','Linear'),('degressive','Degressive')], 'Computation Method', required=True, help="Choose the method to use to compute the amount of depreciation lines.\n"\
43             "  * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n" \
44             "  * Degressive: Calculated on basis of: Remaining Value * Degressive Factor"),
45         'method_number': fields.integer('Number of Depreciations'),
46         'method_period': fields.integer('Period Length', help="State here the time between 2 depreciations, in months", required=True),
47         'method_progress_factor': fields.float('Degressive Factor'),
48         'method_time': fields.selection([('number','Number of Depreciations'),('end','Ending Date')], 'Time Method', required=True,
49                                   help="Choose the method to use to compute the dates and number of depreciation lines.\n"\
50                                        "  * Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" \
51                                        "  * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."),
52         'method_end': fields.date('Ending date'),
53         'prorata':fields.boolean('Prorata Temporis', help='Indicates that the first depreciation entry for this asset have to be done from the purchase date instead of the first January'),
54         'open_asset': fields.boolean('Skip Draft State', help="Check this if you want to automatically confirm the assets of this category when created by invoices."),
55     }
56
57     _defaults = {
58         'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'account.asset.category', context=context),
59         'method': 'linear',
60         'method_number': 5,
61         'method_time': 'number',
62         'method_period': 12,
63         'method_progress_factor': 0.3,
64     }
65
66     def onchange_account_asset(self, cr, uid, ids, account_asset_id, context=None):
67         res = {'value':{}}
68         if account_asset_id:
69            res['value'] = {'account_depreciation_id': account_asset_id}
70         return res
71
72 account_asset_category()
73
74 class account_asset_asset(osv.osv):
75     _name = 'account.asset.asset'
76     _description = 'Asset'
77
78     def _get_period(self, cr, uid, context=None):
79         periods = self.pool.get('account.period').find(cr, uid)
80         if periods:
81             return periods[0]
82         else:
83             return False
84
85     def _get_last_depreciation_date(self, cr, uid, ids, context=None):
86         """
87         @param id: ids of a account.asset.asset objects
88         @return: Returns a dictionary of the effective dates of the last depreciation entry made for given asset ids. If there isn't any, return the purchase date of this asset
89         """
90         cr.execute("""
91             SELECT a.id as id, COALESCE(MAX(l.date),a.purchase_date) AS date
92             FROM account_asset_asset a
93             LEFT JOIN account_move_line l ON (l.asset_id = a.id)
94             WHERE a.id IN %s
95             GROUP BY a.id, a.purchase_date """, (tuple(ids),))
96         return dict(cr.fetchall())
97
98     def _compute_board_amount(self, cr, uid, asset, i, residual_amount, amount_to_depr, undone_dotation_number, posted_depreciation_line_ids, total_days, depreciation_date, context=None):
99         #by default amount = 0
100         amount = 0
101         if i == undone_dotation_number:
102             amount = residual_amount
103         else:
104             if asset.method == 'linear':
105                 amount = amount_to_depr / (undone_dotation_number - len(posted_depreciation_line_ids))
106                 if asset.prorata:
107                     amount = amount_to_depr / asset.method_number
108                     days = total_days - float(depreciation_date.strftime('%j'))
109                     if i == 1:
110                         amount = (amount_to_depr / asset.method_number) / total_days * days
111                     elif i == undone_dotation_number:
112                         amount = (amount_to_depr / asset.method_number) / total_days * (total_days - days)
113             elif asset.method == 'degressive':
114                 amount = residual_amount * asset.method_progress_factor
115                 if asset.prorata:
116                     days = total_days - float(depreciation_date.strftime('%j'))
117                     if i == 1:
118                         amount = (residual_amount * asset.method_progress_factor) / total_days * days
119                     elif i == undone_dotation_number:
120                         amount = (residual_amount * asset.method_progress_factor) / total_days * (total_days - days)
121         return amount
122
123     def _compute_board_undone_dotation_nb(self, cr, uid, asset, depreciation_date, total_days, context=None):
124         undone_dotation_number = asset.method_number
125         if asset.method_time == 'end':
126             end_date = datetime.strptime(asset.method_end, '%Y-%m-%d')
127             undone_dotation_number = 0
128             while depreciation_date <= end_date:
129                 depreciation_date = (datetime(depreciation_date.year, depreciation_date.month, depreciation_date.day) + relativedelta(months=+asset.method_period))
130                 undone_dotation_number += 1
131         if asset.prorata:
132             undone_dotation_number += 1
133         return undone_dotation_number
134
135     def compute_depreciation_board(self, cr, uid, ids, context=None):
136         depreciation_lin_obj = self.pool.get('account.asset.depreciation.line')
137         for asset in self.browse(cr, uid, ids, context=context):
138             if asset.value_residual == 0.0:
139                 continue
140             posted_depreciation_line_ids = depreciation_lin_obj.search(cr, uid, [('asset_id', '=', asset.id), ('move_check', '=', True)])
141             old_depreciation_line_ids = depreciation_lin_obj.search(cr, uid, [('asset_id', '=', asset.id), ('move_id', '=', False)])
142             if old_depreciation_line_ids:
143                 depreciation_lin_obj.unlink(cr, uid, old_depreciation_line_ids, context=context)
144
145             amount_to_depr = residual_amount = asset.value_residual
146             if asset.prorata:
147                 depreciation_date = datetime.strptime(self._get_last_depreciation_date(cr, uid, [asset.id], context)[asset.id], '%Y-%m-%d')
148             else:
149                 # depreciation_date = 1st January of purchase year
150                 purchase_date = datetime.strptime(asset.purchase_date, '%Y-%m-%d')
151                 depreciation_date = datetime(purchase_date.year, 1, 1)
152             day = depreciation_date.day
153             month = depreciation_date.month
154             year = depreciation_date.year
155             total_days = (year % 4) and 365 or 366
156
157             undone_dotation_number = self._compute_board_undone_dotation_nb(cr, uid, asset, depreciation_date, total_days, context=context)
158             for x in range(len(posted_depreciation_line_ids), undone_dotation_number):
159                 i = x + 1
160                 amount = self._compute_board_amount(cr, uid, asset, i, residual_amount, amount_to_depr, undone_dotation_number, posted_depreciation_line_ids, total_days, depreciation_date, context=context)
161                 residual_amount -= amount
162                 vals = {
163                      'amount': amount,
164                      'asset_id': asset.id,
165                      'sequence': i,
166                      'name': str(asset.id) +'/' + str(i),
167                      'remaining_value': residual_amount,
168                      'depreciated_value': (asset.purchase_value - asset.salvage_value) - (residual_amount + amount),
169                      'depreciation_date': depreciation_date.strftime('%Y-%m-%d'),
170                 }
171                 depreciation_lin_obj.create(cr, uid, vals, context=context)
172                 # Considering Depr. Period as months
173                 depreciation_date = (datetime(year, month, day) + relativedelta(months=+asset.method_period))
174                 day = depreciation_date.day
175                 month = depreciation_date.month
176                 year = depreciation_date.year
177         return True
178
179     def validate(self, cr, uid, ids, context=None):
180         if context is None:
181             context = {}
182         return self.write(cr, uid, ids, {
183             'state':'open'
184         }, context)
185
186     def set_to_close(self, cr, uid, ids, context=None):
187         return self.write(cr, uid, ids, {'state': 'close'}, context=context)
188
189     def set_to_draft(self, cr, uid, ids, context=None):
190         return self.write(cr, uid, ids, {'state': 'draft'}, context=context)
191
192     def _amount_residual(self, cr, uid, ids, name, args, context=None):
193         cr.execute("""SELECT
194                 l.asset_id as id, round(SUM(abs(l.debit-l.credit))) AS amount
195             FROM
196                 account_move_line l
197             WHERE
198                 l.asset_id IN %s GROUP BY l.asset_id """, (tuple(ids),))
199         res=dict(cr.fetchall())
200         for asset in self.browse(cr, uid, ids, context):
201             res[asset.id] = asset.purchase_value - res.get(asset.id, 0.0) - asset.salvage_value
202         for id in ids:
203             res.setdefault(id, 0.0)
204         return res
205
206     def onchange_company_id(self, cr, uid, ids, company_id=False, context=None):
207         val = {}
208         if company_id:
209             company = self.pool.get('res.company').browse(cr, uid, company_id, context=context)
210             if company.currency_id.company_id and company.currency_id.company_id.id != company_id:
211                 val['currency_id'] = False
212             else:
213                 val['currency_id'] = company.currency_id.id
214         return {'value': val}
215
216     _columns = {
217         'account_move_line_ids': fields.one2many('account.move.line', 'asset_id', 'Entries', readonly=True, states={'draft':[('readonly',False)]}),
218         'name': fields.char('Asset Name', size=64, required=True, readonly=True, states={'draft':[('readonly',False)]}),
219         'code': fields.char('Reference', size=32, readonly=True, states={'draft':[('readonly',False)]}),
220         'purchase_value': fields.float('Gross Value', required=True, readonly=True, states={'draft':[('readonly',False)]}),
221         'currency_id': fields.many2one('res.currency','Currency',required=True, readonly=True, states={'draft':[('readonly',False)]}),
222         'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True, states={'draft':[('readonly',False)]}),
223         'note': fields.text('Note'),
224         'category_id': fields.many2one('account.asset.category', 'Asset Category', required=True, change_default=True, readonly=True, states={'draft':[('readonly',False)]}),
225         'parent_id': fields.many2one('account.asset.asset', 'Parent Asset', readonly=True, states={'draft':[('readonly',False)]}),
226         'child_ids': fields.one2many('account.asset.asset', 'parent_id', 'Children Assets'),
227         'purchase_date': fields.date('Purchase Date', required=True, readonly=True, states={'draft':[('readonly',False)]}),
228         'state': fields.selection([('draft','Draft'),('open','Running'),('close','Close')], 'Status', required=True,
229                                   help="When an asset is created, the status is 'Draft'.\n" \
230                                        "If the asset is confirmed, the status goes in 'Running' and the depreciation lines can be posted in the accounting.\n" \
231                                        "You can manually close an asset when the depreciation is over. If the last line of depreciation is posted, the asset automatically goes in that status."),
232         'active': fields.boolean('Active'),
233         'partner_id': fields.many2one('res.partner', 'Partner', readonly=True, states={'draft':[('readonly',False)]}),
234         'method': fields.selection([('linear','Linear'),('degressive','Degressive')], 'Computation Method', required=True, readonly=True, states={'draft':[('readonly',False)]}, help="Choose the method to use to compute the amount of depreciation lines.\n"\
235             "  * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n" \
236             "  * Degressive: Calculated on basis of: Remaining Value * Degressive Factor"),
237         'method_number': fields.integer('Number of Depreciations', readonly=True, states={'draft':[('readonly',False)]}, help="Calculates Depreciation within specified interval"),
238         'method_period': fields.integer('Number of Months in a Period', required=True, readonly=True, states={'draft':[('readonly',False)]}, help="State here the time during 2 depreciations, in months"),
239         'method_end': fields.date('Ending Date', readonly=True, states={'draft':[('readonly',False)]}),
240         'method_progress_factor': fields.float('Degressive Factor', readonly=True, states={'draft':[('readonly',False)]}),
241         'value_residual': fields.function(_amount_residual, method=True, digits_compute=dp.get_precision('Account'), string='Residual Value'),
242         'method_time': fields.selection([('number','Number of Depreciations'),('end','Ending Date')], 'Time Method', required=True, readonly=True, states={'draft':[('readonly',False)]},
243                                   help="Choose the method to use to compute the dates and number of depreciation lines.\n"\
244                                        "  * Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" \
245                                        "  * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."),
246         'prorata':fields.boolean('Prorata Temporis', readonly=True, states={'draft':[('readonly',False)]}, help='Indicates that the first depreciation entry for this asset have to be done from the purchase date instead of the first January'),
247         'history_ids': fields.one2many('account.asset.history', 'asset_id', 'History', readonly=True),
248         'depreciation_line_ids': fields.one2many('account.asset.depreciation.line', 'asset_id', 'Depreciation Lines', readonly=True, states={'draft':[('readonly',False)],'open':[('readonly',False)]}),
249         'salvage_value': fields.float('Salvage Value', digits_compute=dp.get_precision('Account'), help="It is the amount you plan to have that you cannot depreciate.", readonly=True, states={'draft':[('readonly',False)]}),
250     }
251     _defaults = {
252         'code': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'account.asset.code'),
253         'purchase_date': lambda obj, cr, uid, context: time.strftime('%Y-%m-%d'),
254         'active': True,
255         'state': 'draft',
256         'method': 'linear',
257         'method_number': 5,
258         'method_time': 'number',
259         'method_period': 12,
260         'method_progress_factor': 0.3,
261         'currency_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.currency_id.id,
262         'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'account.asset.asset',context=context),
263     }
264
265     def _check_recursion(self, cr, uid, ids, context=None, parent=None):
266         return super(account_asset_asset, self)._check_recursion(cr, uid, ids, context=context, parent=parent)
267
268     def _check_prorata(self, cr, uid, ids, context=None):
269         for asset in self.browse(cr, uid, ids, context=context):
270             if asset.prorata and asset.method_time != 'number':
271                 return False
272         return True
273
274     _constraints = [
275         (_check_recursion, 'Error ! You cannot create recursive assets.', ['parent_id']),
276         (_check_prorata, 'Prorata temporis can be applied only for time method "number of depreciations".', ['prorata']),
277     ]
278
279     def onchange_category_id(self, cr, uid, ids, category_id, context=None):
280         res = {'value':{}}
281         asset_categ_obj = self.pool.get('account.asset.category')
282         if category_id:
283             category_obj = asset_categ_obj.browse(cr, uid, category_id, context=context)
284             res['value'] = {
285                             'method': category_obj.method,
286                             'method_number': category_obj.method_number,
287                             'method_time': category_obj.method_time,
288                             'method_period': category_obj.method_period,
289                             'method_progress_factor': category_obj.method_progress_factor,
290                             'method_end': category_obj.method_end,
291                             'prorata': category_obj.prorata,
292             }
293         return res
294
295     def onchange_method_time(self, cr, uid, ids, method_time='number', context=None):
296         res = {'value': {}}
297         if method_time != 'number':
298             res['value'] = {'prorata': False}
299         return res
300
301     def copy(self, cr, uid, id, default=None, context=None):
302         if default is None:
303             default = {}
304         if context is None:
305             context = {}
306         default.update({'depreciation_line_ids': [], 'state': 'draft'})
307         return super(account_asset_asset, self).copy(cr, uid, id, default, context=context)
308
309     def _compute_entries(self, cr, uid, ids, period_id, context=None):
310         result = []
311         period_obj = self.pool.get('account.period')
312         depreciation_obj = self.pool.get('account.asset.depreciation.line')
313         period = period_obj.browse(cr, uid, period_id, context=context)
314         depreciation_ids = depreciation_obj.search(cr, uid, [('asset_id', 'in', ids), ('depreciation_date', '<=', period.date_stop), ('depreciation_date', '>=', period.date_start), ('move_check', '=', False)], context=context)
315         return depreciation_obj.create_move(cr, uid, depreciation_ids, context=context)
316
317     def create(self, cr, uid, vals, context=None):
318         asset_id = super(account_asset_asset, self).create(cr, uid, vals, context=context)
319         self.compute_depreciation_board(cr, uid, [asset_id], context=context)
320         return asset_id
321     
322     def open_entries(self, cr, uid, ids, context=None):
323         if context is None:
324             context = {}
325         context.update({'search_default_asset_id': ids, 'default_asset_id': ids})
326         return {
327             'view_type': 'form',
328             'view_mode': 'tree,form',
329             'res_model': 'account.move.line',
330             'view_id': False,
331             'type': 'ir.actions.act_window',
332             'context': context,
333         }
334
335 account_asset_asset()
336
337 class account_asset_depreciation_line(osv.osv):
338     _name = 'account.asset.depreciation.line'
339     _description = 'Asset depreciation line'
340
341     def _get_move_check(self, cr, uid, ids, name, args, context=None):
342         res = {}
343         for line in self.browse(cr, uid, ids, context=context):
344             res[line.id] = bool(line.move_id)
345         return res
346
347     _columns = {
348         'name': fields.char('Depreciation Name', size=64, required=True, select=1),
349         'sequence': fields.integer('Sequence', required=True),
350         'asset_id': fields.many2one('account.asset.asset', 'Asset', required=True),
351         'parent_state': fields.related('asset_id', 'state', type='char', string='State of Asset'),
352         'amount': fields.float('Depreciation Amount', required=True),
353         'remaining_value': fields.float('Amount to Depreciate', required=True),
354         'depreciated_value': fields.float('Amount Already Depreciated', required=True),
355         'depreciation_date': fields.date('Depreciation Date', select=1),
356         'move_id': fields.many2one('account.move', 'Depreciation Entry'),
357         'move_check': fields.function(_get_move_check, method=True, type='boolean', string='Posted', store=True)
358     }
359
360     def create_move(self, cr, uid, ids, context=None):
361         can_close = False
362         if context is None:
363             context = {}
364         asset_obj = self.pool.get('account.asset.asset')
365         period_obj = self.pool.get('account.period')
366         move_obj = self.pool.get('account.move')
367         move_line_obj = self.pool.get('account.move.line')
368         currency_obj = self.pool.get('res.currency')
369         created_move_ids = []
370         for line in self.browse(cr, uid, ids, context=context):
371             if currency_obj.is_zero(cr, uid, line.asset_id.currency_id, line.remaining_value):
372                 can_close = True
373             depreciation_date = time.strftime('%Y-%m-%d')
374             period_ids = period_obj.find(cr, uid, depreciation_date, context=context)
375             company_currency = line.asset_id.company_id.currency_id.id
376             current_currency = line.asset_id.currency_id.id
377             context.update({'date': depreciation_date})
378             amount = currency_obj.compute(cr, uid, current_currency, company_currency, line.amount, context=context)
379             sign == (line.asset_id.category_id.journal_id.type == 'purchase' and 1) or -1
380             asset_name = line.asset_id.name
381             reference = line.name
382             move_vals = {
383                 'name': asset_name,
384                 'date': depreciation_date,
385                 'ref': reference,
386                 'period_id': period_ids and period_ids[0] or False,
387                 'journal_id': line.asset_id.category_id.journal_id.id,
388                 }
389             move_id = move_obj.create(cr, uid, move_vals, context=context)
390             journal_id = line.asset_id.category_id.journal_id.id
391             partner_id = line.asset_id.partner_id.id
392             move_line_obj.create(cr, uid, {
393                 'name': asset_name,
394                 'ref': reference,
395                 'move_id': move_id,
396                 'account_id': line.asset_id.category_id.account_depreciation_id.id,
397                 'debit': 0.0,
398                 'credit': amount,
399                 'period_id': period_ids and period_ids[0] or False,
400                 'journal_id': journal_id,
401                 'partner_id': partner_id,
402                 'currency_id': company_currency <> current_currency and  current_currency or False,
403                 'amount_currency': company_currency <> current_currency and - sign * line.amount or 0.0,
404                 'date': depreciation_date,
405             })
406             move_line_obj.create(cr, uid, {
407                 'name': asset_name,
408                 'ref': reference,
409                 'move_id': move_id,
410                 'account_id': line.asset_id.category_id.account_expense_depreciation_id.id,
411                 'credit': 0.0,
412                 'debit': amount,
413                 'period_id': period_ids and period_ids[0] or False,
414                 'journal_id': journal_id,
415                 'partner_id': partner_id,
416                 'currency_id': company_currency <> current_currency and  current_currency or False,
417                 'amount_currency': company_currency <> current_currency and sign * line.amount or 0.0,
418                 'analytic_account_id': line.asset_id.category_id.account_analytic_id.id,
419                 'date': depreciation_date,
420                 'asset_id': line.asset_id.id
421             })
422             self.write(cr, uid, line.id, {'move_id': move_id}, context=context)
423             created_move_ids.append(move_id)
424             if can_close:
425                 asset_obj.write(cr, uid, [line.asset_id.id], {'state': 'close'}, context=context)
426         return created_move_ids
427
428 account_asset_depreciation_line()
429
430 class account_move_line(osv.osv):
431     _inherit = 'account.move.line'
432     _columns = {
433         'asset_id': fields.many2one('account.asset.asset', 'Asset'),
434         'entry_ids': fields.one2many('account.move.line', 'asset_id', 'Entries', readonly=True, states={'draft':[('readonly',False)]}),
435
436     }
437 account_move_line()
438
439 class account_asset_history(osv.osv):
440     _name = 'account.asset.history'
441     _description = 'Asset history'
442     _columns = {
443         'name': fields.char('History name', size=64, select=1),
444         'user_id': fields.many2one('res.users', 'User', required=True),
445         'date': fields.date('Date', required=True),
446         'asset_id': fields.many2one('account.asset.asset', 'Asset', required=True),
447         'method_time': fields.selection([('number','Number of Depreciations'),('end','Ending Date')], 'Time Method', required=True,
448                                   help="The method to use to compute the dates and number of depreciation lines.\n"\
449                                        "Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" \
450                                        "Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."),
451         'method_number': fields.integer('Number of Depreciations'),
452         'method_period': fields.integer('Period Length', help="Time in month between two depreciations"),
453         'method_end': fields.date('Ending date'),
454         'note': fields.text('Note'),
455     }
456     _order = 'date desc'
457     _defaults = {
458         'date': lambda *args: time.strftime('%Y-%m-%d'),
459         'user_id': lambda self, cr, uid, ctx: uid
460     }
461
462 account_asset_history()
463
464 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: