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 openerp.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}
74 class account_asset_asset(osv.osv):
75 _name = 'account.asset.asset'
76 _description = 'Asset'
78 def unlink(self, cr, uid, ids, context=None):
79 for asset in self.browse(cr, uid, ids, context=context):
80 if asset.account_move_line_ids:
81 raise osv.except_osv(_('Error!'), _('You cannot delete an asset that contains posted depreciation lines.'))
82 return super(account_asset_asset, self).unlink(cr, uid, ids, context=context)
84 def _get_period(self, cr, uid, context=None):
85 periods = self.pool.get('account.period').find(cr, uid, context=context)
91 def _get_last_depreciation_date(self, cr, uid, ids, context=None):
93 @param id: ids of a account.asset.asset objects
94 @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
97 SELECT a.id as id, COALESCE(MAX(l.date),a.purchase_date) AS date
98 FROM account_asset_asset a
99 LEFT JOIN account_move_line l ON (l.asset_id = a.id)
101 GROUP BY a.id, a.purchase_date """, (tuple(ids),))
102 return dict(cr.fetchall())
104 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):
105 #by default amount = 0
107 if i == undone_dotation_number:
108 amount = residual_amount
110 if asset.method == 'linear':
111 amount = amount_to_depr / (undone_dotation_number - len(posted_depreciation_line_ids))
113 amount = amount_to_depr / asset.method_number
114 days = total_days - float(depreciation_date.strftime('%j'))
116 amount = (amount_to_depr / asset.method_number) / total_days * days
117 elif i == undone_dotation_number:
118 amount = (amount_to_depr / asset.method_number) / total_days * (total_days - days)
119 elif asset.method == 'degressive':
120 amount = residual_amount * asset.method_progress_factor
122 days = total_days - float(depreciation_date.strftime('%j'))
124 amount = (residual_amount * asset.method_progress_factor) / total_days * days
125 elif i == undone_dotation_number:
126 amount = (residual_amount * asset.method_progress_factor) / total_days * (total_days - days)
129 def _compute_board_undone_dotation_nb(self, cr, uid, asset, depreciation_date, total_days, context=None):
130 undone_dotation_number = asset.method_number
131 if asset.method_time == 'end':
132 end_date = datetime.strptime(asset.method_end, '%Y-%m-%d')
133 undone_dotation_number = 0
134 while depreciation_date <= end_date:
135 depreciation_date = (datetime(depreciation_date.year, depreciation_date.month, depreciation_date.day) + relativedelta(months=+asset.method_period))
136 undone_dotation_number += 1
138 undone_dotation_number += 1
139 return undone_dotation_number
141 def compute_depreciation_board(self, cr, uid, ids, context=None):
142 depreciation_lin_obj = self.pool.get('account.asset.depreciation.line')
143 currency_obj = self.pool.get('res.currency')
144 for asset in self.browse(cr, uid, ids, context=context):
145 if asset.value_residual == 0.0:
147 posted_depreciation_line_ids = depreciation_lin_obj.search(cr, uid, [('asset_id', '=', asset.id), ('move_check', '=', True)],order='depreciation_date desc')
148 old_depreciation_line_ids = depreciation_lin_obj.search(cr, uid, [('asset_id', '=', asset.id), ('move_id', '=', False)])
149 if old_depreciation_line_ids:
150 depreciation_lin_obj.unlink(cr, uid, old_depreciation_line_ids, context=context)
152 amount_to_depr = residual_amount = asset.value_residual
154 depreciation_date = datetime.strptime(self._get_last_depreciation_date(cr, uid, [asset.id], context)[asset.id], '%Y-%m-%d')
156 # depreciation_date = 1st January of purchase year
157 purchase_date = datetime.strptime(asset.purchase_date, '%Y-%m-%d')
158 #if we already have some previous validated entries, starting date isn't 1st January but last entry + method period
159 if (len(posted_depreciation_line_ids)>0):
160 last_depreciation_date = datetime.strptime(depreciation_lin_obj.browse(cr,uid,posted_depreciation_line_ids[0],context=context).depreciation_date, '%Y-%m-%d')
161 depreciation_date = (last_depreciation_date+relativedelta(months=+asset.method_period))
163 depreciation_date = datetime(purchase_date.year, 1, 1)
164 day = depreciation_date.day
165 month = depreciation_date.month
166 year = depreciation_date.year
167 total_days = (year % 4) and 365 or 366
169 undone_dotation_number = self._compute_board_undone_dotation_nb(cr, uid, asset, depreciation_date, total_days, context=context)
170 for x in range(len(posted_depreciation_line_ids), undone_dotation_number):
172 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)
173 company_currency = asset.company_id.currency_id.id
174 current_currency = asset.currency_id.id
175 # compute amount into company currency
176 amount = currency_obj.compute(cr, uid, current_currency, company_currency, amount, context=context)
177 residual_amount -= amount
180 'asset_id': asset.id,
182 'name': str(asset.id) +'/' + str(i),
183 'remaining_value': residual_amount,
184 'depreciated_value': (asset.purchase_value - asset.salvage_value) - (residual_amount + amount),
185 'depreciation_date': depreciation_date.strftime('%Y-%m-%d'),
187 depreciation_lin_obj.create(cr, uid, vals, context=context)
188 # Considering Depr. Period as months
189 depreciation_date = (datetime(year, month, day) + relativedelta(months=+asset.method_period))
190 day = depreciation_date.day
191 month = depreciation_date.month
192 year = depreciation_date.year
195 def validate(self, cr, uid, ids, context=None):
198 return self.write(cr, uid, ids, {
202 def set_to_close(self, cr, uid, ids, context=None):
203 return self.write(cr, uid, ids, {'state': 'close'}, context=context)
205 def set_to_draft(self, cr, uid, ids, context=None):
206 return self.write(cr, uid, ids, {'state': 'draft'}, context=context)
208 def _amount_residual(self, cr, uid, ids, name, args, context=None):
210 l.asset_id as id, SUM(abs(l.debit-l.credit)) AS amount
214 l.asset_id IN %s GROUP BY l.asset_id """, (tuple(ids),))
215 res=dict(cr.fetchall())
216 for asset in self.browse(cr, uid, ids, context):
217 res[asset.id] = asset.purchase_value - res.get(asset.id, 0.0) - asset.salvage_value
219 res.setdefault(id, 0.0)
222 def onchange_company_id(self, cr, uid, ids, company_id=False, context=None):
225 company = self.pool.get('res.company').browse(cr, uid, company_id, context=context)
226 if company.currency_id.company_id and company.currency_id.company_id.id != company_id:
227 val['currency_id'] = False
229 val['currency_id'] = company.currency_id.id
230 return {'value': val}
232 def onchange_purchase_salvage_value(self, cr, uid, ids, purchase_value, salvage_value, context=None):
234 for asset in self.browse(cr, uid, ids, context=context):
236 val['value_residual'] = purchase_value - salvage_value
238 val['value_residual'] = purchase_value - salvage_value
239 return {'value': val}
242 'account_move_line_ids': fields.one2many('account.move.line', 'asset_id', 'Entries', readonly=True, states={'draft':[('readonly',False)]}),
243 'name': fields.char('Asset Name', size=64, required=True, readonly=True, states={'draft':[('readonly',False)]}),
244 'code': fields.char('Reference', size=32, readonly=True, states={'draft':[('readonly',False)]}),
245 'purchase_value': fields.float('Gross Value', required=True, readonly=True, states={'draft':[('readonly',False)]}),
246 'currency_id': fields.many2one('res.currency','Currency',required=True, readonly=True, states={'draft':[('readonly',False)]}),
247 'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True, states={'draft':[('readonly',False)]}),
248 'note': fields.text('Note'),
249 'category_id': fields.many2one('account.asset.category', 'Asset Category', required=True, change_default=True, readonly=True, states={'draft':[('readonly',False)]}),
250 'parent_id': fields.many2one('account.asset.asset', 'Parent Asset', readonly=True, states={'draft':[('readonly',False)]}),
251 'child_ids': fields.one2many('account.asset.asset', 'parent_id', 'Children Assets'),
252 'purchase_date': fields.date('Purchase Date', required=True, readonly=True, states={'draft':[('readonly',False)]}),
253 'state': fields.selection([('draft','Draft'),('open','Running'),('close','Close')], 'Status', required=True,
254 help="When an asset is created, the status is 'Draft'.\n" \
255 "If the asset is confirmed, the status goes in 'Running' and the depreciation lines can be posted in the accounting.\n" \
256 "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."),
257 'active': fields.boolean('Active'),
258 'partner_id': fields.many2one('res.partner', 'Partner', readonly=True, states={'draft':[('readonly',False)]}),
259 '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"\
260 " * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n" \
261 " * Degressive: Calculated on basis of: Residual Value * Degressive Factor"),
262 'method_number': fields.integer('Number of Depreciations', readonly=True, states={'draft':[('readonly',False)]}, help="The number of depreciations needed to depreciate your asset"),
263 '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"),
264 'method_end': fields.date('Ending Date', readonly=True, states={'draft':[('readonly',False)]}),
265 'method_progress_factor': fields.float('Degressive Factor', readonly=True, states={'draft':[('readonly',False)]}),
266 'value_residual': fields.function(_amount_residual, method=True, digits_compute=dp.get_precision('Account'), string='Residual Value'),
267 'method_time': fields.selection([('number','Number of Depreciations'),('end','Ending Date')], 'Time Method', required=True, readonly=True, states={'draft':[('readonly',False)]},
268 help="Choose the method to use to compute the dates and number of depreciation lines.\n"\
269 " * Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" \
270 " * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."),
271 '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'),
272 'history_ids': fields.one2many('account.asset.history', 'asset_id', 'History', readonly=True),
273 'depreciation_line_ids': fields.one2many('account.asset.depreciation.line', 'asset_id', 'Depreciation Lines', readonly=True, states={'draft':[('readonly',False)],'open':[('readonly',False)]}),
274 '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)]}),
277 'code': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'account.asset.code'),
278 'purchase_date': lambda obj, cr, uid, context: time.strftime('%Y-%m-%d'),
283 'method_time': 'number',
285 'method_progress_factor': 0.3,
286 'currency_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.currency_id.id,
287 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'account.asset.asset',context=context),
290 def _check_recursion(self, cr, uid, ids, context=None, parent=None):
291 return super(account_asset_asset, self)._check_recursion(cr, uid, ids, context=context, parent=parent)
293 def _check_prorata(self, cr, uid, ids, context=None):
294 for asset in self.browse(cr, uid, ids, context=context):
295 if asset.prorata and asset.method_time != 'number':
300 (_check_recursion, 'Error ! You cannot create recursive assets.', ['parent_id']),
301 (_check_prorata, 'Prorata temporis can be applied only for time method "number of depreciations".', ['prorata']),
304 def onchange_category_id(self, cr, uid, ids, category_id, context=None):
306 asset_categ_obj = self.pool.get('account.asset.category')
308 category_obj = asset_categ_obj.browse(cr, uid, category_id, context=context)
310 'method': category_obj.method,
311 'method_number': category_obj.method_number,
312 'method_time': category_obj.method_time,
313 'method_period': category_obj.method_period,
314 'method_progress_factor': category_obj.method_progress_factor,
315 'method_end': category_obj.method_end,
316 'prorata': category_obj.prorata,
320 def onchange_method_time(self, cr, uid, ids, method_time='number', context=None):
322 if method_time != 'number':
323 res['value'] = {'prorata': False}
326 def copy(self, cr, uid, id, default=None, context=None):
331 default.update({'depreciation_line_ids': [], 'state': 'draft'})
332 return super(account_asset_asset, self).copy(cr, uid, id, default, context=context)
334 def _compute_entries(self, cr, uid, ids, period_id, context=None):
336 period_obj = self.pool.get('account.period')
337 depreciation_obj = self.pool.get('account.asset.depreciation.line')
338 period = period_obj.browse(cr, uid, period_id, context=context)
339 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)
342 context.update({'depreciation_date':period.date_stop})
343 return depreciation_obj.create_move(cr, uid, depreciation_ids, context=context)
345 def create(self, cr, uid, vals, context=None):
346 asset_id = super(account_asset_asset, self).create(cr, uid, vals, context=context)
347 self.compute_depreciation_board(cr, uid, [asset_id], context=context)
350 def open_entries(self, cr, uid, ids, context=None):
353 context.update({'search_default_asset_id': ids, 'default_asset_id': ids})
355 'name': _('Journal Items'),
357 'view_mode': 'tree,form',
358 'res_model': 'account.move.line',
360 'type': 'ir.actions.act_window',
365 class account_asset_depreciation_line(osv.osv):
366 _name = 'account.asset.depreciation.line'
367 _description = 'Asset depreciation line'
369 def _get_move_check(self, cr, uid, ids, name, args, context=None):
371 for line in self.browse(cr, uid, ids, context=context):
372 res[line.id] = bool(line.move_id)
376 'name': fields.char('Depreciation Name', size=64, required=True, select=1),
377 'sequence': fields.integer('Sequence', required=True),
378 'asset_id': fields.many2one('account.asset.asset', 'Asset', required=True, ondelete='cascade'),
379 'parent_state': fields.related('asset_id', 'state', type='char', string='State of Asset'),
380 'amount': fields.float('Current Depreciation', digits_compute=dp.get_precision('Account'), required=True),
381 'remaining_value': fields.float('Next Period Depreciation', digits_compute=dp.get_precision('Account'),required=True),
382 'depreciated_value': fields.float('Amount Already Depreciated', required=True),
383 'depreciation_date': fields.date('Depreciation Date', select=1),
384 'move_id': fields.many2one('account.move', 'Depreciation Entry'),
385 'move_check': fields.function(_get_move_check, method=True, type='boolean', string='Posted', store=True)
388 def create_move(self, cr, uid, ids, context=None):
392 asset_obj = self.pool.get('account.asset.asset')
393 period_obj = self.pool.get('account.period')
394 move_obj = self.pool.get('account.move')
395 move_line_obj = self.pool.get('account.move.line')
396 currency_obj = self.pool.get('res.currency')
397 created_move_ids = []
399 for line in self.browse(cr, uid, ids, context=context):
400 depreciation_date = context.get('depreciation_date') or time.strftime('%Y-%m-%d')
401 period_ids = period_obj.find(cr, uid, depreciation_date, context=context)
402 company_currency = line.asset_id.company_id.currency_id.id
403 current_currency = line.asset_id.currency_id.id
404 context.update({'date': depreciation_date})
405 amount = currency_obj.compute(cr, uid, current_currency, company_currency, line.amount, context=context)
406 sign = (line.asset_id.category_id.journal_id.type == 'purchase' and 1) or -1
407 asset_name = line.asset_id.name
408 reference = line.name
411 'date': depreciation_date,
413 'period_id': period_ids and period_ids[0] or False,
414 'journal_id': line.asset_id.category_id.journal_id.id,
416 move_id = move_obj.create(cr, uid, move_vals, context=context)
417 journal_id = line.asset_id.category_id.journal_id.id
418 partner_id = line.asset_id.partner_id.id
419 move_line_obj.create(cr, uid, {
423 'account_id': line.asset_id.category_id.account_depreciation_id.id,
426 'period_id': period_ids and period_ids[0] or False,
427 'journal_id': journal_id,
428 'partner_id': partner_id,
429 'currency_id': company_currency != current_currency and current_currency or False,
430 'amount_currency': company_currency != current_currency and - sign * line.amount or 0.0,
431 'date': depreciation_date,
433 move_line_obj.create(cr, uid, {
437 'account_id': line.asset_id.category_id.account_expense_depreciation_id.id,
440 'period_id': period_ids and period_ids[0] or False,
441 'journal_id': journal_id,
442 'partner_id': partner_id,
443 'currency_id': company_currency != current_currency and current_currency or False,
444 'amount_currency': company_currency != current_currency and sign * line.amount or 0.0,
445 'analytic_account_id': line.asset_id.category_id.account_analytic_id.id,
446 'date': depreciation_date,
447 'asset_id': line.asset_id.id
449 self.write(cr, uid, line.id, {'move_id': move_id}, context=context)
450 created_move_ids.append(move_id)
451 asset_ids.append(line.asset_id.id)
452 # we re-evaluate the assets to determine whether we can close them
453 for asset in asset_obj.browse(cr, uid, list(set(asset_ids)), context=context):
454 if currency_obj.is_zero(cr, uid, asset.currency_id, asset.value_residual):
455 asset.write({'state': 'close'})
456 return created_move_ids
459 class account_move_line(osv.osv):
460 _inherit = 'account.move.line'
462 'asset_id': fields.many2one('account.asset.asset', 'Asset', ondelete="restrict"),
463 'entry_ids': fields.one2many('account.move.line', 'asset_id', 'Entries', readonly=True, states={'draft':[('readonly',False)]}),
467 class account_asset_history(osv.osv):
468 _name = 'account.asset.history'
469 _description = 'Asset history'
471 'name': fields.char('History name', size=64, select=1),
472 'user_id': fields.many2one('res.users', 'User', required=True),
473 'date': fields.date('Date', required=True),
474 'asset_id': fields.many2one('account.asset.asset', 'Asset', required=True),
475 'method_time': fields.selection([('number','Number of Depreciations'),('end','Ending Date')], 'Time Method', required=True,
476 help="The method to use to compute the dates and number of depreciation lines.\n"\
477 "Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" \
478 "Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."),
479 'method_number': fields.integer('Number of Depreciations', help="The number of depreciations needed to depreciate your asset"),
480 'method_period': fields.integer('Period Length', help="Time in month between two depreciations"),
481 'method_end': fields.date('Ending date'),
482 'note': fields.text('Note'),
486 'date': lambda *args: time.strftime('%Y-%m-%d'),
487 'user_id': lambda self, cr, uid, ctx: uid
491 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: