Skip to content

Commit 10993d6

Browse files
author
MohamedAliSmk
committed
feat(expenses): enhance shift expense management and validation
- Introduced computed properties for shift expense totals and remaining allowances in ExpenseDialog. - Updated validation logic to check against remaining shift expense allowance instead of maximum expense amount. - Modified API to return shift expense totals and remaining amounts for better expense tracking. - Enhanced translations to reflect new shift expense limit messages and summaries.
1 parent 02b4dbd commit 10993d6

7 files changed

Lines changed: 129 additions & 16 deletions

File tree

POS/src/components/sale/ExpenseDialog.vue

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
v-if="maximumExpenseAmount > 0"
5757
class="mt-1 text-xs text-gray-500 text-start"
5858
>
59-
{{ __("Maximum allowed: {0}", [formatCurrency(maximumExpenseAmount)]) }}
59+
{{ shiftExpenseLimitSummary }}
6060
</p>
6161
</div>
6262

@@ -179,6 +179,35 @@ const maximumExpenseAmount = computed(
179179
0,
180180
)
181181
182+
const shiftExpenseTotal = computed(
183+
() => Number.parseFloat(dialogDataResource.data?.shift_expense_total) || 0,
184+
)
185+
186+
const remainingExpenseAmount = computed(() => {
187+
if (maximumExpenseAmount.value <= 0) {
188+
return 0
189+
}
190+
191+
const remaining = Number.parseFloat(dialogDataResource.data?.remaining_expense_amount)
192+
if (Number.isFinite(remaining)) {
193+
return Math.max(0, remaining)
194+
}
195+
196+
return Math.max(0, maximumExpenseAmount.value - shiftExpenseTotal.value)
197+
})
198+
199+
const shiftExpenseLimitSummary = computed(() => {
200+
if (maximumExpenseAmount.value <= 0) {
201+
return ""
202+
}
203+
204+
return [
205+
__("Shift limit: {0}", { 0: formatCurrency(maximumExpenseAmount.value) }),
206+
__("Recorded: {0}", { 0: formatCurrency(shiftExpenseTotal.value) }),
207+
__("Remaining: {0}", { 0: formatCurrency(remainingExpenseAmount.value) }),
208+
].join(" | ")
209+
})
210+
182211
const dialogDataResource = createResource({
183212
url: "pos_next.api.expenses.get_expense_dialog_data",
184213
makeParams() {
@@ -288,10 +317,10 @@ function validateForm() {
288317
return __("Amount must be greater than zero")
289318
}
290319
291-
if (maximumExpenseAmount.value > 0 && amount > maximumExpenseAmount.value) {
292-
return __("Amount exceeds the maximum allowed expense amount of {0}", [
293-
formatCurrency(maximumExpenseAmount.value),
294-
])
320+
if (maximumExpenseAmount.value > 0 && amount > remainingExpenseAmount.value) {
321+
return __("Amount exceeds the remaining shift expense allowance of {0}", {
322+
0: formatCurrency(remainingExpenseAmount.value),
323+
})
295324
}
296325
297326
if (!form.mode_of_payment) {

pos_next/api/expenses.py

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ def get_expense_dialog_data(pos_profile, pos_opening_shift):
2121
maximum_expense_amount = flt(
2222
frappe.db.get_value("POS Profile", pos_profile, "posa_maximum_expense_amount")
2323
)
24+
shift_expense_total = get_shift_expense_total(pos_opening_shift)
25+
remaining_expense_amount = _get_remaining_shift_expense_amount(
26+
maximum_expense_amount, shift_expense_total
27+
)
2428

2529
from pos_next.api.pos_profile import get_payment_methods
2630

@@ -29,6 +33,8 @@ def get_expense_dialog_data(pos_profile, pos_opening_shift):
2933
"payment_methods": get_payment_methods(pos_profile),
3034
"employees": get_active_employees(company),
3135
"maximum_expense_amount": maximum_expense_amount,
36+
"shift_expense_total": shift_expense_total,
37+
"remaining_expense_amount": remaining_expense_amount,
3238
}
3339

3440

@@ -49,7 +55,7 @@ def create_pos_expense(
4955

5056
validate_pos_expense_enabled(pos_profile)
5157
shift = validate_open_shift(pos_opening_shift, pos_profile)
52-
validate_expense_amount(amount, pos_profile)
58+
validate_expense_amount(amount, pos_profile, pos_opening_shift)
5359
validate_expense_account(expense_account, shift.company)
5460
validate_mode_of_payment(mode_of_payment, pos_profile, shift.company)
5561
if employee:
@@ -118,23 +124,57 @@ def validate_open_shift(pos_opening_shift, pos_profile):
118124
return shift
119125

120126

121-
def validate_expense_amount(amount, pos_profile):
127+
def validate_expense_amount(amount, pos_profile, pos_opening_shift=None):
122128
if flt(amount) <= 0:
123129
frappe.throw(_("Amount must be greater than zero"))
124130

125131
maximum_amount = flt(
126132
frappe.db.get_value("POS Profile", pos_profile, "posa_maximum_expense_amount")
127133
)
128-
if maximum_amount > 0 and flt(amount) > maximum_amount:
134+
if maximum_amount <= 0:
135+
return
136+
137+
shift_total = get_shift_expense_total(pos_opening_shift) if pos_opening_shift else 0
138+
new_shift_total = shift_total + flt(amount)
139+
if new_shift_total > maximum_amount:
140+
remaining = _get_remaining_shift_expense_amount(maximum_amount, shift_total)
129141
frappe.throw(
130-
_("Amount {0} exceeds the maximum allowed expense amount of {1}").format(
131-
frappe.format_value(amount, {"fieldtype": "Currency"}),
142+
_(
143+
"This expense would exceed the shift expense limit of {0}. "
144+
"Expenses recorded this shift: {1}. Remaining allowance: {2}"
145+
).format(
132146
frappe.format_value(maximum_amount, {"fieldtype": "Currency"}),
147+
frappe.format_value(shift_total, {"fieldtype": "Currency"}),
148+
frappe.format_value(remaining, {"fieldtype": "Currency"}),
133149
),
134-
title=_("Maximum Expense Amount Exceeded"),
150+
title=_("Shift Expense Limit Exceeded"),
135151
)
136152

137153

154+
def get_shift_expense_total(pos_opening_shift):
155+
"""Return the total submitted POS expense amount for an opening shift."""
156+
if not pos_opening_shift:
157+
return 0
158+
159+
total = frappe.db.sql(
160+
"""
161+
SELECT COALESCE(SUM(posa_expense_amount), 0)
162+
FROM `tabJournal Entry`
163+
WHERE posa_is_pos_expense = 1
164+
AND posa_pos_opening_shift = %s
165+
AND docstatus = 1
166+
""",
167+
pos_opening_shift,
168+
)
169+
return flt(total[0][0] if total else 0)
170+
171+
172+
def _get_remaining_shift_expense_amount(maximum_amount, shift_expense_total):
173+
if flt(maximum_amount) <= 0:
174+
return 0
175+
return max(0, flt(maximum_amount) - flt(shift_expense_total))
176+
177+
138178
def validate_expense_account(expense_account, company):
139179
if not expense_account:
140180
frappe.throw(_("Expense Account is required"))

pos_next/api/test_expenses.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,40 @@ def test_validate_expense_amount_rejects_zero(self, mock_get_value, _mock_throw)
6969

7070
@patch("pos_next.api.expenses.frappe.throw", side_effect=_raise_runtime_error)
7171
@patch("pos_next.api.expenses.frappe.format_value", side_effect=lambda value, _options: str(value))
72+
@patch("pos_next.api.expenses.get_shift_expense_total", return_value=0)
7273
@patch("pos_next.api.expenses.frappe.db.get_value")
73-
def test_validate_expense_amount_rejects_over_maximum(
74-
self, mock_get_value, _mock_format, _mock_throw
74+
def test_validate_expense_amount_rejects_over_shift_limit(
75+
self, mock_get_value, _mock_shift_total, _mock_format, _mock_throw
7576
):
7677
mock_get_value.return_value = 100
7778

78-
with self.assertRaisesRegex(RuntimeError, "exceeds the maximum"):
79-
expenses.validate_expense_amount(150, "Test POS Profile")
79+
with self.assertRaisesRegex(RuntimeError, "shift expense limit"):
80+
expenses.validate_expense_amount(150, "Test POS Profile", "POS-OS-0001")
81+
82+
@patch("pos_next.api.expenses.frappe.throw", side_effect=_raise_runtime_error)
83+
@patch("pos_next.api.expenses.frappe.format_value", side_effect=lambda value, _options: str(value))
84+
@patch("pos_next.api.expenses.get_shift_expense_total", return_value=80)
85+
@patch("pos_next.api.expenses.frappe.db.get_value")
86+
def test_validate_expense_amount_rejects_when_cumulative_exceeds_limit(
87+
self, mock_get_value, _mock_shift_total, _mock_format, _mock_throw
88+
):
89+
mock_get_value.return_value = 100
90+
91+
with self.assertRaisesRegex(RuntimeError, "shift expense limit"):
92+
expenses.validate_expense_amount(30, "Test POS Profile", "POS-OS-0001")
93+
94+
@patch("pos_next.api.expenses.frappe.db.sql", return_value=((80,),))
95+
def test_get_shift_expense_total_sums_submitted_journal_entries(self, mock_sql):
96+
total = expenses.get_shift_expense_total("POS-OS-0001")
97+
98+
self.assertEqual(total, 80)
99+
mock_sql.assert_called_once()
100+
101+
@patch("pos_next.api.expenses.get_shift_expense_total", return_value=0)
102+
def test_get_remaining_shift_expense_amount(self, _mock_shift_total):
103+
self.assertEqual(expenses._get_remaining_shift_expense_amount(100, 30), 70)
104+
self.assertEqual(expenses._get_remaining_shift_expense_amount(100, 120), 0)
105+
self.assertEqual(expenses._get_remaining_shift_expense_amount(0, 50), 0)
80106

81107
@patch("pos_next.api.expenses.frappe.throw", side_effect=_raise_runtime_error)
82108
@patch("pos_next.api.expenses.frappe.db.get_value")

pos_next/pos_next/custom/pos_profile.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@
334334
"creation": "2026-06-14 10:00:00.000000",
335335
"default": null,
336336
"depends_on": "eval:doc.posa_allow_pos_expense",
337-
"description": "Maximum amount a cashier can spend in a single POS Expense transaction",
337+
"description": "Maximum total POS Expense amount allowed per shift session",
338338
"docstatus": 0,
339339
"dt": "POS Profile",
340340
"fetch_from": null,

pos_next/translations/ar.csv

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1580,6 +1580,12 @@ Points applied: {0}. Please pay remaining {1} with {2},تم خصم النقاط:
15801580
"Expense Account is required","حساب المصروف مطلوب",""
15811581
"Amount must be greater than zero","يجب أن يكون المبلغ أكبر من صفر",""
15821582
"Amount exceeds the maximum allowed expense amount of {0}","المبلغ يتجاوز الحد الأقصى المسموح للمصروف {0}",""
1583+
"Amount exceeds the remaining shift expense allowance of {0}","المبلغ يتجاوز المسموح المتبقي لمصروفات الوردية وهو {0}",""
1584+
"Shift limit: {0}","حد الوردية: {0}",""
1585+
"Recorded: {0}","المسجل: {0}",""
1586+
"Remaining: {0}","المتبقي: {0}",""
1587+
"This expense would exceed the shift expense limit of {0}. Expenses recorded this shift: {1}. Remaining allowance: {2}","سيؤدي هذا المصروف إلى تجاوز حد مصروفات الوردية البالغ {0}. المصروفات المسجلة في هذه الوردية: {1}. المسموح المتبقي: {2}",""
1588+
"Shift Expense Limit Exceeded","تم تجاوز حد مصروفات الوردية",""
15831589
"Mode of Payment is required","طريقة الدفع مطلوبة",""
15841590
"POS Expenses","مصروفات نقطة البيع",""
15851591
"{0} expenses","{0} مصروفات",""

pos_next/translations/id.csv

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1494,3 +1494,9 @@
14941494
"{0} units available in ""{1}""","{0} unit tersedia di ""{1}""",""
14951495
"{0} updated","{0} diperbarui",""
14961496
"{0}%","{0}%",""
1497+
"Shift limit: {0}","Batas shift: {0}",""
1498+
"Recorded: {0}","Tercatat: {0}",""
1499+
"Remaining: {0}","Sisa: {0}",""
1500+
"Amount exceeds the remaining shift expense allowance of {0}","Jumlah melebihi sisa tunjangan pengeluaran shift sebesar {0}",""
1501+
"This expense would exceed the shift expense limit of {0}. Expenses recorded this shift: {1}. Remaining allowance: {2}","Pengeluaran ini akan melebihi batas pengeluaran shift sebesar {0}. Pengeluaran yang tercatat pada shift ini: {1}. Sisa tunjangan: {2}",""
1502+
"Shift Expense Limit Exceeded","Batas Pengeluaran Shift Terlampaui",""

pos_next/translations/pt-br.csv

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,3 +1515,9 @@
15151515
"variants","variantes",""
15161516
"{0} updated","{0} atualizado",""
15171517
"{0}%","{0}%",""
1518+
"Shift limit: {0}","Limite do turno: {0}",""
1519+
"Recorded: {0}","Registrado: {0}",""
1520+
"Remaining: {0}","Restante: {0}",""
1521+
"Amount exceeds the remaining shift expense allowance of {0}","O valor excede a margem restante de despesas do turno de {0}",""
1522+
"This expense would exceed the shift expense limit of {0}. Expenses recorded this shift: {1}. Remaining allowance: {2}","Esta despesa excederia o limite de despesas do turno de {0}. Despesas registradas neste turno: {1}. Margem restante: {2}",""
1523+
"Shift Expense Limit Exceeded","Limite de Despesa do Turno Excedido",""

0 commit comments

Comments
 (0)