sale_pdf_quote_builder/models/ir_actions_report.py

151 lines
7.1 KiB
Python
Raw Normal View History

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import io
from PyPDF2 import PdfFileWriter, PdfFileReader
from PyPDF2.generic import NameObject, createStringObject
from odoo import models
from odoo.tools import format_amount, format_date, format_datetime, pdf
class IrActionsReport(models.Model):
_inherit = 'ir.actions.report'
def _render_qweb_pdf_prepare_streams(self, report_ref, data, res_ids=None):
result = super()._render_qweb_pdf_prepare_streams(report_ref, data, res_ids=res_ids)
if self._get_report(report_ref).report_name != 'sale.report_saleorder':
return result
orders = self.env['sale.order'].browse(res_ids)
for order in orders:
initial_stream = result[order.id]['stream']
if initial_stream:
order_template = order.sale_order_template_id
header_record = order_template if order_template.sale_header else order.company_id
footer_record = order_template if order_template.sale_footer else order.company_id
has_header = bool(header_record.sale_header)
has_footer = bool(footer_record.sale_footer)
included_product_docs = self.env['product.document']
doc_line_id_mapping = {}
for line in order.order_line:
product_product_docs = line.product_id.product_document_ids
product_template_docs = line.product_template_id.product_document_ids
doc_to_include = (
product_product_docs.filtered(lambda d: d.attached_on == 'inside')
or product_template_docs.filtered(lambda d: d.attached_on == 'inside')
)
included_product_docs = included_product_docs | doc_to_include
doc_line_id_mapping.update({doc.id: line.id for doc in doc_to_include})
if (not has_header and not included_product_docs and not has_footer):
continue
writer = PdfFileWriter()
if has_header:
self._add_pages_to_writer(writer, base64.b64decode(header_record.sale_header))
if included_product_docs:
for doc in included_product_docs:
self._add_pages_to_writer(
writer, base64.b64decode(doc.datas), doc_line_id_mapping[doc.id]
)
self._add_pages_to_writer(writer, initial_stream.getvalue())
if has_footer:
self._add_pages_to_writer(writer, base64.b64decode(footer_record.sale_footer))
form_fields = self._get_form_fields_mapping(order, doc_line_id_mapping)
pdf.fill_form_fields_pdf(writer, form_fields=form_fields)
with io.BytesIO() as _buffer:
writer.write(_buffer)
stream = io.BytesIO(_buffer.getvalue())
result[order.id].update({'stream': stream})
return result
def _add_pages_to_writer(self, writer, document, sol_id=None):
prefix = f'{sol_id}_' if sol_id else ''
reader = PdfFileReader(io.BytesIO(document), strict=False)
sol_field_names = self._get_sol_form_fields_names()
for page_id in range(0, reader.getNumPages()):
page = reader.getPage(page_id)
if sol_id and page.get('/Annots'):
# Prefix all form fields in the document with the sale order line id.
# This is necessary to avoid conflicts between fields with the same name.
for j in range(0, len(page['/Annots'])):
reader_annot = page['/Annots'][j].getObject()
if reader_annot.get('/T') in sol_field_names:
reader_annot.update({
NameObject("/T"): createStringObject(prefix + reader_annot.get('/T'))
})
writer.addPage(page)
def _get_sol_form_fields_names(self):
""" List of specific pdf fields name for an order line that needs to be renamed in the pdf.
Override this method to add new fields to the list.
"""
return ['description', 'quantity', 'uom', 'price_unit', 'discount', 'product_sale_price',
'taxes', 'tax_excl_price', 'tax_incl_price']
def _get_form_fields_mapping(self, order, doc_line_id_mapping=None):
""" Dictionary mapping specific pdf fields name to Odoo fields data for a sale order.
Override this method to add new fields to the mapping.
:param recordset order: sale.order record
:rtype: dict
:return: mapping of fields name to Odoo fields data
Note: order.ensure_one()
"""
order.ensure_one()
env = self.with_context(use_babel=True).env
tz = order.partner_id.tz or self.env.user.tz or 'UTC'
lang_code = order.partner_id.lang or self.env.user.lang
form_fields_mapping = {
'name': order.name,
'partner_id__name': order.partner_id.name,
'user_id__name': order.user_id.name,
'amount_untaxed': format_amount(env, order.amount_untaxed, order.currency_id),
'amount_total': format_amount(env, order.amount_total, order.currency_id),
'delivery_date': format_datetime(env, order.commitment_date, tz=tz),
'validity_date': format_date(env, order.validity_date, lang_code=lang_code),
'client_order_ref': order.client_order_ref or '',
}
# Adding fields from each line, prefixed by the line_id to avoid conflicts
lines_with_doc_ids = set(doc_line_id_mapping.values())
for line in order.order_line.filtered(lambda sol: sol.id in lines_with_doc_ids):
form_fields_mapping.update(self._get_sol_form_fields_mapping(line))
return form_fields_mapping
def _get_sol_form_fields_mapping(self, line):
""" Dictionary mapping specific pdf fields name to Odoo fields data for a sale order line.
Fields name are prefixed by the line id to avoid conflict between files.
Override this method to add new fields to the mapping.
:param recordset line: sale.order.line record
:rtype: dict
:return: mapping of prefixed fields name to Odoo fields data
Note: line.ensure_one()
"""
line.ensure_one()
env = self.with_context(use_babel=True).env
return {
f'{line.id}_description': line.name,
f'{line.id}_quantity': line.product_uom_qty,
f'{line.id}_uom': line.product_uom.name,
f'{line.id}_price_unit': format_amount(env, line.price_unit, line.currency_id),
f'{line.id}_discount': line.discount,
f'{line.id}_product_sale_price': format_amount(
env, line.product_id.lst_price, line.product_id.currency_id
),
f'{line.id}_taxes': ', '.join(tax.name for tax in line.tax_id),
f'{line.id}_tax_excl_price': format_amount(env, line.price_subtotal, line.currency_id),
f'{line.id}_tax_incl_price': format_amount(env, line.price_total, line.currency_id),
}