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),
453 'account.analytic.account': (lambda self, cr, uid, ids, c=None: ids, ['quantity_max'], 10),
455 'ca_invoiced': fields.function(_ca_invoiced_calc, type='float', string='Invoiced Amount',
456 help="Total customer invoiced amount for this account.",
457 digits_compute=dp.get_precision('Account')),
458 'total_cost': fields.function(_total_cost_calc, type='float', string='Total Costs',
459 help="Total of costs for this account. It includes real costs (from invoices) and indirect costs, like time spent on timesheets.",
460 digits_compute=dp.get_precision('Account')),
461 'ca_to_invoice': fields.function(_analysis_all, multi='analytic_analysis', type='float', string='Uninvoiced Amount',
462 help="If invoice from analytic account, the remaining amount you can invoice to the customer based on the total costs.",
463 digits_compute=dp.get_precision('Account')),
464 'ca_theorical': fields.function(_analysis_all, multi='analytic_analysis', type='float', string='Theoretical Revenue',
465 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.",
466 digits_compute=dp.get_precision('Account')),
467 'hours_quantity': fields.function(_analysis_all, multi='analytic_analysis', type='float', string='Total Worked Time',
468 help="Number of time you spent on the analytic account (from timesheet). It computes quantities on all journal of type 'general'."),
469 'last_invoice_date': fields.function(_analysis_all, multi='analytic_analysis', type='date', string='Last Invoice Date',
470 help="If invoice from the costs, this is the date of the latest invoiced."),
471 'last_worked_invoiced_date': fields.function(_analysis_all, multi='analytic_analysis', type='date', string='Date of Last Invoiced Cost',
472 help="If invoice from the costs, this is the date of the latest work or cost that have been invoiced."),
473 'last_worked_date': fields.function(_analysis_all, multi='analytic_analysis', type='date', string='Date of Last Cost/Work',
474 help="Date of the latest work done on this account."),
475 'hours_qtt_non_invoiced': fields.function(_analysis_all, multi='analytic_analysis', type='float', string='Uninvoiced Time',
476 help="Number of time (hours/days) (from journal of type 'general') that can be invoiced if you invoice based on analytic account."),
477 'hours_qtt_invoiced': fields.function(_hours_qtt_invoiced_calc, type='float', string='Invoiced Time',
478 help="Number of time (hours/days) that can be invoiced plus those that already have been invoiced."),
479 'remaining_hours': fields.function(_remaining_hours_calc, type='float', string='Remaining Time',
480 help="Computed using the formula: Maximum Time - Total Worked Time"),
481 'remaining_hours_to_invoice': fields.function(_remaining_hours_to_invoice_calc, type='float', string='Remaining Time',
482 help="Computed using the formula: Expected on timesheets - Total invoiced on timesheets"),
483 'fix_price_to_invoice': fields.function(_fix_price_to_invoice_calc, type='float', string='Remaining Time',
484 help="Sum of quotations for this contract."),
485 'timesheet_ca_invoiced': fields.function(_timesheet_ca_invoiced_calc, type='float', string='Remaining Time',
486 help="Sum of timesheet lines invoiced for this contract."),
487 'remaining_ca': fields.function(_remaining_ca_calc, type='float', string='Remaining Revenue',
488 help="Computed using the formula: Max Invoice Price - Invoiced Amount.",
489 digits_compute=dp.get_precision('Account')),
490 'revenue_per_hour': fields.function(_revenue_per_hour_calc, type='float', string='Revenue per Time (real)',
491 help="Computed using the formula: Invoiced Amount / Total Time",
492 digits_compute=dp.get_precision('Account')),
493 'real_margin': fields.function(_real_margin_calc, type='float', string='Real Margin',
494 help="Computed using the formula: Invoiced Amount - Total Costs.",
495 digits_compute=dp.get_precision('Account')),
496 'theorical_margin': fields.function(_theorical_margin_calc, type='float', string='Theoretical Margin',
497 help="Computed using the formula: Theoretical Revenue - Total Costs",
498 digits_compute=dp.get_precision('Account')),
499 'real_margin_rate': fields.function(_real_margin_rate_calc, type='float', string='Real Margin Rate (%)',
500 help="Computes using the formula: (Real Margin / Total Costs) * 100.",
501 digits_compute=dp.get_precision('Account')),
502 'fix_price_invoices' : fields.boolean('Fixed Price'),
503 'invoice_on_timesheets' : fields.boolean("On Timesheets"),
504 'month_ids': fields.function(_analysis_all, multi='analytic_analysis', type='many2many', relation='account_analytic_analysis.summary.month', string='Month'),
505 'user_ids': fields.function(_analysis_all, multi='analytic_analysis', type="many2many", relation='account_analytic_analysis.summary.user', string='User'),
506 'hours_qtt_est': fields.float('Estimation of Hours to Invoice'),
507 'est_total' : fields.function(_sum_of_fields, type="float",multi="sum_of_all", string="Total Estimation"),
508 'invoiced_total' : fields.function(_sum_of_fields, type="float",multi="sum_of_all", string="Total Invoiced"),
509 '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"),
510 '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."),
511 'recurring_invoice_line_ids': fields.one2many('account.analytic.invoice.line', 'analytic_account_id', 'Invoice Lines'),
512 'recurring_invoices' : fields.boolean('Generate recurring invoices automatically'),
513 'recurring_rule_type': fields.selection([
515 ('weekly', 'Week(s)'),
516 ('monthly', 'Month(s)'),
517 ('yearly', 'Year(s)'),
518 ], 'Recurrency', help="Invoice automatically repeat at specified interval"),
519 'recurring_interval': fields.integer('Repeat Every', help="Repeat every (Days/Week/Month/Year)"),
520 'recurring_next_date': fields.date('Date of Next Invoice'),
524 'recurring_interval': 1,
525 'recurring_next_date': lambda *a: time.strftime('%Y-%m-%d'),
526 'recurring_rule_type':'monthly'
529 def open_sale_order_lines(self,cr,uid,ids,context=None):
532 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))])
533 names = [record.name for record in self.browse(cr, uid, ids, context=context)]
534 name = _('Sales Order Lines to Invoice of %s') % ','.join(names)
536 'type': 'ir.actions.act_window',
539 'view_mode': 'tree,form',
541 'domain' : [('order_id','in',sale_ids)],
542 'res_model': 'sale.order.line',
546 def on_change_template(self, cr, uid, ids, template_id, context=None):
549 obj_analytic_line = self.pool.get('account.analytic.invoice.line')
550 res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, context=context)
552 template = self.browse(cr, uid, template_id, context=context)
553 invoice_line_ids = []
554 for x in template.recurring_invoice_line_ids:
555 invoice_line_ids.append((0, 0, {
556 'product_id': x.product_id.id,
557 'uom_id': x.uom_id.id,
559 'quantity': x.quantity,
560 'price_unit': x.price_unit,
561 'analytic_account_id': x.analytic_account_id and x.analytic_account_id.id or False,
563 res['value']['fix_price_invoices'] = template.fix_price_invoices
564 res['value']['invoice_on_timesheets'] = template.invoice_on_timesheets
565 res['value']['hours_qtt_est'] = template.hours_qtt_est
566 res['value']['amount_max'] = template.amount_max
567 res['value']['to_invoice'] = template.to_invoice.id
568 res['value']['pricelist_id'] = template.pricelist_id.id
569 res['value']['recurring_invoices'] = template.recurring_invoices
570 res['value']['recurring_interval'] = template.recurring_interval
571 res['value']['recurring_rule_type'] = template.recurring_rule_type
572 res['value']['recurring_invoice_line_ids'] = invoice_line_ids
575 def onchange_recurring_invoices(self, cr, uid, ids, recurring_invoices, date_start=False, context=None):
577 if date_start and recurring_invoices:
578 value = {'value': {'recurring_next_date': date_start}}
581 def cron_account_analytic_account(self, cr, uid, context=None):
586 def fill_remind(key, domain, write_pending=False):
588 ('type', '=', 'contract'),
589 ('partner_id', '!=', False),
590 ('manager_id', '!=', False),
591 ('manager_id.email', '!=', False),
593 base_domain.extend(domain)
595 accounts_ids = self.search(cr, uid, base_domain, context=context, order='name asc')
596 accounts = self.browse(cr, uid, accounts_ids, context=context)
597 for account in accounts:
599 account.write({'state' : 'pending'}, context=context)
600 remind_user = remind.setdefault(account.manager_id.id, {})
601 remind_type = remind_user.setdefault(key, {})
602 remind_partner = remind_type.setdefault(account.partner_id, []).append(account)
605 fill_remind("old", [('state', 'in', ['pending'])])
608 fill_remind("new", [('state', 'in', ['draft', 'open']), '|', '&', ('date', '!=', False), ('date', '<=', time.strftime('%Y-%m-%d')), ('is_overdue_quantity', '=', True)], True)
610 # Expires in less than 30 days
611 fill_remind("future", [('state', 'in', ['draft', 'open']), ('date', '!=', False), ('date', '<', (datetime.datetime.now() + datetime.timedelta(30)).strftime("%Y-%m-%d"))])
613 context['base_url'] = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
614 context['action_id'] = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account_analytic_analysis', 'action_account_analytic_overdue_all')[1]
615 template_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account_analytic_analysis', 'account_analytic_cron_email_template')[1]
616 for user_id, data in remind.items():
617 context["data"] = data
618 _logger.debug("Sending reminder to uid %s", user_id)
619 self.pool.get('email.template').send_mail(cr, uid, template_id, user_id, force_send=True, context=context)
623 def onchange_invoice_on_timesheets(self, cr, uid, ids, invoice_on_timesheets, context=None):
624 if not invoice_on_timesheets:
626 result = {'value': {'use_timesheets': True}}
628 to_invoice = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'hr_timesheet_invoice', 'timesheet_invoice_factor1')
629 result['value']['to_invoice'] = to_invoice[1]
635 def hr_to_invoice_timesheets(self, cr, uid, ids, context=None):
636 domain = [('invoice_id','=',False),('to_invoice','!=',False), ('journal_id.type', '=', 'general'), ('account_id', 'in', ids)]
637 names = [record.name for record in self.browse(cr, uid, ids, context=context)]
638 name = _('Timesheets to Invoice of %s') % ','.join(names)
640 'type': 'ir.actions.act_window',
643 'view_mode': 'tree,form',
645 'res_model': 'account.analytic.line',
649 def _prepare_invoice(self, cr, uid, contract, context=None):
650 context = context or {}
652 inv_obj = self.pool.get('account.invoice')
653 journal_obj = self.pool.get('account.journal')
654 fpos_obj = self.pool.get('account.fiscal.position')
656 if not contract.partner_id:
657 raise osv.except_osv(_('No Customer Defined!'),_("You must first select a Customer for Contract %s!") % contract.name )
659 fpos = contract.partner_id.property_account_position or False
660 journal_ids = journal_obj.search(cr, uid, [('type', '=','sale'),('company_id', '=', contract.company_id.id or False)], limit=1)
662 raise osv.except_osv(_('Error!'),
663 _('Please define a sale journal for the company "%s".') % (contract.company_id.name or '', ))
665 partner_payment_term = contract.partner_id.property_payment_term and contract.partner_id.property_payment_term.id or False
668 if contract.pricelist_id:
669 currency_id = contract.pricelist_id.currency_id.id
670 elif contract.partner_id.property_product_pricelist:
671 currency_id = contract.partner_id.property_product_pricelist.currency_id.id
672 elif contract.company_id:
673 currency_id = contract.company_id.currency_id.id
676 'reference': contract.code or False,
677 'account_id': contract.partner_id.property_account_receivable.id,
678 'type': 'out_invoice',
679 'partner_id': contract.partner_id.id,
680 'currency_id': currency_id,
681 'journal_id': len(journal_ids) and journal_ids[0] or False,
682 'date_invoice': contract.recurring_next_date,
683 'origin': contract.name,
684 'fiscal_position': fpos and fpos.id,
685 'payment_term': partner_payment_term,
686 'company_id': contract.company_id.id or False,
688 invoice_id = inv_obj.create(cr, uid, inv_data, context=context)
690 for line in contract.recurring_invoice_line_ids:
692 res = line.product_id
693 account_id = res.property_account_income.id
695 account_id = res.categ_id.property_account_income_categ.id
696 account_id = fpos_obj.map_account(cr, uid, fpos, account_id)
698 taxes = res.taxes_id or False
699 tax_id = fpos_obj.map_tax(cr, uid, fpos, taxes)
701 invoice_line_vals = {
703 'account_id': account_id,
704 'account_analytic_id': contract.id,
705 'price_unit': line.price_unit or 0.0,
706 'quantity': line.quantity,
707 'uos_id': line.uom_id.id or False,
708 'product_id': line.product_id.id or False,
709 'invoice_id' : invoice_id,
710 'invoice_line_tax_id': [(6, 0, tax_id)],
712 self.pool.get('account.invoice.line').create(cr, uid, invoice_line_vals, context=context)
714 inv_obj.button_compute(cr, uid, [invoice_id], context=context)
717 def recurring_create_invoice(self, cr, uid, automatic=False, context=None):
718 context = context or {}
719 current_date = time.strftime('%Y-%m-%d')
721 contract_ids = self.search(cr, uid, [('recurring_next_date','<=', current_date), ('state','=', 'open'), ('recurring_invoices','=', True)])
722 for contract in self.browse(cr, uid, contract_ids, context=context):
723 invoice_id = self._prepare_invoice(cr, uid, contract, context=context)
725 next_date = datetime.datetime.strptime(contract.recurring_next_date or current_date, "%Y-%m-%d")
726 interval = contract.recurring_interval
727 if contract.recurring_rule_type == 'daily':
728 new_date = next_date+relativedelta(days=+interval)
729 elif contract.recurring_rule_type == 'weekly':
730 new_date = next_date+relativedelta(weeks=+interval)
732 new_date = next_date+relativedelta(months=+interval)
733 self.write(cr, uid, [contract.id], {'recurring_next_date': new_date.strftime('%Y-%m-%d')}, context=context)
736 class account_analytic_account_summary_user(osv.osv):
737 _name = "account_analytic_analysis.summary.user"
738 _description = "Hours Summary by User"
743 def _unit_amount(self, cr, uid, ids, name, arg, context=None):
745 account_obj = self.pool.get('account.analytic.account')
746 cr.execute('SELECT MAX(id) FROM res_users')
747 max_user = cr.fetchone()[0]
748 account_ids = [int(str(x/max_user - (x%max_user == 0 and 1 or 0))) for x in ids]
749 user_ids = [int(str(x-((x/max_user - (x%max_user == 0 and 1 or 0)) *max_user))) for x in ids]
750 parent_ids = tuple(account_ids) #We don't want consolidation for each of these fields because those complex computation is resource-greedy.
752 cr.execute('SELECT id, unit_amount ' \
753 'FROM account_analytic_analysis_summary_user ' \
754 'WHERE account_id IN %s ' \
755 'AND "user" IN %s',(parent_ids, tuple(user_ids),))
756 for sum_id, unit_amount in cr.fetchall():
757 res[sum_id] = unit_amount
759 res[id] = round(res.get(id, 0.0), 2)
763 'account_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True),
764 'unit_amount': fields.float('Total Time'),
765 'user': fields.many2one('res.users', 'User'),
769 openerp.tools.sql.drop_view_if_exists(cr, 'account_analytic_analysis_summary_user')
770 cr.execute('''CREATE OR REPLACE VIEW account_analytic_analysis_summary_user AS (
772 (select max(id) as max_user from res_users)
775 l.account_id AS account_id,
776 coalesce(l.user_id, 0) AS user_id,
777 SUM(l.unit_amount) AS unit_amount
778 FROM account_analytic_line AS l,
779 account_analytic_journal AS j
780 WHERE (j.type = 'general' ) and (j.id=l.journal_id)
781 GROUP BY l.account_id, l.user_id
783 select (lu.account_id * mu.max_user) + lu.user_id as id,
784 lu.account_id as account_id,
785 lu.user_id as "user",
789 class account_analytic_account_summary_month(osv.osv):
790 _name = "account_analytic_analysis.summary.month"
791 _description = "Hours summary by month"
796 'account_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True),
797 'unit_amount': fields.float('Total Time'),
798 'month': fields.char('Month', size=32, readonly=True),
802 openerp.tools.sql.drop_view_if_exists(cr, 'account_analytic_analysis_summary_month')
803 cr.execute('CREATE VIEW account_analytic_analysis_summary_month AS (' \
805 '(TO_NUMBER(TO_CHAR(d.month, \'YYYYMM\'), \'999999\') + (d.account_id * 1000000::bigint))::bigint AS id, ' \
806 'd.account_id AS account_id, ' \
807 'TO_CHAR(d.month, \'Mon YYYY\') AS month, ' \
808 'TO_NUMBER(TO_CHAR(d.month, \'YYYYMM\'), \'999999\') AS month_id, ' \
809 'COALESCE(SUM(l.unit_amount), 0.0) AS unit_amount ' \
816 'a.id AS account_id, ' \
817 'l.month AS month ' \
820 'DATE_TRUNC(\'month\', l.date) AS month ' \
821 'FROM account_analytic_line AS l, ' \
822 'account_analytic_journal AS j ' \
823 'WHERE j.type = \'general\' ' \
824 'GROUP BY DATE_TRUNC(\'month\', l.date) ' \
826 'account_analytic_account AS a ' \
827 'GROUP BY l.month, a.id ' \
829 'GROUP BY d2.account_id, d2.month ' \
833 'l.account_id AS account_id, ' \
834 'DATE_TRUNC(\'month\', l.date) AS month, ' \
835 'SUM(l.unit_amount) AS unit_amount ' \
836 'FROM account_analytic_line AS l, ' \
837 'account_analytic_journal AS j ' \
838 'WHERE (j.type = \'general\') and (j.id=l.journal_id) ' \
839 'GROUP BY l.account_id, DATE_TRUNC(\'month\', l.date) ' \
842 'd.account_id = l.account_id ' \
843 'AND d.month = l.month' \
845 'GROUP BY d.month, d.account_id ' \
848 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: