diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ed897a6b..f5be898cf 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.1.7) 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.8) in the same pass. Nothing else is required. -This is a big release, with thirty-seven database steps in one go, so take a backup before you update. +This is a big release, with thirty-eight 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: @@ -39,6 +39,10 @@ Categories keep their names, colors, icons, order and records. A color saved as API keys keep their names, end dates and the agent they act as. Admin > API Keys shows each as never used until its next call. A key acting as an agent who is disabled or archived, or has no role, was already turned away by the API, and is now marked Won't work with the reason (see New Features, API Keys). +The built-in Accountant role gets full access to Financial, so it can delete an expense, other income, a transfer or a payment entered by mistake - but only if it is still as it came: named Accountant, with Clients, Support, Sales and Reports at Read and Financial at Modify. One you have changed keeps what you gave it (see Breaking Changes, Roles). + +What the API makes from now on - tickets, replies, clients and the rest - is stamped in ITFlow's timezone, as the pages stamp what they make. What it made before keeps the server's time, which on a server set to UTC is hours off (see Bug Fixes). + 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 @@ -107,7 +111,7 @@ If you are coming from 26.08 or earlier, the database update still has to be run - A trip's miles are the whole trip. The form asks for the miles one way and doubles them for a round trip. The old form saved the number as typed, whether Round Trip was ticked or not, so an older round trip entered as the miles one way still counts once. Edit those to correct them. - The database column `trips.trip_end_odmeter` is now `trip_end_odometer`. Anything of your own that reads it, such as a custom report, needs the new name. - Income: - - Money in that isn't for an invoice is now called other income (it was revenue). Adding and changing it needs write access to Financial, and deleting it full access. It used to need Sales. Payments keep the invoice's rules, full access to Sales and Financial. + - Money in that isn't for an invoice is now called other income (it was revenue). Adding and changing it needs write access to Financial, and deleting it full access. It used to need Sales. Payments go by Financial too, with Sales at Read (see Roles). - Other income counts whether it has a category or not, on the Income list, the dashboard, the client overview and every report, as No category when it has none. The dashboard and reports used to leave it out until it had a category, so their income can go up after the update by what had none. Money coming in from a transfer between your own accounts is still not income. - New other income takes the currency of the account it goes into. It used to take the company's currency whatever the account. - The Income list's Bulk menu has Set Category, Set Account, Set Payment Method and the new Set Client, a window each, and Delete. A payment's category is its invoice's, so Set Category on a payment sets its invoice's category, as before. A payment's client is its invoice's, so Set Client leaves payments alone, and payments are deleted from their invoice, not here. @@ -172,6 +176,9 @@ If you are coming from 26.08 or earlier, the database update still has to be run - Add Tickets on an invoice now adds a line for each ticket's time. To only put the tickets on the invoice, as before, switch off Bill their time. - A ticket's time is counted the same way everywhere: Unbilled Time, the Invoice window, Add Tickets and the Unbilled Work report. A ticket counts once it is resolved or closed, and archived tickets and archived replies are left out. - Calendars: only admins can add, edit or delete calendars now. Agents can still add and edit events. +- Roles: + - The money on invoices goes by Financial, with Sales at Read to see the invoices: write access to Financial to take a payment (one, a saved card or a batch) or change one, and full access to delete or refund one. These used to need Sales as well - write access to take a payment, full access to Sales and Financial to change or delete one. Emailing a payment reminder, a statement or a receipt works with write access to Financial and Sales at Read, as well as with write access to Sales, as before. Nothing a role could do before is taken away. Editing, sending, cancelling and deleting an invoice stay with Sales. + - A client's calendar events follow the role's Clients access: Read to see them, write access to add, change, move, skip or send them, and full access to delete them - or write access, for the agent who added it. Agents still see the events they are on, and an event with no client is anyone's, as before. So an agent whose role has no Clients access no longer sees other agents' client events, on the calendar or through their calendar link, and deleting someone else's client event now takes full access to Clients. - Licenses: archiving a license keeps its seats (the devices and users on it), so restoring it brings everything back. In 26.10, archiving took the seats off. - Vendors: deleting a vendor that has expenses, recurring expenses or payment provider fees archives it instead, so your books keep it. Deleting any vendor now also clears it from the assets, licenses, tickets and domains that named it. - Locations: a client's primary location cannot be archived or deleted. Make another location primary first. Bulk archive skips primary locations. @@ -823,6 +830,8 @@ If you are coming from 26.08 or earlier, the database update still has to be run - A user's quick look shows what they can open, from their role, and the user form shows what the role picked opens. - API: read the roles - an admin key every role, with its level of each part of ITFlow and how many agents have it, archived ones too; any other key only its own user's role. Read the agents, with their role, under the Support permission. - API: a call the key's role doesn't allow says what it needs and what the key's user has, with how to get it changed (see Breaking Changes, API). + - Accounting is the Accountant's job: with Financial and Sales at Read, a role can take payments - one, a saved card or a batch - change them and email receipts, reminders and statements, and with full access to Financial delete and refund them (see Breaking Changes, Roles). The invoice, the invoice list, Income and the payment quick looks offer what the role can do, and the reminder window offers only the reminder to a role that can't send the invoice itself. The built-in Accountant role now has full access to Financial. + - Calendar events follow the role, through the client they are for (see Breaking Changes, Roles). A role that can't add to a client's calendar isn't offered New event there, or a client in the event window, which says why, and its events can't be dragged. - **API Keys** (rebuilt) - Admin > API Keys shows, for each key, the agent it acts as and their role, what it can open and for which clients, whether it works and when it was last used. Whether it works reads Works until a day, Expires in so many days in its last two weeks, Expired, or Won't work with why: its agent is disabled, archived or has no role, or it acts as nobody. Search finds keys by their name or their agent's. - The New API Key and Edit windows show what the picked agent can open as you pick them, and offer only active agents. A new key lasts a year unless you change it, and its last day has to be after today. An expired key can be renamed and stays expired; a later day brings it back. @@ -929,6 +938,9 @@ If you are coming from 26.08 or earlier, the database update still has to be run - API keys: revoking, deleting and bulk deleting keys showed and logged no name ("API Key revoked"), and the bulk message counted the keys ticked rather than those deleted. A name with an apostrophe showed with a backslash in the created and updated messages. - API keys: a new key could be given today as its last day, so it never worked. An expired key couldn't be renamed without giving it a new day, as Edit wouldn't save the one it had. - API: reading an agent's time (`technicians/time.php`) needed Modify on Support, though it only reads. +- API: the API ran on the server's clock, not ITFlow's timezone, so the tickets, replies and other records it made were stamped in the server's time - hours off on a server set to UTC - and an API ticket's SLA times were worked out from that. A key also stopped working at midnight by the server's clock. +- Calendar: any agent could change, move or delete any client's event, whatever their role, and one whose role has no Clients access saw every client's events. +- Roles: the built-in Accountant role couldn't take a payment, email a reminder or statement, or delete an expense or other income entered by mistake. - Contacts: an agent without Support saw a contact's Assets, Documents, Files and Services tabs, which led to pages they were refused. A document's or file's linked assets, licenses, services, domains and certificates were listed to them too. - All Client Docs listed Credentials to agents who can't open it. - Notifications: shared-link notifications reached agents without Support, with a link they couldn't open. @@ -1348,6 +1360,12 @@ If you are coming from 26.08 or earlier, the database update still has to be run - Admin: `admin/api_keys.php` is rebuilt (`q`; no paging or sorting). `admin/post/api_keys.php`: `add_api_key` (`name`, `expire`, `run_as_user`, `key` - 32 of `A-Za-z0-9_-` - and `password`), `edit_api_key` (`api_key_id`, `name`, `expire`, `run_as_user`), GET `revoke_api_key` (`api_key_expire` to today) and `delete_api_key`, and `bulk_delete_api_keys` (`api_key_ids[]`). Each checks the key exists, and add and edit take an active agent only. Audit entries are API Key Create, Edit (with what changed, and only when something did), Revoke and Delete, each linked to its key. The Bulk Delete summary entry is gone, as each key logs its own Delete. - API: `api/v1/roles/read.php` (GET; `role_id`, `archived`, `limit`, `offset`) answers `role_id`, `role_name`, `role_description`, `role_is_admin`, `role_levels` (`module_name` => 0 to 3), `role_agents`, `role_created_at`, `role_updated_at` and `role_archived_at`. A key whose user isn't an admin gets only `$session_user_role`. `enforce_api_rbac.php` maps `roles` to `''`. `api/v1/technicians/read.php` (GET, `module_support`; `technician_id`, `archived`, `limit`, `offset`) answers `technician_id`, `technician_name`, `technician_email`, `technician_status`, `role_id`, `role_name`, `role_is_admin`, `technician_created_at` and `technician_archived_at`. - `enforce_api_rbac.php`: `apiPermissionText($module, $level)` words the module refusal, and `$operation_level` maps `time.php` to 1. +- Payments, calendar events and the API's clock (database 3.1.8): + - The migration gives the built-in Accountant (role 1) level 3 on `module_financial` when it still has its shipped levels (name, not admin, Clients, Support, Sales and Reports 1, Financial 2, no Credentials). `setup/seed_data.php` seeds it at 3. + - `functions/billing.php`: `billingMoneyRules()` (`take`, `edit`, `delete`, `receipt`, `remind`, `statement` - a Financial level with Sales at 1, or a Sales and Financial pair as before), `billingMoneyCan($action)` and `enforceBillingMoney($action)`. Used by `add_payment`, `add_payment_stripe`, `add_bulk_payment`, `edit_payment`, `delete_payment`, `refund_payment_stripe` and `payment_send_receipt` in `agent/post/payment.php`, `email_invoice` with `kind=reminder` (Sales 2 for anything else, a draft, or when nothing is owed) and `send_statement` in `agent/post/invoice.php`, and the payment, batch payment, statement and invoice email windows. `invoice_email.php` posts a hidden `kind=reminder` for a role without Sales 2. `apply_credit` and `add_credit` are unchanged, as credits aren't offered yet. + - `incomeCanEditPayments()` is `billingMoneyCan('edit')`, and new `incomeCanDeletePayments()` is `billingMoneyCan('delete')`. `bulk_edit_income` Set Category on payments also needs Sales 2, as it sets their invoices'. + - Calendar: `calendarEventCan($event, $action)` (`view`, `edit`, `delete`), `enforceCalendarEvent()`, `calendarIsOn($event_id)` and `calendarEventScopeSql()`, which `calendarEventItems()`, `calendarUserFeedItems()` and the dashboard's events add. `calendarEventForRequest()` and `calendarEventForModal()` take the action. `calendar_move` in `ajax.php` checks `edit`, and a calendar item's `editable` follows it. `agent/calendar.php` sets `data-can-add`, which `agent/js/calendar.js` reads to turn off picking dates. + - `includes/inc_set_timezone.php` sets `$session_timezone` and `$session_utc_offset`, puts them in `$_SESSION` only when a session is open, and no longer leaves `$result` and `$row` behind. `api/v1/validate_api_key.php` requires it, and sets `api_key_last_used_at` with `NOW()`. - 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()`, `clientOverviewPersonInitials()`, and `getMonthlyTax()` and `getQuarterlyTax()`, which looked a tax up by name diff --git a/admin/database_updates/3.1.8.php b/admin/database_updates/3.1.8.php new file mode 100644 index 000000000..fe04336d2 --- /dev/null +++ b/admin/database_updates/3.1.8.php @@ -0,0 +1,28 @@ + 1, 'module_financial' => 2, 'module_reporting' => 1, 'module_sales' => 1, 'module_support' => 1]) { + mysqli_query($mysqli, "UPDATE user_role_permissions SET user_role_permission_level = 3 + WHERE user_role_id = 1 AND module_id = (SELECT module_id FROM modules WHERE module_name = 'module_financial')"); + } + } diff --git a/agent/ajax.php b/agent/ajax.php index 1a00d9ee8..7c3f132e0 100644 --- a/agent/ajax.php +++ b/agent/ajax.php @@ -2217,6 +2217,9 @@ if (isset($_POST['calendar_move'])) { if (intval($event['event_client_id']) && !hasClientAccess($event['event_client_id'])) { $fail('You do not have access to that client.', 403); } + if (!calendarEventCan($event, 'edit')) { + $fail(permissionRefusedText('module_client', 2, intval(lookupUserPermission('module_client'))), 403); + } if (calendarEventRepeats($event)) { $fail('A repeating event is moved from Edit, so the whole series moves together.'); } diff --git a/agent/calendar.php b/agent/calendar.php index bfb1c513f..4729ec044 100644 --- a/agent/calendar.php +++ b/agent/calendar.php @@ -26,6 +26,8 @@ $calendars = calendarList(); $sources = calendarSources(); $can_manage = calendarCanManage(); $can_schedule = calendarCanScheduleTickets(); +// Inside a client, a new event is the client's - Clients at Modify (calendarEventCan()) +$can_add_event = calendarEventCan(['event_client_id' => $page_client_id], 'edit'); $csrf = $_SESSION['csrf_token']; $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT user_config_calendar_first_day FROM user_settings WHERE user_id = $session_user_id")); @@ -53,6 +55,7 @@ foreach ($sources as $key => $source) { data-user-id="= intval($session_user_id) ?>" data-first-day="= $first_day ?>" data-can-schedule="= $can_schedule ? 1 : 0 ?>" + data-can-add="= $can_add_event ? 1 : 0 ?>" data-mobile="= !empty($session_mobile) ? 1 : 0 ?>" data-initial-date="= calendarIsDate($_GET['date'] ?? '') ? escapeHtml($_GET['date']) : '' ?>" data-sources="= escapeHtml(json_encode($page_sources)) ?>"> @@ -62,7 +65,9 @@ foreach ($sources as $key => $source) {