summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Trigaux <mat@odoo.com>2015-04-16 13:46:09 +0200
committerMartin Trigaux <mat@odoo.com>2015-04-16 14:55:21 +0200
commit55f9cbf9c7331fdf14cc716768fef308181fc0fa (patch)
tree72662aae531e16c66e3977749eaec60d457b3d27
parent58a481329eb1ec4074a08e12b89fdbd2aaa35343 (diff)
[FIX] hr_timesheet_invoice: description of invoice
Partial backport of 4d912af without the group by partner part. Do not forward port above saas-6. The generation of invoices from analytic lines was messy and mixed the description of lines (e.g. redundant message when same product is invoiced twice with different user, see opw 633047). Grouping was not consistent. In 4d912af, grouping by partner was added with a refactoring of the grouping method. Backport the second part only to get cleaner grouping and avoid mixing messages.
-rw-r--r--addons/hr_timesheet_invoice/hr_timesheet_invoice.py274
1 files changed, 148 insertions, 126 deletions
diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py
index 6e6393bced2..ac7c237d73c 100644
--- a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py
+++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py
@@ -150,14 +150,106 @@ class account_analytic_line(osv.osv):
price = 0.0
return price
+ def _prepare_cost_invoice(self, cr, uid, partner, company_id, currency_id, analytic_lines, context=None):
+ """ returns values used to create main invoice from analytic lines"""
+ account_payment_term_obj = self.pool['account.payment.term']
+ invoice_name = analytic_lines[0].account_id.name
+
+ date_due = False
+ if partner.property_payment_term:
+ pterm_list = account_payment_term_obj.compute(cr, uid,
+ partner.property_payment_term.id, value=1,
+ date_ref=time.strftime('%Y-%m-%d'))
+ if pterm_list:
+ pterm_list = [line[0] for line in pterm_list]
+ pterm_list.sort()
+ date_due = pterm_list[-1]
+ return {
+ 'name': "%s - %s" % (time.strftime('%d/%m/%Y'), invoice_name),
+ 'partner_id': partner.id,
+ 'company_id': company_id,
+ 'payment_term': partner.property_payment_term.id or False,
+ 'account_id': partner.property_account_receivable.id,
+ 'currency_id': currency_id,
+ 'date_due': date_due,
+ 'fiscal_position': partner.property_account_position.id
+ }
+
+ def _prepare_cost_invoice_line(self, cr, uid, invoice_id, product_id, uom, user_id,
+ factor_id, account, analytic_lines, journal_type, data, context=None):
+ product_obj = self.pool['product.product']
+
+ uom_context = dict(context or {}, uom=uom)
+
+ total_price = sum(l.amount for l in analytic_lines)
+ total_qty = sum(l.unit_amount for l in analytic_lines)
+
+ if data.get('product'):
+ # force product, use its public price
+ if isinstance(data['product'], (tuple, list)):
+ product_id = data['product'][0]
+ else:
+ product_id = data['product']
+ unit_price = self._get_invoice_price(cr, uid, account, product_id, user_id, total_qty, uom_context)
+ elif journal_type == 'general' and product_id:
+ # timesheets, use sale price
+ unit_price = self._get_invoice_price(cr, uid, account, product_id, user_id, total_qty, uom_context)
+ else:
+ # expenses, using price from amount field
+ unit_price = total_price*-1.0 / total_qty
+
+ factor = self.pool['hr_timesheet_invoice.factor'].browse(cr, uid, factor_id, context=uom_context)
+ factor_name = factor.customer_name
+ curr_invoice_line = {
+ 'price_unit': unit_price,
+ 'quantity': total_qty,
+ 'product_id': product_id,
+ 'discount': factor.factor,
+ 'invoice_id': invoice_id,
+ 'name': factor_name,
+ 'uos_id': uom,
+ 'account_analytic_id': account.id,
+ }
+
+ if product_id:
+ product = product_obj.browse(cr, uid, product_id, context=uom_context)
+ factor_name = product_obj.name_get(cr, uid, [product_id], context=uom_context)[0][1]
+ if factor.customer_name:
+ factor_name += ' - ' + factor.customer_name
+
+ general_account = product.property_account_income or product.categ_id.property_account_income_categ
+ if not general_account:
+ raise osv.except_osv(_('Error!'), _("Configuration Error!") + '\n' + _("Please define income account for product '%s'.") % product.name)
+ taxes = product.taxes_id or general_account.tax_ids
+ tax = self.pool['account.fiscal.position'].map_tax(cr, uid, account.partner_id.property_account_position, taxes)
+ curr_invoice_line.update({
+ 'invoice_line_tax_id': [(6, 0, tax)],
+ 'name': factor_name,
+ 'invoice_line_tax_id': [(6, 0, tax)],
+ 'account_id': general_account.id,
+ })
+
+ note = []
+ for line in analytic_lines:
+ # set invoice_line_note
+ details = []
+ if data.get('date', False):
+ details.append(line['date'])
+ if data.get('time', False):
+ if line['product_uom_id']:
+ details.append("%s %s" % (line.unit_amount, line.product_uom_id.name))
+ else:
+ details.append("%s" % (line['unit_amount'], ))
+ if data.get('name', False):
+ details.append(line['name'])
+ if details:
+ note.append(u' - '.join(map(lambda x: unicode(x) or '', details)))
+ if note:
+ curr_invoice_line['name'] += "\n" + ("\n".join(map(lambda x: unicode(x) or '', note)))
+ return curr_invoice_line
+
def invoice_cost_create(self, cr, uid, ids, data=None, context=None):
- analytic_account_obj = self.pool.get('account.analytic.account')
- account_payment_term_obj = self.pool.get('account.payment.term')
invoice_obj = self.pool.get('account.invoice')
- product_obj = self.pool.get('product.product')
- invoice_factor_obj = self.pool.get('hr_timesheet_invoice.factor')
- fiscal_pos_obj = self.pool.get('account.fiscal.position')
- product_uom_obj = self.pool.get('product.uom')
invoice_line_obj = self.pool.get('account.invoice.line')
invoices = []
if context is None:
@@ -165,132 +257,62 @@ class account_analytic_line(osv.osv):
if data is None:
data = {}
- journal_types = {}
+ # use key (partner/account, company, currency)
+ # creates one invoice per key
+ invoice_grouping = {}
+ currency_id = False
# prepare for iteration on journal and accounts
- for line in self.pool.get('account.analytic.line').browse(cr, uid, ids, context=context):
- if line.journal_id.type not in journal_types:
- journal_types[line.journal_id.type] = set()
- journal_types[line.journal_id.type].add(line.account_id.id)
- for journal_type, account_ids in journal_types.items():
- for account in analytic_account_obj.browse(cr, uid, list(account_ids), context=context):
- partner = account.partner_id
+ for line in self.browse(cr, uid, ids, context=context):
+
+ key = (line.account_id.id,
+ line.account_id.company_id.id,
+ line.account_id.pricelist_id.currency_id.id)
+ invoice_grouping.setdefault(key, []).append(line)
+
+ for (key_id, company_id, currency_id), analytic_lines in invoice_grouping.items():
+ # key_id is an account.analytic.account
+ partner = analytic_lines[0].account_id.partner_id # will be the same for every line
+
+ curr_invoice = self._prepare_cost_invoice(cr, uid, partner, company_id, currency_id, analytic_lines, context=context)
+ invoice_context = dict(context,
+ lang=partner.lang,
+ force_company=company_id, # set force_company in context so the correct product properties are selected (eg. income account)
+ company_id=company_id) # set company_id in context, so the correct default journal will be selected
+ last_invoice = invoice_obj.create(cr, uid, curr_invoice, context=invoice_context)
+ invoices.append(last_invoice)
+
+ # use key (product, uom, user, invoiceable, analytic account, journal type)
+ # creates one invoice line per key
+ invoice_lines_grouping = {}
+ for analytic_line in analytic_lines:
+ account = analytic_line.account_id
if (not partner) or not (account.pricelist_id):
- raise osv.except_osv(_('Analytic Account Incomplete!'),
- _('Contract incomplete. Please fill in the Customer and Pricelist fields.'))
-
- date_due = False
- if partner.property_payment_term:
- pterm_list= account_payment_term_obj.compute(cr, uid,
- partner.property_payment_term.id, value=1,
- date_ref=time.strftime('%Y-%m-%d'))
- if pterm_list:
- pterm_list = [line[0] for line in pterm_list]
- pterm_list.sort()
- date_due = pterm_list[-1]
-
- curr_invoice = {
- 'name': time.strftime('%d/%m/%Y') + ' - '+account.name,
- 'partner_id': account.partner_id.id,
- 'company_id': account.company_id.id,
- 'payment_term': partner.property_payment_term.id or False,
- 'account_id': partner.property_account_receivable.id,
- 'currency_id': account.pricelist_id.currency_id.id,
- 'date_due': date_due,
- 'fiscal_position': account.partner_id.property_account_position.id
- }
- context2 = context.copy()
- context2['lang'] = partner.lang
- # set company_id in context, so the correct default journal will be selected
- context2['force_company'] = curr_invoice['company_id']
- # set force_company in context so the correct product properties are selected (eg. income account)
- context2['company_id'] = curr_invoice['company_id']
-
- last_invoice = invoice_obj.create(cr, uid, curr_invoice, context=context2)
- invoices.append(last_invoice)
-
- cr.execute("""SELECT product_id, user_id, to_invoice, sum(amount), sum(unit_amount), product_uom_id
- FROM account_analytic_line as line LEFT JOIN account_analytic_journal journal ON (line.journal_id = journal.id)
- WHERE account_id = %s
- AND line.id IN %s AND journal.type = %s AND to_invoice IS NOT NULL
- GROUP BY product_id, user_id, to_invoice, product_uom_id""", (account.id, tuple(ids), journal_type))
-
- for product_id, user_id, factor_id, total_price, qty, uom in cr.fetchall():
- context2.update({'uom': uom})
-
- if data.get('product'):
- # force product, use its public price
- product_id = data['product'][0]
- unit_price = self._get_invoice_price(cr, uid, account, product_id, user_id, qty, context2)
- elif journal_type == 'general' and product_id:
- # timesheets, use sale price
- unit_price = self._get_invoice_price(cr, uid, account, product_id, user_id, qty, context2)
- else:
- # expenses, using price from amount field
- unit_price = total_price*-1.0 / qty
-
- factor = invoice_factor_obj.browse(cr, uid, factor_id, context=context2)
- # factor_name = factor.customer_name and line_name + ' - ' + factor.customer_name or line_name
- factor_name = factor.customer_name
- curr_line = {
- 'price_unit': unit_price,
- 'quantity': qty,
- 'product_id': product_id or False,
- 'discount': factor.factor,
- 'invoice_id': last_invoice,
- 'name': factor_name,
- 'uos_id': uom,
- 'account_analytic_id': account.id,
- }
- product = product_obj.browse(cr, uid, product_id, context=context2)
- if product:
- factor_name = product_obj.name_get(cr, uid, [product_id], context=context2)[0][1]
- if factor.customer_name:
- factor_name += ' - ' + factor.customer_name
-
- general_account = product.property_account_income or product.categ_id.property_account_income_categ
- if not general_account:
- raise osv.except_osv(_("Configuration Error!"), _("Please define income account for product '%s'.") % product.name)
- taxes = product.taxes_id or general_account.tax_ids
- tax = fiscal_pos_obj.map_tax(cr, uid, account.partner_id.property_account_position, taxes)
- curr_line.update({
- 'invoice_line_tax_id': [(6,0,tax )],
- 'name': factor_name,
- 'invoice_line_tax_id': [(6,0,tax)],
- 'account_id': general_account.id,
- })
- #
- # Compute for lines
- #
- cr.execute("SELECT * FROM account_analytic_line WHERE account_id = %s and id IN %s AND product_id=%s and to_invoice=%s ORDER BY account_analytic_line.date", (account.id, tuple(ids), product_id, factor_id))
-
- line_ids = cr.dictfetchall()
- note = []
- for line in line_ids:
- # set invoice_line_note
- details = []
- if data.get('date', False):
- details.append(line['date'])
- if data.get('time', False):
- if line['product_uom_id']:
- details.append("%s %s" % (line['unit_amount'], product_uom_obj.browse(cr, uid, [line['product_uom_id']],context2)[0].name))
- else:
- details.append("%s" % (line['unit_amount'], ))
- if data.get('name', False):
- details.append(line['name'])
- if details:
- note.append(u' - '.join(map(lambda x: unicode(x) or '',details)))
- if note:
- curr_line['name'] += "\n" + ("\n".join(map(lambda x: unicode(x) or '',note)))
- invoice_line_obj.create(cr, uid, curr_line, context=context)
- cr.execute("update account_analytic_line set invoice_id=%s WHERE account_id = %s and id IN %s", (last_invoice, account.id, tuple(ids)))
- self.invalidate_cache(cr, uid, ['invoice_id'], ids, context=context)
-
- invoice_obj.button_reset_taxes(cr, uid, [last_invoice], context)
+ raise osv.except_osv(_('Error!'), _('Contract incomplete. Please fill in the Customer and Pricelist fields for %s.') % (account.name))
+
+ if not analytic_line.to_invoice:
+ raise osv.except_osv(_('Error!'), _('Trying to invoice non invoiceable line for %s.') % (analytic_line.product_id.name))
+
+ key = (analytic_line.product_id.id,
+ analytic_line.product_uom_id.id,
+ analytic_line.user_id.id,
+ analytic_line.to_invoice.id,
+ analytic_line.account_id,
+ analytic_line.journal_id.type)
+ invoice_lines_grouping.setdefault(key, []).append(analytic_line)
+
+ # finally creates the invoice line
+ for (product_id, uom, user_id, factor_id, account, journal_type), lines_to_invoice in invoice_lines_grouping.items():
+ curr_invoice_line = self._prepare_cost_invoice_line(cr, uid, last_invoice,
+ product_id, uom, user_id, factor_id, account, lines_to_invoice,
+ journal_type, data, context=context)
+
+ invoice_line_obj.create(cr, uid, curr_invoice_line, context=context)
+ self.write(cr, uid, [l.id for l in analytic_lines], {'invoice_id': last_invoice}, context=context)
+ invoice_obj.button_reset_taxes(cr, uid, [last_invoice], context)
return invoices
-
class hr_analytic_timesheet(osv.osv):
_inherit = "hr.analytic.timesheet"
def on_change_account_id(self, cr, uid, ids, account_id, user_id=False):