From 55f9cbf9c7331fdf14cc716768fef308181fc0fa Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Thu, 16 Apr 2015 13:46:09 +0200 Subject: [PATCH] [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. --- .../hr_timesheet_invoice.py | 266 ++++++++++-------- 1 file changed, 144 insertions(+), 122 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.')) + raise osv.except_osv(_('Error!'), _('Contract incomplete. Please fill in the Customer and Pricelist fields for %s.') % (account.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] + if not analytic_line.to_invoice: + raise osv.except_osv(_('Error!'), _('Trying to invoice non invoiceable line for %s.') % (analytic_line.product_id.name)) - 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'] + 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) - last_invoice = invoice_obj.create(cr, uid, curr_invoice, context=context2) - invoices.append(last_invoice) + # 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) - 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) + 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):