142fc89df09613ef5ebc7a7c7e5b672c8edd8dc9
[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={}):
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         return amount
116
117     def _compute_board_undone_dotation_nb(self, cr, uid, asset, depreciation_date, total_days, context=None):
118         undone_dotation_number = asset.method_number
119         if asset.method_time == 'end':
120             end_date = datetime.strptime(asset.method_end, '%Y-%m-%d')
121             undone_dotation_number = (end_date - depreciation_date).days / total_days
122         if asset.prorata or asset.method_time == 'end':
123             undone_dotation_number += 1
124         return undone_dotation_number
125
126     def compute_depreciation_board(self, cr, uid,ids, context=None):
127         depreciation_lin_obj = self.pool.get('account.asset.depreciation.line')
128         for asset in self.browse(cr, uid, ids, context=context):
129             if asset.value_residual == 0.0:
130                 continue
131             posted_depreciation_line_ids = depreciation_lin_obj.search(cr, uid, [('asset_id', '=', asset.id), ('move_check', '=', True)])
132             old_depreciation_line_ids = depreciation_lin_obj.search(cr, uid, [('asset_id', '=', asset.id), ('move_id', '=', False)])
133             if old_depreciation_line_ids:
134                 depreciation_lin_obj.unlink(cr, uid, old_depreciation_line_ids, context=context)
135             
136             amount_to_depr = residual_amount = asset.value_residual
137
138             depreciation_date = datetime.strptime(self._get_last_depreciation_date(cr, uid, [asset.id], context)[asset.id], '%Y-%m-%d')
139             day = depreciation_date.day
140             month = depreciation_date.month
141             year = depreciation_date.year
142             total_days = (year % 4) and 365 or 366
143
144             undone_dotation_number = self._compute_board_undone_dotation_nb(cr, uid, asset, depreciation_date, total_days, context=context)
145             for x in range(len(posted_depreciation_line_ids), undone_dotation_number):
146                 i = x + 1
147                 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)
148                 residual_amount -= amount
149                 vals = {
150                      'amount': amount,
151                      'asset_id': asset.id,
152                      'sequence': i,
153                      'name': str(asset.id) +'/' + str(i),
154                      'remaining_value': residual_amount,
155                      'depreciated_value': (asset.purchase_value - asset.salvage_value) - (residual_amount + amount),
156                      'depreciation_date': depreciation_date.strftime('%Y-%m-%d'),
157                 }
158                 depreciation_lin_obj.create(cr, uid, vals, context=context)
159                 # Considering Depr. Period as months
160                 depreciation_date = (datetime(year, month, day) + relativedelta(months=+asset.method_period))
161                 day = depreciation_date.day
162                 month = depreciation_date.month
163                 year = depreciation_date.year
164         return True
165
166     def validate(self, cr, uid, ids, context={}):
167         return self.write(cr, uid, ids, {
168             'state':'open'
169         }, context)
170
171     def set_to_close(self, cr, uid, ids, context=None):
172         return self.write(cr, uid, ids, {'state': 'close'}, context=context)
173
174     def _amount_residual(self, cr, uid, ids, name, args, context=None):
175         cr.execute("""SELECT
176                 l.asset_id as id, round(SUM(abs(l.debit-l.credit))) AS amount
177             FROM
178                 account_move_line l
179             WHERE
180                 l.asset_id IN %s GROUP BY l.asset_id """, (tuple(ids),))
181         res=dict(cr.fetchall())
182         for asset in self.browse(cr, uid, ids, context):
183             res[asset.id] = asset.purchase_value - res.get(asset.id, 0.0) - asset.salvage_value
184         for id in ids:
185             res.setdefault(id, 0.0)
186         return res
187
188     _columns = {
189         'account_move_line_ids': fields.one2many('account.move.line', 'asset_id', 'Entries', readonly=True, states={'draft':[('readonly',False)]}),
190         'name': fields.char('Asset', size=64, required=True, readonly=True, states={'draft':[('readonly',False)]}),
191         'code': fields.char('Reference ', size=16, readonly=True, states={'draft':[('readonly',False)]}),
192         'purchase_value': fields.float('Gross value ', required=True, readonly=True, states={'draft':[('readonly',False)]}),
193         'currency_id': fields.many2one('res.currency','Currency',required=True, readonly=True, states={'draft':[('readonly',False)]}),
194         'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True, states={'draft':[('readonly',False)]}),
195         'note': fields.text('Note'),
196         'category_id': fields.many2one('account.asset.category', 'Asset category', required=True, change_default=True, readonly=True, states={'draft':[('readonly',False)]}),
197         'parent_id': fields.many2one('account.asset.asset', 'Parent Asset', readonly=True, states={'draft':[('readonly',False)]}),
198         'child_ids': fields.one2many('account.asset.asset', 'parent_id', 'Children Assets'),
199         'purchase_date': fields.date('Purchase Date', required=True, readonly=True, states={'draft':[('readonly',False)]}),
200         'state': fields.selection([('draft','Draft'),('open','Running'),('close','Close')], 'State', required=True,
201                                   help="When an asset is created, the state is 'Draft'.\n" \
202                                        "If the asset is confirmed, the state goes in 'Running' and the depreciation lines can be posted in the accounting.\n" \
203                                        "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 state."),
204         'active': fields.boolean('Active'),
205         'partner_id': fields.many2one('res.partner', 'Partner', readonly=True, states={'draft':[('readonly',False)]}),
206         '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"\
207             "  * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n" \
208             "  * Degressive: Calculated on basis of: Remaining Value * Degressive Factor"),
209         'method_number': fields.integer('Number of Depreciations', readonly=True, states={'draft':[('readonly',False)]}, help="Calculates Depreciation within specified interval"),
210         'method_period': fields.integer('Period Length', required=True, readonly=True, states={'draft':[('readonly',False)]}, help="State here the time during 2 depreciations, in months"),
211         'method_end': fields.date('Ending Date', readonly=True, states={'draft':[('readonly',False)]}),
212         'method_progress_factor': fields.float('Degressive Factor', readonly=True, states={'draft':[('readonly',False)]}),
213         'value_residual': fields.function(_amount_residual, method=True, digits_compute=dp.get_precision('Account'), string='Residual Value'),
214         'method_time': fields.selection([('number','Number of Depreciations'),('end','Ending Date')], 'Time Method', required=True, readonly=True, states={'draft':[('readonly',False)]}, 
215                                   help="Choose the method to use to compute the dates and number of depreciation lines.\n"\
216                                        "  * Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" \
217                                        "  * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."),
218         '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'),
219         'history_ids': fields.one2many('account.asset.history', 'asset_id', 'History', readonly=True),
220         'depreciation_line_ids': fields.one2many('account.asset.depreciation.line', 'asset_id', 'Depreciation Lines', readonly=True, states={'draft':[('readonly',False)],'open':[('readonly',False)]}),
221         '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)]}),
222     }
223     _defaults = {
224         'code': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'account.asset.code'),
225         'purchase_date': lambda obj, cr, uid, context: time.strftime('%Y-%m-%d'),
226         'active': True,
227         'state': 'draft',
228         'method': 'linear',
229         'method_number': 5,
230         'method_time': 'number',
231         'method_period': 12,
232         'method_progress_factor': 0.3,
233         'currency_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.currency_id.id,
234         'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'account.asset.asset',context=context),
235     }
236     
237     def _check_recursion(self, cr, uid, ids, context=None, parent=None):
238         return super(account_asset_asset, self)._check_recursion(cr, uid, ids, context=context, parent=parent)
239
240     def _check_prorata(self, cr, uid, ids, context=None):
241         for asset in self.browse(cr, uid, ids, context=context):
242             if asset.prorata and (asset.method != 'linear' or asset.method_time != 'number'):
243                 return False
244         return True
245
246     _constraints = [
247         (_check_recursion, 'Error ! You can not create recursive assets.', ['parent_id']),
248         (_check_prorata, 'Prorata temporis can be applied only for computation method "linear" and time method "number of depreciations".', ['prorata']),
249     ]
250
251     def onchange_category_id(self, cr, uid, ids, category_id, context=None):
252         res = {'value':{}}
253         asset_categ_obj = self.pool.get('account.asset.category')
254         if category_id:
255             category_obj = asset_categ_obj.browse(cr, uid, category_id, context=context)
256             res['value'] = {
257                             'method': category_obj.method,
258                             'method_number': category_obj.method_number,
259                             'method_time': category_obj.method_time,
260                             'method_period': category_obj.method_period,
261                             'method_progress_factor': category_obj.method_progress_factor,
262                             'method_end': category_obj.method_end,
263                             'prorata': category_obj.prorata,
264             }
265         return res
266
267     def onchange_method_time(self, cr, uid, ids, method='linear', method_time='number', context=None):
268         res = {'value': {}}
269         if method != 'linear' or method_time != 'number':
270             res['value'] = {'prorata': False}
271         return res
272
273     def copy(self, cr, uid, id, default=None, context=None):
274         if default is None:
275             default = {}
276         if context is None:
277             context = {}
278         default.update({'depreciation_line_ids': [], 'state': 'draft'})
279         return super(account_asset_asset, self).copy(cr, uid, id, default, context=context)
280
281     def _compute_entries(self, cr, uid, ids, period_id, context={}):
282         result = []
283         period_obj = self.pool.get('account.period')
284         depreciation_obj = self.pool.get('account.asset.depreciation.line')
285         period = period_obj.browse(cr, uid, period_id, context=context) 
286         depreciation_ids = depreciation_obj.search(cr, uid, [('asset_id', 'in', ids), ('depreciation_date','<',period.date_stop), ('depreciation_date', '>', period.date_start)], context=context)
287         return depreciation_obj.create_move(cr, uid, depreciation_ids, context=context)
288
289     def create(self, cr, uid, vals, context=None):
290         asset_id = super(account_asset_asset, self).create(cr, uid, vals, context=context)
291         self.compute_depreciation_board(cr, uid, [asset_id], context=context)
292         return asset_id
293
294 account_asset_asset()
295
296 class account_asset_depreciation_line(osv.osv):
297     _name = 'account.asset.depreciation.line'
298     _description = 'Asset depreciation line'
299
300     def _get_move_check(self, cr, uid, ids, name, args, context=None):
301         res = {}
302         for line in self.browse(cr, uid, ids, context=context):
303             res[line.id] = bool(line.move_id)
304         return res
305
306     _columns = {
307         'name': fields.char('Depreciation Name', size=64, required=True, select=1),
308         'sequence': fields.integer('Sequence of the depreciation', required=True),
309         'asset_id': fields.many2one('account.asset.asset', 'Asset', required=True),
310         'parent_state': fields.related('asset_id', 'state', type='char', string='State of Asset'),
311         'amount': fields.float('Depreciation Amount', required=True),
312         'remaining_value': fields.float('Amount to Depreciate', required=True),
313         'depreciated_value': fields.float('Amount Already Depreciated', required=True),
314         'depreciation_date': fields.char('Depreciation Date', size=64, select=1),
315         'move_id': fields.many2one('account.move', 'Depreciation Entry'),
316         'move_check': fields.function(_get_move_check, method=True, type='boolean', string='Posted', store=True)
317     }
318
319     def create_move(self, cr, uid, ids, context=None):
320         can_close = False
321         if context is None:
322             context = {}
323         asset_obj = self.pool.get('account.asset.asset')
324         period_obj = self.pool.get('account.period')
325         move_obj = self.pool.get('account.move')
326         move_line_obj = self.pool.get('account.move.line')
327         currency_obj = self.pool.get('res.currency')
328         created_move_ids = []
329         for line in self.browse(cr, uid, ids, context=context):
330             if currency_obj.is_zero(cr, uid, line.asset_id.currency_id, line.remaining_value):
331                 can_close = True
332             depreciation_date = line.asset_id.prorata and line.asset_id.purchase_date or time.strftime('%Y-%m-%d')
333             period_ids = period_obj.find(cr, uid, depreciation_date, context=context)
334             company_currency = line.asset_id.company_id.currency_id.id
335             current_currency = line.asset_id.currency_id.id
336             context.update({'date': depreciation_date})
337             amount = currency_obj.compute(cr, uid, current_currency, company_currency, line.amount, context=context)
338             sign = line.asset_id.category_id.journal_id.type = 'purchase' and 1 or -1
339             move_vals = {
340                 'name': line.name,
341                 'date': depreciation_date,
342                 'ref': line.name,
343                 'period_id': period_ids and period_ids[0] or False,
344                 'journal_id': line.asset_id.category_id.journal_id.id,
345                 }
346             move_id = move_obj.create(cr, uid, move_vals, context=context)
347             asset_name = line.asset_id.name
348             reference = line.name
349             journal_id = line.asset_id.category_id.journal_id.id
350             partner_id = line.asset_id.partner_id.id
351             move_line_obj.create(cr, uid, {
352                 'name': asset_name,
353                 'ref': reference,
354                 'move_id': move_id,
355                 'account_id': line.asset_id.category_id.account_depreciation_id.id,
356                 'debit': 0.0,
357                 'credit': amount,
358                 'period_id': period_ids and period_ids[0] or False,
359                 'journal_id': journal_id,
360                 'partner_id': partner_id,
361                 'currency_id': company_currency <> current_currency and  current_currency or False,
362                 'amount_currency': company_currency <> current_currency and - sign * line.amount or 0.0,
363                 'date': depreciation_date,
364             })
365             move_line_obj.create(cr, uid, {
366                 'name': asset_name,
367                 'ref': reference,
368                 'move_id': move_id,
369                 'account_id': line.asset_id.category_id.account_expense_depreciation_id.id,
370                 'credit': 0.0,
371                 'debit': amount,
372                 'period_id': period_ids and period_ids[0] or False,
373                 'journal_id': journal_id,
374                 'partner_id': partner_id,
375                 'currency_id': company_currency <> current_currency and  current_currency or False,
376                 'amount_currency': company_currency <> current_currency and sign * line.amount or 0.0,
377                 'analytic_account_id': line.asset_id.category_id.account_analytic_id.id,
378                 'date': depreciation_date,
379                 'asset_id': line.asset_id.id
380             })
381             self.write(cr, uid, line.id, {'move_id': move_id}, context=context)
382             created_move_ids.append(move_id)
383             if can_close:
384                 asset_obj.write(cr, uid, [line.asset_id.id], {'state': 'close'}, context=context)                
385         return created_move_ids
386
387 account_asset_depreciation_line()
388
389 class account_move_line(osv.osv):
390     _inherit = 'account.move.line'
391     _columns = {
392         'asset_id': fields.many2one('account.asset.asset', 'Asset'),
393         'entry_ids': fields.one2many('account.move.line', 'asset_id', 'Entries', readonly=True, states={'draft':[('readonly',False)]}),
394
395     }
396 account_move_line()
397
398 class account_asset_history(osv.osv):
399     _name = 'account.asset.history'
400     _description = 'Asset history'
401     _columns = {
402         'name': fields.char('History name', size=64, select=1),
403         'user_id': fields.many2one('res.users', 'User', required=True),
404         'date': fields.date('Date', required=True),
405         'asset_id': fields.many2one('account.asset.asset', 'Asset', required=True),
406         'method_time': fields.selection([('number','Number of Depreciations'),('end','Ending Date')], 'Time Method', required=True, 
407                                   help="Choose the method to use to compute the dates and number of depreciation lines.\n"\
408                                        "Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" \
409                                        "Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."),
410         'method_number': fields.integer('Number of Depreciations'),
411         'method_period': fields.integer('Period Length', help="Time in month between two depreciations"),
412         'method_end': fields.date('Ending date'),
413         'note': fields.text('Note'),
414     }
415     _order = 'date desc'
416     _defaults = {
417         'date': lambda *args: time.strftime('%Y-%m-%d'),
418         'user_id': lambda self, cr, uid, ctx: uid
419     }
420     
421 account_asset_history()
422
423 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: