Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion POS/src/components/invoices/InvoiceDetailDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,10 @@
<Button variant="subtle" @click="show = false">
{{ __("Close") }}
</Button>
<Button @click="handlePrint">
<Button
@click="handlePrint"
:disabled="invoiceData && isPrintDisabled(invoiceData)"
>
<template #prefix>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
Expand All @@ -485,6 +488,7 @@

<script setup>
import { useFormatters } from "@/composables/useFormatters";
import { useReprintPermission } from "@/composables/useReprintPermission";
import { DEFAULT_CURRENCY, formatCurrency as formatCurrencyUtil } from "@/utils/currency";
import { getInvoiceStatusColor } from "@/utils/invoice";
import { logger } from "@/utils/logger";
Expand All @@ -494,6 +498,7 @@ import { ref, watch, nextTick, computed } from "vue";

const log = logger.create("InvoiceDetailDialog");
const { formatDate, formatTime } = useFormatters();
const { isPrintDisabled, printTitle } = useReprintPermission();

const props = defineProps({
modelValue: Boolean,
Expand Down
12 changes: 8 additions & 4 deletions POS/src/components/invoices/InvoiceManagement.vue
Original file line number Diff line number Diff line change
Expand Up @@ -741,8 +741,9 @@
</button>
<button
@click="$emit('print-invoice', invoice)"
class="px-3 py-2 text-xs font-semibold text-green-600 bg-green-50 hover:bg-green-100 rounded-lg transition-colors flex items-center gap-1"
:title="__('Print')"
:disabled="isPrintDisabled(invoice)"
class="px-3 py-2 text-xs font-semibold text-green-600 bg-green-50 hover:bg-green-100 rounded-lg transition-colors flex items-center gap-1 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-green-50"
:title="printTitle(invoice)"
>
<svg
class="w-4 h-4"
Expand Down Expand Up @@ -978,8 +979,9 @@
</button>
<button
@click="$emit('print-invoice', invoice)"
class="p-1.5 hover:bg-green-50 rounded transition-colors"
:title="__('Print')"
:disabled="isPrintDisabled(invoice)"
class="p-1.5 hover:bg-green-50 rounded transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent"
:title="printTitle(invoice)"
>
<svg
class="w-4 h-4 text-green-600"
Expand Down Expand Up @@ -1027,6 +1029,7 @@
import InvoiceFilters from "@/components/invoices/InvoiceFilters.vue";
import PaymentDialog from "@/components/sale/PaymentDialog.vue";
import { useInvoiceFilters } from "@/composables/useInvoiceFilters";
import { useReprintPermission } from "@/composables/useReprintPermission";
import { useInvoiceFiltersStore } from "@/stores/invoiceFilters";
import { DEFAULT_CURRENCY, formatCurrency as formatCurrencyUtil } from "@/utils/currency";
import { getInvoiceStatusColor } from "@/utils/invoice";
Expand All @@ -1045,6 +1048,7 @@ import { logger } from "@/utils/logger";

const log = logger.create("InvoiceManagement");
const { showSuccess, showError } = useToast();
const { isPrintDisabled, printTitle } = useReprintPermission();
const { formatDate, formatDateTime, formatTime } = useFormatters();

const props = defineProps({
Expand Down
7 changes: 5 additions & 2 deletions POS/src/components/sale/InvoiceHistoryDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,9 @@
</button>
<button
@click="printInvoice(invoice)"
class="p-1.5 hover:bg-green-50 rounded transition-colors"
:title="__('Print')"
:disabled="isPrintDisabled(invoice)"
class="p-1.5 hover:bg-green-50 rounded transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent"
:title="printTitle(invoice)"
>
<svg
class="w-4 h-4 text-green-600"
Expand Down Expand Up @@ -218,6 +219,7 @@

<script setup>
import { useToast } from "@/composables/useToast";
import { useReprintPermission } from "@/composables/useReprintPermission";
import { useFormatters } from "@/composables/useFormatters";
import { DEFAULT_CURRENCY, formatCurrency as formatCurrencyUtil } from "@/utils/currency";
import { getInvoiceStatusColor } from "@/utils/invoice";
Expand All @@ -227,6 +229,7 @@ import ReturnInvoiceDialog from "./ReturnInvoiceDialog.vue";

const { showError } = useToast();
const { formatDate, formatTime } = useFormatters();
const { isPrintDisabled, printTitle } = useReprintPermission();

const props = defineProps({
modelValue: Boolean,
Expand Down
12 changes: 6 additions & 6 deletions POS/src/components/sale/OfflineInvoicesDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,9 @@
</button>
<button
@click="printInvoice(invoice)"
class="p-1.5 sm:p-2 hover:bg-green-50 rounded-lg transition-colors touch-manipulation"
:title="
invoice.data?.was_printed
? __('Reprint receipt (already printed once)')
: __('Print receipt')
"
:disabled="isPrintDisabled(invoice)"
class="p-1.5 sm:p-2 hover:bg-green-50 rounded-lg transition-colors touch-manipulation disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent"
:title="printTitle(invoice)"
>
<svg
class="w-4 h-4 sm:w-5 sm:h-5 text-green-600"
Expand Down Expand Up @@ -420,6 +417,7 @@

<script setup>
import { DEFAULT_CURRENCY, formatCurrency as formatCurrencyUtil } from "@/utils/currency";
import { useReprintPermission } from "@/composables/useReprintPermission";
import { Button, Dialog } from "frappe-ui";
import { computed, ref, watch } from "vue";

Expand All @@ -443,6 +441,8 @@ const props = defineProps({
},
});

const { isPrintDisabled, printTitle } = useReprintPermission();

const emit = defineEmits([
"update:modelValue",
"sync-all",
Expand Down
37 changes: 37 additions & 0 deletions POS/src/composables/useReprintPermission.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useBootstrapStore } from "@/stores/bootstrap";
import { usePOSShiftStore } from "@/stores/posShift";
import { isInvoicePrinted } from "@/utils/invoice";
import { computed } from "vue";

const DENIED = __("Your role is not allowed to reprint a printed order.");

function allowedRoles(posProfile) {
return (posProfile?.posa_role_allowed_for_reprint ?? [])
.map((row) => row.role)
.filter(Boolean);
}

function userLacksReprintRole(userRoles, posProfile) {
const allowed = allowedRoles(posProfile);
if (!allowed.length) return false;
return !(userRoles ?? []).some((role) => allowed.includes(role));
}

export function useReprintPermission() {
const userRoles = computed(() => useBootstrapStore().data?.user_roles ?? []);
const posProfile = computed(() => usePOSShiftStore().currentProfile);

function isPrintDisabled(invoice) {
if (!userLacksReprintRole(userRoles.value, posProfile.value)) return false;
return isInvoicePrinted(invoice);
}

function printTitle(invoice) {
return isPrintDisabled(invoice) ? DENIED : __("Print");
}

return {
isPrintDisabled,
printTitle,
};
}
7 changes: 7 additions & 0 deletions POS/src/pages/POSSale.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,7 @@ import { Button, Dialog, createResource } from "frappe-ui";
import { call } from "@/utils/apiWrapper";
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { useToast } from "@/composables/useToast";
import { useReprintPermission } from "@/composables/useReprintPermission";

import { useCustomerSearchStore } from "@/stores/customerSearch";
import { useItemSearchStore } from "@/stores/itemSearch";
Expand Down Expand Up @@ -1097,6 +1098,7 @@ const {

// Initialize toast
const { showSuccess, showError, showWarning } = useToast();
const { isPrintDisabled, printTitle } = useReprintPermission();

// Initialize logger
const log = logger.create("POSSale");
Expand Down Expand Up @@ -2952,6 +2954,11 @@ async function handlePrintInvoice(invoiceData) {
invoiceData = offlineSnapshot;
}

if (isPrintDisabled(invoiceData)) {
showWarning(printTitle(invoiceData));
return;
}

// Silent print path — send directly to thermal printer via QZ Tray
if (posSettingsStore.silentPrint) {
const result = await printWithSilentFallback(invoiceData);
Expand Down
7 changes: 7 additions & 0 deletions POS/src/utils/invoice.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
* Common helpers for invoice-related operations across the application
*/

/** Whether a receipt was already printed (server, history, or offline queue). */
export function isInvoicePrinted(invoice) {
if (invoice?.is_printed != null) return Boolean(invoice.is_printed);
if (Number(invoice?.posa_is_printed) === 1) return true;
return Boolean(invoice?.data?.was_printed);
}

/**
* Get the appropriate CSS classes for invoice status badge
* @param {Object} invoice - Invoice object with status and docstatus fields
Expand Down
1 change: 1 addition & 0 deletions pos_next/api/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def get_initial_data():
"locale": _get_user_language(),
"precision": _get_precision_settings(),
"can_switch_to_desk": "Nexus POS Manager" in frappe.get_roles(),
"user_roles": frappe.get_roles(frappe.session.user),
"shift": None,
"pos_profile": None,
"pos_settings": None,
Expand Down
9 changes: 6 additions & 3 deletions pos_next/api/invoices.py
Original file line number Diff line number Diff line change
Expand Up @@ -1619,8 +1619,10 @@ def get_invoice(invoice_name):

# Get invoice document
invoice = frappe.get_doc("Sales Invoice", invoice_name)

return invoice.as_dict()
result = invoice.as_dict()
# Submitted invoices in history are always reprints; drafts use posa_is_printed.
result["is_printed"] = cint(result.get("posa_is_printed")) == 1 or cint(result.get("docstatus")) == 1
return result


@frappe.whitelist()
Expand Down Expand Up @@ -1663,7 +1665,8 @@ def get_invoices(pos_profile: str, limit: int = 100, start: int = 0) -> list:
status,
docstatus,
is_return,
return_against
return_against,
1 AS is_printed
FROM
`tabSales Invoice`
WHERE
Expand Down
24 changes: 14 additions & 10 deletions pos_next/pos_next/custom/pos_profile.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,17 +129,21 @@
"width": null
},
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"creation": "2026-02-23 11:34:27.353168",
"creation": "2026-06-23 12:00:00.000000",
"description": "Users with these roles can reprint an already-printed receipt. Empty = anyone.",
"docstatus": 0,
"dt": "POS Profile",
"fieldname": "posa_role_allowed_for_reprint",
"fieldtype": "Table MultiSelect",
"insert_after": "posa_allow_delete",
"label": "Role Allowed For Reprint",
"module": "POS Next",
"name": "POS Profile-posa_role_allowed_for_reprint",
"options": "POS Role Permitted",
"owner": "Administrator"
},
{
"default": "1",
"depends_on": null,
"description": null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2026-06-23 12:00:00.000000",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": ["role"],
"fields": [
{
"fieldname": "role",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Role",
"options": "Role",
"reqd": 1
}
],
"istable": 1,
"module": "POS Next",
"name": "POS Role Permitted",
"permissions": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from frappe.model.document import Document


class POSRolePermitted(Document):
pass
1 change: 1 addition & 0 deletions pos_next/uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def remove_custom_fields():
custom_fields = [
"Sales Invoice-posa_pos_opening_shift",
"Sales Invoice-posa_is_printed",
"POS Profile-posa_role_allowed_for_reprint",
]

removed_count = 0
Expand Down
Loading