Allow Azerbaijani abbreviation, FY end auto-fill, drop inactive rows
Four pieces of polish requested after the wizard otherwise lands clean:
1. Abbreviation accepts any Unicode letter
The validator regex /^[A-Z0-9 ]{1,10}$/ rejected legitimate
Azerbaijani uppercase characters (Ə Ğ İ Ö Ş Ü Ç) the user may
want in their abbr. Switched to /^[\p{L}\p{N} ]{1,10}$/u so any
letter / digit goes through; toUpperCase() in modern browsers
already handles the AZ casing correctly. Error string updated to
match.
2. Fiscal Year end auto-fills from start
When the Frappe Date control on the start field fires `change`,
we set end = start + 1 year - 1 day via moment. The reverse
trigger (end → start) is intentionally absent so editing end
never overwrites the user's chosen start.
3. Inactive rows removed after the e-taxes loaders
New _purge_inactive_etaxes_records(company) runs right after the
"loaders done" trace. invoice_az writes everything, regardless
of status; the wizard's contract is "active rows only", so we
prune:
- E-Taxes Bank Account where status='C' (closed)
- E-Taxes Object where object_status not in (A,active)
- E-Taxes Cash Register where status_code not in (A,active)
- E-Taxes POS Terminal where status not in (A,active)
- E-Taxes Obligation Pact where expire_date < today
Presented Certificates carry historical state and stay. Per-row
delete failures logged. New trace "inactive purge done" reports
the matched/deleted counts per category.
4. ƏMAS: filter terminated/suspended at cache time
cache_selected_employees now drops anything whose contract_status
isn't in {qüvvədədir, aktiv, active}. Empty status passes through
so we never silently lose someone (e.g. a row from get_employees_
report that omitted the field). The bulk import job hits AMAS
once per row, so cutting these here saves real wall time on the
post-finalize background work.
Background-import architecture explained in chat — the per-employee
sync via _process_bulk_employees_import (one CSRF + AMAS detail call
per row, sequential) is by design in invoice_az; jey_wizard fires
and forgets it.
This commit is contained in:
parent
0e7ce08d7e
commit
38293a8de5
|
|
@ -1 +1 @@
|
||||||
__version__ = "0.1.17"
|
__version__ = "0.1.18"
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,34 @@ import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
|
||||||
|
|
||||||
|
# AMAS contract_status values that count as "still on payroll". Anything else
|
||||||
|
# (terminated/suspended/blank) is filtered out before we cache, so the bulk
|
||||||
|
# import job in invoice_az never even sees them.
|
||||||
|
_ACTIVE_AMAS_STATUSES = {
|
||||||
|
"qüvvədədir",
|
||||||
|
"aktiv",
|
||||||
|
"active",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _is_active_amas_employee(emp):
|
||||||
|
if not isinstance(emp, dict):
|
||||||
|
return False
|
||||||
|
raw = str(emp.get("contract_status") or "").strip().lower()
|
||||||
|
# Empty status → trust the row (be conservative: import unknown rather
|
||||||
|
# than silently drop someone who would have been active).
|
||||||
|
if not raw:
|
||||||
|
return True
|
||||||
|
return raw in _ACTIVE_AMAS_STATUSES
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def cache_selected_employees(employees_json, create_designation=0):
|
def cache_selected_employees(employees_json, create_designation=0):
|
||||||
"""Store the selected ƏMAS employees in the wizard cache so materialize_after_setup
|
"""Store the selected ƏMAS employees in the wizard cache so materialize_after_setup
|
||||||
can enqueue the bulk import with the freshly-created Company.
|
can enqueue the bulk import with the freshly-created Company.
|
||||||
|
|
||||||
|
Drops terminated / suspended contracts up front: import_bulk_employees hits
|
||||||
|
the AMAS API once per row, so cutting them here saves real time.
|
||||||
"""
|
"""
|
||||||
_only_admin()
|
_only_admin()
|
||||||
|
|
||||||
|
|
@ -28,6 +52,10 @@ def cache_selected_employees(employees_json, create_designation=0):
|
||||||
else:
|
else:
|
||||||
parsed = employees_json or []
|
parsed = employees_json or []
|
||||||
|
|
||||||
|
original_count = len(parsed)
|
||||||
|
parsed = [e for e in parsed if _is_active_amas_employee(e)]
|
||||||
|
dropped = original_count - len(parsed)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
create_designation = int(create_designation)
|
create_designation = int(create_designation)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
|
|
@ -44,7 +72,7 @@ def cache_selected_employees(employees_json, create_designation=0):
|
||||||
cache.save()
|
cache.save()
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
return {"ok": True, "count": len(parsed)}
|
return {"ok": True, "count": len(parsed), "dropped_inactive": dropped}
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|
|
||||||
|
|
@ -347,6 +347,12 @@ def materialize_after_setup(args):
|
||||||
"Jey Wizard trace: loaders done",
|
"Jey Wizard trace: loaders done",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# invoice_az's loaders write everything they receive — closed bank
|
||||||
|
# accounts, terminated objects, etc. The wizard's contract is "active
|
||||||
|
# data only", so prune the dead rows here. Best-effort: per-row failures
|
||||||
|
# are logged, not raised.
|
||||||
|
_purge_inactive_etaxes_records(company_name)
|
||||||
|
|
||||||
# invoice_az's bank loader only populates the E-Taxes Bank Account cache doctype.
|
# invoice_az's bank loader only populates the E-Taxes Bank Account cache doctype.
|
||||||
# For the wizard we also want native ERPNext Bank / Bank Account / GL Account so
|
# For the wizard we also want native ERPNext Bank / Bank Account / GL Account so
|
||||||
# the user can post transactions on day one, plus we clear out unused CoA
|
# the user can post transactions on day one, plus we clear out unused CoA
|
||||||
|
|
@ -1102,6 +1108,107 @@ def _find_default_warehouse(company):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _purge_inactive_etaxes_records(company):
|
||||||
|
"""Remove inactive rows from the E-Taxes doctypes that invoice_az's
|
||||||
|
loaders just populated. Active-status semantics per doctype:
|
||||||
|
|
||||||
|
- E-Taxes Bank Account: status='C' means closed (matches the same
|
||||||
|
filter we already apply in _materialize_native_banks).
|
||||||
|
- E-Taxes Object: object_status='A' or 'active' means active (mirrors
|
||||||
|
invoice_az.send_sales_api which filters status=='A' before send).
|
||||||
|
- E-Taxes Cash Register: status_code='A' means active.
|
||||||
|
- E-Taxes POS Terminal: status='A' means active.
|
||||||
|
- E-Taxes Obligation Pact: rows with expire_date in the past are
|
||||||
|
expired → drop.
|
||||||
|
|
||||||
|
Presented Certificates carry historical state (cert_state) but are
|
||||||
|
primarily an audit record — we leave them alone.
|
||||||
|
|
||||||
|
Only deletes rows belonging to the freshly-created Company so prior
|
||||||
|
data on the site stays untouched if anyone re-runs the materialize.
|
||||||
|
"""
|
||||||
|
from frappe.utils import getdate, today
|
||||||
|
|
||||||
|
report = {}
|
||||||
|
|
||||||
|
def _delete_where(doctype, filters, label):
|
||||||
|
try:
|
||||||
|
rows = frappe.get_all(doctype, filters=filters, pluck="name")
|
||||||
|
except Exception as exc:
|
||||||
|
frappe.log_error(
|
||||||
|
f"_purge_inactive_etaxes_records: list {doctype}: {exc}",
|
||||||
|
"Jey Wizard purge inactive",
|
||||||
|
)
|
||||||
|
report[label] = {"error": str(exc)[:200]}
|
||||||
|
return
|
||||||
|
deleted = 0
|
||||||
|
for name in rows:
|
||||||
|
try:
|
||||||
|
frappe.delete_doc(doctype, name, ignore_permissions=True, delete_permanently=True)
|
||||||
|
deleted += 1
|
||||||
|
except Exception as exc:
|
||||||
|
frappe.log_error(
|
||||||
|
f"_purge_inactive_etaxes_records: delete {doctype}/{name}: {exc}",
|
||||||
|
"Jey Wizard purge inactive",
|
||||||
|
)
|
||||||
|
report[label] = {"deleted": deleted, "matched": len(rows)}
|
||||||
|
|
||||||
|
# Closed bank accounts (status="C")
|
||||||
|
_delete_where(
|
||||||
|
"E-Taxes Bank Account",
|
||||||
|
{"company": company, "status": "C"},
|
||||||
|
"bank_accounts_closed",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Inactive objects (anything that's NOT 'A'/'active')
|
||||||
|
_delete_where(
|
||||||
|
"E-Taxes Object",
|
||||||
|
{"company": company, "object_status": ("not in", ("A", "active", "Active"))},
|
||||||
|
"objects_inactive",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Inactive cash registers (status_code anything but 'A')
|
||||||
|
_delete_where(
|
||||||
|
"E-Taxes Cash Register",
|
||||||
|
{"company": company, "status_code": ("not in", ("A", "active", "Active"))},
|
||||||
|
"cash_registers_inactive",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Inactive POS terminals (status anything but 'A')
|
||||||
|
_delete_where(
|
||||||
|
"E-Taxes POS Terminal",
|
||||||
|
{"company": company, "status": ("not in", ("A", "active", "Active"))},
|
||||||
|
"pos_terminals_inactive",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Expired obligation pacts: expire_date strictly in the past
|
||||||
|
try:
|
||||||
|
expired = frappe.get_all(
|
||||||
|
"E-Taxes Obligation Pact",
|
||||||
|
filters={"company": company, "expire_date": ("<", today())},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
deleted = 0
|
||||||
|
for name in expired:
|
||||||
|
try:
|
||||||
|
frappe.delete_doc("E-Taxes Obligation Pact", name, ignore_permissions=True, delete_permanently=True)
|
||||||
|
deleted += 1
|
||||||
|
except Exception as exc:
|
||||||
|
frappe.log_error(
|
||||||
|
f"_purge_inactive_etaxes_records: delete pact/{name}: {exc}",
|
||||||
|
"Jey Wizard purge inactive",
|
||||||
|
)
|
||||||
|
report["obligation_pacts_expired"] = {"deleted": deleted, "matched": len(expired)}
|
||||||
|
except Exception as exc:
|
||||||
|
report["obligation_pacts_expired"] = {"error": str(exc)[:200]}
|
||||||
|
|
||||||
|
frappe.db.commit()
|
||||||
|
frappe.log_error(
|
||||||
|
json.dumps(report, ensure_ascii=False, indent=2),
|
||||||
|
"Jey Wizard trace: inactive purge done",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _drop_alert_messages():
|
def _drop_alert_messages():
|
||||||
"""Wipe everything in frappe.local.message_log that piled up during the
|
"""Wipe everything in frappe.local.message_log that piled up during the
|
||||||
setup_complete request. We need the entire log gone, not just alerts:
|
setup_complete request. We need the entire log gone, not just alerts:
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ frappe.provide("jey_wizard");
|
||||||
// Bump this string in every commit that changes wizard code. Displayed in the badge so
|
// Bump this string in every commit that changes wizard code. Displayed in the badge so
|
||||||
// we can tell at a glance which version is actually running on a given machine. Kept in
|
// we can tell at a glance which version is actually running on a given machine. Kept in
|
||||||
// sync with __version__ in jey_wizard/__init__.py.
|
// sync with __version__ in jey_wizard/__init__.py.
|
||||||
const JEY_WIZARD_VERSION = "0.1.17";
|
const JEY_WIZARD_VERSION = "0.1.18";
|
||||||
|
|
||||||
// Wipe Frappe + ERPNext default slides so their `before_load`/`after_load` listeners
|
// Wipe Frappe + ERPNext default slides so their `before_load`/`after_load` listeners
|
||||||
// don't try to mutate a wizard that isn't slide-based anymore.
|
// don't try to mutate a wizard that isn't slide-based anymore.
|
||||||
|
|
@ -467,6 +467,23 @@ frappe.setup.SetupWizard = class JeySetupWizard {
|
||||||
this.fy_start_ctrl = mountDate(".jey-fy-start", this.data.fy_start_date);
|
this.fy_start_ctrl = mountDate(".jey-fy-start", this.data.fy_start_date);
|
||||||
this.fy_end_ctrl = mountDate(".jey-fy-end", this.data.fy_end_date);
|
this.fy_end_ctrl = mountDate(".jey-fy-end", this.data.fy_end_date);
|
||||||
|
|
||||||
|
// Fiscal year is exactly 12 months: when the user picks a new start
|
||||||
|
// date, default end to start + 1 year - 1 day. The user can still
|
||||||
|
// override end manually after — and the reverse trigger (end → start)
|
||||||
|
// is intentionally absent so editing end never overwrites start.
|
||||||
|
if (this.fy_start_ctrl && this.fy_start_ctrl.$input) {
|
||||||
|
this.fy_start_ctrl.$input.on("change", () => {
|
||||||
|
const start = this.fy_start_ctrl.get_value();
|
||||||
|
if (!start) return;
|
||||||
|
const auto_end = moment(start, "YYYY-MM-DD")
|
||||||
|
.add(1, "year")
|
||||||
|
.subtract(1, "day")
|
||||||
|
.format("YYYY-MM-DD");
|
||||||
|
this.fy_end_ctrl && this.fy_end_ctrl.set_value(auto_end);
|
||||||
|
this.data.fy_end_date = auto_end;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$body.find(".jey-valuation-method").val(this.data.valuation_method || "FIFO");
|
$body.find(".jey-valuation-method").val(this.data.valuation_method || "FIFO");
|
||||||
$body.find(".jey-accounting-method").val(this.data.accounting_method || "Hesablama metodu");
|
$body.find(".jey-accounting-method").val(this.data.accounting_method || "Hesablama metodu");
|
||||||
// Show why the default landed where it did, only when the auto-pick
|
// Show why the default landed where it did, only when the auto-pick
|
||||||
|
|
@ -500,6 +517,9 @@ frappe.setup.SetupWizard = class JeySetupWizard {
|
||||||
$nameEl.on("input", fillAbbrDefault);
|
$nameEl.on("input", fillAbbrDefault);
|
||||||
$abbrEl.on("input", (e) => {
|
$abbrEl.on("input", (e) => {
|
||||||
this.data.company_abbr_user_edited = true;
|
this.data.company_abbr_user_edited = true;
|
||||||
|
// .toUpperCase() in modern browsers handles Azerbaijani correctly
|
||||||
|
// (ə → Ə, i → İ, etc.). Don't strip non-ASCII — the regex below
|
||||||
|
// accepts any Unicode letter.
|
||||||
const v = ($(e.currentTarget).val() || "").toUpperCase().slice(0, 10);
|
const v = ($(e.currentTarget).val() || "").toUpperCase().slice(0, 10);
|
||||||
this.data.company_abbr = v;
|
this.data.company_abbr = v;
|
||||||
$(e.currentTarget).val(v);
|
$(e.currentTarget).val(v);
|
||||||
|
|
@ -691,10 +711,11 @@ frappe.setup.SetupWizard = class JeySetupWizard {
|
||||||
this.$wrap.find(".jey-status").text(__("Company abbreviation is required"));
|
this.$wrap.find(".jey-status").text(__("Company abbreviation is required"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Allow letters, digits and a single space — e.g. "TS MMC" for
|
// Allow any Unicode letters (incl. Azerbaijani Ə Ğ İ Ö Ş Ü Ç),
|
||||||
// the Azerbaijani quoted-core + legal-form pattern.
|
// digits, and spaces. Length 1–10. The `u` flag is required for
|
||||||
if (!/^[A-Z0-9 ]{1,10}$/.test(abbr)) {
|
// \p{L} / \p{N} to match.
|
||||||
this.$wrap.find(".jey-status").text(__("Abbreviation must be 1–10 letters/digits (A–Z, 0–9, spaces allowed)."));
|
if (!/^[\p{L}\p{N} ]{1,10}$/u.test(abbr)) {
|
||||||
|
this.$wrap.find(".jey-status").text(__("Abbreviation must be 1–10 letters or digits (any language, spaces allowed)."));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.data.company_abbr = abbr;
|
this.data.company_abbr = abbr;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue