My client has advanced invoice requirements. In the past we had a custom Drupal module and a custom template file. Added a link "Print Invoice" to the individual contributions Form.tpl and then did a custom template file in Drupal using CiviCRM API to get the relevant data. For Batch, we added a new custom task and called our custom function in a loop.

But now, of course, CiviCRM has invoicing capability baked in, which is awesome! We edited the System Workflow Message but we had a lot of trouble using crmapi calls, and using Smarty. We also see that {php} is deprecated in Smarty 3.

So then I had the thought to just override the callback for civicrm/contribute/invoice. I did that by implementing hook_civicrm_xmlMenu() and providing a custom Menu.xml that uses my own class. My class just extends CRM_Contribute_Form_Task_Invoice and then I just pull in the full objects whose data I need in my own printPDF(). (Specifically, if it's an event, I want to show event start dates, etc - and if it's a membership I want to show them their membership expiration date, etc).

The problem is that the batch invoice processor seems to be hardcoded to use CRM_Contribute_Form_Task_Invoice, and I can't find a way to override it. I've tried the same approach (overriding civicrm/contribute/search in Menu.xml) but that barfs in so many places. I've looked at hook_preProcess and changing the _attributes, etc. I've tried adding my own custom task to the dropdown on the contribute search page, but then it barks about a missing Invoice.tpl for my custom implementation.

Long story short, I'm just not 100% sure on the best way to proceed, and of course I'd rather not just hack the core code, but would rather extend it somehow. We've had a past history of nervousness to upgrade CiviCRM, and I want to not feel like we're in that same boat after this upgrade (to Drupal 7 / CiviCRM 4.7). :)

  • Want to confirm what version you are (or want to be) using
    – petednz - fuzion
    Commented May 23, 2016 at 19:16
  • We are currently one point release back; but 4.7 on Drupal 7. Commented May 23, 2016 at 19:22

3 Answers 3


The short answer is that you can't change that from an extension, but you can change it by patching core. Unfortunately, this requires a change in coding-convention which isn't a trivial one-line patch -- it requires understanding some other layers/concepts and changing multiple files.

The root issue here is that the coding convention has relied too heavily on static functions. For example, if you grep the codebase for CRM_Contribute_Form_Task_Invoice, you'll find snippets like this one (from CRM/Contribute/Form/Task/Invoice.php):

CRM_Contribute_Form_Task_Invoice::printPDF($contributionIDs, $params, $contactId, CRM_Core_DAO::$_nullObject);

If you could change that line to point to your own version of printPDF(), you'd be ready to go. But it's hard-coded -- there's no way for an extension to change it. The good news: there are better patterns which are more flexible, e.g.

// Example: Use a service container
Civi::service('invoice_printer')->print($contributionIDs, $params, $contactId, CRM_Core_DAO::$_nullObject);

// Example: Use a hook
CRM_Utils_Hook::printInvoicePdf($contributionIDs, $params, $contactId, CRM_Core_DAO::$_nullObject);

Either of those formulations would make it possible for extensions to change the implementation of printPDF(). The bad news: either way, you'd to have to refactor the existing implementation to match a different coding convention.

A few links that might be interesting on the general topic of statics, services, and dependency-injection:

  • OK, that's understandable (to an extent :) ). Which makes me think, couldn't I somehow prototype this as a new action from the dropdown from the contribution search? If so, how might I actually do that? Commented May 24, 2016 at 14:58

The way I ended up having to do this was simple, but a less-than-ideal UX. My UF is Drupal.

I used Views to build a view of contributions, to somewhat mimic the native contribution search. (Specifically I added in status of pending, and is pay later), to find contributions for which invoices likely needed to be generated. I grabbed a few less columns.

I then added in civicrm_entity to the mix, so that I could use Views Bulk Operations in Drupal. The action I chose was "pass the id's as arguments to a page," and then I wrote a custom menu callback for that page that grabs the ids and passes them into my overridden printPDF() call. That call natively supports being passed multiple ID's, so it "just worked" to print multiple invoices into a single PDF.

The downside is that my client needs to go to a special menu item I built for him when he wants to do batch invoicing, but that's livable in our particular use case.


I was lead-astray by this post, combined with my own dumb thinking. I'm not sure why the hook I finally found would not work for OP's problem, but have not looked at the bulk action. I kind of suspect the existing hook would have worked as well.

I wanted to override the invoice template and I traced civicrm/contribute/invoice to printPDF().

"I want to override the template to download an invoice. Hmmm... I don't see any hooks to override printPDF... Ah, Tim says I should create a new hook in Core, Okay!"

Got through creating a new hook for printPdfTemplateParams to be raised each time before printPDF() calls CRM_Core_BAO_MessageTemplate::sendTemplate($sendTemplateParams);. Then when I went to get more details on what I need to alter in the params and finally realized that CRM_Core_BAO_MessageTemplate raises hook_civicrm_alterMailParams before loading the template. Even if you are just downloading a PDF, not sending an email. Doh!

If anyone wants the code for the new hook, I'm not submitting a PR at this time. https://github.com/ginkgomzd/civicrm-core/tree/hook-print-pdf-params

Created a PR to update the dev docs for hook_civicrm_alterMailParams: https://lab.civicrm.org/documentation/docs/dev/-/merge_requests/1054

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.