# PRD — por (Purchase Order Requisition)

Evidence date: 2026-10-06.

Source: `git@bitbucket.org:overdrive/por.git`, `master` at `6fabdc090d2a3693f369f9fe50dea47990109349` (2026-06-23).

There is no prior PRD in the source repository or in Silicon Ark. Requirements below are recovered from the implementation, `README.md`, and `CHANGELOG.md`. Provenance is labelled. Connection passwords and the sample PDF link target are not reproduced.

## Product purpose

**Documented.** RGBC purchase-order requisitions, described in `README.md` as a legacy PHP application.

**Implemented.** A signed-in user can list and edit purchase orders, receipts, and vendor-onboarding records stored in a shared SQL Server database, and can queue follow-up rows in `sysCronJob`.

## Users and roles

Access is not a single role enum. The session user is the `mstUser` row joined to `mstUserGroupPOR` on `refUserGroupPORID`.

| Actor | How the code treats them | Status |
|---|---|---|
| Windows user at `login.php` | `$_SERVER['LOGON_USER']` must match `mstUser.strDomainLogin` (domain, bare name, and case variants). The user must have a POR group row | **Implemented** |
| Internet Explorer 6–11 | Login is blocked with a message to use Firefox or Chrome | **Implemented** |
| User with no matching row | Message: insufficient access, login controlled through Active Directory | **Implemented** message. The check is a database match, not an LDAP bind in this code |
| `strUserGroup` Admin or Dev | Menu links for both imports and Reports | **Implemented** |
| `blnSpecial == 1` | Settings menu. Comment in the forms says Dev\|Admin\|Exec. Close PO, complete-balance, and re-receipt controls are shown. Vendor code field is enabled | **Implemented** flag. The comment's role names are **not verified** against `mstUserGroupPOR` |
| Requester (`isUser`) | `challengeUgAccess` sets this when the challenger is `refUserID` on the requisition | **Implemented** |
| Manager (`isManager`) | Level 1 or 2 of `viePOR_UGAccess` when the PO total is within that level's upper limit and the challenger matches SELF, MAN, ALT, or an authoriser email | **Implemented** |
| Executive path (`isExec` from `blnSpecial`) | `blnSpecial == 1` sets `isExec` before the view lookup | **Implemented** flag |
| Level 3 authoriser | `porUgTryLevel3` can also set `isManager` when the total is within `Lvl3_UpperLimit` | **Implemented**. It does not set `isExec` |
| Accounts group | `refUserGroupPORID == 23` may add a receipt on another user's approved PO. A comment names this group Accounts | **Implemented** id. The name is a comment |
| Group 29 | Together with group 23, may edit a Draft purchase order even when the person is not the requester or a manager | **Implemented** id. Group name is **Unknown** |
| Fixed vendor-form user ids | Vendor form enables actions for user ids `189`, `154`, `11`, and `1278` depending on status. A comment names two of those ids | **Implemented** ids. The comment's names **require confirmation** and are not treated as verified identity |
| Dev impersonation | Optional `RGBC_POR_DEV_LOGIN_OVERRIDE` replaces `LOGON_USER` before the lookup. README says local testing only | **Documented** and **Implemented** |
| Deploy operator | README and `web.config` assume IIS. No deploy script is in the repo | **Documented** in the README |

`$SALESMANAGERMAXVALUE = 20000` is set in `include/system.inc.php` and is not referenced anywhere else. It is not an enforced limit.

## Systems the product talks to

1. The user's browser.
2. IIS, which is expected to supply `LOGON_USER`. `web.config` does not contain an authentication element. Windows authentication is **Inferred** from `LOGON_USER` and the login message.
3. SQL Server through PHP ODBC. Catalog name comes from configuration (example `Superbowl`).
4. `sysCronJob` in that database. This application inserts and updates rows. It does not process them.
5. The file system under `include/documents/` for uploads.
6. Google ajax CDN (jQuery 1.9.1 and jQuery UI 1.9.1) and cdnjs (Select2 4.0.3).
7. Accpac, named only in the GL-combination warning string. No Accpac API call is in this repository.
8. `//superbowl_pdf/` appears only in `test_folder_share_read.php` as a hardcoded sample link.

Mail is not sent by PHP. Several status changes set `strMailStatus` to `Mail Pending` and insert a cron row. **Unknown / Requires Confirmation:** which process sends the mail.

## Major workflows

### WF-1 — Sign in

`index.php` redirects to `login.php`.

If `LOGON_USER` is present and the browser is not Internet Explorer 6–11, `Action` becomes `Login` and `loginUser` runs. A match stores the joined user row in `$_SESSION['USER']`, inserts `mstUsageLog` with `strSystem = 'POR'`, and redirects to `requisition.php`.

Optional `RGBC_POR_LOGIN_DEBUG` replaces the failure page with connection and row-count diagnostics, including a probe for domain logins containing `maanie`.

Session lifetime is `21600` seconds. Session files are stored at `{DOCUMENT_ROOT}\rgbc_por\Sessions`.

`logout.php?Action=Logout` destroys the session and links back to `login.php`. The logout page shows the logged-out message even when `Action` is not `Logout`; the session is destroyed only when that action is set.

Provenance: **Implemented**.

### WF-2 — Purchase-order list

`requisition.php` renders filters and a paged list (`REQUISITION_LIST_PAGE_SIZE` is 100).

Filters: requested by, region, vendor, brand, status, manager, date range, ignore dates, and a search box. Search covers PO number, quote number, and status text (mode `list`).

Actions:

- **New Purchase Order** goes to `requisitionForm.php?RequisitionID=0`. The button is shown when the request URI contains `requisition.php`.
- **Filter** / **Clear** store or drop `fr*` values in the session.
- **Cancel Purchase Order** cancels each posted `chkSelect` id.
- **Export** downloads `POR_Approved_PO_List_YYYYMMDD.csv` for rows whose status is not Draft, Cancelled, or Pending.
- **AP Export** streams an HTML table with an Excel content type for the current basic filter, including line GL segments and receipt numbers.

If ignore-dates is checked and no other narrowing filter is set, the list shows "Slow search avoided" and does not run the wide query.

Provenance: **Implemented**.

### WF-3 — Create and submit a purchase order

`requisitionForm.php` loads or starts a requisition. A new row is status `New` in the form until save. `Requisition::save` inserts status `Draft`, allocates `intPONumber` as max + 1, and sets `strPONumber` to `PORGB` plus that number padded to 10 digits.

The form loads region (`mstBranch.strRegionDesc2`), request type (`fsrRequestType`), vendor (`mstVendor`, excluding names `ESN%`, inactive vendors hidden unless already selected), and payment terms (`mstPaymentTerms`). New forms default `refPaymentTermID` to `1`. A comment calls that Standard. The payment-term name for id 1 is **not** in source.

Line items store description, quantity, cost, and GL segments: account, region, brand, key account, PE, and TME. The form asks `ajax.functions.php` to refill the dependent dropdowns from the GL views.

**Upload** saves the requisition and stores a quote file as `{PONumber}_Quote_{filename}` under `include/documents/`, with a `porDocument` row.

**Submit** saves, then `checkItemFullGlCode`. Each line's combined code must exist on `mstGLCode.FullGLCode`. The combination is GL-Region, then brand, key account, PE, and TME only when those segments are non-empty. If any line fails, submit does not change status and the warning names Accpac. If the status is already Approved or Declined, submit shows an alert and does not submit again. Otherwise status becomes `Pending`, `strCapturedBy` is set, and cron type `POR_1NewRequisition` is inserted (param `RequisitionID={id}`).

**Copy Requisition** clones the header and lines into a new Draft with a new PO number. Quote number, authoriser, and email-to-alt-manager timestamp are cleared.

Provenance: **Implemented**.

### WF-4 — Approve, decline, cancel, close

`challengeUgAccess` reads `viePOR_UGAccess` for the requisition. Authoriser tokens:

- `SELF` — the requisition's user.
- `MAN` — `refManagerID` or `refAlternateManagerID`.
- `ALT` — the alternate manager.
- Any other token is matched as an email on `mstUser` with a non-zero POR group.

Level 1 applies when `dblTotal` is within `Lvl1_UpperLimit`. Otherwise level 2, then level 3. A special user is `isExec` even when the view row is missing. A new requisition (id 0) is `isUser` only.

Button visibility for users who are not `blnSpecial`, from `requisitionForm.php`:

| Status | Behaviour |
|---|---|
| Draft (and the form's initial New) | Requester, manager, exec, or group 23 or 29 may edit. Approve stays disabled |
| Pending | Requester may edit but Approve stays disabled. Manager or exec may edit including Approve |
| Approved | Edit controls disabled |
| Declined or Partially Receipted | Completed button shown for manager or exec |
| Completed or Cancelled | Edit controls disabled |

Special users: Close PO is available on Partially Receipted and Approved. The Completed button is available on Partially Receipted. Completed status disables edit.

**Approve** requires the GL check, sets `Approved`, writes `strAuthorizedBy` with the user and date, cron `POR_2ApprovedRequisition`.

**Confirm Decline** saves, sets `Declined`, cron `POR_2DeclinedRequisition`.

**Confirm Cancel Requisition** (and the list cancel) sets the PO and its receipts to `Cancelled`, marks existing cron rows for that param `Removed`, and inserts `POR_2CancelledRequisition` when the id is greater than 0.

**Close PO** sets status `Completed` and does not insert a cron row.

**Completed** sets status `Complete balance no longer applicable`, updates matching cron rows, and inserts `POR_CompletedRequisition`.

Email deep link: `requisitionForm.php` with `Link` set (and the same pattern on the vendor form) calls `loginUser` again. `ldap/adLDAP.php` is included and not used to bind.

Provenance: **Implemented**. The approve, decline, and cancel methods do not call `challengeUgAccess` before writing status. **Inferred:** a posted action can change status even when the button was hidden. **Requires confirmation** whether that is accepted.

### WF-5 — Receipts

`receipts.php` lists requisitions in `Approved`, `Partially Receipted`, or `Completed`, with receipt rows when present. **Export** downloads `POR_Invoiced_PO_List_YYYYMMDD.csv` limited to status `Completed`.

`receiptsForm.php` opens a receipt for a requisition. Creating a receipt is refused when another receipt on that PO is still `New` (`hasOpenRCP`). A new receipt is status `New`, number `RCPRGB` plus `ReceiptID` padded to 10 digits. Invoice documents use the receipt number as the filename prefix.

**Submit** requires a non-blank invoice number. It sets status `Submitted`, sets `dtDate` from the fiscal-period check, and inserts cron `POR_3Receipted` with param `ReceiptID={id}`.

Fiscal period: `sysQuery` row `POR_FISCAL_PERIOD`, column `memQuery`, is treated as an end date. If the receipt date is after that date, the receipt date is forced to the period end and a warning is shown. If the receipt date is before the PO date, the form forces PO status text `FISCAL PERIOD LOCKED` and blocks editing for non-special users.

Quantity check (`jsCheckReceiptItemQty`): receipted quantity plus the new quantity must be less than or equal to the PO line quantity. The sum includes receipts of every status except the current receipt id. A 2018 comment says a status filter was removed.

Who may edit a receipt when the PO is Approved or Partially Receipted: the PO user, either manager id on the requisition, the executive manager id, or group 23, and only when no other open receipt exists. Special users also see **Re-Receipt**, which sets the existing `POR_3Receipted` cron row back to `New` and `Result` to the string `NULL`. It does not change `porReceipt.strStatus`.

`Partially Receipted` on the PO and `Processed` on the receipt are read by the UI. This repository does not assign those two values. **Unknown / Requires Confirmation:** the external worker sets them.

Provenance: **Implemented** for the paths above.

### WF-6 — Vendor onboarding

`vendors.php` lists `porVendor`. **New Vendor** opens `vendorForm.php?VendorID=0`.

Save of a new vendor inserts sales and accounts department rows, then a `porVendor` in status `Draft`, number `VEN` plus the id padded to 5 digits, and attempts uploads typed CIPC, Tax, Bank, and BBBEE. Fields include legal name, trade name, region, address, CIPC, VAT, BBBEE status (posted `strBBBEEstatus` stored as `strBBBEEStatus`), whether the vendor is an RGBC customer, service, and the requester's manager ids copied from `mstUser`.

| Action | New status | Cron type |
|---|---|---|
| Submit | Pending | `POR_NewVendor` |
| Approve | Approved | `POR_ApprovedVendor` |
| Decline | Declined (reason from `strStatusDescription`) | `POR_VendorDeclined` |
| FinanceApproval | Finance Approved | `POR_FinanceApproved` |
| Complete | Complete | `POR_VendorCompleted` |
| Confirm InComplete | InComplete | `POR_VendorInCompleted` |
| Activate | Active | `POR_Activate` |
| Done | Done, plus posted `strVendorCode` | `POR_VendorDone` |

The purchase-order vendor dropdown reads `mstVendor`, not `porVendor`. **Unknown / Requires Confirmation:** how a Done vendor becomes an `mstVendor` row.

Non-special button rules use the hardcoded user ids in `vendorForm.php`. Special users skip that switch and can edit the vendor code. The Pending branch compares `UserID` to `refManagerID` on the **session user**, not to the vendor's manager id. **Requires confirmation** whether that comparison is intentional.

Provenance: **Implemented**.

### WF-7 — Imports, reports, settings

Admin or Dev (`strUserGroup`):

- Import User Group Access: download, upload into `stgUGAccess`, then **Import** truncates `porUGAccess` and inserts the stage rows. **Clear Records** clears the stage table.
- Import User Group GL Codes: the same pattern for `stgUGGL` and `porUGGL`.

Reports (same menu gate): GL Allocation and Vendor Allocation, downloaded as xlsx through PhpSpreadsheet.

Settings (`blnSpecial == 1`): grid of `sysQuery` where `strName LIKE 'POR%'`. Save writes `memQuery`, last user, and `dtLastEdit`. That includes `POR_FISCAL_PERIOD`.

Provenance: **Implemented**.

## Functional requirements

### FR-001 — Windows identity login

The system signs in the current Windows user when `mstUser.strDomainLogin` matches a candidate form of `LOGON_USER` and the user has `mstUserGroupPOR`.

Trigger: open `login.php` with `LOGON_USER` set, or press Login.

Output: session user and redirect to the purchase-order list, or an insufficient-access message.

Validation: Internet Explorer 6–11 never reaches the lookup.

Error: missing ODBC settings throw `SecureConfigException` during bootstrap, before the form.

Evidence: `login.php`, `loginUser`. Implementation: **Implemented**. Confidence: High.

### FR-002 — Session required for the shell

Any page that includes `include/layout.full.inc.php` redirects to `login.php` when `$_SESSION['USER']` is absent.

`ajax.functions.php` includes bootstrap and does not use that layout. It does not call `checkLogin`. **Requires confirmation** whether IIS authentication is enough for that endpoint.

### FR-003 — Draft purchase order and PO number

Saving a new requisition creates a Draft and a `PORGB` number from `MAX(intPONumber)+1`.

Error: two simultaneous saves can allocate the same next number. No unique-constraint handling is in the PHP. **Inferred** race. Confidence: Medium.

### FR-004 — GL combination required before submit and approve

Submit and approve do not proceed when any line's full GL code is missing from `mstGLCode`.

### FR-005 — Approval limits come from user-group access

The manager challenge uses `viePOR_UGAccess` limits and authoriser tokens, not `$SALESMANAGERMAXVALUE`.

### FR-006 — Cancel cascades to receipts

Cancelling a requisition sets related `porReceipt` rows to Cancelled.

### FR-007 — One open receipt per purchase order

A new receipt is not inserted while another receipt for that requisition is status `New`.

### FR-008 — Receipt submit needs an invoice number

A blank invoice number returns the warning and does not set Submitted.

### FR-009 — Fiscal period can rewrite the receipt date

When `POR_FISCAL_PERIOD` is in the past relative to the receipt date, submit stores the period-end timestamp.

If the settings row is missing, the check passes and the current timestamp is used.

### FR-010 — Vendor number format

New vendors receive `VEN` plus a 5-digit id.

### FR-011 — Admin imports replace the whole allocation tables

Import runs `TRUNCATE` then `INSERT` for `porUGAccess` or `porUGGL`.

### FR-012 — Outbound work is a database queue

Status transitions in WF-4, WF-5, and WF-6 insert `sysCronJob` rows with status `New`. This application does not execute those jobs.

## Integrations

| Integration | Direction | Trigger | Data | Auth | Failure |
|---|---|---|---|---|---|
| SQL Server ODBC | App to database | Every page that includes `system.inc.php` | POR and shared RGBC tables | DSN, user, password from env | `SecureConfigException` or ODBC error. `display_errors` is on |
| `sysCronJob` | App writes; consumer unknown | Submit, approve, decline, cancel, receipt submit, re-receipt, vendor transitions | Type, param, status, timestamp | Same database login | No retry in this app. Cancel marks prior rows for that param `Removed` |
| IIS `LOGON_USER` | Server to app | Login and email-link forms | Windows account name | **Inferred** Windows auth. Not set in `web.config` | Empty logon user leaves the Login button without an automatic attempt |
| Browser CDNs | Browser to Google and cdnjs | Every full page | jQuery, jQuery UI, Select2 | None | Pages still render; scripts fail in the browser. Not handled in PHP |
| File upload directory | App to disk | Quote, invoice, vendor documents | Original filename prefixed by PO, receipt, or vendor context | PHP process identity | Upload error codes returned as HTML warnings. Move failure returns "Failed moving document from tmp dir." |
| Accpac | Named in a warning only | — | — | — | Not called from this repo |

## Constraints

- Time zone `Africa/Johannesburg`.
- Page size 100 on the requisition and vendor lists.
- PO numbers `PORGB` + 10 digits. Receipt numbers `RCPRGB` + 10 digits from `ReceiptID`. Vendor numbers `VEN` + 5 digits.
- Vendor names starting with `ESN` are omitted from the PO vendor dropdown and from `ajaxSearchVendorXML`, unless that vendor is already selected.
- Bill-to customer search (`ajaxLoadRegionBillToAccountsXML`) requires `dwdCustomer.blnPromotionalAccounts = 1` plus active and region. Ship-to search does not require the promotional flag.
- Receipt quantity cannot exceed remaining PO line quantity, counting all other receipts.
- Internet Explorer 6–11 cannot sign in.
- `.env` must not be committed (`.gitignore`). `CHANGELOG.md` says previously committed SQL passwords should be rotated. Those historical values are not copied here.

## Edge cases

- Ignore-dates with no other filter does not run the heavy list query.
- Submit of an already Approved or Declined PO alerts and does not submit again. Other statuses are not blocked by `checkStatus`.
- Approve's failure path uses the static GL warning and does not list the bad lines. Submit appends the line list.
- Re-receipt does not set the receipt back to `New`. It only requeues the cron row.
- Close PO (`Completed`) and the Completed button (`Complete balance no longer applicable`) are different statuses.
- `layout.footer.inc.php` at the repo root is commented "not in use". The visible version line is built in `include/layout.header.inc.php`.
- `cursor_security_updates_implementation.md` describes a different VB.NET promotions application. It is not a requirement for POR.
- Snapshot files `requisitionForm.maanie.php`, `receiptsForm.pj.php`, `requisition20210330.php`, and dated class copies are in the tree. Live entry points are the files without those suffixes. **Inferred** they are leftovers. Nothing in the menu links to them.
