oca-financial/odoo-bringout-oca-account-invoicing-account_invoice_merge/account_invoice_merge/models/account_move.py
2025-08-29 15:43:04 +02:00

255 lines
9.7 KiB
Python

# Copyright 2004-2010 Tiny SPRL (http://tiny.be).
# Copyright 2010-2011 Elico Corp.
# Copyright 2016 Acsone (https://www.acsone.eu/)
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)
# Copyright 2019 Okia SPRL
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
import numbers
from odoo import api, models
from odoo.tools import float_is_zero
class AccountMove(models.Model):
_inherit = "account.move"
@api.model
def _get_invoice_key_cols(self):
return [
"partner_id",
"user_id",
"move_type",
"currency_id",
"journal_id",
"company_id",
"partner_bank_id",
]
@api.model
def _get_invoice_line_key_cols(self):
fields = [
"name",
"discount",
"tax_ids",
"price_unit",
"product_id",
"account_id",
"analytic_distribution",
"product_uom_id",
]
for field in [
"sale_line_ids", # odoo/sale
"purchase_line_id", # odoo/purchase
"purchase_price", # OCA/account_invoice_margin
]:
if field in self.env["account.move.line"]._fields:
fields.append(field)
return fields
@api.model
def _get_first_invoice_fields(self, invoice):
return {
"invoice_origin": "%s" % (invoice.invoice_origin or "",),
"partner_id": invoice.partner_id.id,
"journal_id": invoice.journal_id.id,
"user_id": invoice.user_id.id,
"currency_id": invoice.currency_id.id,
"company_id": invoice.company_id.id,
"move_type": invoice.move_type,
"state": "draft",
"payment_reference": "%s" % (invoice.payment_reference or "",),
"name": "%s" % (invoice.name or "",),
"fiscal_position_id": invoice.fiscal_position_id.id,
"invoice_payment_term_id": invoice.invoice_payment_term_id.id,
"invoice_line_ids": {},
"partner_bank_id": invoice.partner_bank_id.id,
}
@api.model
def _get_sum_fields(self):
return ["quantity"]
@api.model
def _get_invoice_line_vals(self, line):
field_names = self._get_invoice_line_key_cols() + self._get_sum_fields()
vals = {}
origin_vals = line._convert_to_write(line._cache)
for field_name, val in origin_vals.items():
if field_name in field_names:
vals[field_name] = val
return vals
def _get_draft_invoices(self):
"""Overridable function to return draft invoices to merge"""
return self.filtered(lambda x: x.state == "draft")
def make_key(self, br, fields):
"""
Return a hashable key
"""
list_key = []
for field in fields:
field_val = getattr(br, field)
if isinstance(field_val, dict):
field_val = str(field_val)
elif isinstance(field_val, models.Model):
field_val = tuple(sorted(field_val.ids))
list_key.append((field, field_val))
list_key.sort()
return tuple(list_key)
# flake8: noqa: C901
def do_merge(
self, keep_references=True, date_invoice=False, remove_empty_invoice_lines=True
):
"""
To merge similar type of account invoices.
Invoices will only be merged if:
* Account invoices are in draft
* Account invoices belong to the same partner
* Account invoices are have same company, partner, address, currency,
journal, currency, salesman, account, type
Lines will only be merged if:
* Invoice lines are exactly the same except for the quantity and unit
@param self: The object pointer.
@param keep_references: If True, keep reference of original invoices
@return: new account invoice id
"""
# compute what the new invoices should contain
new_invoices = {}
seen_origins = {}
seen_client_refs = {}
sum_fields = self._get_sum_fields()
for account_invoice in self._get_draft_invoices():
invoice_key = self.make_key(account_invoice, self._get_invoice_key_cols())
new_invoice = new_invoices.setdefault(invoice_key, ({}, []))
origins = seen_origins.setdefault(invoice_key, set())
client_refs = seen_client_refs.setdefault(invoice_key, set())
new_invoice[1].append(account_invoice.id)
invoice_infos = new_invoice[0]
if not invoice_infos:
invoice_infos.update(self._get_first_invoice_fields(account_invoice))
origins.add(account_invoice.invoice_origin)
client_refs.add(account_invoice.payment_reference)
if not keep_references:
invoice_infos.pop("name")
else:
if (
account_invoice.name
and keep_references
and invoice_infos.get("name") != account_invoice.name
):
invoice_infos["name"] = (
(invoice_infos["name"] or "") + " " + account_invoice.name
)
if (
account_invoice.invoice_origin
and account_invoice.invoice_origin not in origins
):
invoice_infos["invoice_origin"] = (
(invoice_infos["invoice_origin"] or "")
+ " "
+ account_invoice.invoice_origin
)
origins.add(account_invoice.invoice_origin)
if (
account_invoice.payment_reference
and account_invoice.payment_reference not in client_refs
):
invoice_infos["payment_reference"] = (
(invoice_infos["payment_reference"] or "")
+ " "
+ account_invoice.payment_reference
)
client_refs.add(account_invoice.payment_reference)
for invoice_line in account_invoice.invoice_line_ids:
line_key = self.make_key(
invoice_line, self._get_invoice_line_key_cols()
)
o_line = invoice_infos["invoice_line_ids"].setdefault(line_key, {})
if o_line:
# merge the line with an existing line
for sum_field in sum_fields:
if sum_field in invoice_line._fields:
sum_val = invoice_line[sum_field]
if isinstance(sum_val, numbers.Number):
o_line[sum_field] += sum_val
else:
# append a new "standalone" line
o_line.update(self._get_invoice_line_vals(invoice_line))
allinvoices = []
allnewinvoices = []
invoices_info = {}
old_invoices = self.env["account.move"]
qty_prec = self.env["decimal.precision"].precision_get(
"Product Unit of Measure"
)
for invoice_key, (invoice_data, old_ids) in new_invoices.items():
# skip merges with only one invoice
if len(old_ids) < 2:
allinvoices += old_ids or []
continue
if remove_empty_invoice_lines:
invoice_data["invoice_line_ids"] = [
(0, 0, value)
for value in invoice_data["invoice_line_ids"].values()
if not float_is_zero(value["quantity"], precision_digits=qty_prec)
]
else:
invoice_data["invoice_line_ids"] = [
(0, 0, value) for value in invoice_data["invoice_line_ids"].values()
]
if date_invoice:
invoice_data["invoice_date"] = date_invoice
# create the new invoice
newinvoice = self.with_context(is_merge=True).create(invoice_data)
invoices_info.update({newinvoice.id: old_ids})
allinvoices.append(newinvoice.id)
allnewinvoices.append(newinvoice)
# cancel old invoices
old_invoices = self.env["account.move"].browse(old_ids)
old_invoices.with_context(is_merge=True).button_cancel()
self.merge_callback(invoices_info, old_invoices)
return invoices_info
@staticmethod
def order_line_update_invoice_lines(todos, all_old_inv_line):
for todo in todos:
for line in todo.order_line:
invoice_line = line.invoice_lines.filtered(
lambda x: x.parent_state != "cancel" or x.id not in all_old_inv_line
)
if invoice_line:
line.write({"invoice_lines": [(6, 0, invoice_line.ids)]})
@api.model
def merge_callback(self, invoices_info, old_invoices):
# Make link between original sale order
# None if sale is not installed
# None if purchase is not installed
if invoices_info:
all_old_inv_line = old_invoices.mapped("invoice_line_ids").ids
if "sale.order" in self.env.registry:
sale_todos = old_invoices.mapped(
"invoice_line_ids.sale_line_ids.order_id"
)
self.order_line_update_invoice_lines(sale_todos, all_old_inv_line)
if "purchase.order" in self.env.registry:
purchase_todos = old_invoices.mapped(
"invoice_line_ids.purchase_line_id.order_id"
)
self.order_line_update_invoice_lines(purchase_todos, all_old_inv_line)