From d5e05e85ea43cf84c5796c6f6d49081915ef8102 Mon Sep 17 00:00:00 2001 From: MohamedAliSmk Date: Tue, 9 Jun 2026 12:10:21 +0300 Subject: [PATCH 1/2] feat: add bank deposit information to payments and cash control report - Introduced new columns for bank deposit amount and deposit date in the report. - Implemented a function to batch-fetch bank deposit data per shift. - Updated the workspace configuration to include a link to the Bank Deposits DocType. --- .../pos_next/custom/pos_closing_shift.json | 22 +++ .../doctype/bank_deposits/bank_deposits.js | 34 ++++ .../doctype/bank_deposits/bank_deposits.json | 165 ++++++++++++++++++ .../doctype/bank_deposits/bank_deposits.py | 119 +++++++++++++ .../bank_deposits/test_bank_deposits.py | 8 + .../payments_and_cash_control_report.py | 44 ++++- .../pos_next/workspace/posnext/posnext.json | 12 +- 7 files changed, 402 insertions(+), 2 deletions(-) create mode 100644 pos_next/pos_next/custom/pos_closing_shift.json create mode 100644 pos_next/pos_next/doctype/bank_deposits/bank_deposits.js create mode 100644 pos_next/pos_next/doctype/bank_deposits/bank_deposits.json create mode 100644 pos_next/pos_next/doctype/bank_deposits/bank_deposits.py create mode 100644 pos_next/pos_next/doctype/bank_deposits/test_bank_deposits.py diff --git a/pos_next/pos_next/custom/pos_closing_shift.json b/pos_next/pos_next/custom/pos_closing_shift.json new file mode 100644 index 000000000..a8011728d --- /dev/null +++ b/pos_next/pos_next/custom/pos_closing_shift.json @@ -0,0 +1,22 @@ +{ + "custom_fields": [ + { + "allow_on_submit": 1, + "dt": "POS Closing Shift", + "fieldname": "custom_bank_deposit", + "fieldtype": "Link", + "insert_after": "pos_opening_shift", + "label": "Bank Deposit", + "module": "POS Next", + "name": "POS Closing Shift-custom_bank_deposit", + "no_copy": 1, + "options": "Bank Deposits", + "read_only": 1 + } + ], + "custom_perms": [], + "doctype": "POS Closing Shift", + "links": [], + "property_setters": [], + "sync_on_migrate": 1 +} diff --git a/pos_next/pos_next/doctype/bank_deposits/bank_deposits.js b/pos_next/pos_next/doctype/bank_deposits/bank_deposits.js new file mode 100644 index 000000000..c1531e541 --- /dev/null +++ b/pos_next/pos_next/doctype/bank_deposits/bank_deposits.js @@ -0,0 +1,34 @@ +// Copyright (c) 2026, BrainWise and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Bank Deposits", { + onload(frm) { + if (frm.is_new() && !frm.doc.posting_date) { + frm.set_value("posting_date", frappe.datetime.get_today()); + } + + frm.set_query("pos_profile", () => ({ + filters: { disabled: 0 }, + })); + + frm.set_query("pos_closing_shift", (doc) => ({ + query: "pos_next.pos_next.doctype.bank_deposits.bank_deposits.get_pos_closing_shifts", + filters: { pos_profile: doc.pos_profile }, + })); + }, + + pos_closing_shift(frm) { + if (!frm.doc.pos_closing_shift) return; + + frappe.db.get_value( + "POS Closing Shift", + frm.doc.pos_closing_shift, + ["pos_profile"], + (r) => { + if (r && r.pos_profile && r.pos_profile !== frm.doc.pos_profile) { + frm.set_value("pos_profile", r.pos_profile); + } + }, + ); + }, +}); diff --git a/pos_next/pos_next/doctype/bank_deposits/bank_deposits.json b/pos_next/pos_next/doctype/bank_deposits/bank_deposits.json new file mode 100644 index 000000000..8315ac5d5 --- /dev/null +++ b/pos_next/pos_next/doctype/bank_deposits/bank_deposits.json @@ -0,0 +1,165 @@ +{ + "actions": [], + "allow_rename": 0, + "autoname": "BD-.YYYY.-.#####", + "creation": "2026-06-09 10:00:00.000000", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "pos_profile", + "posting_date", + "column_break_1", + "pos_closing_shift", + "section_break_amount", + "deposit_amount", + "section_break_attachment", + "bank_transaction_doc", + "section_break_remarks", + "remarks", + "amended_from" + ], + "fields": [ + { + "fieldname": "pos_profile", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "POS Profile", + "options": "POS Profile", + "reqd": 1 + }, + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Posting Date", + "read_only": 1 + }, + { + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, + { + "fieldname": "pos_closing_shift", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "POS Closing Shift", + "options": "POS Closing Shift", + "reqd": 1 + }, + { + "fieldname": "section_break_amount", + "fieldtype": "Section Break", + "label": "Deposit Details" + }, + { + "fieldname": "deposit_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Deposit Amount", + "reqd": 1 + }, + { + "fieldname": "section_break_attachment", + "fieldtype": "Section Break", + "label": "Bank Receipt" + }, + { + "fieldname": "bank_transaction_doc", + "fieldtype": "Attach", + "label": "Bank Transaction Document", + "reqd": 1 + }, + { + "fieldname": "section_break_remarks", + "fieldtype": "Section Break" + }, + { + "fieldname": "remarks", + "fieldtype": "Small Text", + "label": "Remarks" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Bank Deposits", + "print_hide": 1, + "read_only": 1 + } + ], + "is_submittable": 1, + "links": [], + "modified": "2026-06-09 10:00:00.000000", + "modified_by": "Administrator", + "module": "POS Next", + "name": "Bank Deposits", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Nexus POS Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "POSNext Cashier", + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} diff --git a/pos_next/pos_next/doctype/bank_deposits/bank_deposits.py b/pos_next/pos_next/doctype/bank_deposits/bank_deposits.py new file mode 100644 index 000000000..01cd87c77 --- /dev/null +++ b/pos_next/pos_next/doctype/bank_deposits/bank_deposits.py @@ -0,0 +1,119 @@ +# Copyright (c) 2026, BrainWise and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import flt, today + + +class BankDeposits(Document): + def validate(self): + if not self.posting_date: + self.posting_date = today() + + self.validate_pos_profile() + self.validate_pos_closing_shift() + self.validate_deposit_amount() + self.validate_bank_receipt() + + def validate_pos_profile(self): + if frappe.db.get_value("POS Profile", self.pos_profile, "disabled"): + frappe.throw( + _("POS Profile {0} is not active").format(frappe.bold(self.pos_profile)), + title=_("Invalid POS Profile"), + ) + + def validate_pos_closing_shift(self): + shift = frappe.db.get_value( + "POS Closing Shift", + self.pos_closing_shift, + ["docstatus", "pos_profile", "pos_opening_shift"], + as_dict=True, + ) + if not shift: + frappe.throw(_("POS Closing Shift {0} does not exist").format(self.pos_closing_shift)) + + if shift.docstatus != 1: + frappe.throw(_("POS Closing Shift must be submitted")) + + opening_status = frappe.db.get_value("POS Opening Shift", shift.pos_opening_shift, "status") + if opening_status != "Closed": + frappe.throw(_("POS Closing Shift must be closed")) + + if shift.pos_profile != self.pos_profile: + frappe.throw(_("POS Closing Shift does not belong to the selected POS Profile")) + + existing = frappe.db.exists( + "Bank Deposits", + { + "pos_closing_shift": self.pos_closing_shift, + "name": ["!=", self.name], + "docstatus": ["!=", 2], + }, + ) + if existing: + frappe.throw( + _("Bank Deposit {0} already exists for this shift").format(frappe.bold(existing)), + title=_("Duplicate Bank Deposit"), + ) + + def validate_deposit_amount(self): + if flt(self.deposit_amount) <= 0: + frappe.throw(_("Deposit Amount must be greater than zero")) + + def validate_bank_receipt(self): + if not self.bank_transaction_doc: + frappe.throw(_("Bank Transaction Document is required")) + + def on_submit(self): + frappe.db.set_value( + "POS Closing Shift", + self.pos_closing_shift, + "custom_bank_deposit", + self.name, + update_modified=False, + ) + + def on_cancel(self): + frappe.db.set_value( + "POS Closing Shift", + self.pos_closing_shift, + "custom_bank_deposit", + None, + update_modified=False, + ) + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_pos_closing_shifts(doctype, txt, searchfield, start, page_len, filters): + """Return submitted, closed POS Closing Shifts without an existing Bank Deposit.""" + pos_profile = (filters or {}).get("pos_profile") + profile_condition = "AND pcs.pos_profile = %(pos_profile)s" if pos_profile else "" + + return frappe.db.sql( + f""" + SELECT pcs.name + FROM `tabPOS Closing Shift` pcs + INNER JOIN `tabPOS Opening Shift` pos ON pos.name = pcs.pos_opening_shift + WHERE pcs.docstatus = 1 + AND pos.status = 'Closed' + AND pcs.name NOT IN ( + SELECT bd.pos_closing_shift + FROM `tabBank Deposits` bd + WHERE bd.docstatus != 2 + AND bd.pos_closing_shift IS NOT NULL + ) + {profile_condition} + AND pcs.name LIKE %(txt)s + ORDER BY pcs.period_end_date DESC + LIMIT %(page_len)s OFFSET %(start)s + """, + { + "txt": f"%{txt}%", + "start": start, + "page_len": page_len, + "pos_profile": pos_profile, + }, + ) diff --git a/pos_next/pos_next/doctype/bank_deposits/test_bank_deposits.py b/pos_next/pos_next/doctype/bank_deposits/test_bank_deposits.py new file mode 100644 index 000000000..e7cee58ea --- /dev/null +++ b/pos_next/pos_next/doctype/bank_deposits/test_bank_deposits.py @@ -0,0 +1,8 @@ +# Copyright (c) 2026, BrainWise and contributors +# For license information, please see license.txt + +from frappe.tests import IntegrationTestCase + + +class TestBankDeposits(IntegrationTestCase): + pass diff --git a/pos_next/pos_next/report/payments_and_cash_control_report/payments_and_cash_control_report.py b/pos_next/pos_next/report/payments_and_cash_control_report/payments_and_cash_control_report.py index 3dae6e4c7..d2c4f0f51 100644 --- a/pos_next/pos_next/report/payments_and_cash_control_report/payments_and_cash_control_report.py +++ b/pos_next/pos_next/report/payments_and_cash_control_report/payments_and_cash_control_report.py @@ -103,6 +103,18 @@ def get_columns(payment_methods): ]) columns.extend([ + { + "fieldname": "bank_deposit_amount", + "label": _("Bank Deposit Amount"), + "fieldtype": "Currency", + "width": 140 + }, + { + "fieldname": "deposit_date", + "label": _("Deposit Date"), + "fieldtype": "Date", + "width": 110 + }, { "fieldname": "total_opening", "label": _("Total Opening"), @@ -177,8 +189,9 @@ def get_data(filters): # Discover payment methods (ordered by first appearance) payment_methods = list(dict.fromkeys(r.payment_method for r in raw)) - # Batch-fetch transaction counts per shift (total, not per method) + # Batch-fetch transaction counts and bank deposit data per shift transaction_map = _get_transaction_counts(raw) + bank_deposit_map = _get_bank_deposit_data(raw) # Pivot: group rows by shift into one row each shifts = {} @@ -195,6 +208,7 @@ def get_data(filters): else: shift_hours = 0 + deposit = bank_deposit_map.get(r.shift, {}) shifts[r.shift] = { "shift": r.shift, "pos_profile": r.pos_profile, @@ -204,6 +218,8 @@ def get_data(filters): "shift_end": r.shift_end, "shift_hours": shift_hours, "total_transactions": transaction_map.get(r.shift, 0), + "bank_deposit_amount": deposit.get("deposit_amount"), + "deposit_date": deposit.get("deposit_date"), "total_opening": 0, "total_expected": 0, "total_closing": 0, @@ -275,6 +291,32 @@ def _get_transaction_counts(data): return {r.shift: r.cnt for r in rows} +def _get_bank_deposit_data(data): + """Batch-fetch bank deposit amount and date per shift.""" + shift_names = list({row.shift for row in data}) + if not shift_names: + return {} + + placeholders = ", ".join(["%s"] * len(shift_names)) + + rows = frappe.db.sql( + """ + SELECT + pcs.name as shift, + bd.deposit_amount, + bd.posting_date as deposit_date + FROM `tabPOS Closing Shift` pcs + INNER JOIN `tabBank Deposits` bd ON bd.name = pcs.custom_bank_deposit + WHERE pcs.name IN ({placeholders}) + AND bd.docstatus = 1 + """.format(placeholders=placeholders), + shift_names, + as_dict=1, + ) + + return {r.shift: r for r in rows} + + def get_conditions(filters): """Build WHERE conditions""" conditions = [] diff --git a/pos_next/pos_next/workspace/posnext/posnext.json b/pos_next/pos_next/workspace/posnext/posnext.json index e29e564fb..7563279cb 100644 --- a/pos_next/pos_next/workspace/posnext/posnext.json +++ b/pos_next/pos_next/workspace/posnext/posnext.json @@ -104,7 +104,7 @@ "hidden": 0, "is_query_report": 0, "label": "Shift Management", - "link_count": 2, + "link_count": 3, "link_type": "DocType", "onboard": 0, "type": "Card Break" @@ -129,6 +129,16 @@ "onboard": 0, "type": "Link" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Bank Deposits", + "link_count": 0, + "link_to": "Bank Deposits", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "hidden": 0, "is_query_report": 0, From 33d34825eb9af247056d039cbce50c9f86bde32e Mon Sep 17 00:00:00 2001 From: MohamedAliSmk Date: Wed, 10 Jun 2026 14:19:49 +0300 Subject: [PATCH 2/2] feat: enhance Bank Deposits functionality with improved shift validation - Added a new function to set the query for POS Closing Shifts based on the selected POS Profile. - Updated the refresh and onload events to utilize the new function for better code organization. - Implemented validation to ensure only submitted POS Closing Shifts can be selected, enhancing data integrity. --- .../doctype/bank_deposits/bank_deposits.js | 40 ++++++++++++++++--- .../doctype/bank_deposits/bank_deposits.py | 5 ++- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/pos_next/pos_next/doctype/bank_deposits/bank_deposits.js b/pos_next/pos_next/doctype/bank_deposits/bank_deposits.js index c1531e541..918855b8d 100644 --- a/pos_next/pos_next/doctype/bank_deposits/bank_deposits.js +++ b/pos_next/pos_next/doctype/bank_deposits/bank_deposits.js @@ -1,6 +1,15 @@ // Copyright (c) 2026, BrainWise and contributors // For license information, please see license.txt +function set_pos_closing_shift_query(frm) { + frm.set_query("pos_closing_shift", () => ({ + query: "pos_next.pos_next.doctype.bank_deposits.bank_deposits.get_pos_closing_shifts", + filters: { + pos_profile: frm.doc.pos_profile, + }, + })); +} + frappe.ui.form.on("Bank Deposits", { onload(frm) { if (frm.is_new() && !frm.doc.posting_date) { @@ -11,10 +20,17 @@ frappe.ui.form.on("Bank Deposits", { filters: { disabled: 0 }, })); - frm.set_query("pos_closing_shift", (doc) => ({ - query: "pos_next.pos_next.doctype.bank_deposits.bank_deposits.get_pos_closing_shifts", - filters: { pos_profile: doc.pos_profile }, - })); + set_pos_closing_shift_query(frm); + }, + + refresh(frm) { + set_pos_closing_shift_query(frm); + }, + + pos_profile(frm) { + if (frm.doc.pos_closing_shift) { + frm.set_value("pos_closing_shift", ""); + } }, pos_closing_shift(frm) { @@ -23,9 +39,21 @@ frappe.ui.form.on("Bank Deposits", { frappe.db.get_value( "POS Closing Shift", frm.doc.pos_closing_shift, - ["pos_profile"], + ["pos_profile", "docstatus"], (r) => { - if (r && r.pos_profile && r.pos_profile !== frm.doc.pos_profile) { + if (!r) return; + + if (r.docstatus !== 1) { + frappe.msgprint({ + title: __("Invalid Shift"), + message: __("Only submitted POS Closing Shifts can be selected."), + indicator: "red", + }); + frm.set_value("pos_closing_shift", ""); + return; + } + + if (r.pos_profile && r.pos_profile !== frm.doc.pos_profile) { frm.set_value("pos_profile", r.pos_profile); } }, diff --git a/pos_next/pos_next/doctype/bank_deposits/bank_deposits.py b/pos_next/pos_next/doctype/bank_deposits/bank_deposits.py index 01cd87c77..4d7745874 100644 --- a/pos_next/pos_next/doctype/bank_deposits/bank_deposits.py +++ b/pos_next/pos_next/doctype/bank_deposits/bank_deposits.py @@ -89,7 +89,8 @@ def on_cancel(self): @frappe.validate_and_sanitize_search_inputs def get_pos_closing_shifts(doctype, txt, searchfield, start, page_len, filters): """Return submitted, closed POS Closing Shifts without an existing Bank Deposit.""" - pos_profile = (filters or {}).get("pos_profile") + filters = filters or {} + pos_profile = filters.get("pos_profile") profile_condition = "AND pcs.pos_profile = %(pos_profile)s" if pos_profile else "" return frappe.db.sql( @@ -102,7 +103,7 @@ def get_pos_closing_shifts(doctype, txt, searchfield, start, page_len, filters): AND pcs.name NOT IN ( SELECT bd.pos_closing_shift FROM `tabBank Deposits` bd - WHERE bd.docstatus != 2 + WHERE bd.docstatus = 1 AND bd.pos_closing_shift IS NOT NULL ) {profile_condition}