@@ -429,15 +429,30 @@ def _add_line_item(self, root: ET.Element, item):
429429 line_id = ET .SubElement (line_elem , f"{{{ self .namespaces ['cbc' ]} }}ID" )
430430 line_id .text = str (item .idx )
431431
432+ # Normalize signs for PEPPOL compliance
433+ qty = flt (item .qty , item .precision ("qty" ))
434+ item_amount = flt (item .amount , item .precision ("amount" ))
435+ item_rate = flt (item .rate , item .precision ("rate" ))
436+
437+ if self .document_type == "CreditNote" :
438+ # Credit notes: all values positive (document type implies reversal)
439+ qty = abs (qty )
440+ item_amount = abs (item_amount )
441+ item_rate = abs (item_rate )
442+ elif item_amount < 0 :
443+ # Invoice deduction line: move negative from rate/price to quantity
444+ qty = - abs (qty )
445+ item_rate = abs (item_rate )
446+
432447 quantity = ET .SubElement (line_elem , f"{{{ self .namespaces ['cbc' ]} }}{ quantity_elem_name } " )
433- quantity .text = str (flt ( item . qty , item . precision ( "qty" )) )
448+ quantity .text = str (qty )
434449 quantity .set ("unitCode" , self .map_unit_code (item .uom ))
435450
436451 # Update references from invoice_line to line_elem
437452 invoice_line = line_elem
438453
439454 line_amount = ET .SubElement (invoice_line , f"{{{ self .namespaces ['cbc' ]} }}LineExtensionAmount" )
440- line_amount .text = str ( flt ( item . amount , item . precision ( "amount" )))
455+ line_amount .text = f" { item_amount :.2f } "
441456 line_amount .set ("currencyID" , self .invoice .currency )
442457
443458 item_elem = ET .SubElement (invoice_line , f"{{{ self .namespaces ['cac' ]} }}Item" )
@@ -466,7 +481,8 @@ def _add_line_item(self, root: ET.Element, item):
466481
467482 price = ET .SubElement (invoice_line , f"{{{ self .namespaces ['cac' ]} }}Price" )
468483 price_amount = ET .SubElement (price , f"{{{ self .namespaces ['cbc' ]} }}PriceAmount" )
469- price_amount .text = str (flt (item .rate , item .precision ("rate" )))
484+ price_precision = item .precision ("rate" ) or 2
485+ price_amount .text = f"{ item_rate :.{price_precision }f} "
470486 price_amount .set ("currencyID" , self .invoice .currency )
471487
472488 def _add_tax_totals (self ):
@@ -477,8 +493,12 @@ def _add_tax_totals(self):
477493 tax_total = ET .SubElement (self .root , f"{{{ self .namespaces ['cac' ]} }}TaxTotal" )
478494
479495 # Use invoice.total_taxes_and_charges for total TaxAmount (already correctly calculated after discount)
496+ # Credit notes: use abs() since PEPPOL CreditNote expects positive values
497+ total_tax = flt (self .invoice .total_taxes_and_charges , 2 )
498+ if self .document_type == "CreditNote" :
499+ total_tax = abs (total_tax )
480500 tax_amount = ET .SubElement (tax_total , f"{{{ self .namespaces ['cbc' ]} }}TaxAmount" )
481- tax_amount .text = str ( flt ( self . invoice . total_taxes_and_charges , 2 ))
501+ tax_amount .text = f" { total_tax :.2f } "
482502 tax_amount .set ("currencyID" , self .invoice .currency )
483503
484504 # Group taxes by (category_code, rate)
@@ -500,20 +520,23 @@ def _add_tax_totals(self):
500520 if key not in tax_groups :
501521 tax_groups [key ] = {"taxable_amount" : 0 , "tax_amount" : 0 }
502522
503- item_tax_amount = flt (item .net_amount ) * (rate or 0 ) / 100
523+ net_amount = flt (item .net_amount )
524+ if self .document_type == "CreditNote" :
525+ net_amount = abs (net_amount )
526+ item_tax_amount = net_amount * (rate or 0 ) / 100
504527 tax_groups [key ]["tax_amount" ] += item_tax_amount
505- tax_groups [key ]["taxable_amount" ] += flt ( item . net_amount )
528+ tax_groups [key ]["taxable_amount" ] += net_amount
506529
507530 # Add TaxSubtotal for each (category, rate) group
508531 for (category_code , rate ), data in tax_groups .items ():
509532 tax_subtotal = ET .SubElement (tax_total , f"{{{ self .namespaces ['cac' ]} }}TaxSubtotal" )
510533
511534 taxable_amount = ET .SubElement (tax_subtotal , f"{{{ self .namespaces ['cbc' ]} }}TaxableAmount" )
512- taxable_amount .text = str ( flt (data [" taxable_amount" ], 2 ))
535+ taxable_amount .text = f" { flt (data [' taxable_amount' ], 2 ):.2f } "
513536 taxable_amount .set ("currencyID" , self .invoice .currency )
514537
515538 tax_amount = ET .SubElement (tax_subtotal , f"{{{ self .namespaces ['cbc' ]} }}TaxAmount" )
516- tax_amount .text = str ( flt (data [" tax_amount" ], 2 ))
539+ tax_amount .text = f" { flt (data [' tax_amount' ], 2 ):.2f } "
517540 tax_amount .set ("currencyID" , self .invoice .currency )
518541
519542 tax_category = ET .SubElement (tax_subtotal , f"{{{ self .namespaces ['cac' ]} }}TaxCategory" )
@@ -674,6 +697,8 @@ def _add_allowances_charges(self):
674697
675698 # Handle document-level discounts (AllowanceCharge with ChargeIndicator=false)
676699 discount_amount = flt (self .invoice .total , 2 ) - flt (self .invoice .net_total , 2 )
700+ if self .document_type == "CreditNote" :
701+ discount_amount = abs (discount_amount )
677702 if discount_amount > 0 :
678703 # There's a document-level discount
679704 allowance_charge = ET .SubElement (self .root , f"{{{ self .namespaces ['cac' ]} }}AllowanceCharge" )
@@ -690,52 +715,47 @@ def _add_allowances_charges(self):
690715 reason .text = reason_text
691716
692717 # MultiplierFactorNumeric: Calculate percentage (Amount / BaseAmount * 100)
693- if self .invoice .total > 0 :
694- multiplier = (discount_amount / flt (self .invoice .total , 2 )) * 100
718+ inv_total = abs (flt (self .invoice .total , 2 )) if self .document_type == "CreditNote" else flt (self .invoice .total , 2 )
719+ if inv_total > 0 :
720+ multiplier = (discount_amount / inv_total ) * 100
695721 multiplier_factor = ET .SubElement (
696722 allowance_charge , f"{{{ self .namespaces ['cbc' ]} }}MultiplierFactorNumeric"
697723 )
698724 multiplier_factor .text = str (flt (multiplier , 6 )) # 6 decimal places as in example
699725
700726 # Amount: The discount amount
701727 amount = ET .SubElement (allowance_charge , f"{{{ self .namespaces ['cbc' ]} }}Amount" )
702- amount .text = str ( flt (discount_amount , 2 ))
728+ amount .text = f" { flt (discount_amount , 2 ):.2f } "
703729 amount .set ("currencyID" , self .invoice .currency )
704730
705731 # BaseAmount: The amount before discount (invoice.total)
706732 base_amount = ET .SubElement (allowance_charge , f"{{{ self .namespaces ['cbc' ]} }}BaseAmount" )
707- base_amount .text = str ( flt ( self . invoice . total , 2 ))
733+ base_amount .text = f" { inv_total :.2f } "
708734 base_amount .set ("currencyID" , self .invoice .currency )
709735
710- # Tax Category: Use the tax rate from invoice taxes
736+ # Tax Category (BR-32: always required on AllowanceCharge)
711737 # Get the first non-Actual tax rate (usually VAT)
712- tax_rate = None
738+ tax_rate = 0
713739 sample_tax = None
714740 for tax in self .invoice .taxes :
715741 if tax .charge_type != "Actual" and tax .rate :
716742 tax_rate = tax .rate
717743 sample_tax = tax
718744 break
719745
720- if tax_rate and tax_rate > 0 :
721- tax_category = ET .SubElement (allowance_charge , f"{{{ self .namespaces ['cac' ]} }}TaxCategory" )
746+ tax_category = ET .SubElement (allowance_charge , f"{{{ self .namespaces ['cac' ]} }}TaxCategory" )
722747
723- # Get category code dynamically
724- category_code = (
725- self .get_vat_category_code (self .invoice , tax = sample_tax ) if sample_tax else "S"
726- )
748+ category_code = self .get_vat_category_code (self .invoice , tax = sample_tax )
727749
728- # Category ID
729- category_id = ET .SubElement (tax_category , f"{{{ self .namespaces ['cbc' ]} }}ID" )
730- category_id .text = category_code
750+ category_id = ET .SubElement (tax_category , f"{{{ self .namespaces ['cbc' ]} }}ID" )
751+ category_id .text = category_code
731752
732- tax_percent = ET .SubElement (tax_category , f"{{{ self .namespaces ['cbc' ]} }}Percent" )
733- tax_percent .text = str (flt (tax_rate , 2 ))
753+ tax_percent = ET .SubElement (tax_category , f"{{{ self .namespaces ['cbc' ]} }}Percent" )
754+ tax_percent .text = str (flt (tax_rate , 2 ))
734755
735- # Tax scheme
736- tax_scheme = ET .SubElement (tax_category , f"{{{ self .namespaces ['cac' ]} }}TaxScheme" )
737- scheme_id = ET .SubElement (tax_scheme , f"{{{ self .namespaces ['cbc' ]} }}ID" )
738- scheme_id .text = "VAT"
756+ tax_scheme = ET .SubElement (tax_category , f"{{{ self .namespaces ['cac' ]} }}TaxScheme" )
757+ scheme_id = ET .SubElement (tax_scheme , f"{{{ self .namespaces ['cbc' ]} }}ID" )
758+ scheme_id .text = "VAT"
739759
740760 # Handle document-level charges (AllowanceCharge with ChargeIndicator=true)
741761 for tax in self .invoice .taxes :
@@ -754,28 +774,25 @@ def _add_allowances_charges(self):
754774 reason .text = tax .description
755775
756776 # Amount
777+ charge_amt = abs (flt (tax .tax_amount , 2 )) if self .document_type == "CreditNote" else flt (tax .tax_amount , 2 )
757778 amount = ET .SubElement (allowance_charge , f"{{{ self .namespaces ['cbc' ]} }}Amount" )
758- amount .text = str ( flt ( tax . tax_amount , 2 ))
779+ amount .text = f" { charge_amt :.2f } "
759780 amount .set ("currencyID" , self .invoice .currency )
760781
761- # Tax Category (if applicable)
762- if tax .rate and tax .rate > 0 :
763- tax_category = ET .SubElement (allowance_charge , f"{{{ self .namespaces ['cac' ]} }}TaxCategory" )
782+ # Tax Category (BR-32: always required on AllowanceCharge)
783+ tax_category = ET .SubElement (allowance_charge , f"{{{ self .namespaces ['cac' ]} }}TaxCategory" )
764784
765- # Get category code dynamically
766- category_code = self .get_vat_category_code (self .invoice , tax = tax )
785+ category_code = self .get_vat_category_code (self .invoice , tax = tax )
767786
768- # Category ID
769- category_id = ET .SubElement (tax_category , f"{{{ self .namespaces ['cbc' ]} }}ID" )
770- category_id .text = category_code
787+ category_id = ET .SubElement (tax_category , f"{{{ self .namespaces ['cbc' ]} }}ID" )
788+ category_id .text = category_code
771789
772- tax_percent = ET .SubElement (tax_category , f"{{{ self .namespaces ['cbc' ]} }}Percent" )
773- tax_percent .text = str (flt (tax .rate , 2 ))
790+ tax_percent = ET .SubElement (tax_category , f"{{{ self .namespaces ['cbc' ]} }}Percent" )
791+ tax_percent .text = str (flt (tax .rate or 0 , 2 ))
774792
775- # Tax scheme
776- tax_scheme = ET .SubElement (tax_category , f"{{{ self .namespaces ['cac' ]} }}TaxScheme" )
777- scheme_id = ET .SubElement (tax_scheme , f"{{{ self .namespaces ['cbc' ]} }}ID" )
778- scheme_id .text = "VAT"
793+ tax_scheme = ET .SubElement (tax_category , f"{{{ self .namespaces ['cac' ]} }}TaxScheme" )
794+ scheme_id = ET .SubElement (tax_scheme , f"{{{ self .namespaces ['cbc' ]} }}ID" )
795+ scheme_id .text = "VAT"
779796
780797 def _set_totals (self ):
781798 # Set monetary totals using ERPNext values directly
@@ -785,36 +802,44 @@ def _set_totals(self):
785802 # Add LegalMonetaryTotal section
786803 legal_total = ET .SubElement (self .root , f"{{{ self .namespaces ['cac' ]} }}LegalMonetaryTotal" )
787804
805+ # For credit notes, ERPNext stores negative totals but PEPPOL requires positive values
806+ is_credit = self .document_type == "CreditNote"
807+ inv_total = abs (flt (self .invoice .total , 2 )) if is_credit else flt (self .invoice .total , 2 )
808+ inv_net_total = abs (flt (self .invoice .net_total , 2 )) if is_credit else flt (self .invoice .net_total , 2 )
809+ inv_grand_total = abs (flt (self .invoice .grand_total , 2 )) if is_credit else flt (self .invoice .grand_total , 2 )
810+
788811 # Line Extension Amount (BT-106) - Sum of Invoice line net amount (BEFORE discount)
789812 # Use invoice.total which is the sum of all item.amount values
790813 line_total = ET .SubElement (legal_total , f"{{{ self .namespaces ['cbc' ]} }}LineExtensionAmount" )
791- line_total .text = str ( flt ( self . invoice . total , 2 ))
814+ line_total .text = f" { inv_total :.2f } "
792815 line_total .set ("currencyID" , self .invoice .currency )
793816
794817 # Tax Exclusive Amount (BT-109) - Invoice total amount without VAT
795818 # This equals LineExtensionAmount - AllowanceTotalAmount = net_total (after discount, before tax)
796819 tax_exclusive_amount = ET .SubElement (legal_total , f"{{{ self .namespaces ['cbc' ]} }}TaxExclusiveAmount" )
797- tax_exclusive_amount .text = str ( flt ( self . invoice . net_total , 2 ))
820+ tax_exclusive_amount .text = f" { inv_net_total :.2f } "
798821 tax_exclusive_amount .set ("currencyID" , self .invoice .currency )
799822
800823 # Tax Inclusive Amount (BT-112) - Invoice total amount with VAT
801824 tax_inclusive_amount = ET .SubElement (legal_total , f"{{{ self .namespaces ['cbc' ]} }}TaxInclusiveAmount" )
802- tax_inclusive_amount .text = str ( flt ( self . invoice . grand_total , 2 ))
825+ tax_inclusive_amount .text = f" { inv_grand_total :.2f } "
803826 tax_inclusive_amount .set ("currencyID" , self .invoice .currency )
804827
805828 # Allowance Total Amount (BT-107) - Sum of allowances on document level (discount)
806829 # Calculate as difference between total (before discount) and net_total (after discount)
807- allowance_amount = flt ( self . invoice . total , 2 ) - flt ( self . invoice . net_total , 2 )
830+ allowance_amount = inv_total - inv_net_total
808831 allowance_total = ET .SubElement (legal_total , f"{{{ self .namespaces ['cbc' ]} }}AllowanceTotalAmount" )
809- allowance_total .text = str ( flt (allowance_amount , 2 ))
832+ allowance_total .text = f" { flt (allowance_amount , 2 ):.2f } "
810833 allowance_total .set ("currencyID" , self .invoice .currency )
811834
812835 # Charge Total Amount (BT-108) - Sum of charges on document level
813836 # Must come AFTER AllowanceTotalAmount per XSD schema order
814837 actual_charge_total = sum (tax .tax_amount for tax in self .invoice .taxes if tax .charge_type == "Actual" )
838+ if is_credit :
839+ actual_charge_total = abs (actual_charge_total )
815840 if actual_charge_total :
816841 charge_total = ET .SubElement (legal_total , f"{{{ self .namespaces ['cbc' ]} }}ChargeTotalAmount" )
817- charge_total .text = str ( flt (actual_charge_total , 2 ))
842+ charge_total .text = f" { flt (actual_charge_total , 2 ):.2f } "
818843 charge_total .set ("currencyID" , self .invoice .currency )
819844 else :
820845 # Always add ChargeTotalAmount (set to 0.00 if no charges)
@@ -825,24 +850,22 @@ def _set_totals(self):
825850 # Prepaid Amount (BT-113) - Sum of amounts already paid
826851 # This is required when PayableAmount != TaxInclusiveAmount to satisfy BR-CO-16:
827852 # PayableAmount = TaxInclusiveAmount - PrepaidAmount + RoundingAmount
828- if self . document_type != "CreditNote" :
853+ if not is_credit :
829854 prepaid_value = flt (self .invoice .grand_total , 2 ) - flt (self .invoice .outstanding_amount , 2 )
830855 if prepaid_value > 0 :
831856 prepaid_amount = ET .SubElement (legal_total , f"{{{ self .namespaces ['cbc' ]} }}PrepaidAmount" )
832- prepaid_amount .text = str ( flt (prepaid_value , 2 ))
857+ prepaid_amount .text = f" { flt (prepaid_value , 2 ):.2f } "
833858 prepaid_amount .set ("currencyID" , self .invoice .currency )
834859
835860 # Payable Amount (BT-115) - Amount due for payment
836- # For credit notes, this should be negative (amount to be credited)
837861 payable_amount = ET .SubElement (legal_total , f"{{{ self .namespaces ['cbc' ]} }}PayableAmount" )
838- if self .document_type == "CreditNote" :
839- # For credit notes, PayableAmount should be negative (the amount to be credited)
840- # Use grand_total (which is already negative for credit notes)
841- payable_value = flt (self .invoice .grand_total , 2 )
862+ if is_credit :
863+ # For credit notes, use positive grand_total (amount to be credited)
864+ payable_value = inv_grand_total
842865 else :
843866 # For invoices, use outstanding_amount
844867 payable_value = flt (self .invoice .outstanding_amount , 2 )
845- payable_amount .text = str ( payable_value )
868+ payable_amount .text = f" { payable_value :.2f } "
846869 payable_amount .set ("currencyID" , self .invoice .currency )
847870
848871 def initialize_peppol_xml (self ) -> ET .Element :
@@ -960,12 +983,31 @@ def get_invoice_type_code(self, invoice) -> str:
960983 if hasattr (invoice , "is_return" ) and invoice .is_return :
961984 invoice_type_code = "381"
962985 elif hasattr (invoice , "amended_from" ) and invoice .amended_from :
963- invoice_type_code = "384"
986+ # 384 (Corrected Invoice) only allowed when both parties are German
987+ if self ._both_parties_german ():
988+ invoice_type_code = "384"
964989 except Exception :
965990 pass
966991
967992 return invoice_type_code
968993
994+ def _both_parties_german (self ) -> bool :
995+ # Check if both seller and buyer are German organizations
996+ try :
997+ seller_code = ""
998+ if self .seller_address and self .seller_address .country :
999+ seller_code = (
1000+ frappe .db .get_value ("Country" , self .seller_address .country , "code" ) or ""
1001+ ).upper ()
1002+ buyer_code = ""
1003+ if self .buyer_address and self .buyer_address .country :
1004+ buyer_code = (
1005+ frappe .db .get_value ("Country" , self .buyer_address .country , "code" ) or ""
1006+ ).upper ()
1007+ return seller_code == "DE" and buyer_code == "DE"
1008+ except Exception :
1009+ return False
1010+
9691011 def map_unit_code (self , erpnext_unit : str ) -> str :
9701012 # Map ERPNext unit codes to PEPPOL standard unit codes using CommonCodeRetriever
9711013 if not erpnext_unit :
0 commit comments