diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0332be301..23bac4601 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,9 +6,9 @@ This file documents all notable changes made to ITFlow.
### Upgrading to 26.11
-Update from Maintenance > Update — Queue Update hands the job to cron, which updates the files and applies this release's database changes (2.7.9 to 3.0.9) in the same pass. Nothing else is required.
+Update from Maintenance > Update — Queue Update hands the job to cron, which updates the files and applies this release's database changes (2.7.9 to 3.1.1) in the same pass. Nothing else is required.
-This is a big release, with thirty database steps in one go, so take a backup before you update.
+This is a big release, with thirty-two database steps in one go, so take a backup before you update.
The update also clears out data that deletes in older versions left behind, all of it pointing at something that no longer exists:
@@ -31,6 +31,8 @@ Other income already entered (money in that isn't for an invoice, which used to
Every account is given a kind - checking, savings, credit card, cash or payment processor - guessed from its name: Visa, Amex or card is a credit card, savings or reserve is savings, cash or petty cash is cash, and Stripe, PayPal, Square or merchant is a payment processor. An account given a liability type back when accounts had types (2023) is a credit card, so what is on it reads as owed. Anything else is checking. Change any that came out wrong with Edit on the Accounts page. An account's balance now leaves out archived payments, income and expenses everywhere, so the Accounts page can show less or more than before (see Bug Fixes). No account is reconciled after the update. The first statement you reconcile on each can start from the bank's balance on the day it starts, bringing everything before it forward as reconciled, or from the account's opening balance (see New Features, Accounts and transactions). Transfers already made out of a cash account count as having taken everything in it up to then to the bank, so the first deposit you make after the update only offers the checks and cash taken in since the last one.
+Taxes keep their names and rates, now kept to four decimal places. No client has a rate of its own or is tax exempt, and there is no default rate, so new lines start untaxed as before until you pick one in Admin > Taxes. Billed time, trips and expenses stay untaxed until you switch them on there. Discounts now come off before tax on invoices and quotes that are still open, the next time they are edited (see Breaking Changes, Taxes).
+
If you are coming from 26.08 or earlier, the database update still has to be run from the command line one time, exactly as described under Upgrading to 26.09 below.
### Breaking Changes and Notes
@@ -119,6 +121,12 @@ If you are coming from 26.08 or earlier, the database update still has to be run
- undoing a reimbursement whose money out has cleared
- An account brought forward as reconciled (see New Features, Accounts and transactions) has everything before the day it was brought forward to locked the same way, until that is undone.
- An account's opening balance can't be changed by hand once a statement on it is reconciled, as the first statement starts from it. On an account brought forward from the bank's balance, Correct the opening balance can still move it once.
+- Taxes:
+ - Discounts come off before tax. On an invoice, quote or recurring invoice with a discount, tax is worked out on what the client pays after it, the discount shared across the lines by their amounts. It used to be worked out on the full price. Paid, cancelled and non-billable invoices, and quotes that are answered or invoiced, never change. Others follow the next time a line or the discount is edited, and a recurring invoice when it next bills. To keep taxing the full price, switch off Take discounts off before tax in Admin > Taxes.
+ - Changing the percent of a rate that is on an invoice or quote that has gone out now makes a new rate with the same name, and archives the old one. What was billed keeps the old rate, on the page, the PDF and the Tax Collected report, where the two show as separate rows. Products, clients, the default and recurring invoices move to the new rate, and draft invoices and quotes do too unless you untick it. It used to change the rate in place, so an invoice already sent showed the new percent next to tax worked out at the old one. A rate's name, description and registration number still change in place.
+ - A rate can only be deleted when nothing has ever used it - archive it otherwise. Archiving asks where to move the products, clients, recurring invoices, drafts and default that use it. Left where they are, recurring invoices and drafts keep charging it, and new lines for its products and clients take the next rate along.
+ - Tax Rates in the Admin menu is now Taxes.
+ - A line on a rate made of parts is taxed part by part, each to the cent, so its tax can be a cent off the whole rate times the line.
- Notifications now follow access too. An agent only sees notifications for clients and areas they can open, so someone without sales access no longer sees invoice and quote notifications.
- Drop-downs: Tom Select is gone, replaced by ITFlow's own picker. It works the way you are used to: type to search, tick several in a multi-select, and type a new value in fields that allow it, such as ticket watchers. See Developer Updates if you have custom pages that used Tom Select.
- Several small windows are gone because the change is now made where the value is shown:
@@ -713,6 +721,18 @@ If you are coming from 26.08 or earlier, the database update still has to be run
- Announcements: Admin > Client Portal posts a notice at the top of the portal's home page - to every client or one - as a Notice, Heads up or Urgent, from when to when you choose. Web addresses in it become links.
- View as contact: on a contact's page, View portal as them opens the portal in a new tab as they see it, read only - nothing can be sent, paid or changed, opening an invoice doesn't mark it viewed, and a strip along the top says whose view it is, with Exit. It needs read access to clients and support and to the client; billing shows only when your role reads sales and financial too. Each one is in the audit log.
- The agent's contact page shows whether a contact signs in with a code too, when they last signed in, and the ticket emails they chose.
+- **Taxes** (rebuilt)
+ - Admin > Taxes lists each rate with what it comes to, its parts, its registration number and what uses it: the invoices, quotes and recurring invoices with a line at it, and the products and clients that pick it. Current and Archived tabs, with what replaced a rate and when, Restore, Make default, and Delete for a rate nothing has used.
+ - A rate can be made of parts, such as a state's 6% and a county's 1%, or GST and QST. A part can be charged on top of the parts above it. The whole rate shows as you type. Invoices, quotes, their PDFs and the client's views show each part on its own line in the totals, such as Pennsylvania 6% and Allegheny County 1%.
+ - Rates go to four decimal places, such as Quebec's 9.975%, and lines and the Tax Collected report show the exact rate.
+ - Each rate takes a description and a registration number, and each part can have its own number. The numbers a document charges under print with your company's details on the invoice or quote and its PDF, such as GST No. 123456789 RT0001.
+ - Edit on a rate that has gone out on invoices or quotes says how many, and that a new rate or parts makes a new rate from now on, with a switch to move the draft invoices and quotes too.
+ - How lines are taxed, on the same page: the default rate for new lines, whether billed time, trips and expenses billed to clients are taxed, and whether discounts come off before tax.
+ - Each client can have its own rate, on the Billing tab of its Edit window, for where it is. New lines start on the client's rate, then the product's, then the default, whether typed in, picked from the catalog, or made from tickets, Unbilled Time, trips and expenses. A product set to no tax stays untaxed. Unbilled Time bills each line at its client's usual rate unless you pick one, and the ticket's Invoice and Quote windows, Add Tickets, Add Expenses and Add Trips start on it.
+ - Tax exempt clients, with the certificate number and when it expires, on the Billing tab. Their new lines start untaxed, and their pages say Tax exempt, or that the certificate has run out. An open invoice, quote or recurring invoice that still has taxed lines for them says so, with Take the tax off. An invoice or quote with no tax on it says Tax exempt with the certificate number under the client's name.
+ - The client overview's Terms shows the client's rate, or that it is tax exempt.
+ - API: clients take `client_default_tax_id`, `client_tax_exempt`, `client_tax_exempt_number` and `client_tax_exempt_expires`, and a new invoice line takes `tax_id` `default` for the client's usual rate.
+ - Demo data: both taxes have a registration number, Allegheny County Sales Tax is made of Pennsylvania's 6% and the county's 1%, each client has its rate, and the church and the charter school are tax exempt, one with a certificate that has run out.
### Bug Fixes
@@ -1139,6 +1159,16 @@ If you are coming from 26.08 or earlier, the database update still has to be run
- `agent/post/contact.php`: `portal_view_as` and `reset_contact_portal_mfa` (GET, with the CSRF token).
- Admin: `admin/settings_client_portal.php` with `admin/post/settings_client_portal.php` (`edit_client_portal_settings`) and `admin/post/portal_announcement.php` (`add_portal_announcement`, `edit_portal_announcement`, `archive_`/`restore_`/`delete_portal_announcement`), and the windows in `admin/modals/portal_announcement/`.
- CSS: `.itflow-portal-announcement` with `-icon`, `-title`, `-body` and `is-warning`/`is-danger`, `.itflow-portal-viewas` and `.itflow-announcement-dot`.
+- Taxes (database 3.1.1):
+ - `taxes.tax_percent` is `decimal(9,4)` (it was a float), with new `tax_description`, `tax_registration_number` and `tax_replaced_by` (0, the rate a billed rate was changed to). New table `tax_components` (`component_tax_id` keyed, `component_name`, `component_percent` `decimal(9,4)`, `component_compound`, `component_registration_number`, `component_order`) - a rate without rows is its own single part, and `tax_percent` is the whole rate its parts come to. New `clients.client_default_tax_id` (0, the default), `client_tax_exempt`, `client_tax_exempt_number` and `client_tax_exempt_expires`, and `settings.config_tax_default_id`, `config_tax_time`, `config_tax_trips`, `config_tax_expenses` (all 0) and `config_tax_discount_before` (1).
+ - The new `functions/tax.php` (loaded after `billing_pay.php`): `taxSettings()`, `taxRow($tax_id)` (cached, with `components` - name, percent, compound, registration - and `grouped`) and `taxForget()`, `taxPercentText()`, `taxLabel()`, `taxEffectivePercent($components)`, `taxAmounts($base, $tax)` (`tax` and `parts` - label, name, percent, amount, registration, registration_for - each part to the cent), `taxCurrent($tax_id)` (follows `tax_replaced_by` to a current rate, else 0), `taxClient($client_id)`, `taxClientSetSql($in, $current)`, `taxExemptText($client, $with_expiry)`, `taxDocWarning($type, $doc_id, $client_id, $items, $open, $can_edit)`, `taxDefaultFor($client_id, $kind, $product_tax_id)` (`goods`, `time`, `trip`, `expense`, `product`), `TAX_USUAL` (-1) and `taxChoice($input, $client_id, $kind)`, `taxUsage()`, `taxUsageAll()`, `taxBilled($tax_id)` (on an invoice or quote past draft), `taxFromInput($in)`, `taxSameRate()`, `taxCreate()`, `taxUpdate($tax_id, $fields, $move_drafts)`, `taxChangeRate()`, `taxRepoint($from, $to, $move_drafts)`, `taxDocIds($type, $tax_id)`, `taxRecalcOpen()`, `taxArchive($tax_id, $move_to)`, `taxRestore()`, `taxUnused()`, `taxDelete()`, `taxSetDefault()`, `taxMovedText()`, `taxOptionsHtml($taxes, $selected, $usual_label, $usual_percent)` and `taxSelectOptions()`.
+ - `functions/billing.php`: `billingLineTax($quantity, $price, $tax_id, $base)`, `billingLinesOpen($type, $status)`, `billingDiscountShares($items, $discount)` (by subtotal, to the cent, largest remainder), `billingRecalcTaxes($type, $doc_id, $discount)`, `billingTaxParts($items)`, `billingRemoveTax($type, $doc_id)` and `billingTaxRegistrationLines($totals)`. `billingRecalc()` works each line's tax out again while its lines can change - on a quote, the discount comes off the one-time group, else the first recurring one. `billingTotals()` `taxes` are by part, with `tax_registrations` added, and `quoteLineGroups()` the same per group. `billingBillTo()` adds `tax_exempt_text`. `recurringInvoiceRefresh()` updates quantities and subtotals and leaves the tax to `billingRecalc()`. `billingItemSave()` checks the rate exists. `billingTimeInvoice()`, `expenseBillInvoice()` and `tripBillInvoice()` take `TAX_USUAL`.
+ - `getProductsForAutocomplete($mysqli, $client_id)` gives each product's `tax` and `tax_percent` for that client. The line windows, the add-a-line row, Unbilled Time and the ticket Invoice and Quote windows build their tax select with `taxOptionsHtml()`.
+ - Admin: `admin/tax_rates.php` is rebuilt, with `admin/modals/tax/tax_form.php` shared by `tax_add.php` and `tax_edit.php`, the new `tax_archive.php`, and `js/tax_form.js`. `admin/post/tax.php`: `add_tax` (`name`, `description`, `registration`, `percent`, or `part_name[key]`, `part_percent[key]`, `part_compound[key]` and `part_registration[key]`; `make_default`), `edit_tax` (`tax_id`, `drafts_asked`, `move_drafts`), `archive_tax` (now POST, `tax_id`, `move_to`), GET `restore_tax`, `default_tax` and `delete_tax`, and `edit_tax_settings` (`default_tax_id`, `tax_time`, `tax_trips`, `tax_expenses`, `tax_discount_before`).
+ - Clients: `agent/modals/client/client_tax_fields.php` on the Billing tab of `client_add.php` and `client_edit.php` posts `tax_fields`, `default_tax_id`, `tax_exempt`, `tax_exempt_number` and `tax_exempt_expires`, saved by `agent/post/client.php` through `taxClientSetSql()`. `api/v1/clients/client_model.php` does the same from the API fields. GET `billing_remove_tax` (the document's id, `type`) in `agent/post/billing.php`, at Sales 2.
+ - `api/v1/invoice_items/create.php` takes `tax_id` `default` (a product's line by `taxDefaultFor()` with `product`) and works the invoice out again with `billingRecalc()`.
+ - Seeds: `demoDataTaxes()` rows add a description, registration number and parts, demo clients get `client_default_tax_id`, and a profile's `tax_exempt` (number, expiry) makes it exempt. `demoDataRemove()` counts clients using a rate and removes its parts.
+ - CSS: `.itflow-paper-tax-label`, so a line's rate wraps under its tax.
- Empty states: every list page sets `$empty_state` before including `includes/filter_footer.php`, and `includes/inc_empty_state.php` draws it with `emptyStateHtml()` (`functions/format.php`) when the list has nothing - `thing`, `icon`, `title`, `text`, `actions` (`label`, `modal` or `href`, `size`, `icon`; the first is the main one), `archived`, `view`, `filtered`, `views`, `match_title`, `match_text` and `clear_url`. `emptyStateFiltered($views)` reads the GET string for a search or filter, leaving out paging, sorting, `client_id`, `archived` and the page's views, and `emptyStateClearUrl($views)` is the page without them. `emptyStatePill($pills, $key, $default, $good)` builds a pill's own state from the page's pill array (`label`, `icon`, `title`, `alarm`), with a tick for `alarm` pills and the keys in `$good`. Files, notifications and racks call `emptyStateHtml()` in their own layout; files and notifications set `$empty_state_skip`. A page that sets nothing still gets one, worded from `$page_title`. The pages' own empty blocks (`.itflow-asset-empty` under a list) are gone. CSS: `.itflow-empty-state` with `-icon` (a grey round tile), `-title`, `-text` and `-actions`.
- Removed unused helpers and files:
- `credentialLink()`, `projectLink()`, `linkCounts()`, `assetContactAvatar()` and `clientOverviewPersonInitials()`
diff --git a/admin/database_updates/3.1.1.php b/admin/database_updates/3.1.1.php
new file mode 100644
index 000000000..1a7841f23
--- /dev/null
+++ b/admin/database_updates/3.1.1.php
@@ -0,0 +1,52 @@
+
- Tax Rates Taxes