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={}):
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
117 def _compute_board_undone_dotation_nb(self, cr, uid, asset, depreciation_date, total_days, context=None):
118 undone_dotation_number = asset.method_number
119 if asset.method_time == 'end':
120 end_date = datetime.strptime(asset.method_end, '%Y-%m-%d')
121 undone_dotation_number = (end_date - depreciation_date).days / total_days
122 if asset.prorata or asset.method_time == 'end':
123 undone_dotation_number += 1
124 return undone_dotation_number
126 def compute_depreciation_board(self, cr, uid,ids, context=None):
127 depreciation_lin_obj = self.pool.get('account.asset.depreciation.line')
128 for asset in self.browse(cr, uid, ids, context=context):
129 if asset.value_residual == 0.0:
131 posted_depreciation_line_ids = depreciation_lin_obj.search(cr, uid, [('asset_id', '=', asset.id), ('move_check', '=', True)])
132 old_depreciation_line_ids = depreciation_lin_obj.search(cr, uid, [('asset_id', '=', asset.id), ('move_id', '=', False)])
133 if old_depreciation_line_ids:
134 depreciation_lin_obj.unlink(cr, uid, old_depreciation_line_ids, context=context)
136 amount_to_depr = residual_amount = asset.value_residual
138 depreciation_date = datetime.strptime(self._get_last_depreciation_date(cr, uid, [asset.id], context)[asset.id], '%Y-%m-%d')
139 day = depreciation_date.day
140 month = depreciation_date.month
141 year = depreciation_date.year
142 total_days = (year % 4) and 365 or 366
144 undone_dotation_number = self._compute_board_undone_dotation_nb(cr, uid, asset, depreciation_date, total_days, context=context)
145 for x in range(len(posted_depreciation_line_ids), undone_dotation_number):
147 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)
148 residual_amount -= amount
151 'asset_id': asset.id,
153 'name': str(asset.id) +'/' + str(i),
154 'remaining_value': residual_amount,
155 'depreciated_value': (asset.purchase_value - asset.salvage_value) - (residual_amount + amount),
156 'depreciation_date': depreciation_date.strftime('%Y-%m-%d'),
158 depreciation_lin_obj.create(cr, uid, vals, context=context)
159 # Considering Depr. Period as months
160 depreciation_date = (datetime(year, month, day) + relativedelta(months=+asset.method_period))
161 day = depreciation_date.day
162 month = depreciation_date.month
163 year = depreciation_date.year
166 def validate(self, cr, uid, ids, context={}):
167 return self.write(cr, uid, ids, {
171 def set_to_close(self, cr, uid, ids, context=None):
172 return self.write(cr, uid, ids, {'state': 'close'}, context=context)
174 def _amount_residual(self, cr, uid, ids, name, args, context=None):
176 l.asset_id as id, round(SUM(abs(l.debit-l.credit))) AS amount
180 l.asset_id IN %s GROUP BY l.asset_id """, (tuple(ids),))
181 res=dict(cr.fetchall())
182 for asset in self.browse(cr, uid, ids, context):
183 res[asset.id] = asset.purchase_value - res.get(asset.id, 0.0) - asset.salvage_value
185 res.setdefault(id, 0.0)
189 'account_move_line_ids': fields.one2many('account.move.line', 'asset_id', 'Entries', readonly=True, states={'draft':[('readonly',False)]}),
190 'name': fields.char('Asset', size=64, required=True, readonly=True, states={'draft':[('readonly',False)]}),
191 'code': fields.char('Reference ', size=16, readonly=True, states={'draft':[('readonly',False)]}),
192 'purchase_value': fields.float('Gross value ', required=True, readonly=True, states={'draft':[('readonly',False)]}),
193 'currency_id': fields.many2one('res.currency','Currency',required=True, readonly=True, states={'draft':[('readonly',False)]}),
194 'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True, states={'draft':[('readonly',False)]}),
195 'note': fields.text('Note'),
196 'category_id': fields.many2one('account.asset.category', 'Asset category', required=True, change_default=True, readonly=True, states={'draft':[('readonly',False)]}),
197 'parent_id': fields.many2one('account.asset.asset', 'Parent Asset', readonly=True, states={'draft':[('readonly',False)]}),
198 'child_ids': fields.one2many('account.asset.asset', 'parent_id', 'Children Assets'),
199 'purchase_date': fields.date('Purchase Date', required=True, readonly=True, states={'draft':[('readonly',False)]}),
200 'state': fields.selection([('draft','Draft'),('open','Running'),('close','Close')], 'State', required=True,
201 help="When an asset is created, the state is 'Draft'.\n" \
202 "If the asset is confirmed, the state goes in 'Running' and the depreciation lines can be posted in the accounting.\n" \
203 "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 state."),
204 'active': fields.boolean('Active'),
205 'partner_id': fields.many2one('res.partner', 'Partner', readonly=True, states={'draft':[('readonly',False)]}),
206 '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"\
207 " * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n" \
208 " * Degressive: Calculated on basis of: Remaining Value * Degressive Factor"),
209 'method_number': fields.integer('Number of Depreciations', readonly=True, states={'draft':[('readonly',False)]}, help="Calculates Depreciation within specified interval"),
210 'method_period': fields.integer('Period Length', required=True, readonly=True, states={'draft':[('readonly',False)]}, help="State here the time during 2 depreciations, in months"),
211 'method_end': fields.date('Ending Date', readonly=True, states={'draft':[('readonly',False)]}),
212 'method_progress_factor': fields.float('Degressive Factor', readonly=True, states={'draft':[('readonly',False)]}),
213 'value_residual': fields.function(_amount_residual, method=True, digits_compute=dp.get_precision('Account'), string='Residual Value'),
214 'method_time': fields.selection([('number','Number of Depreciations'),('end','Ending Date')], 'Time Method', required=True, readonly=True, states={'draft':[('readonly',False)]},
215 help="Choose the method to use to compute the dates and number of depreciation lines.\n"\
216 " * Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" \
217 " * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."),
218 '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'),
219 'history_ids': fields.one2many('account.asset.history', 'asset_id', 'History', readonly=True),
220 'depreciation_line_ids': fields.one2many('account.asset.depreciation.line', 'asset_id', 'Depreciation Lines', readonly=True, states={'draft':[('readonly',False)],'open':[('readonly',False)]}),
221 '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)]}),
224 'code': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'account.asset.code'),
225 'purchase_date': lambda obj, cr, uid, context: time.strftime('%Y-%m-%d'),
230 'method_time': 'number',
232 'method_progress_factor': 0.3,
233 'currency_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.currency_id.id,
234 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'account.asset.asset',context=context),
237 def _check_recursion(self, cr, uid, ids, context=None, parent=None):
238 return super(account_asset_asset, self)._check_recursion(cr, uid, ids, context=context, parent=parent)
240 def _check_prorata(self, cr, uid, ids, context=None):
241 for asset in self.browse(cr, uid, ids, context=context):
242 if asset.prorata and (asset.method != 'linear' or asset.method_time != 'number'):
247 (_check_recursion, 'Error ! You can not create recursive assets.', ['parent_id']),
248 (_check_prorata, 'Prorata temporis can be applied only for computation method "linear" and time method "number of depreciations".', ['prorata']),
251 def onchange_category_id(self, cr, uid, ids, category_id, context=None):
253 asset_categ_obj = self.pool.get('account.asset.category')
255 category_obj = asset_categ_obj.browse(cr, uid, category_id, context=context)
257 'method': category_obj.method,
258 'method_number': category_obj.method_number,
259 'method_time': category_obj.method_time,
260 'method_period': category_obj.method_period,
261 'method_progress_factor': category_obj.method_progress_factor,
262 'method_end': category_obj.method_end,
263 'prorata': category_obj.prorata,
267 def onchange_method_time(self, cr, uid, ids, method='linear', method_time='number', context=None):
269 if method != 'linear' or method_time != 'number':
270 res['value'] = {'prorata': False}
273 def copy(self, cr, uid, id, default=None, context=None):
278 default.update({'depreciation_line_ids': [], 'state': 'draft'})
279 return super(account_asset_asset, self).copy(cr, uid, id, default, context=context)
281 def _compute_entries(self, cr, uid, ids, period_id, context={}):
283 period_obj = self.pool.get('account.period')
284 depreciation_obj = self.pool.get('account.asset.depreciation.line')
285 period = period_obj.browse(cr, uid, period_id, context=context)
286 depreciation_ids = depreciation_obj.search(cr, uid, [('asset_id', 'in', ids), ('depreciation_date','<',period.date_stop), ('depreciation_date', '>', period.date_start)], context=context)
287 return depreciation_obj.create_move(cr, uid, depreciation_ids, context=context)
289 def create(self, cr, uid, vals, context=None):
290 asset_id = super(account_asset_asset, self).create(cr, uid, vals, context=context)
291 self.compute_depreciation_board(cr, uid, [asset_id], context=context)
294 account_asset_asset()
296 class account_asset_depreciation_line(osv.osv):
297 _name = 'account.asset.depreciation.line'
298 _description = 'Asset depreciation line'
300 def _get_move_check(self, cr, uid, ids, name, args, context=None):
302 for line in self.browse(cr, uid, ids, context=context):
303 res[line.id] = bool(line.move_id)
307 'name': fields.char('Depreciation Name', size=64, required=True, select=1),
308 'sequence': fields.integer('Sequence of the depreciation', required=True),
309 'asset_id': fields.many2one('account.asset.asset', 'Asset', required=True),
310 'parent_state': fields.related('asset_id', 'state', type='char', string='State of Asset'),
311 'amount': fields.float('Depreciation Amount', required=True),
312 'remaining_value': fields.float('Amount to Depreciate', required=True),
313 'depreciated_value': fields.float('Amount Already Depreciated', required=True),
314 'depreciation_date': fields.char('Depreciation Date', size=64, select=1),
315 'move_id': fields.many2one('account.move', 'Depreciation Entry'),
316 'move_check': fields.function(_get_move_check, method=True, type='boolean', string='Posted', store=True)
319 def create_move(self, cr, uid, ids, context=None):
323 asset_obj = self.pool.get('account.asset.asset')
324 period_obj = self.pool.get('account.period')
325 move_obj = self.pool.get('account.move')
326 move_line_obj = self.pool.get('account.move.line')
327 currency_obj = self.pool.get('res.currency')
328 created_move_ids = []
329 for line in self.browse(cr, uid, ids, context=context):
330 if currency_obj.is_zero(cr, uid, line.asset_id.currency_id, line.remaining_value):
332 depreciation_date = line.asset_id.prorata and line.asset_id.purchase_date or time.strftime('%Y-%m-%d')
333 period_ids = period_obj.find(cr, uid, depreciation_date, context=context)
334 company_currency = line.asset_id.company_id.currency_id.id
335 current_currency = line.asset_id.currency_id.id
336 context.update({'date': depreciation_date})
337 amount = currency_obj.compute(cr, uid, current_currency, company_currency, line.amount, context=context)
338 sign = line.asset_id.category_id.journal_id.type = 'purchase' and 1 or -1
341 'date': depreciation_date,
343 'period_id': period_ids and period_ids[0] or False,
344 'journal_id': line.asset_id.category_id.journal_id.id,
346 move_id = move_obj.create(cr, uid, move_vals, context=context)
347 asset_name = line.asset_id.name
348 reference = line.name
349 journal_id = line.asset_id.category_id.journal_id.id
350 partner_id = line.asset_id.partner_id.id
351 move_line_obj.create(cr, uid, {
355 'account_id': line.asset_id.category_id.account_depreciation_id.id,
358 'period_id': period_ids and period_ids[0] or False,
359 'journal_id': journal_id,
360 'partner_id': partner_id,
361 'currency_id': company_currency <> current_currency and current_currency or False,
362 'amount_currency': company_currency <> current_currency and - sign * line.amount or 0.0,
363 'date': depreciation_date,
365 move_line_obj.create(cr, uid, {
369 'account_id': line.asset_id.category_id.account_expense_depreciation_id.id,
372 'period_id': period_ids and period_ids[0] or False,
373 'journal_id': journal_id,
374 'partner_id': partner_id,
375 'currency_id': company_currency <> current_currency and current_currency or False,
376 'amount_currency': company_currency <> current_currency and sign * line.amount or 0.0,
377 'analytic_account_id': line.asset_id.category_id.account_analytic_id.id,
378 'date': depreciation_date,
379 'asset_id': line.asset_id.id
381 self.write(cr, uid, line.id, {'move_id': move_id}, context=context)
382 created_move_ids.append(move_id)
384 asset_obj.write(cr, uid, [line.asset_id.id], {'state': 'close'}, context=context)
385 return created_move_ids
387 account_asset_depreciation_line()
389 class account_move_line(osv.osv):
390 _inherit = 'account.move.line'
392 'asset_id': fields.many2one('account.asset.asset', 'Asset'),
393 'entry_ids': fields.one2many('account.move.line', 'asset_id', 'Entries', readonly=True, states={'draft':[('readonly',False)]}),
398 class account_asset_history(osv.osv):
399 _name = 'account.asset.history'
400 _description = 'Asset history'
402 'name': fields.char('History name', size=64, select=1),
403 'user_id': fields.many2one('res.users', 'User', required=True),
404 'date': fields.date('Date', required=True),
405 'asset_id': fields.many2one('account.asset.asset', 'Asset', required=True),
406 'method_time': fields.selection([('number','Number of Depreciations'),('end','Ending Date')], 'Time Method', required=True,
407 help="Choose the method to use to compute the dates and number of depreciation lines.\n"\
408 "Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" \
409 "Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."),
410 'method_number': fields.integer('Number of Depreciations'),
411 'method_period': fields.integer('Period Length', help="Time in month between two depreciations"),
412 'method_end': fields.date('Ending date'),
413 'note': fields.text('Note'),
417 'date': lambda *args: time.strftime('%Y-%m-%d'),
418 'user_id': lambda self, cr, uid, ctx: uid
421 account_asset_history()
423 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: