Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: GSTIN Status report #3011

Merged
merged 16 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from 14 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
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Copyright (c) 2025, Resilient Tech and contributors
// For license information, please see license.txt

const GSTIN_FIELDNAME = [
"status",
"registration_date",
"last_updated_on",
"cancelled_date",
"is_blocked",
];

frappe.query_reports["GSTIN Status Report"] = {
html_enabled: true,

filters: [
{
fieldname: "status",
label: __("Status"),
fieldtype: "Select",
options: [
"",
"Active",
"Cancelled",
"Inactive",
"Provisional",
"Suspended",
],
},
{
fieldname: "party_type",
label: __("Party Type"),
fieldtype: "Select",
options: ["", "Customer", "Supplier"],
},
],

formatter: function (value, row, column, data, default_formatter) {
if (data) {
if (column.fieldname == "status") {
value = get_colored_status(value);
} else if (column.fieldname == "update_gstin_details_btn") {
value = create_btn_with_gstin_attr(data.gstin);
} else {
if (column.fieldname == "last_updated_on") {
value = frappe.datetime.prettyDate(value);
} else {
value = default_formatter(value, row, column, data);
}
value = `<span fieldname="${column.fieldname}">${value}</span>`;
}
}

return value;
},

add_on_click_listner(gstin) {
toggle_gstin_update_btn(gstin, (disabled = true));
const affectedElements = $(`div.dt-cell__content[title='${gstin}']`);
set_btn_text(gstin, "Updating");

frappe.call({
method: "india_compliance.gst_india.doctype.gstin.gstin.get_gstin_status",
args: {
gstin: gstin,
force_update: true,
},
callback: function (r) {
if (r.message) {
let data = r.message;
affectedElements.each(function () {
row = this.parentElement.attributes["data-row-index"].value;
for (let fieldname of GSTIN_FIELDNAME) {
update_value(row, fieldname, data[fieldname]);
}
});
set_btn_text(gstin, "Updated");
} else {
toggle_gstin_update_btn(gstin, (disabled = false));
}
},
});
},

get_datatable_options(datatable_options) {
datatable_options.cellHeight = 35;

return datatable_options;
},
};
const STATUS_TO_COLOR_MAPPING = {
Active: "green",
Cancelled: "red",
Inactive: "black",
Provisional: "black",
Suspended: "black",
};

function get_colored_status(status) {
if (!status) return "";
return `<div style="color: ${STATUS_TO_COLOR_MAPPING[status]}; text-align:center; width:100%;">${status}</div>`;
}

function set_btn_text(gstin, text) {
let btn = $(`button[data-gstin='${gstin}']`);
btn.text(text);
}

function toggle_gstin_update_btn(gstin, disabled = null) {
let btn = $(`button[data-gstin='${gstin}']`);
if (disabled == null) {
disabled = btn.prop("disabled");
disabled = !disabled;
}

btn.prop("disabled", disabled);
}

function create_btn_with_gstin_attr(gstin) {
const BUTTON_HTML = `<button
data-fieldname="gstin_update_btn"
class="btn btn-xs btn-primary center"
data-gstin="${gstin}"
onclick="frappe.query_reports['GSTIN Status Report'].add_on_click_listner('${gstin}')"
>
Update
</button>`;

return BUTTON_HTML;
}

function update_value(row, fieldname, value) {
let ele = $(`.dt-row.dt-row-${row}.vrow > div > div > [fieldname='${fieldname}']`);

let column = frappe.query_report.columns.find(column => {
return column.fieldname == fieldname;
});
fieldtype = column.fieldtype;

if (fieldname == "is_blocked") {
value = [undefined, null].includes(value) ? "" : value == 0 ? "No" : "Yes";
} else if (fieldname == "last_updated_on") {
value = frappe.datetime.prettyDate(value);
} else {
const formatter = frappe.form.get_formatter(fieldtype);
value = formatter(value);
}

ele.text(value);
ele.parent().attr("title", value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2025-01-28 17:10:48.875369",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"json": "{}",
"letterhead": null,
"modified": "2025-01-29 13:02:30.885362",
"modified_by": "Administrator",
"module": "GST India",
"name": "GSTIN Status Report",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "GSTIN",
"report_name": "GSTIN Status Report",
"report_type": "Script Report",
"roles": [
{
"role": "System Manager"
},
{
"role": "Accounts Manager"
},
{
"role": "Accounts User"
}
],
"timeout": 1500
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Copyright (c) 2025, Resilient Tech and contributors
# For license information, please see license.txt

import frappe
from frappe import _
from frappe.query_builder import Case, Order
from frappe.query_builder.functions import IfNull, IsNull


def execute(filters: dict | None = None):
"""Return columns and data for the report.

This is the main entry point for the report. It accepts the filters as a
dictionary and should return columns and data. It is called by the framework
every time the report is refreshed or a filter is updated.
"""
report = GSTINDetailedReport(filters=filters)
columns = report.get_columns()
data = report.get_data()

return columns, data


class GSTINDetailedReport:

def __init__(self, filters: dict | None = None):
self.filters = frappe._dict(filters or {})
self.doctypes = (
[self.filters.party_type]
if self.filters.party_type
else ["Customer", "Supplier"]
)

def get_columns(self) -> list[dict]:
"""Return columns for the report.

One field definition per column, just like a DocType field definition.
"""
columns = [
{
"label": _("Party Type"),
"fieldname": "party_type",
"fieldtype": "Link",
"options": "DocType",
"width": 100,
},
{
"label": _("Party Name"),
"fieldname": "party_name",
"fieldtype": "Dynamic Link",
"options": "party_type",
"width": 220,
},
{
"label": _("GSTIN"),
"fieldname": "gstin",
"fieldtype": "Link",
"options": "GSTIN",
"width": 180,
},
{
"label": _("Status"),
"fieldname": "status",
"fieldtype": "Data",
"width": 120,
},
{
"label": _("Registration Date"),
"fieldname": "registration_date",
"fieldtype": "Date",
"width": 150,
},
{
"label": _("Last Updated"),
"fieldname": "last_updated_on",
"fieldtype": "Datetime",
"width": 150,
},
{
"label": _("Cancelled Date"),
"fieldname": "cancelled_date",
"fieldtype": "Date",
"width": 150,
},
{
"label": _("Is Blocked"),
"fieldname": "is_blocked",
"fieldtype": "Data",
"width": 80,
},
{
"label": _("Update GSTIN Details"),
"fieldname": "update_gstin_details_btn",
"fieldtype": "Button",
"width": 120,
},
]

return columns

def get_data(self):
gstin = frappe.qb.DocType("GSTIN")
address = frappe.qb.DocType("Address")
dynamic_link = frappe.qb.DocType("Dynamic Link")

party_query = (
frappe.qb.from_(address)
.inner_join(dynamic_link)
.on(address.name == dynamic_link.parent)
.select(
address.gstin,
dynamic_link.link_doctype.as_("party_type"),
dynamic_link.link_name.as_("party_name"),
)
.where(dynamic_link.link_doctype.isin(self.doctypes))
.where(IfNull(address.gstin, "") != "")
).as_("party")

for doctype in self.doctypes:
party_query.union(get_party_query(doctype))

gstin_query = (
frappe.qb.from_(party_query)
.left_join(gstin)
.on(gstin.gstin == party_query.gstin)
.select(
party_query.gstin,
gstin.status,
gstin.registration_date,
gstin.last_updated_on,
gstin.cancelled_date,
Case()
.when(IsNull(gstin.is_blocked), "")
.when(gstin.is_blocked == 0, "No")
.else_("Yes")
.as_("is_blocked"),
party_query.party_type,
party_query.party_name,
)
.orderby(gstin.modified, order=Order.desc)
)

if self.filters.status:
gstin_query = gstin_query.where(gstin.status == self.filters.status)

return gstin_query.run(as_dict=True)


def get_party_query(doctype):
dt = frappe.qb.DocType(doctype)

query = (
frappe.qb.from_(dt)
.select(
dt.gstin,
dt.doctype.as_("party_type"),
dt.name.as_("party_name"),
)
.where(IfNull(dt.gstin, "") != "")
)

return query
Loading