← All articles

Checking Quebec subcontractors in Business Central: RBQ licence, Revenu Québec, CNESST, CCQ

By Fred Morin ·

Business version (in French): Licence RBQ, attestation de Revenu Québec, CNESST : Business Central peut vérifier vos sous-traitants

Share this articleLinkedInFacebookEmail

In Quebec, the company that hires a subcontractor carries part of the risk when that subcontractor isn't in good standing. Four documents matter, and in most companies they live in email threads and spreadsheets, disconnected from the ERP that actually sends the purchase orders and the payments.

So I built a Business Central extension that checks them where the money moves: on purchase release and posting, and on payment posting. This post covers what it does and how it is put together, with enough detail for an AL developer to get started.

This is about helping with compliance, not legal advice. The rules below are summarized from the RBQ, Revenu Québec, the CNESST and the CCQ, and the Autorité des marchés publics for the RENA.

Updated October 7, 2026: the extension now also checks the RENA on public contracts. See the last row of the table, and steps 8 and 9.

The four documents, and one register

Document Rule When the ERP should care
RBQ licence The contractor needs an active licence in the right subcategory. Using an unlicensed contractor exposes a company to fines of $32,267 to $161,334 Purchase release and posting
Revenu Québec attestation (ARQ) Required once construction contracts with the same subcontractor reach $25,000 before taxes in a calendar year. Copy by the 7th day of work, authenticity verified by the 10th. Valid until the end of the month of issue plus 3 months Purchases, once the threshold is reached
CNESST letter of compliance Without it at the end of the contract, the CNESST can claim the contractor's unpaid premiums from you Payments
CCQ status letter The general contractor is jointly liable for wages owed to a subcontractor's workers Payments
RENA (public contracts only) A business on the register of enterprises ineligible for public contracts, kept by the Autorité des marchés publics, cannot be a subcontractor on a public contract, and the contractor must check before subcontracting (Act respecting contracting by public bodies, s. 21.5.5 and 21.11). Fines of $7,500 to $40,000 for a company, for both parties (s. 27.8) Purchases and payments flagged as a public contract

What it does

A Contractors at Risk page lists every flagged subcontractor that needs attention, most urgent first.

The Contractors at Risk page, in French: an RBQ licence not found, an expired Revenu Québec attestation, a missing CNESST letter, an attestation expiring soon

One subcontractor per situation. The demo runs in Quebec French; every string is translated.

The RBQ licence is checked against the RBQ's own data, live, and again every night. When it isn't in the list of active licences, the purchase order doesn't go out.

Animation: the RBQ check returns Not Found, then releasing the purchase order is stopped with Show issues and Override

The extension sums the year's purchases per subcontractor, including the document being released, and requires the Revenu Québec attestation once the threshold is crossed.

Release stopped because the Revenu Québec attestation on file expired, with $31,500 of purchases this year in the FactBox

On the payment journal, the CNESST and CCQ letters are checked before posting. Add the missing document, and the payment goes through.

Animation: the payment journal flags the missing CNESST letter, the letter is added, the contractor turns Compliant and the payment posts

On a public contract, the subcontractor is also checked against the RENA. The extension downloads the register every week and matches vendors by NEQ, the Quebec enterprise number. A purchase order flagged as a public contract is stopped when the subcontractor is listed; without the flag, the listing is shown for information, since a listed business can still do private work.

How I built it

About 60 AL objects. Here's the recipe.

1. Model the rules as data

A Document Type table holds each document: its kind (RBQ, ARQ, CNESST, CCQ or your own, like an insurance certificate), when it's required (on purchases, once the ARQ threshold is reached, or before payment), the enforcement (None, Warn, Block) and a default validity as a DateFormula (<CM+3M> for the ARQ: end of the month of issue plus three months).

A Vendor Document table holds what each subcontractor gave you: number, issue date, expiry date, received date, and who verified it and when. Two setup profiles (a construction company, or a business hiring contractors for its own buildings) filter which types apply.

2. Read the RBQ open data

The RBQ publishes its active licences daily on Données Québec, behind a public CKAN datastore_search API. No key needed:

Url := StrSubstNo('%1?resource_id=%2&filters=%3', ApiUrl, ResourceId,
    Uri.EscapeDataString(StrSubstNo('{"Numero de licence":"%1"}', LicenceNo)));
HttpClient.Get(Url, Response);

The response has one record per subcategory. An unknown or suspended licence returns zero records, which is your "not found". I cache status, restriction, holder name, NEQ and subcategories on the vendor, with a timestamp. A job queue entry refreshes them nightly, and each lookup runs in its own Codeunit.Run so one failure doesn't stop the batch. In a sandbox, remember to turn on Allow HttpClient Requests for the extension.

A light sanity check compares the RBQ holder name with the vendor name. A licence number typed with one wrong digit can point to a real, active, completely different company.

3. Sum the threshold from ledger entries

The ARQ threshold is a sum of Purchase (LCY) on the vendor ledger entries for the calendar year, plus the amount of the document being released. Once any year since the rule started crossed it, the attestation is required for every later contract, so the check looks back, not just at this year.

4. One check function, many callers

Like the Bill 96 extension, a single codeunit takes a vendor, a context (purchase, payment, review), a date and an amount, and fills a temporary table of issues. It writes nothing. The same function feeds the release check, the payment check, the FactBox, the notification and the at-risk worksheet.

5. Hook purchases and payments

  • Release Purchase Document: OnBeforeReleasePurchaseDoc
  • Purch.-Post: OnBeforePostPurchaseDoc
  • Gen. Jnl.-Post Batch: OnBeforeProcessLines, filtered on payment lines to a vendor

Errors are an ErrorInfo with Show issues and Override... actions, and overrides go into a log keyed by a SHA-256 fingerprint of the vendor, the document and the exact issues. If the issues change, the override no longer applies.

6. The trap: journals swallow your error actions

Release and posting show the ErrorInfo with its buttons. Journal posting doesn't. Gen. Jnl.-Post Batch collects errors into the Error Messages page, and the actions are gone: the user sees why the payment was stopped, with no way to fix or override it.

The fix is to give the payment journal its own Check and Override... actions that call the same code, so the user can resolve the issue before posting. Test your error paths in a journal, not just on documents.

7. Translations aren't only labels

The document type descriptions are data, saved in the language of whoever set up the company. My first French run-through showed "Aucune CCQ status letter au dossier". Messages about the official documents now use labels by document kind, and only custom types fall back to the stored description.

8. A register that is a file, not an API

The RENA is a monthly JSON file on Données Québec, about 1,800 entries, with no datastore to query. Three things are worth copying.

Resolve the file's address at run time. The dataset page lists its files, and their addresses can change from one publication to the next. Ask CKAN's package_show for the dataset and take the resource whose format is JSON:

foreach ResourceToken in Resources do
    if UpperCase(GetText(ResourceToken.AsObject(), 'format')) = 'JSON' then
        exit(GetText(ResourceToken.AsObject(), 'url'));

Validate before you replace. Parse the file into a temporary table first. An unreadable file, an empty one, or one with less than half of the rows already on file is refused: the previous copy stays, and the reason is shown on the setup page. A compliance check that quietly empties its own reference data is worse than no check.

Store less. The file names individuals: related persons, majority shareholders, the municipality where they live. Matching only needs the NEQ, so those fields are never read. That is a Law 25 question answered by not having the data.

One surprise in the data: provisional entries have no flag. The only sign is the words "Inscription provisoire" in the decision text, and no end date, which is exactly what section 21.7 of the Act prescribes.

9. What the tests caught

The RENA check came with a test app, and its first run earned its keep:

  • 45 days don't fit in an Integer. "Out of date after 45 days" was written as a difference of DateTimes compared with 45 * 24 * 60 * 60 * 1000 milliseconds. That is 3.9 billion, above the 2,147,483,647 an Integer holds. The compiler refused the constant in the test; in the app, where the 45 came from a setup field, it would only have failed at run time. Compare dates instead.
  • asserterror rolls back your test data. A test that expects an error and then continues must Commit() its setup first.
  • A 10-character NEQ field is exactly too short. An NEQ is 10 digits, but people type "1172 411 358". Ignoring spaces and dashes only helps if the field can hold them.

Where it can go next

Per-project tracking of work start and end dates (the CNESST letter is per contract), reminders emailed to subcontractors before their documents expire, and reading the dates from an uploaded PDF instead of typing them.

If you run Business Central with Quebec subcontractors and want this working in your company, adapted to your rules and your documents, let's talk.

Share this articleLinkedInFacebookEmail