From 4c14d3f8d3777e68a6a91a5ad4ce6741d08ef871 Mon Sep 17 00:00:00 2001 From: Ramees-k Date: Sun, 1 Mar 2026 15:02:35 +0300 Subject: [PATCH 1/9] using payment entry insted of pos --- sf_trading/api/sales_invoice_payment.py | 179 +++++++ .../js/sales_invoice_pos_total_popup.js | 451 +++++++++--------- 2 files changed, 397 insertions(+), 233 deletions(-) create mode 100644 sf_trading/api/sales_invoice_payment.py diff --git a/sf_trading/api/sales_invoice_payment.py b/sf_trading/api/sales_invoice_payment.py new file mode 100644 index 0000000..8286fd1 --- /dev/null +++ b/sf_trading/api/sales_invoice_payment.py @@ -0,0 +1,179 @@ +import json + +import frappe +from frappe import _ + + +@frappe.whitelist() +def get_payment_modes_with_account(company: str, mode_list: str | list = None): + """ + Return Mode of Payment names that are enabled and have a default Cash/Bank + account for the given company. Use this to avoid "Please set default Cash or + Bank account" or disabled-mode errors when showing the payment popup. + + Args: + company: Company name + mode_list: Optional JSON list or Python list of mode names to filter + (e.g. from POS Profile). If None, all enabled modes with account are returned. + + Returns: + List of mode names that are enabled and have default account for company. + """ + if not company: + return [] + + if isinstance(mode_list, str): + try: + mode_list = json.loads(mode_list) if mode_list else None + except Exception: + mode_list = None + + # Modes that have a default account for this company + has_account = frappe.db.sql( + """ + SELECT DISTINCT parent + FROM `tabMode of Payment Account` + WHERE company = %s AND default_account IS NOT NULL AND default_account != '' + """, + (company,), + as_list=True, + ) + modes_with_account = {r[0] for r in has_account} + + # Enabled modes: get_list with enabled=1 + if mode_list is not None: + names = [m if isinstance(m, str) else (m.get("name") or m.get("mode_of_payment")) for m in (mode_list or [])] + names = [n for n in names if n] + if not names: + return [] + enabled = frappe.get_all( + "Mode of Payment", + filters={"name": ["in", names], "enabled": 1}, + pluck="name", + ) + else: + enabled = frappe.get_all( + "Mode of Payment", + filters={"enabled": 1}, + pluck="name", + ) + + # Intersection: enabled and has default account for company + valid = [m for m in enabled if m in modes_with_account] + # Preserve order of mode_list when filtering from profile + if mode_list is not None and names: + order = {m: i for i, m in enumerate(names)} + valid.sort(key=lambda m: order.get(m, 999)) + return valid + + +@frappe.whitelist() +def create_pos_payments_for_invoice(sales_invoice: str, payments: str | list): + """ + Create Payment Entry records for a submitted POS Sales Invoice, one per mode of payment. + + Args: + sales_invoice: Sales Invoice name + payments: JSON list or Python list of dicts: + [{ "mode_of_payment": "Cash", "amount": 100.0 }, ...] + + Returns: + List of created Payment Entry names. + """ + + if not sales_invoice: + frappe.throw(_("Sales Invoice is required")) + + si = frappe.get_doc("Sales Invoice", sales_invoice) + + if si.docstatus != 1: + frappe.throw( + _("Sales Invoice {0} must be submitted before creating payments.").format( + si.name + ) + ) + + # Parse payments argument + if isinstance(payments, str): + try: + payments = json.loads(payments) + except Exception: + frappe.throw(_("Invalid payments payload")) + + if not isinstance(payments, (list, tuple)) or not payments: + frappe.throw(_("No payment rows were provided.")) + + # Filter and validate rows (Cash and non-Cash; for Bank we set reference_no/reference_date) + valid_rows = [] + for row in payments: + mode_of_payment = (row or {}).get("mode_of_payment") + amount = frappe.utils.flt((row or {}).get("amount")) + if not mode_of_payment or amount <= 0: + continue + valid_rows.append({"mode_of_payment": mode_of_payment, "amount": amount}) + + if not valid_rows: + frappe.throw(_("No valid payment rows found (non-zero amounts with mode of payment).")) + + created = [] + + from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account + + for row in valid_rows: + # Reload invoice each time so outstanding is up to date after previous payments + si.reload() + outstanding = frappe.utils.flt(si.outstanding_amount) + amount = frappe.utils.flt(row["amount"]) + + if amount <= 0: + continue + + if amount - outstanding > 0.5: + frappe.throw( + _( + "Payment amount {0} is greater than outstanding amount {1} for invoice {2}." + ).format(amount, outstanding, si.name) + ) + + pe = get_payment_entry("Sales Invoice", si.name) + + # Set the specific mode of payment and amount + pe.mode_of_payment = row["mode_of_payment"] + + # paid_to must be the default account from Mode of Payment (get_payment_entry uses SI's mode, not this row's) + bank_cash = get_bank_cash_account(row["mode_of_payment"], si.company) + pe.paid_to = bank_cash.get("account") + if pe.paid_to: + acc = frappe.get_cached_value( + "Account", pe.paid_to, ["account_currency", "account_type"], as_dict=True + ) + if acc: + pe.paid_to_account_currency = acc.account_currency + pe.paid_to_account_type = acc.account_type + + # paid_amount / received_amount in payment currency + pe.paid_amount = amount + pe.received_amount = amount + + # Update reference allocation to match this amount + if pe.references: + # Assume single reference row for this invoice + ref = pe.references[0] + ref.allocated_amount = amount + + # Posting date same as invoice if not set + if not pe.posting_date: + pe.posting_date = si.posting_date + + # Reference No/Date: mandatory when payment account is Bank (PE validates on account type, not mode type). + # Set for all so Cash mode linked to a Bank account does not fail. + pe.reference_no = si.name + pe.reference_date = si.posting_date + + pe.insert() + pe.submit() + created.append(pe.name) + + return created + diff --git a/sf_trading/public/js/sales_invoice_pos_total_popup.js b/sf_trading/public/js/sales_invoice_pos_total_popup.js index 6cc188a..634bfee 100644 --- a/sf_trading/public/js/sales_invoice_pos_total_popup.js +++ b/sf_trading/public/js/sales_invoice_pos_total_popup.js @@ -1,4 +1,4 @@ -// sf_trading: Popup to enter payment amounts when is_pos is checked +// sf_trading: Popup to enter payment amounts for Sales Invoice // Shows after save when the correct grand_total is available frappe.ui.form.on("Sales Invoice", { refresh: function (frm) { @@ -8,14 +8,40 @@ frappe.ui.form.on("Sales Invoice", { const orig = frm.save.bind(frm); frm.save = function (save_action, callback, btn, on_error) { frappe.flags._sf_save_action = save_action || "Save"; + // Show payment popup BEFORE submit (no confirmation); skip when submitting from popup + if ( + save_action === "Submit" && + !frappe.flags.sf_trading_skip_payment_popup && + frm.doc.docstatus === 0 && + frm.doc.name && !String(frm.doc.name).startsWith("new-") && + frm.doc.custom_payment_mode !== "Credit" && + frm.doc.grand_total > 0 + ) { + function show_popup() { + sf_trading_show_pos_total_popup(frm); + return Promise.resolve(); + } + if (frm.doc.pos_profile) { + return frappe.db.get_value( + "POS Profile", + frm.doc.pos_profile, + "disable_grand_total_to_default_mop" + ).then(function (r) { + if (r && r.message === 1) return orig(save_action, callback, btn, on_error); + return show_popup(); + }); + } + return show_popup(); + } return orig(save_action, callback, btn, on_error).finally(function () { delete frappe.flags._sf_save_action; }); }; }, before_save: function (frm) { - // When Include Payment (POS) is NOT checked, ask to submit on Save (not Submit) - if (frm.doc.is_pos || frm.doc.docstatus !== 0) return; + // Only show "Do you want to Submit?" for Credit sales + if (!frm.doc.custom_payment_mode || frm.doc.custom_payment_mode !== "Credit") return; + if (frm.doc.docstatus !== 0) return; if (frappe.flags._sf_save_action === "Submit") return; if (frm._asked_to_submit) return; @@ -43,66 +69,86 @@ frappe.ui.form.on("Sales Invoice", { // Prevent popup if already showing if (frappe.flags.sf_trading_popup_showing) return; - - // Only show for POS invoices in draft state - if (!frm.doc.is_pos || frm.doc.docstatus !== 0) return; - - // Validate required fields - if (!frm.doc.pos_profile || !frm.doc.grand_total || frm.doc.grand_total <= 0) return; - - // Ensure form is ready + + // Credit: no payment popup (only "Do you want to Submit?" in before_save) + if (frm.doc.custom_payment_mode && frm.doc.custom_payment_mode === "Credit") { + return; + } + + // Non-credit: show payment popup after save + if (!frm.doc.grand_total || frm.doc.grand_total <= 0) return; if (!frm.doc.name || frm.doc.name.startsWith("new-")) return; - // Show popup on every save (unless POS Profile disables it) - frappe.db.get_value( - "POS Profile", - frm.doc.pos_profile, - "disable_grand_total_to_default_mop", - function (r) { - if (r && r.message === 1) return; - sf_trading_show_pos_total_popup(frm); - } - ); + if (frm.doc.pos_profile) { + frappe.db.get_value( + "POS Profile", + frm.doc.pos_profile, + "disable_grand_total_to_default_mop", + function (r) { + if (r && r.message === 1) return; + sf_trading_show_pos_total_popup(frm); + } + ); + } else { + sf_trading_show_pos_total_popup(frm); + } }, }); function sf_trading_show_pos_total_popup(frm) { - // Prevent multiple popups if (frappe.flags.sf_trading_popup_showing) return; - - // Validate form state - if (!frm || !frm.doc || !frm.doc.pos_profile) { - console.warn("sf_trading: Cannot show popup - invalid form state"); - return; - } - + if (!frm || !frm.doc) return; + frappe.flags.sf_trading_popup_showing = true; - - function do_show_popup() { - // Load payment modes from POS Profile if empty - if (!frm.doc.payments || frm.doc.payments.length === 0) { + + function ensure_payments_then_show() { + if (frm.doc.payments && frm.doc.payments.length > 0) { + sf_trading_render_dialog(frm); + return; + } + if (frm.doc.pos_profile) { frappe.call({ method: "frappe.client.get", args: { doctype: "POS Profile", name: frm.doc.pos_profile }, callback: function (r) { if (r.message && r.message.payments && r.message.payments.length > 0) { - frm.clear_table("payments"); - r.message.payments.forEach(function (pay) { - const row = frm.add_child("payments"); - row.mode_of_payment = pay.mode_of_payment; - row.default = pay.default; - }); - frm.refresh_field("payments"); + const profile_payments = r.message.payments; + const mode_list = profile_payments.map(function (p) { return p.mode_of_payment; }); + const default_by_mode = {}; + profile_payments.forEach(function (p) { default_by_mode[p.mode_of_payment] = p.default; }); frappe.call({ - doc: frm.doc, - method: "set_account_for_mode_of_payment", - callback: function () { + method: "sf_trading.api.sales_invoice_payment.get_payment_modes_with_account", + args: { company: frm.doc.company, mode_list: mode_list }, + callback: function (res) { + const valid_modes = res.message || []; + if (valid_modes.length === 0) { + frappe.flags.sf_trading_popup_showing = false; + frappe.msgprint(__("No enabled payment modes with a default account for this company. Please set default Cash or Bank account in Mode of Payment or enable the mode.")); + return; + } + frm.clear_table("payments"); + valid_modes.forEach(function (mode) { + const row = frm.add_child("payments"); + row.mode_of_payment = mode; + row.default = default_by_mode[mode] || 0; + }); frm.refresh_field("payments"); - sf_trading_render_dialog(frm); + frappe.call({ + doc: frm.doc, + method: "set_account_for_mode_of_payment", + callback: function () { + frm.refresh_field("payments"); + sf_trading_render_dialog(frm); + }, + error: function () { + frappe.flags.sf_trading_popup_showing = false; + frappe.msgprint(__("Error loading payment accounts. Please try again.")); + } + }); }, - error: function() { + error: function () { frappe.flags.sf_trading_popup_showing = false; - frappe.msgprint(__("Error loading payment accounts. Please try again.")); + frappe.msgprint(__("Error loading payment modes. Please try again.")); } }); } else { @@ -110,17 +156,51 @@ function sf_trading_show_pos_total_popup(frm) { frappe.msgprint(__("Add payment modes in POS Profile first")); } }, - error: function() { + error: function () { frappe.flags.sf_trading_popup_showing = false; frappe.msgprint(__("Error loading POS Profile. Please try again.")); } }); } else { - sf_trading_render_dialog(frm); + // Non-credit without POS Profile: only enabled modes with default account + frappe.call({ + method: "sf_trading.api.sales_invoice_payment.get_payment_modes_with_account", + args: { company: frm.doc.company }, + callback: function (r) { + const modes = r.message || []; + if (modes.length === 0) { + frappe.flags.sf_trading_popup_showing = false; + frappe.msgprint(__("No enabled Mode of Payment with default Cash or Bank account for this company. Please set default account in Mode of Payment.")); + return; + } + frm.clear_table("payments"); + modes.forEach(function (name) { + const row = frm.add_child("payments"); + row.mode_of_payment = name; + }); + frm.refresh_field("payments"); + frappe.call({ + doc: frm.doc, + method: "set_account_for_mode_of_payment", + callback: function () { + frm.refresh_field("payments"); + sf_trading_render_dialog(frm); + }, + error: function () { + frappe.flags.sf_trading_popup_showing = false; + frappe.msgprint(__("Error loading payment accounts. Please try again.")); + } + }); + }, + error: function () { + frappe.flags.sf_trading_popup_showing = false; + frappe.msgprint(__("Error loading payment modes. Please try again.")); + } + }); } } - do_show_popup(); + ensure_payments_then_show(); } function sf_trading_render_dialog(frm) { @@ -190,26 +270,6 @@ function sf_trading_render_dialog(frm) { }); function apply_payments_and_close(vals, submit) { - // Prevent multiple simultaneous saves - if (frappe.flags.sf_trading_saving) { - frappe.msgprint({ - title: __("Please Wait"), - message: __("Saving in progress. Please wait..."), - indicator: "orange", - }); - return; - } - - // Validate form state - if (!frm || !frm.doc || frm.doc.docstatus !== 0) { - frappe.msgprint({ - title: __("Error"), - message: __("Cannot update payments. Form is not in draft state."), - indicator: "red", - }); - return; - } - // Validate inputs if (!vals) { frappe.msgprint({ @@ -220,190 +280,104 @@ function sf_trading_render_dialog(frm) { return; } + // Collect entered payments let total = 0; - // First validate total - payments.forEach(function (p, i) { - const amt = flt(vals["pay_" + i]) || 0; - total += amt; - }); - - if (total < invoice_total) { - frappe.msgprint({ - title: __("Incomplete"), - message: __("{0} still to be allocated", [format_currency(invoice_total - total, currency)]), - indicator: "red", - }); - return; - } - - // Ensure form payments exist and match - const form_payments = frm.doc.payments || []; - if (form_payments.length === 0) { - frappe.msgprint({ - title: __("Error"), - message: __("No payment methods found. Please refresh the form."), - indicator: "red", - }); - return; - } - - // Ensure conversion_rate is valid - const conversion_rate = flt(frm.doc.conversion_rate) || 1; - - // Helper function for precision - const get_precision = function(fieldname, doc) { - try { - return precision(fieldname, doc) || 2; - } catch(e) { - return 2; // Default precision - } - }; - - // Update payments with robust matching - update ALL payments (including zero amounts) - let update_count = 0; + const payments_payload = []; payments.forEach(function (p, i) { const amt = flt(vals["pay_" + i]) || 0; - const base_amt = flt(amt * conversion_rate, get_precision("base_amount", p)); - - // Try multiple matching strategies for reliability - let form_payment = null; - - // Strategy 1: Match by mode_of_payment - if (p.mode_of_payment) { - form_payment = form_payments.find(fp => fp.mode_of_payment === p.mode_of_payment); - } - - // Strategy 2: Match by index if same length - if (!form_payment && i < form_payments.length && payments.length === form_payments.length) { - form_payment = form_payments[i]; - } - - // Strategy 3: Match by idx if available - if (!form_payment && p.idx) { - form_payment = form_payments.find(fp => fp.idx === p.idx); - } - - // Strategy 4: Match by name if available - if (!form_payment && p.name) { - form_payment = form_payments.find(fp => fp.name === p.name); - } - - // Update if match found - update ALL payments including zero amounts - if (form_payment) { - // Update directly on the form doc - this is synchronous - form_payment.amount = amt; - form_payment.base_amount = base_amt; - update_count++; + if (amt > 0) { + payments_payload.push({ + mode_of_payment: p.mode_of_payment, + amount: amt, + }); + total += amt; } }); - - // Validate that we updated at least one payment - if (update_count === 0) { + + if (!payments_payload.length) { frappe.msgprint({ title: __("Error"), - message: __("Could not match payments. Please refresh the form and try again."), + message: __("Please enter at least one payment amount."), indicator: "red", }); return; } - - // Verify payments were updated - const updated_payments = frm.doc.payments.filter(p => flt(p.amount) > 0); - if (updated_payments.length === 0) { + + // Optional: ensure we are not under-allocating + if (total < invoice_total) { frappe.msgprint({ - title: __("Error"), - message: __("No payment amounts were set. Please try again."), + title: __("Incomplete"), + message: __("{0} still to be allocated", [ + format_currency(invoice_total - total, currency), + ]), indicator: "red", }); return; } - - // Ensure form recognizes payments as changed - // Update the local doclist to ensure changes are tracked - if (frm.local_doclist && frm.local_doclist["Sales Invoice Payment"]) { - frm.doc.payments.forEach(function(payment) { - const doclist_item = frm.local_doclist["Sales Invoice Payment"].find( - item => item.name === payment.name || item.idx === payment.idx - ); - if (doclist_item) { - doclist_item.amount = payment.amount; - doclist_item.base_amount = payment.base_amount; - } - }); - } - - // Mark form as dirty to ensure changes are saved - frm.dirty(); - - // Refresh payments field to update UI before saving - frm.refresh_field("payments"); - - // Close dialog before saving + + // Close dialog before creating Payment Entries d.hide(); - frappe.flags.sf_trading_skip_payment_popup = true; frappe.flags.sf_trading_popup_showing = false; - frappe.flags.sf_trading_saving = true; - - // Use save with "Submit" action instead of savesubmit - const save_action = submit ? "Submit" : "Save"; - - // Delay to ensure refresh_field completes and form processes updates - setTimeout(function() { - // Double-check payments are in form doc before saving - if (!frm.doc.payments || frm.doc.payments.length === 0) { - frappe.msgprint({ - title: __("Error"), - message: __("Payments were not updated. Please try again."), - indicator: "red", - }); - delete frappe.flags.sf_trading_skip_payment_popup; - delete frappe.flags.sf_trading_saving; - return; - } - - // Verify payments have amounts - const total_payment = frm.doc.payments.reduce((sum, p) => sum + flt(p.amount), 0); - if (total_payment <= 0) { - frappe.msgprint({ - title: __("Error"), - message: __("Total payment amount must be greater than zero."), - indicator: "red", - }); - delete frappe.flags.sf_trading_skip_payment_popup; - delete frappe.flags.sf_trading_saving; - return; - } - - // Save - payments are already updated in frm.doc.payments - frm.save(save_action).then(function(r) { - // After save, refresh payments field to show updated values - // Frappe automatically refreshes the form, but we ensure payments are visible - setTimeout(function() { - frm.refresh_field("payments"); - - if (submit) { - // Reload after submit to show updated status - setTimeout(function() { - frm.reload_doc(); - }, 200); + + // Helper to call backend and create Payment Entries + const create_payments = () => { + frappe.call({ + method: "sf_trading.api.sales_invoice_payment.create_pos_payments_for_invoice", + args: { + sales_invoice: frm.doc.name, + payments: JSON.stringify(payments_payload), + }, + freeze: true, + freeze_message: __("Creating payments..."), + callback: function (r) { + if (r && r.message && r.message.length) { + frappe.show_alert( + { + message: __( + "Created {0} Payment Entries for this invoice", + [r.message.length] + ), + indicator: "green", + }, + 5 + ); + frm.reload_doc(); } - // For Save, don't reload - just refresh payments field - // The form refresh happens automatically, payments should be visible - }, 100); - }).catch(function(err) { - // Show error if save fails - frappe.msgprint({ - title: __("Error"), - message: __("Failed to save invoice: {0}", [err.message || err]), - indicator: "red", - }); - }).finally(function () { - setTimeout(function () { - delete frappe.flags.sf_trading_skip_payment_popup; - delete frappe.flags.sf_trading_saving; - }, 500); + }, + error: function (err) { + console.error("sf_trading: error creating Payment Entries", err); + frappe.msgprint({ + title: __("Error"), + message: __("Could not create Payment Entries. Please try again."), + indicator: "red", + }); + }, }); - }, 300); + }; + + // Ensure invoice is submitted before creating payments if user chose Save & Submit + if (submit && frm.doc.docstatus === 0) { + frappe.flags.sf_trading_skip_payment_popup = true; + frm + .save("Submit") + .then(() => { + create_payments(); + }) + .finally(() => { + setTimeout(() => { + delete frappe.flags.sf_trading_skip_payment_popup; + }, 500); + }); + } else if (frm.doc.docstatus === 1) { + // Already submitted: just create payments + create_payments(); + } else { + // Draft + user clicked Save: no payment creation; popup will show again when they Submit + frappe.show_alert({ + message: __("Invoice saved. Submit the invoice when ready to add payments."), + indicator: "blue", + }, 4); + } } const d = new frappe.ui.Dialog({ @@ -411,6 +385,17 @@ function sf_trading_render_dialog(frm) { fields: fields, primary_action_label: __("Save"), primary_action: function (vals) { + // Draft + Save: just close, no validation, no payment creation; reload so form shows saved state + if (frm.doc.docstatus === 0) { + d.hide(); + frappe.flags.sf_trading_popup_showing = false; + frappe.show_alert({ + message: __("Invoice saved. Submit the invoice when ready to add payments."), + indicator: "blue", + }, 4); + frm.reload_doc(); + return; + } apply_payments_and_close(vals, false); }, secondary_action_label: __("Save & Submit"), From da5ba44e1f67a3c43b91114a2b27f86368c17644 Mon Sep 17 00:00:00 2001 From: Ramees-k Date: Sun, 1 Mar 2026 15:09:09 +0300 Subject: [PATCH 2/9] submit popup fix --- .../js/sales_invoice_pos_total_popup.js | 77 +++++++++++++++++-- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/sf_trading/public/js/sales_invoice_pos_total_popup.js b/sf_trading/public/js/sales_invoice_pos_total_popup.js index 634bfee..93928c3 100644 --- a/sf_trading/public/js/sales_invoice_pos_total_popup.js +++ b/sf_trading/public/js/sales_invoice_pos_total_popup.js @@ -2,6 +2,69 @@ // Shows after save when the correct grand_total is available frappe.ui.form.on("Sales Invoice", { refresh: function (frm) { + // Remove "Permanently Submit?" confirmation when clicking Submit + if (!frm._sf_savesubmit_wrapped) { + frm._sf_savesubmit_wrapped = true; + const orig_savesubmit = frm.savesubmit.bind(frm); + frm.savesubmit = function (btn, callback, on_error) { + var me = this; + // Submit without confirm: either show payment popup first or run submit directly + function do_submit_without_confirm() { + return new Promise(function (resolve) { + me.validate_form_action("Submit"); + frappe.validated = true; + me.script_manager.trigger("before_submit").then(function () { + if (!frappe.validated) return me.handle_save_fail(btn, on_error); + me.save( + "Submit", + function (r) { + if (r.exc) me.handle_save_fail(btn, on_error); + else { + frappe.utils.play_sound("submit"); + callback && callback(); + me.script_manager.trigger("on_submit").then(function () { + resolve(me); + }).then(function () { + if (frappe.route_hooks.after_submit) { + var cb = frappe.route_hooks.after_submit; + delete frappe.route_hooks.after_submit; + cb(me); + } + }); + } + }, + btn, + function () { me.handle_save_fail(btn, on_error); resolve(); } + ); + }); + }); + } + // Show payment popup before submit when applicable (no confirm) + if ( + !frappe.flags.sf_trading_skip_payment_popup && + frm.doc.docstatus === 0 && + frm.doc.name && !String(frm.doc.name).startsWith("new-") && + frm.doc.custom_payment_mode !== "Credit" && + frm.doc.grand_total > 0 + ) { + if (frm.doc.pos_profile) { + return frappe.db.get_value( + "POS Profile", + frm.doc.pos_profile, + "disable_grand_total_to_default_mop" + ).then(function (r) { + if (r && r.message === 1) return do_submit_without_confirm(); + sf_trading_show_pos_total_popup(frm); + return Promise.resolve(); + }); + } + sf_trading_show_pos_total_popup(frm); + return Promise.resolve(); + } + return do_submit_without_confirm(); + }; + } + // Capture save action so before_save knows if user clicked Submit (skip confirm) if (frm._sf_save_wrapped) return; frm._sf_save_wrapped = true; @@ -383,8 +446,13 @@ function sf_trading_render_dialog(frm) { const d = new frappe.ui.Dialog({ title: __("Enter Payment Amounts"), fields: fields, - primary_action_label: __("Save"), + primary_action_label: __("Save & Submit"), primary_action: function (vals) { + if (vals) apply_payments_and_close(vals, true); + }, + secondary_action_label: __("Save"), + secondary_action: function () { + const vals = d.get_values(); // Draft + Save: just close, no validation, no payment creation; reload so form shows saved state if (frm.doc.docstatus === 0) { d.hide(); @@ -396,12 +464,7 @@ function sf_trading_render_dialog(frm) { frm.reload_doc(); return; } - apply_payments_and_close(vals, false); - }, - secondary_action_label: __("Save & Submit"), - secondary_action: function () { - const vals = d.get_values(); - if (vals) apply_payments_and_close(vals, true); + if (vals) apply_payments_and_close(vals, false); }, onhide: function() { // Reset flag when dialog is closed From b937933ab6360b50deaf445b205602d3b2538b47 Mon Sep 17 00:00:00 2001 From: Ramees-k Date: Sun, 1 Mar 2026 15:23:42 +0300 Subject: [PATCH 3/9] DCR fix based on payment entry --- .../report/dcr_report/dcr_report.py | 188 +++++++----------- 1 file changed, 74 insertions(+), 114 deletions(-) diff --git a/sf_trading/sf_trading/report/dcr_report/dcr_report.py b/sf_trading/sf_trading/report/dcr_report/dcr_report.py index 3fb7102..900e8c8 100644 --- a/sf_trading/sf_trading/report/dcr_report/dcr_report.py +++ b/sf_trading/sf_trading/report/dcr_report/dcr_report.py @@ -389,8 +389,8 @@ def get_opening_cash_balance(from_date, company, cost_center): def get_cash_sales(from_date, to_date, company, cost_center): - """Get cash sales (Sales Invoices with is_pos=1 only) - For POS invoices: net_total - change_amount""" + """Get cash sales: Sales Invoices that have a Payment Entry on the same day or before invoice date. + Cash sale = invoice paid on or before invoice date (no credit).""" conditions = "si.docstatus = 1 AND si.is_return = 0" if from_date: conditions += " AND si.posting_date >= %(from_date)s" @@ -398,69 +398,46 @@ def get_cash_sales(from_date, to_date, company, cost_center): conditions += " AND si.posting_date <= %(to_date)s" if company: conditions += " AND si.company = %(company)s" - - cost_center_condition = "" - if cost_center: - cost_center_condition = " AND sii.cost_center = %(cost_center)s" - - # Get cash sales: is_pos=1 only (no mode of payment check) + cost_center_condition = " AND sii.cost_center = %(cost_center)s" if cost_center else "" + # Must have at least one Payment Entry (Receive) with posting_date <= invoice posting_date result = frappe.db.sql(""" - SELECT + SELECT si.name, - si.net_total, si.base_net_total, - COALESCE(si.change_amount, 0) as change_amount, - COALESCE(si.base_change_amount, 0) as base_change_amount, - si.is_pos, si.total_taxes_and_charges as vat_amount, si.discount_amount as discount, SUM(COALESCE(sii.incoming_rate, 0) * sii.stock_qty) as cost FROM `tabSales Invoice` si LEFT JOIN `tabSales Invoice Item` sii ON sii.parent = si.name + INNER JOIN `tabPayment Entry Reference` per ON per.reference_doctype = 'Sales Invoice' AND per.reference_name = si.name + INNER JOIN `tabPayment Entry` pe ON pe.name = per.parent AND pe.docstatus = 1 AND pe.payment_type = 'Receive' WHERE {conditions} - AND si.is_pos = 1 + AND pe.posting_date <= si.posting_date {cost_center_condition} - GROUP BY si.name, si.net_total, si.base_net_total, si.change_amount, - si.base_change_amount, si.is_pos, si.total_taxes_and_charges, si.discount_amount + GROUP BY si.name, si.base_net_total, si.total_taxes_and_charges, si.discount_amount """.format(conditions=conditions, cost_center_condition=cost_center_condition), { "from_date": from_date, "to_date": to_date, "company": company, - "cost_center": cost_center + "cost_center": cost_center, }, as_dict=True) - if result: - total_net = 0 - total_vat = 0 - total_discount = 0 - total_cost = 0 - - for r in result: - # For POS invoices: subtract change_amount - if r.is_pos: - # Use base_net_total - base_change_amount for base currency - net_amount = flt(r.base_net_total) - flt(r.base_change_amount) - else: - net_amount = flt(r.net_total) - - total_net += net_amount - total_vat += flt(r.vat_amount) if r.vat_amount else 0 - total_discount += flt(r.discount) if r.discount else 0 - total_cost += flt(r.cost) if r.cost else 0 - + total_net = sum(flt(r.base_net_total) for r in result) + total_vat = sum(flt(r.vat_amount or 0) for r in result) + total_discount = sum(flt(r.discount or 0) for r in result) + total_cost = sum(flt(r.cost or 0) for r in result) return { "net_total": total_net, "vat_amount": total_vat, "discount": total_discount, - "cost": total_cost + "cost": total_cost, } return {"net_total": 0, "vat_amount": 0, "discount": 0, "cost": 0} def get_credit_sales(from_date, to_date, company, cost_center): - """Get credit sales (Sales Invoices with is_pos=0 only) - Credit sales = invoices where is_pos=0 (non-POS invoices) - Even if they receive payment via Payment Entry later, they're still credit sales""" + """Get credit sales: Sales Invoices that do NOT have a Payment Entry on the same day or before invoice date. + Credit sale = invoice not paid on/before invoice date (payment later or not yet).""" conditions = "si.docstatus = 1 AND si.is_return = 0" if from_date: conditions += " AND si.posting_date >= %(from_date)s" @@ -468,45 +445,45 @@ def get_credit_sales(from_date, to_date, company, cost_center): conditions += " AND si.posting_date <= %(to_date)s" if company: conditions += " AND si.company = %(company)s" - - cost_center_condition = "" - if cost_center: - cost_center_condition = " AND sii.cost_center = %(cost_center)s" - - # Get invoices that are NOT POS (is_pos=0 only) + cost_center_condition = " AND sii.cost_center = %(cost_center)s" if cost_center else "" + # Exclude invoices that have any PE (Receive) with posting_date <= si.posting_date (those are cash sales) result = frappe.db.sql(""" - SELECT - SUM(DISTINCT si.net_total) as net_total, - SUM(DISTINCT si.total_taxes_and_charges) as vat_amount, + SELECT + si.name, + si.base_net_total, + si.total_taxes_and_charges as vat_amount, SUM(COALESCE(sii.incoming_rate, 0) * sii.stock_qty) as cost FROM `tabSales Invoice` si LEFT JOIN `tabSales Invoice Item` sii ON sii.parent = si.name WHERE {conditions} - AND si.is_pos = 0 + AND NOT EXISTS ( + SELECT 1 FROM `tabPayment Entry Reference` per + INNER JOIN `tabPayment Entry` pe ON pe.name = per.parent AND pe.docstatus = 1 AND pe.payment_type = 'Receive' + WHERE per.reference_doctype = 'Sales Invoice' AND per.reference_name = si.name + AND pe.posting_date <= si.posting_date + ) {cost_center_condition} - GROUP BY si.name + GROUP BY si.name, si.base_net_total, si.total_taxes_and_charges """.format(conditions=conditions, cost_center_condition=cost_center_condition), { "from_date": from_date, "to_date": to_date, "company": company, - "cost_center": cost_center + "cost_center": cost_center, }, as_dict=True) - if result: - total_net = sum([flt(r.net_total) for r in result if r.net_total]) - total_vat = sum([flt(r.vat_amount) for r in result if r.vat_amount]) - total_cost = sum([flt(r.cost) for r in result if r.cost]) - + total_net = sum(flt(r.base_net_total) for r in result) + total_vat = sum(flt(r.vat_amount or 0) for r in result) + total_cost = sum(flt(r.cost or 0) for r in result) return { "net_total": total_net, "vat_amount": total_vat, - "cost": total_cost + "cost": total_cost, } return {"net_total": 0, "vat_amount": 0, "cost": 0} def get_sales_returns_cash(from_date, to_date, company, cost_center): - """Get sales returns for POS invoices (is_pos=1 only)""" + """Get sales returns for cash sales (return against invoice that had payment on same day or before).""" conditions = "si.docstatus = 1 AND si.is_return = 1" if from_date: conditions += " AND si.posting_date >= %(from_date)s" @@ -514,31 +491,29 @@ def get_sales_returns_cash(from_date, to_date, company, cost_center): conditions += " AND si.posting_date <= %(to_date)s" if company: conditions += " AND si.company = %(company)s" - - cost_center_condition = "" - if cost_center: - cost_center_condition = " AND sii.cost_center = %(cost_center)s" - + cost_center_condition = " AND EXISTS (SELECT 1 FROM `tabSales Invoice Item` sii WHERE sii.parent = si.name AND sii.cost_center = %(cost_center)s)" if cost_center else "" + # Return is "cash" if the original invoice (return_against) has a PE with posting_date <= original's posting_date result = frappe.db.sql(""" - SELECT - SUM(ABS(si.net_total)) as net_total, - SUM(ABS(si.total_taxes_and_charges)) as vat_amount + SELECT SUM(ABS(si.base_net_total)) as net_total, SUM(ABS(si.total_taxes_and_charges)) as vat_amount FROM `tabSales Invoice` si - LEFT JOIN `tabSales Invoice Item` sii ON sii.parent = si.name WHERE {conditions} - AND si.is_pos = 1 + AND EXISTS ( + SELECT 1 FROM `tabSales Invoice` orig + INNER JOIN `tabPayment Entry Reference` per ON per.reference_doctype = 'Sales Invoice' AND per.reference_name = orig.name + INNER JOIN `tabPayment Entry` pe ON pe.name = per.parent AND pe.docstatus = 1 AND pe.payment_type = 'Receive' + WHERE orig.name = si.return_against AND orig.docstatus = 1 AND pe.posting_date <= orig.posting_date + ) {cost_center_condition} """.format(conditions=conditions, cost_center_condition=cost_center_condition), { "from_date": from_date, "to_date": to_date, "company": company, - "cost_center": cost_center + "cost_center": cost_center, }, as_dict=True) - - if result and result[0].net_total: + if result and (result[0].net_total or result[0].vat_amount): return { "net_total": flt(result[0].net_total), - "vat_amount": flt(result[0].vat_amount) + "vat_amount": flt(result[0].vat_amount), } return {"net_total": 0, "vat_amount": 0} @@ -588,53 +563,41 @@ def get_credit_purchases(from_date, to_date, company, cost_center): def get_cash_receipts_from_pos(from_date, to_date, company, cost_center): - """Get cash receipts from POS invoices (only cash mode payments) - Returns cash payment amounts minus change_amount (VAT is included proportionally in payment amounts)""" - conditions = "si.docstatus = 1 AND si.is_return = 0 AND si.is_pos = 1" + """Get cash receipts from cash sales (Payment Entry Receive, Cash, where payment date <= invoice date). + Same-day or before payment = cash sale; sum received_amount for those PEs in the date range.""" + conditions = "pe.docstatus = 1 AND pe.payment_type = 'Receive'" if from_date: - conditions += " AND si.posting_date >= %(from_date)s" + conditions += " AND pe.posting_date >= %(from_date)s" if to_date: - conditions += " AND si.posting_date <= %(to_date)s" + conditions += " AND pe.posting_date <= %(to_date)s" if company: - conditions += " AND si.company = %(company)s" - - cost_center_condition = "" - if cost_center: - cost_center_condition = " AND EXISTS (SELECT 1 FROM `tabSales Invoice Item` sii WHERE sii.parent = si.name AND sii.cost_center = %(cost_center)s)" - + conditions += " AND pe.company = %(company)s" + cost_center_condition = " AND pe.cost_center = %(cost_center)s" if cost_center else "" result = frappe.db.sql(""" - SELECT - si.name, - COALESCE(si.base_change_amount, 0) as change_amount, - SUM(sip.base_amount) as cash_payment_amount - FROM `tabSales Invoice` si - INNER JOIN `tabSales Invoice Payment` sip ON sip.parent = si.name - INNER JOIN `tabMode of Payment` mop ON mop.name = sip.mode_of_payment + SELECT SUM(pe.received_amount) as amount + FROM `tabPayment Entry` pe + INNER JOIN `tabPayment Entry Reference` per ON per.parent = pe.name + INNER JOIN `tabMode of Payment` mop ON mop.name = pe.mode_of_payment + INNER JOIN `tabSales Invoice` si ON si.name = per.reference_name AND per.reference_doctype = 'Sales Invoice' WHERE {conditions} + AND per.reference_doctype = 'Sales Invoice' + AND pe.posting_date <= si.posting_date AND mop.type = 'Cash' {cost_center_condition} - GROUP BY si.name, si.base_change_amount """.format(conditions=conditions, cost_center_condition=cost_center_condition), { "from_date": from_date, "to_date": to_date, "company": company, - "cost_center": cost_center + "cost_center": cost_center, }, as_dict=True) - - if result: - total_cash_receipts = 0 - for r in result: - # Cash payment amount already includes proportional VAT - # Subtract change_amount which is only for cash payments - cash_receipt = flt(r.cash_payment_amount) - flt(r.change_amount) - total_cash_receipts += cash_receipt - - return flt(total_cash_receipts) + if result and result[0].amount: + return flt(result[0].amount) return 0 def get_cash_received_credit_sales(from_date, to_date, company, cost_center): - """Get cash received against credit sales (Payment Entries for Sales Invoices with Cash mode)""" + """Get cash received against credit sales only. + Count Payment Entries (Receive, Cash) where payment date is AFTER invoice date (collecting on credit).""" conditions = "pe.docstatus = 1 AND pe.payment_type = 'Receive'" if from_date: conditions += " AND pe.posting_date >= %(from_date)s" @@ -642,28 +605,25 @@ def get_cash_received_credit_sales(from_date, to_date, company, cost_center): conditions += " AND pe.posting_date <= %(to_date)s" if company: conditions += " AND pe.company = %(company)s" - - cost_center_condition = "" - if cost_center: - cost_center_condition = " AND pe.cost_center = %(cost_center)s" - + cost_center_condition = " AND pe.cost_center = %(cost_center)s" if cost_center else "" + # Only Sales Invoice refs where si.posting_date < pe.posting_date (payment after invoice = credit collection) result = frappe.db.sql(""" - SELECT - SUM(per.allocated_amount) as amount + SELECT SUM(per.allocated_amount) as amount FROM `tabPayment Entry` pe INNER JOIN `tabPayment Entry Reference` per ON per.parent = pe.name INNER JOIN `tabMode of Payment` mop ON mop.name = pe.mode_of_payment + INNER JOIN `tabSales Invoice` si ON si.name = per.reference_name AND per.reference_doctype = 'Sales Invoice' WHERE {conditions} - AND per.reference_doctype IN ('Sales Invoice', 'Sales Order') + AND per.reference_doctype = 'Sales Invoice' + AND si.posting_date < pe.posting_date AND mop.type = 'Cash' {cost_center_condition} """.format(conditions=conditions, cost_center_condition=cost_center_condition), { "from_date": from_date, "to_date": to_date, "company": company, - "cost_center": cost_center + "cost_center": cost_center, }, as_dict=True) - if result and result[0].amount: return flt(result[0].amount) return 0 From 8064cc6edbef94fdafd79691a3feec79fa3ff34e Mon Sep 17 00:00:00 2001 From: Ramees-k Date: Wed, 11 Mar 2026 12:16:19 +0530 Subject: [PATCH 4/9] dcr update --- sf_trading/fixtures/custom_field.json | 219 ++++++---- sf_trading/hooks.py | 17 +- .../sf_trading/report/dcr_detail/__init__.py | 1 + .../report/dcr_detail/dcr_detail.js | 58 +++ .../report/dcr_detail/dcr_detail.json | 22 + .../report/dcr_detail/dcr_detail.py | 413 ++++++++++++++++++ .../report/dcr_report/dcr_report.py | 112 ++--- 7 files changed, 666 insertions(+), 176 deletions(-) create mode 100644 sf_trading/sf_trading/report/dcr_detail/__init__.py create mode 100644 sf_trading/sf_trading/report/dcr_detail/dcr_detail.js create mode 100644 sf_trading/sf_trading/report/dcr_detail/dcr_detail.json create mode 100644 sf_trading/sf_trading/report/dcr_detail/dcr_detail.py diff --git a/sf_trading/fixtures/custom_field.json b/sf_trading/fixtures/custom_field.json index 54f4af6..75cff80 100644 --- a/sf_trading/fixtures/custom_field.json +++ b/sf_trading/fixtures/custom_field.json @@ -1,94 +1,129 @@ [ - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": "eval:doc.is_internal_customer && doc.represents_company", - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "inter_company_branch", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "ignore_user_permissions": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "insert_after": "represents_company", - "is_system_generated": 0, - "label": "Inter Company Branch", - "module": "Sf Trading", - "name": "Sales Invoice-inter_company_branch", - "no_copy": 0, - "options": "Inter Company Branch", - "permlevel": 0, - "read_only": 0, - "reqd": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Customer", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "custom_commercial_registration_number", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "custom_vat_registration_number", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Commercial Registration Number", - "length": 0, - "link_filters": null, - "mandatory_depends_on": null, - "modified": "2026-01-21 22:02:44.991168", - "module": null, - "name": "Customer-custom_commercial_registration_number", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "placeholder": null, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "show_dashboard": 0, - "sort_options": 0, - "translatable": 0, - "unique": 0, - "width": null - } + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": "eval:doc.is_internal_customer && doc.represents_company", + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "inter_company_branch", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "ignore_user_permissions": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "insert_after": "represents_company", + "is_system_generated": 0, + "label": "Inter Company Branch", + "module": "Sf Trading", + "name": "Sales Invoice-inter_company_branch", + "no_copy": 0, + "options": "Inter Company Branch", + "permlevel": 0, + "read_only": 0, + "reqd": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": "Credit", + "depends_on": null, + "description": "Cash = paid on or before invoice date; Credit = paid later. Set automatically.", + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_sale_type", + "fieldtype": "Select", + "hidden": 0, + "hide_border": 0, + "ignore_user_permissions": 0, + "in_list_view": 1, + "in_standard_filter": 1, + "insert_after": "inter_company_branch", + "is_system_generated": 0, + "label": "Sale Type", + "module": "Sf Trading", + "name": "Sales Invoice-custom_sale_type", + "no_copy": 0, + "options": "Cash\nCredit", + "permlevel": 0, + "read_only": 1, + "reqd": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Customer", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_commercial_registration_number", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "custom_vat_registration_number", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Commercial Registration Number", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2026-01-21 22:02:44.991168", + "module": null, + "name": "Customer-custom_commercial_registration_number", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "placeholder": null, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "show_dashboard": 0, + "sort_options": 0, + "translatable": 0, + "unique": 0, + "width": null + } ] \ No newline at end of file diff --git a/sf_trading/hooks.py b/sf_trading/hooks.py index b6eb98a..c7b64fe 100644 --- a/sf_trading/hooks.py +++ b/sf_trading/hooks.py @@ -270,14 +270,15 @@ { "doctype": "Report", "filters": [ - [ - "name", - "in", - ( - "DCR Report", - "DCR Detailed", - ) - ] + [ + "name", + "in", + ( + "DCR Report", + "DCR Detailed", + "DCR Detail", + ) + ] ] }, { diff --git a/sf_trading/sf_trading/report/dcr_detail/__init__.py b/sf_trading/sf_trading/report/dcr_detail/__init__.py new file mode 100644 index 0000000..8b92b2f --- /dev/null +++ b/sf_trading/sf_trading/report/dcr_detail/__init__.py @@ -0,0 +1 @@ +# DCR Detail report diff --git a/sf_trading/sf_trading/report/dcr_detail/dcr_detail.js b/sf_trading/sf_trading/report/dcr_detail/dcr_detail.js new file mode 100644 index 0000000..efe8068 --- /dev/null +++ b/sf_trading/sf_trading/report/dcr_detail/dcr_detail.js @@ -0,0 +1,58 @@ +// Copyright (c) 2025, sf_trading and contributors +// For license information, please see license.txt + +frappe.query_reports["DCR Detail"] = { + filters: [ + { + fieldname: "report_type", + label: __("Type"), + fieldtype: "Select", + options: [ + "Cash Sales", + "Credit Sales", + "Sales Return - Cash", + "VAT Collected on Cash Sales", + "VAT Applied on Credit Sales", + "VAT Refund on Sales Return", + "Credit Purchase - DIRECT PURCHASE", + "Cash Received : Credit Sales", + "Payments-Petty Cash (Total Payments)", + "Cash Receipts (Cash Sales)", + "Bank Sales Receipts", + "Bank Sales Payments", + "Internal Transfer (Cash Out)", + "Internal Transfer (Cash In)", + ], + default: "Cash Sales", + reqd: 1, + }, + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + }, + { + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + }, + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, + }, + { + fieldname: "cost_center", + label: __("Cost Center"), + fieldtype: "Link", + options: "Cost Center", + }, + ], +}; diff --git a/sf_trading/sf_trading/report/dcr_detail/dcr_detail.json b/sf_trading/sf_trading/report/dcr_detail/dcr_detail.json new file mode 100644 index 0000000..c123b75 --- /dev/null +++ b/sf_trading/sf_trading/report/dcr_detail/dcr_detail.json @@ -0,0 +1,22 @@ +{ + "add_total_row": 1, + "apply_user_permissions": 1, + "creation": "2025-01-21 22:00:00", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "module": "Sf Trading", + "name": "DCR Detail", + "owner": "Administrator", + "ref_doctype": "Sales Invoice", + "report_name": "DCR Detail", + "report_type": "Script Report", + "roles": [ + {"role": "Accounts Manager"}, + {"role": "Accounts User"}, + {"role": "System Manager"} + ] +} diff --git a/sf_trading/sf_trading/report/dcr_detail/dcr_detail.py b/sf_trading/sf_trading/report/dcr_detail/dcr_detail.py new file mode 100644 index 0000000..e2bfc42 --- /dev/null +++ b/sf_trading/sf_trading/report/dcr_detail/dcr_detail.py @@ -0,0 +1,413 @@ +# Copyright (c) 2025, sf_trading and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.utils import flt, getdate + + +REPORT_TYPES = [ + "Cash Sales", + "Credit Sales", + "Sales Return - Cash", + "VAT Collected on Cash Sales", + "VAT Applied on Credit Sales", + "VAT Refund on Sales Return", + "Credit Purchase - DIRECT PURCHASE", + "Cash Received : Credit Sales", + "Payments-Petty Cash (Total Payments)", + "Cash Receipts (Cash Sales)", + "Bank Sales Receipts", + "Bank Sales Payments", + "Internal Transfer (Cash Out)", + "Internal Transfer (Cash In)", +] + + +def execute(filters=None): + if not filters: + filters = {} + if not filters.get("report_type"): + return get_columns("Cash Sales"), [] + if not filters.get("from_date") or not filters.get("to_date"): + frappe.throw(_("Please select From Date and To Date")) + from_date = getdate(filters.get("from_date")) + to_date = getdate(filters.get("to_date")) + company = filters.get("company") + cost_center = filters.get("cost_center") + report_type = filters.get("report_type") + columns = get_columns(report_type) + data = get_data_for_type(report_type, from_date, to_date, company, cost_center) + return columns, data + + +def get_columns(report_type): + """Columns vary by report type.""" + if report_type in ("Cash Sales", "Credit Sales", "VAT Collected on Cash Sales", "VAT Applied on Credit Sales"): + return [ + _("Sales Invoice") + ":Link/Sales Invoice:120", + _("Posting Date") + ":Date:100", + _("Customer") + ":Link/Customer:150", + _("Customer Name") + ":Data:180", + _("Net Total") + ":Currency:120", + _("VAT") + ":Currency:100", + _("Grand Total") + ":Currency:120", + _("Discount") + ":Currency:90", + ] + if report_type in ("Sales Return - Cash", "VAT Refund on Sales Return"): + return [ + _("Sales Invoice") + ":Link/Sales Invoice:120", + _("Posting Date") + ":Date:100", + _("Return Against") + ":Link/Sales Invoice:120", + _("Customer") + ":Link/Customer:150", + _("Net Total") + ":Currency:120", + _("VAT") + ":Currency:100", + ] + if report_type == "Credit Purchase - DIRECT PURCHASE": + return [ + _("Purchase Invoice") + ":Link/Purchase Invoice:120", + _("Posting Date") + ":Date:100", + _("Supplier") + ":Link/Supplier:150", + _("Supplier Name") + ":Data:180", + _("Net Total") + ":Currency:120", + _("VAT") + ":Currency:100", + _("Grand Total") + ":Currency:120", + ] + if report_type in ("Cash Received : Credit Sales", "Cash Receipts (Cash Sales)"): + return [ + _("Payment Entry") + ":Link/Payment Entry:120", + _("Posting Date") + ":Date:100", + _("Party") + ":Data:150", + _("Received Amount") + ":Currency:120", + _("Mode of Payment") + ":Data:120", + _("Reference") + ":Link/Sales Invoice:120", + ] + if report_type == "Payments-Petty Cash (Total Payments)": + return [ + _("Document Type") + ":Data:120", + _("Document") + ":Data:120", + _("Posting Date") + ":Date:100", + _("Party") + ":Data:150", + _("Amount") + ":Currency:120", + _("Mode of Payment") + ":Data:120", + ] + if report_type == "Bank Sales Receipts": + return [ + _("Payment Entry") + ":Link/Payment Entry:120", + _("Posting Date") + ":Date:100", + _("Party") + ":Data:150", + _("Received Amount") + ":Currency:120", + _("Mode of Payment") + ":Data:120", + _("Reference") + ":Data:120", + ] + if report_type == "Bank Sales Payments": + return [ + _("Document Type") + ":Data:120", + _("Document") + ":Data:120", + _("Posting Date") + ":Date:100", + _("Party") + ":Data:150", + _("Amount") + ":Currency:120", + _("Mode of Payment") + ":Data:120", + ] + if report_type in ("Internal Transfer (Cash Out)", "Internal Transfer (Cash In)"): + return [ + _("Payment Entry") + ":Link/Payment Entry:120", + _("Posting Date") + ":Date:100", + _("From Account") + ":Link/Account:150", + _("To Account") + ":Link/Account:150", + _("Amount") + ":Currency:120", + ] + return [] + + +def _base_conditions(from_date, to_date, company, cost_center, date_field="si.posting_date", table_alias="si"): + conditions = [] + if from_date: + conditions.append(f" {date_field} >= %(from_date)s") + if to_date: + conditions.append(f" {date_field} <= %(to_date)s") + if company: + conditions.append(f" {table_alias}.company = %(company)s") + return " AND ".join(conditions) if conditions else " 1=1 " + + +def get_data_for_type(report_type, from_date, to_date, company, cost_center): + params = {"from_date": from_date, "to_date": to_date, "company": company, "cost_center": cost_center} + if report_type in ("Cash Sales", "VAT Collected on Cash Sales"): + return _detail_cash_sales(params, cost_center) + if report_type in ("Credit Sales", "VAT Applied on Credit Sales"): + return _detail_credit_sales(params, cost_center) + if report_type in ("Sales Return - Cash", "VAT Refund on Sales Return"): + return _detail_sales_return_cash(params, cost_center) + if report_type == "Credit Purchase - DIRECT PURCHASE": + return _detail_credit_purchase(params, cost_center) + if report_type == "Cash Received : Credit Sales": + return _detail_cash_received_credit_sales(params, cost_center) + if report_type == "Payments-Petty Cash (Total Payments)": + return _detail_payments_petty_cash(params, cost_center) + if report_type == "Cash Receipts (Cash Sales)": + return _detail_cash_receipts_cash_sales(params, cost_center) + if report_type == "Bank Sales Receipts": + return _detail_bank_sales_receipts(params, cost_center) + if report_type == "Bank Sales Payments": + return _detail_bank_sales_payments(params, cost_center) + if report_type == "Internal Transfer (Cash Out)": + return _detail_internal_transfer_out(params, cost_center) + if report_type == "Internal Transfer (Cash In)": + return _detail_internal_transfer_in(params, cost_center) + return [] + + +def _detail_cash_sales(params, cost_center): + cond = _base_conditions(params["from_date"], params["to_date"], params["company"], cost_center, "si.posting_date") + cc = " AND sii.cost_center = %(cost_center)s" if cost_center else "" + sql = """ + SELECT si.name, si.posting_date, si.customer, si.customer_name, + si.base_net_total, si.total_taxes_and_charges, si.grand_total, si.discount_amount + FROM `tabSales Invoice` si + LEFT JOIN `tabSales Invoice Item` sii ON sii.parent = si.name + INNER JOIN `tabPayment Entry Reference` per ON per.reference_doctype = 'Sales Invoice' AND per.reference_name = si.name + INNER JOIN `tabPayment Entry` pe ON pe.name = per.parent AND pe.docstatus = 1 AND pe.payment_type = 'Receive' + WHERE si.docstatus = 1 AND si.is_return = 0 AND """ + cond + """ + AND pe.posting_date <= si.posting_date + """ + cc + """ + GROUP BY si.name, si.posting_date, si.customer, si.customer_name, si.base_net_total, + si.total_taxes_and_charges, si.grand_total, si.discount_amount + ORDER BY si.posting_date, si.name + """ + rows = frappe.db.sql(sql, params, as_dict=True) + return [_row_si(r) for r in rows] + + +def _row_si(r): + return [ + r.name, + r.posting_date, + r.get("customer"), + r.get("customer_name") or "", + flt(r.get("base_net_total")), + flt(r.get("total_taxes_and_charges")), + flt(r.get("grand_total")), + flt(r.get("discount_amount")), + ] + + +def _detail_credit_sales(params, cost_center): + cond = _base_conditions(params["from_date"], params["to_date"], params["company"], cost_center, "si.posting_date") + cc = " AND sii.cost_center = %(cost_center)s" if cost_center else "" + sql = """ + SELECT si.name, si.posting_date, si.customer, si.customer_name, + si.base_net_total, si.total_taxes_and_charges, si.grand_total, si.discount_amount + FROM `tabSales Invoice` si + LEFT JOIN `tabSales Invoice Item` sii ON sii.parent = si.name + WHERE si.docstatus = 1 AND si.is_return = 0 AND """ + cond + """ + AND NOT EXISTS ( + SELECT 1 FROM `tabPayment Entry Reference` per + INNER JOIN `tabPayment Entry` pe ON pe.name = per.parent AND pe.docstatus = 1 AND pe.payment_type = 'Receive' + WHERE per.reference_doctype = 'Sales Invoice' AND per.reference_name = si.name + AND pe.posting_date <= si.posting_date + ) + """ + cc + """ + GROUP BY si.name, si.posting_date, si.customer, si.customer_name, si.base_net_total, + si.total_taxes_and_charges, si.grand_total, si.discount_amount + ORDER BY si.posting_date, si.name + """ + rows = frappe.db.sql(sql, params, as_dict=True) + return [_row_si(r) for r in rows] + + +def _detail_sales_return_cash(params, cost_center): + cond = _base_conditions(params["from_date"], params["to_date"], params["company"], cost_center, "si.posting_date") + cc = " AND EXISTS (SELECT 1 FROM `tabSales Invoice Item` sii WHERE sii.parent = si.name AND sii.cost_center = %(cost_center)s)" if cost_center else "" + sql = """ + SELECT si.name, si.posting_date, si.return_against, si.customer, + ABS(si.base_net_total) as base_net_total, ABS(si.total_taxes_and_charges) as total_taxes_and_charges + FROM `tabSales Invoice` si + WHERE si.docstatus = 1 AND si.is_return = 1 AND """ + cond + """ + AND EXISTS ( + SELECT 1 FROM `tabSales Invoice` orig + INNER JOIN `tabPayment Entry Reference` per ON per.reference_doctype = 'Sales Invoice' AND per.reference_name = orig.name + INNER JOIN `tabPayment Entry` pe ON pe.name = per.parent AND pe.docstatus = 1 AND pe.payment_type = 'Receive' + WHERE orig.name = si.return_against AND orig.docstatus = 1 AND pe.posting_date <= orig.posting_date + ) + """ + cc + """ + ORDER BY si.posting_date, si.name + """ + rows = frappe.db.sql(sql, params, as_dict=True) + return [[r.name, r.posting_date, r.return_against, r.get("customer"), flt(r.base_net_total), flt(r.total_taxes_and_charges)] for r in rows] + + +def _detail_credit_purchase(params, cost_center): + cond = _base_conditions(params["from_date"], params["to_date"], params["company"], cost_center, "pi.posting_date", "pi") + cc = " AND pii.cost_center = %(cost_center)s" if cost_center else "" + sql = """ + SELECT pi.name, pi.posting_date, pi.supplier, pi.supplier_name, + pi.net_total, pi.total_taxes_and_charges, pi.grand_total + FROM `tabPurchase Invoice` pi + LEFT JOIN `tabPurchase Invoice Item` pii ON pii.parent = pi.name + WHERE pi.docstatus = 1 AND """ + cond + """ + """ + cc + """ + GROUP BY pi.name, pi.posting_date, pi.supplier, pi.supplier_name, pi.net_total, pi.total_taxes_and_charges, pi.grand_total + ORDER BY pi.posting_date, pi.name + """ + rows = frappe.db.sql(sql, params, as_dict=True) + return [[r.name, r.posting_date, r.get("supplier"), r.get("supplier_name") or "", flt(r.net_total), flt(r.total_taxes_and_charges), flt(r.grand_total)] for r in rows] + + +def _detail_cash_received_credit_sales(params, cost_center): + cond = _base_conditions(params["from_date"], params["to_date"], params["company"], cost_center, "pe.posting_date", "pe") + cc = " AND pe.cost_center = %(cost_center)s" if cost_center else "" + sql = """ + SELECT pe.name, pe.posting_date, pe.party, pe.received_amount, pe.mode_of_payment, per.reference_name + FROM `tabPayment Entry` pe + INNER JOIN `tabPayment Entry Reference` per ON per.parent = pe.name + INNER JOIN `tabMode of Payment` mop ON mop.name = pe.mode_of_payment + INNER JOIN `tabSales Invoice` si ON si.name = per.reference_name AND per.reference_doctype = 'Sales Invoice' + WHERE pe.docstatus = 1 AND pe.payment_type = 'Receive' AND """ + cond + """ + AND per.reference_doctype = 'Sales Invoice' AND si.posting_date < pe.posting_date AND mop.type = 'Cash' + """ + cc + """ + ORDER BY pe.posting_date, pe.name + """ + rows = frappe.db.sql(sql, params, as_dict=True) + return [[r.name, r.posting_date, r.get("party") or "", flt(r.received_amount), r.get("mode_of_payment") or "", r.get("reference_name") or ""] for r in rows] + + +def _detail_cash_receipts_cash_sales(params, cost_center): + cond = _base_conditions(params["from_date"], params["to_date"], params["company"], cost_center, "pe.posting_date", "pe") + cc = " AND pe.cost_center = %(cost_center)s" if cost_center else "" + sql = """ + SELECT pe.name, pe.posting_date, pe.party, pe.received_amount, pe.mode_of_payment, per.reference_name + FROM `tabPayment Entry` pe + INNER JOIN `tabPayment Entry Reference` per ON per.parent = pe.name + INNER JOIN `tabMode of Payment` mop ON mop.name = pe.mode_of_payment + INNER JOIN `tabSales Invoice` si ON si.name = per.reference_name AND per.reference_doctype = 'Sales Invoice' + WHERE pe.docstatus = 1 AND pe.payment_type = 'Receive' AND """ + cond + """ + AND per.reference_doctype = 'Sales Invoice' AND pe.posting_date <= si.posting_date AND mop.type = 'Cash' + """ + cc + """ + ORDER BY pe.posting_date, pe.name + """ + rows = frappe.db.sql(sql, params, as_dict=True) + return [[r.name, r.posting_date, r.get("party") or "", flt(r.received_amount), r.get("mode_of_payment") or "", r.get("reference_name") or ""] for r in rows] + + +def _detail_payments_petty_cash(params, cost_center): + cond = _base_conditions(params["from_date"], params["to_date"], params["company"], cost_center, "pe.posting_date", "pe") + cc = " AND pe.cost_center = %(cost_center)s" if cost_center else "" + # Payment Entry (Pay, Cash, PI/PO) + sql_pe = """ + SELECT 'Payment Entry' as doctype, pe.name, pe.posting_date, pe.party, pe.paid_amount, pe.mode_of_payment + FROM `tabPayment Entry` pe + INNER JOIN `tabPayment Entry Reference` per ON per.parent = pe.name + INNER JOIN `tabMode of Payment` mop ON mop.name = pe.mode_of_payment + WHERE pe.docstatus = 1 AND """ + cond + """ + AND pe.payment_type = 'Pay' AND per.reference_doctype IN ('Purchase Invoice', 'Purchase Order') AND mop.type = 'Cash' + """ + cc + """ + ORDER BY pe.posting_date, pe.name + """ + rows_pe = frappe.db.sql(sql_pe, params, as_dict=True) + # Purchase Invoice (is_paid, Cash) + pi_cond = _base_conditions(params["from_date"], params["to_date"], params["company"], cost_center, "pi.posting_date", "pi") + pi_cc = " AND pi.cost_center = %(cost_center)s" if cost_center else "" + sql_pi = """ + SELECT 'Purchase Invoice' as doctype, pi.name, pi.posting_date, pi.supplier as party, pi.base_paid_amount as paid_amount, pi.mode_of_payment + FROM `tabPurchase Invoice` pi + INNER JOIN `tabMode of Payment` mop ON mop.name = pi.mode_of_payment + WHERE pi.docstatus = 1 AND pi.is_paid = 1 AND """ + pi_cond + """ + AND mop.type = 'Cash' + """ + pi_cc + """ + ORDER BY pi.posting_date, pi.name + """ + rows_pi = frappe.db.sql(sql_pi, params, as_dict=True) + out = [] + for r in rows_pe: + out.append([r.doctype, r.name, r.posting_date, r.get("party") or "", flt(r.paid_amount), r.get("mode_of_payment") or ""]) + for r in rows_pi: + out.append([r.doctype, r.name, r.posting_date, r.get("party") or "", flt(r.paid_amount), r.get("mode_of_payment") or ""]) + out.sort(key=lambda x: (x[2], x[1])) + return out + + +def _detail_bank_sales_receipts(params, cost_center): + cond = _base_conditions(params["from_date"], params["to_date"], params["company"], cost_center, "pe.posting_date", "pe") + cc = " AND pe.cost_center = %(cost_center)s" if cost_center else "" + sql = """ + SELECT pe.name, pe.posting_date, pe.party, pe.received_amount, pe.mode_of_payment, + GROUP_CONCAT(DISTINCT per.reference_name ORDER BY per.reference_name SEPARATOR ', ') as refs + FROM `tabPayment Entry` pe + INNER JOIN `tabPayment Entry Reference` per ON per.parent = pe.name + INNER JOIN `tabMode of Payment` mop ON mop.name = pe.mode_of_payment + WHERE pe.docstatus = 1 AND """ + cond + """ + AND pe.payment_type = 'Receive' AND per.reference_doctype IN ('Sales Invoice', 'Sales Order') + AND (mop.type IS NULL OR mop.type != 'Cash') + """ + cc + """ + GROUP BY pe.name, pe.posting_date, pe.party, pe.received_amount, pe.mode_of_payment + ORDER BY pe.posting_date, pe.name + """ + rows = frappe.db.sql(sql, params, as_dict=True) + return [[r.name, r.posting_date, r.get("party") or "", flt(r.received_amount), r.get("mode_of_payment") or "", r.get("refs") or ""] for r in rows] + + +def _detail_bank_sales_payments(params, cost_center): + cond = _base_conditions(params["from_date"], params["to_date"], params["company"], cost_center, "pe.posting_date", "pe") + cc = " AND pe.cost_center = %(cost_center)s" if cost_center else "" + sql_pe = """ + SELECT 'Payment Entry' as doctype, pe.name, pe.posting_date, pe.party, pe.paid_amount, pe.mode_of_payment + FROM `tabPayment Entry` pe + INNER JOIN `tabMode of Payment` mop ON mop.name = pe.mode_of_payment + WHERE pe.docstatus = 1 AND """ + cond + """ + AND pe.payment_type = 'Pay' AND (mop.type IS NULL OR mop.type != 'Cash') + """ + cc + """ + ORDER BY pe.posting_date, pe.name + """ + rows_pe = frappe.db.sql(sql_pe, params, as_dict=True) + pi_cond = _base_conditions(params["from_date"], params["to_date"], params["company"], cost_center, "pi.posting_date", "pi") + pi_cc = " AND pi.cost_center = %(cost_center)s" if cost_center else "" + sql_pi = """ + SELECT 'Purchase Invoice' as doctype, pi.name, pi.posting_date, pi.supplier as party, pi.base_paid_amount as paid_amount, pi.mode_of_payment + FROM `tabPurchase Invoice` pi + INNER JOIN `tabMode of Payment` mop ON mop.name = pi.mode_of_payment + WHERE pi.docstatus = 1 AND pi.is_paid = 1 AND """ + pi_cond + """ + AND (mop.type IS NULL OR mop.type != 'Cash') + """ + pi_cc + """ + ORDER BY pi.posting_date, pi.name + """ + rows_pi = frappe.db.sql(sql_pi, params, as_dict=True) + out = [] + for r in rows_pe: + out.append([r.doctype, r.name, r.posting_date, r.get("party") or "", flt(r.paid_amount), r.get("mode_of_payment") or ""]) + for r in rows_pi: + out.append([r.doctype, r.name, r.posting_date, r.get("party") or "", flt(r.paid_amount), r.get("mode_of_payment") or ""]) + out.sort(key=lambda x: (x[2], x[1])) + return out + + +def _detail_internal_transfer_out(params, cost_center): + cond = _base_conditions(params["from_date"], params["to_date"], params["company"], cost_center, "pe.posting_date", "pe") + cc = " AND pe.cost_center = %(cost_center)s" if cost_center else "" + sql = """ + SELECT pe.name, pe.posting_date, pe.paid_from, pe.paid_to, pe.paid_amount + FROM `tabPayment Entry` pe + INNER JOIN `tabAccount` acc_from ON acc_from.name = pe.paid_from + WHERE pe.docstatus = 1 AND pe.payment_type = 'Internal Transfer' AND """ + cond + """ + AND acc_from.account_type = 'Cash' + """ + cc + """ + ORDER BY pe.posting_date, pe.name + """ + rows = frappe.db.sql(sql, params, as_dict=True) + return [[r.name, r.posting_date, r.paid_from, r.paid_to, flt(r.paid_amount)] for r in rows] + + +def _detail_internal_transfer_in(params, cost_center): + cond = _base_conditions(params["from_date"], params["to_date"], params["company"], cost_center, "pe.posting_date", "pe") + cc = " AND pe.cost_center = %(cost_center)s" if cost_center else "" + sql = """ + SELECT pe.name, pe.posting_date, pe.paid_from, pe.paid_to, pe.received_amount + FROM `tabPayment Entry` pe + INNER JOIN `tabAccount` acc_to ON acc_to.name = pe.paid_to + WHERE pe.docstatus = 1 AND pe.payment_type = 'Internal Transfer' AND """ + cond + """ + AND acc_to.account_type = 'Cash' + """ + cc + """ + ORDER BY pe.posting_date, pe.name + """ + rows = frappe.db.sql(sql, params, as_dict=True) + return [[r.name, r.posting_date, r.paid_from, r.paid_to, flt(r.received_amount)] for r in rows] diff --git a/sf_trading/sf_trading/report/dcr_report/dcr_report.py b/sf_trading/sf_trading/report/dcr_report/dcr_report.py index 900e8c8..2c1f6a2 100644 --- a/sf_trading/sf_trading/report/dcr_report/dcr_report.py +++ b/sf_trading/sf_trading/report/dcr_report/dcr_report.py @@ -105,6 +105,23 @@ def get_list_view_link(doctype, label, filters_dict): return f'{label}' +def get_report_link(label, report_type, from_date_str, to_date_str, company, cost_center): + """Create a clickable link to DCR Detail report with the given type and filters.""" + from frappe.utils import get_url + from urllib.parse import urlencode, quote + query_params = { + "report_type": report_type, + "from_date": from_date_str or "", + "to_date": to_date_str or "", + "company": company or "", + "cost_center": cost_center or "", + } + query_string = urlencode({k: v for k, v in query_params.items() if v}) + report_path = quote("DCR Detail", safe="") + url = get_url(uri=f"/app/query-report/{report_path}?{query_string}") + return f'{label}' + + def get_data(filters): data = [] @@ -225,97 +242,40 @@ def _row(particulars, income, expense, discount_adj, margin=0): except: to_date_str = None - cash_sales_filters = { - "company": company, - "docstatus": "1", - "is_return": "0" - } - # Only add date filter if both dates are valid and not "0000-01-01" - if from_date_str and to_date_str and from_date_str != "0000-01-01" and to_date_str != "0000-01-01" and not from_date_str.startswith("0000-") and not to_date_str.startswith("0000-"): - cash_sales_filters["posting_date"] = [from_date_str, to_date_str] - if cost_center: - cash_sales_filters["cost_center"] = cost_center - data.append(_row(get_list_view_link("Sales Invoice", "CASH SALES", cash_sales_filters), cash_sales, 0, -total_discount_adj, gross_margin_cash)) + data.append(_row(get_report_link("CASH SALES", "Cash Sales", from_date_str, to_date_str, company, cost_center), cash_sales, 0, -total_discount_adj, gross_margin_cash)) - # CREDIT SALES - link to Sales Invoice list filtered by credit sales - credit_sales_filters = { - "company": company, - "docstatus": "1", - "is_return": "0" - } - if from_date_str and to_date_str and from_date_str != "0000-01-01" and to_date_str != "0000-01-01" and not from_date_str.startswith("0000-") and not to_date_str.startswith("0000-"): - credit_sales_filters["posting_date"] = [from_date_str, to_date_str] - if cost_center: - credit_sales_filters["cost_center"] = cost_center - data.append(_row(get_list_view_link("Sales Invoice", "CREDIT SALES", credit_sales_filters), credit_sales, 0, 0, gross_margin_credit)) + # CREDIT SALES - link to DCR Detail report + data.append(_row(get_report_link("CREDIT SALES", "Credit Sales", from_date_str, to_date_str, company, cost_center), credit_sales, 0, 0, gross_margin_credit)) - # Sales Return - Cash - link to Sales Invoice list filtered by returns (moved to position 4) - returns_filters = { - "company": company, - "docstatus": "1", - "is_return": "1" - } - if from_date_str and to_date_str and from_date_str != "0000-01-01" and to_date_str != "0000-01-01" and not from_date_str.startswith("0000-") and not to_date_str.startswith("0000-"): - returns_filters["posting_date"] = [from_date_str, to_date_str] - if cost_center: - returns_filters["cost_center"] = cost_center - data.append(_row(get_list_view_link("Sales Invoice", "Sales Return - Cash", returns_filters), 0, sales_return_cash, 0, 0)) + # Sales Return - Cash - link to DCR Detail report + data.append(_row(get_report_link("Sales Return - Cash", "Sales Return - Cash", from_date_str, to_date_str, company, cost_center), 0, sales_return_cash, 0, 0)) - # VAT Collected on Cash Sales - link to Sales Invoice list (same as cash sales) (moved to position 5) - # VAT collected is income, so it should be positive - data.append(_row(get_list_view_link("Sales Invoice", "VAT Collected on Cash Sales", cash_sales_filters), vat_collected_cash, 0, 0, 0)) + # VAT Collected on Cash Sales - link to DCR Detail report (same invoices as Cash Sales) + data.append(_row(get_report_link("VAT Collected on Cash Sales", "VAT Collected on Cash Sales", from_date_str, to_date_str, company, cost_center), vat_collected_cash, 0, 0, 0)) - # VAT Applied on Credit Sales - link to Sales Invoice list (same as credit sales) (moved to position 6) - # VAT applied is income, so it should be positive - data.append(_row(get_list_view_link("Sales Invoice", "VAT Applied on Credit Sales", credit_sales_filters), vat_applied_credit, 0, 0, 0)) + # VAT Applied on Credit Sales - link to DCR Detail report + data.append(_row(get_report_link("VAT Applied on Credit Sales", "VAT Applied on Credit Sales", from_date_str, to_date_str, company, cost_center), vat_applied_credit, 0, 0, 0)) - # VAT Refund on Sales Return - link to Sales Invoice list (same as returns) (position 7) - # VAT refund is expense, so it should be in expense column - data.append(_row(get_list_view_link("Sales Invoice", "VAT Refund on Sales Return", returns_filters), 0, vat_refund_sales_return, 0, 0)) + # VAT Refund on Sales Return - link to DCR Detail report + data.append(_row(get_report_link("VAT Refund on Sales Return", "VAT Refund on Sales Return", from_date_str, to_date_str, company, cost_center), 0, vat_refund_sales_return, 0, 0)) - # Credit Purchase - link to Purchase Invoice list - purchase_filters = { - "company": company, - "docstatus": "1" - } - if from_date_str and to_date_str and from_date_str != "0000-01-01" and to_date_str != "0000-01-01" and not from_date_str.startswith("0000-") and not to_date_str.startswith("0000-"): - purchase_filters["posting_date"] = [from_date_str, to_date_str] - if cost_center: - purchase_filters["cost_center"] = cost_center - data.append(_row(get_list_view_link("Purchase Invoice", "Credit Purchase - DIRECT PURCHASE", purchase_filters), 0, credit_purchase, 0, 0)) + # Credit Purchase - link to DCR Detail report + data.append(_row(get_report_link("Credit Purchase - DIRECT PURCHASE", "Credit Purchase - DIRECT PURCHASE", from_date_str, to_date_str, company, cost_center), 0, credit_purchase, 0, 0)) - # Cash Received : Credit Sales - link to Payment Entry list filtered by Receive type - payment_receive_filters = { - "company": company, - "docstatus": "1", - "payment_type": "Receive" - } - if from_date_str and to_date_str and from_date_str != "0000-01-01" and to_date_str != "0000-01-01" and not from_date_str.startswith("0000-") and not to_date_str.startswith("0000-"): - payment_receive_filters["posting_date"] = [from_date_str, to_date_str] - if cost_center: - payment_receive_filters["cost_center"] = cost_center - data.append(_row(get_list_view_link("Payment Entry", "Cash Received : Credit Sales", payment_receive_filters), cash_received_credit_sales, 0, 0, 0)) + # Cash Received : Credit Sales - link to DCR Detail report + data.append(_row(get_report_link("Cash Received : Credit Sales", "Cash Received : Credit Sales", from_date_str, to_date_str, company, cost_center), cash_received_credit_sales, 0, 0, 0)) - # Payments-Petty Cash - link to Payment Entry list filtered by Pay type - payment_pay_filters = { - "company": company, - "docstatus": "1", - "payment_type": "Pay" - } - if from_date_str and to_date_str and from_date_str != "0000-01-01" and to_date_str != "0000-01-01" and not from_date_str.startswith("0000-") and not to_date_str.startswith("0000-"): - payment_pay_filters["posting_date"] = [from_date_str, to_date_str] - if cost_center: - payment_pay_filters["cost_center"] = cost_center - data.append(_row(get_list_view_link("Payment Entry", "Payments-Petty Cash (Total Payments)", payment_pay_filters), 0, payments_petty_cash, 0, 0)) + # Payments-Petty Cash - link to DCR Detail report + data.append(_row(get_report_link("Payments-Petty Cash (Total Payments)", "Payments-Petty Cash (Total Payments)", from_date_str, to_date_str, company, cost_center), 0, payments_petty_cash, 0, 0)) # Total receipt petty cash (row 11 - bold) total_receipt_petty_cash = cash_receipts_pos + cash_received_credit_sales data.append(_row("" + _("Total Receipt-Petty Cash") + "", total_receipt_petty_cash, 0, 0, 0)) - # Bank Sales (row 12 - bold) + # Bank Sales (row 12 - bold) - link to DCR Detail (Bank Sales Receipts; user can change type for payments) non_cash_data = get_non_cash_transactions(from_date, to_date, company, cost_center) data.append(_row( - "" + _("Bank Sales") + "", + "" + get_report_link(_("Bank Sales"), "Bank Sales Receipts", from_date_str, to_date_str, company, cost_center) + "", non_cash_data.get("receipts", 0), non_cash_data.get("payments", 0), 0, From 4415b68e45f70fadc1f54a711fd23ea12f23c965 Mon Sep 17 00:00:00 2001 From: Ramees-k Date: Mon, 16 Mar 2026 17:02:16 +0530 Subject: [PATCH 5/9] bug fix- no invoice submit if greater than invoice amount --- sf_trading/fixtures/custom_field.json | 6 +++--- .../public/js/sales_invoice_pos_total_popup.js | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/sf_trading/fixtures/custom_field.json b/sf_trading/fixtures/custom_field.json index 75cff80..4b2c3ec 100644 --- a/sf_trading/fixtures/custom_field.json +++ b/sf_trading/fixtures/custom_field.json @@ -51,11 +51,11 @@ "fetch_if_empty": 0, "fieldname": "custom_sale_type", "fieldtype": "Select", - "hidden": 0, + "hidden": 1, "hide_border": 0, "ignore_user_permissions": 0, - "in_list_view": 1, - "in_standard_filter": 1, + "in_list_view": 0, + "in_standard_filter": 0, "insert_after": "inter_company_branch", "is_system_generated": 0, "label": "Sale Type", diff --git a/sf_trading/public/js/sales_invoice_pos_total_popup.js b/sf_trading/public/js/sales_invoice_pos_total_popup.js index 93928c3..28a1e19 100644 --- a/sf_trading/public/js/sales_invoice_pos_total_popup.js +++ b/sf_trading/public/js/sales_invoice_pos_total_popup.js @@ -366,6 +366,19 @@ function sf_trading_render_dialog(frm) { return; } + // Prevent over-payment: do not allow total entered payments to exceed invoice total + if (total - invoice_total > 0.5) { + frappe.msgprint({ + title: __("Error"), + message: __( + "Total payment amount {0} cannot be greater than invoice total {1}.", + [format_currency(total, currency), format_currency(invoice_total, currency)] + ), + indicator: "red", + }); + return; + } + // Optional: ensure we are not under-allocating if (total < invoice_total) { frappe.msgprint({ From 1d61c7cfc4ec9865b4fbbe9ade875dd3e7b0f06b Mon Sep 17 00:00:00 2001 From: Ramees-k Date: Mon, 16 Mar 2026 17:29:01 +0530 Subject: [PATCH 6/9] added print on submit in sales invoice --- .../js/sales_invoice_pos_total_popup.js | 100 ++++++++++++------ 1 file changed, 68 insertions(+), 32 deletions(-) diff --git a/sf_trading/public/js/sales_invoice_pos_total_popup.js b/sf_trading/public/js/sales_invoice_pos_total_popup.js index 28a1e19..a356126 100644 --- a/sf_trading/public/js/sales_invoice_pos_total_popup.js +++ b/sf_trading/public/js/sales_invoice_pos_total_popup.js @@ -1,5 +1,29 @@ // sf_trading: Popup to enter payment amounts for Sales Invoice // Shows after save when the correct grand_total is available +function sf_trading_open_invoice_print(frm) { + if (!frm || !frm.doc || !frm.doc.name) return; + const base_url = window.location.origin; + const format = encodeURIComponent(frm.meta.default_print_format || ""); + + const url = + `${base_url}/printview?` + + `doctype=Sales%20Invoice` + + `&name=${encodeURIComponent(frm.doc.name)}` + + `&trigger_print=1` + + `&format=${format}` + + `&no_letterhead=0` + + `&settings=%7B%7D` + + `&_lang=${frappe.boot.lang}`; + + const a = document.createElement("a"); + a.href = url; + a.target = "_blank"; + a.rel = "noopener noreferrer"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); +} + frappe.ui.form.on("Sales Invoice", { refresh: function (frm) { // Remove "Permanently Submit?" confirmation when clicking Submit @@ -66,40 +90,48 @@ frappe.ui.form.on("Sales Invoice", { } // Capture save action so before_save knows if user clicked Submit (skip confirm) - if (frm._sf_save_wrapped) return; - frm._sf_save_wrapped = true; - const orig = frm.save.bind(frm); - frm.save = function (save_action, callback, btn, on_error) { - frappe.flags._sf_save_action = save_action || "Save"; - // Show payment popup BEFORE submit (no confirmation); skip when submitting from popup - if ( - save_action === "Submit" && - !frappe.flags.sf_trading_skip_payment_popup && - frm.doc.docstatus === 0 && - frm.doc.name && !String(frm.doc.name).startsWith("new-") && - frm.doc.custom_payment_mode !== "Credit" && - frm.doc.grand_total > 0 - ) { - function show_popup() { - sf_trading_show_pos_total_popup(frm); - return Promise.resolve(); - } - if (frm.doc.pos_profile) { - return frappe.db.get_value( - "POS Profile", - frm.doc.pos_profile, - "disable_grand_total_to_default_mop" - ).then(function (r) { - if (r && r.message === 1) return orig(save_action, callback, btn, on_error); - return show_popup(); - }); + if (!frm._sf_save_wrapped) { + frm._sf_save_wrapped = true; + const orig = frm.save.bind(frm); + frm.save = function (save_action, callback, btn, on_error) { + frappe.flags._sf_save_action = save_action || "Save"; + // Show payment popup BEFORE submit (no confirmation); skip when submitting from popup + if ( + save_action === "Submit" && + !frappe.flags.sf_trading_skip_payment_popup && + frm.doc.docstatus === 0 && + frm.doc.name && !String(frm.doc.name).startsWith("new-") && + frm.doc.custom_payment_mode !== "Credit" && + frm.doc.grand_total > 0 + ) { + function show_popup() { + sf_trading_show_pos_total_popup(frm); + return Promise.resolve(); + } + if (frm.doc.pos_profile) { + return frappe.db.get_value( + "POS Profile", + frm.doc.pos_profile, + "disable_grand_total_to_default_mop" + ).then(function (r) { + if (r && r.message === 1) return orig(save_action, callback, btn, on_error); + return show_popup(); + }); + } + return show_popup(); } - return show_popup(); - } - return orig(save_action, callback, btn, on_error).finally(function () { - delete frappe.flags._sf_save_action; + return orig(save_action, callback, btn, on_error).finally(function () { + delete frappe.flags._sf_save_action; + }); + }; + } + + // Add Print button on submitted invoices + if (frm.doc.docstatus === 1) { + frm.add_custom_button(__("Print Invoice"), function () { + sf_trading_open_invoice_print(frm); }); - }; + } }, before_save: function (frm) { // Only show "Do you want to Submit?" for Credit sales @@ -116,6 +148,8 @@ frappe.ui.form.on("Sales Invoice", { function () { frm.save("Submit").then(function () { frm._asked_to_submit = false; + // Open print immediately after successful submit from confirm dialog + sf_trading_open_invoice_print(frm); frm.reload_doc(); }); }, @@ -437,6 +471,8 @@ function sf_trading_render_dialog(frm) { frm .save("Submit") .then(() => { + // Open print window immediately after successful submit + sf_trading_open_invoice_print(frm); create_payments(); }) .finally(() => { From 87ac3e42083e4439d1feae4bd8e267f7d3410cf6 Mon Sep 17 00:00:00 2001 From: Ramees-k Date: Wed, 18 Mar 2026 20:36:07 +0530 Subject: [PATCH 7/9] ignore attachment validation --- sf_trading/api/sales_invoice_payment.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/sf_trading/api/sales_invoice_payment.py b/sf_trading/api/sales_invoice_payment.py index 8286fd1..73af6c8 100644 --- a/sf_trading/api/sales_invoice_payment.py +++ b/sf_trading/api/sales_invoice_payment.py @@ -172,6 +172,18 @@ def create_pos_payments_for_invoice(sales_invoice: str, payments: str | list): pe.reference_date = si.posting_date pe.insert() + + # Bypass "Draft -> Pending" server script attachment requirement for + # auto-created Payment Entries from the Sales Invoice POS popup. + # + # Setting ignore_validate skips `validate` / `before_submit` for this submit, + # which is where such workflow attachment checks typically run. + pe.flags.ignore_validate = True + + # Keep workflow_state consistent with the expected submitted state. + if hasattr(pe, "workflow_state"): + pe.workflow_state = "Pending" + pe.submit() created.append(pe.name) From 1f6e5dfad8aa3df6e5b9a9b8d219645c0d76cb32 Mon Sep 17 00:00:00 2001 From: sayanthns Date: Mon, 13 Apr 2026 07:11:05 +0300 Subject: [PATCH 8/9] feat: add PI overbilling prevention at draft stage and PR approval status sync - Block Purchase Invoice save when total billed qty (drafts + submitted) exceeds PO qty - Update Purchase Receipt with "Pending Approval" status when linked PI is awaiting approval - Add custom_billing_approval_status field on Purchase Receipt (visible in list view) Co-Authored-By: Claude Opus 4.6 (1M context) --- sf_trading/fixtures/custom_field.json | 35 +++++++++ sf_trading/hooks.py | 4 ++ sf_trading/overrides/purchase_invoice.py | 92 ++++++++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 sf_trading/overrides/purchase_invoice.py diff --git a/sf_trading/fixtures/custom_field.json b/sf_trading/fixtures/custom_field.json index 4b2c3ec..a242f76 100644 --- a/sf_trading/fixtures/custom_field.json +++ b/sf_trading/fixtures/custom_field.json @@ -125,5 +125,40 @@ "translatable": 0, "unique": 0, "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 1, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": "Shows Pending Approval when a linked Purchase Invoice is awaiting manager approval", + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Purchase Receipt", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_billing_approval_status", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "ignore_user_permissions": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "insert_after": "status", + "is_system_generated": 0, + "label": "Billing Approval Status", + "module": "Sf Trading", + "name": "Purchase Receipt-custom_billing_approval_status", + "no_copy": 0, + "options": null, + "permlevel": 0, + "read_only": 1, + "reqd": 0, + "translatable": 0, + "unique": 0 } ] \ No newline at end of file diff --git a/sf_trading/hooks.py b/sf_trading/hooks.py index c7b64fe..61b8be1 100644 --- a/sf_trading/hooks.py +++ b/sf_trading/hooks.py @@ -151,6 +151,8 @@ }, "Purchase Invoice": { "before_validate": "sf_trading.inter_company.purchase_invoice_before_validate", + "validate": "sf_trading.overrides.purchase_invoice.validate", + "on_save": "sf_trading.overrides.purchase_invoice.on_save", }, } @@ -263,6 +265,8 @@ ( "Customer-custom_commercial_registration_number", "Sales Invoice-inter_company_branch", + "Sales Invoice-custom_sale_type", + "Purchase Receipt-custom_billing_approval_status", ) ] ] diff --git a/sf_trading/overrides/purchase_invoice.py b/sf_trading/overrides/purchase_invoice.py new file mode 100644 index 0000000..e83ed99 --- /dev/null +++ b/sf_trading/overrides/purchase_invoice.py @@ -0,0 +1,92 @@ +import frappe +from frappe.utils import flt + + +def validate(doc, method=None): + """Block overbilling at draft/save stage by checking all non-cancelled PIs.""" + _check_overbilling(doc) + + +def on_save(doc, method=None): + """Update Purchase Receipt billing approval status after every save.""" + _update_pr_approval_status(doc) + + +def _check_overbilling(doc): + """Prevent total billed qty (drafts + submitted) from exceeding PO qty.""" + for item in doc.items: + if not item.get("po_detail"): + continue + + po_item = frappe.db.get_value( + "Purchase Order Item", + item.po_detail, + ["qty", "item_code"], + as_dict=True, + ) + if not po_item: + continue + + # Sum qty across ALL non-cancelled PIs for this PO line (excluding current doc) + existing_billed_qty = frappe.db.sql( + """ + SELECT IFNULL(SUM(pi_item.qty), 0) + FROM `tabPurchase Invoice Item` pi_item + INNER JOIN `tabPurchase Invoice` pi ON pi.name = pi_item.parent + WHERE pi_item.po_detail = %s + AND pi.docstatus != 2 + AND pi.name != %s + """, + (item.po_detail, doc.name or ""), + )[0][0] + + total_qty = flt(existing_billed_qty) + flt(item.qty) + + if total_qty > flt(po_item.qty): + over_by = total_qty - flt(po_item.qty) + frappe.throw( + "Overbilling blocked for item {0}:
" + "PO qty: {1} | Already billed (incl. drafts): {2} " + "| This PI qty: {3}
" + "Over by {4}. Cancel or reduce qty in another draft PI first.".format( + po_item.item_code, + flt(po_item.qty), + flt(existing_billed_qty), + flt(item.qty), + over_by, + ) + ) + + +def _update_pr_approval_status(doc): + """Set custom_billing_approval_status on linked Purchase Receipts.""" + linked_prs = list( + set([item.purchase_receipt for item in doc.items if item.get("purchase_receipt")]) + ) + + for pr_name in linked_prs: + # Check if any OTHER non-cancelled PI for this PR is in Pending Approval + pending_elsewhere = frappe.db.sql( + """ + SELECT COUNT(pi.name) + FROM `tabPurchase Invoice` pi + INNER JOIN `tabPurchase Invoice Item` pi_item ON pi_item.parent = pi.name + WHERE pi_item.purchase_receipt = %s + AND pi.docstatus != 2 + AND pi.workflow_state = 'Pending Approval' + AND pi.name != %s + """, + (pr_name, doc.name or ""), + )[0][0] + + current_is_pending = doc.workflow_state == "Pending Approval" + + new_status = "Pending Approval" if (pending_elsewhere or current_is_pending) else "" + + frappe.db.set_value( + "Purchase Receipt", + pr_name, + "custom_billing_approval_status", + new_status, + update_modified=False, + ) From 5a349101d24762edef1aee33d318a2faee6ecff0 Mon Sep 17 00:00:00 2001 From: roshmol Date: Mon, 4 May 2026 13:24:14 +0000 Subject: [PATCH 9/9] fate:crate branch configuration --- .../doctype/branch_configuration/__init__.py | 0 .../branch_configuration.js | 47 ++++ .../branch_configuration.json | 94 +++++++ .../branch_configuration.py | 231 ++++++++++++++++++ .../test_branch_configuration.py | 9 + .../__init__.py | 0 .../branch_configuration_cost_center.js | 8 + .../branch_configuration_cost_center.json | 34 +++ .../branch_configuration_cost_center.py | 9 + .../test_branch_configuration_cost_center.py | 9 + .../__init__.py | 0 .../branch_configuration_mode_of_payment.js | 8 + .../branch_configuration_mode_of_payment.json | 55 +++++ .../branch_configuration_mode_of_payment.py | 9 + ...st_branch_configuration_mode_of_payment.py | 9 + .../branch_configuration_user/__init__.py | 0 .../branch_configuration_user.js | 8 + .../branch_configuration_user.json | 43 ++++ .../branch_configuration_user.py | 9 + .../test_branch_configuration_user.py | 9 + .../__init__.py | 0 .../branch_configuration_warehouse.js | 8 + .../branch_configuration_warehouse.json | 34 +++ .../branch_configuration_warehouse.py | 9 + .../test_branch_configuration_warehouse.py | 9 + 25 files changed, 651 insertions(+) create mode 100644 sf_trading/sf_trading/doctype/branch_configuration/__init__.py create mode 100644 sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.js create mode 100644 sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.json create mode 100644 sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.py create mode 100644 sf_trading/sf_trading/doctype/branch_configuration/test_branch_configuration.py create mode 100644 sf_trading/sf_trading/doctype/branch_configuration_cost_center/__init__.py create mode 100644 sf_trading/sf_trading/doctype/branch_configuration_cost_center/branch_configuration_cost_center.js create mode 100644 sf_trading/sf_trading/doctype/branch_configuration_cost_center/branch_configuration_cost_center.json create mode 100644 sf_trading/sf_trading/doctype/branch_configuration_cost_center/branch_configuration_cost_center.py create mode 100644 sf_trading/sf_trading/doctype/branch_configuration_cost_center/test_branch_configuration_cost_center.py create mode 100644 sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/__init__.py create mode 100644 sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/branch_configuration_mode_of_payment.js create mode 100644 sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/branch_configuration_mode_of_payment.json create mode 100644 sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/branch_configuration_mode_of_payment.py create mode 100644 sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/test_branch_configuration_mode_of_payment.py create mode 100644 sf_trading/sf_trading/doctype/branch_configuration_user/__init__.py create mode 100644 sf_trading/sf_trading/doctype/branch_configuration_user/branch_configuration_user.js create mode 100644 sf_trading/sf_trading/doctype/branch_configuration_user/branch_configuration_user.json create mode 100644 sf_trading/sf_trading/doctype/branch_configuration_user/branch_configuration_user.py create mode 100644 sf_trading/sf_trading/doctype/branch_configuration_user/test_branch_configuration_user.py create mode 100644 sf_trading/sf_trading/doctype/branch_configuration_warehouse/__init__.py create mode 100644 sf_trading/sf_trading/doctype/branch_configuration_warehouse/branch_configuration_warehouse.js create mode 100644 sf_trading/sf_trading/doctype/branch_configuration_warehouse/branch_configuration_warehouse.json create mode 100644 sf_trading/sf_trading/doctype/branch_configuration_warehouse/branch_configuration_warehouse.py create mode 100644 sf_trading/sf_trading/doctype/branch_configuration_warehouse/test_branch_configuration_warehouse.py diff --git a/sf_trading/sf_trading/doctype/branch_configuration/__init__.py b/sf_trading/sf_trading/doctype/branch_configuration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.js b/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.js new file mode 100644 index 0000000..3ad9aa2 --- /dev/null +++ b/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.js @@ -0,0 +1,47 @@ +// Copyright (c) 2026, Enfono and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Branch Configuration", { + refresh(frm) { + set_child_filters(frm); + }, + company(frm) { + set_child_filters(frm); + + // Clear warehouse + cost center + account fields when company changes + // (they may belong to the old company) + if (frm.doc.warehouse && frm.doc.warehouse.length) { + frm.clear_table("warehouse"); + frm.refresh_field("warehouse"); + } + if (frm.doc.cost_center && frm.doc.cost_center.length) { + frm.clear_table("cost_center"); + frm.refresh_field("cost_center"); + } + // MoPs are global, not company-bound — no need to clear on company change. + } +}); + +function set_child_filters(frm) { + frm.set_query("warehouse", "warehouse", function () { + return { + filters: { + company: frm.doc.company, + is_group: 0 + } + }; + }); + + frm.set_query("cost_center", "cost_center", function () { + return { + filters: { + company: frm.doc.company, + is_group: 0 + } + }; + }); + + frm.set_query("mode_of_payment", "mode_of_payment", function () { + return { filters: { type: ["in", ["Cash", "Bank"]] } }; + }); +} diff --git a/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.json b/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.json new file mode 100644 index 0000000..29cb565 --- /dev/null +++ b/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.json @@ -0,0 +1,94 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-05-04 18:33:59.085305", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "branch", + "column_break_jvyk", + "company", + "mode_of_payment_section", + "mode_of_payment", + "warehouse", + "cost_center", + "user" + ], + "fields": [ + { + "fieldname": "branch", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Branch", + "options": "Branch", + "reqd": 1 + }, + { + "fieldname": "column_break_jvyk", + "fieldtype": "Column Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "fieldname": "mode_of_payment_section", + "fieldtype": "Section Break", + "label": "Mode of Payment" + }, + { + "description": "Cash + Bank Modes of Payment allowed for this branch. On Sales Invoice for Branch Users,payment rows whose MoP type is Cash or Bank are constrained to this list (or swapped to the first matching row); BNPL is left untouched.", + "fieldname": "mode_of_payment", + "fieldtype": "Table", + "label": "Mode of Payment", + "options": "Mode of Payment Account" + }, + { + "fieldname": "warehouse", + "fieldtype": "Table", + "label": "Warehouse", + "options": "Branch Configuration Warehouse" + }, + { + "fieldname": "cost_center", + "fieldtype": "Table", + "label": "Cost Center", + "options": "Branch Configuration Cost Center" + }, + { + "fieldname": "user", + "fieldtype": "Table", + "label": "User", + "options": "Branch Configuration User" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-05-04 18:42:48.031006", + "modified_by": "Administrator", + "module": "Sf Trading", + "name": "Branch Configuration", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.py b/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.py new file mode 100644 index 0000000..b405479 --- /dev/null +++ b/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.py @@ -0,0 +1,231 @@ +# User permissions are created for company, branch, warehouse and cost center. +# Company and first warehouse are set as is_default=1 so Frappe uses them as +# the user's Session Defaults instead of falling back to global defaults. +# The "Branch User" role is auto-assigned to users added here. + +import frappe +from frappe.model.document import Document + +BRANCH_USER_ROLE = "Branch User" + + +class BranchConfiguration(Document): + + def validate(self): + # Ensure warehouse and cost center belong to the selected company + if self.company: + for w in self.warehouse: + if w.warehouse: + wh_company = frappe.db.get_value("Warehouse", w.warehouse, "company") + if wh_company and wh_company != self.company: + frappe.throw( + f"Warehouse {w.warehouse} belongs to company {wh_company}, " + f"not {self.company}. Please select a warehouse from the correct company." + ) + + for c in self.cost_center: + if c.cost_center: + cc_company = frappe.db.get_value("Cost Center", c.cost_center, "company") + if cc_company and cc_company != self.company: + frappe.throw( + f"Cost Center {c.cost_center} belongs to company {cc_company}, " + f"not {self.company}. Please select a cost center from the correct company." + ) + + def before_save(self): + if self.is_new(): + return + + old_doc = self.get_doc_before_save() + + old_users = {d.user for d in old_doc.user} + new_users = {d.user for d in self.user} + + removed_users = old_users - new_users + + for user in removed_users: + # Delete company permission + if old_doc.get("company"): + delete_permission(user, "Company", old_doc.company) + + delete_permission(user, "Branch", old_doc.branch) + + for w in old_doc.warehouse: + delete_permission(user, "Warehouse", w.warehouse) + + for c in old_doc.cost_center: + delete_permission(user, "Cost Center", c.cost_center) + + # Remove role if user is not in any other Branch Configuration + # Find what role they had in this branch + old_role = None + for old_u in old_doc.user: + if old_u.user == user: + old_role = old_u.get("role") or BRANCH_USER_ROLE + break + _maybe_remove_role(user, old_role or BRANCH_USER_ROLE, exclude_branch=self.name) + + # Handle company change — remove old company permission for remaining users + old_company = old_doc.get("company") + new_company = self.get("company") + if old_company and old_company != new_company: + for u in self.user: + delete_permission(u.user, "Company", old_company) + + def on_update(self): + self.create_permissions() + + def create_permissions(self): + for u in self.user: + # Create company permission (marked as default so Session Defaults picks it) + if self.company: + create_permission(u.user, "Company", self.company, is_default=1) + + create_permission(u.user, "Branch", self.branch) + + for idx, w in enumerate(self.warehouse): + # First warehouse is the default + create_permission(u.user, "Warehouse", w.warehouse, is_default=1 if idx == 0 else 0) + + for idx, c in enumerate(self.cost_center): + # First cost center is the default + create_permission(u.user, "Cost Center", c.cost_center, is_default=1 if idx == 0 else 0) + + # Also grant access to the company's default cost center (used in tax templates) + # without marking it as default — the branch cost center stays as the user's default + if self.company: + company_default_cc = frappe.db.get_value("Company", self.company, "cost_center") + if company_default_cc: + create_permission(u.user, "Cost Center", company_default_cc, is_default=0) + + # Auto-assign the selected role (Branch User, Warehouse User, or Stock User) + selected_role = u.get("role") or BRANCH_USER_ROLE + _assign_role(u.user, selected_role) + + # Auto-set Module Profile to restrict sidebar modules + if selected_role == BRANCH_USER_ROLE: + _set_module_profile(u.user, "Branch User") + elif selected_role == "Damage User": + _set_module_profile(u.user, "Damage User") + + +def create_permission(user, allow, value, is_default=0): + if not value: + return + + existing = frappe.db.exists("User Permission", { + "user": user, + "allow": allow, + "for_value": value + }) + + if existing: + # Update is_default if needed — but only if no other default exists + if is_default and not _has_existing_default(user, allow, exclude_value=value): + frappe.db.set_value("User Permission", existing, "is_default", 1) + else: + # If we want to set default but one already exists, don't override it + if is_default and _has_existing_default(user, allow): + is_default = 0 + + doc = frappe.new_doc("User Permission") + doc.user = user + doc.allow = allow + doc.for_value = value + doc.is_default = is_default + doc.apply_to_all_doctypes = 1 + doc.insert(ignore_permissions=True) + + +def _has_existing_default(user, allow, exclude_value=None): + """Check if user already has a default User Permission for this allow type.""" + filters = { + "user": user, + "allow": allow, + "is_default": 1, + } + if exclude_value: + filters["for_value"] = ["!=", exclude_value] + + return frappe.db.exists("User Permission", filters) + + +def delete_permission(user, allow, value): + if not value: + return + + perms = frappe.get_all( + "User Permission", + filters={ + "user": user, + "allow": allow, + "for_value": value + }, + pluck="name" + ) + + for p in perms: + frappe.delete_doc("User Permission", p, ignore_permissions=True) + + +def _ensure_system_user(user): + """Website Users cannot hold desk roles. Upgrade to System User so role takes effect.""" + current_type = frappe.db.get_value("User", user, "user_type") + if current_type and current_type != "System User": + frappe.db.set_value("User", user, "user_type", "System User") + + +def _assign_role(user, role): + """Assign the specified role if not already assigned. Uses direct DB for reliability.""" + if not role or not frappe.db.exists("Role", role): + return + + # Desk roles (Branch User / Stock User / Damage User / Stock Manager) require + # System User user_type; Website Users silently lose role assignments. + _ensure_system_user(user) + + # Check if role already assigned + if frappe.db.exists("Has Role", {"parent": user, "role": role}): + return + + # Direct insert — more reliable than user_doc.add_roles() which can fail + # during Branch Configuration save context + frappe.get_doc({ + "doctype": "Has Role", + "parent": user, + "parenttype": "User", + "parentfield": "roles", + "role": role, + }).insert(ignore_permissions=True) + + +def _set_module_profile(user, profile_name): + """Set the Module Profile on a user if not already set.""" + if not frappe.db.exists("Module Profile", profile_name): + return + current = frappe.db.get_value("User", user, "module_profile") + if current != profile_name: + frappe.db.set_value("User", user, "module_profile", profile_name) + + +def _maybe_remove_role(user, role, exclude_branch=None): + """Remove role if user is not in any other Branch Configuration with the same role.""" + if not role: + return + + other_configs = frappe.get_all( + "Branch Configuration User", + filters={"user": user}, + fields=["parent", "role"], + ) + + # Check if user has this same role in any other Branch Configuration + has_role_elsewhere = any( + c.parent != exclude_branch and (c.get("role") or BRANCH_USER_ROLE) == role + for c in other_configs + ) + + if not has_role_elsewhere: + user_doc = frappe.get_doc("User", user) + if role in [r.role for r in user_doc.roles]: + user_doc.remove_roles(role) diff --git a/sf_trading/sf_trading/doctype/branch_configuration/test_branch_configuration.py b/sf_trading/sf_trading/doctype/branch_configuration/test_branch_configuration.py new file mode 100644 index 0000000..39c1427 --- /dev/null +++ b/sf_trading/sf_trading/doctype/branch_configuration/test_branch_configuration.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, enfono and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestBranchConfiguration(FrappeTestCase): + pass diff --git a/sf_trading/sf_trading/doctype/branch_configuration_cost_center/__init__.py b/sf_trading/sf_trading/doctype/branch_configuration_cost_center/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sf_trading/sf_trading/doctype/branch_configuration_cost_center/branch_configuration_cost_center.js b/sf_trading/sf_trading/doctype/branch_configuration_cost_center/branch_configuration_cost_center.js new file mode 100644 index 0000000..abffdac --- /dev/null +++ b/sf_trading/sf_trading/doctype/branch_configuration_cost_center/branch_configuration_cost_center.js @@ -0,0 +1,8 @@ +// Copyright (c) 2026, enfono and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Branch Configuration Cost Center", { +// refresh(frm) { + +// }, +// }); diff --git a/sf_trading/sf_trading/doctype/branch_configuration_cost_center/branch_configuration_cost_center.json b/sf_trading/sf_trading/doctype/branch_configuration_cost_center/branch_configuration_cost_center.json new file mode 100644 index 0000000..50aef62 --- /dev/null +++ b/sf_trading/sf_trading/doctype/branch_configuration_cost_center/branch_configuration_cost_center.json @@ -0,0 +1,34 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-05-04 18:31:01.031387", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "cost_center" + ], + "fields": [ + { + "fieldname": "cost_center", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Cost Center", + "options": "Cost Center" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2026-05-04 18:41:12.932631", + "modified_by": "Administrator", + "module": "Sf Trading", + "name": "Branch Configuration Cost Center", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/sf_trading/sf_trading/doctype/branch_configuration_cost_center/branch_configuration_cost_center.py b/sf_trading/sf_trading/doctype/branch_configuration_cost_center/branch_configuration_cost_center.py new file mode 100644 index 0000000..e1f24cc --- /dev/null +++ b/sf_trading/sf_trading/doctype/branch_configuration_cost_center/branch_configuration_cost_center.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, enfono and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class BranchConfigurationCostCenter(Document): + pass diff --git a/sf_trading/sf_trading/doctype/branch_configuration_cost_center/test_branch_configuration_cost_center.py b/sf_trading/sf_trading/doctype/branch_configuration_cost_center/test_branch_configuration_cost_center.py new file mode 100644 index 0000000..41f1e69 --- /dev/null +++ b/sf_trading/sf_trading/doctype/branch_configuration_cost_center/test_branch_configuration_cost_center.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, enfono and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestBranchConfigurationCostCenter(FrappeTestCase): + pass diff --git a/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/__init__.py b/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/branch_configuration_mode_of_payment.js b/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/branch_configuration_mode_of_payment.js new file mode 100644 index 0000000..442a179 --- /dev/null +++ b/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/branch_configuration_mode_of_payment.js @@ -0,0 +1,8 @@ +// Copyright (c) 2026, enfono and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Branch Configuration Mode of Payment", { +// refresh(frm) { + +// }, +// }); diff --git a/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/branch_configuration_mode_of_payment.json b/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/branch_configuration_mode_of_payment.json new file mode 100644 index 0000000..f85d738 --- /dev/null +++ b/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/branch_configuration_mode_of_payment.json @@ -0,0 +1,55 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-05-04 18:24:35.941149", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "mode_of_payment", + "type" + ], + "fields": [ + { + "fieldname": "mode_of_payment", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Mode of Payment", + "options": "Mode of Payment", + "reqd": 1 + }, + { + "fetch_from": "mode_of_payment.type", + "fetch_if_empty": 1, + "fieldname": "type", + "fieldtype": "Data", + "label": "Type" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-05-04 18:27:04.017025", + "modified_by": "Administrator", + "module": "Sf Trading", + "name": "Branch Configuration Mode of Payment", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/branch_configuration_mode_of_payment.py b/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/branch_configuration_mode_of_payment.py new file mode 100644 index 0000000..29d8807 --- /dev/null +++ b/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/branch_configuration_mode_of_payment.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, enfono and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class BranchConfigurationModeofPayment(Document): + pass diff --git a/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/test_branch_configuration_mode_of_payment.py b/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/test_branch_configuration_mode_of_payment.py new file mode 100644 index 0000000..4780c28 --- /dev/null +++ b/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/test_branch_configuration_mode_of_payment.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, enfono and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestBranchConfigurationModeofPayment(FrappeTestCase): + pass diff --git a/sf_trading/sf_trading/doctype/branch_configuration_user/__init__.py b/sf_trading/sf_trading/doctype/branch_configuration_user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sf_trading/sf_trading/doctype/branch_configuration_user/branch_configuration_user.js b/sf_trading/sf_trading/doctype/branch_configuration_user/branch_configuration_user.js new file mode 100644 index 0000000..a98d5a1 --- /dev/null +++ b/sf_trading/sf_trading/doctype/branch_configuration_user/branch_configuration_user.js @@ -0,0 +1,8 @@ +// Copyright (c) 2026, enfono and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Branch Configuration User", { +// refresh(frm) { + +// }, +// }); diff --git a/sf_trading/sf_trading/doctype/branch_configuration_user/branch_configuration_user.json b/sf_trading/sf_trading/doctype/branch_configuration_user/branch_configuration_user.json new file mode 100644 index 0000000..8d264c3 --- /dev/null +++ b/sf_trading/sf_trading/doctype/branch_configuration_user/branch_configuration_user.json @@ -0,0 +1,43 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-05-04 18:27:33.473806", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "user", + "role" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "label": "User", + "options": "User", + "reqd": 1 + }, + { + "fieldname": "role", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Role", + "options": "Branch User\nStock User\nStock Manager\nDamage User" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2026-05-04 18:40:57.963773", + "modified_by": "Administrator", + "module": "Sf Trading", + "name": "Branch Configuration User", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/sf_trading/sf_trading/doctype/branch_configuration_user/branch_configuration_user.py b/sf_trading/sf_trading/doctype/branch_configuration_user/branch_configuration_user.py new file mode 100644 index 0000000..0172efe --- /dev/null +++ b/sf_trading/sf_trading/doctype/branch_configuration_user/branch_configuration_user.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, enfono and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class BranchConfigurationUser(Document): + pass diff --git a/sf_trading/sf_trading/doctype/branch_configuration_user/test_branch_configuration_user.py b/sf_trading/sf_trading/doctype/branch_configuration_user/test_branch_configuration_user.py new file mode 100644 index 0000000..396838d --- /dev/null +++ b/sf_trading/sf_trading/doctype/branch_configuration_user/test_branch_configuration_user.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, enfono and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestBranchConfigurationUser(FrappeTestCase): + pass diff --git a/sf_trading/sf_trading/doctype/branch_configuration_warehouse/__init__.py b/sf_trading/sf_trading/doctype/branch_configuration_warehouse/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sf_trading/sf_trading/doctype/branch_configuration_warehouse/branch_configuration_warehouse.js b/sf_trading/sf_trading/doctype/branch_configuration_warehouse/branch_configuration_warehouse.js new file mode 100644 index 0000000..d292758 --- /dev/null +++ b/sf_trading/sf_trading/doctype/branch_configuration_warehouse/branch_configuration_warehouse.js @@ -0,0 +1,8 @@ +// Copyright (c) 2026, enfono and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Branch Configuration Warehouse", { +// refresh(frm) { + +// }, +// }); diff --git a/sf_trading/sf_trading/doctype/branch_configuration_warehouse/branch_configuration_warehouse.json b/sf_trading/sf_trading/doctype/branch_configuration_warehouse/branch_configuration_warehouse.json new file mode 100644 index 0000000..e15b5f5 --- /dev/null +++ b/sf_trading/sf_trading/doctype/branch_configuration_warehouse/branch_configuration_warehouse.json @@ -0,0 +1,34 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-05-04 18:29:57.062305", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "warehouse" + ], + "fields": [ + { + "fieldname": "warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Warehouse", + "options": "Warehouse" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2026-05-04 18:40:44.055040", + "modified_by": "Administrator", + "module": "Sf Trading", + "name": "Branch Configuration Warehouse", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/sf_trading/sf_trading/doctype/branch_configuration_warehouse/branch_configuration_warehouse.py b/sf_trading/sf_trading/doctype/branch_configuration_warehouse/branch_configuration_warehouse.py new file mode 100644 index 0000000..2caaf64 --- /dev/null +++ b/sf_trading/sf_trading/doctype/branch_configuration_warehouse/branch_configuration_warehouse.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, enfono and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class BranchConfigurationWarehouse(Document): + pass diff --git a/sf_trading/sf_trading/doctype/branch_configuration_warehouse/test_branch_configuration_warehouse.py b/sf_trading/sf_trading/doctype/branch_configuration_warehouse/test_branch_configuration_warehouse.py new file mode 100644 index 0000000..a5bd8a6 --- /dev/null +++ b/sf_trading/sf_trading/doctype/branch_configuration_warehouse/test_branch_configuration_warehouse.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, enfono and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestBranchConfigurationWarehouse(FrappeTestCase): + pass