1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
23 from datetime import datetime
24 from dateutil.relativedelta import relativedelta
26 from openerp.osv import fields, osv
27 import openerp.addons.decimal_precision as dp
28 from tools.translate import _
30 class account_asset_category(osv.osv):
31 _name = 'account.asset.category'
32 _description = 'Asset category'
35 'name': fields.char('Name', size=64, required=True, select=1),
36 'note': fields.text('Note'),
37 'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic account'),
38 'account_asset_id': fields.many2one('account.account', 'Asset Account', required=True, domain=[('type','=','other')]),
39 'account_depreciation_id': fields.many2one('account.account', 'Depreciation Account', required=True, domain=[('type','=','other')]),
40 'account_expense_depreciation_id': fields.many2one('account.account', 'Depr. Expense Account', required=True, domain=[('type','=','other')]),
41 'journal_id': fields.many2one('account.journal', 'Journal', required=True),
42 'company_id': fields.many2one('res.company', 'Company', required=True),
43 '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"\
44 " * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n" \
45 " * Degressive: Calculated on basis of: Residual Value * Degressive Factor"),
46 'method_number': fields.integer('Number of Depreciations', help="The number of depreciations needed to depreciate your asset"),
47 'method_period': fields.integer('Period Length', help="State here the time between 2 depreciations, in months", required=True),
48 'method_progress_factor': fields.float('Degressive Factor'),
49 'method_time': fields.selection([('number','Number of Depreciations'),('end','Ending Date')], 'Time Method', required=True,
50 help="Choose the method to use to compute the dates and number of depreciation lines.\n"\
51 " * Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" \
52 " * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."),
53 'method_end': fields.date('Ending date'),
54 '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'),
55 '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."),
59 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'account.asset.category', context=context),
62 'method_time': 'number',
64 'method_progress_factor': 0.3,
67 def onchange_account_asset(self, cr, uid, ids, account_asset_id, context=None):
70 res['value'] = {'account_depreciation_id': account_asset_id}
73 account_asset_category()
75 class account_asset_asset(osv.osv):
76 _name = 'account.asset.asset'
77 _description = 'Asset'
79 def unlink(self, cr, uid, ids, context=None):
80 for asset in self.browse(cr, uid, ids, context=context):
81 if asset.account_move_line_ids:
82 raise osv.except_osv(_('Error!'), _('You cannot delete an asset that contains posted depreciation lines.'))
83 return super(account_asset_asset, self).unlink(cr, uid, ids, context=context)
85 def _get_period(self, cr, uid, context=None):
86 ctx = dict(context or {}, account_period_prefer_normal=True)
87 periods = self.pool.get('account.period').find(cr, uid, context=ctx)
93 def _get_last_depreciation_date(self, cr, uid, ids, context=None):
95 @param id: ids of a account.asset.asset objects
96 @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
99 SELECT a.id as id, COALESCE(MAX(l.date),a.purchase_date) AS date
100 FROM account_asset_asset a
101 LEFT JOIN account_move_line l ON (l.asset_id = a.id)
103 GROUP BY a.id, a.purchase_date """, (tuple(ids),))
104 return dict(cr.fetchall())
106 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):
107 #by default amount = 0
109 if i == undone_dotation_number:
110 amount = residual_amount
112 if asset.method == 'linear':
113 amount = amount_to_depr / (undone_dotation_number - len(posted_depreciation_line_ids))
115 amount = amount_to_depr / asset.method_number
116 days = total_days - float(depreciation_date.strftime('%j'))
118 amount = (amount_to_depr / asset.method_number) / total_days * days
119 elif i == undone_dotation_number:
120 amount = (amount_to_depr / asset.method_number) / total_days * (total_days - days)
121 elif asset.method == 'degressive':
122 amount = residual_amount * asset.method_progress_factor
124 days = total_days - float(depreciation_date.strftime('%j'))
126 amount = (residual_amount * asset.method_progress_factor) / total_days * days
127 elif i == undone_dotation_number:
128 amount = (residual_amount * asset.method_progress_factor) / total_days * (total_days - days)
131 def _compute_board_undone_dotation_nb(self, cr, uid, asset, depreciation_date, total_days, context=None):
132 undone_dotation_number = asset.method_number
133 if asset.method_time == 'end':
134 end_date = datetime.strptime(asset.method_end, '%Y-%m-%d')
135 undone_dotation_number = 0
136 while depreciation_date <= end_date:
137 depreciation_date = (datetime(depreciation_date.year, depreciation_date.month, depreciation_date.day) + relativedelta(months=+asset.method_period))
138 undone_dotation_number += 1
140 undone_dotation_number += 1
141 return undone_dotation_number
143 def compute_depreciation_board(self, cr, uid, ids, context=None):
144 depreciation_lin_obj = self.pool.get('account.asset.depreciation.line')
145 currency_obj = self.pool.get('res.currency')
146 for asset in self.browse(cr, uid, ids, context=context):
147 if asset.value_residual == 0.0:
149 posted_depreciation_line_ids = depreciation_lin_obj.search(cr, uid, [('asset_id', '=', asset.id), ('move_check', '=', True)],order='depreciation_date desc')
150 old_depreciation_line_ids = depreciation_lin_obj.search(cr, uid, [('asset_id', '=', asset.id), ('move_id', '=', False)])
151 if old_depreciation_line_ids:
152 depreciation_lin_obj.unlink(cr, uid, old_depreciation_line_ids, context=context)
154 amount_to_depr = residual_amount = asset.value_residual
156 depreciation_date = datetime.strptime(self._get_last_depreciation_date(cr, uid, [asset.id], context)[asset.id], '%Y-%m-%d')
158 # depreciation_date = 1st January of purchase year
159 purchase_date = datetime.strptime(asset.purchase_date, '%Y-%m-%d')
160 #if we already have some previous validated entries, starting date isn't 1st January but last entry + method period
161 if (len(posted_depreciation_line_ids)>0):
162 last_depreciation_date = datetime.strptime(depreciation_lin_obj.browse(cr,uid,posted_depreciation_line_ids[0],context=context).depreciation_date, '%Y-%m-%d')
163 depreciation_date = (last_depreciation_date+relativedelta(months=+asset.method_period))
165 depreciation_date = datetime(purchase_date.year, 1, 1)
166 day = depreciation_date.day
167 month = depreciation_date.month
168 year = depreciation_date.year
169 total_days = (year % 4) and 365 or 366
171 undone_dotation_number = self._compute_board_undone_dotation_nb(cr, uid, asset, depreciation_date, total_days, context=context)
172 for x in range(len(posted_depreciation_line_ids), undone_dotation_number):
174 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)
175 company_currency = asset.company_id.currency_id.id
176 current_currency = asset.currency_id.id
177 # compute amount into company currency
178 amount = currency_obj.compute(cr, uid, current_currency, company_currency, amount, context=context)
179 residual_amount -= amount
182 'asset_id': asset.id,
184 'name': "%s/%s" %(i, undone_dotation_number),
185 'remaining_value': residual_amount,
186 'depreciated_value': (asset.purchase_value - asset.salvage_value) - (residual_amount + amount),
187 'depreciation_date': depreciation_date.strftime('%Y-%m-%d'),
189 depreciation_lin_obj.create(cr, uid, vals, context=context)
190 # Considering Depr. Period as months
191 depreciation_date = (datetime(year, month, day) + relativedelta(months=+asset.method_period))
192 day = depreciation_date.day
193 month = depreciation_date.month
194 year = depreciation_date.year
197 def validate(self, cr, uid, ids, context=None):
200 return self.write(cr, uid, ids, {
204 def set_to_close(self, cr, uid, ids, context=None):
205 return self.write(cr, uid, ids, {'state': 'close'}, context=context)
207 def set_to_draft(self, cr, uid, ids, context=None):
208 return self.write(cr, uid, ids, {'state': 'draft'}, context=context)
210 def _amount_residual(self, cr, uid, ids, name, args, context=None):
212 l.asset_id as id, SUM(abs(l.debit-l.credit)) AS amount
216 l.asset_id IN %s GROUP BY l.asset_id """, (tuple(ids),))
217 res=dict(cr.fetchall())
218 for asset in self.browse(cr, uid, ids, context):
219 res[asset.id] = asset.purchase_value - res.get(asset.id, 0.0) - asset.salvage_value
221 res.setdefault(id, 0.0)
224 def onchange_company_id(self, cr, uid, ids, company_id=False, context=None):
227 company = self.pool.get('res.company').browse(cr, uid, company_id, context=context)
228 if company.currency_id.company_id and company.currency_id.company_id.id != company_id:
229 val['currency_id'] = False
231 val['currency_id'] = company.currency_id.id
232 return {'value': val}
234 def onchange_purchase_salvage_value(self, cr, uid, ids, purchase_value, salvage_value, context=None):
236 for asset in self.browse(cr, uid, ids, context=context):
238 val['value_residual'] = purchase_value - salvage_value
240 val['value_residual'] = purchase_value - salvage_value
241 return {'value': val}
244 'account_move_line_ids': fields.one2many('account.move.line', 'asset_id', 'Entries', readonly=True, states={'draft':[('readonly',False)]}),
245 'name': fields.char('Asset Name', size=64, required=True, readonly=True, states={'draft':[('readonly',False)]}),
246 'code': fields.char('Reference', size=32, readonly=True, states={'draft':[('readonly',False)]}),
247 'purchase_value': fields.float('Gross Value', required=True, readonly=True, states={'draft':[('readonly',False)]}),
248 'currency_id': fields.many2one('res.currency','Currency',required=True, readonly=True, states={'draft':[('readonly',False)]}),
249 'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True, states={'draft':[('readonly',False)]}),
250 'note': fields.text('Note'),
251 'category_id': fields.many2one('account.asset.category', 'Asset Category', required=True, change_default=True, readonly=True, states={'draft':[('readonly',False)]}),
252 'parent_id': fields.many2one('account.asset.asset', 'Parent Asset', readonly=True, states={'draft':[('readonly',False)]}),
253 'child_ids': fields.one2many('account.asset.asset', 'parent_id', 'Children Assets'),
254 'purchase_date': fields.date('Purchase Date', required=True, readonly=True, states={'draft':[('readonly',False)]}),
255 'state': fields.selection([('draft','Draft'),('open','Running'),('close','Close')], 'Status', required=True,
256 help="When an asset is created, the status is 'Draft'.\n" \
257 "If the asset is confirmed, the status goes in 'Running' and the depreciation lines can be posted in the accounting.\n" \
258 "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."),
259 'active': fields.boolean('Active'),
260 'partner_id': fields.many2one('res.partner', 'Partner', readonly=True, states={'draft':[('readonly',False)]}),
261 '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"\
262 " * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n" \
263 " * Degressive: Calculated on basis of: Residual Value * Degressive Factor"),
264 'method_number': fields.integer('Number of Depreciations', readonly=True, states={'draft':[('readonly',False)]}, help="The number of depreciations needed to depreciate your asset"),
265 'method_period': fields.integer('Number of Months in a Period', required=True, readonly=True, states={'draft':[('readonly',False)]}, help="The amount of time between two depreciations, in months"),
266 'method_end': fields.date('Ending Date', readonly=True, states={'draft':[('readonly',False)]}),
267 'method_progress_factor': fields.float('Degressive Factor', readonly=True, states={'draft':[('readonly',False)]}),
268 'value_residual': fields.function(_amount_residual, method=True, digits_compute=dp.get_precision('Account'), string='Residual Value'),
269 'method_time': fields.selection([('number','Number of Depreciations'),('end','Ending Date')], 'Time Method', required=True, readonly=True, states={'draft':[('readonly',False)]},
270 help="Choose the method to use to compute the dates and number of depreciation lines.\n"\
271 " * Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" \
272 " * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."),
273 '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'),
274 'history_ids': fields.one2many('account.asset.history', 'asset_id', 'History', readonly=True),
275 'depreciation_line_ids': fields.one2many('account.asset.depreciation.line', 'asset_id', 'Depreciation Lines', readonly=True, states={'draft':[('readonly',False)],'open':[('readonly',False)]}),
276 '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)]}),
279 'code': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'account.asset.code'),
280 'purchase_date': lambda obj, cr, uid, context: time.strftime('%Y-%m-%d'),
285 'method_time': 'number',
287 'method_progress_factor': 0.3,
288 'currency_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.currency_id.id,
289 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'account.asset.asset',context=context),
292 def _check_recursion(self, cr, uid, ids, context=None, parent=None):
293 return super(account_asset_asset, self)._check_recursion(cr, uid, ids, context=context, parent=parent)
295 def _check_prorata(self, cr, uid, ids, context=None):
296 for asset in self.browse(cr, uid, ids, context=context):
297 if asset.prorata and asset.method_time != 'number':
302 (_check_recursion, 'Error ! You cannot create recursive assets.', ['parent_id']),
303 (_check_prorata, 'Prorata temporis can be applied only for time method "number of depreciations".', ['prorata']),
306 def onchange_category_id(self, cr, uid, ids, category_id, context=None):
308 asset_categ_obj = self.pool.get('account.asset.category')
310 category_obj = asset_categ_obj.browse(cr, uid, category_id, context=context)
312 'method': category_obj.method,
313 'method_number': category_obj.method_number,
314 'method_time': category_obj.method_time,
315 'method_period': category_obj.method_period,
316 'method_progress_factor': category_obj.method_progress_factor,
317 'method_end': category_obj.method_end,
318 'prorata': category_obj.prorata,
322 def onchange_method_time(self, cr, uid, ids, method_time='number', context=None):
324 if method_time != 'number':
325 res['value'] = {'prorata': False}
328 def copy(self, cr, uid, id, default=None, context=None):
333 default.update({'depreciation_line_ids': [], 'account_move_line_ids': [], 'history_ids': [], 'state': 'draft'})
334 return super(account_asset_asset, self).copy(cr, uid, id, default, context=context)
336 def _compute_entries(self, cr, uid, ids, period_id, context=None):
338 period_obj = self.pool.get('account.period')
339 depreciation_obj = self.pool.get('account.asset.depreciation.line')
340 period = period_obj.browse(cr, uid, period_id, context=context)
341 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)
344 context.update({'depreciation_date':period.date_stop})
345 return depreciation_obj.create_move(cr, uid, depreciation_ids, context=context)
347 def create(self, cr, uid, vals, context=None):
348 asset_id = super(account_asset_asset, self).create(cr, uid, vals, context=context)
349 self.compute_depreciation_board(cr, uid, [asset_id], context=context)
352 def open_entries(self, cr, uid, ids, context=None):
355 context.update({'search_default_asset_id': ids, 'default_asset_id': ids})
358 'view_mode': 'tree,form',
359 'res_model': 'account.move.line',
361 'type': 'ir.actions.act_window',
365 account_asset_asset()
367 class account_asset_depreciation_line(osv.osv):
368 _name = 'account.asset.depreciation.line'
369 _description = 'Asset depreciation line'
371 def _get_move_check(self, cr, uid, ids, name, args, context=None):
373 for line in self.browse(cr, uid, ids, context=context):
374 res[line.id] = bool(line.move_id)
378 'name': fields.char('Depreciation Name', size=64, required=True, select=1),
379 'sequence': fields.integer('Sequence', required=True),
380 'asset_id': fields.many2one('account.asset.asset', 'Asset', required=True, ondelete='cascade'),
381 'parent_state': fields.related('asset_id', 'state', type='char', string='State of Asset'),
382 'amount': fields.float('Current Depreciation', digits_compute=dp.get_precision('Account'), required=True),
383 'remaining_value': fields.float('Next Period Depreciation', digits_compute=dp.get_precision('Account'),required=True),
384 'depreciated_value': fields.float('Amount Already Depreciated', required=True),
385 'depreciation_date': fields.date('Depreciation Date', select=1),
386 'move_id': fields.many2one('account.move', 'Depreciation Entry'),
387 'move_check': fields.function(_get_move_check, method=True, type='boolean', string='Posted', store=True)
390 def create_move(self, cr, uid, ids, context=None):
394 asset_obj = self.pool.get('account.asset.asset')
395 period_obj = self.pool.get('account.period')
396 move_obj = self.pool.get('account.move')
397 move_line_obj = self.pool.get('account.move.line')
398 currency_obj = self.pool.get('res.currency')
399 created_move_ids = []
401 for line in self.browse(cr, uid, ids, context=context):
402 depreciation_date = context.get('depreciation_date') or time.strftime('%Y-%m-%d')
403 ctx = dict(context, account_period_prefer_normal=True)
404 period_ids = period_obj.find(cr, uid, depreciation_date, context=ctx)
405 company_currency = line.asset_id.company_id.currency_id.id
406 current_currency = line.asset_id.currency_id.id
407 context.update({'date': depreciation_date})
408 amount = currency_obj.compute(cr, uid, current_currency, company_currency, line.amount, context=context)
409 sign = (line.asset_id.category_id.journal_id.type == 'purchase' and 1) or -1
410 asset_name = line.asset_id.name
411 reference = line.name
413 'date': depreciation_date,
414 'ref': "%s %s" %(line.asset_id.code or line.asset_id.name, line.name),
415 'period_id': period_ids and period_ids[0] or False,
416 'journal_id': line.asset_id.category_id.journal_id.id,
418 move_id = move_obj.create(cr, uid, move_vals, context=context)
419 journal_id = line.asset_id.category_id.journal_id.id
420 partner_id = line.asset_id.partner_id.id
421 move_line_obj.create(cr, uid, {
425 'account_id': line.asset_id.category_id.account_depreciation_id.id,
428 'period_id': period_ids and period_ids[0] or False,
429 'journal_id': journal_id,
430 'partner_id': partner_id,
431 'currency_id': company_currency != current_currency and current_currency or False,
432 'amount_currency': company_currency != current_currency and - sign * line.amount or 0.0,
433 'date': depreciation_date,
435 move_line_obj.create(cr, uid, {
439 'account_id': line.asset_id.category_id.account_expense_depreciation_id.id,
442 'period_id': period_ids and period_ids[0] or False,
443 'journal_id': journal_id,
444 'partner_id': partner_id,
445 'currency_id': company_currency != current_currency and current_currency or False,
446 'amount_currency': company_currency != current_currency and sign * line.amount or 0.0,
447 'analytic_account_id': line.asset_id.category_id.account_analytic_id.id,
448 'date': depreciation_date,
449 'asset_id': line.asset_id.id
451 self.write(cr, uid, line.id, {'move_id': move_id}, context=context)
452 created_move_ids.append(move_id)
453 asset_ids.append(line.asset_id.id)
454 # we re-evaluate the assets to determine whether we can close them
455 for asset in asset_obj.browse(cr, uid, list(set(asset_ids)), context=context):
456 if currency_obj.is_zero(cr, uid, asset.currency_id, asset.value_residual):
457 asset.write({'state': 'close'})
458 return created_move_ids
460 account_asset_depreciation_line()
462 class account_move_line(osv.osv):
463 _inherit = 'account.move.line'
465 'asset_id': fields.many2one('account.asset.asset', 'Asset', ondelete="restrict"),
466 'entry_ids': fields.one2many('account.move.line', 'asset_id', 'Entries', readonly=True, states={'draft':[('readonly',False)]}),
471 class account_asset_history(osv.osv):
472 _name = 'account.asset.history'
473 _description = 'Asset history'
475 'name': fields.char('History name', size=64, select=1),
476 'user_id': fields.many2one('res.users', 'User', required=True),
477 'date': fields.date('Date', required=True),
478 'asset_id': fields.many2one('account.asset.asset', 'Asset', required=True),
479 'method_time': fields.selection([('number','Number of Depreciations'),('end','Ending Date')], 'Time Method', required=True,
480 help="The method to use to compute the dates and number of depreciation lines.\n"\
481 "Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" \
482 "Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."),
483 'method_number': fields.integer('Number of Depreciations', help="The number of depreciations needed to depreciate your asset"),
484 'method_period': fields.integer('Period Length', help="Time in month between two depreciations"),
485 'method_end': fields.date('Ending date'),
486 'note': fields.text('Note'),
490 'date': lambda *args: time.strftime('%Y-%m-%d'),
491 'user_id': lambda self, cr, uid, ctx: uid
494 account_asset_history()
496 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: