Skip to content

Commit 53fb15f

Browse files
TRS-WOBclaude
andcommitted
fix: v16 compatibility for DSFinV-K Cash Point Closing
Three v16-related issues in dsfinv_k_cash_point_closing.py: 1. _build_amounts_per_vat_definition: Same issue as _build_amounts_per_vat_rate (fixed in previous commit). The JSON field `item_wise_tax_detail` on tax rows no longer exists in v16. Added fallback to read from the v16 Child Table `item_wise_tax_details` (DocType: "Item Wise Tax Detail"). The v15 JSON path is preserved as primary, making this fully backwards-compatible. 2. _get_pos_invoices_from_closing: In v15 the POS Closing Entry stores invoice references in the child table field `pos_transactions`. In v16 this was renamed to `pos_invoices`. Added fallback: tries `pos_invoices` first (v16), then `pos_transactions` (v15). 3. _build_cash_point_closing_head: In v16, `posting_date` on POS Closing Entry is a `datetime.date` object instead of a string. This causes `TypeError: Object of type date is not JSON serializable` when sending the payload to the fiskaly DSFINVK API. Fixed by explicitly converting to string via `str(doc.posting_date)`. All three fixes follow the same pattern as the tse_transaction fix: v15 path first, v16 fallback, fully backwards-compatible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 74d0ecb commit 53fb15f

1 file changed

Lines changed: 92 additions & 17 deletions

File tree

erpnext_tse/erpnext_tse/doctype/dsfinv_k_cash_point_closing/dsfinv_k_cash_point_closing.py

Lines changed: 92 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,9 @@ def _build_cash_point_closing_head(doc, transactions: list[dict[str, Any]]) -> d
217217
}
218218

219219
if getattr(doc, "posting_date", None):
220-
head["business_date"] = doc.posting_date
220+
# In v16 posting_date can be a datetime.date object which is not
221+
# JSON-serializable. Convert to ISO format string explicitly.
222+
head["business_date"] = str(doc.posting_date)
221223

222224
return head
223225

@@ -331,6 +333,14 @@ def _build_transaction_from_pos_invoice(pos_inv, company_currency: str) -> dict[
331333

332334

333335
def _build_amounts_per_vat_definition(pos_inv) -> list[dict[str, Any]]:
336+
"""
337+
Build VAT breakdown per DSFinV-K VAT definition for a POS Invoice.
338+
339+
In v15 the tax details are stored as a JSON field `item_wise_tax_detail`
340+
on each tax row. In v16 this field no longer exists — the data is in the
341+
Child Table `item_wise_tax_details` (DocType: "Item Wise Tax Detail").
342+
Both formats are supported with v15 as primary and v16 as fallback.
343+
"""
334344
items = pos_inv.get("items") or []
335345
if not items:
336346
frappe.throw(_("POS Invoice has no items."))
@@ -376,17 +386,59 @@ def parse_item_wise_detail(detail_value) -> tuple[float, float]:
376386
return rate_percent, tax_amount
377387
return 0.0, 0.0
378388

389+
# --- v16 Child Table fallback (same approach as _build_amounts_per_vat_rate) ---
390+
item_wise_tax_details_table = pos_inv.get("item_wise_tax_details") or []
391+
if not item_wise_tax_details_table and pos_inv.name:
392+
item_wise_tax_details_table = frappe.get_all(
393+
"Item Wise Tax Detail",
394+
filters={"parent": pos_inv.name, "parenttype": pos_inv.doctype},
395+
fields=["tax_row", "item_row", "rate", "amount", "taxable_amount"],
396+
order_by="idx",
397+
)
398+
399+
# Lookup: (tax_row_name, item_code) -> {rate, amount}
400+
iwtd_by_tax_item: dict[tuple[str, str], dict] = {}
401+
if item_wise_tax_details_table:
402+
item_code_by_row_name = {}
403+
for item_row in items:
404+
row_name = getattr(item_row, "name", None) or (
405+
item_row.get("name") if isinstance(item_row, dict) else None
406+
)
407+
ic = getattr(item_row, "item_code", None) or (
408+
item_row.get("item_code") if isinstance(item_row, dict) else None
409+
)
410+
if row_name and ic:
411+
item_code_by_row_name[row_name] = ic
412+
413+
for detail_row in item_wise_tax_details_table:
414+
tax_row_name = getattr(detail_row, "tax_row", None) or (
415+
detail_row.get("tax_row") if isinstance(detail_row, dict) else None
416+
)
417+
item_row_name = getattr(detail_row, "item_row", None) or (
418+
detail_row.get("item_row") if isinstance(detail_row, dict) else None
419+
)
420+
if tax_row_name and item_row_name:
421+
rate = float(
422+
getattr(detail_row, "rate", 0)
423+
or (detail_row.get("rate", 0) if isinstance(detail_row, dict) else 0)
424+
)
425+
amount = float(
426+
getattr(detail_row, "amount", 0)
427+
or (detail_row.get("amount", 0) if isinstance(detail_row, dict) else 0)
428+
)
429+
item_code = item_code_by_row_name.get(item_row_name)
430+
if item_code:
431+
iwtd_by_tax_item[(tax_row_name, item_code)] = {
432+
"rate": rate,
433+
"amount": amount,
434+
}
435+
379436
for tax_row in taxes:
380437
tax_account = getattr(tax_row, "account_head", None) or (
381438
tax_row.get("account_head") if isinstance(tax_row, dict) else None
382439
)
383-
item_wise_tax_detail_json = (
384-
getattr(tax_row, "item_wise_tax_detail", None)
385-
if not isinstance(tax_row, dict)
386-
else tax_row.get("item_wise_tax_detail")
387-
)
388440

389-
if not tax_account or not item_wise_tax_detail_json:
441+
if not tax_account:
390442
continue
391443

392444
vat_id = vat_id_by_tax_account.get(tax_account)
@@ -400,18 +452,39 @@ def parse_item_wise_detail(detail_value) -> tuple[float, float]:
400452
)
401453
vat_id_by_tax_account[tax_account] = int(vat_id)
402454

403-
item_wise_details = frappe.parse_json(item_wise_tax_detail_json)
404-
if not isinstance(item_wise_details, dict):
405-
continue
455+
# v15 path: JSON field on tax row
456+
item_wise_tax_detail_json = (
457+
getattr(tax_row, "item_wise_tax_detail", None)
458+
if not isinstance(tax_row, dict)
459+
else tax_row.get("item_wise_tax_detail")
460+
)
406461

407-
for item_code, detail_value in item_wise_details.items():
408-
if item_code not in net_amount_by_item_code:
462+
if item_wise_tax_detail_json:
463+
item_wise_details = frappe.parse_json(item_wise_tax_detail_json)
464+
if not isinstance(item_wise_details, dict):
409465
continue
410466

411-
rate_percent, tax_amount = parse_item_wise_detail(detail_value)
412-
bucket = vat_sums.setdefault(int(vat_id), {"excl_vat": 0.0, "vat": 0.0})
413-
bucket["excl_vat"] += net_amount_by_item_code[item_code]
414-
bucket["vat"] += float(tax_amount or 0)
467+
for item_code, detail_value in item_wise_details.items():
468+
if item_code not in net_amount_by_item_code:
469+
continue
470+
471+
rate_percent, tax_amount = parse_item_wise_detail(detail_value)
472+
bucket = vat_sums.setdefault(int(vat_id), {"excl_vat": 0.0, "vat": 0.0})
473+
bucket["excl_vat"] += net_amount_by_item_code[item_code]
474+
bucket["vat"] += float(tax_amount or 0)
475+
476+
elif iwtd_by_tax_item:
477+
# v16 path: Child Table "Item Wise Tax Detail"
478+
tax_row_name = getattr(tax_row, "name", None) or (
479+
tax_row.get("name") if isinstance(tax_row, dict) else None
480+
)
481+
for item_code in net_amount_by_item_code:
482+
detail = iwtd_by_tax_item.get((tax_row_name, item_code))
483+
if not detail:
484+
continue
485+
bucket = vat_sums.setdefault(int(vat_id), {"excl_vat": 0.0, "vat": 0.0})
486+
bucket["excl_vat"] += net_amount_by_item_code[item_code]
487+
bucket["vat"] += float(detail["amount"] or 0)
415488

416489
if not vat_sums:
417490
frappe.throw(_("Could not derive VAT amounts for POS Invoice {0}.").format(pos_inv.name))
@@ -498,7 +571,9 @@ def _build_payment_types_for_pos_invoice(pos_inv, company_currency: str) -> list
498571

499572

500573
def _get_pos_invoices_from_closing(doc) -> list[Document]:
501-
rows = doc.get("pos_transactions") or []
574+
# In v15 the child table is called "pos_transactions",
575+
# in v16 it was renamed to "pos_invoices".
576+
rows = doc.get("pos_invoices") or doc.get("pos_transactions") or []
502577
if not rows:
503578
frappe.throw(_("POS Closing Entry has no POS Transactions."))
504579

0 commit comments

Comments
 (0)