Simplified PO Spec V1
1) Overview (what we’re building + why)
Section titled “1) Overview (what we’re building + why)”We’re adding a fast “Generate PO from Order Queue” workflow that lets a user take all card-triggered replenishment needs, grouped by Vendor + Order Mechanism, review/edit them in a PO side panel, and generate a vendor-ready PO PDF via the Documint service.
The goal is to reduce PO friction
2) Scope (In) / Out of Scope
Section titled “2) Scope (In) / Out of Scope”In scope
Section titled “In scope”- Order Queue view groups items by Vendor and Order Mechanism
- “Order All” per group opens a side panel populated from queue data
- Aggregation of like items into single PO lines (combining quantities, preserving contributing card traceability)
- Full editability in the side panel (PO header + PO lines + vendor fields)
- Vendor name typeahead lookup with free-text fallback
- Generate PO action: send PO payload → Documint service → receive PO PDF (or link)
- On clicking “generate”, copy standard vendor email body to clipboard + generate PDF
- Explicit empty/error states for missing vendor/mechanism, doc failures, clipboard failures
Out of scope (for this iteration)
Section titled “Out of scope (for this iteration)”- Persisting generated POs as a first-class “PO object” in Arda (unless already exists)
- Full Vendor master list / vendor management UI (we only do typeahead + free entry)
- Sending email directly from Arda (we only copy email body; user attaches PDF manually)
- Advanced pricing, tax, shipping cost calculations (unless already in Arda core)
- Receiving workflow changes
3) Primary User Flow (Order Queue → Side Panel → Generate PO)
Section titled “3) Primary User Flow (Order Queue → Side Panel → Generate PO)”- User navigates to Order Queue
- Order Queue renders grouped sections by:
- Vendor
- Order Mechanism
- User selects a section and clicks Order All or Order Item
- Side panel opens with:
- Vendor fields
- Vendor Name taken from Order Queue aggregation
- Address, free field (until we have a vendor database)
- Contact info - Free field (until we have a vendor database)
- PO header fields (free entry)
- Aggregated PO lines derived from all cards in that group
- Editable email template “Hi there, Please see attached PO”
- Vendor fields
- User optionally edits header/lines (including quantity and units)
- User clicks Generate PO
- System sends PO data to Documint
- Documint returns PDF in new tab
- UI shows a progress wheel while PO is generated in the side panel as well as in the Order Queue.
- System copies edited email body to clipboard and confirms “Copied”
4) Interaction Spec (screen-by-screen + component behaviors)
Section titled “4) Interaction Spec (screen-by-screen + component behaviors)”4.1 Order Queue (grouped view)
Section titled “4.1 Order Queue (grouped view)”Wireframe outline (text)
Order Queue
- Search / filter (optional) ………………… Refresh
Group: Vendor = McMaster-Carr | Mechanism = Email
-
Button: Order All
-
Summary: N cards, M unique items
-
List of queue entries (can remain per-card/per-trigger in the queue view)
Group: Vendor = Grainger | Mechanism = Portal
-
Button: Order All
-
Summary: N cards, M unique items
-
List entries
Group: Vendor = Unassigned | Mechanism = Unknown
-
Order All disabled (default)
-
Guidance: “Assign a vendor to order these items.”
Behaviors
Section titled “Behaviors”- Enter Order Queue
- System must fetch current queue entries (card-triggered items).
- UI must group them into sections by Vendor + Order Mechanism.
- Group header
- Must display: vendor name and mechanism.
- Must show an Order All button
- Order Queue item definition
- Each entry is a card-triggered replenishment request.
- Group CTA: Order All
- Click Order All
- Must open PO side panel anchored to the right.
- Must load only items belonging to the clicked
vendor + mechanismgroup.
- Selectable lines
- Select box next to each line in order queue.
- If some are selected, Order All changes to Order Selected
4.2 PO Side Panel (create/review/edit)
Section titled “4.2 PO Side Panel (create/review/edit)”Wireframe outline (text)
Side Panel Title: Create Purchase Order
Section: Vendor
- Vendor Name (Populated from Vendor Queue)
- Contact Email (free text)
- Phone (free text)
- Address (free text)
- Order Mechanism (read-only, inherited from group)
Section: PO Header
- PO Number (auto or editable?)
- Date (defaults to today, editable)
- Good Through
- Ship To (free text until we have vendors)
- Bill To (free text until we have vendors)
- Payment Terms (free text until we have vendors)
- Shipping Terms [Selectable overnight, ground]+ free text override)
- Notes / Instructions (free text until we have vendors)
Section: Lines (Aggregated) (AG Grid element?)
- Table/list: Item / Description | Qty | Unit | Vendor SKU | Taxable status | Notes (all editable)
- Shipping (free text)
- Taxes Rate (auto calculates based on line items with unit price)
- Total
Footer
- Primary CTA: Generate PO (Generate PO + Copy Email body)
- Secondary: Cancel
After generation
- Show: PDF generated
- Show: “Email body copied to clipboard” (if successful)
Behaviors
Section titled “Behaviors”- Vendor Name (filled in from order queue)
- Allow to edit Vendor name on PO
- Minimum required vendor field: vendor name (required to generate).
- Optional vendor fields (free entry): email, phone, address.
- PO header fields
- Must be editable free fields.
- Defaults:
- Date defaults to today (is editable)
- PO Number either auto-generated placeholder or blank (decision).
- Aggregated lines section
- Populated automatically from the chosen Order Queue group or selected lines
- Each line represents a unique item (same item ID/SKU) aggregated across contributing cards when generated
- Each line must have editable fields:
- Quantity (editable; overrides computed quantity)
- Description (editable)
- Vendor SKU
- Unit (editable if present)
- Notes (editable)
- Unit price only if we include it in the UI (optional for this iteration)
- Contributing Cards
- The cards are aggregated into each line. Contributing card references are not persisted after PO generation
- The cards that aggregate to each line are marked as Order in progress when Generate PO is clicked
- Cancel / close
- Cancel closes the panel without generating.
- Contributing cards are not marked as Order in Progress
- If edits exist, show confirmation:
- Title: “Discard changes?”
- Buttons: “Discard” / “Keep editing”
- Generate PO
-
Button label: Generate PO
-
On click:
- Validate required fields (vendor name; at least one line).
- Show loading state: “Generating…”
Call Document service.
-
On success:
- Show confirmation: “PO generated.”
- Opens new tab with PO PDF (unless there’s other behavior we can do with Documint)
- Copy vendor email body to clipboard and confirm “Email body copied to clipboard”
-
On failure:
- Show: “Couldn’t generate PO. Try again.”
- Do not show clipboard success unless copy actually occurred.
- Clipboard copy behavior
-
Recommended: copy occurs after successful PDF generation.
-
If clipboard copy fails, show fallback:
-
Message: “Clipboard permission blocked.”
-
Button: Copy email text
-
5) Data Behavior Rules (grouping, aggregation, quantities, editability)
Section titled “5) Data Behavior Rules (grouping, aggregation, quantities, editability)”5.1 Grouping rules (Order Queue → group lists)
Section titled “5.1 Grouping rules (Order Queue → group lists)”-
Group key = Vendor + Order Mechanism
-
If vendor missing:
-
Place in “Unassigned vendor” group
-
Order All disabled by default (recommended)
- If mechanism missing:
-
Place in “Unknown mechanism” group
-
Order All disabled by default (recommended)
5.2 Aggregation rules (group → PO lines)
Section titled “5.2 Aggregation rules (group → PO lines)”-
“Like items” definition: same item ID (canonical), fallback to SKU if item ID missing
-
For each unique item:
-
Create one PO line
-
Attach all contributing cards for that item from the selected group
- Quantity computation:
-
Computed quantity = sum of reorder quantities across contributing cards
-
Line quantity defaults to computed quantity
5.3 Units / packaging default
Section titled “5.3 Units / packaging default”- Apply units from first card
5.4 Editability rules (side panel)
Section titled “5.4 Editability rules (side panel)”-
All line fields are editable prior to generation
-
Quantity override:
- User-entered quantity becomes the final quantity for the generated PO
- Cards traceability:
- None once the PO is generated
- Free text entry
- Button to add a line to the PO
- All columns free text
- Quantity
6) Backend / Integration Touchpoints (requests, responses, error handling)
Section titled “6) Backend / Integration Touchpoints (requests, responses, error handling)”6.1 Inputs needed from the Order Queue
Section titled “6.1 Inputs needed from the Order Queue”Order Queue entries must provide:
-
Item identity (ID + display label)
-
Card identity (ID + optional friendly label)
-
Reorder quantity (+ unit if applicable)
-
Vendor association (name or ID)
-
Order mechanism (for example: Email, Portal, Unknown)
6.2 Document service: Generate PO PDF
Section titled “6.2 Document service: Generate PO PDF”When the user clicks Generate PO:
- Frontend assembles a PO payload containing:
-
Header fields (vendor + PO header fields)
-
Lines (aggregated by item)
-
Per-line: computed quantity, final quantity, and list of contributing cards with their quantities
-
Context metadata (source: order queue; vendor/mechanism group key; user)
- Document service returns:
- Either a PDF URL/link + filename, or a PDF blob (preferred: URL + metadata)
Link to Documint Template Details (https://docs.google.com/document/d/10kmDva7aiIT1N-x3BAm6fmDOfjZAj1IfWJT1tCtBIeo/edit?tab=t.0\#heading=h.nel4km8rdz8q)
6.3 Error handling expectations
Section titled “6.3 Error handling expectations”-
Document service failure:
-
UI: “Couldn’t generate PO. Try again.”
-
Enable retry
-
Do not claim clipboard copied if generation failed
-
-
Clipboard failure:
-
UI: “Clipboard permission blocked. Click to copy.”
-
Provide manual copy button
-
7) Edge Cases & UX Fail-safes
Section titled “7) Edge Cases & UX Fail-safes”- Unassigned vendor
-
Group appears as “Unassigned vendor”
-
Order All disabled (default)
-
Tooltip: “Assign a vendor to order these items.”
- Order Queue changes while panel open
- Nothing happens.
- Validation
-
Missing vendor name: inline error “Vendor name is required.”
-
No lines: “Add at least one line item to generate a PO.”
- User cancels mid-generation
- If in-flight request, confirm: “Cancel generation?” and either abort (if supported) or explain “Finishing request…” (decision).
8) Success Criteria (what “working” means)
Section titled “8) Success Criteria (what “working” means)”-
Order Queue groups entries by vendor + mechanism with clear, correct grouping.
-
Order All opens side panel with aggregated lines:
-
Like items combined into one line
-
Quantities equal the sum of contributing card reorder quantities
-
Contributing cards visible via expand/collapse
-
-
All header and line fields are editable prior to generation.
-
Generate PO produces a valid PDF and presents it to the user.
-
On successful generation, email body is copied to clipboard and confirmed; clipboard failures have a usable fallback.
-
Failures never show false success states.
9) Open Questions for CTO
Section titled “9) Open Questions for CTO”-
PO Number: auto-generated by Arda vs user-entered vs generated by Document service?Resolved: auto-generated by Arda, editable by the user in the side panel before generation. -
Snapshot vs live sync: how to handle Order Queue changes while side panel is open?
-
Can users exclude a line or exclude specific contributing cards from the PO draft (without changing the queue)?
-
Do we have item “order unit”, pack size, or ordering increments today? If not, is “quantities as-is” acceptable?
-
Typeahead suggestion source: past vendor strings, item supply records, or something else?
-
Document service response: PDF URL vs PDF blob; do we need storage, access control, expiration?
-
After generation: do we persist a PO record in Arda (even minimal) or treat this as export-only for now?
-
Clipboard timing: copy email body only on successful generation (recommended) vs immediately on click?
-
Email template ownership: fixed template vs org-configurable (signature, wording)?
10) Documint Template
Section titled “10) Documint Template”| Path | Type | Used in PO Template? |
|---|---|---|
| $ | object | |
| eId | string | |
| header | object | |
| header.rId | string | |
| header.asOf | object | |
| header.asOf.effective | number | |
| header.asOf.recorded | number | |
| header.payload | object | |
| header.payload.eId | string | |
| header.payload.status | string | |
| header.payload.orderNumber | string | Yes |
| header.payload.orderDate | object | |
| header.payload.orderDate.timestamp | date | Yes |
| header.payload.orderDate.tz | string | |
| header.payload.allowPartial | boolean | |
| header.payload.expedite | boolean | |
| header.payload.deliverBy | object | |
| header.payload.deliverBy.timestamp | date | Yes |
| header.payload.deliverBy.tz | string | |
| header.payload.deliveryAddress | object | |
| header.payload.deliveryAddress.addressLine1 | string | Yes |
| header.payload.deliveryAddress.addressLine2 | string | Yes |
| header.payload.deliveryAddress.city | string | Yes |
| header.payload.deliveryAddress.state | string | Yes |
| header.payload.deliveryAddress.postalCode | string | Yes |
| header.payload.deliveryAddress.country | string | |
| header.payload.deliveryAddress.geoLocation | object | |
| header.payload.deliveryAddress.geoLocation.latitude | number | |
| header.payload.deliveryAddress.geoLocation.longitude | number | |
| header.payload.deliveryAddress.geoLocation.elevation | number | |
| header.payload.procurement | object | |
| header.payload.procurement.salutation | string | |
| header.payload.procurement.firstName | string | |
| header.payload.procurement.middleName | string | |
| header.payload.procurement.lastName | string | |
| header.payload.procurement.jobTitle | string | |
| header.payload.procurement.email | string | Yes |
| header.payload.procurement.phone | string | Yes |
| header.payload.procurement.postalAddress | object | |
| header.payload.procurement.postalAddress.addressLine1 | string | Yes |
| header.payload.procurement.postalAddress.addressLine2 | string | Yes |
| header.payload.procurement.postalAddress.city | string | Yes |
| header.payload.procurement.postalAddress.state | string | Yes |
| header.payload.procurement.postalAddress.postalCode | string | Yes |
| header.payload.procurement.postalAddress.country | string | Yes |
| header.payload.procurement.postalAddress.geoLocation | object | |
| header.payload.procurement.postalAddress.geoLocation.latitude | number | |
| header.payload.procurement.postalAddress.geoLocation.longitude | number | |
| header.payload.procurement.postalAddress.geoLocation.elevation | number | |
| header.payload.procurement.emails | object | |
| header.payload.procurement.emails.vacation | string | |
| header.payload.procurement.phones | object | |
| header.payload.procurement.addresses | object | |
| header.payload.procurement.addresses.in laws | object | |
| header.payload.procurement.addresses.in laws.addressLine1 | string | |
| header.payload.procurement.addresses.in laws.addressLine2 | string | |
| header.payload.procurement.addresses.in laws.city | string | |
| header.payload.procurement.addresses.in laws.state | string | |
| header.payload.procurement.addresses.in laws.postalCode | string | |
| header.payload.procurement.addresses.in laws.country | string | |
| header.payload.procurement.addresses.in laws.geoLocation | object | |
| header.payload.procurement.addresses.in laws.geoLocation.latitude | number | |
| header.payload.procurement.addresses.in laws.geoLocation.longitude | number | |
| header.payload.procurement.addresses.in laws.geoLocation.elevation | number | |
| header.payload.procurement.sites | object | |
| header.payload.procurement.sites.linkedIn | string | |
| header.payload.supplierName | string | Yes |
| header.payload.supplierAddress | object | |
| header.payload.supplierAddress.addressLine1 | string | Yes |
| header.payload.supplierAddress.addressLine2 | string | Yes |
| header.payload.supplierAddress.city | string | Yes |
| header.payload.supplierAddress.state | string | Yes |
| header.payload.supplierAddress.postalCode | string | Yes |
| header.payload.supplierAddress.country | string | |
| header.payload.supplierAddress.geoLocation | object | |
| header.payload.supplierAddress.geoLocation.latitude | number | |
| header.payload.supplierAddress.geoLocation.longitude | number | |
| header.payload.supplierAddress.geoLocation.elevation | number | |
| header.payload.orderMethod | string | |
| header.payload.sales | object | |
| header.payload.sales.salutation | string | |
| header.payload.sales.firstName | string | |
| header.payload.sales.middleName | string | |
| header.payload.sales.lastName | string | |
| header.payload.sales.jobTitle | string | |
| header.payload.sales.email | string | Yes |
| header.payload.sales.phone | string | Yes |
| header.payload.sales.postalAddress | object | |
| header.payload.sales.postalAddress.addressLine1 | string | |
| header.payload.sales.postalAddress.addressLine2 | string | |
| header.payload.sales.postalAddress.city | string | |
| header.payload.sales.postalAddress.state | string | |
| header.payload.sales.postalAddress.postalCode | string | |
| header.payload.sales.postalAddress.country | string | |
| header.payload.sales.postalAddress.geoLocation | object | |
| header.payload.sales.postalAddress.geoLocation.latitude | number | |
| header.payload.sales.postalAddress.geoLocation.longitude | number | |
| header.payload.sales.postalAddress.geoLocation.elevation | number | |
| header.payload.sales.emails | object | |
| header.payload.sales.emails.vacation | string | |
| header.payload.sales.phones | object | |
| header.payload.sales.addresses | object | |
| header.payload.sales.addresses.in laws | object | |
| header.payload.sales.addresses.in laws.addressLine1 | string | |
| header.payload.sales.addresses.in laws.addressLine2 | string | |
| header.payload.sales.addresses.in laws.city | string | |
| header.payload.sales.addresses.in laws.state | string | |
| header.payload.sales.addresses.in laws.postalCode | string | |
| header.payload.sales.addresses.in laws.country | string | |
| header.payload.sales.addresses.in laws.geoLocation | object | |
| header.payload.sales.addresses.in laws.geoLocation.latitude | number | |
| header.payload.sales.addresses.in laws.geoLocation.longitude | number | |
| header.payload.sales.addresses.in laws.geoLocation.elevation | number | |
| header.payload.sales.sites | object | |
| header.payload.sales.sites.linkedIn | string | |
| header.payload.goodsValue | object | |
| header.payload.goodsValue.value | number | Yes |
| header.payload.goodsValue.currency | string | |
| header.payload.taxesAndFees | object | |
| header.payload.taxesAndFees.Sales Tax | object | |
| header.payload.taxesAndFees.Sales Tax.value | number | Yes |
| header.payload.taxesAndFees.Sales Tax.currency | string | |
| header.payload.taxesAndFees.Shipping | object | |
| header.payload.taxesAndFees.Shipping.value | number | Yes |
| header.payload.taxesAndFees.Shipping.currency | string | |
| header.payload.taxesAndFees.Other | object | |
| header.payload.taxesAndFees.Other.value | number | Yes |
| header.payload.taxesAndFees.Other.currency | string | |
| header.payload.totalAmount | object | |
| header.payload.totalAmount.value | number | Yes |
| header.payload.totalAmount.currency | string | |
| header.payload.accountingReference | string | Yes |
| header.payload.termsAndConditions | string | Yes |
| header.payload.notes | string | Yes |
| header.payload.privateNotes | string | |
| header.payload.companyName | string | Yes |
| header.metadata | object | |
| header.metadata.tenantId | string | |
| header.metadata.currentLines | number | |
| header.author | string | |
| header.createdBy | string | |
| header.createdAt | object | |
| header.createdAt.effective | number | |
| header.createdAt.recorded | number | |
| header.retired | boolean | |
| lines | object | |
| lines.thisPage | string | |
| lines.nextPage | string | |
| lines.previousPage | string | |
| lines.results | array | |
| lines.results[] | object | |
| lines.results[].rId | string | |
| lines.results[].asOf | object | |
| lines.results[].asOf.effective | number | |
| lines.results[].asOf.recorded | number | |
| lines.results[].payload | object | |
| lines.results[].payload.eId | string | |
| lines.results[].payload.title | string | |
| lines.results[].payload.description | string | |
| lines.results[].payload.status | string | |
| lines.results[].payload.item | object | |
| lines.results[].payload.item.eId | string | |
| lines.results[].payload.item.name | string | Yes |
| lines.results[].payload.supplierSku | string | Yes |
| lines.results[].payload.quantity | object | |
| lines.results[].payload.quantity.amount | number | Yes |
| lines.results[].payload.quantity.unit | string | Yes |
| lines.results[].payload.unitCost | object | |
| lines.results[].payload.unitCost.value | number | Yes |
| lines.results[].payload.unitCost.currency | string | |
| lines.results[].payload.received | object | |
| lines.results[].payload.received.amount | number | |
| lines.results[].payload.received.unit | string | |
| lines.results[].payload.notes | string | Yes |
| lines.results[].payload.privateNotes | string | |
| lines.results[].payload.servicedCards | array | |
| lines.results[].payload.servicedCards[] | object | |
| lines.results[].payload.servicedCards[].cardEId | string | |
| lines.results[].payload.servicedCards[].serialNumber | string | |
| lines.results[].payload.servicedCards[].quantity | object | |
| lines.results[].payload.servicedCards[].quantity.amount | number | |
| lines.results[].payload.servicedCards[].quantity.unit | string | |
| lines.results[].payload.servicedCards[].reference | object | |
| lines.results[].payload.servicedCards[].reference.cardEId | string | |
| lines.results[].payload.cost | object | |
| lines.results[].payload.cost.value | number | Yes |
| lines.results[].payload.cost.currency | string | |
| lines.results[].metadata | object | |
| lines.results[].metadata.parentEid | string | |
| lines.results[].metadata.rank | number | |
| lines.results[].author | string | |
| lines.results[].createdBy | string | |
| lines.results[].createdAt | object | |
| lines.results[].createdAt.effective | number | |
| lines.results[].createdAt.recorded | number | |
| lines.results[].retired | boolean | |
| lines.totalCount | number | |
\ No newline at end of file
Copyright: © Arda Systems 2025-2026, All rights reserved