1 # -*- coding: 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
25 from osv import fields, osv
27 from tools.translate import _
28 import decimal_precision as dp
30 class account_analytic_account(osv.osv):
31 _name = 'account.analytic.account'
32 _inherit = ['mail.thread']
33 _description = 'Analytic Account'
35 def _compute_level_tree(self, cr, uid, ids, child_ids, res, field_names, context=None):
36 currency_obj = self.pool.get('res.currency')
38 def recursive_computation(account):
39 result2 = res[account.id].copy()
40 for son in account.child_ids:
41 result = recursive_computation(son)
42 for field in field_names:
43 if (account.currency_id.id != son.currency_id.id) and (field!='quantity'):
44 result[field] = currency_obj.compute(cr, uid, son.currency_id.id, account.currency_id.id, result[field], context=context)
45 result2[field] += result[field]
47 for account in self.browse(cr, uid, ids, context=context):
48 if account.id not in child_ids:
50 recres[account.id] = recursive_computation(account)
53 def _debit_credit_bal_qtty(self, cr, uid, ids, fields, arg, context=None):
57 child_ids = tuple(self.search(cr, uid, [('parent_id', 'child_of', ids)]))
67 where_clause_args = [tuple(child_ids)]
68 if context.get('from_date', False):
69 where_date += " AND l.date >= %s"
70 where_clause_args += [context['from_date']]
71 if context.get('to_date', False):
72 where_date += " AND l.date <= %s"
73 where_clause_args += [context['to_date']]
77 CASE WHEN l.amount > 0
83 CASE WHEN l.amount < 0
88 COALESCE(SUM(l.amount),0) AS balance,
89 COALESCE(SUM(l.unit_amount),0) AS quantity
90 FROM account_analytic_account a
91 LEFT JOIN account_analytic_line l ON (a.id = l.account_id)
93 """ + where_date + """
94 GROUP BY a.id""", where_clause_args)
95 for row in cr.dictfetchall():
98 res[row['id']][field] = row[field]
99 return self._compute_level_tree(cr, uid, ids, child_ids, res, fields, context)
101 def name_get(self, cr, uid, ids, context=None):
105 if isinstance(ids, (int, long)):
108 elmt = self.browse(cr, uid, id, context=context)
109 res.append((id, self._get_one_full_name(elmt)))
112 def _get_full_name(self, cr, uid, ids, name=None, args=None, context=None):
116 for elmt in self.browse(cr, uid, ids, context=context):
117 res[elmt.id] = self._get_one_full_name(elmt)
120 def _get_one_full_name(self, elmt, level=6):
123 if elmt.parent_id and not elmt.type == 'template':
124 parent_path = self._get_one_full_name(elmt.parent_id, level-1) + "/"
127 return parent_path + elmt.name
129 def _child_compute(self, cr, uid, ids, name, arg, context=None):
134 for account in self.browse(cr, uid, ids, context=context):
135 result[account.id] = map(lambda x: x.id, [child for child in account.child_ids if child.state != 'template'])
139 def _get_analytic_account(self, cr, uid, ids, context=None):
140 company_obj = self.pool.get('res.company')
141 analytic_obj = self.pool.get('account.analytic.account')
143 for company in company_obj.browse(cr, uid, ids, context=context):
144 accounts += analytic_obj.search(cr, uid, [('company_id', '=', company.id)])
147 def _set_company_currency(self, cr, uid, ids, name, value, arg, context=None):
148 if isinstance(ids, (int, long)):
150 for account in self.browse(cr, uid, ids, context=context):
151 if account.company_id:
152 if account.company_id.currency_id.id != value:
153 raise osv.except_osv(_('Error!'), _("If you set a company, the currency selected has to be the same as it's currency. \nYou can remove the company belonging, and thus change the currency, only on analytic account of type 'view'. This can be really usefull for consolidation purposes of several companies charts with different currencies, for example."))
155 return cr.execute("""update account_analytic_account set currency_id=%s where id=%s""", (value, account.id, ))
157 def _currency(self, cr, uid, ids, field_name, arg, context=None):
159 for rec in self.browse(cr, uid, ids, context=context):
161 result[rec.id] = rec.company_id.currency_id.id
163 result[rec.id] = rec.currency_id.id
167 'name': fields.char('Account/Contract Name', size=128, required=True),
168 'complete_name': fields.function(_get_full_name, type='char', string='Full Account Name'),
169 'code': fields.char('Reference', size=24, select=True),
170 'type': fields.selection([('view','Analytic View'), ('normal','Analytic Account'),('contract','Contract or Project'),('template','Template of Contract')], 'Type of Account', required=True,
171 help="If you select the View Type, it means you won\'t allow to create journal entries using that account.\n"\
172 "The type 'Analytic account' stands for usual accounts that you only want to use in accounting.\n"\
173 "If you select Contract or Project, it offers you the possibility to manage the validity and the invoicing options for this account.\n"\
174 "The special type 'Template of Contract' allows you to define a template with default data that you can reuse easily."),
175 'template_id': fields.many2one('account.analytic.account', 'Template of Contract'),
176 'description': fields.text('Description'),
177 'parent_id': fields.many2one('account.analytic.account', 'Parent Analytic Account', select=2),
178 'child_ids': fields.one2many('account.analytic.account', 'parent_id', 'Child Accounts'),
179 'child_complete_ids': fields.function(_child_compute, relation='account.analytic.account', string="Account Hierarchy", type='many2many'),
180 'line_ids': fields.one2many('account.analytic.line', 'account_id', 'Analytic Entries'),
181 'balance': fields.function(_debit_credit_bal_qtty, type='float', string='Balance', multi='debit_credit_bal_qtty', digits_compute=dp.get_precision('Account')),
182 'debit': fields.function(_debit_credit_bal_qtty, type='float', string='Debit', multi='debit_credit_bal_qtty', digits_compute=dp.get_precision('Account')),
183 'credit': fields.function(_debit_credit_bal_qtty, type='float', string='Credit', multi='debit_credit_bal_qtty', digits_compute=dp.get_precision('Account')),
184 'quantity': fields.function(_debit_credit_bal_qtty, type='float', string='Quantity', multi='debit_credit_bal_qtty'),
185 'quantity_max': fields.float('Prepaid Service Units', help='Sets the higher limit of time to work on the contract, based on the timesheet. (for instance, number of hours in a limited support contract.)'),
186 'partner_id': fields.many2one('res.partner', 'Customer'),
187 'user_id': fields.many2one('res.users', 'Project Manager'),
188 'manager_id': fields.many2one('res.users', 'Account Manager'),
189 'date_start': fields.date('Start Date'),
190 'date': fields.date('Date End', select=True),
191 'company_id': fields.many2one('res.company', 'Company', required=False), #not required because we want to allow different companies to use the same chart of account, except for leaf accounts.
192 'state': fields.selection([('template', 'Template'),('draft','New'),('open','In Progress'), ('cancelled', 'Cancelled'),('pending','To Renew'),('close','Closed')], 'Status', required=True,),
193 'currency_id': fields.function(_currency, fnct_inv=_set_company_currency, #the currency_id field is readonly except if it's a view account and if there is no company
195 'res.company': (_get_analytic_account, ['currency_id'], 10),
196 }, string='Currency', type='many2one', relation='res.currency'),
199 def on_change_template(self, cr, uid, ids, template_id, context=None):
203 template = self.browse(cr, uid, template_id, context=context)
204 if template.date_start and template.date:
205 from_dt = datetime.strptime(template.date_start, tools.DEFAULT_SERVER_DATE_FORMAT)
206 to_dt = datetime.strptime(template.date, tools.DEFAULT_SERVER_DATE_FORMAT)
207 timedelta = to_dt - from_dt
208 res['value']['date'] = datetime.strftime(datetime.now() + timedelta, tools.DEFAULT_SERVER_DATE_FORMAT)
209 res['value']['date_start'] = fields.date.today()
210 res['value']['quantity_max'] = template.quantity_max
211 res['value']['parent_id'] = template.parent_id and template.parent_id.id or False
212 res['value']['description'] = template.description
215 def on_change_partner_id(self, cr, uid, ids,partner_id, name, context={}):
218 partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
220 res['manager_id'] = partner.user_id.id
222 res['name'] = _('Contract: ') + partner.name
223 return {'value': res}
225 def _default_company(self, cr, uid, context=None):
226 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
228 return user.company_id.id
229 return self.pool.get('res.company').search(cr, uid, [('parent_id', '=', False)])[0]
231 def _get_default_currency(self, cr, uid, context=None):
232 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
233 return user.company_id.currency_id.id
237 'company_id': _default_company,
238 'code' : lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'account.analytic.account'),
240 'user_id': lambda self, cr, uid, ctx: uid,
241 'partner_id': lambda self, cr, uid, ctx: ctx.get('partner_id', False),
242 'date_start': lambda *a: time.strftime('%Y-%m-%d'),
243 'currency_id': _get_default_currency,
246 def check_recursion(self, cr, uid, ids, context=None, parent=None):
247 return super(account_analytic_account, self)._check_recursion(cr, uid, ids, context=context, parent=parent)
251 (check_recursion, 'Error! You cannot create recursive analytic accounts.', ['parent_id']),
254 def copy(self, cr, uid, id, default=None, context=None):
257 analytic = self.browse(cr, uid, id, context=context)
261 name=_("%s (copy)") % (analytic['name']))
262 return super(account_analytic_account, self).copy(cr, uid, id, default, context=context)
264 def on_change_company(self, cr, uid, id, company_id):
267 currency = self.pool.get('res.company').read(cr, uid, [company_id], ['currency_id'])[0]['currency_id']
268 return {'value': {'currency_id': currency}}
270 def on_change_parent(self, cr, uid, id, parent_id):
273 parent = self.read(cr, uid, [parent_id], ['partner_id','code'])[0]
274 if parent['partner_id']:
275 partner = parent['partner_id'][0]
280 res['value']['partner_id'] = partner
283 def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
288 if context.get('current_model') == 'project.project':
289 project_obj = self.pool.get("account.analytic.account")
290 project_ids = project_obj.search(cr, uid, args)
291 return self.name_get(cr, uid, project_ids, context=context)
293 account_ids = self.search(cr, uid, [('code', '=', name)] + args, limit=limit, context=context)
295 names=map(lambda i : i.strip(),name.split('/'))
296 for i in range(len(names)):
297 dom=[('name', operator, names[i])]
299 dom+=[('id','child_of',account_ids)]
300 account_ids = self.search(cr, uid, dom, limit=limit, context=context)
303 newacc = self.search(cr, uid, [('parent_id', 'in', newacc)], limit=limit, context=context)
304 account_ids += newacc
306 account_ids = self.search(cr, uid, [('id', 'in', account_ids)] + args, limit=limit, context=context)
308 account_ids = self.search(cr, uid, args, limit=limit, context=context)
309 return self.name_get(cr, uid, account_ids, context=context)
311 def create(self, cr, uid, vals, context=None):
312 contract = super(account_analytic_account, self).create(cr, uid, vals, context=context)
314 self.create_send_note(cr, uid, [contract], context=context)
317 def create_send_note(self, cr, uid, ids, context=None):
318 for obj in self.browse(cr, uid, ids, context=context):
319 message = _("Contract <b>created</b>.")
321 message = _("Contract for <em>%s</em> has been <b>created</b>.") % (obj.partner_id.name,)
322 self.message_post(cr, uid, [obj.id], body=message,
323 subtype="analytic.mt_account_status", context=context)
325 account_analytic_account()
328 class account_analytic_line(osv.osv):
329 _name = 'account.analytic.line'
330 _description = 'Analytic Line'
333 'name': fields.char('Description', size=256, required=True),
334 'date': fields.date('Date', required=True, select=True),
335 'amount': fields.float('Amount', required=True, help='Calculated by multiplying the quantity and the price given in the Product\'s cost price. Always expressed in the company main currency.', digits_compute=dp.get_precision('Account')),
336 'unit_amount': fields.float('Quantity', help='Specifies the amount of quantity to count.'),
337 'account_id': fields.many2one('account.analytic.account', 'Analytic Account', required=True, ondelete='restrict', select=True, domain=[('type','<>','view')]),
338 'user_id': fields.many2one('res.users', 'User'),
339 'company_id': fields.related('account_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
343 def _get_default_date(self, cr, uid, context=None):
344 return fields.date.context_today(self, cr, uid, context=context)
346 def __get_default_date(self, cr, uid, context=None):
347 return self._get_default_date(cr, uid, context=context)
350 'date': __get_default_date,
351 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.analytic.line', context=c),
357 def _check_no_view(self, cr, uid, ids, context=None):
358 analytic_lines = self.browse(cr, uid, ids, context=context)
359 for line in analytic_lines:
360 if line.account_id.type == 'view':
365 (_check_no_view, 'You cannot create analytic line on view account.', ['account_id']),
368 account_analytic_line()
370 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: