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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion aqrar_ext/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,27 @@
from .price_history import get_last_sold_price, get_item_insights, get_item_price_history
# import frappe

# @frappe.whitelist()
# def get_item_uoms(item_code):
# try:
# item = frappe.get_doc("Item", item_code)
# uoms = [item.stock_uom]
# for u in item.uoms:
# if u.uom not in uoms:
# uoms.append(u.uom)
# return uoms
# except Exception:
# return []

import frappe

@frappe.whitelist()
def get_item_uoms(item_code):
try:
item = frappe.get_doc("Item", item_code)
uoms = [item.stock_uom]
for u in item.uoms:
if u.uom not in uoms:
uoms.append(u.uom)
return uoms
except Exception:
return []
151 changes: 151 additions & 0 deletions aqrar_ext/api/sales_invoice_payment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import json

import frappe
from frappe import _


@frappe.whitelist()
def get_payment_modes_with_account(company: str, mode_list: str | list = None):
"""
Return Mode of Payment names that are enabled and have a default Cash/Bank
account for the given company.
"""
if not company:
return []

if isinstance(mode_list, str):
try:
mode_list = json.loads(mode_list) if mode_list else None
except Exception:
mode_list = None

has_account = frappe.db.sql(
"""
SELECT DISTINCT parent
FROM `tabMode of Payment Account`
WHERE company = %s AND default_account IS NOT NULL AND default_account != ''
""",
(company,),
as_list=True,
)
modes_with_account = {r[0] for r in has_account}

if mode_list is not None:
names = [
m if isinstance(m, str) else (m.get("name") or m.get("mode_of_payment"))
for m in (mode_list or [])
]
names = [n for n in names if n]
if not names:
return []
enabled = frappe.get_all(
"Mode of Payment",
filters={"name": ["in", names], "enabled": 1},
pluck="name",
)
else:
enabled = frappe.get_all(
"Mode of Payment",
filters={"enabled": 1},
pluck="name",
)

valid = [m for m in enabled if m in modes_with_account]
if mode_list is not None and names:
order = {m: i for i, m in enumerate(names)}
valid.sort(key=lambda m: order.get(m, 999))
return valid


@frappe.whitelist()
def create_pos_payments_for_invoice(sales_invoice: str, payments: str | list):
"""
Create Payment Entry records for a submitted Sales Invoice, one per mode of payment.

payments: JSON list or Python list of dicts:
[{ "mode_of_payment": "Cash", "amount": 100.0 }, ...]
"""
if not sales_invoice:
frappe.throw(_("Sales Invoice is required"))

si = frappe.get_doc("Sales Invoice", sales_invoice)
if si.docstatus != 1:
frappe.throw(
_("Sales Invoice {0} must be submitted before creating payments.").format(si.name)
)

if isinstance(payments, str):
try:
payments = json.loads(payments)
except Exception:
frappe.throw(_("Invalid payments payload"))

if not isinstance(payments, (list, tuple)) or not payments:
frappe.throw(_("No payment rows were provided."))

valid_rows: list[dict] = []
for row in payments:
mode_of_payment = (row or {}).get("mode_of_payment")
amount = frappe.utils.flt((row or {}).get("amount"))
if not mode_of_payment or amount <= 0:
continue
valid_rows.append({"mode_of_payment": mode_of_payment, "amount": amount})

if not valid_rows:
frappe.throw(
_("No valid payment rows found (non-zero amounts with mode of payment).")
)

from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account

created: list[str] = []

for row in valid_rows:
si.reload()
outstanding = frappe.utils.flt(si.outstanding_amount)
amount = frappe.utils.flt(row["amount"])

if amount - outstanding > 0.5:
frappe.throw(
_(
"Payment amount {0} is greater than outstanding amount {1} for invoice {2}."
).format(amount, outstanding, si.name)
)

pe = get_payment_entry("Sales Invoice", si.name)
pe.mode_of_payment = row["mode_of_payment"]

bank_cash = get_bank_cash_account(row["mode_of_payment"], si.company)
pe.paid_to = bank_cash.get("account")

if pe.paid_to:
acc = frappe.get_cached_value(
"Account", pe.paid_to, ["account_currency", "account_type"], as_dict=True
)
if acc:
pe.paid_to_account_currency = acc.account_currency
pe.paid_to_account_type = acc.account_type

pe.paid_amount = amount
pe.received_amount = amount

if pe.references:
pe.references[0].allocated_amount = amount

if not pe.posting_date:
pe.posting_date = si.posting_date

pe.reference_no = si.name
pe.reference_date = si.posting_date

pe.insert()

pe.flags.ignore_validate = True
if hasattr(pe, "workflow_state"):
pe.workflow_state = "Pending"

pe.submit()
created.append(pe.name)

return created
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"column_break_aubg",
"company",
"section_break_dlxc",
"mode_of_payment",
"warehouse",
"cost_center",
"user"
Expand Down Expand Up @@ -53,12 +54,19 @@
"fieldtype": "Table",
"label": "User",
"options": "Branch Configuration User"
},
{
"description": "Cash + Bank Modes of Payment allowed for this branch. On Sales Invoice for Branch Users,payment rows whose MoP type is Cash or Bank are constrained to this list (or swapped to the first matching row); BNPL is left untouched.",
"fieldname": "mode_of_payment",
"fieldtype": "Table",
"label": "Mode of Payment",
"options": "Branch Configuration Mode of Payment"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-04-27 16:07:12.519306",
"modified": "2026-05-16 19:23:57.892929",
"modified_by": "Administrator",
"module": "Aqrar Ext",
"name": "Branch Configuration",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2026-05-16 19:14:41.500922",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"mode_of_payment",
"type"
],
"fields": [
{
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Mode of Payment",
"options": "Mode of Payment"
},
{
"fieldname": "type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-05-16 19:17:59.460731",
"modified_by": "Administrator",
"module": "Aqrar Ext",
"name": "Branch Configuration Mode of Payment",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Copyright (c) 2026, Enfono and contributors
# For license information, please see license.txt

# import frappe
from frappe.model.document import Document


class BranchConfigurationModeofPayment(Document):
pass
Empty file.
109 changes: 109 additions & 0 deletions aqrar_ext/aqrar_ext/report/work_flow_approval/work_flow_approval.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
let wf_action = "";

frappe.query_reports["Work Flow Approval"] = {
filters: [
{ fieldname: "user", label: "User", fieldtype: "Link", options: "User", default: frappe.session.user },
{ fieldname: "company", label: "Company", fieldtype: "Link", options: "Company" },
{
fieldname: "doctype", label: "Document Type", fieldtype: "Link", options: "DocType",
get_query: function() {
return {
query: "aqrar_ext.aqrar_ext.report.work_flow_approval.work_flow_approval.get_workflow_doctypes"
};
},
on_change: function() {
wf_action = "";
$("#wf-action-select").val("");
load_actions();
frappe.query_report.refresh();
}
},
{ fieldname: "from_date", label: "From Date", fieldtype: "Date" },
{ fieldname: "to_date", label: "To Date", fieldtype: "Date" }
],

after_datatable_render: function(dt) {
$(dt.wrapper).find(".dt-cell--col-0").each(function(i) {
if (i === 0) return;
let $c = $(this).css({ "text-align": "center", "cursor": "pointer" });
if (!$c.find('input[type="checkbox"]').length)
$c.html('<input type="checkbox" class="row-check" style="width:16px;height:16px;cursor:pointer;">');
$c.off("click").on("click", function(e) {
if (!$(e.target).is("input"))
$c.find('input[type="checkbox"]').prop("checked", v => !v);
});
});
},

onload: function(report) {
setTimeout(function() {
if (report.page.page_form.length && !$("#wf-action-select").length) {
report.page.page_form.append(`
<div style="display:inline-block;margin-left:8px;vertical-align:middle;">
<select id="wf-action-select" style="height:30px;border:none;border-radius:25px;
padding:0 14px;font-size:13px;font-family:inherit;color:#333;
background-color:#f4f5f6;min-width:160px;cursor:pointer;outline:none;">
<option value="">Workflow Action</option>
</select>
</div>`);
$("#wf-action-select").on("change", function() { wf_action = $(this).val(); });
}
load_actions();
}, 800);

report.page.add_inner_button("Apply Workflow Action", function() {
let docs = [];

$(report.datatable.wrapper).find("input.row-check:checked").each(function() {
let idx = parseInt($(this).closest(".dt-row").attr("data-row-index"));
if (!isNaN(idx) && report.data[idx])
docs.push({ doctype: report.data[idx].doctype, name: report.data[idx].name });
});

if (!docs.length) {
$(report.datatable.wrapper).find(".dt-row").each(function(i) {
if ($(this).find("input.row-check").prop("checked") && report.data[i])
docs.push({ doctype: report.data[i].doctype, name: report.data[i].name });
});
}

if (!docs.length) return frappe.msgprint(__("Please select at least one document."));

let action = $("#wf-action-select").val() || wf_action;
if (!action) return frappe.msgprint(__("Please select a Workflow Action."));

frappe.confirm(
__("Apply <b>{0}</b> to {1} document(s)?", [action, docs.length]),
() => frappe.call({
method: "aqrar_ext.aqrar_ext.report.work_flow_approval.work_flow_approval.apply_bulk_workflow",
args: { docs: JSON.stringify(docs), action },
freeze: true,
freeze_message: __("Applying..."),
callback: r => {
if (!r.exc) {
frappe.msgprint({ title: __("Result"), message: r.message, indicator: "green" });
frappe.query_report.refresh();
}
}
})
);
});
}
};

function load_actions() {
let doctype = frappe.query_report.get_filter_value("doctype");
let method = doctype
? "aqrar_ext.aqrar_ext.report.work_flow_approval.work_flow_approval.get_workflow_actions"
: "aqrar_ext.aqrar_ext.report.work_flow_approval.work_flow_approval.get_all_workflow_actions";
let args = doctype ? { doctype } : {};

frappe.call({
method: method,
args: args,
callback: r => {
let $s = $("#wf-action-select").empty().append('<option value="">Workflow Action</option>');
(r.message || []).forEach(a => $s.append(`<option value="${a}">${a}</option>`));
}
});
}
Loading
Loading