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 osv import osv, fields
27 import decimal_precision as dp
29 class account_asset_category(osv.osv):
30 _name = 'account.asset.category'
31 _description = 'Asset category'
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: Residual Value * Degressive Factor"),
45 'method_number': fields.integer('Number of Depreciations', help="The number of depreciations needed to depreciate your asset"),
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."),
58 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'account.asset.category', context=context),
61 'method_time': 'number',
63 'method_progress_factor': 0.3,
66 def onchange_account_asset(self, cr, uid, ids, account_asset_id, context=None):
69 res['value'] = {'account_depreciation_id': account_asset_id}
72 account_asset_category()
74 class account_asset_asset(osv.osv):
75 _name = 'account.asset.asset'
76 _description = 'Asset'
78 def _get_period(self, cr, uid, context=None):
79 periods = self.pool.get('account.period').find(cr, uid)
85 def _get_last_depreciation_date(self, cr, uid, ids, context=None):
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
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)
95 GROUP BY a.id, a.purchase_date """, (tuple(ids),))
96 return dict(cr.fetchall())
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
101 if i == undone_dotation_number:
102 amount = residual_amount
104 if asset.method == 'linear':
105 amount = amount_to_depr / (undone_dotation_number - len(posted_depreciation_line_ids))
107 amount = amount_to_depr / asset.method_number
108 days = total_days - float(depreciation_date.strftime('%j'))
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
116 days = total_days - float(depreciation_date.strftime('%j'))
118 amount = (residual_amount * asset.method_progress_factor) / total_days * days
119 elif i == undone_dotation_number:
120 amount = (residual_amount * asset.method_progress_factor) / total_days * (total_days - days)
123 def _compute_board_undone_dotation_nb(self, cr, uid, asset, depreciation_date, total_days, context=None):
124 undone_dotation_number = asset.method_number
125 if asset.method_time == 'end':
126 end_date = datetime.strptime(asset.method_end, '%Y-%m-%d')
127 undone_dotation_number = 0
128 while depreciation_date <= end_date:
129 depreciation_date = (datetime(depreciation_date.year, depreciation_date.month, depreciation_date.day) + relativedelta(months=+asset.method_period))
130 undone_dotation_number += 1
132 undone_dotation_number += 1
133 return undone_dotation_number
135 def compute_depreciation_board(self, cr, uid, ids, context=None):
136 depreciation_lin_obj = self.pool.get('account.asset.depreciation.line')
137 currency_obj = self.pool.get('res.currency')
138 for asset in self.browse(cr, uid, ids, context=context):
139 if asset.value_residual == 0.0:
141 posted_depreciation_line_ids = depreciation_lin_obj.search(cr, uid, [('asset_id', '=', asset.id), ('move_check', '=', True)],order='depreciation_date desc')
142 old_depreciation_line_ids = depreciation_lin_obj.search(cr, uid, [('asset_id', '=', asset.id), ('move_id', '=', False)])
143 if old_depreciation_line_ids:
144 depreciation_lin_obj.unlink(cr, uid, old_depreciation_line_ids, context=context)
146 amount_to_depr = residual_amount = asset.value_residual
148 depreciation_date = datetime.strptime(self._get_last_depreciation_date(cr, uid, [asset.id], context)[asset.id], '%Y-%m-%d')
150 # depreciation_date = 1st January of purchase year
151 purchase_date = datetime.strptime(asset.purchase_date, '%Y-%m-%d')
152 #if we already have some previous validated entries, starting date isn't 1st January but last entry + method period
153 if (len(posted_depreciation_line_ids)>0):
154 last_depreciation_date = datetime.strptime(depreciation_lin_obj.browse(cr,uid,posted_depreciation_line_ids[0],context=context).depreciation_date, '%Y-%m-%d')
155 depreciation_date = (last_depreciation_date+relativedelta(months=+asset.method_period))
157 depreciation_date = datetime(purchase_date.year, 1, 1)
158 day = depreciation_date.day
159 month = depreciation_date.month
160 year = depreciation_date.year
161 total_days = (year % 4) and 365 or 366
163 undone_dotation_number = self._compute_board_undone_dotation_nb(cr, uid, asset, depreciation_date, total_days, context=context)
164 for x in range(len(posted_depreciation_line_ids), undone_dotation_number):
166 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)
167 company_currency = asset.company_id.currency_id.id
168 current_currency = asset.currency_id.id
169 # compute amount into company currency
170 amount = currency_obj.compute(cr, uid, current_currency, company_currency, amount, context=context)
171 residual_amount -= amount
174 'asset_id': asset.id,
176 'name': str(asset.id) +'/' + str(i),
177 'remaining_value': residual_amount,
178 'depreciated_value': (asset.purchase_value - asset.salvage_value) - (residual_amount + amount),
179 'depreciation_date': depreciation_date.strftime('%Y-%m-%d'),
181 depreciation_lin_obj.create(cr, uid, vals, context=context)
182 # Considering Depr. Period as months
183 depreciation_date = (datetime(year, month, day) + relativedelta(months=+asset.method_period))
184 day = depreciation_date.day
185 month = depreciation_date.month
186 year = depreciation_date.year
189 def validate(self, cr, uid, ids, context=None):
192 return self.write(cr, uid, ids, {
196 def set_to_close(self, cr, uid, ids, context=None):
197 return self.write(cr, uid, ids, {'state': 'close'}, context=context)
199 def set_to_draft(self, cr, uid, ids, context=None):
200 return self.write(cr, uid, ids, {'state': 'draft'}, context=context)
202 def _amount_residual(self, cr, uid, ids, name, args, context=None):
204 l.asset_id as id, SUM(abs(l.debit-l.credit)) AS amount
208 l.asset_id IN %s GROUP BY l.asset_id """, (tuple(ids),))
209 res=dict(cr.fetchall())
210 for asset in self.browse(cr, uid, ids, context):
211 res[asset.id] = asset.purchase_value - res.get(asset.id, 0.0) - asset.salvage_value
213 res.setdefault(id, 0.0)
216 def onchange_company_id(self, cr, uid, ids, company_id=False, context=None):
219 company = self.pool.get('res.company').browse(cr, uid, company_id, context=context)
220 if company.currency_id.company_id and company.currency_id.company_id.id != company_id:
221 val['currency_id'] = False
223 val['currency_id'] = company.currency_id.id
224 return {'value': val}
226 def onchange_purchase_salvage_value(self, cr, uid, ids, purchase_value, salvage_value, context=None):
228 for asset in self.browse(cr, uid, ids, context=context):
230 val['value_residual'] = purchase_value - salvage_value
232 val['value_residual'] = purchase_value - salvage_value
233 return {'value': val}
236 'account_move_line_ids': fields.one2many('account.move.line', 'asset_id', 'Entries', readonly=True, states={'draft':[('readonly',False)]}),
237 'name': fields.char('Asset Name', size=64, required=True, readonly=True, states={'draft':[('readonly',False)]}),
238 'code': fields.char('Reference', size=32, readonly=True, states={'draft':[('readonly',False)]}),
239 'purchase_value': fields.float('Gross Value', required=True, readonly=True, states={'draft':[('readonly',False)]}),
240 'currency_id': fields.many2one('res.currency','Currency',required=True, readonly=True, states={'draft':[('readonly',False)]}),
241 'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True, states={'draft':[('readonly',False)]}),
242 'note': fields.text('Note'),
243 'category_id': fields.many2one('account.asset.category', 'Asset Category', required=True, change_default=True, readonly=True, states={'draft':[('readonly',False)]}),
244 'parent_id': fields.many2one('account.asset.asset', 'Parent Asset', readonly=True, states={'draft':[('readonly',False)]}),
245 'child_ids': fields.one2many('account.asset.asset', 'parent_id', 'Children Assets'),
246 'purchase_date': fields.date('Purchase Date', required=True, readonly=True, states={'draft':[('readonly',False)]}),
247 'state': fields.selection([('draft','Draft'),('open','Running'),('close','Close')], 'Status', required=True,
248 help="When an asset is created, the status is 'Draft'.\n" \
249 "If the asset is confirmed, the status goes in 'Running' and the depreciation lines can be posted in the accounting.\n" \
250 "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."),
251 'active': fields.boolean('Active'),
252 'partner_id': fields.many2one('res.partner', 'Partner', readonly=True, states={'draft':[('readonly',False)]}),
253 '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"\
254 " * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n" \
255 " * Degressive: Calculated on basis of: Residual Value * Degressive Factor"),
256 'method_number': fields.integer('Number of Depreciations', readonly=True, states={'draft':[('readonly',False)]}, help="The number of depreciations needed to depreciate your asset"),
257 '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"),
258 'method_end': fields.date('Ending Date', readonly=True, states={'draft':[('readonly',False)]}),
259 'method_progress_factor': fields.float('Degressive Factor', readonly=True, states={'draft':[('readonly',False)]}),
260 'value_residual': fields.function(_amount_residual, method=True, digits_compute=dp.get_precision('Account'), string='Residual Value'),
261 'method_time': fields.selection([('number','Number of Depreciations'),('end','Ending Date')], 'Time Method', required=True, readonly=True, states={'draft':[('readonly',False)]},
262 help="Choose the method to use to compute the dates and number of depreciation lines.\n"\
263 " * Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" \
264 " * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."),
265 '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'),
266 'history_ids': fields.one2many('account.asset.history', 'asset_id', 'History', readonly=True),
267 'depreciation_line_ids': fields.one2many('account.asset.depreciation.line', 'asset_id', 'Depreciation Lines', readonly=True, states={'draft':[('readonly',False)],'open':[('readonly',False)]}),
268 '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)]}),
271 'code': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'account.asset.code'),
272 'purchase_date': lambda obj, cr, uid, context: time.strftime('%Y-%m-%d'),
277 'method_time': 'number',
279 'method_progress_factor': 0.3,
280 'currency_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.currency_id.id,
281 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'account.asset.asset',context=context),
284 def _check_recursion(self, cr, uid, ids, context=None, parent=None):
285 return super(account_asset_asset, self)._check_recursion(cr, uid, ids, context=context, parent=parent)
287 def _check_prorata(self, cr, uid, ids, context=None):
288 for asset in self.browse(cr, uid, ids, context=context):
289 if asset.prorata and asset.method_time != 'number':
294 (_check_recursion, 'Error ! You cannot create recursive assets.', ['parent_id']),
295 (_check_prorata, 'Prorata temporis can be applied only for time method "number of depreciations".', ['prorata']),
298 def onchange_category_id(self, cr, uid, ids, category_id, context=None):
300 asset_categ_obj = self.pool.get('account.asset.category')
302 category_obj = asset_categ_obj.browse(cr, uid, category_id, context=context)
304 'method': category_obj.method,
305 'method_number': category_obj.method_number,
306 'method_time': category_obj.method_time,
307 'method_period': category_obj.method_period,
308 'method_progress_factor': category_obj.method_progress_factor,
309 'method_end': category_obj.method_end,
310 'prorata': category_obj.prorata,
314 def onchange_method_time(self, cr, uid, ids, method_time='number', context=None):
316 if method_time != 'number':
317 res['value'] = {'prorata': False}
320 def copy(self, cr, uid, id, default=None, context=None):
325 default.update({'depreciation_line_ids': [], 'state': 'draft'})
326 return super(account_asset_asset, self).copy(cr, uid, id, default, context=context)
328 def _compute_entries(self, cr, uid, ids, period_id, context=None):
330 period_obj = self.pool.get('account.period')
331 depreciation_obj = self.pool.get('account.asset.depreciation.line')
332 period = period_obj.browse(cr, uid, period_id, context=context)
333 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)
334 return depreciation_obj.create_move(cr, uid, depreciation_ids, context=context)
336 def create(self, cr, uid, vals, context=None):
337 asset_id = super(account_asset_asset, self).create(cr, uid, vals, context=context)
338 self.compute_depreciation_board(cr, uid, [asset_id], context=context)
341 def open_entries(self, cr, uid, ids, context=None):
344 context.update({'search_default_asset_id': ids, 'default_asset_id': ids})
347 'view_mode': 'tree,form',
348 'res_model': 'account.move.line',
350 'type': 'ir.actions.act_window',
354 account_asset_asset()
356 class account_asset_depreciation_line(osv.osv):
357 _name = 'account.asset.depreciation.line'
358 _description = 'Asset depreciation line'
360 def _get_move_check(self, cr, uid, ids, name, args, context=None):
362 for line in self.browse(cr, uid, ids, context=context):
363 res[line.id] = bool(line.move_id)
367 'name': fields.char('Depreciation Name', size=64, required=True, select=1),
368 'sequence': fields.integer('Sequence', required=True),
369 'asset_id': fields.many2one('account.asset.asset', 'Asset', required=True),
370 'parent_state': fields.related('asset_id', 'state', type='char', string='State of Asset'),
371 'amount': fields.float('Current Depreciation', digits_compute=dp.get_precision('Account'), required=True),
372 'remaining_value': fields.float('Next Period Depreciation', digits_compute=dp.get_precision('Account'),required=True),
373 'depreciated_value': fields.float('Amount Already Depreciated', required=True),
374 'depreciation_date': fields.date('Depreciation Date', select=1),
375 'move_id': fields.many2one('account.move', 'Depreciation Entry'),
376 'move_check': fields.function(_get_move_check, method=True, type='boolean', string='Posted', store=True)
379 def create_move(self, cr, uid, ids, context=None):
383 asset_obj = self.pool.get('account.asset.asset')
384 period_obj = self.pool.get('account.period')
385 move_obj = self.pool.get('account.move')
386 move_line_obj = self.pool.get('account.move.line')
387 currency_obj = self.pool.get('res.currency')
388 created_move_ids = []
390 for line in self.browse(cr, uid, ids, context=context):
391 depreciation_date = time.strftime('%Y-%m-%d')
392 period_ids = period_obj.find(cr, uid, depreciation_date, context=context)
393 company_currency = line.asset_id.company_id.currency_id.id
394 current_currency = line.asset_id.currency_id.id
395 context.update({'date': depreciation_date})
396 amount = currency_obj.compute(cr, uid, current_currency, company_currency, line.amount, context=context)
397 sign = (line.asset_id.category_id.journal_id.type == 'purchase' and 1) or -1
398 asset_name = line.asset_id.name
399 reference = line.name
402 'date': depreciation_date,
404 'period_id': period_ids and period_ids[0] or False,
405 'journal_id': line.asset_id.category_id.journal_id.id,
407 move_id = move_obj.create(cr, uid, move_vals, context=context)
408 journal_id = line.asset_id.category_id.journal_id.id
409 partner_id = line.asset_id.partner_id.id
410 move_line_obj.create(cr, uid, {
414 'account_id': line.asset_id.category_id.account_depreciation_id.id,
417 'period_id': period_ids and period_ids[0] or False,
418 'journal_id': journal_id,
419 'partner_id': partner_id,
420 'currency_id': company_currency != current_currency and current_currency or False,
421 'amount_currency': company_currency != current_currency and - sign * line.amount or 0.0,
422 'date': depreciation_date,
424 move_line_obj.create(cr, uid, {
428 'account_id': line.asset_id.category_id.account_expense_depreciation_id.id,
431 'period_id': period_ids and period_ids[0] or False,
432 'journal_id': journal_id,
433 'partner_id': partner_id,
434 'currency_id': company_currency != current_currency and current_currency or False,
435 'amount_currency': company_currency != current_currency and sign * line.amount or 0.0,
436 'analytic_account_id': line.asset_id.category_id.account_analytic_id.id,
437 'date': depreciation_date,
438 'asset_id': line.asset_id.id
440 self.write(cr, uid, line.id, {'move_id': move_id}, context=context)
441 created_move_ids.append(move_id)
442 asset_ids.append(line.asset_id.id)
443 # we re-evaluate the assets to determine whether we can close them
444 for asset in asset_obj.browse(cr, uid, list(set(asset_ids)), context=context):
445 if currency_obj.is_zero(cr, uid, asset.currency_id, asset.value_residual):
446 asset.write({'state': 'close'})
447 return created_move_ids
449 account_asset_depreciation_line()
451 class account_move_line(osv.osv):
452 _inherit = 'account.move.line'
454 'asset_id': fields.many2one('account.asset.asset', 'Asset'),
455 'entry_ids': fields.one2many('account.move.line', 'asset_id', 'Entries', readonly=True, states={'draft':[('readonly',False)]}),
460 class account_asset_history(osv.osv):
461 _name = 'account.asset.history'
462 _description = 'Asset history'
464 'name': fields.char('History name', size=64, select=1),
465 'user_id': fields.many2one('res.users', 'User', required=True),
466 'date': fields.date('Date', required=True),
467 'asset_id': fields.many2one('account.asset.asset', 'Asset', required=True),
468 'method_time': fields.selection([('number','Number of Depreciations'),('end','Ending Date')], 'Time Method', required=True,
469 help="The method to use to compute the dates and number of depreciation lines.\n"\
470 "Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" \
471 "Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."),
472 'method_number': fields.integer('Number of Depreciations', help="The number of depreciations needed to depreciate your asset"),
473 'method_period': fields.integer('Period Length', help="Time in month between two depreciations"),
474 'method_end': fields.date('Ending date'),
475 'note': fields.text('Note'),
479 'date': lambda *args: time.strftime('%Y-%m-%d'),
480 'user_id': lambda self, cr, uid, ctx: uid
483 account_asset_history()
485 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: