@@ -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
333335def _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
500573def _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