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
97 changes: 97 additions & 0 deletions aqrar_ext/api/commission.py
Original file line number Diff line number Diff line change
@@ -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'<a href="/app/journal-entry/{existing}">{existing}</a>'
)
)

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.")
)
126 changes: 126 additions & 0 deletions aqrar_ext/api/day_close.py
Original file line number Diff line number Diff line change
@@ -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
)
)
8 changes: 8 additions & 0 deletions aqrar_ext/api/item_naming.py
Original file line number Diff line number Diff line change
@@ -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")
65 changes: 65 additions & 0 deletions aqrar_ext/api/navigation.py
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 1 addition & 1 deletion aqrar_ext/api/sales_invoice_payment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
48 changes: 48 additions & 0 deletions aqrar_ext/aqrar_ext/doctype/aqrar_settings/aqrar_settings.json
Original file line number Diff line number Diff line change
@@ -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
}
6 changes: 6 additions & 0 deletions aqrar_ext/aqrar_ext/doctype/aqrar_settings/aqrar_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import frappe
from frappe.model.document import Document


class AqrarSettings(Document):
pass
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
Loading
Loading