Skip to content
54 changes: 53 additions & 1 deletion POS/src/components/ShiftClosingDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,10 @@
{{ __('✓ Shift closed successfully') }}
</div>

<div v-if="eodPrintFailed" class="text-xs md:text-sm text-amber-600 font-medium text-center sm:text-end">
{{ __('EOD report pending print') }}
</div>

<!-- Submit/Close button (only shown in entry mode) -->
<Button
v-if="!showSuccessReport"
Expand All @@ -497,6 +501,16 @@
>
{{ submitResource.loading ? __('Closing Shift...') : __('Close Shift') }}
</Button>

<Button
v-if="eodPrintFailed"
variant="solid"
theme="blue"
@click="retryEodPrint"
:loading="retryPrintLoading"
>
{{ __('Print EOD Report') }}
</Button>
</div>
</div>
</template>
Expand All @@ -509,8 +523,10 @@ import { computed, onBeforeUnmount, reactive, ref, watch } from "vue"
import { storeToRefs } from "pinia"
import { useShift, shiftState } from "../composables/useShift"
import { useFormatters } from "../composables/useFormatters"
import { useToast } from "../composables/useToast"
import { usePOSSettingsStore } from "../stores/posSettings"
import { usePOSShiftStore } from "../stores/posShift"
import { printEODReport } from "../utils/printEod"
import TranslatedHTML from "./common/TranslatedHTML.vue"

const props = defineProps({
Expand All @@ -534,6 +550,7 @@ const open = computed({

const { getClosingShiftData, submitClosingShift } = useShift()
const { formatCurrency, formatQuantity, formatDateTime, formatTime } = useFormatters()
const { showSuccess, showWarning } = useToast()
const posSettingsStore = usePOSSettingsStore()
const { hideExpectedAmount } = storeToRefs(posSettingsStore)

Expand All @@ -545,6 +562,8 @@ const submitResource = submitClosingShift
const showInvoiceDetails = ref(false)
const showSuccessReport = ref(false) // Track if shift is closed and showing report
const errorMessage = ref('') // User-friendly error message
const eodPrintFailed = ref(null)
const retryPrintLoading = ref(false)
const showIdleWarning = ref(false)
let _idleWarningTimer = null

Expand All @@ -571,6 +590,7 @@ watch(open, async (isOpen) => {
clearTimeout(_idleWarningTimer)
_idleWarningTimer = null
}
eodPrintFailed.value = null
}
})

Expand Down Expand Up @@ -660,7 +680,20 @@ async function submitClosing() {
}

// Submit to server
await submitResource.submit({ closing_shift: closingData.value })
const result = await submitResource.submit({ closing_shift: closingData.value })
const closingShiftName = result?.name ?? submitResource.data?.name
if (closingShiftName) {
try {
await printEODReport(closingShiftName)
eodPrintFailed.value = null
} catch (err) {
console.warn("[eod] print failed", err)
showWarning(__("EOD report did not print. Use the Reprint button to retry."))
eodPrintFailed.value = { closingShiftName }
showSuccessReport.value = true
return
}
}

// If hideExpectedAmount is enabled, show success report before closing
if (hideExpectedAmount.value) {
Expand All @@ -680,6 +713,24 @@ async function submitClosing() {
}
}

async function retryEodPrint() {
const closingShiftName = eodPrintFailed.value?.closingShiftName
if (!closingShiftName) return

retryPrintLoading.value = true
try {
await printEODReport(closingShiftName)
eodPrintFailed.value = null
showSuccess(__("EOD report printed successfully"))
closeDialog()
} catch (err) {
console.warn("[eod] retry print failed", err)
showWarning(__("EOD report did not print. Please check QZ Tray and retry."))
} finally {
retryPrintLoading.value = false
}
}

function closeDialog() {
// Emit shift-closed event if we're closing from success report
if (showSuccessReport.value) {
Expand All @@ -691,6 +742,7 @@ function closeDialog() {
showInvoiceDetails.value = false
showSuccessReport.value = false // Reset report view
errorMessage.value = '' // Clear error messages
eodPrintFailed.value = null
}

// UI State Computed Properties
Expand Down
7 changes: 7 additions & 0 deletions POS/src/utils/printEod.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { silentPrintDoc } from "./printInvoice"

const EOD_PRINT_FORMAT = "POS Next EOD Report"

export async function printEODReport(closingShiftName) {
await silentPrintDoc("POS Closing Shift", closingShiftName, EOD_PRINT_FORMAT)
}
41 changes: 23 additions & 18 deletions POS/src/utils/printInvoice.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,28 @@ export async function printInvoiceByName(invoiceName, printFormat = null, letter
// Silent printing (QZ Tray — no browser dialog)
// ============================================================================

export async function silentPrintDoc(doctype, name, printFormat) {
const result = await call("frappe.www.printview.get_html_and_style", {
doc: doctype,
name,
print_format: printFormat,
no_letterhead: 1,
})

const html = result?.html || result?.message?.html
const style = result?.style || result?.message?.style || ""
if (!html) throw new Error("Failed to get print HTML from server")

const fullHTML = `<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><style>${style}</style></head>
<body>${html}</body>
</html>`

await qzPrintHTML(fullHTML)
return true
}

/**
* Fetch the server-rendered print HTML and send it to a thermal printer
* via QZ Tray. Uses Frappe's get_html_and_style API which returns the
Expand All @@ -381,24 +403,7 @@ export async function silentPrintInvoice(invoiceName, printFormat = null) {
}
const format = printFormat || DEFAULT_PRINT_FORMAT

const result = await call("frappe.www.printview.get_html_and_style", {
doc: "Sales Invoice",
name: invoiceName,
print_format: format,
no_letterhead: 1,
})

const html = result?.html || result?.message?.html
const style = result?.style || result?.message?.style || ""
if (!html) throw new Error("Failed to get print HTML from server")

const fullHTML = `<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><style>${style}</style></head>
<body>${html}</body>
</html>`

await qzPrintHTML(fullHTML)
await silentPrintDoc("Sales Invoice", invoiceName, format)
log.info(`Silent print sent for ${invoiceName}`)
return true
}
Expand Down
88 changes: 88 additions & 0 deletions docs/superpowers/plans/2026-05-24-eod-shift-report-print.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Plan: EOD Shift Report Auto-Print + Reprint

Date: 2026-05-24
Feature: EOD shift report print on closing shift

## Work Breakdown

### 1) Documentation

- Add design spec file under `docs/superpowers/specs/`
- Add implementation plan file under `docs/superpowers/plans/`

### 2) Backend (Print Data + Jinja hook)

- Create `pos_next/pos_next/utils/pos_closing_print.py`
- implement `get_items_sold(doc)`
- aggregate by `item_code`, `item_name`
- sum `qty`, `amount`
- order by `amount` descending
- handle `sales_invoice` and `pos_invoice`
- follow `POS Invoice.consolidated_invoice` when set
- Register Jinja method in `pos_next/hooks.py`

### 3) Backend Tests

- Create `pos_next/pos_next/utils/tests/test_pos_closing_print.py`
- verify mixed invoice references are handled
- verify consolidated invoice path is followed
- verify output shape uses float values

### 4) Print Format

- Add fixture:
- `pos_next/pos_next/print_format/pos_next_eod_report/pos_next_eod_report.json`
- Configure:
- `doc_type = POS Closing Shift`
- `name = POS Next EOD Report`
- `module = POS Next`
- Jinja type with 80mm thermal styling
- explicit LTR layout for receipt rows

### 5) Desk Reprint Action

- Update `pos_next/pos_next/doctype/pos_closing_shift/pos_closing_shift.js`
- on submitted docs add custom button `Print EOD Report` under `Print`
- call `frappe.utils.print` using `POS Next EOD Report`

### 6) POS Frontend Auto-Print + Retry

- Update `POS/src/utils/printInvoice.js`
- add reusable `silentPrintDoc(doctype, name, printFormat)`
- Create `POS/src/utils/printEod.js`
- implement `printEODReport(closingShiftName)` with `silentPrintDoc`
- Update `POS/src/components/ShiftClosingDialog.vue`
- after successful submit, resolve `result.name` first
- attempt EOD print
- on print failure:
- warning toast
- keep dialog open
- show retry action
- on retry success:
- success toast
- emit `shift-closed`
- close dialog

### 7) Uninstall Cleanup

- Update `pos_next/uninstall.py`
- include `POS Next EOD Report` in print format cleanup list

### 8) Validation

- Run migrate to sync print format:
- `bench --site <site> migrate`
- Run backend tests:
- `bench --site <site> run-tests pos_next.utils.tests.test_pos_closing_print`
- Run frontend build:
- `cd POS && npm run build`

## Sequenced Commits

1. `docs: spec EOD shift report auto-print + closing-shift reprint`
2. `docs: plan EOD shift report auto-print + closing-shift reprint`
3. `feat(eod): add EOD shift report print format + helper + Closing Shift button`
4. `feat(eod): auto-print EOD report on shift close with retry path`
5. `fix(eod): resolve closing shift name from submit response`
6. `fix(eod): follow consolidated_invoice for items + force LTR layout`
7. `feat(eod): polish POS Next EOD Report print format`
122 changes: 122 additions & 0 deletions docs/superpowers/specs/2026-05-24-eod-shift-report-print-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# EOD Shift Report Print Design

Date: 2026-05-24
App: POS Next
Scope: POS Closing Shift auto-print and Desk reprint for End-of-Day report.

## Problem Statement

Cashiers need a printed EOD report when closing a shift. Currently, shift close does not automatically print this report, and Desk users do not have a dedicated reprint action for submitted closing shifts.

## Goal

Implement EOD report printing for `POS Closing Shift` with:

- Auto-print after successful shift close in POS frontend
- Non-blocking print failure behavior (close succeeds, warning shown, retry path provided)
- Reprint button on submitted `POS Closing Shift` in Desk
- Thermal 80mm print format named `POS Next EOD Report`

## Non-Goals

- No schema changes to `POS Closing Shift` or related doctypes
- No changes to receipt transport beyond existing QZ Tray pipeline
- No changes to `POS Next Receipt` format

## Primary Flow

1. Cashier closes shift in POS.
2. Backend submits `POS Closing Shift`.
3. Frontend resolves returned `closing_shift` name from submit response.
4. Frontend requests print HTML/style via `frappe.www.printview.get_html_and_style`.
5. Frontend sends full HTML document to QZ Tray (`qzPrintHTML`).
6. If printing fails:
- Shift remains submitted (no rollback)
- Warning toast appears
- Dialog stays open with retry button

## Data Sources

- `doc.pos_transactions` child rows (Sales Invoice Reference)
- rows may contain `sales_invoice` and/or `pos_invoice`
- `doc.payment_reconciliation` rows (POS Closing Shift Detail)
- `doc.taxes` rows (POS Closing Shift Taxes)
- totals on closing doc:
- `grand_total`, `net_total`, `total_quantity`

## Consolidation Requirement

For closed shifts where ERPNext consolidated POS Invoices into Sales Invoices:

- `get_items_sold(doc)` must follow `POS Invoice.consolidated_invoice`
- If `consolidated_invoice` is set, fetch items from `Sales Invoice Item` with:
- `parent = consolidated_invoice`
- `parenttype = "Sales Invoice"`
- Otherwise use:
- `parent = pos_invoice`
- `parenttype = "POS Invoice"`

## Backend Components

- New helper module:
- `pos_next/pos_next/utils/pos_closing_print.py`
- exposed to Jinja via `hooks.py`
- Jinja method:
- `get_items_sold(doc)` returning aggregated item rows sorted by amount desc
- Test module:
- `pos_next/pos_next/utils/tests/test_pos_closing_print.py`

## Print Format

- New print format fixture JSON:
- `pos_next/pos_next/print_format/pos_next_eod_report/pos_next_eod_report.json`
- Name: `POS Next EOD Report`
- DocType: `POS Closing Shift`
- Thermal styling:
- 80mm width
- explicit `direction: ltr` on receipt container and rows
- Sections:
- Header
- Session details
- Sales totals
- Items sold
- Taxes
- Payments
- Final summary
- Footer with print timestamp and user

## Desk Integration

File: `pos_next/pos_next/doctype/pos_closing_shift/pos_closing_shift.js`

- On submitted doc (`docstatus === 1`), add button:
- Label: `Print EOD Report`
- Group: `Print`
- Calls `frappe.utils.print` with print format `POS Next EOD Report`

## POS Frontend Integration

Files:

- `POS/src/utils/printInvoice.js`
- add generic `silentPrintDoc(doctype, name, printFormat)`
- `POS/src/utils/printEod.js`
- add `printEODReport(closingShiftName)`
- `POS/src/components/ShiftClosingDialog.vue`
- after successful submit, print EOD
- if print fails: show warning + keep dialog open with retry action
- retry success closes dialog and emits `shift-closed`

## Error Handling

- Print errors are non-fatal
- Submit errors remain fatal for closing flow
- Print retry does not resubmit shift

## Acceptance Criteria

- Shift close triggers EOD print via QZ Tray
- Print failure does not block close
- Retry button can print and finish dialog flow
- Desk submitted `POS Closing Shift` supports one-click EOD reprint
- Items sold section works for consolidated and non-consolidated invoices
Loading
Loading