@@ -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,51 @@ 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 = (
719+ abs (flt (self .invoice .total , 2 ))
720+ if self .document_type == "CreditNote"
721+ else flt (self .invoice .total , 2 )
722+ )
723+ if inv_total > 0 :
724+ multiplier = (discount_amount / inv_total ) * 100
695725 multiplier_factor = ET .SubElement (
696726 allowance_charge , f"{{{ self .namespaces ['cbc' ]} }}MultiplierFactorNumeric"
697727 )
698728 multiplier_factor .text = str (flt (multiplier , 6 )) # 6 decimal places as in example
699729
700730 # Amount: The discount amount
701731 amount = ET .SubElement (allowance_charge , f"{{{ self .namespaces ['cbc' ]} }}Amount" )
702- amount .text = str ( flt (discount_amount , 2 ))
732+ amount .text = f" { flt (discount_amount , 2 ):.2f } "
703733 amount .set ("currencyID" , self .invoice .currency )
704734
705735 # BaseAmount: The amount before discount (invoice.total)
706736 base_amount = ET .SubElement (allowance_charge , f"{{{ self .namespaces ['cbc' ]} }}BaseAmount" )
707- base_amount .text = str ( flt ( self . invoice . total , 2 ))
737+ base_amount .text = f" { inv_total :.2f } "
708738 base_amount .set ("currencyID" , self .invoice .currency )
709739
710- # Tax Category: Use the tax rate from invoice taxes
740+ # Tax Category (BR-32: always required on AllowanceCharge)
711741 # Get the first non-Actual tax rate (usually VAT)
712- tax_rate = None
742+ tax_rate = 0
713743 sample_tax = None
714744 for tax in self .invoice .taxes :
715745 if tax .charge_type != "Actual" and tax .rate :
716746 tax_rate = tax .rate
717747 sample_tax = tax
718748 break
719749
720- if tax_rate and tax_rate > 0 :
721- tax_category = ET .SubElement (allowance_charge , f"{{{ self .namespaces ['cac' ]} }}TaxCategory" )
750+ tax_category = ET .SubElement (allowance_charge , f"{{{ self .namespaces ['cac' ]} }}TaxCategory" )
722751
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- )
752+ category_code = self .get_vat_category_code (self .invoice , tax = sample_tax )
727753
728- # Category ID
729- category_id = ET .SubElement (tax_category , f"{{{ self .namespaces ['cbc' ]} }}ID" )
730- category_id .text = category_code
754+ category_id = ET .SubElement (tax_category , f"{{{ self .namespaces ['cbc' ]} }}ID" )
755+ category_id .text = category_code
731756
732- tax_percent = ET .SubElement (tax_category , f"{{{ self .namespaces ['cbc' ]} }}Percent" )
733- tax_percent .text = str (flt (tax_rate , 2 ))
757+ tax_percent = ET .SubElement (tax_category , f"{{{ self .namespaces ['cbc' ]} }}Percent" )
758+ tax_percent .text = str (flt (tax_rate , 2 ))
734759
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"
760+ tax_scheme = ET .SubElement (tax_category , f"{{{ self .namespaces ['cac' ]} }}TaxScheme" )
761+ scheme_id = ET .SubElement (tax_scheme , f"{{{ self .namespaces ['cbc' ]} }}ID" )
762+ scheme_id .text = "VAT"
739763
740764 # Handle document-level charges (AllowanceCharge with ChargeIndicator=true)
741765 for tax in self .invoice .taxes :
@@ -754,28 +778,29 @@ def _add_allowances_charges(self):
754778 reason .text = tax .description
755779
756780 # Amount
781+ charge_amt = (
782+ abs (flt (tax .tax_amount , 2 ))
783+ if self .document_type == "CreditNote"
784+ else flt (tax .tax_amount , 2 )
785+ )
757786 amount = ET .SubElement (allowance_charge , f"{{{ self .namespaces ['cbc' ]} }}Amount" )
758- amount .text = str ( flt ( tax . tax_amount , 2 ))
787+ amount .text = f" { charge_amt :.2f } "
759788 amount .set ("currencyID" , self .invoice .currency )
760789
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" )
790+ # Tax Category (BR-32: always required on AllowanceCharge)
791+ tax_category = ET .SubElement (allowance_charge , f"{{{ self .namespaces ['cac' ]} }}TaxCategory" )
764792
765- # Get category code dynamically
766- category_code = self .get_vat_category_code (self .invoice , tax = tax )
793+ category_code = self .get_vat_category_code (self .invoice , tax = tax )
767794
768- # Category ID
769- category_id = ET .SubElement (tax_category , f"{{{ self .namespaces ['cbc' ]} }}ID" )
770- category_id .text = category_code
795+ category_id = ET .SubElement (tax_category , f"{{{ self .namespaces ['cbc' ]} }}ID" )
796+ category_id .text = category_code
771797
772- tax_percent = ET .SubElement (tax_category , f"{{{ self .namespaces ['cbc' ]} }}Percent" )
773- tax_percent .text = str (flt (tax .rate , 2 ))
798+ tax_percent = ET .SubElement (tax_category , f"{{{ self .namespaces ['cbc' ]} }}Percent" )
799+ tax_percent .text = str (flt (tax .rate or 0 , 2 ))
774800
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"
801+ tax_scheme = ET .SubElement (tax_category , f"{{{ self .namespaces ['cac' ]} }}TaxScheme" )
802+ scheme_id = ET .SubElement (tax_scheme , f"{{{ self .namespaces ['cbc' ]} }}ID" )
803+ scheme_id .text = "VAT"
779804
780805 def _set_totals (self ):
781806 # Set monetary totals using ERPNext values directly
@@ -785,36 +810,46 @@ def _set_totals(self):
785810 # Add LegalMonetaryTotal section
786811 legal_total = ET .SubElement (self .root , f"{{{ self .namespaces ['cac' ]} }}LegalMonetaryTotal" )
787812
813+ # For credit notes, ERPNext stores negative totals but PEPPOL requires positive values
814+ is_credit = self .document_type == "CreditNote"
815+ inv_total = abs (flt (self .invoice .total , 2 )) if is_credit else flt (self .invoice .total , 2 )
816+ inv_net_total = abs (flt (self .invoice .net_total , 2 )) if is_credit else flt (self .invoice .net_total , 2 )
817+ inv_grand_total = (
818+ abs (flt (self .invoice .grand_total , 2 )) if is_credit else flt (self .invoice .grand_total , 2 )
819+ )
820+
788821 # Line Extension Amount (BT-106) - Sum of Invoice line net amount (BEFORE discount)
789822 # Use invoice.total which is the sum of all item.amount values
790823 line_total = ET .SubElement (legal_total , f"{{{ self .namespaces ['cbc' ]} }}LineExtensionAmount" )
791- line_total .text = str ( flt ( self . invoice . total , 2 ))
824+ line_total .text = f" { inv_total :.2f } "
792825 line_total .set ("currencyID" , self .invoice .currency )
793826
794827 # Tax Exclusive Amount (BT-109) - Invoice total amount without VAT
795828 # This equals LineExtensionAmount - AllowanceTotalAmount = net_total (after discount, before tax)
796829 tax_exclusive_amount = ET .SubElement (legal_total , f"{{{ self .namespaces ['cbc' ]} }}TaxExclusiveAmount" )
797- tax_exclusive_amount .text = str ( flt ( self . invoice . net_total , 2 ))
830+ tax_exclusive_amount .text = f" { inv_net_total :.2f } "
798831 tax_exclusive_amount .set ("currencyID" , self .invoice .currency )
799832
800833 # Tax Inclusive Amount (BT-112) - Invoice total amount with VAT
801834 tax_inclusive_amount = ET .SubElement (legal_total , f"{{{ self .namespaces ['cbc' ]} }}TaxInclusiveAmount" )
802- tax_inclusive_amount .text = str ( flt ( self . invoice . grand_total , 2 ))
835+ tax_inclusive_amount .text = f" { inv_grand_total :.2f } "
803836 tax_inclusive_amount .set ("currencyID" , self .invoice .currency )
804837
805838 # Allowance Total Amount (BT-107) - Sum of allowances on document level (discount)
806839 # 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 )
840+ allowance_amount = inv_total - inv_net_total
808841 allowance_total = ET .SubElement (legal_total , f"{{{ self .namespaces ['cbc' ]} }}AllowanceTotalAmount" )
809- allowance_total .text = str ( flt (allowance_amount , 2 ))
842+ allowance_total .text = f" { flt (allowance_amount , 2 ):.2f } "
810843 allowance_total .set ("currencyID" , self .invoice .currency )
811844
812845 # Charge Total Amount (BT-108) - Sum of charges on document level
813846 # Must come AFTER AllowanceTotalAmount per XSD schema order
814847 actual_charge_total = sum (tax .tax_amount for tax in self .invoice .taxes if tax .charge_type == "Actual" )
848+ if is_credit :
849+ actual_charge_total = abs (actual_charge_total )
815850 if actual_charge_total :
816851 charge_total = ET .SubElement (legal_total , f"{{{ self .namespaces ['cbc' ]} }}ChargeTotalAmount" )
817- charge_total .text = str ( flt (actual_charge_total , 2 ))
852+ charge_total .text = f" { flt (actual_charge_total , 2 ):.2f } "
818853 charge_total .set ("currencyID" , self .invoice .currency )
819854 else :
820855 # Always add ChargeTotalAmount (set to 0.00 if no charges)
@@ -825,24 +860,22 @@ def _set_totals(self):
825860 # Prepaid Amount (BT-113) - Sum of amounts already paid
826861 # This is required when PayableAmount != TaxInclusiveAmount to satisfy BR-CO-16:
827862 # PayableAmount = TaxInclusiveAmount - PrepaidAmount + RoundingAmount
828- if self . document_type != "CreditNote" :
863+ if not is_credit :
829864 prepaid_value = flt (self .invoice .grand_total , 2 ) - flt (self .invoice .outstanding_amount , 2 )
830865 if prepaid_value > 0 :
831866 prepaid_amount = ET .SubElement (legal_total , f"{{{ self .namespaces ['cbc' ]} }}PrepaidAmount" )
832- prepaid_amount .text = str ( flt (prepaid_value , 2 ))
867+ prepaid_amount .text = f" { flt (prepaid_value , 2 ):.2f } "
833868 prepaid_amount .set ("currencyID" , self .invoice .currency )
834869
835870 # Payable Amount (BT-115) - Amount due for payment
836- # For credit notes, this should be negative (amount to be credited)
837871 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 )
872+ if is_credit :
873+ # For credit notes, use positive grand_total (amount to be credited)
874+ payable_value = inv_grand_total
842875 else :
843876 # For invoices, use outstanding_amount
844877 payable_value = flt (self .invoice .outstanding_amount , 2 )
845- payable_amount .text = str ( payable_value )
878+ payable_amount .text = f" { payable_value :.2f } "
846879 payable_amount .set ("currencyID" , self .invoice .currency )
847880
848881 def initialize_peppol_xml (self ) -> ET .Element :
@@ -960,12 +993,31 @@ def get_invoice_type_code(self, invoice) -> str:
960993 if hasattr (invoice , "is_return" ) and invoice .is_return :
961994 invoice_type_code = "381"
962995 elif hasattr (invoice , "amended_from" ) and invoice .amended_from :
963- invoice_type_code = "384"
996+ # 384 (Corrected Invoice) only allowed when both parties are German
997+ if self ._both_parties_german ():
998+ invoice_type_code = "384"
964999 except Exception :
9651000 pass
9661001
9671002 return invoice_type_code
9681003
1004+ def _both_parties_german (self ) -> bool :
1005+ # Check if both seller and buyer are German organizations
1006+ try :
1007+ seller_code = ""
1008+ if self .seller_address and self .seller_address .country :
1009+ seller_code = (
1010+ frappe .db .get_value ("Country" , self .seller_address .country , "code" ) or ""
1011+ ).upper ()
1012+ buyer_code = ""
1013+ if self .buyer_address and self .buyer_address .country :
1014+ buyer_code = (
1015+ frappe .db .get_value ("Country" , self .buyer_address .country , "code" ) or ""
1016+ ).upper ()
1017+ return seller_code == "DE" and buyer_code == "DE"
1018+ except Exception :
1019+ return False
1020+
9691021 def map_unit_code (self , erpnext_unit : str ) -> str :
9701022 # Map ERPNext unit codes to PEPPOL standard unit codes using CommonCodeRetriever
9711023 if not erpnext_unit :
0 commit comments