From b1f4c3b1297f3adf6bd747e6a3217232c684c5c7 Mon Sep 17 00:00:00 2001 From: Neha Fathima Date: Mon, 1 Jun 2026 14:33:25 +0530 Subject: [PATCH 1/2] feat:aqrar implmentation --- aqrar_ext/api/commission.py | 97 +++ aqrar_ext/api/day_close.py | 126 ++++ aqrar_ext/api/item_naming.py | 8 + aqrar_ext/api/navigation.py | 65 ++ aqrar_ext/api/sales_invoice_payment.py | 2 +- .../aqrar_settings/aqrar_settings.json | 48 ++ .../doctype/aqrar_settings/aqrar_settings.py | 6 + .../branch_configuration.py | 13 +- .../branch_configuration_user.json | 11 +- .../doctype/custom_quote/custom_quote.js | 93 ++- .../doctype/custom_quote/custom_quote.json | 12 +- .../doctype/custom_quote/custom_quote.py | 47 +- .../custom_quote_item/custom_quote_item.json | 13 +- .../aqrar_ext/overrides/sales_invoice.py | 109 ++++ .../page/price_list_bulk_editor/__init__.py | 1 + .../price_list_bulk_editor.css | 131 ++++ .../price_list_bulk_editor.js | 263 ++++++++ .../price_list_bulk_editor.json | 13 + .../price_list_bulk_editor.py | 144 +++++ .../sales_invoice_aqrar.html | 149 +++++ .../sales_invoice_aqrar.json | 27 + aqrar_ext/aqrar_ext/utils/__init__.py | 0 aqrar_ext/aqrar_ext/utils/print_helpers.py | 36 ++ aqrar_ext/aqrar_ext/workflow/__init__.py | 1 + aqrar_ext/aqrar_ext/workflow/conditions.py | 217 +++++++ .../aqrar_ext/workflow/email_approval.py | 81 +++ .../aqrar_ext/workflow/test_workflows.py | 368 ++++++++++++ aqrar_ext/fixtures/custom_docperm.json | 52 ++ aqrar_ext/fixtures/custom_field.json | 565 +++++++++++++++++- aqrar_ext/fixtures/notification.json | 110 ++++ aqrar_ext/fixtures/workflow.json | 341 ++++++++++- .../fixtures/workflow_action_master.json | 20 + aqrar_ext/fixtures/workflow_state.json | 50 ++ aqrar_ext/hooks.py | 81 ++- aqrar_ext/public/js/auto_print_preview.js | 151 +++++ aqrar_ext/public/js/item_naming_from_group.js | 31 + .../public/js/journal_entry_commission.js | 5 + aqrar_ext/public/js/notification_sound.js | 8 + .../js/sales_invoice_book_commission.js | 61 ++ .../js/sales_invoice_branch_price_list.js | 128 ++++ .../public/js/sales_invoice_item_display.js | 40 ++ .../public/js/sales_invoice_navigation.js | 66 ++ .../public/js/sales_invoice_payment_terms.js | 10 + aqrar_ext/public/js/sales_invoice_return.js | 32 + aqrar_ext/setup_data.py | 120 ++++ 45 files changed, 3920 insertions(+), 32 deletions(-) create mode 100644 aqrar_ext/api/commission.py create mode 100644 aqrar_ext/api/day_close.py create mode 100644 aqrar_ext/api/item_naming.py create mode 100644 aqrar_ext/api/navigation.py create mode 100644 aqrar_ext/aqrar_ext/doctype/aqrar_settings/aqrar_settings.json create mode 100644 aqrar_ext/aqrar_ext/doctype/aqrar_settings/aqrar_settings.py create mode 100644 aqrar_ext/aqrar_ext/overrides/sales_invoice.py create mode 100644 aqrar_ext/aqrar_ext/page/price_list_bulk_editor/__init__.py create mode 100644 aqrar_ext/aqrar_ext/page/price_list_bulk_editor/price_list_bulk_editor.css create mode 100644 aqrar_ext/aqrar_ext/page/price_list_bulk_editor/price_list_bulk_editor.js create mode 100644 aqrar_ext/aqrar_ext/page/price_list_bulk_editor/price_list_bulk_editor.json create mode 100644 aqrar_ext/aqrar_ext/page/price_list_bulk_editor/price_list_bulk_editor.py create mode 100644 aqrar_ext/aqrar_ext/print_format/sales_invoice_aqrar/sales_invoice_aqrar.html create mode 100644 aqrar_ext/aqrar_ext/print_format/sales_invoice_aqrar/sales_invoice_aqrar.json create mode 100644 aqrar_ext/aqrar_ext/utils/__init__.py create mode 100644 aqrar_ext/aqrar_ext/utils/print_helpers.py create mode 100644 aqrar_ext/aqrar_ext/workflow/__init__.py create mode 100644 aqrar_ext/aqrar_ext/workflow/conditions.py create mode 100644 aqrar_ext/aqrar_ext/workflow/email_approval.py create mode 100644 aqrar_ext/aqrar_ext/workflow/test_workflows.py create mode 100644 aqrar_ext/fixtures/custom_docperm.json create mode 100644 aqrar_ext/fixtures/notification.json create mode 100644 aqrar_ext/fixtures/workflow_action_master.json create mode 100644 aqrar_ext/fixtures/workflow_state.json create mode 100644 aqrar_ext/public/js/auto_print_preview.js create mode 100644 aqrar_ext/public/js/item_naming_from_group.js create mode 100644 aqrar_ext/public/js/journal_entry_commission.js create mode 100644 aqrar_ext/public/js/notification_sound.js create mode 100644 aqrar_ext/public/js/sales_invoice_book_commission.js create mode 100644 aqrar_ext/public/js/sales_invoice_branch_price_list.js create mode 100644 aqrar_ext/public/js/sales_invoice_item_display.js create mode 100644 aqrar_ext/public/js/sales_invoice_navigation.js create mode 100644 aqrar_ext/public/js/sales_invoice_payment_terms.js create mode 100644 aqrar_ext/public/js/sales_invoice_return.js create mode 100644 aqrar_ext/setup_data.py diff --git a/aqrar_ext/api/commission.py b/aqrar_ext/api/commission.py new file mode 100644 index 0000000..cd76b89 --- /dev/null +++ b/aqrar_ext/api/commission.py @@ -0,0 +1,97 @@ +import frappe +from frappe import _ + + +@frappe.whitelist() +def get_commission_je_status(sales_invoice): + """Return whether a commission Journal Entry already exists for this invoice.""" + je_name = frappe.db.exists( + "Journal Entry", + {"custom_reference_invoice": sales_invoice, "docstatus": ["!=", 2]}, + ) + if je_name: + je = frappe.db.get_value("Journal Entry", je_name, ["name", "docstatus"], as_dict=True) + return {"exists": True, "je_name": je.name, "je_status": "Submitted" if je.docstatus == 1 else "Draft"} + return {"exists": False} + + +@frappe.whitelist() +def create_commission_je(sales_invoice): + """Create and return a draft Journal Entry pre-filled with commission data.""" + si = frappe.get_doc("Sales Invoice", sales_invoice) + + if si.docstatus != 1: + frappe.throw(_("Can only book commission for submitted invoices.")) + + existing = frappe.db.exists( + "Journal Entry", + {"custom_reference_invoice": sales_invoice, "docstatus": ["!=", 2]}, + ) + if existing: + frappe.throw( + _("A Journal Entry for this invoice already exists: {0}").format( + f'{existing}' + ) + ) + + expense_account = _get_commission_expense_account(si.company) + payable_account = _get_commission_payable_account(si.company) + amount = si.total_commission or 0 + + je = frappe.get_doc({ + "doctype": "Journal Entry", + "company": si.company, + "posting_date": si.posting_date, + "custom_reference_invoice": si.name, + "user_remark": f"Commission for {si.name} — {si.customer}", + "accounts": [ + { + "account": expense_account, + "debit_in_account_currency": amount, + "credit_in_account_currency": 0, + "cost_center": si.cost_center, + }, + { + "account": payable_account, + "debit_in_account_currency": 0, + "credit_in_account_currency": amount, + "cost_center": si.cost_center, + }, + ], + }) + je.flags.ignore_validate = True + je.insert(ignore_permissions=True) + + return je.name + + +def _get_commission_expense_account(company): + acct = frappe.db.get_value("Company", company, "default_commission_expense_account") + if acct: + return acct + acct = frappe.db.get_value( + "Account", + {"company": company, "account_name": ("like", "%Commission%Expense%"), "is_group": 0}, + "name", + ) + if acct: + return acct + frappe.throw( + _("No Commission Expense account found. Please set it in Company settings.") + ) + + +def _get_commission_payable_account(company): + acct = frappe.db.get_value("Company", company, "default_commission_payable_account") + if acct: + return acct + acct = frappe.db.get_value( + "Account", + {"company": company, "account_name": ("like", "%Commission%Payable%"), "is_group": 0}, + "name", + ) + if acct: + return acct + frappe.throw( + _("No Commission Payable account found. Please set it in Company settings.") + ) diff --git a/aqrar_ext/api/day_close.py b/aqrar_ext/api/day_close.py new file mode 100644 index 0000000..1ed6867 --- /dev/null +++ b/aqrar_ext/api/day_close.py @@ -0,0 +1,126 @@ +import frappe +from frappe import _ +from frappe.utils import today + + +@frappe.whitelist() +def run_day_close(date=None, company=None): + """Aggregate daily commissions and discounts into summary Journal Entries. + + Creates one commission JE (Dr expense, Cr payable) and one discount JE + (Dr expense, Cr payable) for all submitted Sales Invoices on the given date + that have not already been booked individually. + """ + date = date or today() + company = company or frappe.defaults.get_user_default("company") + if not company: + frappe.throw(_("No company specified and no default company found.")) + + comm_remark = f"Day Close Commission for {date}" + disc_remark = f"Day Close Discount for {date}" + + if frappe.db.exists("Journal Entry", {"user_remark": comm_remark, "docstatus": ["!=", 2]}): + frappe.throw(_("Day-close commission Journal Entry already exists for {0}").format(date)) + if frappe.db.exists("Journal Entry", {"user_remark": disc_remark, "docstatus": ["!=", 2]}): + frappe.throw(_("Day-close discount Journal Entry already exists for {0}").format(date)) + + sis = frappe.db.get_all( + "Sales Invoice", + filters={"posting_date": date, "company": company, "docstatus": 1}, + fields=["name", "total_commission", "discount_amount", "cost_center"], + ) + + if not sis: + frappe.throw(_("No submitted Sales Invoices found for {0}").format(date)) + + eligible = [] + skipped = 0 + for si in sis: + if frappe.db.exists("Journal Entry", {"custom_reference_invoice": si.name, "docstatus": ["!=", 2]}): + skipped += 1 + else: + eligible.append(si) + + if not eligible: + frappe.throw( + _("All Sales Invoices for {0} have already been booked individually. Nothing to reconcile.").format(date) + ) + + total_commission = sum(si.total_commission or 0 for si in eligible) + total_discount = sum(si.discount_amount or 0 for si in eligible) + cost_center = eligible[0].cost_center + + comm_expense_acct = _get_company_account(company, "default_commission_expense_account") + comm_payable_acct = _get_company_account(company, "default_commission_payable_account") + disc_expense_acct = _get_company_account(company, "default_discount_expense_account") + disc_payable_acct = _get_company_account(company, "default_discount_payable_account") + + result = {} + + if total_commission > 0: + je = frappe.get_doc({ + "doctype": "Journal Entry", + "company": company, + "posting_date": date, + "user_remark": comm_remark, + "accounts": [ + { + "account": comm_expense_acct, + "debit_in_account_currency": total_commission, + "credit_in_account_currency": 0, + "cost_center": cost_center, + }, + { + "account": comm_payable_acct, + "debit_in_account_currency": 0, + "credit_in_account_currency": total_commission, + "cost_center": cost_center, + }, + ], + }) + je.insert(ignore_permissions=True) + result["commission_je"] = je.name + + if total_discount > 0: + je = frappe.get_doc({ + "doctype": "Journal Entry", + "company": company, + "posting_date": date, + "user_remark": disc_remark, + "accounts": [ + { + "account": disc_expense_acct, + "debit_in_account_currency": total_discount, + "credit_in_account_currency": 0, + "cost_center": cost_center, + }, + { + "account": disc_payable_acct, + "debit_in_account_currency": 0, + "credit_in_account_currency": total_discount, + "cost_center": cost_center, + }, + ], + }) + je.insert(ignore_permissions=True) + result["discount_je"] = je.name + + result.update({ + "total_commission": total_commission, + "total_discount": total_discount, + "invoices_processed": len(eligible), + "invoices_skipped": skipped, + }) + + return result + + +def _get_company_account(company, fieldname): + acct = frappe.db.get_value("Company", company, fieldname) + if acct: + return acct + frappe.throw( + _("Please set {0} in Company {1} before running day-close.").format( + frappe.get_meta("Company").get_field(fieldname).label, company + ) + ) diff --git a/aqrar_ext/api/item_naming.py b/aqrar_ext/api/item_naming.py new file mode 100644 index 0000000..d1b59b0 --- /dev/null +++ b/aqrar_ext/api/item_naming.py @@ -0,0 +1,8 @@ +import frappe +from frappe.model.naming import make_autoname + + +@frappe.whitelist() +def get_next_item_code(naming_series): + """Return the next item_code for the given naming series.""" + return make_autoname(naming_series, "Item") diff --git a/aqrar_ext/api/navigation.py b/aqrar_ext/api/navigation.py new file mode 100644 index 0000000..a0dfd33 --- /dev/null +++ b/aqrar_ext/api/navigation.py @@ -0,0 +1,65 @@ +import frappe +import json + + +@frappe.whitelist() +def get_sibling(doctype, docname, direction, list_filters=None, order_by=None): + """Return the next/previous document name respecting list filters and sort.""" + if isinstance(list_filters, str): + list_filters = json.loads(list_filters) + + filters = _build_filters(doctype, docname, direction, list_filters) + sort = _resolve_order(doctype, direction, order_by) + + docs = frappe.get_all( + doctype, + filters=filters, + pluck="name", + order_by=sort, + limit=1, + ) + + return docs[0] if docs else None + + +def _build_filters(doctype, docname, direction, list_filters): + filters = {} + + # Apply list view filters + if list_filters: + for f in list_filters: + if isinstance(f, list) and len(f) == 3: + field, op, val = f + if op == "=": + filters[field] = val + elif op in ("like", ">" , "<", ">=", "<="): + filters[field] = [op, val] + elif op == "in": + filters[field] = val if isinstance(val, list) else [val] + + # Cursor: get sibling after/before current doc + if direction == "next": + filters["name"] = [">", docname] + else: + filters["name"] = ["<", docname] + + return filters + + +def _resolve_order(doctype, direction, order_by): + # Use list's sort field if provided, else fall back to name + meta = frappe.get_meta(doctype) + field = "name" + + if order_by: + field = order_by + else: + sort_field = meta.sort_field or "modified" + sort_order = meta.sort_order or "desc" + field = f"{sort_field} {sort_order}, name" + + if direction == "next": + return f"{field} asc" + else: + # Reverse the sort for prev + return f"{field} desc" diff --git a/aqrar_ext/api/sales_invoice_payment.py b/aqrar_ext/api/sales_invoice_payment.py index f30ea16..5ab79e8 100644 --- a/aqrar_ext/api/sales_invoice_payment.py +++ b/aqrar_ext/api/sales_invoice_payment.py @@ -143,7 +143,7 @@ def create_pos_payments_for_invoice(sales_invoice: str, payments: str | list): pe.flags.ignore_validate = True if hasattr(pe, "workflow_state"): - pe.workflow_state = "Pending" + pe.workflow_state = "Pending Approval" pe.submit() created.append(pe.name) diff --git a/aqrar_ext/aqrar_ext/doctype/aqrar_settings/aqrar_settings.json b/aqrar_ext/aqrar_ext/doctype/aqrar_settings/aqrar_settings.json new file mode 100644 index 0000000..9c3d299 --- /dev/null +++ b/aqrar_ext/aqrar_ext/doctype/aqrar_settings/aqrar_settings.json @@ -0,0 +1,48 @@ +{ + "actions": [], + "allow_rename": 0, + "creation": "2026-05-16 00:00:00.000000", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "item_display_mode" + ], + "fields": [ + { + "default": "Item Name + Description", + "fieldname": "item_display_mode", + "fieldtype": "Select", + "label": "Default Item Display Mode", + "options": "Item Name\nItem Code\nItem Name + Description\nItem Code + Description", + "reqd": 1 + } + ], + "issingle": 1, + "links": [], + "modified": "2026-05-16 00:00:00.000000", + "modified_by": "Administrator", + "module": "Aqrar Ext", + "name": "Aqrar Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 0, + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 0 +} diff --git a/aqrar_ext/aqrar_ext/doctype/aqrar_settings/aqrar_settings.py b/aqrar_ext/aqrar_ext/doctype/aqrar_settings/aqrar_settings.py new file mode 100644 index 0000000..d66ffed --- /dev/null +++ b/aqrar_ext/aqrar_ext/doctype/aqrar_settings/aqrar_settings.py @@ -0,0 +1,6 @@ +import frappe +from frappe.model.document import Document + + +class AqrarSettings(Document): + pass diff --git a/aqrar_ext/aqrar_ext/doctype/branch_configuration/branch_configuration.py b/aqrar_ext/aqrar_ext/doctype/branch_configuration/branch_configuration.py index 1c80690..810060d 100644 --- a/aqrar_ext/aqrar_ext/doctype/branch_configuration/branch_configuration.py +++ b/aqrar_ext/aqrar_ext/doctype/branch_configuration/branch_configuration.py @@ -7,6 +7,7 @@ from frappe.model.document import Document BRANCH_USER_ROLE = "Branch User" +APPROVER_ROLES = {"Branch Approver", "Branch Accountant", "Branch User", "Damage User"} class BranchConfiguration(Document): @@ -103,10 +104,14 @@ def create_permissions(self): _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") + _module_profile_map = { + "Branch User": "Branch User", + "Damage User": "Damage User", + "Branch Approver": "Branch User", + "Branch Accountant": "Branch User", + } + profile = _module_profile_map.get(selected_role, "Branch User") + _set_module_profile(u.user, profile) def create_permission(user, allow, value, is_default=0): diff --git a/aqrar_ext/aqrar_ext/doctype/branch_configuration_user/branch_configuration_user.json b/aqrar_ext/aqrar_ext/doctype/branch_configuration_user/branch_configuration_user.json index 6407f69..f604e07 100644 --- a/aqrar_ext/aqrar_ext/doctype/branch_configuration_user/branch_configuration_user.json +++ b/aqrar_ext/aqrar_ext/doctype/branch_configuration_user/branch_configuration_user.json @@ -6,7 +6,8 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "user" + "user", + "role" ], "fields": [ { @@ -15,6 +16,14 @@ "in_list_view": 1, "label": "User", "options": "User" + }, + { + "fieldname": "role", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Role", + "options": "Branch User\nBranch Approver\nBranch Accountant\nDamage User", + "default": "Branch User" } ], "grid_page_length": 50, diff --git a/aqrar_ext/aqrar_ext/doctype/custom_quote/custom_quote.js b/aqrar_ext/aqrar_ext/doctype/custom_quote/custom_quote.js index f8821fc..7a25770 100644 --- a/aqrar_ext/aqrar_ext/doctype/custom_quote/custom_quote.js +++ b/aqrar_ext/aqrar_ext/doctype/custom_quote/custom_quote.js @@ -1,35 +1,60 @@ // Copyright (c) 2026, Enfono and contributors // For license information, please see license.txt - -//Set valid till date to 30 days from posting date - frappe.ui.form.on("Custom Quote", { onload(frm) { if (frm.is_new() && !frm.doc.posting_date) { - frm.set_value( - "posting_date", - frappe.datetime.now_datetime() - ); + frm.set_value("posting_date", frappe.datetime.now_datetime()); } if (frm.is_new() && !frm.doc.valid_till) { set_valid_till(frm); } + if (frm.is_new()) { + auto_set_price_list(frm); + } }, posting_date(frm) { set_valid_till(frm); }, + customer(frm) { + auto_set_price_list(frm); + }, + refresh(frm) { calculate_net_total(frm); } }); -//Set total vat tax include vat +// ── Auto-set selling price list from Customer default ───────────────── +function auto_set_price_list(frm) { + if (!frm.doc.customer) return; + + frappe.db.get_value("Customer", frm.doc.customer, "default_price_list", function (r) { + var cust_pl = r && r.default_price_list ? r.default_price_list : null; + if (cust_pl) { + frappe.db.get_value("Price List", cust_pl, "enabled", function (pl) { + if (pl && pl.enabled) { + frm.set_value("selling_price_list", cust_pl); + } else { + frm.set_value("selling_price_list", "Standard Selling"); + } + }); + } else { + frm.set_value("selling_price_list", "Standard Selling"); + } + }); +} + +// ── Custom Quote Item handlers ──────────────────────────────────────── frappe.ui.form.on("Custom Quote Item", { + item_code(frm, cdt, cdn) { + fetch_item_details(frm, cdt, cdn); + }, + qty(frm, cdt, cdn) { calculate_row(frm, cdt, cdn); }, @@ -47,18 +72,56 @@ frappe.ui.form.on("Custom Quote Item", { } }); +// ── Fetch item description, UOM, and rate from Item Price ───────────── +function fetch_item_details(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (!row.item_code) return; + + frappe.db.get_value("Item", row.item_code, ["item_name", "stock_uom"], function (r) { + if (r) { + if (!row.item) frappe.model.set_value(cdt, cdn, "item", r.item_name); + if (!row.unit) frappe.model.set_value(cdt, cdn, "unit", r.stock_uom); + } + }); + + // Fetch rate from Item Price for the current selling_price_list + var price_list = frm.doc.selling_price_list; + if (!price_list) return; + + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "Item Price", + filters: { + item_code: row.item_code, + price_list: price_list, + selling: 1, + }, + fields: ["price_list_rate", "uom"], + limit: 1, + }, + callback: function (res) { + if (res.message && res.message.length) { + var ip = res.message[0]; + if (row.rate === undefined || row.rate === 0) { + frappe.model.set_value(cdt, cdn, "rate", ip.price_list_rate); + } + if (!row.unit && ip.uom) { + frappe.model.set_value(cdt, cdn, "unit", ip.uom); + } + } + }, + }); +} + +// ── Row calculations ────────────────────────────────────────────────── function set_valid_till(frm) { if (!frm.doc.posting_date) return; - - frm.set_value( - "valid_till", - frappe.datetime.add_days(frm.doc.posting_date, 30) - ); + frm.set_value("valid_till", frappe.datetime.add_days(frm.doc.posting_date, 30)); } function calculate_row(frm, cdt, cdn) { let row = locals[cdt][cdn]; - let qty = row.qty || 0; let rate = row.rate || 0; let tax_rate = row.tax_rate || 0; @@ -68,11 +131,9 @@ function calculate_row(frm, cdt, cdn) { row.total_incl_vat = row.total + row.vat; frm.refresh_field("items"); - calculate_net_total(frm); } -//Set net total vat total grand total function calculate_net_total(frm) { let net_total = 0; let vat_total = 0; diff --git a/aqrar_ext/aqrar_ext/doctype/custom_quote/custom_quote.json b/aqrar_ext/aqrar_ext/doctype/custom_quote/custom_quote.json index 3adca7b..9ecde4c 100644 --- a/aqrar_ext/aqrar_ext/doctype/custom_quote/custom_quote.json +++ b/aqrar_ext/aqrar_ext/doctype/custom_quote/custom_quote.json @@ -19,6 +19,7 @@ "attention", "mob_no", "rqf_no", + "selling_price_list", "section_break_dwhc", "items", "section_break_iajw", @@ -154,12 +155,19 @@ "fieldname": "terms_and_conditions", "fieldtype": "Text Editor", "label": "Terms and Conditions" + }, + { + "fieldname": "selling_price_list", + "fieldtype": "Link", + "label": "Selling Price List", + "options": "Price List", + "insert_after": "rqf_no" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2026-02-05 10:42:18.239467", + "modified": "2026-05-18 00:00:00.000000", "modified_by": "Administrator", "module": "Aqrar Ext", "name": "Custom Quote", @@ -184,4 +192,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/aqrar_ext/aqrar_ext/doctype/custom_quote/custom_quote.py b/aqrar_ext/aqrar_ext/doctype/custom_quote/custom_quote.py index 0ff74c4..78388c5 100644 --- a/aqrar_ext/aqrar_ext/doctype/custom_quote/custom_quote.py +++ b/aqrar_ext/aqrar_ext/doctype/custom_quote/custom_quote.py @@ -1,9 +1,54 @@ # Copyright (c) 2026, Enfono and contributors # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document +from frappe.utils import flt +from frappe import _, bold class CustomQuote(Document): pass + + +def validate(doc, method=None): + """Block Custom Quote submit if any item rate is below its minimum.""" + price_list = doc.get("selling_price_list") + if not price_list: + return + + below_min = [] + for item in doc.items: + if not item.item_code: + continue + + ip = frappe.db.get_value( + "Item Price", + { + "item_code": item.item_code, + "price_list": price_list, + "selling": 1, + }, + ["custom_minimum_selling_rate"], + as_dict=True, + ) + if not ip: + continue + + net = flt(item.rate) + if ip.custom_minimum_selling_rate and net < flt(ip.custom_minimum_selling_rate): + below_min.append({"idx": item.idx, "item_name": item.item_code, "net_rate": net, "limit": ip.custom_minimum_selling_rate}) + + if not below_min: + return + + msg = [] + msg.append(_("The following items are below the minimum selling rate:")) + msg.append("") + for v in below_min: + msg.append(_("Row #{idx}: {item_name} — Rate {net_rate} below Minimum {limit}").format( + idx=v["idx"], item_name=bold(v["item_name"]), + net_rate=bold(frappe.format_value(v["net_rate"], "Currency")), + limit=bold(frappe.format_value(v["limit"], "Currency")), + )) + frappe.throw("
".join(msg), title=_("Selling Rate Band Violation")) diff --git a/aqrar_ext/aqrar_ext/doctype/custom_quote_item/custom_quote_item.json b/aqrar_ext/aqrar_ext/doctype/custom_quote_item/custom_quote_item.json index 252c8c5..19ba269 100644 --- a/aqrar_ext/aqrar_ext/doctype/custom_quote_item/custom_quote_item.json +++ b/aqrar_ext/aqrar_ext/doctype/custom_quote_item/custom_quote_item.json @@ -6,6 +6,7 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "item_code", "item", "unit", "qty", @@ -65,13 +66,21 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Total Incl. VAT" + }, + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "insert_after": null } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-02-04 13:00:17.426085", + "modified": "2026-05-18 00:00:00.000000", "modified_by": "Administrator", "module": "Aqrar Ext", "name": "Custom Quote Item", @@ -82,4 +91,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/aqrar_ext/aqrar_ext/overrides/sales_invoice.py b/aqrar_ext/aqrar_ext/overrides/sales_invoice.py new file mode 100644 index 0000000..545f52b --- /dev/null +++ b/aqrar_ext/aqrar_ext/overrides/sales_invoice.py @@ -0,0 +1,109 @@ +import frappe +from frappe import _, bold +from frappe.utils import flt + + +def before_save(doc, event=None): + """Propagate Customer default payment_terms to Sales Invoice. + + When no template is explicitly set and the customer has a default payment + terms template, auto-populate it and regenerate the installment schedule.""" + if doc.get("ignore_default_payment_terms_template"): + return + if doc.get("payment_terms_template") or not doc.get("customer"): + return + + customer_terms = frappe.db.get_value("Customer", doc.customer, "payment_terms") + if not customer_terms: + return + + doc.payment_terms_template = customer_terms + + from erpnext.controllers.accounts_controller import get_payment_terms + + grand_total = doc.get("rounded_total") or doc.grand_total + base_grand_total = doc.get("base_rounded_total") or doc.base_grand_total + data = get_payment_terms(customer_terms, doc.posting_date, grand_total, base_grand_total) + if data: + doc.payment_schedule = [] + for item in data: + doc.append("payment_schedule", item) + + +def before_print(doc, event=None, print_settings=None): + """Apply item_display_mode before print — modifies items in-memory. + + Attaches _item_display_mode to doc so templates can read it without + needing any Jinja sandbox ORM calls.""" + mode = frappe.db.get_value( + "Aqrar Settings", "Aqrar Settings", "item_display_mode" + ) or "Item Name + Description" + + doc._item_display_mode = mode + + for item in doc.items: + if not item.item_code: + continue + + if mode in ("Item Code", "Item Name"): + item.description = "" + elif mode == "Item Code + Description": + if item.description == item.item_code: + item.description = "" + else: # Item Name + Description + if item.description == item.item_name: + item.description = "" + + +def validate(doc, method=None): + """Block Sales Invoice submit if any item rate is below its minimum.""" + if doc.get("is_return") or doc.get("custom_override_minimum_price"): + return + + price_list = doc.get("selling_price_list") + if not price_list: + return + + below_min = [] + for item in doc.items: + if not item.item_code or item.get("is_free_item"): + continue + + ip = frappe.db.get_value( + "Item Price", + { + "item_code": item.item_code, + "price_list": price_list, + "uom": item.uom, + "selling": 1, + }, + ["custom_minimum_selling_rate"], + as_dict=True, + ) + if not ip: + continue + + net = flt(item.net_rate) + if ip.custom_minimum_selling_rate and net < flt(ip.custom_minimum_selling_rate): + below_min.append({ + "idx": item.idx, + "item_name": item.item_name, + "net_rate": net, + "limit": ip.custom_minimum_selling_rate, + }) + + if not below_min: + return + + msg = [] + msg.append(_("The following items are below the minimum selling rate:")) + msg.append("") + for v in below_min: + msg.append(_("Row #{idx}: {item_name} — Net Rate {net_rate} below Minimum {limit}").format( + idx=v["idx"], + item_name=bold(v["item_name"]), + net_rate=bold(frappe.format_value(v["net_rate"], "Currency")), + limit=bold(frappe.format_value(v["limit"], "Currency")), + )) + msg += ["", _("To override, check Override Minimum Price and try again.")] + frappe.throw("
".join(msg), title=_("Selling Rate Band Violation")) diff --git a/aqrar_ext/aqrar_ext/page/price_list_bulk_editor/__init__.py b/aqrar_ext/aqrar_ext/page/price_list_bulk_editor/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/aqrar_ext/aqrar_ext/page/price_list_bulk_editor/__init__.py @@ -0,0 +1 @@ + diff --git a/aqrar_ext/aqrar_ext/page/price_list_bulk_editor/price_list_bulk_editor.css b/aqrar_ext/aqrar_ext/page/price_list_bulk_editor/price_list_bulk_editor.css new file mode 100644 index 0000000..41bd04f --- /dev/null +++ b/aqrar_ext/aqrar_ext/page/price_list_bulk_editor/price_list_bulk_editor.css @@ -0,0 +1,131 @@ +.ple-card { + background: #fff; + border: 1px solid #d1d8e0; + border-radius: 8px; + margin: 14px 14px 0 14px; + box-shadow: 0 1px 3px rgba(0, 0, 0, .05); +} +.ple-card-body { padding: 12px 14px 8px 14px; } + +.ple-filters { + display: flex; + align-items: flex-end; + gap: 12px; + flex-wrap: wrap; +} +.ple-field { min-width: 140px; width: 150px; } +.ple-actions { display: flex; align-items: flex-end; padding-bottom: 1px; } + +.ple-status { margin: 8px 14px 0 14px; } +.ple-status-bar { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + padding: 6px 10px; + background: #f0f5fb; + border: 1px solid #d4e4fc; + border-radius: 6px; + font-size: 12px; +} +.ple-badge { + display: inline-flex; + align-items: center; + gap: 3px; + padding: 2px 9px; + border-radius: 100px; + font-size: 11px; + font-weight: 500; + background: #e8f0fe; + color: #1a3a6b; +} +.ple-hint { color: #6b7280; font-style: italic; margin-left: auto; font-size: 11px; } + +.ple-table-shell { + margin: 10px 14px 14px 14px; + border: 1px solid #e2e8f0; + border-radius: 8px; + overflow: hidden; + min-height: 200px; + background: #fff; +} +.ple-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; + text-align: center; +} +.ple-empty-title { font-size: 15px; color: #374151; margin: 0 0 4px 0; } +.ple-empty-sub { font-size: 13px; color: #9ca3af; margin: 0; } + +.ple-scroll { width: 100%; overflow: auto; max-height: 65vh; } + +.ple-table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 13px; } +.ple-table thead { position: sticky; top: 0; z-index: 3; } +.ple-table th { + padding: 8px 10px; + text-align: left; + font-weight: 600; + font-size: 11px; + white-space: nowrap; +} +.ple-th-fixed { background: #f4f7fc; color: #374151; border-bottom: 2px solid #d1d5db; } +.ple-th-price { background: linear-gradient(180deg, #e8f0fe, #d4e4fc); color: #1a3a6b; border-bottom: 2px solid #2490ef; } + +.ple-col-sticky { + position: sticky; + z-index: 1; +} +.ple-table thead .ple-col-sticky { z-index: 3; } +.ple-col-sticky:nth-child(1) { left: 0; } +.ple-col-sticky:nth-child(2) { left: 140px; } +.ple-col-sticky:nth-child(3) { left: 340px; } + +.ple-table td { + padding: 5px 8px; + border-bottom: 1px solid #f0f2f5; + white-space: nowrap; + vertical-align: top; +} +.ple-table tr:nth-child(even) td { background: #f9fafc; } +.ple-table tr:nth-child(even) td.ple-col-sticky { background: #f4f7fc; } +.ple-table tr:hover td { background: #eef4fc !important; } + +.ple-edit { padding: 2px 4px !important; } +.ple-cell-stack { display: flex; flex-direction: column; gap: 2px; min-width: 100px; } + +.ple-inp-min, +.ple-inp-rate { + width: 100%; + padding: 2px 6px; + border: 1px solid transparent; + border-radius: 3px; + font-size: 12px; + text-align: right; + background: transparent; + transition: border-color .12s; + box-sizing: border-box; +} +.ple-inp-min { height: 22px; font-size: 11px; color: #6b7280; } +.ple-inp-rate { height: 28px; font-weight: 500; } + +.ple-inp-min::placeholder { font-size: 10px; color: #cbd5e1; } + +.ple-inp-min:hover, +.ple-inp-rate:hover { border-color: #93c5fd; background: #fafbfc; } + +.ple-inp-min:focus, +.ple-inp-rate:focus { + outline: none; + border-color: #2490ef; + box-shadow: 0 0 0 2px rgba(36, 144, 239, .10); + background: #fff; +} + +.ple-saving { background: #fef9e7 !important; } +.ple-saving .ple-inp-min, +.ple-saving .ple-inp-rate { border-color: #f59e0b !important; } +.ple-saved { background: #ecfdf5 !important; transition: background .3s; } +.ple-error { background: #fef2f2 !important; } diff --git a/aqrar_ext/aqrar_ext/page/price_list_bulk_editor/price_list_bulk_editor.js b/aqrar_ext/aqrar_ext/page/price_list_bulk_editor/price_list_bulk_editor.js new file mode 100644 index 0000000..1b3802a --- /dev/null +++ b/aqrar_ext/aqrar_ext/page/price_list_bulk_editor/price_list_bulk_editor.js @@ -0,0 +1,263 @@ +frappe.pages["price-list-bulk-editor"].on_page_load = function (wrapper) { + var page = frappe.ui.make_app_page({ + parent: wrapper, + title: __("Price List Bulk Editor"), + single_column: true, + }); + + page.main.css("padding", "0"); + + var columns = []; + var data = []; + var current_pls = []; + + var chrome_html = ` +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+

${__("Loading…")}

+

${__("Click a cell to edit. Press Enter or Tab to save.")}

+
+
+ `; + $(chrome_html).appendTo(page.main); + + var item_code_field = frappe.ui.form.make_control({ + parent: $("#ple-f-item-code"), + df: { + fieldname: "item_code", label: __("Item Code"), fieldtype: "Link", options: "Item", + change: function () { load_data(); } + }, + render_input: true, + }); + + var item_group_field = frappe.ui.form.make_control({ + parent: $("#ple-f-item-group"), + df: { + fieldname: "item_group", label: __("Item Group"), fieldtype: "Link", options: "Item Group", + change: function () { load_data(); } + }, + render_input: true, + }); + + var price_list_field = frappe.ui.form.make_control({ + parent: $("#ple-f-plist"), + df: { + fieldname: "price_list", label: __("Price List"), fieldtype: "Link", options: "Price List", + change: function () { load_data(); } + }, + render_input: true, + }); + + var cost_center_field = frappe.ui.form.make_control({ + parent: $("#ple-f-cost-center"), + df: { + fieldname: "cost_center", label: __("Cost Center"), fieldtype: "Link", options: "Cost Center", + change: function () { load_price_lists_and_data(); } + }, + render_input: true, + }); + + $("#ple-btn-refresh").on("click", load_data); + + function load_price_lists_and_data() { + var $btn = $("#ple-btn-refresh").prop("disabled", true); + frappe.call({ + method: "aqrar_ext.aqrar_ext.page.price_list_bulk_editor.price_list_bulk_editor.get_selling_price_lists", + args: { cost_center: cost_center_field.get_value() || "" }, + callback: function (r) { + current_pls = (r.message || []).map(function (p) { return p.name; }); + $btn.prop("disabled", false); + load_data(); + }, + error: function () { + $btn.prop("disabled", false); + frappe.show_alert({ + message: __("Failed to load price lists."), + indicator: "red", + }); + }, + }); + } + + function load_data() { + var selected_pl = price_list_field.get_value(); + var pls = selected_pl ? [selected_pl] : current_pls; + if (!pls.length) { + if (!current_pls.length) { + show_empty(__("Loading price lists…")); + } else { + show_empty(__("No selling price lists found for this cost center.")); + } + return; + } + $("#ple-empty").show().find(".ple-empty-title").text(__("Loading…")); + $("#ple-table-shell").find(".ple-scroll, table").remove(); + $("#ple-btn-refresh").prop("disabled", true); + + frappe.call({ + method: "aqrar_ext.aqrar_ext.page.price_list_bulk_editor.price_list_bulk_editor.get_item_price_matrix", + args: { + item_group: item_group_field.get_value() || "", + item_code: item_code_field.get_value() || "", + price_lists: pls, + cost_center: cost_center_field.get_value() || "", + }, + callback: function (r) { + $("#ple-btn-refresh").prop("disabled", false); + var msg = r.message; + if (!msg || !msg.data.length) { + show_empty(__("No items found.")); + return; + } + columns = msg.columns; + data = msg.data; + build_table(msg.item_count, msg.price_lists.length); + }, + error: function () { + $("#ple-btn-refresh").prop("disabled", false); + frappe.show_alert({ + message: __("Failed to load data."), + indicator: "red", + }); + }, + }); + } + + function build_table(item_count, pl_count) { + $("#ple-empty").hide(); + var $shell = $("#ple-table-shell"); + $shell.find(".ple-scroll, table").remove(); + + var c = columns; + var d = data; + + var h = '
'; + c.forEach(function (col, ci) { + var sticky = ci < 3 ? " ple-col-sticky" : ""; + var cls = (col.editable ? "ple-th-price" : "ple-th-fixed") + sticky; + h += ''; + }); + h += ''; + + d.forEach(function (row, ri) { + h += ''; + row.forEach(function (cell, ci) { + var sticky_cls = ci < 3 ? " ple-col-sticky" : ""; + if (!c[ci].editable) { + h += ''; + } else { + var info = cell && typeof cell === "object" ? cell : {}; + var rate = info.rate != null ? info.rate : ""; + var minr = info.min_rate != null ? info.min_rate : ""; + h += ''; + } + }); + h += ''; + }); + h += '
' + col.name + '
' + frappe.utils.escape_html(String(cell || "")) + ''; + h += '
'; + h += ''; + h += ''; + h += '
'; + h += '
'; + $shell.prepend(h); + + $("#ple-status").html( + '
' + + '' + item_count + ' ' + __("items") + '' + + '' + pl_count + ' ' + __("price lists") + '' + + '' + __("Edit then Enter / Tab to save.") + '' + + '
' + ); + + $shell.find(".ple-inp-min, .ple-inp-rate").off("blur keydown").on("blur", function () { + save_cell($(this).closest("td")); + }).on("keydown", function (e) { + if (e.key === "Enter") { + e.preventDefault(); + $(this).blur(); + } + }); + } + + function save_cell($td) { + if ($td.hasClass("ple-saving")) return; + + var ri = parseInt($td.attr("data-ri")); + var ci = parseInt($td.attr("data-ci")); + if (isNaN(ri) || isNaN(ci)) return; + + var row = data[ri]; + var col = columns[ci]; + + var $min_inp = $td.find(".ple-inp-min"); + var $rate_inp = $td.find(".ple-inp-rate"); + + var old_info = row[ci] && typeof row[ci] === "object" ? row[ci] : {}; + var old_rate = parseFloat(old_info.rate) || 0; + var old_min = parseFloat(old_info.min_rate) || 0; + + var new_rate = parseFloat($rate_inp.val().trim()); + var new_min = parseFloat($min_inp.val().trim()); + if (isNaN(new_rate) || new_rate < 0) { $rate_inp.val(old_rate || ""); return; } + if (isNaN(new_min) || new_min < 0) { $min_inp.val(old_min || ""); return; } + if (new_rate === old_rate && new_min === old_min) return; + + $td.addClass("ple-saving"); + + frappe.call({ + method: "aqrar_ext.aqrar_ext.page.price_list_bulk_editor.price_list_bulk_editor.save_cell", + args: { + item_code: row[0], + price_list: col.price_list, + uom: row[2], + rate: new_rate, + min_rate: new_min, + }, + callback: function (r) { + $td.removeClass("ple-saving"); + if (r.message) { + row[ci] = { rate: new_rate, min_rate: new_min, item_price_name: r.message.name, uom: row[2] }; + $td.addClass("ple-saved"); + setTimeout(function () { $td.removeClass("ple-saved"); }, 900); + frappe.show_alert({ + message: r.message.action === "updated" + ? __("Updated") + ": " + row[0] + " → " + col.price_list + : __("Created") + ": " + row[0] + " → " + col.price_list, + indicator: "green", + }); + } + }, + error: function () { + $td.removeClass("ple-saving").addClass("ple-error"); + setTimeout(function () { $td.removeClass("ple-error"); }, 1500); + frappe.show_alert({ + message: __("Failed to save") + ": " + row[0] + " → " + col.price_list, + indicator: "red", + }); + }, + }); + } + + function show_empty(msg) { + $("#ple-status").empty(); + $("#ple-table-shell").find(".ple-scroll, table").remove(); + $("#ple-empty").show().find(".ple-empty-title").text(msg || __("No items found.")); + } + + load_price_lists_and_data(); +}; diff --git a/aqrar_ext/aqrar_ext/page/price_list_bulk_editor/price_list_bulk_editor.json b/aqrar_ext/aqrar_ext/page/price_list_bulk_editor/price_list_bulk_editor.json new file mode 100644 index 0000000..be010f4 --- /dev/null +++ b/aqrar_ext/aqrar_ext/page/price_list_bulk_editor/price_list_bulk_editor.json @@ -0,0 +1,13 @@ +{ + "content": null, + "doctype": "Page", + "module": "Aqrar Ext", + "name": "price-list-bulk-editor", + "page_name": "price-list-bulk-editor", + "roles": [ + {"role": "System Manager"}, + {"role": "Sales Master Manager"} + ], + "standard": "Yes", + "title": "Price List Bulk Editor" +} diff --git a/aqrar_ext/aqrar_ext/page/price_list_bulk_editor/price_list_bulk_editor.py b/aqrar_ext/aqrar_ext/page/price_list_bulk_editor/price_list_bulk_editor.py new file mode 100644 index 0000000..c507fbe --- /dev/null +++ b/aqrar_ext/aqrar_ext/page/price_list_bulk_editor/price_list_bulk_editor.py @@ -0,0 +1,144 @@ +import frappe +from frappe import _ +from frappe.utils import flt + + +@frappe.whitelist() +def get_selling_price_lists(cost_center=None): + """Return enabled selling price lists, optionally filtered by cost center.""" + filters = {"enabled": 1, "selling": 1} + if cost_center: + filters["custom_branch"] = cost_center + return frappe.get_all( + "Price List", + filters=filters, + fields=["name", "currency"], + order_by="name", + ) + + +@frappe.whitelist() +def get_item_price_matrix(item_group=None, price_lists=None, cost_center=None, item_code=None): + """Return a pivot grid: rows = items, cols = one per price list with rate/min.""" + import json + + if isinstance(price_lists, str): + price_lists = json.loads(price_lists) + + if not price_lists: + price_lists = frappe.get_all( + "Price List", + filters={"enabled": 1, "selling": 1}, + pluck="name", + order_by="name", + ) + + item_filters = {"disabled": 0, "is_stock_item": 1} + if item_group: + item_filters["item_group"] = item_group + if item_code: + item_filters["item_code"] = ("like", "%{}%".format(item_code)) + + items = frappe.get_all( + "Item", + filters=item_filters, + fields=["item_code", "item_name", "stock_uom"], + order_by="item_code", + limit_page_length=500, + ) + + if not items or not price_lists: + return {"columns": [], "data": [], "price_lists": [], "item_count": 0} + + item_codes = [d.item_code for d in items] + + all_prices = frappe.get_all( + "Item Price", + filters={ + "item_code": ("in", item_codes), + "price_list": ("in", price_lists), + "selling": 1, + }, + fields=[ + "name", "item_code", "price_list", "uom", + "price_list_rate", "custom_minimum_selling_rate", + "customer", "supplier", + ], + ) + + price_map = {} + for p in all_prices: + key = (p.item_code, p.price_list, p.uom) + if key not in price_map or (not p.customer and not p.supplier): + price_map[key] = { + "item_price_name": p.name, + "rate": p.price_list_rate, + "min_rate": p.custom_minimum_selling_rate, + "uom": p.uom, + } + + columns = [ + {"id": "item_code", "name": _("Item Code"), "editable": False, "width": 140}, + {"id": "item_name", "name": _("Item Name"), "editable": False, "width": 200}, + {"id": "uom", "name": _("UOM"), "editable": False, "width": 60}, + ] + for pl_name in price_lists: + col_id = "pl_" + pl_name.replace(" ", "_").replace("-", "_") + columns.append({ + "id": col_id, + "name": pl_name, + "editable": True, + "width": 130, + "price_list": pl_name, + }) + + data = [] + for item in items: + row = [item.item_code, item.item_name, item.stock_uom] + for pl_name in price_lists: + info = price_map.get((item.item_code, pl_name, item.stock_uom)) or {} + row.append(info if info else {}) + data.append(row) + + return { + "columns": columns, + "data": data, + "price_lists": price_lists, + "item_count": len(items), + } + + +@frappe.whitelist() +def save_cell(item_code, price_list, uom, rate, min_rate=None): + """Create or update a single Item Price row.""" + rate = flt(rate) + min_rate = flt(min_rate) if min_rate not in (None, "", 0) else None + + existing = frappe.db.exists( + "Item Price", + { + "item_code": item_code, + "price_list": price_list, + "uom": uom, + "selling": 1, + }, + ) + + if existing: + update = {"price_list_rate": rate} + if min_rate is not None: + update["custom_minimum_selling_rate"] = min_rate + frappe.db.set_value("Item Price", existing, update) + return {"name": existing, "action": "updated"} + + doc = frappe.get_doc({ + "doctype": "Item Price", + "item_code": item_code, + "price_list": price_list, + "uom": uom, + "price_list_rate": rate, + "custom_minimum_selling_rate": min_rate, + "selling": 1, + }) + doc.save(ignore_permissions=True) + return {"name": doc.name, "action": "created"} diff --git a/aqrar_ext/aqrar_ext/print_format/sales_invoice_aqrar/sales_invoice_aqrar.html b/aqrar_ext/aqrar_ext/print_format/sales_invoice_aqrar/sales_invoice_aqrar.html new file mode 100644 index 0000000..a8e2e3a --- /dev/null +++ b/aqrar_ext/aqrar_ext/print_format/sales_invoice_aqrar/sales_invoice_aqrar.html @@ -0,0 +1,149 @@ +{%- set mode = doc._item_display_mode or "Item Name + Description" -%} + +{%- macro render_item(item) -%} +{%- if mode == "Item Code" -%} + {{ item.item_code }} +{%- elif mode == "Item Name" -%} + {{ item.item_name }} +{%- elif mode == "Item Code + Description" -%} + {{ item.item_code }} + {%- if item.description and item.description != item.item_code -%}
{{ item.description }}{%- endif -%} +{%- else -%} + {{ item.item_name }} + {%- if item.description and item.description != item.item_name -%}
{{ item.description }}{%- endif -%} +{%- endif -%} +{%- endmacro -%} + +{%- macro add_header(page_num, max_pages, doc, letter_head, no_letterhead, footer, print_settings=None, print_heading_template=None) -%} + {% if letter_head and not no_letterhead %} +
{{ letter_head }}
+ {% endif %} + {% if print_heading_template %} + {{ frappe.render_template(print_heading_template, {"doc":doc}) }} + {% endif %} + {%- if doc.meta.is_submittable and doc.docstatus==2-%} +
+

{{ _("CANCELLED") }}

+
+ {%- endif -%} +{%- endmacro -%} + +{% for page in layout %} +
+
+ {{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings) }} +
+ + {% if print_settings.repeat_header_footer %} + + {% endif %} + +
+
+
{{ doc.customer }}
+
{{ doc.address_display }}
+
{{ _("Contact: ")+doc.contact_display if doc.contact_display else '' }}
+
{{ _("Mobile: ")+doc.contact_mobile if doc.contact_mobile else '' }}
+
+
+
+
+
+
{{ doc.name }}
+
+
+
+
{{ frappe.utils.format_date(doc.posting_date) }}
+
+
+
+
{{ frappe.utils.format_date(doc.due_date) }}
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + {% for item in doc.items %} + + + + + + + + {% endfor %} +
{{ _("Sr") }}{{ _("Details") }}{{ _("Qty") }}{{ _("Rate") }}{{ _("Amount") }}
{{ loop.index }}{{ render_item(item) }} + {{ item.get_formatted("qty", 0) }} + {{ item.get_formatted("uom", 0) }} + {{ item.get_formatted("net_rate", doc) }}{{ item.get_formatted("net_amount", doc) }}
+ +
+
+
+ + {{ doc.in_words }} +
+
+ + {{ doc.status }} +
+
+
+
+
{{ _("Sub Total") }}
+
{{ doc.get_formatted("net_total", doc) }}
+
+
+ {% for d in doc.taxes %} + {% if d.tax_amount %} +
+
{{ _(d.description) }}
+
{{ d.get_formatted("tax_amount") }}
+
+ {% endif %} + {% endfor %} +
+
+
{{ _("Total") }}
+
{{ doc.get_formatted("grand_total", doc) }}
+
+
+
+
+ +
+
+
+
+
{{ doc.terms if doc.terms else '' }}
+
+
+
+
+{% endfor %} diff --git a/aqrar_ext/aqrar_ext/print_format/sales_invoice_aqrar/sales_invoice_aqrar.json b/aqrar_ext/aqrar_ext/print_format/sales_invoice_aqrar/sales_invoice_aqrar.json new file mode 100644 index 0000000..c57dfc1 --- /dev/null +++ b/aqrar_ext/aqrar_ext/print_format/sales_invoice_aqrar/sales_invoice_aqrar.json @@ -0,0 +1,27 @@ +{ + "absolute_value": 0, + "align_labels_right": 0, + "css": "", + "custom_format": 1, + "default_print_language": "en", + "disabled": 0, + "doc_type": "Sales Invoice", + "doctype": "Print Format", + "font": "", + "font_size": 14, + "idx": 0, + "line_breaks": 0, + "margin_bottom": 0.0, + "margin_left": 0.0, + "margin_right": 0.0, + "margin_top": 0.0, + "module": "Aqrar Ext", + "name": "Sales Invoice Aqrar", + "page_number": "Hide", + "print_format_builder": 0, + "print_format_builder_beta": 0, + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 0, + "standard": "No" +} diff --git a/aqrar_ext/aqrar_ext/utils/__init__.py b/aqrar_ext/aqrar_ext/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aqrar_ext/aqrar_ext/utils/print_helpers.py b/aqrar_ext/aqrar_ext/utils/print_helpers.py new file mode 100644 index 0000000..18641b1 --- /dev/null +++ b/aqrar_ext/aqrar_ext/utils/print_helpers.py @@ -0,0 +1,36 @@ +"""Print format helper functions — exposed to Jinja via jenv hook.""" +import frappe + + +def get_display_mode(): + """Return the current item display mode from Aqrar Settings.""" + mode = frappe.db.get_value( + "Aqrar Settings", "Aqrar Settings", "item_display_mode" + ) + return mode or "Item Name + Description" + + +def format_item_display(item_code, item_name, description): + """Return HTML string for item display based on Aqrar Settings mode. + + Called from print format Jinja templates as: {{ format_item_display(...) }} + """ + mode = get_display_mode() + + if mode == "Item Code": + return "{}".format(frappe.utils.escape_html(item_code)) + + if mode == "Item Name": + return "{}".format(frappe.utils.escape_html(item_name)) + + if mode == "Item Code + Description": + out = "{}".format(frappe.utils.escape_html(item_code)) + if description and description != item_code: + out += "
{}".format(frappe.utils.escape_html(description)) + return out + + # Default: Item Name + Description + out = "{}".format(frappe.utils.escape_html(item_name)) + if description and description != item_name: + out += "
{}".format(frappe.utils.escape_html(description)) + return out diff --git a/aqrar_ext/aqrar_ext/workflow/__init__.py b/aqrar_ext/aqrar_ext/workflow/__init__.py new file mode 100644 index 0000000..25ce2e8 --- /dev/null +++ b/aqrar_ext/aqrar_ext/workflow/__init__.py @@ -0,0 +1 @@ +# Aqrar Workflow module — approval conditions and email-based approval handlers diff --git a/aqrar_ext/aqrar_ext/workflow/conditions.py b/aqrar_ext/aqrar_ext/workflow/conditions.py new file mode 100644 index 0000000..2442e98 --- /dev/null +++ b/aqrar_ext/aqrar_ext/workflow/conditions.py @@ -0,0 +1,217 @@ +""" +Server-side validation for Aqrar approval workflows. + +Workflow transitions are role-based (Branch Approver, Branch Accountant). +These functions enforce branch-level granularity via doc_events. +They are called from validate hooks to ensure only the correct branch +personnel can approve documents at each workflow state. +""" + +import frappe + + +def validate_stock_entry_approval(doc, method=None): + """ + Enforce that only a Branch Approver for the receiving branch can + approve a Stock Entry (Material Transfer). Called via doc_events + validate hook. + """ + if not doc.get("workflow_state") or doc.get("workflow_state") not in ("Pending Approval", "Committed"): + return + + if doc.get("workflow_state") == "Pending Approval": + _ensure_user_is_branch_approver(doc) + + +def validate_journal_entry_approval(doc, method=None): + """Enforce branch-accountant approval for Journal Entry.""" + if not doc.get("workflow_state") or doc.get("workflow_state") not in ("Pending Approval", "Posted"): + return + + if doc.get("workflow_state") == "Pending Approval": + _ensure_user_is_branch_accountant(doc) + + +def validate_expense_claim_approval(doc, method=None): + """Enforce branch-accountant approval for Expense Claim.""" + if not doc.get("workflow_state") or doc.get("workflow_state") not in ("Pending Approval", "Approved"): + return + + if doc.get("workflow_state") == "Pending Approval": + _ensure_user_is_branch_accountant(doc) + + +def validate_payment_entry_approval(doc, method=None): + """Enforce branch-accountant approval for Payment Entry.""" + if not doc.get("workflow_state") or doc.get("workflow_state") not in ("Pending Approval", "Approved"): + return + + if doc.get("workflow_state") == "Pending Approval": + _ensure_user_is_branch_accountant(doc) + + +# ── helpers ──────────────────────────────────────────────────────────── + + +def _ensure_user_is_branch_approver(doc): + """ + Raise if the current user is not a Branch Approver for at least one + of the target-warehouse branches in this Stock Entry. + """ + user = frappe.session.user + + # Collect target warehouses from items + target_warehouses = [] + if doc.get("items"): + for item in doc.items: + t_wh = item.get("t_warehouse") + if t_wh: + target_warehouses.append(t_wh) + + if not target_warehouses: + return # no target warehouse to validate against — allow + + # Branches that own those target warehouses + wh_parents = frappe.get_all( + "Branch Configuration Warehouse", + filters={"warehouse": ("in", target_warehouses)}, + pluck="parent", + ) + + if not wh_parents: + frappe.throw( + "No Branch Configuration found for the target warehouse(s). " + "Please set up the branch-warehouse mapping first." + ) + + # Check if user is a Branch Approver for any of those branches + user_configs = frappe.get_all( + "Branch Configuration User", + filters={ + "user": user, + "role": "Branch Approver", + "parent": ("in", wh_parents), + }, + pluck="parent", + ) + + if not user_configs: + frappe.throw( + f"User {user} is not a Branch Approver for the receiving " + "branch of this Stock Transfer. Only the receiving-branch approver " + "can approve this document." + ) + + +def _ensure_user_is_branch_accountant(doc): + """ + Raise if the current user is not a Branch Accountant for the company + associated with this document. + """ + user = frappe.session.user + company = doc.get("company") + + if not company: + return # no company to validate against — allow + + # Find branch configurations for this company + branch_configs = frappe.get_all( + "Branch Configuration", + filters={"company": company}, + pluck="name", + ) + + if not branch_configs: + frappe.throw( + f"No Branch Configuration found for company {company}. " + "Please set up branch configurations first." + ) + + # Check if user is a Branch Accountant for any branch of this company + user_configs = frappe.get_all( + "Branch Configuration User", + filters={ + "user": user, + "role": "Branch Accountant", + "parent": ("in", branch_configs), + }, + pluck="parent", + ) + + if not user_configs: + frappe.throw( + f"User {user} is not a Branch Accountant for company " + f"{company}. Only a branch accountant of this company " + "can approve this document." + ) + + +# ── utility for programmatic checks (can be used from other code) ────── + + +@frappe.whitelist() +def is_receiving_branch_approver(doc, user=None): + """ + Check if *user* (defaults to current user) is a Branch Approver for + the target warehouses in *doc*. Returns True/False. + """ + user = user or frappe.session.user + + target_warehouses = set() + if doc.get("items"): + for item in doc.items: + t_wh = item.get("t_warehouse") + if t_wh: + target_warehouses.add(t_wh) + + if not target_warehouses: + return False + + wh_parents = frappe.get_all( + "Branch Configuration Warehouse", + filters={"warehouse": ("in", list(target_warehouses))}, + pluck="parent", + ) + + if not wh_parents: + return False + + return frappe.db.exists( + "Branch Configuration User", + { + "user": user, + "role": "Branch Approver", + "parent": ("in", wh_parents), + }, + ) + + +@frappe.whitelist() +def is_branch_accountant(doc, user=None): + """ + Check if *user* is a Branch Accountant for *doc*'s company. + Returns True/False. + """ + user = user or frappe.session.user + company = doc.get("company") + + if not company: + return False + + branch_configs = frappe.get_all( + "Branch Configuration", + filters={"company": company}, + pluck="name", + ) + + if not branch_configs: + return False + + return frappe.db.exists( + "Branch Configuration User", + { + "user": user, + "role": "Branch Accountant", + "parent": ("in", branch_configs), + }, + ) diff --git a/aqrar_ext/aqrar_ext/workflow/email_approval.py b/aqrar_ext/aqrar_ext/workflow/email_approval.py new file mode 100644 index 0000000..e4bb97f --- /dev/null +++ b/aqrar_ext/aqrar_ext/workflow/email_approval.py @@ -0,0 +1,81 @@ +""" +Email-based approval for Aqrar workflows. + +When a workflow sends an email alert to an approver, the email contains +a link to the document. The approver clicks the link, logs in, and sees +the Approve/Reject buttons on the form. + +This module provides: +1. A whitelisted endpoint that generates a signed approval link +2. A utility to auto-apply workflow action via URL token + +The signed-URL approach uses Frappe's built-in get_signed_params / +verify_request for one-click approval without requiring the user to be +logged in (optional — can be restricted to logged-in users only). +""" + +import frappe +from frappe.utils.verified_command import get_signed_params, verify_request +from frappe.utils import get_url + + +@frappe.whitelist(allow_guest=True) +def approve_via_email(doctype, docname, action, **kwargs): + """ + Approve a document via an email link. + + URL: /api/method/aqrar_ext.aqrar_ext.workflow.email_approval.approve_via_email + ?doctype=Stock Entry + &docname=MAT-STE-2026-00001 + &action=Approve + &... (signed params) + + When called with valid signed params, applies the workflow action + as if the user clicked the button in the UI. + + If the request is not signed, the user must be logged in and have + the required role for the action. + """ + if kwargs: + # Request came with signed params — verify them + verify_request() + elif frappe.session.user == "Guest": + frappe.throw( + "You must be logged in to approve documents. " + "Please use the signed link from your email.", + frappe.PermissionError, + ) + + doc = frappe.get_doc(doctype, docname) + from frappe.model.workflow import apply_workflow + + apply_workflow(doc, action) + frappe.db.commit() + + return { + "success": True, + "message": f"{doctype} {docname} has been {action.lower()}d.", + "doctype": doctype, + "docname": docname, + "workflow_state": doc.get(doc.meta.workflow_state_field or "workflow_state"), + } + + +@frappe.whitelist() +def get_approval_link(doctype, docname, action): + """ + Generate a signed URL for one-click approval. + The link is valid for 7 days. + + Returns a full URL that can be included in email templates. + """ + params = { + "doctype": doctype, + "docname": docname, + "action": action, + } + signed = get_signed_params(params) + return get_url( + f"/api/method/aqrar_ext.aqrar_ext.workflow.email_approval.approve_via_email" + f"?{signed}" + ) diff --git a/aqrar_ext/aqrar_ext/workflow/test_workflows.py b/aqrar_ext/aqrar_ext/workflow/test_workflows.py new file mode 100644 index 0000000..caba88a --- /dev/null +++ b/aqrar_ext/aqrar_ext/workflow/test_workflows.py @@ -0,0 +1,368 @@ +""" +Tests for CR-017: Configurable approval workflows. + +Covers: + - Workflow fixture integrity (4 doctypes) + - Branch-level approval enforcement (conditions.py) + - Email approval link generation + - Whitelisted utility functions +""" + +import frappe +from frappe.tests.utils import FrappeTestCase, change_settings + + +class TestWorkflowFixtures(FrappeTestCase): + """Verify all 4 workflows are defined with correct states + transitions.""" + + def setUp(self): + self.workflows = frappe.get_all("Workflow", fields=["name", "document_type"]) + + def test_four_workflows_exist(self): + """CR-017 requires workflows for all 4 doctypes.""" + doctypes = {w.document_type for w in self.workflows} + self.assertIn("Stock Entry", doctypes) + self.assertIn("Journal Entry", doctypes) + self.assertIn("Expense Claim", doctypes) + self.assertIn("Payment Entry", doctypes) + self.assertEqual(len(self.workflows), 4) + + def test_workflow_states_and_transitions(self): + """Each workflow must have Draft→Pending→Final + Reject states.""" + expected = { + "Stock Transfer Approval": { + "states": {"Draft", "Pending Approval", "Committed", "Rejected"}, + "final_state": "Committed", + }, + "Journal Entry Approval": { + "states": {"Draft", "Pending Approval", "Posted", "Rejected"}, + "final_state": "Posted", + }, + "Expense Claim Approval": { + "states": {"Draft", "Pending Approval", "Approved", "Rejected"}, + "final_state": "Approved", + }, + "Payment Entry Approval": { + "states": {"Draft", "Pending Approval", "Approved", "Rejected"}, + "final_state": "Approved", + }, + } + + for name, spec in expected.items(): + wf = frappe.get_doc("Workflow", name) + actual_states = {s.state for s in wf.states} + self.assertEqual(actual_states, spec["states"], + f"{name}: expected states {spec['states']}, got {actual_states}") + + # Verify the approve transition lands on the correct final state + approve_transitions = [t for t in wf.transitions if t.action == "Approve"] + self.assertEqual(len(approve_transitions), 1, f"{name}: expected 1 Approve transition") + self.assertEqual(approve_transitions[0].next_state, spec["final_state"], + f"{name}: Approve should go to {spec['final_state']}") + + # Verify reject is optional + reject_state = next((s for s in wf.states if s.state == "Rejected"), None) + self.assertIsNotNone(reject_state, f"{name}: missing Rejected state") + self.assertTrue(reject_state.is_optional_state, + f"{name}: Rejected should be optional") + + def test_workflow_state_field_custom_fields_exist(self): + """Each doctype must have a workflow_state custom field.""" + for dt in ("Stock Entry", "Journal Entry", "Expense Claim", "Payment Entry"): + meta = frappe.get_meta(dt) + self.assertIn("workflow_state", [f.fieldname for f in meta.fields], + f"{dt} missing workflow_state custom field") + + def test_workflow_actions_exist(self): + """Required workflow actions must be defined.""" + actions = frappe.get_all("Workflow Action Master", pluck="name") + for action in ("Submit for Approval", "Approve", "Reject"): + self.assertIn(action, actions) + + +class TestBranchApprovalConditions(FrappeTestCase): + """Test the server-side enforcement in conditions.py.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._create_test_roles() + cls._create_test_users() + cls._create_test_data() + + @classmethod + def _create_test_roles(cls): + for role_name in ("Branch Approver", "Branch Accountant", "Accounts User", "Stock User"): + if not frappe.db.exists("Role", role_name): + frappe.get_doc({"doctype": "Role", "role_name": role_name}).insert( + ignore_permissions=True + ) + + @classmethod + def _create_test_users(cls): + users = { + "test_approver@aqrar.com": ["Branch Approver"], + "test_accountant@aqrar.com": ["Branch Accountant"], + "test_stranger@aqrar.com": ["Accounts User"], + } + for email, roles in users.items(): + if frappe.db.exists("User", email): + continue + user = frappe.get_doc({ + "doctype": "User", + "email": email, + "first_name": email.split("@")[0].replace("_", " ").title(), + "send_welcome_email": 0, + "roles": [{"role": r} for r in roles], + }) + user.insert(ignore_permissions=True) + frappe.db.commit() + + @classmethod + def _create_test_data(cls): + # Company + if not frappe.db.exists("Company", "_Test Company AQR"): + company = frappe.get_doc({ + "doctype": "Company", + "company_name": "_Test Company AQR", + "abbr": "TCA", + "default_currency": "SAR", + }) + company.insert(ignore_permissions=True) + + # Branch + if not frappe.db.exists("Branch", "_Test Branch AQR"): + frappe.get_doc({ + "doctype": "Branch", "branch_name": "_Test Branch AQR", + }).insert(ignore_permissions=True) + + # Warehouse + if not frappe.db.exists("Warehouse", "_Test Warehouse AQR - TCA"): + frappe.get_doc({ + "doctype": "Warehouse", + "warehouse_name": "_Test Warehouse AQR", + "company": "_Test Company AQR", + }).insert(ignore_permissions=True) + + # Branch Configuration (links branch → company, warehouse, users) + if not frappe.db.exists("Branch Configuration", "_Test BC AQR"): + bc = frappe.get_doc({ + "doctype": "Branch Configuration", + "branch": "_Test Branch AQR", + "company": "_Test Company AQR", + "warehouse": [{"warehouse": "_Test Warehouse AQR - TCA"}], + "user": [ + {"user": "test_approver@aqrar.com", "role": "Branch Approver"}, + {"user": "test_accountant@aqrar.com", "role": "Branch Accountant"}, + ], + }) + bc.insert(ignore_permissions=True) + + frappe.db.commit() + + # ── Stock Entry (Branch Approver) ────────────────────────────────── + + def test_stock_entry_blocks_non_approver(self): + """A user who is NOT a Branch Approver for the receiving branch cannot approve.""" + doc = frappe.new_doc("Stock Entry") + doc.stock_entry_type = "Material Transfer" + doc.company = "_Test Company AQR" + doc.workflow_state = "Pending Approval" + doc.append("items", { + "item_code": "_Test Item", + "qty": 1, + "s_warehouse": "_Test Warehouse AQR - TCA", + "t_warehouse": "_Test Warehouse AQR - TCA", + }) + + frappe.set_user("test_stranger@aqrar.com") + from aqrar_ext.aqrar_ext.workflow.conditions import validate_stock_entry_approval + with self.assertRaises(frappe.ValidationError): + validate_stock_entry_approval(doc) + + def test_stock_entry_allows_branch_approver(self): + """A Branch Approver for the receiving branch may approve.""" + doc = frappe.new_doc("Stock Entry") + doc.stock_entry_type = "Material Transfer" + doc.company = "_Test Company AQR" + doc.workflow_state = "Pending Approval" + doc.append("items", { + "item_code": "_Test Item", + "qty": 1, + "s_warehouse": "_Test Warehouse AQR - TCA", + "t_warehouse": "_Test Warehouse AQR - TCA", + }) + + frappe.set_user("test_approver@aqrar.com") + from aqrar_ext.aqrar_ext.workflow.conditions import validate_stock_entry_approval + try: + validate_stock_entry_approval(doc) + except frappe.ValidationError: + self.fail("Branch Approver should be allowed to approve Stock Entry") + + # ── Journal Entry (Branch Accountant) ────────────────────────────── + + def test_journal_entry_blocks_non_accountant(self): + """A user who is NOT a Branch Accountant for the company cannot approve.""" + doc = frappe.new_doc("Journal Entry") + doc.company = "_Test Company AQR" + doc.workflow_state = "Pending Approval" + + frappe.set_user("test_stranger@aqrar.com") + from aqrar_ext.aqrar_ext.workflow.conditions import validate_journal_entry_approval + with self.assertRaises(frappe.ValidationError): + validate_journal_entry_approval(doc) + + def test_journal_entry_allows_branch_accountant(self): + """A Branch Accountant for the company may approve.""" + doc = frappe.new_doc("Journal Entry") + doc.company = "_Test Company AQR" + doc.workflow_state = "Pending Approval" + + frappe.set_user("test_accountant@aqrar.com") + from aqrar_ext.aqrar_ext.workflow.conditions import validate_journal_entry_approval + try: + validate_journal_entry_approval(doc) + except frappe.ValidationError: + self.fail("Branch Accountant should be allowed to approve Journal Entry") + + # ── Expense Claim (Branch Accountant) ────────────────────────────── + + def test_expense_claim_blocks_non_accountant(self): + doc = frappe.new_doc("Expense Claim") + doc.company = "_Test Company AQR" + doc.workflow_state = "Pending Approval" + + frappe.set_user("test_stranger@aqrar.com") + from aqrar_ext.aqrar_ext.workflow.conditions import validate_expense_claim_approval + with self.assertRaises(frappe.ValidationError): + validate_expense_claim_approval(doc) + + def test_expense_claim_allows_branch_accountant(self): + doc = frappe.new_doc("Expense Claim") + doc.company = "_Test Company AQR" + doc.workflow_state = "Pending Approval" + + frappe.set_user("test_accountant@aqrar.com") + from aqrar_ext.aqrar_ext.workflow.conditions import validate_expense_claim_approval + try: + validate_expense_claim_approval(doc) + except frappe.ValidationError: + self.fail("Branch Accountant should be allowed to approve Expense Claim") + + # ── Payment Entry (Branch Accountant) ────────────────────────────── + + def test_payment_entry_blocks_non_accountant(self): + doc = frappe.new_doc("Payment Entry") + doc.payment_type = "Receive" + doc.company = "_Test Company AQR" + doc.workflow_state = "Pending Approval" + + frappe.set_user("test_stranger@aqrar.com") + from aqrar_ext.aqrar_ext.workflow.conditions import validate_payment_entry_approval + with self.assertRaises(frappe.ValidationError): + validate_payment_entry_approval(doc) + + def test_payment_entry_allows_branch_accountant(self): + doc = frappe.new_doc("Payment Entry") + doc.payment_type = "Receive" + doc.company = "_Test Company AQR" + doc.workflow_state = "Pending Approval" + + frappe.set_user("test_accountant@aqrar.com") + from aqrar_ext.aqrar_ext.workflow.conditions import validate_payment_entry_approval + try: + validate_payment_entry_approval(doc) + except frappe.ValidationError: + self.fail("Branch Accountant should be allowed to approve Payment Entry") + + # ── Skip validation for non-workflow states ─────────────────────── + + def test_draft_state_skips_validation(self): + """Documents in Draft state should NOT trigger approval checks.""" + for dt, validator in [ + ("Stock Entry", "validate_stock_entry_approval"), + ("Journal Entry", "validate_journal_entry_approval"), + ("Expense Claim", "validate_expense_claim_approval"), + ("Payment Entry", "validate_payment_entry_approval"), + ]: + doc = frappe.new_doc(dt) + if dt == "Stock Entry": + doc.stock_entry_type = "Material Transfer" + elif dt == "Payment Entry": + doc.payment_type = "Receive" + doc.company = "_Test Company AQR" + doc.workflow_state = "Draft" + + frappe.set_user("test_stranger@aqrar.com") + from aqrar_ext.aqrar_ext.workflow import conditions + validate_fn = getattr(conditions, validator) + try: + validate_fn(doc) + except frappe.ValidationError: + self.fail(f"{dt} in Draft should not trigger validation") + + +class TestWhitelistedUtilities(FrappeTestCase): + """Test the whitelisted helper functions.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + # Reuse data from TestBranchApprovalConditions + TestBranchApprovalConditions.setUpClass() + + def test_is_receiving_branch_approver_true(self): + doc = frappe.new_doc("Stock Entry") + doc.stock_entry_type = "Material Transfer" + doc.append("items", { + "t_warehouse": "_Test Warehouse AQR - TCA", + }) + from aqrar_ext.aqrar_ext.workflow.conditions import is_receiving_branch_approver + self.assertTrue( + is_receiving_branch_approver(doc, user="test_approver@aqrar.com") + ) + + def test_is_receiving_branch_approver_false(self): + doc = frappe.new_doc("Stock Entry") + doc.stock_entry_type = "Material Transfer" + doc.append("items", { + "t_warehouse": "_Test Warehouse AQR - TCA", + }) + from aqrar_ext.aqrar_ext.workflow.conditions import is_receiving_branch_approver + self.assertFalse( + is_receiving_branch_approver(doc, user="test_stranger@aqrar.com") + ) + + def test_is_branch_accountant_true(self): + doc = frappe.new_doc("Journal Entry") + doc.company = "_Test Company AQR" + from aqrar_ext.aqrar_ext.workflow.conditions import is_branch_accountant + self.assertTrue( + is_branch_accountant(doc, user="test_accountant@aqrar.com") + ) + + def test_is_branch_accountant_false(self): + doc = frappe.new_doc("Journal Entry") + doc.company = "_Test Company AQR" + from aqrar_ext.aqrar_ext.workflow.conditions import is_branch_accountant + self.assertFalse( + is_branch_accountant(doc, user="test_stranger@aqrar.com") + ) + + +class TestEmailApproval(FrappeTestCase): + """Test email-based approval link generation.""" + + def test_get_approval_link_returns_url(self): + from aqrar_ext.aqrar_ext.workflow.email_approval import get_approval_link + url = get_approval_link("Stock Entry", "MAT-STE-2026-00001", "Approve") + self.assertIn("/api/method/", url) + self.assertIn("aqrar_ext.aqrar_ext.workflow.email_approval.approve_via_email", url) + + def test_approve_via_email_rejects_unsigned_guest(self): + """Guest without signed params should get PermissionError.""" + frappe.set_user("Guest") + from aqrar_ext.aqrar_ext.workflow.email_approval import approve_via_email + with self.assertRaises(frappe.PermissionError): + approve_via_email("Stock Entry", "nonexistent", "Approve") diff --git a/aqrar_ext/fixtures/custom_docperm.json b/aqrar_ext/fixtures/custom_docperm.json new file mode 100644 index 0000000..51eedb0 --- /dev/null +++ b/aqrar_ext/fixtures/custom_docperm.json @@ -0,0 +1,52 @@ +[ + { + "amend": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "docstatus": 0, + "doctype": "Custom DocPerm", + "email": 0, + "export": 0, + "if_owner": 0, + "import": 0, + "name": "Project-Branch User", + "parent": "Project", + "parentfield": "permissions", + "parenttype": "DocType", + "permlevel": 0, + "print": 0, + "read": 1, + "report": 0, + "role": "Branch User", + "select": 0, + "share": 0, + "submit": 0, + "write": 1 + }, + { + "amend": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "docstatus": 0, + "doctype": "Custom DocPerm", + "email": 0, + "export": 0, + "if_owner": 0, + "import": 0, + "name": "Project-Sales User", + "parent": "Project", + "parentfield": "permissions", + "parenttype": "DocType", + "permlevel": 0, + "print": 0, + "read": 1, + "report": 0, + "role": "Sales User", + "select": 0, + "share": 0, + "submit": 0, + "write": 1 + } +] diff --git a/aqrar_ext/fixtures/custom_field.json b/aqrar_ext/fixtures/custom_field.json index a8c8c26..75884c9 100644 --- a/aqrar_ext/fixtures/custom_field.json +++ b/aqrar_ext/fixtures/custom_field.json @@ -113,6 +113,174 @@ "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": null, + "description": "Minimum allowed selling rate. Sales Invoice submit blocked if item rate falls below this value.", + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Item Price", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_minimum_selling_rate", + "fieldtype": "Currency", + "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": "price_list_rate", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Minimum Selling Rate", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "module": null, + "name": "Item Price-custom_minimum_selling_rate", + "no_copy": 0, + "non_negative": 1, + "options": "currency", + "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": 1, + "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": null, + "description": "Cost center this price list applies to. Used for cost-center-wise price differentiation.", + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Price List", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_branch", + "fieldtype": "Link", + "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": 1, + "in_preview": 0, + "in_standard_filter": 1, + "insert_after": "selling", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Cost Center", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "module": null, + "name": "Price List-custom_branch", + "no_copy": 0, + "non_negative": 0, + "options": "Cost Center", + "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": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": "0", + "depends_on": null, + "description": "If checked, the minimum selling rate validation is bypassed for this Sales Invoice.", + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_override_minimum_price", + "fieldtype": "Check", + "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": "update_stock", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Override Minimum Price", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "module": null, + "name": "Sales Invoice-custom_override_minimum_price", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 1, + "placeholder": null, + "precision": "", + "print_hide": 1, + "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": 1, + "unique": 0, + "width": null + }, { "allow_in_quick_entry": 0, "allow_on_submit": 0, @@ -147,7 +315,6 @@ "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2026-04-22 13:02:16.808435", "module": null, "name": "Sales Invoice-custom_payment_mode", "no_copy": 0, @@ -172,12 +339,17 @@ }, { "allow_in_quick_entry": 0, +<<<<<<< HEAD "allow_on_submit": 1, +======= + "allow_on_submit": 0, +>>>>>>> f2e4231 (feat:aqrar implmentation) "bold": 0, "collapsible": 0, "collapsible_depends_on": null, "columns": 0, "default": null, +<<<<<<< HEAD "depends_on": "eval:doc.custom_payment_mode === 'Card' || doc.custom_payment_mode === 'Credit'", "description": null, "docstatus": 0, @@ -240,6 +412,13 @@ "docstatus": 0, "doctype": "Custom Field", "dt": "Material Request", +======= + "depends_on": null, + "description": "Current approval state for the Aqrar Stock Transfer approval workflow.", + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Stock Entry", +>>>>>>> f2e4231 (feat:aqrar implmentation) "fetch_from": null, "fetch_if_empty": 0, "fieldname": "workflow_state", @@ -251,19 +430,31 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_global_search": 0, +<<<<<<< HEAD "in_list_view": 1, "in_preview": 0, "in_standard_filter": 1, "insert_after": "status", +======= + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "stock_entry_type", +>>>>>>> f2e4231 (feat:aqrar implmentation) "is_system_generated": 0, "is_virtual": 0, "label": "Workflow State", "length": 0, "link_filters": null, "mandatory_depends_on": null, +<<<<<<< HEAD "modified": "2026-05-17 00:00:00", "module": null, "name": "Material Request-workflow_state", +======= + "module": "Aqrar Ext", + "name": "Stock Entry-workflow_state", +>>>>>>> f2e4231 (feat:aqrar implmentation) "no_copy": 0, "non_negative": 0, "options": "Workflow State", @@ -280,7 +471,11 @@ "search_index": 0, "show_dashboard": 0, "sort_options": 0, +<<<<<<< HEAD "translatable": 0, +======= + "translatable": 1, +>>>>>>> f2e4231 (feat:aqrar implmentation) "unique": 0, "width": null }, @@ -293,6 +488,7 @@ "columns": 0, "default": null, "depends_on": null, +<<<<<<< HEAD "description": null, "docstatus": 0, "doctype": "Custom Field", @@ -301,6 +497,16 @@ "fetch_if_empty": 0, "fieldname": "custom_last_price", "fieldtype": "Currency", +======= + "description": "Current approval state for the Aqrar Journal Entry approval workflow.", + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Journal Entry", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "workflow_state", + "fieldtype": "Link", +>>>>>>> f2e4231 (feat:aqrar implmentation) "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -308,6 +514,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_global_search": 0, +<<<<<<< HEAD "in_list_view": 1, "in_preview": 0, "in_standard_filter": 0, @@ -328,6 +535,27 @@ "placeholder": null, "precision": "", "print_hide": 0, +======= + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "voucher_type", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Workflow State", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "module": "Aqrar Ext", + "name": "Journal Entry-workflow_state", + "no_copy": 0, + "non_negative": 0, + "options": "Workflow State", + "permlevel": 0, + "placeholder": null, + "precision": "", + "print_hide": 1, +>>>>>>> f2e4231 (feat:aqrar implmentation) "print_hide_if_no_value": 0, "print_width": null, "read_only": 1, @@ -337,7 +565,11 @@ "search_index": 0, "show_dashboard": 0, "sort_options": 0, +<<<<<<< HEAD "translatable": 0, +======= + "translatable": 1, +>>>>>>> f2e4231 (feat:aqrar implmentation) "unique": 0, "width": null }, @@ -350,6 +582,7 @@ "columns": 0, "default": null, "depends_on": null, +<<<<<<< HEAD "description": null, "docstatus": 0, "doctype": "Custom Field", @@ -358,6 +591,16 @@ "fetch_if_empty": 0, "fieldname": "custom_last_price", "fieldtype": "Currency", +======= + "description": "Current approval state for the Aqrar Payment Entry approval workflow.", + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Payment Entry", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "workflow_state", + "fieldtype": "Link", +>>>>>>> f2e4231 (feat:aqrar implmentation) "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -365,6 +608,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_global_search": 0, +<<<<<<< HEAD "in_list_view": 1, "in_preview": 0, "in_standard_filter": 0, @@ -385,6 +629,27 @@ "placeholder": null, "precision": "", "print_hide": 0, +======= + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "payment_type", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Workflow State", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "module": "Aqrar Ext", + "name": "Payment Entry-workflow_state", + "no_copy": 0, + "non_negative": 0, + "options": "Workflow State", + "permlevel": 0, + "placeholder": null, + "precision": "", + "print_hide": 1, +>>>>>>> f2e4231 (feat:aqrar implmentation) "print_hide_if_no_value": 0, "print_width": null, "read_only": 1, @@ -394,7 +659,11 @@ "search_index": 0, "show_dashboard": 0, "sort_options": 0, +<<<<<<< HEAD "translatable": 0, +======= + "translatable": 1, +>>>>>>> f2e4231 (feat:aqrar implmentation) "unique": 0, "width": null }, @@ -407,6 +676,7 @@ "columns": 0, "default": null, "depends_on": null, +<<<<<<< HEAD "description": null, "docstatus": 0, "doctype": "Custom Field", @@ -415,6 +685,16 @@ "fetch_if_empty": 0, "fieldname": "custom_last_price", "fieldtype": "Currency", +======= + "description": "Default naming series applied when creating Items under this Item Group.", + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Item Group", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_default_item_naming_series", + "fieldtype": "Select", +>>>>>>> f2e4231 (feat:aqrar implmentation) "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -422,6 +702,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_global_search": 0, +<<<<<<< HEAD "in_list_view": 1, "in_preview": 0, "in_standard_filter": 0, @@ -435,6 +716,76 @@ "modified": "2026-05-19 16:12:31.457961", "module": null, "name": "Delivery Note Item-custom_last_price", +======= + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "parent_item_group", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Default Item Naming Series", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "module": "Aqrar Ext", + "name": "Item Group-custom_default_item_naming_series", + "no_copy": 0, + "non_negative": 0, + "options": "\nSTO-ITEM-.YYYY.-\nSTO-ITEM-", + "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": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": "0", + "depends_on": null, + "description": "Play a sound when a desk notification arrives.", + "docstatus": 0, + "doctype": "Custom Field", + "dt": "User", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_enable_sound_alerts", + "fieldtype": "Check", + "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": "user_image", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Enable Sound Alerts", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "module": "Aqrar Ext", + "name": "User-custom_enable_sound_alerts", +>>>>>>> f2e4231 (feat:aqrar implmentation) "no_copy": 0, "non_negative": 0, "options": null, @@ -444,14 +795,22 @@ "print_hide": 0, "print_hide_if_no_value": 0, "print_width": null, +<<<<<<< HEAD "read_only": 1, +======= + "read_only": 0, +>>>>>>> f2e4231 (feat:aqrar implmentation) "read_only_depends_on": null, "report_hide": 0, "reqd": 0, "search_index": 0, "show_dashboard": 0, "sort_options": 0, +<<<<<<< HEAD "translatable": 0, +======= + "translatable": 1, +>>>>>>> f2e4231 (feat:aqrar implmentation) "unique": 0, "width": null }, @@ -464,6 +823,7 @@ "columns": 0, "default": null, "depends_on": null, +<<<<<<< HEAD "description": null, "docstatus": 0, "doctype": "Custom Field", @@ -472,6 +832,16 @@ "fetch_if_empty": 0, "fieldname": "custom_last_price", "fieldtype": "Currency", +======= + "description": "Link to the source Sales Invoice this commission Journal Entry relates to.", + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Journal Entry", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_reference_invoice", + "fieldtype": "Link", +>>>>>>> f2e4231 (feat:aqrar implmentation) "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -481,6 +851,7 @@ "in_global_search": 0, "in_list_view": 1, "in_preview": 0, +<<<<<<< HEAD "in_standard_filter": 0, "insert_after": "rate", "is_system_generated": 0, @@ -495,20 +866,43 @@ "no_copy": 0, "non_negative": 0, "options": null, +======= + "in_standard_filter": 1, + "insert_after": "cheque_no", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Reference Invoice", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "module": "Aqrar Ext", + "name": "Journal Entry-custom_reference_invoice", + "no_copy": 0, + "non_negative": 0, + "options": "Sales Invoice", +>>>>>>> f2e4231 (feat:aqrar implmentation) "permlevel": 0, "placeholder": null, "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "print_width": null, +<<<<<<< HEAD "read_only": 1, +======= + "read_only": 0, +>>>>>>> f2e4231 (feat:aqrar implmentation) "read_only_depends_on": null, "report_hide": 0, "reqd": 0, "search_index": 0, "show_dashboard": 0, "sort_options": 0, +<<<<<<< HEAD "translatable": 0, +======= + "translatable": 1, +>>>>>>> f2e4231 (feat:aqrar implmentation) "unique": 0, "width": null }, @@ -521,6 +915,7 @@ "columns": 0, "default": null, "depends_on": null, +<<<<<<< HEAD "description": null, "docstatus": 0, "doctype": "Custom Field", @@ -529,6 +924,16 @@ "fetch_if_empty": 0, "fieldname": "custom_last_price", "fieldtype": "Currency", +======= + "description": "Default expense account for commission journal entries.", + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "default_commission_expense_account", + "fieldtype": "Link", +>>>>>>> f2e4231 (feat:aqrar implmentation) "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -536,6 +941,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_global_search": 0, +<<<<<<< HEAD "in_list_view": 1, "in_preview": 0, "in_standard_filter": 0, @@ -552,20 +958,45 @@ "no_copy": 0, "non_negative": 0, "options": null, +======= + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "default_expense_account", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Default Commission Expense Account", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "module": "Accounts", + "name": "Company-default_commission_expense_account", + "no_copy": 0, + "non_negative": 0, + "options": "Account", +>>>>>>> f2e4231 (feat:aqrar implmentation) "permlevel": 0, "placeholder": null, "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "print_width": null, +<<<<<<< HEAD "read_only": 1, +======= + "read_only": 0, +>>>>>>> f2e4231 (feat:aqrar implmentation) "read_only_depends_on": null, "report_hide": 0, "reqd": 0, "search_index": 0, "show_dashboard": 0, "sort_options": 0, +<<<<<<< HEAD "translatable": 0, +======= + "translatable": 1, +>>>>>>> f2e4231 (feat:aqrar implmentation) "unique": 0, "width": null }, @@ -578,6 +1009,7 @@ "columns": 0, "default": null, "depends_on": null, +<<<<<<< HEAD "description": null, "docstatus": 0, "doctype": "Custom Field", @@ -586,6 +1018,16 @@ "fetch_if_empty": 0, "fieldname": "custom_last_price", "fieldtype": "Currency", +======= + "description": "Default payable account for commission journal entries.", + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "default_commission_payable_account", + "fieldtype": "Link", +>>>>>>> f2e4231 (feat:aqrar implmentation) "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -593,6 +1035,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_global_search": 0, +<<<<<<< HEAD "in_list_view": 1, "in_preview": 0, "in_standard_filter": 0, @@ -609,20 +1052,45 @@ "no_copy": 0, "non_negative": 0, "options": null, +======= + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "default_commission_expense_account", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Default Commission Payable Account", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "module": "Accounts", + "name": "Company-default_commission_payable_account", + "no_copy": 0, + "non_negative": 0, + "options": "Account", +>>>>>>> f2e4231 (feat:aqrar implmentation) "permlevel": 0, "placeholder": null, "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "print_width": null, +<<<<<<< HEAD "read_only": 1, +======= + "read_only": 0, +>>>>>>> f2e4231 (feat:aqrar implmentation) "read_only_depends_on": null, "report_hide": 0, "reqd": 0, "search_index": 0, "show_dashboard": 0, "sort_options": 0, +<<<<<<< HEAD "translatable": 0, +======= + "translatable": 1, +>>>>>>> f2e4231 (feat:aqrar implmentation) "unique": 0, "width": null }, @@ -635,6 +1103,7 @@ "columns": 0, "default": null, "depends_on": null, +<<<<<<< HEAD "description": null, "docstatus": 0, "doctype": "Custom Field", @@ -643,6 +1112,16 @@ "fetch_if_empty": 0, "fieldname": "custom_last_price", "fieldtype": "Currency", +======= + "description": "Default expense account for discount day-close journal entries.", + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "default_discount_expense_account", + "fieldtype": "Link", +>>>>>>> f2e4231 (feat:aqrar implmentation) "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -650,6 +1129,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_global_search": 0, +<<<<<<< HEAD "in_list_view": 1, "in_preview": 0, "in_standard_filter": 0, @@ -666,21 +1146,102 @@ "no_copy": 0, "non_negative": 0, "options": null, +======= + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "default_commission_payable_account", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Default Discount Expense Account", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "module": "Accounts", + "name": "Company-default_discount_expense_account", + "no_copy": 0, + "non_negative": 0, + "options": "Account", +>>>>>>> f2e4231 (feat:aqrar implmentation) "permlevel": 0, "placeholder": null, "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "print_width": null, +<<<<<<< HEAD "read_only": 1, +======= + "read_only": 0, +>>>>>>> f2e4231 (feat:aqrar implmentation) "read_only_depends_on": null, "report_hide": 0, "reqd": 0, "search_index": 0, "show_dashboard": 0, "sort_options": 0, +<<<<<<< HEAD "translatable": 0, +======= + "translatable": 1, + "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": null, + "description": "Default payable/clearing account for discount day-close journal entries.", + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "default_discount_payable_account", + "fieldtype": "Link", + "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": "default_discount_expense_account", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Default Discount Payable Account", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "module": "Accounts", + "name": "Company-default_discount_payable_account", + "no_copy": 0, + "non_negative": 0, + "options": "Account", + "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": 1, +>>>>>>> f2e4231 (feat:aqrar implmentation) "unique": 0, "width": null } -] \ No newline at end of file +] diff --git a/aqrar_ext/fixtures/notification.json b/aqrar_ext/fixtures/notification.json new file mode 100644 index 0000000..8ffec69 --- /dev/null +++ b/aqrar_ext/fixtures/notification.json @@ -0,0 +1,110 @@ +[ + { + "channel": "System Notification", + "condition": null, + "days_in_advance": 0, + "docstatus": 0, + "doctype": "Notification", + "document_type": "Item", + "enabled": 1, + "event": "New", + "is_standard": 1, + "method": null, + "module": "Aqrar Ext", + "name": "Item Created Alert", + "print_format": null, + "recipients": [ + { + "cc": null, + "bcc": null, + "condition": null, + "parent": "Item Created Alert", + "parentfield": "recipients", + "parenttype": "Notification", + "receiver_by_document_field": null, + "receiver_by_role": "Item Manager" + }, + { + "cc": null, + "bcc": null, + "condition": null, + "parent": "Item Created Alert", + "parentfield": "recipients", + "parenttype": "Notification", + "receiver_by_document_field": null, + "receiver_by_role": "Stock Manager" + }, + { + "cc": null, + "bcc": null, + "condition": null, + "parent": "Item Created Alert", + "parentfield": "recipients", + "parenttype": "Notification", + "receiver_by_document_field": null, + "receiver_by_role": "Stock User" + } + ], + "send_to_all_assignees": 0, + "sender": null, + "sender_email": null, + "set_property_after_alert": null, + "slack_webhook_url": null, + "subject": "New Item Created: {{ doc.item_code }} - {{ doc.item_name }}", + "value_changed": null + }, + { + "channel": "System Notification", + "condition": null, + "days_in_advance": 0, + "docstatus": 0, + "doctype": "Notification", + "document_type": "Item Price", + "enabled": 1, + "event": "Value Change", + "is_standard": 1, + "method": null, + "module": "Aqrar Ext", + "name": "Item Price Updated Alert", + "print_format": null, + "recipients": [ + { + "cc": null, + "bcc": null, + "condition": null, + "parent": "Item Price Updated Alert", + "parentfield": "recipients", + "parenttype": "Notification", + "receiver_by_document_field": null, + "receiver_by_role": "Item Manager" + }, + { + "cc": null, + "bcc": null, + "condition": null, + "parent": "Item Price Updated Alert", + "parentfield": "recipients", + "parenttype": "Notification", + "receiver_by_document_field": null, + "receiver_by_role": "Stock Manager" + }, + { + "cc": null, + "bcc": null, + "condition": null, + "parent": "Item Price Updated Alert", + "parentfield": "recipients", + "parenttype": "Notification", + "receiver_by_document_field": null, + "receiver_by_role": "Sales Master Manager" + } + ], + "send_to_all_assignees": 0, + "sender": null, + "sender_email": null, + "set_property_after_alert": null, + "slack_webhook_url": null, + "subject": "Item Price Updated: {{ doc.item_code }} — {{ doc.price_list }} rate changed to {{ doc.price_list_rate }}", + "value_changed": "price_list_rate" + } +] diff --git a/aqrar_ext/fixtures/workflow.json b/aqrar_ext/fixtures/workflow.json index 5547090..81a2600 100644 --- a/aqrar_ext/fixtures/workflow.json +++ b/aqrar_ext/fixtures/workflow.json @@ -2,6 +2,7 @@ { "docstatus": 0, "doctype": "Workflow", +<<<<<<< HEAD "document_type": "Material Request", "is_active": 1, "modified": "2026-05-19 16:15:59.237356", @@ -17,12 +18,48 @@ "message": "Waiting for Branch User approval", "next_action_email_template": null, "parent": "Material Request Approval", +======= + "document_type": "Stock Entry", + "is_active": 1, + "name": "Stock Transfer Approval", + "override_status": 0, + "send_email_alert": 1, + "states": [ + { + "allow_edit": "Stock User", + "avoid_status_override": 0, + "doc_status": "0", + "is_optional_state": 0, + "message": "", + "next_action_email_template": "", + "parent": "Stock Transfer Approval", + "parentfield": "states", + "parenttype": "Workflow", + "send_email": 0, + "state": "Draft", + "update_field": "workflow_state", + "update_value": "Draft", + "workflow_builder_id": null + }, + { + "allow_edit": "Branch Approver", + "avoid_status_override": 0, + "doc_status": "0", + "is_optional_state": 0, + "message": "Stock Transfer requires your approval. Please review and approve or reject.", + "next_action_email_template": "", + "parent": "Stock Transfer Approval", +>>>>>>> f2e4231 (feat:aqrar implmentation) "parentfield": "states", "parenttype": "Workflow", "send_email": 0, "state": "Pending Approval", "update_field": "workflow_state", +<<<<<<< HEAD "update_value": null, +======= + "update_value": "Pending Approval", +>>>>>>> f2e4231 (feat:aqrar implmentation) "workflow_builder_id": null }, { @@ -30,6 +67,7 @@ "avoid_status_override": 0, "doc_status": "1", "is_optional_state": 0, +<<<<<<< HEAD "message": "Approved - Ready for Transfer", "next_action_email_template": null, "parent": "Material Request Approval", @@ -71,17 +109,66 @@ "state": "Closed", "update_field": "workflow_state", "update_value": null, +======= + "message": "Stock Transfer has been approved and committed.", + "next_action_email_template": "", + "parent": "Stock Transfer Approval", + "parentfield": "states", + "parenttype": "Workflow", + "send_email": 0, + "state": "Committed", + "update_field": "workflow_state", + "update_value": "Committed", + "workflow_builder_id": null + }, + { + "allow_edit": "Stock User", + "avoid_status_override": 0, + "doc_status": "1", + "is_optional_state": 1, + "message": "Stock Transfer has been rejected.", + "next_action_email_template": "", + "parent": "Stock Transfer Approval", + "parentfield": "states", + "parenttype": "Workflow", + "send_email": 0, + "state": "Rejected", + "update_field": "workflow_state", + "update_value": "Rejected", +>>>>>>> f2e4231 (feat:aqrar implmentation) "workflow_builder_id": null } ], "transitions": [ { +<<<<<<< HEAD "action": "Approve", "allow_self_approval": 0, "allowed": "Branch User", "condition": "frappe.session.user != doc.owner", "next_state": "Approved", "parent": "Material Request Approval", +======= + "action": "Submit for Approval", + "allow_self_approval": 0, + "allowed": "Stock User", + "condition": null, + "next_state": "Pending Approval", + "parent": "Stock Transfer Approval", + "parentfield": "transitions", + "parenttype": "Workflow", + "send_email_to_creator": 0, + "state": "Draft", + "workflow_builder_id": null + }, + { + "action": "Approve", + "allow_self_approval": 0, + "allowed": "Branch Approver", + "condition": null, + "next_state": "Committed", + "parent": "Stock Transfer Approval", +>>>>>>> f2e4231 (feat:aqrar implmentation) "parentfield": "transitions", "parenttype": "Workflow", "send_email_to_creator": 0, @@ -91,17 +178,131 @@ { "action": "Reject", "allow_self_approval": 0, +<<<<<<< HEAD "allowed": "Branch User", "condition": null, "next_state": "Closed", "parent": "Material Request Approval", +======= + "allowed": "Branch Approver", + "condition": null, + "next_state": "Rejected", + "parent": "Stock Transfer Approval", "parentfield": "transitions", "parenttype": "Workflow", "send_email_to_creator": 0, "state": "Pending Approval", "workflow_builder_id": null + } + ], + "workflow_data": null, + "workflow_name": "Stock Transfer Approval", + "workflow_state_field": "workflow_state" + }, + { + "docstatus": 0, + "doctype": "Workflow", + "document_type": "Journal Entry", + "is_active": 1, + "name": "Journal Entry Approval", + "override_status": 0, + "send_email_alert": 1, + "states": [ + { + "allow_edit": "Accounts User", + "avoid_status_override": 0, + "doc_status": "0", + "is_optional_state": 0, + "message": "", + "next_action_email_template": "", + "parent": "Journal Entry Approval", + "parentfield": "states", + "parenttype": "Workflow", + "send_email": 0, + "state": "Draft", + "update_field": "workflow_state", + "update_value": "Draft", + "workflow_builder_id": null + }, + { + "allow_edit": "Branch Accountant", + "avoid_status_override": 0, + "doc_status": "0", + "is_optional_state": 0, + "message": "Journal Entry requires your approval. Please review and approve or reject.", + "next_action_email_template": "", + "parent": "Journal Entry Approval", + "parentfield": "states", + "parenttype": "Workflow", + "send_email": 0, + "state": "Pending Approval", + "update_field": "workflow_state", + "update_value": "Pending Approval", + "workflow_builder_id": null }, { + "allow_edit": "Accounts Manager", + "avoid_status_override": 0, + "doc_status": "1", + "is_optional_state": 0, + "message": "Journal Entry has been approved and posted.", + "next_action_email_template": "", + "parent": "Journal Entry Approval", + "parentfield": "states", + "parenttype": "Workflow", + "send_email": 0, + "state": "Posted", + "update_field": "workflow_state", + "update_value": "Posted", + "workflow_builder_id": null + }, + { + "allow_edit": "Accounts User", + "avoid_status_override": 0, + "doc_status": "1", + "is_optional_state": 1, + "message": "Journal Entry has been rejected.", + "next_action_email_template": "", + "parent": "Journal Entry Approval", + "parentfield": "states", + "parenttype": "Workflow", + "send_email": 0, + "state": "Rejected", + "update_field": "workflow_state", + "update_value": "Rejected", + "workflow_builder_id": null + } + ], + "transitions": [ + { + "action": "Submit for Approval", + "allow_self_approval": 0, + "allowed": "Accounts User", + "condition": null, + "next_state": "Pending Approval", + "parent": "Journal Entry Approval", + "parentfield": "transitions", + "parenttype": "Workflow", + "send_email_to_creator": 0, + "state": "Draft", + "workflow_builder_id": null + }, + { + "action": "Approve", + "allow_self_approval": 0, + "allowed": "Branch Accountant", + "condition": null, + "next_state": "Posted", + "parent": "Journal Entry Approval", +>>>>>>> f2e4231 (feat:aqrar implmentation) + "parentfield": "transitions", + "parenttype": "Workflow", + "send_email_to_creator": 0, + "state": "Pending Approval", + "workflow_builder_id": null + }, + { +<<<<<<< HEAD "action": "Cancel", "allow_self_approval": 0, "allowed": "Branch User", @@ -125,11 +326,149 @@ "parenttype": "Workflow", "send_email_to_creator": 0, "state": "Approved", +======= + "action": "Reject", + "allow_self_approval": 0, + "allowed": "Branch Accountant", + "condition": null, + "next_state": "Rejected", + "parent": "Journal Entry Approval", + "parentfield": "transitions", + "parenttype": "Workflow", + "send_email_to_creator": 0, + "state": "Pending Approval", +>>>>>>> f2e4231 (feat:aqrar implmentation) "workflow_builder_id": null } ], "workflow_data": null, +<<<<<<< HEAD "workflow_name": "Material Request Approval", "workflow_state_field": "workflow_state" } -] \ No newline at end of file +] +======= + "workflow_name": "Journal Entry Approval", + "workflow_state_field": "workflow_state" + }, + { + "docstatus": 0, + "doctype": "Workflow", + "document_type": "Payment Entry", + "is_active": 1, + "name": "Payment Entry Approval", + "override_status": 0, + "send_email_alert": 1, + "states": [ + { + "allow_edit": "Accounts User", + "avoid_status_override": 0, + "doc_status": "0", + "is_optional_state": 0, + "message": "", + "next_action_email_template": "", + "parent": "Payment Entry Approval", + "parentfield": "states", + "parenttype": "Workflow", + "send_email": 0, + "state": "Draft", + "update_field": "workflow_state", + "update_value": "Draft", + "workflow_builder_id": null + }, + { + "allow_edit": "Branch Accountant", + "avoid_status_override": 0, + "doc_status": "0", + "is_optional_state": 0, + "message": "Payment Entry requires your approval. Please verify the payment details.", + "next_action_email_template": "", + "parent": "Payment Entry Approval", + "parentfield": "states", + "parenttype": "Workflow", + "send_email": 0, + "state": "Pending Approval", + "update_field": "workflow_state", + "update_value": "Pending Approval", + "workflow_builder_id": null + }, + { + "allow_edit": "Accounts Manager", + "avoid_status_override": 0, + "doc_status": "1", + "is_optional_state": 0, + "message": "Payment Entry has been approved.", + "next_action_email_template": "", + "parent": "Payment Entry Approval", + "parentfield": "states", + "parenttype": "Workflow", + "send_email": 0, + "state": "Approved", + "update_field": "workflow_state", + "update_value": "Approved", + "workflow_builder_id": null + }, + { + "allow_edit": "Accounts User", + "avoid_status_override": 0, + "doc_status": "1", + "is_optional_state": 1, + "message": "Payment Entry has been rejected.", + "next_action_email_template": "", + "parent": "Payment Entry Approval", + "parentfield": "states", + "parenttype": "Workflow", + "send_email": 0, + "state": "Rejected", + "update_field": "workflow_state", + "update_value": "Rejected", + "workflow_builder_id": null + } + ], + "transitions": [ + { + "action": "Submit for Approval", + "allow_self_approval": 0, + "allowed": "Accounts User", + "condition": null, + "next_state": "Pending Approval", + "parent": "Payment Entry Approval", + "parentfield": "transitions", + "parenttype": "Workflow", + "send_email_to_creator": 0, + "state": "Draft", + "workflow_builder_id": null + }, + { + "action": "Approve", + "allow_self_approval": 0, + "allowed": "Branch Accountant", + "condition": null, + "next_state": "Approved", + "parent": "Payment Entry Approval", + "parentfield": "transitions", + "parenttype": "Workflow", + "send_email_to_creator": 0, + "state": "Pending Approval", + "workflow_builder_id": null + }, + { + "action": "Reject", + "allow_self_approval": 0, + "allowed": "Branch Accountant", + "condition": null, + "next_state": "Rejected", + "parent": "Payment Entry Approval", + "parentfield": "transitions", + "parenttype": "Workflow", + "send_email_to_creator": 0, + "state": "Pending Approval", + "workflow_builder_id": null + } + ], + "workflow_data": null, + "workflow_name": "Payment Entry Approval", + "workflow_state_field": "workflow_state" + } +] +>>>>>>> f2e4231 (feat:aqrar implmentation) diff --git a/aqrar_ext/fixtures/workflow_action_master.json b/aqrar_ext/fixtures/workflow_action_master.json new file mode 100644 index 0000000..d96bb2e --- /dev/null +++ b/aqrar_ext/fixtures/workflow_action_master.json @@ -0,0 +1,20 @@ +[ + { + "docstatus": 0, + "doctype": "Workflow Action Master", + "name": "Submit for Approval", + "workflow_action_name": "Submit for Approval" + }, + { + "docstatus": 0, + "doctype": "Workflow Action Master", + "name": "Approve", + "workflow_action_name": "Approve" + }, + { + "docstatus": 0, + "doctype": "Workflow Action Master", + "name": "Reject", + "workflow_action_name": "Reject" + } +] diff --git a/aqrar_ext/fixtures/workflow_state.json b/aqrar_ext/fixtures/workflow_state.json new file mode 100644 index 0000000..a8e99a1 --- /dev/null +++ b/aqrar_ext/fixtures/workflow_state.json @@ -0,0 +1,50 @@ +[ + { + "docstatus": 0, + "doctype": "Workflow State", + "icon": "question-sign", + "name": "Draft", + "style": "", + "workflow_state_name": "Draft" + }, + { + "docstatus": 0, + "doctype": "Workflow State", + "icon": "question-sign", + "name": "Pending Approval", + "style": "Warning", + "workflow_state_name": "Pending Approval" + }, + { + "docstatus": 0, + "doctype": "Workflow State", + "icon": "ok-sign", + "name": "Approved", + "style": "Success", + "workflow_state_name": "Approved" + }, + { + "docstatus": 0, + "doctype": "Workflow State", + "icon": "remove", + "name": "Rejected", + "style": "Danger", + "workflow_state_name": "Rejected" + }, + { + "docstatus": 0, + "doctype": "Workflow State", + "icon": "ok-sign", + "name": "Committed", + "style": "Success", + "workflow_state_name": "Committed" + }, + { + "docstatus": 0, + "doctype": "Workflow State", + "icon": "ok-sign", + "name": "Posted", + "style": "Success", + "workflow_state_name": "Posted" + } +] diff --git a/aqrar_ext/hooks.py b/aqrar_ext/hooks.py index 5b66895..063a528 100644 --- a/aqrar_ext/hooks.py +++ b/aqrar_ext/hooks.py @@ -12,9 +12,21 @@ # include js, css files in header of desk.html # app_include_css = "/assets/develop/css/develop.css" +doctype_js = { + "Journal Entry": "public/js/journal_entry_commission.js", +} + app_include_js = [ "/assets/aqrar_ext/js/sales_invoice_pos_total_popup.js", "/assets/sf_trading/js/workflowapproval.js", + "/assets/aqrar_ext/js/sales_invoice_return.js", + "/assets/aqrar_ext/js/sales_invoice_branch_price_list.js", + "/assets/aqrar_ext/js/auto_print_preview.js", + "/assets/aqrar_ext/js/item_naming_from_group.js", + "/assets/aqrar_ext/js/notification_sound.js", + "/assets/aqrar_ext/js/sales_invoice_navigation.js", + "/assets/aqrar_ext/js/sales_invoice_book_commission.js", + "/assets/aqrar_ext/js/sales_invoice_payment_terms.js", ] # include js, css files in header of web template @@ -302,8 +314,73 @@ [ # Sales Invoice "Sales Invoice-custom_payment_mode", - ], + # CR-015: Price List Bulk Editor & Min Price + "Item Price-custom_minimum_selling_rate", + "Price List-custom_branch", + "Sales Invoice-custom_override_minimum_price", + # CR-023: Commission JE reference + "Journal Entry-custom_reference_invoice", + "Company-default_commission_expense_account", + "Company-default_commission_payable_account", + "Company-default_discount_expense_account", + "Company-default_discount_payable_account", + # CR-021: Sound alert toggle + "User-custom_enable_sound_alerts", + # CR-020: Item naming series per item group + "Item Group-custom_default_item_naming_series", + # CR-017: Approval Workflows — workflow_state fields + "Stock Entry-workflow_state", + "Journal Entry-workflow_state", + "Payment Entry-workflow_state", + ], ] ] }, -] \ No newline at end of file + "Workflow State", + "Workflow Action Master", + "Workflow", + "Custom DocPerm", + "Notification", +] + +scheduler_events = { + "daily": [ + "aqrar_ext.api.day_close.run_day_close", + ], +} + +doc_events = { + "Sales Invoice": { + "validate": "aqrar_ext.aqrar_ext.overrides.sales_invoice.validate", + "before_save": "aqrar_ext.aqrar_ext.overrides.sales_invoice.before_save", + "before_print": "aqrar_ext.aqrar_ext.overrides.sales_invoice.before_print", + }, + # CR-017: Branch-level approval validation + "Stock Entry": { + "validate": "aqrar_ext.aqrar_ext.workflow.conditions.validate_stock_entry_approval", + }, + "Journal Entry": { + "validate": "aqrar_ext.aqrar_ext.workflow.conditions.validate_journal_entry_approval", + }, + "Payment Entry": { + "validate": "aqrar_ext.aqrar_ext.workflow.conditions.validate_payment_entry_approval", + }, + "Expense Claim": { + "validate": "aqrar_ext.aqrar_ext.workflow.conditions.validate_expense_claim_approval", + }, + "Custom Quote": { + "validate": "aqrar_ext.aqrar_ext.doctype.custom_quote.custom_quote.validate", + }, +} + +after_migrate = [ + "aqrar_ext.setup_data.create", +] + +# CR-024: Expose print helper functions to Jinja templates +jenv = { + "methods": [ + "aqrar_ext.aqrar_ext.utils.print_helpers.format_item_display", + "aqrar_ext.aqrar_ext.utils.print_helpers.get_display_mode", + ] +} diff --git a/aqrar_ext/public/js/auto_print_preview.js b/aqrar_ext/public/js/auto_print_preview.js new file mode 100644 index 0000000..1a512de --- /dev/null +++ b/aqrar_ext/public/js/auto_print_preview.js @@ -0,0 +1,151 @@ +frappe.ui.form.on("Sales Invoice", { + refresh: function (frm) { + if (!frm.doc.__islocal) init_or_refresh_preview(frm); + }, + after_save: function (frm) { + init_or_refresh_preview(frm); + }, +}); + +frappe.ui.form.on("Quotation", { + refresh: function (frm) { + if (!frm.doc.__islocal) init_or_refresh_preview(frm); + }, + after_save: function (frm) { + init_or_refresh_preview(frm); + }, +}); + +frappe.ui.form.on("Custom Quote", { + refresh: function (frm) { + if (!frm.doc.__islocal) init_or_refresh_preview(frm); + }, + after_save: function (frm) { + init_or_refresh_preview(frm); + }, +}); + +// Cache print formats per doctype so we don't re-fetch on every save +var print_format_cache = {}; + +function get_print_formats(doctype, callback) { + if (print_format_cache[doctype]) { + callback(print_format_cache[doctype]); + return; + } + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "Print Format", + filters: { doc_type: doctype, disabled: 0 }, + fields: ["name"], + order_by: "name", + }, + callback: function (r) { + var formats = (r.message || []).map(function (f) { return f.name; }); + if (formats.indexOf("Standard") === -1) formats.unshift("Standard"); + // Prefer Aqrar print format for Sales Invoice + if (doctype === "Sales Invoice") { + var aqrar_idx = formats.indexOf("Sales Invoice Aqrar"); + if (aqrar_idx > 0) { + formats.splice(aqrar_idx, 1); + formats.splice(1, 0, "Sales Invoice Aqrar"); + } + } + print_format_cache[doctype] = formats; + callback(formats); + }, + }); +} + +function init_or_refresh_preview(frm) { + if (!frm.doc.name) return; + if (frm.in_form === false) return; + + var footer = frm.page.footer; + footer.removeClass("hide"); + + var existing = footer.find(".auto-print-preview"); + if (existing.length) { + refresh_iframe(frm, existing); + return; + } + + get_print_formats(frm.doc.doctype, function (formats) { + var panel = $( + '
' + + '
' + + '
' + + '' + __("Print Preview") + '' + + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + + '' + + '
' + + '
' + ); + + footer.append(panel); + + // Default to Sales Invoice Aqrar when available + if (frm.doc.doctype === "Sales Invoice" && formats.indexOf("Sales Invoice Aqrar") !== -1) { + panel.find(".preview-format-select").val("Sales Invoice Aqrar"); + } + + // Format selector change + panel.find(".preview-format-select").on("change", function () { + refresh_iframe(frm, panel); + }); + + // Toggle show/hide + panel.find(".btn-toggle-preview").on("click", function () { + var body = panel.find(".preview-body"); + var btn = $(this); + if (body.is(":visible")) { + body.hide(); + btn.text(__("Show")); + } else { + body.show(); + btn.text(__("Hide")); + } + }); + + // Close — collapse to header bar so preview can be reopened + panel.find(".btn-close-preview").on("click", function () { + var body = panel.find(".preview-body"); + var toggle_btn = panel.find(".btn-toggle-preview"); + body.hide(); + toggle_btn.text(__("Show")); + }); + + refresh_iframe(frm, panel); + }); +} + +function refresh_iframe(frm, panel) { + var doctype = encodeURIComponent(frm.doc.doctype); + var docname = encodeURIComponent(frm.doc.name); + var format = encodeURIComponent(panel.find(".preview-format-select").val() || "Standard"); + var url = "/printview?doctype=" + doctype + "&name=" + docname + "&format=" + format + "&_ts=" + Date.now(); + + var iframe = panel.find(".preview-iframe"); + iframe.attr("src", url); + + iframe.off("load").on("load", function () { + try { + var h = iframe[0].contentWindow.document.body.scrollHeight; + if (h > 600) iframe.css("min-height", h + "px"); + } catch (e) { + // cross-origin or empty — keep default height + } + }); +} diff --git a/aqrar_ext/public/js/item_naming_from_group.js b/aqrar_ext/public/js/item_naming_from_group.js new file mode 100644 index 0000000..748d516 --- /dev/null +++ b/aqrar_ext/public/js/item_naming_from_group.js @@ -0,0 +1,31 @@ +frappe.ui.form.on("Item", { + setup: function (frm) { + frm.toggle_display("naming_series", false); + }, + refresh: function (frm) { + if (frm.doc.__islocal && frm.doc.item_group && !frm.doc._naming_applied) { + set_naming_from_group(frm); + } + }, + item_group: function (frm) { + if (!frm.doc.__islocal) return; + if (!frm.doc.item_group) { + frm.toggle_display("naming_series", false); + return; + } + set_naming_from_group(frm); + }, +}); + +function set_naming_from_group(frm) { + frappe.db.get_value("Item Group", frm.doc.item_group, "custom_default_item_naming_series", function (r) { + if (!r || !r.custom_default_item_naming_series) { + frm.toggle_display("naming_series", false); + return; + } + + frm.doc._naming_applied = true; + frm.toggle_display("naming_series", true); + frm.set_value("naming_series", r.custom_default_item_naming_series); + }); +} diff --git a/aqrar_ext/public/js/journal_entry_commission.js b/aqrar_ext/public/js/journal_entry_commission.js new file mode 100644 index 0000000..bb64444 --- /dev/null +++ b/aqrar_ext/public/js/journal_entry_commission.js @@ -0,0 +1,5 @@ +frappe.ui.form.on("Journal Entry", { + refresh: function (frm) { + frm.set_df_property("custom_reference_invoice", "hidden", 0); + }, +}); diff --git a/aqrar_ext/public/js/notification_sound.js b/aqrar_ext/public/js/notification_sound.js new file mode 100644 index 0000000..dcc2b70 --- /dev/null +++ b/aqrar_ext/public/js/notification_sound.js @@ -0,0 +1,8 @@ +frappe.realtime.on("notification", function () { + frappe.db.get_value("User", frappe.session.user, "custom_enable_sound_alerts", function (r) { + if (r && r.custom_enable_sound_alerts) { + var audio = new Audio("/assets/frappe/sounds/alert.mp3"); + audio.play().catch(function () { /* browser may block autoplay */ }); + } + }); +}); diff --git a/aqrar_ext/public/js/sales_invoice_book_commission.js b/aqrar_ext/public/js/sales_invoice_book_commission.js new file mode 100644 index 0000000..8f73ef3 --- /dev/null +++ b/aqrar_ext/public/js/sales_invoice_book_commission.js @@ -0,0 +1,61 @@ +frappe.ui.form.on("Sales Invoice", { + refresh: function (frm) { + if (frm.doc.__islocal || frm.doc.docstatus !== 1) return; + + frappe.call({ + method: "aqrar_ext.api.commission.get_commission_je_status", + args: { sales_invoice: frm.doc.name }, + callback: function (r) { + if (r.message && r.message.exists) { + frm.add_custom_button(__("View Commission JE"), function () { + frappe.set_route("Form", "Journal Entry", r.message.je_name); + }); + return; + } + + frm.add_custom_button(__("Book Commission"), function () { + frappe.call({ + method: "aqrar_ext.api.commission.create_commission_je", + args: { sales_invoice: frm.doc.name }, + freeze: true, + freeze_message: __("Creating Commission Journal Entry..."), + callback: function (res) { + if (res.message) { + frappe.set_route("Form", "Journal Entry", res.message); + } + }, + error: function () { + frappe.msgprint({ + title: __("Error"), + message: __("Failed to create Commission Journal Entry."), + indicator: "red", + }); + }, + }); + }); + }, + error: function () { + frm.add_custom_button(__("Book Commission"), function () { + frappe.call({ + method: "aqrar_ext.api.commission.create_commission_je", + args: { sales_invoice: frm.doc.name }, + freeze: true, + freeze_message: __("Creating Commission Journal Entry..."), + callback: function (res) { + if (res.message) { + frappe.set_route("Form", "Journal Entry", res.message); + } + }, + error: function () { + frappe.msgprint({ + title: __("Error"), + message: __("Failed to create Commission Journal Entry."), + indicator: "red", + }); + }, + }); + }); + }, + }); + }, +}); diff --git a/aqrar_ext/public/js/sales_invoice_branch_price_list.js b/aqrar_ext/public/js/sales_invoice_branch_price_list.js new file mode 100644 index 0000000..434f996 --- /dev/null +++ b/aqrar_ext/public/js/sales_invoice_branch_price_list.js @@ -0,0 +1,128 @@ +frappe.ui.form.on("Sales Invoice", { + onload: function (frm) { + auto_set_cost_center_price_list(frm); + }, + customer: function (frm) { + auto_set_cost_center_price_list(frm); + }, + cost_center: function (frm) { + auto_set_cost_center_price_list(frm); + }, +}); + +frappe.ui.form.on("Quotation", { + onload: function (frm) { + auto_set_cost_center_price_list(frm); + }, + customer: function (frm) { + auto_set_cost_center_price_list(frm); + }, + cost_center: function (frm) { + auto_set_cost_center_price_list(frm); + }, +}); + +/** + * Find an enabled, selling Price List tagged with this cost center. + * Uses the custom_branch field on Price List (which links to Cost Center). + */ +function get_cost_center_price_list(cost_center) { + return new Promise(function (resolve) { + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "Price List", + filters: { + custom_branch: cost_center, + enabled: 1, + selling: 1, + }, + fields: ["name"], + limit: 1, + }, + callback: function (r) { + var pl = (r.message && r.message.length) ? r.message[0].name : null; + resolve(pl); + }, + }); + }); +} + +/** + * Check a price list name exists and is enabled. + */ +function is_price_list_enabled(pl_name) { + return new Promise(function (resolve) { + if (!pl_name) return resolve(false); + frappe.db.get_value("Price List", pl_name, "enabled", function (r) { + resolve(!!(r && r.enabled)); + }); + }); +} + +/** + * Auto-select the selling price list. + * + * Priority: + * 1. Cost-center-specific Price List (if cost_center is set on the invoice) + * 2. Customer default_price_list (if set and enabled) + * 3. Standard Selling (global fallback) + */ +function auto_set_cost_center_price_list(frm) { + if (!frm.doc.customer) return; + + resolve_cost_center(frm).then(function (cost_center) { + if (cost_center) { + get_cost_center_price_list(cost_center).then(function (cc_pl) { + if (cc_pl) { + frm.set_value("selling_price_list", cc_pl); + } else { + set_from_customer_or_fallback(frm); + } + }); + } else { + set_from_customer_or_fallback(frm); + } + }); +} + +function set_from_customer_or_fallback(frm) { + frappe.db.get_value("Customer", frm.doc.customer, "default_price_list", function (r) { + var cust_pl = r && r.default_price_list ? r.default_price_list : null; + is_price_list_enabled(cust_pl).then(function (ok) { + if (ok) { + frm.set_value("selling_price_list", cust_pl); + } else { + is_price_list_enabled("Standard Selling").then(function (std_ok) { + if (std_ok) frm.set_value("selling_price_list", "Standard Selling"); + }); + } + }); + }); +} + +function resolve_cost_center(frm) { + if (frm.doc.cost_center) { + return Promise.resolve(frm.doc.cost_center); + } + // Fallback: user's default cost center from User Permission + return new Promise(function (resolve) { + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "User Permission", + filters: { + user: frappe.session.user, + allow: "Cost Center", + is_default: 1, + }, + fields: ["for_value"], + limit: 1, + }, + callback: function (r) { + var val = (r.message && r.message.length) ? r.message[0].for_value : null; + resolve(val); + }, + }); + }); +} diff --git a/aqrar_ext/public/js/sales_invoice_item_display.js b/aqrar_ext/public/js/sales_invoice_item_display.js new file mode 100644 index 0000000..7bbb7bf --- /dev/null +++ b/aqrar_ext/public/js/sales_invoice_item_display.js @@ -0,0 +1,40 @@ +frappe.ui.form.on("Sales Invoice", { + refresh: function (frm) { + frm.trigger("apply_item_display_mode"); + }, + apply_item_display_mode: function (frm) { + frappe.call({ + method: "frappe.client.get_value", + args: { + doctype: "Aqrar Settings", + fieldname: "item_display_mode", + }, + callback: function (r) { + var mode = (r && r.message) ? r.message.item_display_mode : "Item Name + Description"; + update_grid_columns(frm, mode); + }, + }); + }, +}); + +function update_grid_columns(frm, mode) { + var grid = frm.fields_dict.items.grid; + + if (mode === "Item Code") { + grid.set_column_disp("item_code", true); + grid.set_column_disp("item_name", false); + grid.set_column_disp("description", false); + } else if (mode === "Item Name") { + grid.set_column_disp("item_code", false); + grid.set_column_disp("item_name", true); + grid.set_column_disp("description", false); + } else if (mode === "Item Code + Description") { + grid.set_column_disp("item_code", true); + grid.set_column_disp("item_name", false); + grid.set_column_disp("description", true); + } else { + grid.set_column_disp("item_code", false); + grid.set_column_disp("item_name", true); + grid.set_column_disp("description", true); + } +} diff --git a/aqrar_ext/public/js/sales_invoice_navigation.js b/aqrar_ext/public/js/sales_invoice_navigation.js new file mode 100644 index 0000000..bc6e63a --- /dev/null +++ b/aqrar_ext/public/js/sales_invoice_navigation.js @@ -0,0 +1,66 @@ +frappe.ui.form.on("Sales Invoice", { + refresh: function (frm) { + if (frm.doc.__islocal) return; + + frm.page.add_inner_button(__("Prev"), function () { + _navigate(frm, "prev"); + }); + + frm.page.add_inner_button(__("Next"), function () { + _navigate(frm, "next"); + }); + + _bind_keys(frm); + }, +}); + +function _bind_keys(frm) { + $(document).off("keydown.si_nav").on("keydown.si_nav", function (e) { + if (frappe.get_route_str() !== "Form/Sales Invoice/" + frm.doc.name) return; + if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA" || e.target.tagName === "SELECT") return; + if (e.key === "ArrowLeft" && !e.altKey && !e.ctrlKey) { + e.preventDefault(); + _navigate(frm, "prev"); + } + if (e.key === "ArrowRight" && !e.altKey && !e.ctrlKey) { + e.preventDefault(); + _navigate(frm, "next"); + } + }); +} + +function _navigate(frm, direction) { + var ctx = _get_list_context(); + + frappe.call({ + method: "aqrar_ext.api.navigation.get_sibling", + args: { + doctype: frm.doc.doctype, + docname: frm.doc.name, + direction: direction, + list_filters: ctx.filters, + order_by: ctx.order_by, + }, + callback: function (r) { + if (r.message) { + frappe.set_route("Form", frm.doc.doctype, r.message); + } else { + frappe.show_alert({ + message: direction === "next" ? __("No next document") : __("No previous document"), + indicator: "blue", + }); + } + }, + }); +} + +function _get_list_context() { + try { + var raw = localStorage.getItem("Sales Invoice_list_view"); + if (raw) { + var state = JSON.parse(raw); + return { filters: state.filters || [], order_by: state.sort_by || null }; + } + } catch (e) {} + return { filters: [], order_by: null }; +} diff --git a/aqrar_ext/public/js/sales_invoice_payment_terms.js b/aqrar_ext/public/js/sales_invoice_payment_terms.js new file mode 100644 index 0000000..1b890a0 --- /dev/null +++ b/aqrar_ext/public/js/sales_invoice_payment_terms.js @@ -0,0 +1,10 @@ +frappe.ui.form.on("Sales Invoice", { + customer: function (frm) { + if (!frm.doc.customer || frm.doc.ignore_default_payment_terms_template) return; + frappe.db.get_value("Customer", frm.doc.customer, "payment_terms", function (r) { + if (r && r.payment_terms) { + frm.set_value("payment_terms_template", r.payment_terms); + } + }); + }, +}); diff --git a/aqrar_ext/public/js/sales_invoice_return.js b/aqrar_ext/public/js/sales_invoice_return.js new file mode 100644 index 0000000..2b9255b --- /dev/null +++ b/aqrar_ext/public/js/sales_invoice_return.js @@ -0,0 +1,32 @@ +frappe.ui.form.on("Sales Invoice", { + onload(frm) { + handle_return_invoice(frm); + }, + refresh(frm) { + handle_return_invoice(frm); + }, + validate(frm) { + if (!frm.doc.is_return) return; + (frm.doc.items || []).forEach(row => { + if (row.qty > 0) { + row.qty = -Math.abs(row.qty); + } + }); + } +}); + + +function handle_return_invoice(frm) { + if (!frm.doc.is_return) return; + let changed = false; + (frm.doc.items || []).forEach(row => { + if (row.qty < 0) { + + row.qty = Math.abs(row.qty); + changed = true; + } + }); + if (changed) { + frm.refresh_field("items"); + } +} \ No newline at end of file diff --git a/aqrar_ext/setup_data.py b/aqrar_ext/setup_data.py new file mode 100644 index 0000000..bd6f4b1 --- /dev/null +++ b/aqrar_ext/setup_data.py @@ -0,0 +1,120 @@ +"""Setup / fixture utilities — called from after_migrate hooks.""" + +import frappe +from frappe import _ +from frappe.custom.doctype.custom_field.custom_field import create_custom_field + + +def create(): + """Called on every migrate. Handles conditional setup.""" + install_expense_claim_workflow() + + +def install_expense_claim_workflow(): + """Install Expense Claim workflow + custom field only if HRMS is available. + + Expense Claim doctype lives in the HRMS app, which may not be installed + on all sites. This avoids fixture sync errors when HRMS is absent. + """ + if not frappe.db.exists("DocType", "Expense Claim"): + return + + # ── Custom field: workflow_state on Expense Claim ────────────────── + if not frappe.db.exists("Custom Field", "Expense Claim-workflow_state"): + create_custom_field("Expense Claim", { + "fieldname": "workflow_state", + "fieldtype": "Link", + "label": "Workflow State", + "options": "Workflow State", + "insert_after": "expense_approver", + "read_only": 1, + "print_hide": 1, + "module": "Aqrar Ext", + "description": "Current approval state for the Aqrar Expense Claim approval workflow.", + }) + frappe.db.commit() + + # ── Workflow: Expense Claim Approval ─────────────────────────────── + if frappe.db.exists("Workflow", "Expense Claim Approval"): + return + + # Ensure prerequisite states and actions exist + for state in ("Draft", "Pending Approval", "Approved", "Rejected"): + if not frappe.db.exists("Workflow State", state): + frappe.get_doc({"doctype": "Workflow State", "workflow_state_name": state}).insert( + ignore_permissions=True + ) + + for action in ("Submit for Approval", "Approve", "Reject"): + if not frappe.db.exists("Workflow Action Master", action): + frappe.get_doc({ + "doctype": "Workflow Action Master", + "workflow_action_name": action, + }).insert(ignore_permissions=True) + + wf = frappe.get_doc({ + "doctype": "Workflow", + "name": "Expense Claim Approval", + "workflow_name": "Expense Claim Approval", + "document_type": "Expense Claim", + "workflow_state_field": "workflow_state", + "is_active": 1, + "send_email_alert": 1, + "states": [ + { + "state": "Draft", + "doc_status": "0", + "allow_edit": "Accounts User", + "update_field": "workflow_state", + "update_value": "Draft", + }, + { + "state": "Pending Approval", + "doc_status": "0", + "allow_edit": "Branch Accountant", + "update_field": "workflow_state", + "update_value": "Pending Approval", + "message": "Expense Claim requires your approval. Please review and approve or reject.", + }, + { + "state": "Approved", + "doc_status": "1", + "allow_edit": "Accounts Manager", + "update_field": "workflow_state", + "update_value": "Approved", + "message": "Expense Claim has been approved.", + }, + { + "state": "Rejected", + "doc_status": "1", + "allow_edit": "Accounts User", + "is_optional_state": 1, + "update_field": "workflow_state", + "update_value": "Rejected", + "message": "Expense Claim has been rejected.", + }, + ], + "transitions": [ + { + "state": "Draft", + "action": "Submit for Approval", + "next_state": "Pending Approval", + "allowed": "Accounts User", + }, + { + "state": "Pending Approval", + "action": "Approve", + "next_state": "Approved", + "allowed": "Branch Accountant", + }, + { + "state": "Pending Approval", + "action": "Reject", + "next_state": "Rejected", + "allowed": "Branch Accountant", + }, + ], + }) + wf.insert(ignore_permissions=True) + frappe.db.commit() + print(f"[aqrar_ext] Installed Expense Claim Approval workflow") From 059677d43c0cf4f812a2eede31224161fd8e13d6 Mon Sep 17 00:00:00 2001 From: Neha Fathima Date: Wed, 3 Jun 2026 11:01:52 +0530 Subject: [PATCH 2/2] fix: resolve merge conflicts in fixture JSON files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kept all objects from both branches — Material Request Approval workflow (HEAD) alongside Stock Transfer, Journal Entry, and Payment Entry Approval workflows (feat:aqrar implmentation), and all custom fields from both sides including last_price fields and workflow state fields. Co-Authored-By: Claude Sonnet 4.6 --- aqrar_ext/fixtures/custom_field.json | 736 ++++++++++++++++----------- aqrar_ext/fixtures/workflow.json | 196 +++---- 2 files changed, 558 insertions(+), 374 deletions(-) diff --git a/aqrar_ext/fixtures/custom_field.json b/aqrar_ext/fixtures/custom_field.json index 75884c9..25b782b 100644 --- a/aqrar_ext/fixtures/custom_field.json +++ b/aqrar_ext/fixtures/custom_field.json @@ -339,17 +339,12 @@ }, { "allow_in_quick_entry": 0, -<<<<<<< HEAD "allow_on_submit": 1, -======= - "allow_on_submit": 0, ->>>>>>> f2e4231 (feat:aqrar implmentation) "bold": 0, "collapsible": 0, "collapsible_depends_on": null, "columns": 0, "default": null, -<<<<<<< HEAD "depends_on": "eval:doc.custom_payment_mode === 'Card' || doc.custom_payment_mode === 'Credit'", "description": null, "docstatus": 0, @@ -401,24 +396,17 @@ }, { "allow_in_quick_entry": 0, - "allow_on_submit": 1, + "allow_on_submit": 0, "bold": 0, "collapsible": 0, "collapsible_depends_on": null, - "columns": 1, + "columns": 0, "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Material Request", -======= "depends_on": null, "description": "Current approval state for the Aqrar Stock Transfer approval workflow.", "docstatus": 0, "doctype": "Custom Field", "dt": "Stock Entry", ->>>>>>> f2e4231 (feat:aqrar implmentation) "fetch_from": null, "fetch_if_empty": 0, "fieldname": "workflow_state", @@ -430,31 +418,18 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_global_search": 0, -<<<<<<< HEAD - "in_list_view": 1, - "in_preview": 0, - "in_standard_filter": 1, - "insert_after": "status", -======= "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, "insert_after": "stock_entry_type", ->>>>>>> f2e4231 (feat:aqrar implmentation) "is_system_generated": 0, "is_virtual": 0, "label": "Workflow State", "length": 0, "link_filters": null, "mandatory_depends_on": null, -<<<<<<< HEAD - "modified": "2026-05-17 00:00:00", - "module": null, - "name": "Material Request-workflow_state", -======= "module": "Aqrar Ext", "name": "Stock Entry-workflow_state", ->>>>>>> f2e4231 (feat:aqrar implmentation) "no_copy": 0, "non_negative": 0, "options": "Workflow State", @@ -471,11 +446,7 @@ "search_index": 0, "show_dashboard": 0, "sort_options": 0, -<<<<<<< HEAD - "translatable": 0, -======= "translatable": 1, ->>>>>>> f2e4231 (feat:aqrar implmentation) "unique": 0, "width": null }, @@ -488,16 +459,6 @@ "columns": 0, "default": null, "depends_on": null, -<<<<<<< HEAD - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice Item", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "custom_last_price", - "fieldtype": "Currency", -======= "description": "Current approval state for the Aqrar Journal Entry approval workflow.", "docstatus": 0, "doctype": "Custom Field", @@ -506,7 +467,6 @@ "fetch_if_empty": 0, "fieldname": "workflow_state", "fieldtype": "Link", ->>>>>>> f2e4231 (feat:aqrar implmentation) "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -514,28 +474,6 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_global_search": 0, -<<<<<<< HEAD - "in_list_view": 1, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "rate", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Last Price", - "length": 0, - "link_filters": null, - "mandatory_depends_on": null, - "modified": "2026-05-19 16:12:16.316135", - "module": null, - "name": "Sales Invoice Item-custom_last_price", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "placeholder": null, - "precision": "", - "print_hide": 0, -======= "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, @@ -555,7 +493,6 @@ "placeholder": null, "precision": "", "print_hide": 1, ->>>>>>> f2e4231 (feat:aqrar implmentation) "print_hide_if_no_value": 0, "print_width": null, "read_only": 1, @@ -565,11 +502,7 @@ "search_index": 0, "show_dashboard": 0, "sort_options": 0, -<<<<<<< HEAD - "translatable": 0, -======= "translatable": 1, ->>>>>>> f2e4231 (feat:aqrar implmentation) "unique": 0, "width": null }, @@ -582,16 +515,6 @@ "columns": 0, "default": null, "depends_on": null, -<<<<<<< HEAD - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Purchase Invoice Item", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "custom_last_price", - "fieldtype": "Currency", -======= "description": "Current approval state for the Aqrar Payment Entry approval workflow.", "docstatus": 0, "doctype": "Custom Field", @@ -600,7 +523,6 @@ "fetch_if_empty": 0, "fieldname": "workflow_state", "fieldtype": "Link", ->>>>>>> f2e4231 (feat:aqrar implmentation) "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -608,40 +530,75 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_global_search": 0, -<<<<<<< HEAD - "in_list_view": 1, + "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "rate", + "insert_after": "payment_type", "is_system_generated": 0, "is_virtual": 0, - "label": "Last Price", + "label": "Workflow State", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2026-05-19 16:13:25.722972", - "module": null, - "name": "Purchase Invoice Item-custom_last_price", + "module": "Aqrar Ext", + "name": "Payment Entry-workflow_state", "no_copy": 0, "non_negative": 0, - "options": null, + "options": "Workflow State", "permlevel": 0, "placeholder": null, "precision": "", - "print_hide": 0, -======= - "in_list_view": 0, + "print_hide": 1, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 1, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "show_dashboard": 0, + "sort_options": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 1, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Material Request", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "workflow_state", + "fieldtype": "Link", + "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": 1, "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "payment_type", + "in_standard_filter": 1, + "insert_after": "status", "is_system_generated": 0, "is_virtual": 0, "label": "Workflow State", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "module": "Aqrar Ext", - "name": "Payment Entry-workflow_state", + "modified": "2026-05-17 00:00:00", + "module": null, + "name": "Material Request-workflow_state", "no_copy": 0, "non_negative": 0, "options": "Workflow State", @@ -649,7 +606,6 @@ "placeholder": null, "precision": "", "print_hide": 1, ->>>>>>> f2e4231 (feat:aqrar implmentation) "print_hide_if_no_value": 0, "print_width": null, "read_only": 1, @@ -659,11 +615,7 @@ "search_index": 0, "show_dashboard": 0, "sort_options": 0, -<<<<<<< HEAD "translatable": 0, -======= - "translatable": 1, ->>>>>>> f2e4231 (feat:aqrar implmentation) "unique": 0, "width": null }, @@ -676,16 +628,6 @@ "columns": 0, "default": null, "depends_on": null, -<<<<<<< HEAD - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Delivery Note Item", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "custom_last_price", - "fieldtype": "Currency", -======= "description": "Default naming series applied when creating Items under this Item Group.", "docstatus": 0, "doctype": "Custom Field", @@ -694,7 +636,6 @@ "fetch_if_empty": 0, "fieldname": "custom_default_item_naming_series", "fieldtype": "Select", ->>>>>>> f2e4231 (feat:aqrar implmentation) "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -702,21 +643,6 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_global_search": 0, -<<<<<<< HEAD - "in_list_view": 1, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "rate", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Last Price", - "length": 0, - "link_filters": null, - "mandatory_depends_on": null, - "modified": "2026-05-19 16:12:31.457961", - "module": null, - "name": "Delivery Note Item-custom_last_price", -======= "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, @@ -785,7 +711,6 @@ "mandatory_depends_on": null, "module": "Aqrar Ext", "name": "User-custom_enable_sound_alerts", ->>>>>>> f2e4231 (feat:aqrar implmentation) "no_copy": 0, "non_negative": 0, "options": null, @@ -795,22 +720,14 @@ "print_hide": 0, "print_hide_if_no_value": 0, "print_width": null, -<<<<<<< HEAD - "read_only": 1, -======= "read_only": 0, ->>>>>>> f2e4231 (feat:aqrar implmentation) "read_only_depends_on": null, "report_hide": 0, "reqd": 0, "search_index": 0, "show_dashboard": 0, "sort_options": 0, -<<<<<<< HEAD - "translatable": 0, -======= "translatable": 1, ->>>>>>> f2e4231 (feat:aqrar implmentation) "unique": 0, "width": null }, @@ -823,16 +740,6 @@ "columns": 0, "default": null, "depends_on": null, -<<<<<<< HEAD - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Order Item", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "custom_last_price", - "fieldtype": "Currency", -======= "description": "Link to the source Sales Invoice this commission Journal Entry relates to.", "docstatus": 0, "doctype": "Custom Field", @@ -841,7 +748,6 @@ "fetch_if_empty": 0, "fieldname": "custom_reference_invoice", "fieldtype": "Link", ->>>>>>> f2e4231 (feat:aqrar implmentation) "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -851,22 +757,6 @@ "in_global_search": 0, "in_list_view": 1, "in_preview": 0, -<<<<<<< HEAD - "in_standard_filter": 0, - "insert_after": "rate", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Last Price", - "length": 0, - "link_filters": null, - "mandatory_depends_on": null, - "modified": "2026-05-19 16:12:46.012588", - "module": null, - "name": "Sales Order Item-custom_last_price", - "no_copy": 0, - "non_negative": 0, - "options": null, -======= "in_standard_filter": 1, "insert_after": "cheque_no", "is_system_generated": 0, @@ -880,29 +770,20 @@ "no_copy": 0, "non_negative": 0, "options": "Sales Invoice", ->>>>>>> f2e4231 (feat:aqrar implmentation) "permlevel": 0, "placeholder": null, "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "print_width": null, -<<<<<<< HEAD - "read_only": 1, -======= "read_only": 0, ->>>>>>> f2e4231 (feat:aqrar implmentation) "read_only_depends_on": null, "report_hide": 0, "reqd": 0, "search_index": 0, "show_dashboard": 0, "sort_options": 0, -<<<<<<< HEAD - "translatable": 0, -======= "translatable": 1, ->>>>>>> f2e4231 (feat:aqrar implmentation) "unique": 0, "width": null }, @@ -915,16 +796,6 @@ "columns": 0, "default": null, "depends_on": null, -<<<<<<< HEAD - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Quotation Item", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "custom_last_price", - "fieldtype": "Currency", -======= "description": "Default expense account for commission journal entries.", "docstatus": 0, "doctype": "Custom Field", @@ -933,7 +804,6 @@ "fetch_if_empty": 0, "fieldname": "default_commission_expense_account", "fieldtype": "Link", ->>>>>>> f2e4231 (feat:aqrar implmentation) "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -941,24 +811,6 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_global_search": 0, -<<<<<<< HEAD - "in_list_view": 1, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "rate", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Last Price", - "length": 0, - "link_filters": null, - "mandatory_depends_on": null, - "modified": "2026-05-19 16:13:13.434131", - "module": null, - "name": "Quotation Item-custom_last_price", - "no_copy": 0, - "non_negative": 0, - "options": null, -======= "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, @@ -974,29 +826,20 @@ "no_copy": 0, "non_negative": 0, "options": "Account", ->>>>>>> f2e4231 (feat:aqrar implmentation) "permlevel": 0, "placeholder": null, "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "print_width": null, -<<<<<<< HEAD - "read_only": 1, -======= "read_only": 0, ->>>>>>> f2e4231 (feat:aqrar implmentation) "read_only_depends_on": null, "report_hide": 0, "reqd": 0, "search_index": 0, "show_dashboard": 0, "sort_options": 0, -<<<<<<< HEAD - "translatable": 0, -======= "translatable": 1, ->>>>>>> f2e4231 (feat:aqrar implmentation) "unique": 0, "width": null }, @@ -1009,16 +852,6 @@ "columns": 0, "default": null, "depends_on": null, -<<<<<<< HEAD - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Purchase Order Item", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "custom_last_price", - "fieldtype": "Currency", -======= "description": "Default payable account for commission journal entries.", "docstatus": 0, "doctype": "Custom Field", @@ -1027,7 +860,6 @@ "fetch_if_empty": 0, "fieldname": "default_commission_payable_account", "fieldtype": "Link", ->>>>>>> f2e4231 (feat:aqrar implmentation) "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -1035,24 +867,6 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_global_search": 0, -<<<<<<< HEAD - "in_list_view": 1, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "rate", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Last Price", - "length": 0, - "link_filters": null, - "mandatory_depends_on": null, - "modified": "2026-05-19 16:13:42.476090", - "module": null, - "name": "Purchase Order Item-custom_last_price", - "no_copy": 0, - "non_negative": 0, - "options": null, -======= "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, @@ -1068,29 +882,20 @@ "no_copy": 0, "non_negative": 0, "options": "Account", ->>>>>>> f2e4231 (feat:aqrar implmentation) "permlevel": 0, "placeholder": null, "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "print_width": null, -<<<<<<< HEAD - "read_only": 1, -======= "read_only": 0, ->>>>>>> f2e4231 (feat:aqrar implmentation) "read_only_depends_on": null, "report_hide": 0, "reqd": 0, "search_index": 0, "show_dashboard": 0, "sort_options": 0, -<<<<<<< HEAD - "translatable": 0, -======= "translatable": 1, ->>>>>>> f2e4231 (feat:aqrar implmentation) "unique": 0, "width": null }, @@ -1103,16 +908,6 @@ "columns": 0, "default": null, "depends_on": null, -<<<<<<< HEAD - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Purchase Receipt Item", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "custom_last_price", - "fieldtype": "Currency", -======= "description": "Default expense account for discount day-close journal entries.", "docstatus": 0, "doctype": "Custom Field", @@ -1121,7 +916,6 @@ "fetch_if_empty": 0, "fieldname": "default_discount_expense_account", "fieldtype": "Link", ->>>>>>> f2e4231 (feat:aqrar implmentation) "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -1129,24 +923,6 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_global_search": 0, -<<<<<<< HEAD - "in_list_view": 1, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "rate", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Last Price", - "length": 0, - "link_filters": null, - "mandatory_depends_on": null, - "modified": "2026-05-19 16:14:07.152340", - "module": null, - "name": "Purchase Receipt Item-custom_last_price", - "no_copy": 0, - "non_negative": 0, - "options": null, -======= "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, @@ -1162,27 +938,19 @@ "no_copy": 0, "non_negative": 0, "options": "Account", ->>>>>>> f2e4231 (feat:aqrar implmentation) "permlevel": 0, "placeholder": null, "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "print_width": null, -<<<<<<< HEAD - "read_only": 1, -======= "read_only": 0, ->>>>>>> f2e4231 (feat:aqrar implmentation) "read_only_depends_on": null, "report_hide": 0, "reqd": 0, "search_index": 0, "show_dashboard": 0, "sort_options": 0, -<<<<<<< HEAD - "translatable": 0, -======= "translatable": 1, "unique": 0, "width": null @@ -1240,7 +1008,405 @@ "show_dashboard": 0, "sort_options": 0, "translatable": 1, ->>>>>>> f2e4231 (feat:aqrar implmentation) + "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": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice Item", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_last_price", + "fieldtype": "Currency", + "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": 1, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "rate", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Last Price", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2026-05-19 16:12:16.316135", + "module": null, + "name": "Sales Invoice Item-custom_last_price", + "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": 1, + "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": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Purchase Invoice Item", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_last_price", + "fieldtype": "Currency", + "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": 1, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "rate", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Last Price", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2026-05-19 16:13:25.722972", + "module": null, + "name": "Purchase Invoice Item-custom_last_price", + "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": 1, + "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": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Delivery Note Item", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_last_price", + "fieldtype": "Currency", + "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": 1, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "rate", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Last Price", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2026-05-19 16:12:31.457961", + "module": null, + "name": "Delivery Note Item-custom_last_price", + "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": 1, + "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": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Order Item", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_last_price", + "fieldtype": "Currency", + "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": 1, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "rate", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Last Price", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2026-05-19 16:12:46.012588", + "module": null, + "name": "Sales Order Item-custom_last_price", + "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": 1, + "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": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Quotation Item", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_last_price", + "fieldtype": "Currency", + "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": 1, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "rate", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Last Price", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2026-05-19 16:13:13.434131", + "module": null, + "name": "Quotation Item-custom_last_price", + "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": 1, + "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": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Purchase Order Item", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_last_price", + "fieldtype": "Currency", + "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": 1, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "rate", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Last Price", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2026-05-19 16:13:42.476090", + "module": null, + "name": "Purchase Order Item-custom_last_price", + "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": 1, + "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": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Purchase Receipt Item", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_last_price", + "fieldtype": "Currency", + "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": 1, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "rate", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Last Price", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2026-05-19 16:14:07.152340", + "module": null, + "name": "Purchase Receipt Item-custom_last_price", + "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": 1, + "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 } diff --git a/aqrar_ext/fixtures/workflow.json b/aqrar_ext/fixtures/workflow.json index 81a2600..cf4f294 100644 --- a/aqrar_ext/fixtures/workflow.json +++ b/aqrar_ext/fixtures/workflow.json @@ -2,7 +2,6 @@ { "docstatus": 0, "doctype": "Workflow", -<<<<<<< HEAD "document_type": "Material Request", "is_active": 1, "modified": "2026-05-19 16:15:59.237356", @@ -18,48 +17,12 @@ "message": "Waiting for Branch User approval", "next_action_email_template": null, "parent": "Material Request Approval", -======= - "document_type": "Stock Entry", - "is_active": 1, - "name": "Stock Transfer Approval", - "override_status": 0, - "send_email_alert": 1, - "states": [ - { - "allow_edit": "Stock User", - "avoid_status_override": 0, - "doc_status": "0", - "is_optional_state": 0, - "message": "", - "next_action_email_template": "", - "parent": "Stock Transfer Approval", - "parentfield": "states", - "parenttype": "Workflow", - "send_email": 0, - "state": "Draft", - "update_field": "workflow_state", - "update_value": "Draft", - "workflow_builder_id": null - }, - { - "allow_edit": "Branch Approver", - "avoid_status_override": 0, - "doc_status": "0", - "is_optional_state": 0, - "message": "Stock Transfer requires your approval. Please review and approve or reject.", - "next_action_email_template": "", - "parent": "Stock Transfer Approval", ->>>>>>> f2e4231 (feat:aqrar implmentation) "parentfield": "states", "parenttype": "Workflow", "send_email": 0, "state": "Pending Approval", "update_field": "workflow_state", -<<<<<<< HEAD "update_value": null, -======= - "update_value": "Pending Approval", ->>>>>>> f2e4231 (feat:aqrar implmentation) "workflow_builder_id": null }, { @@ -67,7 +30,6 @@ "avoid_status_override": 0, "doc_status": "1", "is_optional_state": 0, -<<<<<<< HEAD "message": "Approved - Ready for Transfer", "next_action_email_template": null, "parent": "Material Request Approval", @@ -109,7 +71,113 @@ "state": "Closed", "update_field": "workflow_state", "update_value": null, -======= + "workflow_builder_id": null + } + ], + "transitions": [ + { + "action": "Approve", + "allow_self_approval": 0, + "allowed": "Branch User", + "condition": "frappe.session.user != doc.owner", + "next_state": "Approved", + "parent": "Material Request Approval", + "parentfield": "transitions", + "parenttype": "Workflow", + "send_email_to_creator": 0, + "state": "Pending Approval", + "workflow_builder_id": null + }, + { + "action": "Reject", + "allow_self_approval": 0, + "allowed": "Branch User", + "condition": null, + "next_state": "Closed", + "parent": "Material Request Approval", + "parentfield": "transitions", + "parenttype": "Workflow", + "send_email_to_creator": 0, + "state": "Pending Approval", + "workflow_builder_id": null + }, + { + "action": "Cancel", + "allow_self_approval": 0, + "allowed": "Branch User", + "condition": null, + "next_state": "Closed", + "parent": "Material Request Approval", + "parentfield": "transitions", + "parenttype": "Workflow", + "send_email_to_creator": 0, + "state": "Approved", + "workflow_builder_id": null + }, + { + "action": "Transfer", + "allow_self_approval": 1, + "allowed": "Stock Manager", + "condition": "frappe.db.get_value('Material Request', doc.name, 'per_ordered') == 100", + "next_state": "Transferred", + "parent": "Material Request Approval", + "parentfield": "transitions", + "parenttype": "Workflow", + "send_email_to_creator": 0, + "state": "Approved", + "workflow_builder_id": null + } + ], + "workflow_data": null, + "workflow_name": "Material Request Approval", + "workflow_state_field": "workflow_state" + }, + { + "docstatus": 0, + "doctype": "Workflow", + "document_type": "Stock Entry", + "is_active": 1, + "name": "Stock Transfer Approval", + "override_status": 0, + "send_email_alert": 1, + "states": [ + { + "allow_edit": "Stock User", + "avoid_status_override": 0, + "doc_status": "0", + "is_optional_state": 0, + "message": "", + "next_action_email_template": "", + "parent": "Stock Transfer Approval", + "parentfield": "states", + "parenttype": "Workflow", + "send_email": 0, + "state": "Draft", + "update_field": "workflow_state", + "update_value": "Draft", + "workflow_builder_id": null + }, + { + "allow_edit": "Branch Approver", + "avoid_status_override": 0, + "doc_status": "0", + "is_optional_state": 0, + "message": "Stock Transfer requires your approval. Please review and approve or reject.", + "next_action_email_template": "", + "parent": "Stock Transfer Approval", + "parentfield": "states", + "parenttype": "Workflow", + "send_email": 0, + "state": "Pending Approval", + "update_field": "workflow_state", + "update_value": "Pending Approval", + "workflow_builder_id": null + }, + { + "allow_edit": "Stock Manager", + "avoid_status_override": 0, + "doc_status": "1", + "is_optional_state": 0, "message": "Stock Transfer has been approved and committed.", "next_action_email_template": "", "parent": "Stock Transfer Approval", @@ -135,20 +203,11 @@ "state": "Rejected", "update_field": "workflow_state", "update_value": "Rejected", ->>>>>>> f2e4231 (feat:aqrar implmentation) "workflow_builder_id": null } ], "transitions": [ { -<<<<<<< HEAD - "action": "Approve", - "allow_self_approval": 0, - "allowed": "Branch User", - "condition": "frappe.session.user != doc.owner", - "next_state": "Approved", - "parent": "Material Request Approval", -======= "action": "Submit for Approval", "allow_self_approval": 0, "allowed": "Stock User", @@ -168,7 +227,6 @@ "condition": null, "next_state": "Committed", "parent": "Stock Transfer Approval", ->>>>>>> f2e4231 (feat:aqrar implmentation) "parentfield": "transitions", "parenttype": "Workflow", "send_email_to_creator": 0, @@ -178,12 +236,6 @@ { "action": "Reject", "allow_self_approval": 0, -<<<<<<< HEAD - "allowed": "Branch User", - "condition": null, - "next_state": "Closed", - "parent": "Material Request Approval", -======= "allowed": "Branch Approver", "condition": null, "next_state": "Rejected", @@ -294,7 +346,6 @@ "condition": null, "next_state": "Posted", "parent": "Journal Entry Approval", ->>>>>>> f2e4231 (feat:aqrar implmentation) "parentfield": "transitions", "parenttype": "Workflow", "send_email_to_creator": 0, @@ -302,31 +353,6 @@ "workflow_builder_id": null }, { -<<<<<<< HEAD - "action": "Cancel", - "allow_self_approval": 0, - "allowed": "Branch User", - "condition": null, - "next_state": "Closed", - "parent": "Material Request Approval", - "parentfield": "transitions", - "parenttype": "Workflow", - "send_email_to_creator": 0, - "state": "Approved", - "workflow_builder_id": null - }, - { - "action": "Transfer", - "allow_self_approval": 1, - "allowed": "Stock Manager", - "condition": "frappe.db.get_value('Material Request', doc.name, 'per_ordered') == 100", - "next_state": "Transferred", - "parent": "Material Request Approval", - "parentfield": "transitions", - "parenttype": "Workflow", - "send_email_to_creator": 0, - "state": "Approved", -======= "action": "Reject", "allow_self_approval": 0, "allowed": "Branch Accountant", @@ -337,17 +363,10 @@ "parenttype": "Workflow", "send_email_to_creator": 0, "state": "Pending Approval", ->>>>>>> f2e4231 (feat:aqrar implmentation) "workflow_builder_id": null } ], "workflow_data": null, -<<<<<<< HEAD - "workflow_name": "Material Request Approval", - "workflow_state_field": "workflow_state" - } -] -======= "workflow_name": "Journal Entry Approval", "workflow_state_field": "workflow_state" }, @@ -471,4 +490,3 @@ "workflow_state_field": "workflow_state" } ] ->>>>>>> f2e4231 (feat:aqrar implmentation)