Pull /v2/profile during Asan step + fill Company Tax Policy on finalize
Mirrors the manual "Update from E-Taxes" flow that already lives in taxes_az/client/company.js — but unattended, as part of the wizard. Asan step: fetch_company_profile() (new whitelisted method, jey_wizard.etaxes) wraps taxes_az.company_etaxes.get_company_profile, which hits /api/po/profile/public/v2/profile with the main_token. Same call also resolves taxAuthorityCode and propertyType to display names via the e-taxes dictionaries (token still fresh here — by finalize it may have expired). The merged blob is cached in a new Jey Wizard Etaxes Cache.company_profile_json field so finalize can apply it without another network round-trip. Wire-up: fetch_etaxes_data → fetch_company_profile, runs back-to- back during the existing "fetching" sub-state. Profile pull is best-effort — failure doesn't block the rest of the flow. Finalize: _apply_company_profile(company) parses the cached blob, builds a field_updates dict mirroring the JS _parseProfileData mapping (BASIC, MANAGEMENT, BUSINESS/TAX INFO, VAT INFO, ORG STRUCTURE, REGISTRATION DATES, ADDRESSES, TAX SYSTEMS, EMPLOYER) plus the resolved tax_authority/property_type names. Date fields are normalised: YYYYMMDD compact dates → YYYY-MM-DD; ISO datetimes → YYYY-MM-DD prefix. Booleans → "1"/"0" so Check fields take them. Actual DB writes go through taxes_az.company_etaxes.force_update_ company so the table-child handling for additionalActivityCodes and affiliatesNames/affiliatesTins stays in one place. Schema: Jey Wizard Etaxes Cache picks up two new fields (company_profile_section, company_profile_json) — needs `bench migrate` on existing sites. Trace: two new breadcrumbs in Error Log, "Jey Wizard trace: fetch_company_profile done" and "Jey Wizard trace: company profile applied" (with the list of fieldnames written and the force_update_company result summary).
This commit is contained in:
parent
d93a024ba8
commit
89ae5e237c
|
|
@ -1 +1 @@
|
|||
__version__ = "0.1.12"
|
||||
__version__ = "0.1.13"
|
||||
|
|
|
|||
|
|
@ -136,6 +136,82 @@ def fetch_all_etaxes():
|
|||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def fetch_company_profile():
|
||||
"""Pull /api/po/profile/public/v2/profile via taxes_az helper + resolve
|
||||
taxAuthorityCode/propertyType to human names through the e-taxes
|
||||
dictionaries. Stash the merged blob in cache.company_profile_json so the
|
||||
finalize step can write it onto Company without another network round-trip
|
||||
(by which point main_token may already be invalid).
|
||||
"""
|
||||
_only_admin()
|
||||
|
||||
from taxes_az.company_etaxes import (
|
||||
get_company_profile,
|
||||
get_tax_authority_name_by_code,
|
||||
get_property_type_name_by_code,
|
||||
)
|
||||
|
||||
resp = get_company_profile()
|
||||
if not resp or not resp.get("success"):
|
||||
return {
|
||||
"ok": False,
|
||||
"message": (resp or {}).get("message", "get_company_profile failed"),
|
||||
"error": (resp or {}).get("error"),
|
||||
}
|
||||
|
||||
profile_blob = resp.get("data") or {}
|
||||
# taxes_az's helper sometimes wraps the actual profile in {"profile": {...}}.
|
||||
# Mirror the JS unwrap (see updateCompanyFromProfile in taxes_az/client/company.js).
|
||||
profile = profile_blob.get("profile") if isinstance(profile_blob, dict) and isinstance(profile_blob.get("profile"), dict) else profile_blob
|
||||
|
||||
# Resolve dictionary codes to display names while we still have a fresh
|
||||
# main_token. Failures degrade gracefully: keep the raw code.
|
||||
resolved = {}
|
||||
tax_authority_code = profile.get("taxAuthorityCode") if isinstance(profile, dict) else None
|
||||
if tax_authority_code:
|
||||
try:
|
||||
r = get_tax_authority_name_by_code(tax_authority_code)
|
||||
if r and r.get("success"):
|
||||
resolved["tax_authority"] = r.get("name") or tax_authority_code
|
||||
except Exception as exc:
|
||||
frappe.log_error(
|
||||
f"tax_authority lookup failed for {tax_authority_code}: {exc}",
|
||||
"Jey Wizard fetch_company_profile",
|
||||
)
|
||||
property_type_code = profile.get("propertyType") if isinstance(profile, dict) else None
|
||||
if property_type_code:
|
||||
try:
|
||||
r = get_property_type_name_by_code(property_type_code)
|
||||
if r and r.get("success"):
|
||||
resolved["property_type"] = r.get("name") or property_type_code
|
||||
except Exception as exc:
|
||||
frappe.log_error(
|
||||
f"property_type lookup failed for {property_type_code}: {exc}",
|
||||
"Jey Wizard fetch_company_profile",
|
||||
)
|
||||
|
||||
cache = frappe.get_single("Jey Wizard Etaxes Cache")
|
||||
cache.company_profile_json = json.dumps(
|
||||
{"profile": profile, "resolved": resolved},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
cache.flags.ignore_permissions = True
|
||||
cache.save()
|
||||
frappe.db.commit()
|
||||
|
||||
frappe.log_error(
|
||||
json.dumps({
|
||||
"has_profile": bool(profile),
|
||||
"profile_keys": sorted(list(profile.keys())) if isinstance(profile, dict) else None,
|
||||
"resolved": resolved,
|
||||
}, ensure_ascii=False, indent=2),
|
||||
"Jey Wizard trace: fetch_company_profile done",
|
||||
)
|
||||
|
||||
return {"ok": True, "resolved": resolved}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_cached(kind):
|
||||
"""Return parsed cached data for a given kind. Used by future wizard steps that
|
||||
|
|
@ -258,6 +334,8 @@ def materialize_after_setup(args):
|
|||
# placeholders. Runs off the just-populated cache, not another e-taxes fetch.
|
||||
_materialize_native_banks(company_name)
|
||||
|
||||
_apply_company_profile(company_name)
|
||||
|
||||
_materialize_amas_employees(company_name)
|
||||
|
||||
# Drop toast-style messages that piled up during the various install/translate
|
||||
|
|
@ -620,6 +698,245 @@ def _currency_sort_key(acc):
|
|||
return (1, 0, cur)
|
||||
|
||||
|
||||
def _apply_company_profile(company):
|
||||
"""Read cache.company_profile_json (populated during the Asan step by
|
||||
fetch_company_profile) and write the Tax Policy fields onto Company.
|
||||
Mapping mirrors taxes_az/client/company.js::_parseProfileData; we delegate
|
||||
the actual write to taxes_az.company_etaxes.force_update_company so the
|
||||
logic stays aligned with the in-app "Update from E-Taxes" button.
|
||||
Best-effort: failures are logged, never raised.
|
||||
"""
|
||||
cache = frappe.get_single("Jey Wizard Etaxes Cache")
|
||||
raw = (cache.company_profile_json or "").strip()
|
||||
if not raw:
|
||||
frappe.log_error(
|
||||
f"company={company} reason=profile_cache_empty",
|
||||
"Jey Wizard trace: company profile skipped",
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
blob = json.loads(raw)
|
||||
except Exception as exc:
|
||||
frappe.log_error(
|
||||
f"company={company} bad profile cache JSON: {exc}",
|
||||
"Jey Wizard apply company profile",
|
||||
)
|
||||
return
|
||||
|
||||
profile = blob.get("profile") if isinstance(blob, dict) else None
|
||||
resolved = (blob.get("resolved") if isinstance(blob, dict) else None) or {}
|
||||
if not isinstance(profile, dict):
|
||||
frappe.log_error(
|
||||
f"company={company} no profile dict in cache",
|
||||
"Jey Wizard apply company profile",
|
||||
)
|
||||
return
|
||||
|
||||
field_updates = _build_company_field_updates(profile, resolved)
|
||||
additional_activities = []
|
||||
if isinstance(profile.get("additionalActivityCodes"), list):
|
||||
additional_activities = [c for c in profile["additionalActivityCodes"] if c]
|
||||
affiliate_names = list(profile.get("affiliatesNames") or [])
|
||||
affiliate_tins = list(profile.get("affiliatesTins") or [])
|
||||
|
||||
try:
|
||||
from taxes_az.company_etaxes import force_update_company
|
||||
result = force_update_company(
|
||||
company_name=company,
|
||||
field_updates=json.dumps(field_updates, ensure_ascii=False),
|
||||
additional_activities=json.dumps(additional_activities, ensure_ascii=False),
|
||||
affiliate_names=json.dumps(affiliate_names, ensure_ascii=False),
|
||||
affiliate_tins=json.dumps(affiliate_tins, ensure_ascii=False),
|
||||
) or {}
|
||||
except Exception as exc:
|
||||
frappe.log_error(
|
||||
f"force_update_company failed for {company}: {exc}\n{traceback.format_exc()}",
|
||||
"Jey Wizard apply company profile",
|
||||
)
|
||||
return
|
||||
|
||||
frappe.log_error(
|
||||
json.dumps({
|
||||
"company": company,
|
||||
"updated_fields_count": len(field_updates),
|
||||
"updated_field_names": sorted(list(field_updates.keys())),
|
||||
"additional_activities": len(additional_activities),
|
||||
"affiliate_organizations": max(len(affiliate_names), len(affiliate_tins)),
|
||||
"force_update_result": {
|
||||
k: result.get(k) for k in ("success", "message", "updated_fields", "additional_activities_added", "affiliates_added")
|
||||
} if isinstance(result, dict) else None,
|
||||
}, ensure_ascii=False, indent=2),
|
||||
"Jey Wizard trace: company profile applied",
|
||||
)
|
||||
|
||||
|
||||
def _build_company_field_updates(profile, resolved):
|
||||
"""Map the e-taxes /v2/profile JSON onto Company fieldnames. Mirrors the
|
||||
JS function _parseProfileData in taxes_az/client/company.js: same source
|
||||
keys, same target fieldnames, same value normalisation.
|
||||
"""
|
||||
updates = {}
|
||||
|
||||
def _put(field, value):
|
||||
if value is None:
|
||||
return
|
||||
# Booleans → "1"/"0" so the same code path works for Check fields.
|
||||
if isinstance(value, bool):
|
||||
updates[field] = "1" if value else "0"
|
||||
return
|
||||
s = str(value).strip()
|
||||
if s == "":
|
||||
return
|
||||
updates[field] = s
|
||||
|
||||
def _fmt_compact_date(s):
|
||||
# E-Taxes uses YYYYMMDD for VAT dates (see formatDate in JS).
|
||||
if not s or len(str(s)) != 8:
|
||||
return None
|
||||
s = str(s)
|
||||
return f"{s[0:4]}-{s[4:6]}-{s[6:8]}"
|
||||
|
||||
def _fmt_iso_date(s):
|
||||
# Other dates come as ISO already; keep YYYY-MM-DD prefix only.
|
||||
if not s:
|
||||
return None
|
||||
return str(s)[:10]
|
||||
|
||||
# BASIC
|
||||
_put("tax_id", profile.get("tin"))
|
||||
_put("registration_details", profile.get("stateRegistrationDocumentNumber"))
|
||||
mobile = profile.get("mobilePhoneNumber") or _extract_phone(profile, "mobile")
|
||||
landline = profile.get("landlinePhoneNumber") or _extract_phone(profile, "landline")
|
||||
if mobile:
|
||||
_put("phone_no", mobile)
|
||||
elif landline:
|
||||
_put("phone_no", landline)
|
||||
_put("mobile_phone", mobile)
|
||||
_put("landline_phone", landline)
|
||||
_put("email", profile.get("email"))
|
||||
_put("date_of_establishment", _fmt_iso_date(profile.get("taxpayerRegistrationDate")))
|
||||
_put("state_registration_document_issued_date", _fmt_iso_date(profile.get("stateRegistrationDocumentIssuedDate")))
|
||||
if profile.get("mainActivity"):
|
||||
_put("domain", profile.get("mainActivity"))
|
||||
_put("main_activity", profile.get("mainActivity"))
|
||||
|
||||
# MANAGEMENT
|
||||
_put("director_name_etaxes", profile.get("companyDirectorName"))
|
||||
_put("director_pin", profile.get("pin"))
|
||||
|
||||
# BUSINESS / TAX INFO
|
||||
_put("legal_form_code", profile.get("legalFormCode"))
|
||||
|
||||
tax_rate = profile.get("taxRate")
|
||||
if tax_rate:
|
||||
if tax_rate in ("ƏDV", "VAT", "18%"):
|
||||
_put("taxation_system", "18% Value-added tax")
|
||||
elif tax_rate in ("0%", "ƏDV 0%"):
|
||||
_put("taxation_system", "0% Value-added tax")
|
||||
else:
|
||||
_put("taxation_system", tax_rate)
|
||||
|
||||
criteria = profile.get("criteriaOfBusinessEntity")
|
||||
if criteria:
|
||||
c = str(criteria).lower()
|
||||
mapping = {"micro": "Micro Entrepreneur", "small": "Small Entrepreneur",
|
||||
"middle": "Medium Entrepreneur", "medium": "Medium Entrepreneur",
|
||||
"big": "Big Entrepreneur", "large": "Big Entrepreneur"}
|
||||
_put("business_classification", mapping.get(c, f"{criteria} Entrepreneur"))
|
||||
|
||||
_put("state_registration_authority", profile.get("stateRegistrationName"))
|
||||
_put("taxpayer_activity_group", profile.get("activityGroup"))
|
||||
if profile.get("specialTaxRegime") is not None:
|
||||
_put("special_tax_regime", bool(profile.get("specialTaxRegime")))
|
||||
_put("tin_type", profile.get("tinType"))
|
||||
if profile.get("isRisky") is not None:
|
||||
_put("is_risky_taxpayer", bool(profile.get("isRisky")))
|
||||
if profile.get("sportBettingOperator") is not None:
|
||||
_put("is_sport_betting_operator", bool(profile.get("sportBettingOperator")))
|
||||
if profile.get("hasActiveProductionObject") is not None:
|
||||
_put("has_active_production_object", bool(profile.get("hasActiveProductionObject")))
|
||||
_put("tax_system_type", profile.get("taxSystemType"))
|
||||
if profile.get("isTaxPayerInCancellationProcess") is not None:
|
||||
_put("is_taxpayer_in_cancellation_process", bool(profile.get("isTaxPayerInCancellationProcess")))
|
||||
if profile.get("isChiefOfAnyLegalEntity") is not None:
|
||||
_put("is_chief_of_any_legal_entity", bool(profile.get("isChiefOfAnyLegalEntity")))
|
||||
|
||||
# VAT INFO (sub-object)
|
||||
vat_info = profile.get("vatInfo")
|
||||
if isinstance(vat_info, dict):
|
||||
_put("vat_registration_date", _fmt_compact_date(vat_info.get("prOperationTableOperationDate")))
|
||||
_put("vat_certificate_number", vat_info.get("recDocumentDocumentNumber"))
|
||||
_put("vat_certificate_date", _fmt_compact_date(vat_info.get("prVatOperationsRegDate")))
|
||||
|
||||
# ORG STRUCTURE
|
||||
head_org = profile.get("headOrganizationName")
|
||||
if isinstance(head_org, list) and head_org:
|
||||
_put("parent_organization", ", ".join(str(x) for x in head_org if x))
|
||||
_put("parent_organization_tin", profile.get("headOrganizationTin"))
|
||||
if isinstance(profile.get("employeeData"), dict):
|
||||
_put("employee_count", profile["employeeData"].get("employeeCount"))
|
||||
|
||||
# REGISTRATION DATES
|
||||
_put("suspension_start_date", _fmt_iso_date(profile.get("suspensionStartDate")))
|
||||
_put("suspension_end_date", _fmt_iso_date(profile.get("suspensionEndDate")))
|
||||
_put("liquidation_date", _fmt_iso_date(profile.get("liquidationDate")))
|
||||
|
||||
# ADDRESSES
|
||||
_put("legal_address_full", profile.get("legalAddress"))
|
||||
_put("actual_address_full", profile.get("actualAddress"))
|
||||
_put("address_for_mail", profile.get("addressForMail"))
|
||||
addr = profile.get("legalAddressObject")
|
||||
if isinstance(addr, dict):
|
||||
_put("legal_address_postcode", addr.get("postcode"))
|
||||
_put("legal_address_region", addr.get("region"))
|
||||
_put("legal_address_locality", addr.get("locality"))
|
||||
_put("legal_address_street", addr.get("street"))
|
||||
_put("legal_address_house", addr.get("houseNumber"))
|
||||
_put("legal_address_room", addr.get("roomNumber"))
|
||||
|
||||
# TAX SYSTEMS
|
||||
tax_systems = profile.get("taxSystems")
|
||||
if isinstance(tax_systems, list) and tax_systems:
|
||||
_put("tax_systems_list", ", ".join(str(x) for x in tax_systems if x))
|
||||
|
||||
# EMPLOYER
|
||||
emp = profile.get("employer")
|
||||
if isinstance(emp, dict):
|
||||
_put("employer_name", emp.get("name"))
|
||||
_put("employer_position", emp.get("position"))
|
||||
|
||||
# Resolved dictionary names (set by fetch_company_profile)
|
||||
if resolved.get("tax_authority"):
|
||||
_put("tax_authority", resolved["tax_authority"])
|
||||
if resolved.get("property_type"):
|
||||
_put("property_type", resolved["property_type"])
|
||||
|
||||
return updates
|
||||
|
||||
|
||||
def _extract_phone(profile, preferred=None):
|
||||
"""Mirror of ETaxesCompany._extractPhoneNumber from taxes_az/client/company.js
|
||||
— picks the best phone from `contacts` when the top-level fields are blank.
|
||||
"""
|
||||
if preferred == "mobile" and profile.get("mobilePhoneNumber"):
|
||||
return profile["mobilePhoneNumber"]
|
||||
if preferred == "landline" and profile.get("landlinePhoneNumber"):
|
||||
return profile["landlinePhoneNumber"]
|
||||
if profile.get("mobilePhoneNumber"):
|
||||
return profile["mobilePhoneNumber"]
|
||||
if profile.get("landlinePhoneNumber"):
|
||||
return profile["landlinePhoneNumber"]
|
||||
contacts = profile.get("contacts")
|
||||
if isinstance(contacts, list):
|
||||
for wanted in ("mobile", "homePhone"):
|
||||
for c in contacts:
|
||||
if isinstance(c, dict) and c.get("type") == wanted and c.get("contact"):
|
||||
prefix = c.get("prefix") or ""
|
||||
return f"{prefix}{c['contact']}"
|
||||
return None
|
||||
|
||||
|
||||
def _materialize_amas_employees(company_name):
|
||||
"""If the user loaded ƏMAS employees during the wizard, enqueue bulk import now
|
||||
that Company exists. Skipped silently when the cache is empty (i.e. user skipped
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
"bank_accounts_json",
|
||||
"obligation_pacts_json",
|
||||
"presented_certs_json",
|
||||
"company_profile_section",
|
||||
"company_profile_json",
|
||||
"amas_section",
|
||||
"amas_selected_employees_json"
|
||||
],
|
||||
|
|
@ -91,6 +93,18 @@
|
|||
"options": "JSON",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company_profile_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Company Profile (Tax Policy)"
|
||||
},
|
||||
{
|
||||
"fieldname": "company_profile_json",
|
||||
"fieldtype": "Code",
|
||||
"label": "Company Profile",
|
||||
"options": "JSON",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amas_section",
|
||||
"fieldtype": "Section Break",
|
||||
|
|
|
|||
|
|
@ -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.12";
|
||||
const JEY_WIZARD_VERSION = "0.1.13";
|
||||
|
||||
// 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.
|
||||
|
|
@ -1004,18 +1004,39 @@ frappe.setup.SetupWizard = class JeySetupWizard {
|
|||
}
|
||||
|
||||
fetch_etaxes_data() {
|
||||
// Sequence: fetch_all_etaxes → fetch_company_profile. Both run with the
|
||||
// same fresh main_token; the profile pull also does dictionary lookups
|
||||
// (taxAuthority/propertyType) that need the token, so we want them done
|
||||
// while the user is still on the "fetching" screen — by finalize-time
|
||||
// the token may have expired.
|
||||
frappe.call({
|
||||
method: "jey_wizard.etaxes.fetch_all_etaxes",
|
||||
callback: (r) => {
|
||||
const m = r.message || {};
|
||||
this.asan_fetch_summary = m.summary || {};
|
||||
this.asan_fetch_errors = m.errors || {};
|
||||
this.asan_state = "done";
|
||||
this.show_current_step();
|
||||
this.fetch_company_profile();
|
||||
},
|
||||
error: () => {
|
||||
this.asan_fetch_summary = {};
|
||||
this.asan_fetch_errors = { all: __("fetch call failed — see server logs") };
|
||||
// Still try the profile pull — independent payload, useful even
|
||||
// if the lists call hiccupped.
|
||||
this.fetch_company_profile();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
fetch_company_profile() {
|
||||
frappe.call({
|
||||
method: "jey_wizard.etaxes.fetch_company_profile",
|
||||
callback: () => {
|
||||
this.asan_state = "done";
|
||||
this.show_current_step();
|
||||
},
|
||||
error: () => {
|
||||
// Profile fetch is best-effort. Materialize will skip cleanly
|
||||
// if the cache stays empty; nothing to surface to the user here.
|
||||
this.asan_state = "done";
|
||||
this.show_current_step();
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue