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
27 from openerp.osv import osv, fields
28 from openerp.osv.orm import intersect, except_orm
30 from openerp.tools.translate import _
32 from openerp.addons.decimal_precision import decimal_precision as dp
34 _logger = logging.getLogger(__name__)
36 class account_analytic_invoice_line(osv.osv):
37 _name = "account.analytic.invoice.line"
39 def _amount_line(self, cr, uid, ids, prop, unknow_none, unknow_dict, context=None):
41 for line in self.browse(cr, uid, ids, context=context):
42 res[line.id] = line.quantity * line.price_unit
43 if line.analytic_account_id.pricelist_id:
44 cur = line.analytic_account_id.pricelist_id.currency_id
45 res[line.id] = self.pool.get('res.currency').round(cr, uid, cur, res[line.id])
49 'product_id': fields.many2one('product.product','Product',required=True),
50 'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account'),
51 'name': fields.text('Description', required=True),
52 'quantity': fields.float('Quantity', required=True),
53 'uom_id': fields.many2one('product.uom', 'Unit of Measure',required=True),
54 'price_unit': fields.float('Unit Price', required=True),
55 'price_subtotal': fields.function(_amount_line, string='Sub Total', type="float",digits_compute= dp.get_precision('Account')),
61 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):
62 context = context or {}
63 uom_obj = self.pool.get('product.uom')
64 company_id = company_id or False
65 local_context = dict(context, company_id=company_id, force_company=company_id, pricelist=pricelist_id)
68 return {'value': {'price_unit': 0.0}, 'domain':{'product_uom':[]}}
70 part = self.pool.get('res.partner').browse(cr, uid, partner_id, context=local_context)
72 local_context.update({'lang': part.lang})
75 res = self.pool.get('product.product').browse(cr, uid, product, context=local_context)
76 if price_unit is not False:
81 price = res.list_price
83 name = self.pool.get('product.product').name_get(cr, uid, [res.id], context=local_context)[0][1]
84 if res.description_sale:
85 name += '\n'+res.description_sale
87 result.update({'name': name or False,'uom_id': uom_id or res.uom_id.id or False, 'price_unit': price})
89 res_final = {'value':result}
90 if result['uom_id'] != res.uom_id.id:
91 selected_uom = uom_obj.browse(cr, uid, result['uom_id'], context=local_context)
92 new_price = uom_obj._compute_price(cr, uid, res.uom_id.id, res_final['value']['price_unit'], result['uom_id'])
93 res_final['value']['price_unit'] = new_price
97 class account_analytic_account(osv.osv):
98 _name = "account.analytic.account"
99 _inherit = "account.analytic.account"
101 def _analysis_all(self, cr, uid, ids, fields, arg, context=None):
103 res = dict([(i, {}) for i in ids])
104 parent_ids = tuple(ids) #We don't want consolidation for each of these fields because those complex computation is resource-greedy.
105 accounts = self.browse(cr, uid, ids, context=context)
109 cr.execute('SELECT MAX(id) FROM res_users')
110 max_user = cr.fetchone()[0]
112 cr.execute('SELECT DISTINCT("user") FROM account_analytic_analysis_summary_user ' \
113 'WHERE account_id IN %s AND unit_amount <> 0.0', (parent_ids,))
114 result = cr.fetchall()
118 res[id][f] = [int((id * max_user) + x[0]) for x in result]
119 elif f == 'month_ids':
121 cr.execute('SELECT DISTINCT(month_id) FROM account_analytic_analysis_summary_month ' \
122 'WHERE account_id IN %s AND unit_amount <> 0.0', (parent_ids,))
123 result = cr.fetchall()
127 res[id][f] = [int(id * 1000000 + int(x[0])) for x in result]
128 elif f == 'last_worked_invoiced_date':
132 cr.execute("SELECT account_analytic_line.account_id, MAX(date) \
133 FROM account_analytic_line \
134 WHERE account_id IN %s \
135 AND invoice_id IS NOT NULL \
136 GROUP BY account_analytic_line.account_id;", (parent_ids,))
137 for account_id, sum in cr.fetchall():
138 if account_id not in res:
140 res[account_id][f] = sum
141 elif f == 'ca_to_invoice':
145 for account in accounts:
147 SELECT product_id, sum(amount), user_id, to_invoice, sum(unit_amount), product_uom_id, line.name
148 FROM account_analytic_line line
149 LEFT JOIN account_analytic_journal journal ON (journal.id = line.journal_id)
150 WHERE account_id = %s
151 AND journal.type != 'purchase'
152 AND invoice_id IS NULL
153 AND to_invoice IS NOT NULL
154 GROUP BY product_id, user_id, to_invoice, product_uom_id, line.name""", (account.id,))
156 res[account.id][f] = 0.0
157 for product_id, price, user_id, factor_id, qty, uom, line_name in cr.fetchall():
160 price = self.pool.get('account.analytic.line')._get_invoice_price(cr, uid, account, product_id, user_id, qty, context)
161 factor = self.pool.get('hr_timesheet_invoice.factor').browse(cr, uid, factor_id, context=context)
162 res[account.id][f] += price * qty * (100-factor.factor or 0.0) / 100.0
164 # sum both result on account_id
166 res[id][f] = round(res.get(id, {}).get(f, 0.0), dp) + round(res2.get(id, 0.0), 2)
167 elif f == 'last_invoice_date':
171 cr.execute ("SELECT account_analytic_line.account_id, \
172 DATE(MAX(account_invoice.date_invoice)) \
173 FROM account_analytic_line \
174 JOIN account_invoice \
175 ON account_analytic_line.invoice_id = account_invoice.id \
176 WHERE account_analytic_line.account_id IN %s \
177 AND account_analytic_line.invoice_id IS NOT NULL \
178 GROUP BY account_analytic_line.account_id",(parent_ids,))
179 for account_id, lid in cr.fetchall():
180 res[account_id][f] = lid
181 elif f == 'last_worked_date':
185 cr.execute("SELECT account_analytic_line.account_id, MAX(date) \
186 FROM account_analytic_line \
187 WHERE account_id IN %s \
188 AND invoice_id IS NULL \
189 GROUP BY account_analytic_line.account_id",(parent_ids,))
190 for account_id, lwd in cr.fetchall():
191 if account_id not in res:
193 res[account_id][f] = lwd
194 elif f == 'hours_qtt_non_invoiced':
198 cr.execute("SELECT account_analytic_line.account_id, COALESCE(SUM(unit_amount), 0.0) \
199 FROM account_analytic_line \
200 JOIN account_analytic_journal \
201 ON account_analytic_line.journal_id = account_analytic_journal.id \
202 WHERE account_analytic_line.account_id IN %s \
203 AND account_analytic_journal.type='general' \
204 AND invoice_id IS NULL \
205 AND to_invoice IS NOT NULL \
206 GROUP BY account_analytic_line.account_id;",(parent_ids,))
207 for account_id, sua in cr.fetchall():
208 if account_id not in res:
210 res[account_id][f] = round(sua, dp)
212 res[id][f] = round(res[id][f], dp)
213 elif f == 'hours_quantity':
217 cr.execute("SELECT account_analytic_line.account_id, COALESCE(SUM(unit_amount), 0.0) \
218 FROM account_analytic_line \
219 JOIN account_analytic_journal \
220 ON account_analytic_line.journal_id = account_analytic_journal.id \
221 WHERE account_analytic_line.account_id IN %s \
222 AND account_analytic_journal.type='general' \
223 GROUP BY account_analytic_line.account_id",(parent_ids,))
225 for account_id, hq in ff:
226 if account_id not in res:
228 res[account_id][f] = round(hq, dp)
230 res[id][f] = round(res[id][f], dp)
231 elif f == 'ca_theorical':
232 # TODO Take care of pricelist and purchase !
236 # This computation doesn't take care of pricelist !
237 # Just consider list_price
239 cr.execute("""SELECT account_analytic_line.account_id AS account_id, \
240 COALESCE(SUM((account_analytic_line.unit_amount * pt.list_price) \
241 - (account_analytic_line.unit_amount * pt.list_price \
242 * hr.factor)), 0.0) AS somme
243 FROM account_analytic_line \
244 LEFT JOIN account_analytic_journal \
245 ON (account_analytic_line.journal_id = account_analytic_journal.id) \
246 JOIN product_product pp \
247 ON (account_analytic_line.product_id = pp.id) \
248 JOIN product_template pt \
249 ON (pp.product_tmpl_id = pt.id) \
250 JOIN account_analytic_account a \
251 ON (a.id=account_analytic_line.account_id) \
252 JOIN hr_timesheet_invoice_factor hr \
253 ON (hr.id=a.to_invoice) \
254 WHERE account_analytic_line.account_id IN %s \
255 AND a.to_invoice IS NOT NULL \
256 AND account_analytic_journal.type IN ('purchase', 'general')
257 GROUP BY account_analytic_line.account_id""",(parent_ids,))
258 for account_id, sum in cr.fetchall():
259 res[account_id][f] = round(sum, dp)
262 def _ca_invoiced_calc(self, cr, uid, ids, name, arg, context=None):
265 child_ids = tuple(ids) #We don't want consolidation for each of these fields because those complex computation is resource-greedy.
272 #Search all invoice lines not in cancelled state that refer to this analytic account
273 inv_line_obj = self.pool.get("account.invoice.line")
274 inv_lines = inv_line_obj.search(cr, uid, ['&', ('account_analytic_id', 'in', child_ids), ('invoice_id.state', '!=', 'cancel')], context=context)
275 for line in inv_line_obj.browse(cr, uid, inv_lines, context=context):
276 res[line.account_analytic_id.id] += line.price_subtotal
277 for acc in self.browse(cr, uid, res.keys(), context=context):
278 res[acc.id] = res[acc.id] - (acc.timesheet_ca_invoiced or 0.0)
283 def _total_cost_calc(self, cr, uid, ids, name, arg, context=None):
286 child_ids = tuple(ids) #We don't want consolidation for each of these fields because those complex computation is resource-greedy.
292 cr.execute("""SELECT account_analytic_line.account_id, COALESCE(SUM(amount), 0.0) \
293 FROM account_analytic_line \
294 JOIN account_analytic_journal \
295 ON account_analytic_line.journal_id = account_analytic_journal.id \
296 WHERE account_analytic_line.account_id IN %s \
298 GROUP BY account_analytic_line.account_id""",(child_ids,))
299 for account_id, sum in cr.fetchall():
300 res[account_id] = round(sum,2)
304 def _remaining_hours_calc(self, cr, uid, ids, name, arg, context=None):
306 for account in self.browse(cr, uid, ids, context=context):
307 if account.quantity_max != 0:
308 res[account.id] = account.quantity_max - account.hours_quantity
310 res[account.id] = 0.0
312 res[id] = round(res.get(id, 0.0),2)
315 def _remaining_hours_to_invoice_calc(self, cr, uid, ids, name, arg, context=None):
317 for account in self.browse(cr, uid, ids, context=context):
318 res[account.id] = max(account.hours_qtt_est - account.timesheet_ca_invoiced, account.ca_to_invoice)
321 def _hours_qtt_invoiced_calc(self, cr, uid, ids, name, arg, context=None):
323 for account in self.browse(cr, uid, ids, context=context):
324 res[account.id] = account.hours_quantity - account.hours_qtt_non_invoiced
325 if res[account.id] < 0:
326 res[account.id] = 0.0
328 res[id] = round(res.get(id, 0.0),2)
331 def _revenue_per_hour_calc(self, cr, uid, ids, name, arg, context=None):
333 for account in self.browse(cr, uid, ids, context=context):
334 if account.hours_qtt_invoiced == 0:
337 res[account.id] = account.ca_invoiced / account.hours_qtt_invoiced
339 res[id] = round(res.get(id, 0.0),2)
342 def _real_margin_rate_calc(self, cr, uid, ids, name, arg, context=None):
344 for account in self.browse(cr, uid, ids, context=context):
345 if account.ca_invoiced == 0:
347 elif account.total_cost != 0.0:
348 res[account.id] = -(account.real_margin / account.total_cost) * 100
350 res[account.id] = 0.0
352 res[id] = round(res.get(id, 0.0),2)
355 def _fix_price_to_invoice_calc(self, cr, uid, ids, name, arg, context=None):
356 sale_obj = self.pool.get('sale.order')
358 for account in self.browse(cr, uid, ids, context=context):
359 res[account.id] = 0.0
360 sale_ids = sale_obj.search(cr, uid, [('project_id','=', account.id), ('state', '=', 'manual')], context=context)
361 for sale in sale_obj.browse(cr, uid, sale_ids, context=context):
362 res[account.id] += sale.amount_untaxed
363 for invoice in sale.invoice_ids:
364 if invoice.state != 'cancel':
365 res[account.id] -= invoice.amount_untaxed
368 def _timesheet_ca_invoiced_calc(self, cr, uid, ids, name, arg, context=None):
369 lines_obj = self.pool.get('account.analytic.line')
372 for account in self.browse(cr, uid, ids, context=context):
373 res[account.id] = 0.0
374 line_ids = lines_obj.search(cr, uid, [('account_id','=', account.id), ('invoice_id','!=',False), ('to_invoice','!=', False), ('journal_id.type', '=', 'general')], context=context)
375 for line in lines_obj.browse(cr, uid, line_ids, context=context):
376 if line.invoice_id not in inv_ids:
377 inv_ids.append(line.invoice_id)
378 res[account.id] += line.invoice_id.amount_untaxed
381 def _remaining_ca_calc(self, cr, uid, ids, name, arg, context=None):
383 for account in self.browse(cr, uid, ids, context=context):
384 res[account.id] = max(account.amount_max - account.ca_invoiced, account.fix_price_to_invoice)
387 def _real_margin_calc(self, cr, uid, ids, name, arg, context=None):
389 for account in self.browse(cr, uid, ids, context=context):
390 res[account.id] = account.ca_invoiced + account.total_cost
392 res[id] = round(res.get(id, 0.0),2)
395 def _theorical_margin_calc(self, cr, uid, ids, name, arg, context=None):
397 for account in self.browse(cr, uid, ids, context=context):
398 res[account.id] = account.ca_theorical + account.total_cost
400 res[id] = round(res.get(id, 0.0),2)
403 def _is_overdue_quantity(self, cr, uid, ids, fieldnames, args, context=None):
404 result = dict.fromkeys(ids, 0)
405 for record in self.browse(cr, uid, ids, context=context):
406 if record.quantity_max > 0.0:
407 result[record.id] = int(record.hours_quantity >= record.quantity_max)
409 result[record.id] = 0
412 def _get_analytic_account(self, cr, uid, ids, context=None):
414 for line in self.pool.get('account.analytic.line').browse(cr, uid, ids, context=context):
415 result.add(line.account_id.id)
418 def _get_total_estimation(self, account):
420 if account.fix_price_invoices:
421 tot_est += account.amount_max
422 if account.invoice_on_timesheets:
423 tot_est += account.hours_qtt_est
426 def _get_total_invoiced(self, account):
428 if account.fix_price_invoices:
429 total_invoiced += account.ca_invoiced
430 if account.invoice_on_timesheets:
431 total_invoiced += account.timesheet_ca_invoiced
432 return total_invoiced
434 def _get_total_remaining(self, account):
435 total_remaining = 0.0
436 if account.fix_price_invoices:
437 total_remaining += account.remaining_ca
438 if account.invoice_on_timesheets:
439 total_remaining += account.remaining_hours_to_invoice
440 return total_remaining
442 def _get_total_toinvoice(self, account):
443 total_toinvoice = 0.0
444 if account.fix_price_invoices:
445 total_toinvoice += account.fix_price_to_invoice
446 if account.invoice_on_timesheets:
447 total_toinvoice += account.ca_to_invoice
448 return total_toinvoice
450 def _sum_of_fields(self, cr, uid, ids, name, arg, context=None):
451 res = dict([(i, {}) for i in ids])
452 for account in self.browse(cr, uid, ids, context=context):
453 res[account.id]['est_total'] = self._get_total_estimation(account)
454 res[account.id]['invoiced_total'] = self._get_total_invoiced(account)
455 res[account.id]['remaining_total'] = self._get_total_remaining(account)
456 res[account.id]['toinvoice_total'] = self._get_total_toinvoice(account)
460 'is_overdue_quantity' : fields.function(_is_overdue_quantity, method=True, type='boolean', string='Overdue Quantity',
462 'account.analytic.line' : (_get_analytic_account, None, 20),
463 'account.analytic.account': (lambda self, cr, uid, ids, c=None: ids, ['quantity_max'], 10),
465 'ca_invoiced': fields.function(_ca_invoiced_calc, type='float', string='Invoiced Amount',
466 help="Total customer invoiced amount for this account.",
467 digits_compute=dp.get_precision('Account')),
468 'total_cost': fields.function(_total_cost_calc, type='float', string='Total Costs',
469 help="Total of costs for this account. It includes real costs (from invoices) and indirect costs, like time spent on timesheets.",
470 digits_compute=dp.get_precision('Account')),
471 'ca_to_invoice': fields.function(_analysis_all, multi='analytic_analysis', type='float', string='Uninvoiced Amount',
472 help="If invoice from analytic account, the remaining amount you can invoice to the customer based on the total costs.",
473 digits_compute=dp.get_precision('Account')),
474 'ca_theorical': fields.function(_analysis_all, multi='analytic_analysis', type='float', string='Theoretical Revenue',
475 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.",
476 digits_compute=dp.get_precision('Account')),
477 'hours_quantity': fields.function(_analysis_all, multi='analytic_analysis', type='float', string='Total Worked Time',
478 help="Number of time you spent on the analytic account (from timesheet). It computes quantities on all journal of type 'general'."),
479 'last_invoice_date': fields.function(_analysis_all, multi='analytic_analysis', type='date', string='Last Invoice Date',
480 help="If invoice from the costs, this is the date of the latest invoiced."),
481 'last_worked_invoiced_date': fields.function(_analysis_all, multi='analytic_analysis', type='date', string='Date of Last Invoiced Cost',
482 help="If invoice from the costs, this is the date of the latest work or cost that have been invoiced."),
483 'last_worked_date': fields.function(_analysis_all, multi='analytic_analysis', type='date', string='Date of Last Cost/Work',
484 help="Date of the latest work done on this account."),
485 'hours_qtt_non_invoiced': fields.function(_analysis_all, multi='analytic_analysis', type='float', string='Uninvoiced Time',
486 help="Number of time (hours/days) (from journal of type 'general') that can be invoiced if you invoice based on analytic account."),
487 'hours_qtt_invoiced': fields.function(_hours_qtt_invoiced_calc, type='float', string='Invoiced Time',
488 help="Number of time (hours/days) that can be invoiced plus those that already have been invoiced."),
489 'remaining_hours': fields.function(_remaining_hours_calc, type='float', string='Remaining Time',
490 help="Computed using the formula: Maximum Time - Total Worked Time"),
491 'remaining_hours_to_invoice': fields.function(_remaining_hours_to_invoice_calc, type='float', string='Remaining Time',
492 help="Computed using the formula: Expected on timesheets - Total invoiced on timesheets"),
493 'fix_price_to_invoice': fields.function(_fix_price_to_invoice_calc, type='float', string='Remaining Time',
494 help="Sum of quotations for this contract."),
495 'timesheet_ca_invoiced': fields.function(_timesheet_ca_invoiced_calc, type='float', string='Remaining Time',
496 help="Sum of timesheet lines invoiced for this contract."),
497 'remaining_ca': fields.function(_remaining_ca_calc, type='float', string='Remaining Revenue',
498 help="Computed using the formula: Max Invoice Price - Invoiced Amount.",
499 digits_compute=dp.get_precision('Account')),
500 'revenue_per_hour': fields.function(_revenue_per_hour_calc, type='float', string='Revenue per Time (real)',
501 help="Computed using the formula: Invoiced Amount / Total Time",
502 digits_compute=dp.get_precision('Account')),
503 'real_margin': fields.function(_real_margin_calc, type='float', string='Real Margin',
504 help="Computed using the formula: Invoiced Amount - Total Costs.",
505 digits_compute=dp.get_precision('Account')),
506 'theorical_margin': fields.function(_theorical_margin_calc, type='float', string='Theoretical Margin',
507 help="Computed using the formula: Theoretical Revenue - Total Costs",
508 digits_compute=dp.get_precision('Account')),
509 'real_margin_rate': fields.function(_real_margin_rate_calc, type='float', string='Real Margin Rate (%)',
510 help="Computes using the formula: (Real Margin / Total Costs) * 100.",
511 digits_compute=dp.get_precision('Account')),
512 'fix_price_invoices' : fields.boolean('Fixed Price'),
513 'invoice_on_timesheets' : fields.boolean("On Timesheets"),
514 'month_ids': fields.function(_analysis_all, multi='analytic_analysis', type='many2many', relation='account_analytic_analysis.summary.month', string='Month'),
515 'user_ids': fields.function(_analysis_all, multi='analytic_analysis', type="many2many", relation='account_analytic_analysis.summary.user', string='User'),
516 'hours_qtt_est': fields.float('Estimation of Hours to Invoice'),
517 'est_total' : fields.function(_sum_of_fields, type="float",multi="sum_of_all", string="Total Estimation"),
518 'invoiced_total' : fields.function(_sum_of_fields, type="float",multi="sum_of_all", string="Total Invoiced"),
519 '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"),
520 '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."),
521 'recurring_invoice_line_ids': fields.one2many('account.analytic.invoice.line', 'analytic_account_id', 'Invoice Lines'),
522 'recurring_invoices' : fields.boolean('Generate recurring invoices automatically'),
523 'recurring_rule_type': fields.selection([
525 ('weekly', 'Week(s)'),
526 ('monthly', 'Month(s)'),
527 ('yearly', 'Year(s)'),
528 ], 'Recurrency', help="Invoice automatically repeat at specified interval"),
529 'recurring_interval': fields.integer('Repeat Every', help="Repeat every (Days/Week/Month/Year)"),
530 'recurring_next_date': fields.date('Date of Next Invoice'),
534 'recurring_interval': 1,
535 'recurring_next_date': lambda *a: time.strftime('%Y-%m-%d'),
536 'recurring_rule_type':'monthly'
539 def open_sale_order_lines(self,cr,uid,ids,context=None):
542 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))])
543 names = [record.name for record in self.browse(cr, uid, ids, context=context)]
544 name = _('Sales Order Lines to Invoice of %s') % ','.join(names)
546 'type': 'ir.actions.act_window',
549 'view_mode': 'tree,form',
551 'domain' : [('order_id','in',sale_ids)],
552 'res_model': 'sale.order.line',
556 def on_change_template(self, cr, uid, ids, template_id, date_start=False, context=None):
559 res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, date_start=date_start, context=context)
561 template = self.browse(cr, uid, template_id, context=context)
564 res['value']['fix_price_invoices'] = template.fix_price_invoices
565 res['value']['amount_max'] = template.amount_max
567 res['value']['invoice_on_timesheets'] = template.invoice_on_timesheets
568 res['value']['hours_qtt_est'] = template.hours_qtt_est
570 if template.to_invoice.id:
571 res['value']['to_invoice'] = template.to_invoice.id
572 if template.pricelist_id.id:
573 res['value']['pricelist_id'] = template.pricelist_id.id
575 invoice_line_ids = []
576 for x in template.recurring_invoice_line_ids:
577 invoice_line_ids.append((0, 0, {
578 'product_id': x.product_id.id,
579 'uom_id': x.uom_id.id,
581 'quantity': x.quantity,
582 'price_unit': x.price_unit,
583 'analytic_account_id': x.analytic_account_id and x.analytic_account_id.id or False,
585 res['value']['recurring_invoices'] = template.recurring_invoices
586 res['value']['recurring_interval'] = template.recurring_interval
587 res['value']['recurring_rule_type'] = template.recurring_rule_type
588 res['value']['recurring_invoice_line_ids'] = invoice_line_ids
591 def onchange_recurring_invoices(self, cr, uid, ids, recurring_invoices, date_start=False, context=None):
593 if date_start and recurring_invoices:
594 value = {'value': {'recurring_next_date': date_start}}
597 def cron_account_analytic_account(self, cr, uid, context=None):
602 def fill_remind(key, domain, write_pending=False):
604 ('type', '=', 'contract'),
605 ('partner_id', '!=', False),
606 ('manager_id', '!=', False),
607 ('manager_id.email', '!=', False),
609 base_domain.extend(domain)
611 accounts_ids = self.search(cr, uid, base_domain, context=context, order='name asc')
612 accounts = self.browse(cr, uid, accounts_ids, context=context)
613 for account in accounts:
615 account.write({'state' : 'pending'}, context=context)
616 remind_user = remind.setdefault(account.manager_id.id, {})
617 remind_type = remind_user.setdefault(key, {})
618 remind_partner = remind_type.setdefault(account.partner_id, []).append(account)
621 fill_remind("old", [('state', 'in', ['pending'])])
624 fill_remind("new", [('state', 'in', ['draft', 'open']), '|', '&', ('date', '!=', False), ('date', '<=', time.strftime('%Y-%m-%d')), ('is_overdue_quantity', '=', True)], True)
626 # Expires in less than 30 days
627 fill_remind("future", [('state', 'in', ['draft', 'open']), ('date', '!=', False), ('date', '<', (datetime.datetime.now() + datetime.timedelta(30)).strftime("%Y-%m-%d"))])
629 context['base_url'] = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
630 context['action_id'] = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account_analytic_analysis', 'action_account_analytic_overdue_all')[1]
631 template_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account_analytic_analysis', 'account_analytic_cron_email_template')[1]
632 for user_id, data in remind.items():
633 context["data"] = data
634 _logger.debug("Sending reminder to uid %s", user_id)
635 self.pool.get('email.template').send_mail(cr, uid, template_id, user_id, force_send=True, context=context)
639 def onchange_invoice_on_timesheets(self, cr, uid, ids, invoice_on_timesheets, context=None):
640 if not invoice_on_timesheets:
641 return {'value': {'to_invoice': False}}
642 result = {'value': {'use_timesheets': True}}
644 to_invoice = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'hr_timesheet_invoice', 'timesheet_invoice_factor1')
645 result['value']['to_invoice'] = to_invoice[1]
651 def hr_to_invoice_timesheets(self, cr, uid, ids, context=None):
652 domain = [('invoice_id','=',False),('to_invoice','!=',False), ('journal_id.type', '=', 'general'), ('account_id', 'in', ids)]
653 names = [record.name for record in self.browse(cr, uid, ids, context=context)]
654 name = _('Timesheets to Invoice of %s') % ','.join(names)
656 'type': 'ir.actions.act_window',
659 'view_mode': 'tree,form',
661 'res_model': 'account.analytic.line',
665 def _prepare_invoice_data(self, cr, uid, contract, context=None):
666 context = context or {}
668 journal_obj = self.pool.get('account.journal')
670 if not contract.partner_id:
671 raise osv.except_osv(_('No Customer Defined!'),_("You must first select a Customer for Contract %s!") % contract.name )
673 fpos = contract.partner_id.property_account_position or False
674 journal_ids = journal_obj.search(cr, uid, [('type', '=','sale'),('company_id', '=', contract.company_id.id or False)], limit=1)
676 raise osv.except_osv(_('Error!'),
677 _('Please define a sale journal for the company "%s".') % (contract.company_id.name or '', ))
679 partner_payment_term = contract.partner_id.property_payment_term and contract.partner_id.property_payment_term.id or False
682 if contract.pricelist_id:
683 currency_id = contract.pricelist_id.currency_id.id
684 elif contract.partner_id.property_product_pricelist:
685 currency_id = contract.partner_id.property_product_pricelist.currency_id.id
686 elif contract.company_id:
687 currency_id = contract.company_id.currency_id.id
690 'account_id': contract.partner_id.property_account_receivable.id,
691 'type': 'out_invoice',
692 'partner_id': contract.partner_id.id,
693 'currency_id': currency_id,
694 'journal_id': len(journal_ids) and journal_ids[0] or False,
695 'date_invoice': contract.recurring_next_date,
696 'origin': contract.code,
697 'fiscal_position': fpos and fpos.id,
698 'payment_term': partner_payment_term,
699 'company_id': contract.company_id.id or False,
703 def _prepare_invoice_lines(self, cr, uid, contract, fiscal_position_id, context=None):
704 fpos_obj = self.pool.get('account.fiscal.position')
705 fiscal_position = fpos_obj.browse(cr, uid, fiscal_position_id, context=context)
707 for line in contract.recurring_invoice_line_ids:
709 res = line.product_id
710 account_id = res.property_account_income.id
712 account_id = res.categ_id.property_account_income_categ.id
713 account_id = fpos_obj.map_account(cr, uid, fiscal_position, account_id)
715 taxes = res.taxes_id or False
716 tax_id = fpos_obj.map_tax(cr, uid, fiscal_position, taxes)
718 invoice_lines.append((0, 0, {
720 'account_id': account_id,
721 'account_analytic_id': contract.id,
722 'price_unit': line.price_unit or 0.0,
723 'quantity': line.quantity,
724 'uos_id': line.uom_id.id or False,
725 'product_id': line.product_id.id or False,
726 'invoice_line_tax_id': [(6, 0, tax_id)],
730 def _prepare_invoice(self, cr, uid, contract, context=None):
731 invoice = self._prepare_invoice_data(cr, uid, contract, context=context)
732 invoice['invoice_line'] = self._prepare_invoice_lines(cr, uid, contract, invoice['fiscal_position'], context=context)
735 def recurring_create_invoice(self, cr, uid, ids, context=None):
736 return self._recurring_create_invoice(cr, uid, ids, context=context)
738 def _cron_recurring_create_invoice(self, cr, uid, context=None):
739 return self._recurring_create_invoice(cr, uid, [], automatic=True, context=context)
741 def _recurring_create_invoice(self, cr, uid, ids, automatic=False, context=None):
742 context = context or {}
744 current_date = time.strftime('%Y-%m-%d')
748 contract_ids = self.search(cr, uid, [('recurring_next_date','<=', current_date), ('state','=', 'open'), ('recurring_invoices','=', True), ('type', '=', 'contract')])
749 for contract in self.browse(cr, uid, contract_ids, context=context):
751 invoice_values = self._prepare_invoice(cr, uid, contract, context=context)
752 invoice_ids.append(self.pool['account.invoice'].create(cr, uid, invoice_values, context=context))
753 next_date = datetime.datetime.strptime(contract.recurring_next_date or current_date, "%Y-%m-%d")
754 interval = contract.recurring_interval
755 if contract.recurring_rule_type == 'daily':
756 new_date = next_date+relativedelta(days=+interval)
757 elif contract.recurring_rule_type == 'weekly':
758 new_date = next_date+relativedelta(weeks=+interval)
759 elif contract.recurring_rule_type == 'monthly':
760 new_date = next_date+relativedelta(months=+interval)
762 new_date = next_date+relativedelta(years=+interval)
763 self.write(cr, uid, [contract.id], {'recurring_next_date': new_date.strftime('%Y-%m-%d')}, context=context)
769 _logger.error(traceback.format_exc())
774 class account_analytic_account_summary_user(osv.osv):
775 _name = "account_analytic_analysis.summary.user"
776 _description = "Hours Summary by User"
781 def _unit_amount(self, cr, uid, ids, name, arg, context=None):
783 account_obj = self.pool.get('account.analytic.account')
784 cr.execute('SELECT MAX(id) FROM res_users')
785 max_user = cr.fetchone()[0]
786 account_ids = [int(str(x/max_user - (x%max_user == 0 and 1 or 0))) for x in ids]
787 user_ids = [int(str(x-((x/max_user - (x%max_user == 0 and 1 or 0)) *max_user))) for x in ids]
788 parent_ids = tuple(account_ids) #We don't want consolidation for each of these fields because those complex computation is resource-greedy.
790 cr.execute('SELECT id, unit_amount ' \
791 'FROM account_analytic_analysis_summary_user ' \
792 'WHERE account_id IN %s ' \
793 'AND "user" IN %s',(parent_ids, tuple(user_ids),))
794 for sum_id, unit_amount in cr.fetchall():
795 res[sum_id] = unit_amount
797 res[id] = round(res.get(id, 0.0), 2)
801 'account_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True),
802 'unit_amount': fields.float('Total Time'),
803 'user': fields.many2one('res.users', 'User'),
807 openerp.tools.sql.drop_view_if_exists(cr, 'account_analytic_analysis_summary_user')
808 cr.execute('''CREATE OR REPLACE VIEW account_analytic_analysis_summary_user AS (
810 (select max(id) as max_user from res_users)
813 l.account_id AS account_id,
814 coalesce(l.user_id, 0) AS user_id,
815 SUM(l.unit_amount) AS unit_amount
816 FROM account_analytic_line AS l,
817 account_analytic_journal AS j
818 WHERE (j.type = 'general' ) and (j.id=l.journal_id)
819 GROUP BY l.account_id, l.user_id
821 select (lu.account_id * mu.max_user) + lu.user_id as id,
822 lu.account_id as account_id,
823 lu.user_id as "user",
827 class account_analytic_account_summary_month(osv.osv):
828 _name = "account_analytic_analysis.summary.month"
829 _description = "Hours summary by month"
834 'account_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True),
835 'unit_amount': fields.float('Total Time'),
836 'month': fields.char('Month', size=32, readonly=True),
840 openerp.tools.sql.drop_view_if_exists(cr, 'account_analytic_analysis_summary_month')
841 cr.execute('CREATE VIEW account_analytic_analysis_summary_month AS (' \
843 '(TO_NUMBER(TO_CHAR(d.month, \'YYYYMM\'), \'999999\') + (d.account_id * 1000000::bigint))::bigint AS id, ' \
844 'd.account_id AS account_id, ' \
845 'TO_CHAR(d.month, \'Mon YYYY\') AS month, ' \
846 'TO_NUMBER(TO_CHAR(d.month, \'YYYYMM\'), \'999999\') AS month_id, ' \
847 'COALESCE(SUM(l.unit_amount), 0.0) AS unit_amount ' \
854 'a.id AS account_id, ' \
855 'l.month AS month ' \
858 'DATE_TRUNC(\'month\', l.date) AS month ' \
859 'FROM account_analytic_line AS l, ' \
860 'account_analytic_journal AS j ' \
861 'WHERE j.type = \'general\' ' \
862 'GROUP BY DATE_TRUNC(\'month\', l.date) ' \
864 'account_analytic_account AS a ' \
865 'GROUP BY l.month, a.id ' \
867 'GROUP BY d2.account_id, d2.month ' \
871 'l.account_id AS account_id, ' \
872 'DATE_TRUNC(\'month\', l.date) AS month, ' \
873 'SUM(l.unit_amount) AS unit_amount ' \
874 'FROM account_analytic_line AS l, ' \
875 'account_analytic_journal AS j ' \
876 'WHERE (j.type = \'general\') and (j.id=l.journal_id) ' \
877 'GROUP BY l.account_id, DATE_TRUNC(\'month\', l.date) ' \
880 'd.account_id = l.account_id ' \
881 'AND d.month = l.month' \
883 'GROUP BY d.month, d.account_id ' \
886 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: