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 ##############################################################################
21 from dateutil.relativedelta import relativedelta
26 from openerp.osv import osv, fields
27 from openerp.osv.orm import intersect, except_orm
29 from openerp.tools.translate import _
31 from openerp.addons.decimal_precision import decimal_precision as dp
33 _logger = logging.getLogger(__name__)
35 class account_analytic_invoice_line(osv.osv):
36 _name = "account.analytic.invoice.line"
38 def _amount_line(self, cr, uid, ids, prop, unknow_none, unknow_dict, context=None):
40 for line in self.browse(cr, uid, ids, context=context):
41 res[line.id] = line.quantity * line.price_unit
42 if line.analytic_account_id.pricelist_id:
43 cur = line.analytic_account_id.pricelist_id.currency_id
44 res[line.id] = self.pool.get('res.currency').round(cr, uid, cur, res[line.id])
48 'product_id': fields.many2one('product.product','Product',required=True),
49 'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account'),
50 'name': fields.text('Description', required=True),
51 'quantity': fields.float('Quantity', required=True),
52 'uom_id': fields.many2one('product.uom', 'Unit of Measure',required=True),
53 'price_unit': fields.float('Unit Price', required=True),
54 'price_subtotal': fields.function(_amount_line, string='Sub Total', type="float",digits_compute= dp.get_precision('Account')),
60 def product_id_change(self, cr, uid, ids, product, uom_id, qty=0, name='', partner_id=False, price_unit=False, pricelist_id=False, company_id=None, context=None):
61 context = context or {}
62 uom_obj = self.pool.get('product.uom')
63 company_id = company_id or False
64 context.update({'company_id': company_id, 'force_company': company_id, 'pricelist_id': pricelist_id})
67 return {'value': {'price_unit': 0.0}, 'domain':{'product_uom':[]}}
69 part = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
71 context.update({'lang': part.lang})
74 res = self.pool.get('product.product').browse(cr, uid, product, context=context)
75 result.update({'name':res.partner_ref or False,'uom_id': uom_id or res.uom_id.id or False, 'price_unit': res.list_price or 0.0})
77 result['name'] += '\n'+res.description
79 res_final = {'value':result}
80 if result['uom_id'] != res.uom_id.id:
81 selected_uom = uom_obj.browse(cr, uid, result['uom_id'], context=context)
82 new_price = uom_obj._compute_price(cr, uid, res.uom_id.id, res_final['value']['price_unit'], result['uom_id'])
83 res_final['value']['price_unit'] = new_price
87 class account_analytic_account(osv.osv):
88 _name = "account.analytic.account"
89 _inherit = "account.analytic.account"
91 def _analysis_all(self, cr, uid, ids, fields, arg, context=None):
93 res = dict([(i, {}) for i in ids])
94 parent_ids = tuple(ids) #We don't want consolidation for each of these fields because those complex computation is resource-greedy.
95 accounts = self.browse(cr, uid, ids, context=context)
99 cr.execute('SELECT MAX(id) FROM res_users')
100 max_user = cr.fetchone()[0]
102 cr.execute('SELECT DISTINCT("user") FROM account_analytic_analysis_summary_user ' \
103 'WHERE account_id IN %s AND unit_amount <> 0.0', (parent_ids,))
104 result = cr.fetchall()
108 res[id][f] = [int((id * max_user) + x[0]) for x in result]
109 elif f == 'month_ids':
111 cr.execute('SELECT DISTINCT(month_id) FROM account_analytic_analysis_summary_month ' \
112 'WHERE account_id IN %s AND unit_amount <> 0.0', (parent_ids,))
113 result = cr.fetchall()
117 res[id][f] = [int(id * 1000000 + int(x[0])) for x in result]
118 elif f == 'last_worked_invoiced_date':
122 cr.execute("SELECT account_analytic_line.account_id, MAX(date) \
123 FROM account_analytic_line \
124 WHERE account_id IN %s \
125 AND invoice_id IS NOT NULL \
126 GROUP BY account_analytic_line.account_id;", (parent_ids,))
127 for account_id, sum in cr.fetchall():
128 if account_id not in res:
130 res[account_id][f] = sum
131 elif f == 'ca_to_invoice':
135 for account in accounts:
137 SELECT product_id, sum(amount), user_id, to_invoice, sum(unit_amount), product_uom_id, line.name
138 FROM account_analytic_line line
139 LEFT JOIN account_analytic_journal journal ON (journal.id = line.journal_id)
140 WHERE account_id = %s
141 AND journal.type != 'purchase'
142 AND invoice_id IS NULL
143 AND to_invoice IS NOT NULL
144 GROUP BY product_id, user_id, to_invoice, product_uom_id, line.name""", (account.id,))
146 res[account.id][f] = 0.0
147 for product_id, price, user_id, factor_id, qty, uom, line_name in cr.fetchall():
150 price = self.pool.get('account.analytic.line')._get_invoice_price(cr, uid, account, product_id, user_id, qty, context)
151 factor = self.pool.get('hr_timesheet_invoice.factor').browse(cr, uid, factor_id, context=context)
152 res[account.id][f] += price * qty * (100-factor.factor or 0.0) / 100.0
154 # sum both result on account_id
156 res[id][f] = round(res.get(id, {}).get(f, 0.0), dp) + round(res2.get(id, 0.0), 2)
157 elif f == 'last_invoice_date':
161 cr.execute ("SELECT account_analytic_line.account_id, \
162 DATE(MAX(account_invoice.date_invoice)) \
163 FROM account_analytic_line \
164 JOIN account_invoice \
165 ON account_analytic_line.invoice_id = account_invoice.id \
166 WHERE account_analytic_line.account_id IN %s \
167 AND account_analytic_line.invoice_id IS NOT NULL \
168 GROUP BY account_analytic_line.account_id",(parent_ids,))
169 for account_id, lid in cr.fetchall():
170 res[account_id][f] = lid
171 elif f == 'last_worked_date':
175 cr.execute("SELECT account_analytic_line.account_id, MAX(date) \
176 FROM account_analytic_line \
177 WHERE account_id IN %s \
178 AND invoice_id IS NULL \
179 GROUP BY account_analytic_line.account_id",(parent_ids,))
180 for account_id, lwd in cr.fetchall():
181 if account_id not in res:
183 res[account_id][f] = lwd
184 elif f == 'hours_qtt_non_invoiced':
188 cr.execute("SELECT account_analytic_line.account_id, COALESCE(SUM(unit_amount), 0.0) \
189 FROM account_analytic_line \
190 JOIN account_analytic_journal \
191 ON account_analytic_line.journal_id = account_analytic_journal.id \
192 WHERE account_analytic_line.account_id IN %s \
193 AND account_analytic_journal.type='general' \
194 AND invoice_id IS NULL \
195 AND to_invoice IS NOT NULL \
196 GROUP BY account_analytic_line.account_id;",(parent_ids,))
197 for account_id, sua in cr.fetchall():
198 if account_id not in res:
200 res[account_id][f] = round(sua, dp)
202 res[id][f] = round(res[id][f], dp)
203 elif f == 'hours_quantity':
207 cr.execute("SELECT account_analytic_line.account_id, COALESCE(SUM(unit_amount), 0.0) \
208 FROM account_analytic_line \
209 JOIN account_analytic_journal \
210 ON account_analytic_line.journal_id = account_analytic_journal.id \
211 WHERE account_analytic_line.account_id IN %s \
212 AND account_analytic_journal.type='general' \
213 GROUP BY account_analytic_line.account_id",(parent_ids,))
215 for account_id, hq in ff:
216 if account_id not in res:
218 res[account_id][f] = round(hq, dp)
220 res[id][f] = round(res[id][f], dp)
221 elif f == 'ca_theorical':
222 # TODO Take care of pricelist and purchase !
226 # This computation doesn't take care of pricelist !
227 # Just consider list_price
229 cr.execute("""SELECT account_analytic_line.account_id AS account_id, \
230 COALESCE(SUM((account_analytic_line.unit_amount * pt.list_price) \
231 - (account_analytic_line.unit_amount * pt.list_price \
232 * hr.factor)), 0.0) AS somme
233 FROM account_analytic_line \
234 LEFT JOIN account_analytic_journal \
235 ON (account_analytic_line.journal_id = account_analytic_journal.id) \
236 JOIN product_product pp \
237 ON (account_analytic_line.product_id = pp.id) \
238 JOIN product_template pt \
239 ON (pp.product_tmpl_id = pt.id) \
240 JOIN account_analytic_account a \
241 ON (a.id=account_analytic_line.account_id) \
242 JOIN hr_timesheet_invoice_factor hr \
243 ON (hr.id=a.to_invoice) \
244 WHERE account_analytic_line.account_id IN %s \
245 AND a.to_invoice IS NOT NULL \
246 AND account_analytic_journal.type IN ('purchase', 'general')
247 GROUP BY account_analytic_line.account_id""",(parent_ids,))
248 for account_id, sum in cr.fetchall():
249 res[account_id][f] = round(sum, dp)
252 def _ca_invoiced_calc(self, cr, uid, ids, name, arg, context=None):
255 child_ids = tuple(ids) #We don't want consolidation for each of these fields because those complex computation is resource-greedy.
262 #Search all invoice lines not in cancelled state that refer to this analytic account
263 inv_line_obj = self.pool.get("account.invoice.line")
264 inv_lines = inv_line_obj.search(cr, uid, ['&', ('account_analytic_id', 'in', child_ids), ('invoice_id.state', '!=', 'cancel')], context=context)
265 for line in inv_line_obj.browse(cr, uid, inv_lines, context=context):
266 res[line.account_analytic_id.id] += line.price_subtotal
267 for acc in self.browse(cr, uid, res.keys(), context=context):
268 res[acc.id] = res[acc.id] - (acc.timesheet_ca_invoiced or 0.0)
273 def _total_cost_calc(self, cr, uid, ids, name, arg, context=None):
276 child_ids = tuple(ids) #We don't want consolidation for each of these fields because those complex computation is resource-greedy.
282 cr.execute("""SELECT account_analytic_line.account_id, COALESCE(SUM(amount), 0.0) \
283 FROM account_analytic_line \
284 JOIN account_analytic_journal \
285 ON account_analytic_line.journal_id = account_analytic_journal.id \
286 WHERE account_analytic_line.account_id IN %s \
288 GROUP BY account_analytic_line.account_id""",(child_ids,))
289 for account_id, sum in cr.fetchall():
290 res[account_id] = round(sum,2)
294 def _remaining_hours_calc(self, cr, uid, ids, name, arg, context=None):
296 for account in self.browse(cr, uid, ids, context=context):
297 if account.quantity_max != 0:
298 res[account.id] = account.quantity_max - account.hours_quantity
300 res[account.id] = 0.0
302 res[id] = round(res.get(id, 0.0),2)
305 def _remaining_hours_to_invoice_calc(self, cr, uid, ids, name, arg, context=None):
307 for account in self.browse(cr, uid, ids, context=context):
308 res[account.id] = max(account.hours_qtt_est - account.timesheet_ca_invoiced, account.ca_to_invoice)
311 def _hours_qtt_invoiced_calc(self, cr, uid, ids, name, arg, context=None):
313 for account in self.browse(cr, uid, ids, context=context):
314 res[account.id] = account.hours_quantity - account.hours_qtt_non_invoiced
315 if res[account.id] < 0:
316 res[account.id] = 0.0
318 res[id] = round(res.get(id, 0.0),2)
321 def _revenue_per_hour_calc(self, cr, uid, ids, name, arg, context=None):
323 for account in self.browse(cr, uid, ids, context=context):
324 if account.hours_qtt_invoiced == 0:
327 res[account.id] = account.ca_invoiced / account.hours_qtt_invoiced
329 res[id] = round(res.get(id, 0.0),2)
332 def _real_margin_rate_calc(self, cr, uid, ids, name, arg, context=None):
334 for account in self.browse(cr, uid, ids, context=context):
335 if account.ca_invoiced == 0:
337 elif account.total_cost != 0.0:
338 res[account.id] = -(account.real_margin / account.total_cost) * 100
340 res[account.id] = 0.0
342 res[id] = round(res.get(id, 0.0),2)
345 def _fix_price_to_invoice_calc(self, cr, uid, ids, name, arg, context=None):
346 sale_obj = self.pool.get('sale.order')
348 for account in self.browse(cr, uid, ids, context=context):
349 res[account.id] = 0.0
350 sale_ids = sale_obj.search(cr, uid, [('project_id','=', account.id), ('state', '=', 'manual')], context=context)
351 for sale in sale_obj.browse(cr, uid, sale_ids, context=context):
352 res[account.id] += sale.amount_untaxed
353 for invoice in sale.invoice_ids:
354 if invoice.state != 'cancel':
355 res[account.id] -= invoice.amount_untaxed
358 def _timesheet_ca_invoiced_calc(self, cr, uid, ids, name, arg, context=None):
359 lines_obj = self.pool.get('account.analytic.line')
362 for account in self.browse(cr, uid, ids, context=context):
363 res[account.id] = 0.0
364 line_ids = lines_obj.search(cr, uid, [('account_id','=', account.id), ('invoice_id','!=',False), ('to_invoice','!=', False), ('journal_id.type', '=', 'general')], context=context)
365 for line in lines_obj.browse(cr, uid, line_ids, context=context):
366 if line.invoice_id not in inv_ids:
367 inv_ids.append(line.invoice_id)
368 res[account.id] += line.invoice_id.amount_untaxed
371 def _remaining_ca_calc(self, cr, uid, ids, name, arg, context=None):
373 for account in self.browse(cr, uid, ids, context=context):
374 res[account.id] = max(account.amount_max - account.ca_invoiced, account.fix_price_to_invoice)
377 def _real_margin_calc(self, cr, uid, ids, name, arg, context=None):
379 for account in self.browse(cr, uid, ids, context=context):
380 res[account.id] = account.ca_invoiced + account.total_cost
382 res[id] = round(res.get(id, 0.0),2)
385 def _theorical_margin_calc(self, cr, uid, ids, name, arg, context=None):
387 for account in self.browse(cr, uid, ids, context=context):
388 res[account.id] = account.ca_theorical + account.total_cost
390 res[id] = round(res.get(id, 0.0),2)
393 def _is_overdue_quantity(self, cr, uid, ids, fieldnames, args, context=None):
394 result = dict.fromkeys(ids, 0)
395 for record in self.browse(cr, uid, ids, context=context):
396 if record.quantity_max > 0.0:
397 result[record.id] = int(record.hours_quantity >= record.quantity_max)
399 result[record.id] = 0
402 def _get_analytic_account(self, cr, uid, ids, context=None):
404 for line in self.pool.get('account.analytic.line').browse(cr, uid, ids, context=context):
405 result.add(line.account_id.id)
408 def _get_total_estimation(self, account):
410 if account.fix_price_invoices:
411 tot_est += account.amount_max
412 if account.invoice_on_timesheets:
413 tot_est += account.hours_qtt_est
416 def _get_total_invoiced(self, account):
418 if account.fix_price_invoices:
419 total_invoiced += account.ca_invoiced
420 if account.invoice_on_timesheets:
421 total_invoiced += account.timesheet_ca_invoiced
422 return total_invoiced
424 def _get_total_remaining(self, account):
425 total_remaining = 0.0
426 if account.fix_price_invoices:
427 total_remaining += account.remaining_ca
428 if account.invoice_on_timesheets:
429 total_remaining += account.remaining_hours_to_invoice
430 return total_remaining
432 def _get_total_toinvoice(self, account):
433 total_toinvoice = 0.0
434 if account.fix_price_invoices:
435 total_toinvoice += account.fix_price_to_invoice
436 if account.invoice_on_timesheets:
437 total_toinvoice += account.ca_to_invoice
438 return total_toinvoice
440 def _sum_of_fields(self, cr, uid, ids, name, arg, context=None):
441 res = dict([(i, {}) for i in ids])
442 for account in self.browse(cr, uid, ids, context=context):
443 res[account.id]['est_total'] = self._get_total_estimation(account)
444 res[account.id]['invoiced_total'] = self._get_total_invoiced(account)
445 res[account.id]['remaining_total'] = self._get_total_remaining(account)
446 res[account.id]['toinvoice_total'] = self._get_total_toinvoice(account)
450 'is_overdue_quantity' : fields.function(_is_overdue_quantity, method=True, type='boolean', string='Overdue Quantity',
452 'account.analytic.line' : (_get_analytic_account, None, 20),
454 'ca_invoiced': fields.function(_ca_invoiced_calc, type='float', string='Invoiced Amount',
455 help="Total customer invoiced amount for this account.",
456 digits_compute=dp.get_precision('Account')),
457 'total_cost': fields.function(_total_cost_calc, type='float', string='Total Costs',
458 help="Total of costs for this account. It includes real costs (from invoices) and indirect costs, like time spent on timesheets.",
459 digits_compute=dp.get_precision('Account')),
460 'ca_to_invoice': fields.function(_analysis_all, multi='analytic_analysis', type='float', string='Uninvoiced Amount',
461 help="If invoice from analytic account, the remaining amount you can invoice to the customer based on the total costs.",
462 digits_compute=dp.get_precision('Account')),
463 'ca_theorical': fields.function(_analysis_all, multi='analytic_analysis', type='float', string='Theoretical Revenue',
464 help="Based on the costs you had on the project, what would have been the revenue if all these costs have been invoiced at the normal sale price provided by the pricelist.",
465 digits_compute=dp.get_precision('Account')),
466 'hours_quantity': fields.function(_analysis_all, multi='analytic_analysis', type='float', string='Total Worked Time',
467 help="Number of time you spent on the analytic account (from timesheet). It computes quantities on all journal of type 'general'."),
468 'last_invoice_date': fields.function(_analysis_all, multi='analytic_analysis', type='date', string='Last Invoice Date',
469 help="If invoice from the costs, this is the date of the latest invoiced."),
470 'last_worked_invoiced_date': fields.function(_analysis_all, multi='analytic_analysis', type='date', string='Date of Last Invoiced Cost',
471 help="If invoice from the costs, this is the date of the latest work or cost that have been invoiced."),
472 'last_worked_date': fields.function(_analysis_all, multi='analytic_analysis', type='date', string='Date of Last Cost/Work',
473 help="Date of the latest work done on this account."),
474 'hours_qtt_non_invoiced': fields.function(_analysis_all, multi='analytic_analysis', type='float', string='Uninvoiced Time',
475 help="Number of time (hours/days) (from journal of type 'general') that can be invoiced if you invoice based on analytic account."),
476 'hours_qtt_invoiced': fields.function(_hours_qtt_invoiced_calc, type='float', string='Invoiced Time',
477 help="Number of time (hours/days) that can be invoiced plus those that already have been invoiced."),
478 'remaining_hours': fields.function(_remaining_hours_calc, type='float', string='Remaining Time',
479 help="Computed using the formula: Maximum Time - Total Worked Time"),
480 'remaining_hours_to_invoice': fields.function(_remaining_hours_to_invoice_calc, type='float', string='Remaining Time',
481 help="Computed using the formula: Expected on timesheets - Total invoiced on timesheets"),
482 'fix_price_to_invoice': fields.function(_fix_price_to_invoice_calc, type='float', string='Remaining Time',
483 help="Sum of quotations for this contract."),
484 'timesheet_ca_invoiced': fields.function(_timesheet_ca_invoiced_calc, type='float', string='Remaining Time',
485 help="Sum of timesheet lines invoiced for this contract."),
486 'remaining_ca': fields.function(_remaining_ca_calc, type='float', string='Remaining Revenue',
487 help="Computed using the formula: Max Invoice Price - Invoiced Amount.",
488 digits_compute=dp.get_precision('Account')),
489 'revenue_per_hour': fields.function(_revenue_per_hour_calc, type='float', string='Revenue per Time (real)',
490 help="Computed using the formula: Invoiced Amount / Total Time",
491 digits_compute=dp.get_precision('Account')),
492 'real_margin': fields.function(_real_margin_calc, type='float', string='Real Margin',
493 help="Computed using the formula: Invoiced Amount - Total Costs.",
494 digits_compute=dp.get_precision('Account')),
495 'theorical_margin': fields.function(_theorical_margin_calc, type='float', string='Theoretical Margin',
496 help="Computed using the formula: Theoretical Revenue - Total Costs",
497 digits_compute=dp.get_precision('Account')),
498 'real_margin_rate': fields.function(_real_margin_rate_calc, type='float', string='Real Margin Rate (%)',
499 help="Computes using the formula: (Real Margin / Total Costs) * 100.",
500 digits_compute=dp.get_precision('Account')),
501 'fix_price_invoices' : fields.boolean('Fixed Price'),
502 'invoice_on_timesheets' : fields.boolean("On Timesheets"),
503 'month_ids': fields.function(_analysis_all, multi='analytic_analysis', type='many2many', relation='account_analytic_analysis.summary.month', string='Month'),
504 'user_ids': fields.function(_analysis_all, multi='analytic_analysis', type="many2many", relation='account_analytic_analysis.summary.user', string='User'),
505 'hours_qtt_est': fields.float('Estimation of Hours to Invoice'),
506 'est_total' : fields.function(_sum_of_fields, type="float",multi="sum_of_all", string="Total Estimation"),
507 'invoiced_total' : fields.function(_sum_of_fields, type="float",multi="sum_of_all", string="Total Invoiced"),
508 'remaining_total' : fields.function(_sum_of_fields, type="float",multi="sum_of_all", string="Total Remaining", help="Expectation of remaining income for this contract. Computed as the sum of remaining subtotals which, in turn, are computed as the maximum between '(Estimation - Invoiced)' and 'To Invoice' amounts"),
509 'toinvoice_total' : fields.function(_sum_of_fields, type="float",multi="sum_of_all", string="Total to Invoice", help=" Sum of everything that could be invoiced for this contract."),
510 'recurring_invoice_line_ids': fields.one2many('account.analytic.invoice.line', 'analytic_account_id', 'Invoice Lines'),
511 'recurring_invoices' : fields.boolean('Generate recurring invoices automatically'),
512 'recurring_rule_type': fields.selection([
514 ('weekly', 'Week(s)'),
515 ('monthly', 'Month(s)'),
516 ('yearly', 'Year(s)'),
517 ], 'Recurrency', help="Invoice automatically repeat at specified interval"),
518 'recurring_interval': fields.integer('Repeat Every', help="Repeat every (Days/Week/Month/Year)"),
519 'recurring_next_date': fields.date('Date of Next Invoice'),
523 'recurring_interval': 1,
524 'recurring_next_date': lambda *a: time.strftime('%Y-%m-%d'),
525 'recurring_rule_type':'monthly'
528 def open_sale_order_lines(self,cr,uid,ids,context=None):
531 sale_ids = self.pool.get('sale.order').search(cr,uid,[('project_id','=',context.get('search_default_project_id',False)),('partner_id','in',context.get('search_default_partner_id',False))])
532 names = [record.name for record in self.browse(cr, uid, ids, context=context)]
533 name = _('Sales Order Lines to Invoice of %s') % ','.join(names)
535 'type': 'ir.actions.act_window',
538 'view_mode': 'tree,form',
540 'domain' : [('order_id','in',sale_ids)],
541 'res_model': 'sale.order.line',
545 def on_change_template(self, cr, uid, ids, template_id, context=None):
548 obj_analytic_line = self.pool.get('account.analytic.invoice.line')
549 res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, context=context)
551 template = self.browse(cr, uid, template_id, context=context)
552 invoice_line_ids = []
553 for x in template.recurring_invoice_line_ids:
554 invoice_line_ids.append((0, 0, {
555 'product_id': x.product_id.id,
556 'uom_id': x.uom_id.id,
558 'quantity': x.quantity,
559 'price_unit': x.price_unit,
560 'analytic_account_id': x.analytic_account_id and x.analytic_account_id.id or False,
562 res['value']['fix_price_invoices'] = template.fix_price_invoices
563 res['value']['invoice_on_timesheets'] = template.invoice_on_timesheets
564 res['value']['hours_qtt_est'] = template.hours_qtt_est
565 res['value']['amount_max'] = template.amount_max
566 res['value']['to_invoice'] = template.to_invoice.id
567 res['value']['pricelist_id'] = template.pricelist_id.id
568 res['value']['recurring_invoices'] = template.recurring_invoices
569 res['value']['recurring_interval'] = template.recurring_interval
570 res['value']['recurring_rule_type'] = template.recurring_rule_type
571 res['value']['recurring_invoice_line_ids'] = invoice_line_ids
574 def onchange_recurring_invoices(self, cr, uid, ids, recurring_invoices, date_start=False, context=None):
576 if date_start and recurring_invoices:
577 value = {'value': {'recurring_next_date': date_start}}
580 def cron_account_analytic_account(self, cr, uid, context=None):
585 def fill_remind(key, domain, write_pending=False):
587 ('type', '=', 'contract'),
588 ('partner_id', '!=', False),
589 ('manager_id', '!=', False),
590 ('manager_id.email', '!=', False),
592 base_domain.extend(domain)
594 accounts_ids = self.search(cr, uid, base_domain, context=context, order='name asc')
595 accounts = self.browse(cr, uid, accounts_ids, context=context)
596 for account in accounts:
598 account.write({'state' : 'pending'}, context=context)
599 remind_user = remind.setdefault(account.manager_id.id, {})
600 remind_type = remind_user.setdefault(key, {})
601 remind_partner = remind_type.setdefault(account.partner_id, []).append(account)
604 fill_remind("old", [('state', 'in', ['pending'])])
607 fill_remind("new", [('state', 'in', ['draft', 'open']), '|', '&', ('date', '!=', False), ('date', '<=', time.strftime('%Y-%m-%d')), ('is_overdue_quantity', '=', True)], True)
609 # Expires in less than 30 days
610 fill_remind("future", [('state', 'in', ['draft', 'open']), ('date', '!=', False), ('date', '<', (datetime.datetime.now() + datetime.timedelta(30)).strftime("%Y-%m-%d"))])
612 context['base_url'] = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
613 context['action_id'] = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account_analytic_analysis', 'action_account_analytic_overdue_all')[1]
614 template_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account_analytic_analysis', 'account_analytic_cron_email_template')[1]
615 for user_id, data in remind.items():
616 context["data"] = data
617 _logger.debug("Sending reminder to uid %s", user_id)
618 self.pool.get('email.template').send_mail(cr, uid, template_id, user_id, force_send=True, context=context)
622 def onchange_invoice_on_timesheets(self, cr, uid, ids, invoice_on_timesheets, context=None):
623 if not invoice_on_timesheets:
625 result = {'value': {'use_timesheets': True}}
627 to_invoice = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'hr_timesheet_invoice', 'timesheet_invoice_factor1')
628 result['value']['to_invoice'] = to_invoice[1]
634 def hr_to_invoice_timesheets(self, cr, uid, ids, context=None):
635 domain = [('invoice_id','=',False),('to_invoice','!=',False), ('journal_id.type', '=', 'general'), ('account_id', 'in', ids)]
636 names = [record.name for record in self.browse(cr, uid, ids, context=context)]
637 name = _('Timesheets to Invoice of %s') % ','.join(names)
639 'type': 'ir.actions.act_window',
642 'view_mode': 'tree,form',
644 'res_model': 'account.analytic.line',
648 def _prepare_invoice(self, cr, uid, contract, context=None):
649 context = context or {}
651 inv_obj = self.pool.get('account.invoice')
652 journal_obj = self.pool.get('account.journal')
653 fpos_obj = self.pool.get('account.fiscal.position')
655 if not contract.partner_id:
656 raise osv.except_osv(_('No Customer Defined!'),_("You must first select a Customer for Contract %s!") % contract.name )
658 fpos = contract.partner_id.property_account_position or False
659 journal_ids = journal_obj.search(cr, uid, [('type', '=','sale'),('company_id', '=', contract.company_id.id or False)], limit=1)
661 raise osv.except_osv(_('Error!'),
662 _('Please define a sale journal for the company "%s".') % (contract.company_id.name or '', ))
664 partner_payment_term = contract.partner_id.property_payment_term and contract.partner_id.property_payment_term.id or False
668 'reference': contract.code or False,
669 'account_id': contract.partner_id.property_account_receivable.id,
670 'type': 'out_invoice',
671 'partner_id': contract.partner_id.id,
672 'currency_id': contract.partner_id.property_product_pricelist.id or False,
673 'journal_id': len(journal_ids) and journal_ids[0] or False,
674 'date_invoice': contract.recurring_next_date,
675 'origin': contract.name,
676 'fiscal_position': fpos and fpos.id,
677 'payment_term': partner_payment_term,
678 'company_id': contract.company_id.id or False,
680 invoice_id = inv_obj.create(cr, uid, inv_data, context=context)
682 for line in contract.recurring_invoice_line_ids:
684 res = line.product_id
685 account_id = res.property_account_income.id
687 account_id = res.categ_id.property_account_income_categ.id
688 account_id = fpos_obj.map_account(cr, uid, fpos, account_id)
690 taxes = res.taxes_id or False
691 tax_id = fpos_obj.map_tax(cr, uid, fpos, taxes)
693 invoice_line_vals = {
695 'account_id': account_id,
696 'account_analytic_id': contract.id,
697 'price_unit': line.price_unit or 0.0,
698 'quantity': line.quantity,
699 'uos_id': line.uom_id.id or False,
700 'product_id': line.product_id.id or False,
701 'invoice_id' : invoice_id,
702 'invoice_line_tax_id': [(6, 0, tax_id)],
704 self.pool.get('account.invoice.line').create(cr, uid, invoice_line_vals, context=context)
706 inv_obj.button_compute(cr, uid, [invoice_id], context=context)
709 def recurring_create_invoice(self, cr, uid, automatic=False, context=None):
710 context = context or {}
711 current_date = time.strftime('%Y-%m-%d')
713 contract_ids = self.search(cr, uid, [('recurring_next_date','<=', current_date), ('state','=', 'open'), ('recurring_invoices','=', True)])
714 for contract in self.browse(cr, uid, contract_ids, context=context):
715 invoice_id = self._prepare_invoice(cr, uid, contract, context=context)
717 next_date = datetime.datetime.strptime(contract.recurring_next_date or current_date, "%Y-%m-%d")
718 interval = contract.recurring_interval
719 if contract.recurring_rule_type == 'daily':
720 new_date = next_date+relativedelta(days=+interval)
721 elif contract.recurring_rule_type == 'weekly':
722 new_date = next_date+relativedelta(weeks=+interval)
724 new_date = next_date+relativedelta(months=+interval)
725 self.write(cr, uid, [contract.id], {'recurring_next_date': new_date.strftime('%Y-%m-%d')}, context=context)
728 class account_analytic_account_summary_user(osv.osv):
729 _name = "account_analytic_analysis.summary.user"
730 _description = "Hours Summary by User"
735 def _unit_amount(self, cr, uid, ids, name, arg, context=None):
737 account_obj = self.pool.get('account.analytic.account')
738 cr.execute('SELECT MAX(id) FROM res_users')
739 max_user = cr.fetchone()[0]
740 account_ids = [int(str(x/max_user - (x%max_user == 0 and 1 or 0))) for x in ids]
741 user_ids = [int(str(x-((x/max_user - (x%max_user == 0 and 1 or 0)) *max_user))) for x in ids]
742 parent_ids = tuple(account_ids) #We don't want consolidation for each of these fields because those complex computation is resource-greedy.
744 cr.execute('SELECT id, unit_amount ' \
745 'FROM account_analytic_analysis_summary_user ' \
746 'WHERE account_id IN %s ' \
747 'AND "user" IN %s',(parent_ids, tuple(user_ids),))
748 for sum_id, unit_amount in cr.fetchall():
749 res[sum_id] = unit_amount
751 res[id] = round(res.get(id, 0.0), 2)
755 'account_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True),
756 'unit_amount': fields.float('Total Time'),
757 'user': fields.many2one('res.users', 'User'),
761 openerp.tools.sql.drop_view_if_exists(cr, 'account_analytic_analysis_summary_user')
762 cr.execute('''CREATE OR REPLACE VIEW account_analytic_analysis_summary_user AS (
764 (select max(id) as max_user from res_users)
767 l.account_id AS account_id,
768 coalesce(l.user_id, 0) AS user_id,
769 SUM(l.unit_amount) AS unit_amount
770 FROM account_analytic_line AS l,
771 account_analytic_journal AS j
772 WHERE (j.type = 'general' ) and (j.id=l.journal_id)
773 GROUP BY l.account_id, l.user_id
775 select (lu.account_id * mu.max_user) + lu.user_id as id,
776 lu.account_id as account_id,
777 lu.user_id as "user",
781 class account_analytic_account_summary_month(osv.osv):
782 _name = "account_analytic_analysis.summary.month"
783 _description = "Hours summary by month"
788 'account_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True),
789 'unit_amount': fields.float('Total Time'),
790 'month': fields.char('Month', size=32, readonly=True),
794 openerp.tools.sql.drop_view_if_exists(cr, 'account_analytic_analysis_summary_month')
795 cr.execute('CREATE VIEW account_analytic_analysis_summary_month AS (' \
797 '(TO_NUMBER(TO_CHAR(d.month, \'YYYYMM\'), \'999999\') + (d.account_id * 1000000::bigint))::bigint AS id, ' \
798 'd.account_id AS account_id, ' \
799 'TO_CHAR(d.month, \'Mon YYYY\') AS month, ' \
800 'TO_NUMBER(TO_CHAR(d.month, \'YYYYMM\'), \'999999\') AS month_id, ' \
801 'COALESCE(SUM(l.unit_amount), 0.0) AS unit_amount ' \
808 'a.id AS account_id, ' \
809 'l.month AS month ' \
812 'DATE_TRUNC(\'month\', l.date) AS month ' \
813 'FROM account_analytic_line AS l, ' \
814 'account_analytic_journal AS j ' \
815 'WHERE j.type = \'general\' ' \
816 'GROUP BY DATE_TRUNC(\'month\', l.date) ' \
818 'account_analytic_account AS a ' \
819 'GROUP BY l.month, a.id ' \
821 'GROUP BY d2.account_id, d2.month ' \
825 'l.account_id AS account_id, ' \
826 'DATE_TRUNC(\'month\', l.date) AS month, ' \
827 'SUM(l.unit_amount) AS unit_amount ' \
828 'FROM account_analytic_line AS l, ' \
829 'account_analytic_journal AS j ' \
830 'WHERE (j.type = \'general\') and (j.id=l.journal_id) ' \
831 'GROUP BY l.account_id, DATE_TRUNC(\'month\', l.date) ' \
834 'd.account_id = l.account_id ' \
835 'AND d.month = l.month' \
837 'GROUP BY d.month, d.account_id ' \
840 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: