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 _
|
||||
|
||||
|
||||
# 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()
|
||||
def cache_selected_employees(employees_json, create_designation=0):
|
||||
"""Store the selected ƏMAS employees in the wizard cache so materialize_after_setup
|
||||
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()
|
||||
|
||||
|
|
@ -28,6 +52,10 @@ def cache_selected_employees(employees_json, create_designation=0):
|
|||
else:
|
||||
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:
|
||||
create_designation = int(create_designation)
|
||||
except (ValueError, TypeError):
|
||||
|
|
@ -44,7 +72,7 @@ def cache_selected_employees(employees_json, create_designation=0):
|
|||
cache.save()
|
||||
frappe.db.commit()
|
||||
|
||||
return {"ok": True, "count": len(parsed)}
|
||||
return {"ok": True, "count": len(parsed), "dropped_inactive": dropped}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -347,6 +347,12 @@ def materialize_after_setup(args):
|
|||
"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.
|
||||
# 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
|
||||
|
|
@ -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():
|
||||
"""Wipe everything in frappe.local.message_log that piled up during the
|
||||
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
|
||||
// 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.
|
||||
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
|
||||
// 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_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-accounting-method").val(this.data.accounting_method || "Hesablama metodu");
|
||||
// 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);
|
||||
$abbrEl.on("input", (e) => {
|
||||
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);
|
||||
this.data.company_abbr = v;
|
||||
$(e.currentTarget).val(v);
|
||||
|
|
@ -691,10 +711,11 @@ frappe.setup.SetupWizard = class JeySetupWizard {
|
|||
this.$wrap.find(".jey-status").text(__("Company abbreviation is required"));
|
||||
return false;
|
||||
}
|
||||
// Allow letters, digits and a single space — e.g. "TS MMC" for
|
||||
// the Azerbaijani quoted-core + legal-form pattern.
|
||||
if (!/^[A-Z0-9 ]{1,10}$/.test(abbr)) {
|
||||
this.$wrap.find(".jey-status").text(__("Abbreviation must be 1–10 letters/digits (A–Z, 0–9, spaces allowed)."));
|
||||
// Allow any Unicode letters (incl. Azerbaijani Ə Ğ İ Ö Ş Ü Ç),
|
||||
// digits, and spaces. Length 1–10. The `u` flag is required for
|
||||
// \p{L} / \p{N} to match.
|
||||
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;
|
||||
}
|
||||
this.data.company_abbr = abbr;
|
||||
|
|
|
|||
Loading…
Reference in New Issue