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: Remaining Value * Degressive Factor"),
45 'method_number': fields.integer('Number of Depreciations'),
46 'method_period': fields.integer('Period Length', help="State here the time between 2 depreciations, in months", required=True),
47 'method_progress_factor': fields.float('Degressive Factor'),
48 'method_time': fields.selection([('number','Number of Depreciations'),('end','Ending Date')], 'Time Method', required=True,
49 help="Choose the method to use to compute the dates and number of depreciation lines.\n"\
50 " * Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" \
51 " * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."),
52 'method_end': fields.date('Ending date'),
53 'prorata':fields.boolean('Prorata Temporis', help='Indicates that the first depreciation entry for this asset have to be done from the purchase date instead of the first January'),
54 'open_asset': fields.boolean('Skip Draft State', help="Check this if you want to automatically confirm the assets of this category when created by invoices."),
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 for asset in self.browse(cr, uid, ids, context=context):
138 if asset.value_residual == 0.0:
140 posted_depreciation_line_ids = depreciation_lin_obj.search(cr, uid, [('asset_id', '=', asset.id), ('move_check', '=', True)])
141 old_depreciation_line_ids = depreciation_lin_obj.search(cr, uid, [('asset_id', '=', asset.id), ('move_id', '=', False)])
142 if old_depreciation_line_ids:
143 depreciation_lin_obj.unlink(cr, uid, old_depreciation_line_ids, context=context)
145 amount_to_depr = residual_amount = asset.value_residual
147 depreciation_date = datetime.strptime(self._get_last_depreciation_date(cr, uid, [asset.id], context)[asset.id], '%Y-%m-%d')
149 # depreciation_date = 1st January of purchase year
150 purchase_date = datetime.strptime(asset.purchase_date, '%Y-%m-%d')
151 depreciation_date = datetime(purchase_date.year, 1, 1)
152 day = depreciation_date.day
153 month = depreciation_date.month
154 year = depreciation_date.year
155 total_days = (year % 4) and 365 or 366
157 undone_dotation_number = self._compute_board_undone_dotation_nb(cr, uid, asset, depreciation_date, total_days, context=context)
158 for x in range(len(posted_depreciation_line_ids), undone_dotation_number):
160 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)
161 residual_amount -= amount
164 'asset_id': asset.id,
166 'name': str(asset.id) +'/' + str(i),
167 'remaining_value': residual_amount,
168 'depreciated_value': (asset.purchase_value - asset.salvage_value) - (residual_amount + amount),
169 'depreciation_date': depreciation_date.strftime('%Y-%m-%d'),
171 depreciation_lin_obj.create(cr, uid, vals, context=context)
172 # Considering Depr. Period as months
173 depreciation_date = (datetime(year, month, day) + relativedelta(months=+asset.method_period))
174 day = depreciation_date.day
175 month = depreciation_date.month
176 year = depreciation_date.year
179 def validate(self, cr, uid, ids, context=None):
182 return self.write(cr, uid, ids, {
186 def set_to_close(self, cr, uid, ids, context=None):
187 return self.write(cr, uid, ids, {'state': 'close'}, context=context)
189 def set_to_draft(self, cr, uid, ids, context=None):
190 return self.write(cr, uid, ids, {'state': 'draft'}, context=context)
192 def _amount_residual(self, cr, uid, ids, name, args, context=None):
194 l.asset_id as id, round(SUM(abs(l.debit-l.credit))) AS amount
198 l.asset_id IN %s GROUP BY l.asset_id """, (tuple(ids),))
199 res=dict(cr.fetchall())
200 for asset in self.browse(cr, uid, ids, context):
201 res[asset.id] = asset.purchase_value - res.get(asset.id, 0.0) - asset.salvage_value
203 res.setdefault(id, 0.0)
206 def onchange_company_id(self, cr, uid, ids, company_id=False, context=None):
209 company = self.pool.get('res.company').browse(cr, uid, company_id, context=context)
210 if company.currency_id.company_id and company.currency_id.company_id.id != company_id:
211 val['currency_id'] = False
213 val['currency_id'] = company.currency_id.id
214 return {'value': val}
217 'account_move_line_ids': fields.one2many('account.move.line', 'asset_id', 'Entries', readonly=True, states={'draft':[('readonly',False)]}),
218 'name': fields.char('Asset Name', size=64, required=True, readonly=True, states={'draft':[('readonly',False)]}),
219 'code': fields.char('Reference', size=32, readonly=True, states={'draft':[('readonly',False)]}),
220 'purchase_value': fields.float('Gross Value', required=True, readonly=True, states={'draft':[('readonly',False)]}),
221 'currency_id': fields.many2one('res.currency','Currency',required=True, readonly=True, states={'draft':[('readonly',False)]}),
222 'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True, states={'draft':[('readonly',False)]}),
223 'note': fields.text('Note'),
224 'category_id': fields.many2one('account.asset.category', 'Asset Category', required=True, change_default=True, readonly=True, states={'draft':[('readonly',False)]}),
225 'parent_id': fields.many2one('account.asset.asset', 'Parent Asset', readonly=True, states={'draft':[('readonly',False)]}),
226 'child_ids': fields.one2many('account.asset.asset', 'parent_id', 'Children Assets'),
227 'purchase_date': fields.date('Purchase Date', required=True, readonly=True, states={'draft':[('readonly',False)]}),
228 'state': fields.selection([('draft','Draft'),('open','Running'),('close','Close')], 'Status', required=True,
229 help="When an asset is created, the status is 'Draft'.\n" \
230 "If the asset is confirmed, the status goes in 'Running' and the depreciation lines can be posted in the accounting.\n" \
231 "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."),
232 'active': fields.boolean('Active'),
233 'partner_id': fields.many2one('res.partner', 'Partner', readonly=True, states={'draft':[('readonly',False)]}),
234 '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"\
235 " * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n" \
236 " * Degressive: Calculated on basis of: Remaining Value * Degressive Factor"),
237 'method_number': fields.integer('Number of Depreciations', readonly=True, states={'draft':[('readonly',False)]}, help="Calculates Depreciation within specified interval"),
238 'method_period': fields.integer('Number of Months in a Period', required=True, readonly=True, states={'draft':[('readonly',False)]}, help="State here the time during 2 depreciations, in months"),
239 'method_end': fields.date('Ending Date', readonly=True, states={'draft':[('readonly',False)]}),
240 'method_progress_factor': fields.float('Degressive Factor', readonly=True, states={'draft':[('readonly',False)]}),
241 'value_residual': fields.function(_amount_residual, method=True, digits_compute=dp.get_precision('Account'), string='Residual Value'),
242 'method_time': fields.selection([('number','Number of Depreciations'),('end','Ending Date')], 'Time Method', required=True, readonly=True, states={'draft':[('readonly',False)]},
243 help="Choose the method to use to compute the dates and number of depreciation lines.\n"\
244 " * Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" \
245 " * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."),
246 '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'),
247 'history_ids': fields.one2many('account.asset.history', 'asset_id', 'History', readonly=True),
248 'depreciation_line_ids': fields.one2many('account.asset.depreciation.line', 'asset_id', 'Depreciation Lines', readonly=True, states={'draft':[('readonly',False)],'open':[('readonly',False)]}),
249 '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)]}),
252 'code': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'account.asset.code'),
253 'purchase_date': lambda obj, cr, uid, context: time.strftime('%Y-%m-%d'),
258 'method_time': 'number',
260 'method_progress_factor': 0.3,
261 'currency_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.currency_id.id,
262 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'account.asset.asset',context=context),
265 def _check_recursion(self, cr, uid, ids, context=None, parent=None):
266 return super(account_asset_asset, self)._check_recursion(cr, uid, ids, context=context, parent=parent)
268 def _check_prorata(self, cr, uid, ids, context=None):
269 for asset in self.browse(cr, uid, ids, context=context):
270 if asset.prorata and asset.method_time != 'number':
275 (_check_recursion, 'Error ! You cannot create recursive assets.', ['parent_id']),
276 (_check_prorata, 'Prorata temporis can be applied only for time method "number of depreciations".', ['prorata']),
279 def onchange_category_id(self, cr, uid, ids, category_id, context=None):
281 asset_categ_obj = self.pool.get('account.asset.category')
283 category_obj = asset_categ_obj.browse(cr, uid, category_id, context=context)
285 'method': category_obj.method,
286 'method_number': category_obj.method_number,
287 'method_time': category_obj.method_time,
288 'method_period': category_obj.method_period,
289 'method_progress_factor': category_obj.method_progress_factor,
290 'method_end': category_obj.method_end,
291 'prorata': category_obj.prorata,
295 def onchange_method_time(self, cr, uid, ids, method_time='number', context=None):
297 if method_time != 'number':
298 res['value'] = {'prorata': False}
301 def copy(self, cr, uid, id, default=None, context=None):
306 default.update({'depreciation_line_ids': [], 'state': 'draft'})
307 return super(account_asset_asset, self).copy(cr, uid, id, default, context=context)
309 def _compute_entries(self, cr, uid, ids, period_id, context=None):
311 period_obj = self.pool.get('account.period')
312 depreciation_obj = self.pool.get('account.asset.depreciation.line')
313 period = period_obj.browse(cr, uid, period_id, context=context)
314 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)
315 return depreciation_obj.create_move(cr, uid, depreciation_ids, context=context)
317 def create(self, cr, uid, vals, context=None):
318 asset_id = super(account_asset_asset, self).create(cr, uid, vals, context=context)
319 self.compute_depreciation_board(cr, uid, [asset_id], context=context)
322 def open_entries(self, cr, uid, ids, context=None):
325 context.update({'search_default_asset_id': ids, 'default_asset_id': ids})
328 'view_mode': 'tree,form',
329 'res_model': 'account.move.line',
331 'type': 'ir.actions.act_window',
335 account_asset_asset()
337 class account_asset_depreciation_line(osv.osv):
338 _name = 'account.asset.depreciation.line'
339 _description = 'Asset depreciation line'
341 def _get_move_check(self, cr, uid, ids, name, args, context=None):
343 for line in self.browse(cr, uid, ids, context=context):
344 res[line.id] = bool(line.move_id)
348 'name': fields.char('Depreciation Name', size=64, required=True, select=1),
349 'sequence': fields.integer('Sequence', required=True),
350 'asset_id': fields.many2one('account.asset.asset', 'Asset', required=True),
351 'parent_state': fields.related('asset_id', 'state', type='char', string='State of Asset'),
352 'amount': fields.float('Depreciation Amount', required=True),
353 'remaining_value': fields.float('Amount to Depreciate', required=True),
354 'depreciated_value': fields.float('Amount Already Depreciated', required=True),
355 'depreciation_date': fields.date('Depreciation Date', select=1),
356 'move_id': fields.many2one('account.move', 'Depreciation Entry'),
357 'move_check': fields.function(_get_move_check, method=True, type='boolean', string='Posted', store=True)
360 def create_move(self, cr, uid, ids, context=None):
364 asset_obj = self.pool.get('account.asset.asset')
365 period_obj = self.pool.get('account.period')
366 move_obj = self.pool.get('account.move')
367 move_line_obj = self.pool.get('account.move.line')
368 currency_obj = self.pool.get('res.currency')
369 created_move_ids = []
370 for line in self.browse(cr, uid, ids, context=context):
371 if currency_obj.is_zero(cr, uid, line.asset_id.currency_id, line.remaining_value):
373 depreciation_date = time.strftime('%Y-%m-%d')
374 period_ids = period_obj.find(cr, uid, depreciation_date, context=context)
375 company_currency = line.asset_id.company_id.currency_id.id
376 current_currency = line.asset_id.currency_id.id
377 context.update({'date': depreciation_date})
378 amount = currency_obj.compute(cr, uid, current_currency, company_currency, line.amount, context=context)
379 sign == (line.asset_id.category_id.journal_id.type == 'purchase' and 1) or -1
380 asset_name = line.asset_id.name
381 reference = line.name
384 'date': depreciation_date,
386 'period_id': period_ids and period_ids[0] or False,
387 'journal_id': line.asset_id.category_id.journal_id.id,
389 move_id = move_obj.create(cr, uid, move_vals, context=context)
390 journal_id = line.asset_id.category_id.journal_id.id
391 partner_id = line.asset_id.partner_id.id
392 move_line_obj.create(cr, uid, {
396 'account_id': line.asset_id.category_id.account_depreciation_id.id,
399 'period_id': period_ids and period_ids[0] or False,
400 'journal_id': journal_id,
401 'partner_id': partner_id,
402 'currency_id': company_currency <> current_currency and current_currency or False,
403 'amount_currency': company_currency <> current_currency and - sign * line.amount or 0.0,
404 'date': depreciation_date,
406 move_line_obj.create(cr, uid, {
410 'account_id': line.asset_id.category_id.account_expense_depreciation_id.id,
413 'period_id': period_ids and period_ids[0] or False,
414 'journal_id': journal_id,
415 'partner_id': partner_id,
416 'currency_id': company_currency <> current_currency and current_currency or False,
417 'amount_currency': company_currency <> current_currency and sign * line.amount or 0.0,
418 'analytic_account_id': line.asset_id.category_id.account_analytic_id.id,
419 'date': depreciation_date,
420 'asset_id': line.asset_id.id
422 self.write(cr, uid, line.id, {'move_id': move_id}, context=context)
423 created_move_ids.append(move_id)
425 asset_obj.write(cr, uid, [line.asset_id.id], {'state': 'close'}, context=context)
426 return created_move_ids
428 account_asset_depreciation_line()
430 class account_move_line(osv.osv):
431 _inherit = 'account.move.line'
433 'asset_id': fields.many2one('account.asset.asset', 'Asset'),
434 'entry_ids': fields.one2many('account.move.line', 'asset_id', 'Entries', readonly=True, states={'draft':[('readonly',False)]}),
439 class account_asset_history(osv.osv):
440 _name = 'account.asset.history'
441 _description = 'Asset history'
443 'name': fields.char('History name', size=64, select=1),
444 'user_id': fields.many2one('res.users', 'User', required=True),
445 'date': fields.date('Date', required=True),
446 'asset_id': fields.many2one('account.asset.asset', 'Asset', required=True),
447 'method_time': fields.selection([('number','Number of Depreciations'),('end','Ending Date')], 'Time Method', required=True,
448 help="The method to use to compute the dates and number of depreciation lines.\n"\
449 "Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" \
450 "Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."),
451 'method_number': fields.integer('Number of Depreciations'),
452 'method_period': fields.integer('Period Length', help="Time in month between two depreciations"),
453 'method_end': fields.date('Ending date'),
454 'note': fields.text('Note'),
458 'date': lambda *args: time.strftime('%Y-%m-%d'),
459 'user_id': lambda self, cr, uid, ctx: uid
462 account_asset_history()
464 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: