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', 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: Computed on basis of Gross Value / Number of Depreciations'),('degressive','Degressive: Computed on basis of Residual Value * Degressive Factor')], '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}
240 def _entry_count(self, cr, uid, ids, field_name, arg, context=None):
241 MoveLine = self.pool('account.move.line')
243 asset_id: MoveLine.search_count(cr, uid, [('asset_id', '=', asset_id)], context=context)
247 'account_move_line_ids': fields.one2many('account.move.line', 'asset_id', 'Entries', readonly=True, states={'draft':[('readonly',False)]}),
248 'entry_count': fields.function(_entry_count, string='# Asset Entries', type='integer'),
249 'name': fields.char('Asset Name', required=True, readonly=True, states={'draft':[('readonly',False)]}),
250 'code': fields.char('Reference', size=32, readonly=True, states={'draft':[('readonly',False)]}),
251 'purchase_value': fields.float('Gross Value', required=True, readonly=True, states={'draft':[('readonly',False)]}),
252 'currency_id': fields.many2one('res.currency','Currency',required=True, readonly=True, states={'draft':[('readonly',False)]}),
253 'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True, states={'draft':[('readonly',False)]}),
254 'note': fields.text('Note'),
255 'category_id': fields.many2one('account.asset.category', 'Asset Category', required=True, change_default=True, readonly=True, states={'draft':[('readonly',False)]}),
256 'parent_id': fields.many2one('account.asset.asset', 'Parent Asset', readonly=True, states={'draft':[('readonly',False)]}),
257 'child_ids': fields.one2many('account.asset.asset', 'parent_id', 'Children Assets', copy=True),
258 'purchase_date': fields.date('Purchase Date', required=True, readonly=True, states={'draft':[('readonly',False)]}),
259 'state': fields.selection([('draft','Draft'),('open','Running'),('close','Close')], 'Status', required=True, copy=False,
260 help="When an asset is created, the status is 'Draft'.\n" \
261 "If the asset is confirmed, the status goes in 'Running' and the depreciation lines can be posted in the accounting.\n" \
262 "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."),
263 'active': fields.boolean('Active'),
264 'partner_id': fields.many2one('res.partner', 'Partner', readonly=True, states={'draft':[('readonly',False)]}),
265 'method': fields.selection([('linear','Linear: Computed on basis of Gross Value / Number of Depreciations'),('degressive','Degressive: Computed on basis of Residual Value * Degressive Factor')], 'Computation Method', required=True, readonly=True, states={'draft':[('readonly',False)]}, help="Choose the method to use to compute the amount of depreciation lines.\n"\
266 " * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n" \
267 " * Degressive: Calculated on basis of: Residual Value * Degressive Factor"),
268 'method_number': fields.integer('Number of Depreciations', readonly=True, states={'draft':[('readonly',False)]}, help="The number of depreciations needed to depreciate your asset"),
269 '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"),
270 'method_end': fields.date('Ending Date', readonly=True, states={'draft':[('readonly',False)]}),
271 'method_progress_factor': fields.float('Degressive Factor', readonly=True, states={'draft':[('readonly',False)]}),
272 'value_residual': fields.function(_amount_residual, method=True, digits_compute=dp.get_precision('Account'), string='Residual Value'),
273 'method_time': fields.selection([('number','Number of Depreciations'),('end','Ending Date')], 'Time Method', required=True, readonly=True, states={'draft':[('readonly',False)]},
274 help="Choose the method to use to compute the dates and number of depreciation lines.\n"\
275 " * Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" \
276 " * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."),
277 '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'),
278 'history_ids': fields.one2many('account.asset.history', 'asset_id', 'History', readonly=True),
279 'depreciation_line_ids': fields.one2many('account.asset.depreciation.line', 'asset_id', 'Depreciation Lines', readonly=True, states={'draft':[('readonly',False)],'open':[('readonly',False)]}),
280 '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)]}),
283 'code': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').next_by_code(cr, uid, 'account.asset.code'),
284 'purchase_date': lambda obj, cr, uid, context: time.strftime('%Y-%m-%d'),
289 'method_time': 'number',
291 'method_progress_factor': 0.3,
292 'currency_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.currency_id.id,
293 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'account.asset.asset',context=context),
296 def _check_recursion(self, cr, uid, ids, context=None, parent=None):
297 return super(account_asset_asset, self)._check_recursion(cr, uid, ids, context=context, parent=parent)
299 def _check_prorata(self, cr, uid, ids, context=None):
300 for asset in self.browse(cr, uid, ids, context=context):
301 if asset.prorata and asset.method_time != 'number':
306 (_check_recursion, 'Error ! You cannot create recursive assets.', ['parent_id']),
307 (_check_prorata, 'Prorata temporis can be applied only for time method "number of depreciations".', ['prorata']),
310 def onchange_category_id(self, cr, uid, ids, category_id, context=None):
312 asset_categ_obj = self.pool.get('account.asset.category')
314 category_obj = asset_categ_obj.browse(cr, uid, category_id, context=context)
316 'method': category_obj.method,
317 'method_number': category_obj.method_number,
318 'method_time': category_obj.method_time,
319 'method_period': category_obj.method_period,
320 'method_progress_factor': category_obj.method_progress_factor,
321 'method_end': category_obj.method_end,
322 'prorata': category_obj.prorata,
326 def onchange_method_time(self, cr, uid, ids, method_time='number', context=None):
328 if method_time != 'number':
329 res['value'] = {'prorata': False}
332 def _compute_entries(self, cr, uid, ids, period_id, context=None):
334 period_obj = self.pool.get('account.period')
335 depreciation_obj = self.pool.get('account.asset.depreciation.line')
336 period = period_obj.browse(cr, uid, period_id, context=context)
337 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)
338 context = dict(context or {}, depreciation_date=period.date_stop)
339 return depreciation_obj.create_move(cr, uid, depreciation_ids, context=context)
341 def create(self, cr, uid, vals, context=None):
342 asset_id = super(account_asset_asset, self).create(cr, uid, vals, context=context)
343 self.compute_depreciation_board(cr, uid, [asset_id], context=context)
346 def open_entries(self, cr, uid, ids, context=None):
347 context = dict(context or {}, search_default_asset_id=ids, default_asset_id=ids)
349 'name': _('Journal Items'),
351 'view_mode': 'tree,form',
352 'res_model': 'account.move.line',
354 'type': 'ir.actions.act_window',
359 class account_asset_depreciation_line(osv.osv):
360 _name = 'account.asset.depreciation.line'
361 _description = 'Asset depreciation line'
363 def _get_move_check(self, cr, uid, ids, name, args, context=None):
365 for line in self.browse(cr, uid, ids, context=context):
366 res[line.id] = bool(line.move_id)
370 'name': fields.char('Depreciation Name', required=True, select=1),
371 'sequence': fields.integer('Sequence', required=True),
372 'asset_id': fields.many2one('account.asset.asset', 'Asset', required=True, ondelete='cascade'),
373 'parent_state': fields.related('asset_id', 'state', type='char', string='State of Asset'),
374 'amount': fields.float('Current Depreciation', digits_compute=dp.get_precision('Account'), required=True),
375 'remaining_value': fields.float('Next Period Depreciation', digits_compute=dp.get_precision('Account'),required=True),
376 'depreciated_value': fields.float('Amount Already Depreciated', required=True),
377 'depreciation_date': fields.date('Depreciation Date', select=1),
378 'move_id': fields.many2one('account.move', 'Depreciation Entry'),
379 'move_check': fields.function(_get_move_check, method=True, type='boolean', string='Posted', store=True)
382 def create_move(self, cr, uid, ids, context=None):
383 context = dict(context or {})
385 asset_obj = self.pool.get('account.asset.asset')
386 period_obj = self.pool.get('account.period')
387 move_obj = self.pool.get('account.move')
388 move_line_obj = self.pool.get('account.move.line')
389 currency_obj = self.pool.get('res.currency')
390 created_move_ids = []
392 for line in self.browse(cr, uid, ids, context=context):
393 depreciation_date = context.get('depreciation_date') or line.depreciation_date or time.strftime('%Y-%m-%d')
394 period_ids = period_obj.find(cr, uid, depreciation_date, context=context)
395 company_currency = line.asset_id.company_id.currency_id.id
396 current_currency = line.asset_id.currency_id.id
397 context.update({'date': depreciation_date})
398 amount = currency_obj.compute(cr, uid, current_currency, company_currency, line.amount, context=context)
399 sign = (line.asset_id.category_id.journal_id.type == 'purchase' and 1) or -1
400 asset_name = line.asset_id.name
401 reference = line.name
404 'date': depreciation_date,
406 'period_id': period_ids and period_ids[0] or False,
407 'journal_id': line.asset_id.category_id.journal_id.id,
409 move_id = move_obj.create(cr, uid, move_vals, context=context)
410 journal_id = line.asset_id.category_id.journal_id.id
411 partner_id = line.asset_id.partner_id.id
412 move_line_obj.create(cr, uid, {
416 'account_id': line.asset_id.category_id.account_depreciation_id.id,
419 'period_id': period_ids and period_ids[0] or False,
420 'journal_id': journal_id,
421 'partner_id': partner_id,
422 'currency_id': company_currency != current_currency and current_currency or False,
423 'amount_currency': company_currency != current_currency and - sign * line.amount or 0.0,
424 'date': depreciation_date,
426 move_line_obj.create(cr, uid, {
430 'account_id': line.asset_id.category_id.account_expense_depreciation_id.id,
433 'period_id': period_ids and period_ids[0] or False,
434 'journal_id': journal_id,
435 'partner_id': partner_id,
436 'currency_id': company_currency != current_currency and current_currency or False,
437 'amount_currency': company_currency != current_currency and sign * line.amount or 0.0,
438 'analytic_account_id': line.asset_id.category_id.account_analytic_id.id,
439 'date': depreciation_date,
440 'asset_id': line.asset_id.id
442 self.write(cr, uid, line.id, {'move_id': move_id}, context=context)
443 created_move_ids.append(move_id)
444 asset_ids.append(line.asset_id.id)
445 # we re-evaluate the assets to determine whether we can close them
446 for asset in asset_obj.browse(cr, uid, list(set(asset_ids)), context=context):
447 if currency_obj.is_zero(cr, uid, asset.currency_id, asset.value_residual):
448 asset.write({'state': 'close'})
449 return created_move_ids
452 class account_move_line(osv.osv):
453 _inherit = 'account.move.line'
455 'asset_id': fields.many2one('account.asset.asset', 'Asset', ondelete="restrict"),
458 class account_asset_history(osv.osv):
459 _name = 'account.asset.history'
460 _description = 'Asset history'
462 'name': fields.char('History name', select=1),
463 'user_id': fields.many2one('res.users', 'User', required=True),
464 'date': fields.date('Date', required=True),
465 'asset_id': fields.many2one('account.asset.asset', 'Asset', required=True),
466 'method_time': fields.selection([('number','Number of Depreciations'),('end','Ending Date')], 'Time Method', required=True,
467 help="The method to use to compute the dates and number of depreciation lines.\n"\
468 "Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" \
469 "Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."),
470 'method_number': fields.integer('Number of Depreciations', help="The number of depreciations needed to depreciate your asset"),
471 'method_period': fields.integer('Period Length', help="Time in month between two depreciations"),
472 'method_end': fields.date('Ending date'),
473 'note': fields.text('Note'),
477 'date': lambda *args: time.strftime('%Y-%m-%d'),
478 'user_id': lambda self, cr, uid, ctx: uid
482 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: