Backdating: Inbound Shipment Received Date
Overview
Allow users to backdate the received date on an inbound shipment that has already been received. The date can only be moved earlier, never forward — this prevents ledger inconsistencies from stock being "ungrown" into the future.
Design
- The received date can only be moved earlier than the current received date. Forward moves are rejected.
- No lines are deleted — existing invoice lines and resulting stock lines are preserved.
- Other invoice datetimes (
shipped_datetime,delivered_datetime,created_datetime) are intentionally left untouched. The resulting out-of-order dates are the signal that the received date was backdated. - Location movements belonging to stock lines from this invoice have their
enter_datetimeupdated to the new received date; ifexit_datetimewas set, it is also updated. - Only editable when the shipment is in
ReceivedorVerifiedstatus. - Gated by the global
Backdatingpreference (enabled+max_days). - The frontend sends a full RFC3339 datetime carrying the client's UTC offset (e.g.
2024-12-10T00:00:00+13:00), parsed server-side asDateTime<FixedOffset>. Storage and validation use.naive_utc(); the offset is retained only so the audit log can record the calendar date the user actually picked. - On backdating, an
InvoiceDateBackdatedactivity log entry is written with the old and new received dates, formatted in the client's offset (not raw UTC) so they match the date the user selected — otherwise a user east of UTC picking local midnight would see the previous day in the log (issue #11673). - The user is shown a confirmation warning that the change is one-way before it is applied.
- A stocktake warning is shown if a stocktake exists after the selected date.
Backend validation (server-side enforced)
From inbound_shipment/update/validate.rs:
Backdating.enabledmust be true →BackdatingNotEnabled- Invoice status must be
ReceivedorVerified→CanOnlyBackdateReceivedShipments - New datetime (UTC) must be strictly earlier than the current
received_datetime→CannotMoveReceivedDateForward - If
Backdating.max_days > 0, must not be beforenow - max_days→ExceedsMaximumBackdatingDays
Backend generate
From inbound_shipment/update/generate.rs:
invoice.received_datetimeis updated (stored asNaiveDateTimevia.naive_utc()).- All stock lines linked to invoice lines of this invoice (StockIn lines) are queried, and every
location_movementfor those stock lines has itsenter_datetime(andexit_datetime, when set) updated to the new received date. These are returned asbackdate_location_movementsand upserted in the transaction. shipped_datetime,delivered_datetime, andcreated_datetimeare not modified.
The InvoiceDateBackdated activity log entry is written at the top-level update in inbound_shipment/update/mod.rs, capturing the pre-update received_datetime and the new value. Both are formatted as %Y-%m-%d after converting the stored naive-UTC datetime into the client's offset, so the logged dates match what the user picked.
Key files
Backend (Rust)
| File | Change |
|---|---|
server/service/src/invoice/inbound_shipment/update/mod.rs | received_datetime: Option<DateTime<FixedOffset>> field; new error variants; upsert of backdate_location_movements; InvoiceDateBackdated activity log entry (dates formatted in the client's offset) |
server/service/src/invoice/inbound_shipment/update/validate.rs | Validation: preference enabled, received/verified status, strictly earlier (UTC datetime comparison), within max_days |
server/service/src/invoice/inbound_shipment/update/generate.rs | Updates received_datetime via .naive_utc(); collects location movement rows to update enter_datetime / exit_datetime |
server/repository/src/db_diesel/activity_log_row.rs | InvoiceDateBackdated activity log type |
server/graphql/types/src/types/activity_log.rs | InvoiceDateBackdated GraphQL enum variant |
server/graphql/invoice/src/mutations/inbound_shipment/update.rs | receivedDatetime: DateTime<FixedOffset> on the GraphQL input (preserves the client's UTC offset); error mapping |
server/repository/src/migrations/v2_18_00/add_invoice_date_backdated_activity_log_type.rs | Postgres migration for the InvoiceDateBackdated enum value |
Frontend (TypeScript)
| File | Change |
|---|---|
client/packages/invoices/src/InboundShipment/DetailView/ReceivedDateInput.tsx | Received date picker in the toolbar; sends DateTime via Formatter.localIsoString (RFC3339 with the local offset, so the audit log shows the picked date); maxDate is the current received date (server-side check enforces strictly earlier) |
client/packages/invoices/src/InboundShipment/api/api.ts | receivedDatetime in toUpdate |
client/packages/invoices/src/InboundShipment/api/operations.graphql | stocktakeCountAfterDate query for the stocktake warning |
client/packages/common/src/authentication/api/operations.graphql | backdating preference query (combined struct) |
client/packages/common/src/intl/locales/en/common.json | Translations including log.invoice-date-backdated |
User flow
- Shipment is received (status =
Received);received_datetimeis set automatically. - If the
Backdatingpreference is enabled, the Received date field appears in the toolbar. - The date picker constrains to: earlier than the current received date, and not before
max_daysago (+1 day buffer so the boundary date isn't rejected by the server's UTC check). - User picks an earlier date.
- A confirmation dialog warns the change is one-way and shows the selected date.
- If a stocktake exists on or after the chosen date, an additional warning is shown with the date.
- On confirm, the backend:
- Updates
received_datetime. - Updates
enter_datetime(andexit_datetimeif set) on location movements for stock lines from this invoice. - Writes an
InvoiceDateBackdatedactivity log entry.
- Updates