Skip to content

[FIX] purchase_ux: reuse the stored PDF of the purchase order report - #367

Closed
jcadhoc wants to merge 1 commit into
ingadhoc:19.0from
adhoc-dev:19.0-h-126274-jc
Closed

[FIX] purchase_ux: reuse the stored PDF of the purchase order report#367
jcadhoc wants to merge 1 commit into
ingadhoc:19.0from
adhoc-dev:19.0-h-126274-jc

Conversation

@jcadhoc

@jcadhoc jcadhoc commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Sending a purchase order by email renders the same PDF 2 to 6 times, and that burst is what takes the instance down.

Rendering a PDF is circular: wkhtmltopdf resolves the report asset links against base_url, that is, against the very same Odoo instance. A render occupies one HTTP worker and needs another free one to answer itself. With workers = 3, three concurrent renders exhaust the three, nobody calls accept(), and everything stays blocked until limit_time_real (300s) — but the liveness probe kills the container at 120s, so the pod never recovers by itself.

The bursts come from the composer: attachment_ids is a compute with store=True, readonly=False, so the web client recomputes it on every round trip while the composer opens, and each recompute rendered the report again.

The change

Two fields on the two reports the purchase order mail templates carry. No Python.

<record id="purchase.action_report_purchase_order" model="ir.actions.report">
    <field name="attachment">'Purchase Order - %s - %s.pdf' % (object.name, object.write_date and object.write_date.strftime('%Y%m%d%H%M%S') or '')</field>
    <field name="attachment_use" eval="True"/>
</record>

With attachment and attachment_use set, _render_qweb_pdf_prepare_streams stores the generated PDF on the record and the next render returns the stored one without calling wkhtmltopdf. This is core's own mechanism — the same one account uses for invoices.

The write_date in the attachment name is the invalidation: an edited purchase order gets a new name and is rendered fresh, so the PDF is never frozen. The expression tolerates an empty write_date so printing can never break because of it.

Why this and not a code fix

The first version of this fix overrode _compute_attachment_ids and post-processed the report html to inline the asset bundles — around 290 lines across two modules. It was replaced by this: core already persists and reuses report PDFs, so the same behaviour is two data fields.

Raising workers was ruled out: an instance with 8 effective workers (workers = 4, replicas = 2) hit the same deadlock, and it would mean paying for capacity to render the same PDF several times.

Measurements

From the access logs of a production 19.0 instance, over 9 hours:

  • 259 renders, 508 requests to /web/assets/*report_assets* from the loopback address — exactly 2 self-requests per render.
  • 253 of 256 requests carry exactly one render, so the duplicates land on separate requests. That is why the reuse has to be persisted, which is what attachment does.
  • Across the cell, the only instances that went down are the three that produced bursts of 3 or more renders within 4 seconds.

What this does not do

The circular fetch of the two CSS bundles is still there — one render still asks itself for them. This removes the bursts, not the circularity. Closing that properly is a separate discussion on the platform side.

Trade-offs

  • The PDF becomes an attachment of the purchase order, named with the modification timestamp. Visible change, and it is core's behaviour for reports configured this way.
  • Filestore growth: one stored PDF per version of each document that gets printed or emailed. Re-prints of the same version become free, which cuts renders well beyond the composer.

Test plan

  • Open the send-by-email wizard on a purchase order, close it, open it again. Only one The PDF report has been generated in the log per document version, instead of one per round trip.
  • Check the loopback requests to /web/assets/*report_assets* during those openings: two for the first render, none for the reuses.
  • Edit the purchase order and reopen the wizard: a new render and a new stored attachment.
  • Print the report from the record: same reuse, and the emailed filename still comes from print_report_name, unchanged.
  • Same checks on a draft order, which uses report_purchase_quotation.

Verified against core: with these two fields set, render 1 calls wkhtmltopdf, renders 2 and 3 call it zero times, and a change to the record makes render 4 call it again.

Internal reference: https://www.adhoc.inc/odoo/helpdesk.ticket/126274

Rendering a PDF report is circular: wkhtmltopdf resolves the report asset links
against base_url, that is, against the very same Odoo instance. So a render
occupies one HTTP worker and needs another free one to answer itself. On an
instance with workers = 3, three concurrent renders exhaust the three workers,
nobody calls accept() and everything stays blocked until limit_time_real (300s);
the liveness probe kills the container at 120s, so the pod never recovers by
itself and the service is down for 2 to 4 minutes.

What brings three renders together is the mail composer: attachment_ids is a
compute with store=True and readonly=False, so the web client recomputes it on
every round trip while the composer is opening, and each recompute rendered the
same purchase order PDF again. Measured on production logs, the same document is
generated 2 to 6 times per opening of the composer, and the only instances that
went down are the ones that produced bursts of 3 or more renders within 4
seconds.

Rather than overriding that compute, use what core already offers: with
'attachment' and 'attachment_use' set on the report,
_render_qweb_pdf_prepare_streams stores the generated PDF on the record and the
next render returns the stored one without calling wkhtmltopdf. The write_date
in the attachment name is what invalidates it, so an edited purchase order is
rendered fresh instead of being frozen, and the expression tolerates an empty
write_date so printing can never break because of it.

Applied to the two reports the purchase order mail templates carry:
action_report_purchase_order and report_purchase_quotation.

Change note: al mandar una orden de compra por mail, el PDF se genera una sola
vez en lugar de 2 a 6 veces, y reimprimir la misma versión del documento reusa
el PDF ya generado. Si la OC se edita, se vuelve a generar. Como efecto visible,
el PDF queda guardado como adjunto de la OC, con la fecha de modificación en el
nombre del archivo.
@roboadhoc

Copy link
Copy Markdown
Contributor

Pull request status dashboard

@les-adhoc

Copy link
Copy Markdown
Contributor

@roboadhoc r+ bump

roboadhoc pushed a commit that referenced this pull request Sep 2, 2026
Rendering a PDF report is circular: wkhtmltopdf resolves the report asset links
against base_url, that is, against the very same Odoo instance. So a render
occupies one HTTP worker and needs another free one to answer itself. On an
instance with workers = 3, three concurrent renders exhaust the three workers,
nobody calls accept() and everything stays blocked until limit_time_real (300s);
the liveness probe kills the container at 120s, so the pod never recovers by
itself and the service is down for 2 to 4 minutes.

What brings three renders together is the mail composer: attachment_ids is a
compute with store=True and readonly=False, so the web client recomputes it on
every round trip while the composer is opening, and each recompute rendered the
same purchase order PDF again. Measured on production logs, the same document is
generated 2 to 6 times per opening of the composer, and the only instances that
went down are the ones that produced bursts of 3 or more renders within 4
seconds.

Rather than overriding that compute, use what core already offers: with
'attachment' and 'attachment_use' set on the report,
_render_qweb_pdf_prepare_streams stores the generated PDF on the record and the
next render returns the stored one without calling wkhtmltopdf. The write_date
in the attachment name is what invalidates it, so an edited purchase order is
rendered fresh instead of being frozen, and the expression tolerates an empty
write_date so printing can never break because of it.

Applied to the two reports the purchase order mail templates carry:
action_report_purchase_order and report_purchase_quotation.

Change note: al mandar una orden de compra por mail, el PDF se genera una sola
vez en lugar de 2 a 6 veces, y reimprimir la misma versión del documento reusa
el PDF ya generado. Si la OC se edita, se vuelve a generar. Como efecto visible,
el PDF queda guardado como adjunto de la OC, con la fecha de modificación en el
nombre del archivo.

closes #367

Signed-off-by: Luciano Esperlazza <les@adhoc.inc>
@roboadhoc roboadhoc closed this in d1491dc Sep 2, 2026
roboadhoc added a commit that referenced this pull request Sep 2, 2026
@roboadhoc
roboadhoc deleted the 19.0-h-126274-jc branch September 2, 2026 13:58
@roboadhoc roboadhoc added the 18.1 label Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants