Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

X-Rechnung 3.1 UBL for Deutsche Bahn - writing taxes #261

Open
stephanstapel opened this issue May 6, 2024 · 12 comments
Open

X-Rechnung 3.1 UBL for Deutsche Bahn - writing taxes #261

stephanstapel opened this issue May 6, 2024 · 12 comments

Comments

@stephanstapel
Copy link
Owner

broken down from #254, submitted by @goedo

InvoiceDescriptor22Writer.cs

  private void _writeOptionalTaxesNew(ProfileAwareXmlTextWriter writer)
        {
            decimal lineTotal = 0m;
            List<Tax> taxes = new List<Tax>();
            foreach (Tax tax in this.Descriptor.Taxes)
            {
                Tax t = taxes.Find(x => x.CategoryCode == tax.CategoryCode);
                if (t == null) // neu
                {
                    t = new Tax();
                    t.ExemptionReason = tax.ExemptionReason;
                    t.Percent = tax.Percent;
                    t.AllowanceChargeBasisAmount = tax.AllowanceChargeBasisAmount;
                    t.BasisAmount = tax.BasisAmount; // as is
                    t.ExemptionReasonCode = tax.ExemptionReasonCode;
                    t.AllowanceChargeBasisAmount = tax.AllowanceChargeBasisAmount;
                    t.TypeCode = tax.TypeCode;
                    taxes.Add(t);
                }
                else // vorhanden
                {
                    t.AllowanceChargeBasisAmount += tax.AllowanceChargeBasisAmount;
                    t.BasisAmount += tax.BasisAmount; // addieren
                    t.AllowanceChargeBasisAmount += tax.AllowanceChargeBasisAmount;
                }

            }
            foreach (Tax tax in taxes)
            {
                writer.WriteStartElement("ram:ApplicableTradeTax");

                writer.WriteStartElement("ram:CalculatedAmount");
                writer.WriteValue(_formatDecimal(tax.TaxAmount));
                writer.WriteEndElement(); // !CalculatedAmount

                writer.WriteElementString("ram:TypeCode", tax.TypeCode.EnumToString());
                writer.WriteOptionalElementString("ram:ExemptionReason", tax.ExemptionReason);
                writer.WriteStartElement("ram:BasisAmount");
                writer.WriteValue(_formatDecimal(tax.BasisAmount));
                writer.WriteEndElement(); // !BasisAmount

                if (tax.CategoryCode.HasValue)
                {
                    writer.WriteElementString("ram:CategoryCode", tax.CategoryCode?.EnumToString());
                }
                
                if (tax.ExemptionReasonCode.HasValue)
                {
                    writer.WriteElementString("ram:ExemptionReasonCode", tax.ExemptionReasonCode?.EnumToString());
                }

                writer.WriteElementString("ram:RateApplicablePercent", _formatDecimal(tax.Percent));
                writer.WriteEndElement(); // !RateApplicablePercent
            }
        } // !_writeOptionalTaxesNew()
@stephanstapel
Copy link
Owner Author

sum up taxes, probably easier with LINQ

@goedo
Copy link

goedo commented May 7, 2024

Hi!
// 11. ApplicableTradeTax (optional)
_writeOptionalTaxes(Writer);
//_writeOptionalTaxesNew(Writer); //check

With the original _writeOptionalTaxes, I get
image

With my "new" instead
image

I'm still trying to fix it; even when DB accepts the invoice in both cases, Kosit fails for both, but then "new" is less erroneous :-)

Possibly it would be better to move this from issue to discussion?

As far as I suspect some roundings or value misunderstanding, I'm experimenting a lot.
Current trial:

        private void _writeOptionalTaxesNew(ProfileAwareXmlTextWriter writer)
        {
            List<BG23> BG23List = new List<BG23>();
            // collect tax info
            foreach (Tax tax in this.Descriptor.Taxes) 
            {
                // new bg32 object
                BG23 bg23 = new BG23(); 
                
                bg23.AllowanceChargeBasisAmount = tax.AllowanceChargeBasisAmount;
                bg23.Percent = tax.Percent;
                bg23.ExemptionReasonCode = tax.ExemptionReasonCode;
                bg23.ExemptionReason = tax.ExemptionReason;
                bg23.BasisAmount = tax.BasisAmount;
                bg23.TaxAmount = tax.TaxAmount;
                bg23.CategoryCode = tax.CategoryCode;
                bg23.TypeCode = tax.TypeCode;
                BG23 checkBG23 = BG23List.Find(x => x.CategoryCode == bg23.CategoryCode);
                // add BT values 
                if (checkBG23 == null) // neu
                {
                    bg23.BT116 = tax.BasisAmount;
                    bg23.TypeCode = bg23.TypeCode;
                    bg23.BT131 = tax.BasisAmount;
                    BG23List.Add(bg23);
                }
                else // vorhanden
                {
                    bg23 = checkBG23;
                    bg23.BT131 += tax.BasisAmount;
                    bg23.BT116 += tax.BasisAmount;
                    bg23.TaxAmount += tax.TaxAmount;
                }
                // BT-116 sum of Invoice line net amounts (BT-131) plus the sum of document level charge amounts (BT-99) minus the sum of document level allowance amounts (BT-92) 
                Console.WriteLine(string.Format("1 BG-23 BT-116({0} BT-131({1})", bg23.BT116, bg23.BT131));
            }
            foreach (var c in this.Descriptor.GetTradeAllowanceCharges())
            {
                BG23 bg23 = BG23List.Find(x => x.CategoryCode == c.Tax.CategoryCode);
                if (bg23 == null)
                {
                    // cannot be!
                    throw (new Exception("BG23List error"));
                }
                if (c.ChargeIndicator == true)
                {
                    bg23.BT99 += c.ActualAmount;
                    bg23.BT116 += c.ActualAmount;
                }
                else
                {
                    bg23.BT92 += c.ActualAmount;
                    bg23.BT116 -= c.ActualAmount;
                }
                Console.WriteLine(string.Format("2 BG-23 BT-116({0} BT-131({1})", bg23.BT116, bg23.BT131));
            }
            foreach (BG23 bg23 in BG23List)
            {
#if DEBUG
                Console.WriteLine(string.Format("BG-23 BT-116({0}/{1})", bg23.BT116, bg23.BasisAmount));
#endif
                writer.WriteStartElement("ram:ApplicableTradeTax");

                writer.WriteStartElement("ram:CalculatedAmount");
                writer.WriteValue(_formatDecimal(bg23.TaxAmount));
                writer.WriteEndElement(); // !CalculatedAmount

                writer.WriteElementString("ram:TypeCode", bg23.TypeCode.EnumToString());
                writer.WriteOptionalElementString("ram:ExemptionReason", bg23.ExemptionReason);
                writer.WriteStartElement("ram:BasisAmount");
                // sum of Invoice line net amounts (BT-131) plus the sum of document level charge amounts (BT-99) minus the sum of document level allowance amounts (BT-92) 
                writer.WriteValue(_formatDecimal(bg23.BT131));
                //writer.WriteValue(_formatDecimal(bg23.BT116));
                writer.WriteEndElement(); // !BasisAmount

                if (bg23.CategoryCode.HasValue)
                {
                    writer.WriteElementString("ram:CategoryCode", bg23.CategoryCode?.EnumToString());
                }
                
                if (bg23.ExemptionReasonCode.HasValue)
                {
                    writer.WriteElementString("ram:ExemptionReasonCode", bg23.ExemptionReasonCode?.EnumToString());
                }

                writer.WriteElementString("ram:RateApplicablePercent", _formatDecimal(bg23.Percent));
                writer.WriteEndElement(); // !RateApplicablePercent
            }
        } // !_writeOptionalTaxesNew()

and a new class

public class BG23 : Tax
{
    public new decimal TaxAmount { get; set; } // comes from tax
    public decimal BT116 { get; set; } // comes from tax
    public decimal BT131 { get; set; } // comes from tax
    public decimal BT99 { get; set; }
    public decimal BT92 { get; set; }
}

@stephanstapel
Copy link
Owner Author

Are you sure that grouping the taxes by category code is correct?
I guess that at least the percent need to be taken into account, otherwise the result will be wrong.
And also, I thought the idea behind the taxes is that you can set different exemption reasons and type codes for each tax.

@goedo
Copy link

goedo commented May 15, 2024 via email

@stephanstapel
Copy link
Owner Author

yep, good link!

The text says: "Sum of all taxable amounts subject to a specific VAT category code and VAT category rate (if the VAT category rate is applicable)."

I.e. this would be the taxes grouped by category and rate (percentage). This reflects the data structure of Peppol which only comes with these two categorizations:

grafik

XRechnung comes with more detailed categorizations:

grafik

i.e. I guess in this case, the grouping must happen by

  • category code
  • category rate
  • exemption reason code (if existing)

@goedo
Copy link

goedo commented May 20, 2024

Hi I post the current version (which in combination with the linetotalamoun change works). Don't forget #269...Without the changes in invoicedesriptor, thus providing the correct LineTotalAmount, it will not work.

    private void _writeOptionalTaxesNew(ProfileAwareXmlTextWriter writer)
    {
        List<BG23> BG23List = new List<BG23>();
        // collect tax info
        foreach (Tax tax in this.Descriptor.Taxes) 
        {
            // new bg32 object
            BG23 bg23 = new BG23(); 
            
            bg23.AllowanceChargeBasisAmount = tax.AllowanceChargeBasisAmount;
            bg23.Percent = tax.Percent;
            bg23.ExemptionReasonCode = tax.ExemptionReasonCode;
            bg23.ExemptionReason = tax.ExemptionReason;
            bg23.BasisAmount = tax.BasisAmount;
            bg23.TaxAmount = tax.TaxAmount;
            bg23.CategoryCode = tax.CategoryCode;
            bg23.TypeCode = tax.TypeCode;
            BG23 checkBG23 = BG23List.Find(x => x.CategoryCode == bg23.CategoryCode);
            // add BT values 
            if (checkBG23 == null) // neu
            {
                bg23.BT116 = tax.BasisAmount;
                bg23.TypeCode = bg23.TypeCode;
                bg23.BT131 = tax.BasisAmount;
                BG23List.Add(bg23);
            }
            else // vorhanden
            {
                bg23 = checkBG23;
                bg23.BT131 += tax.BasisAmount;
                bg23.BT116 += tax.BasisAmount;
                bg23.BasisAmount += tax.BasisAmount;
                bg23.TaxAmount += tax.TaxAmount;
            }
            // BT-116 sum of Invoice line net amounts (BT-131) plus the sum of document level charge amounts (BT-99) minus the sum of document level allowance amounts (BT-92) 
            //Console.WriteLine(string.Format("1 BG-23 BT-116:{0} BT-131:{1} BT-117:{2})", bg23.BT116, bg23.BT131, bg23.TaxAmount));
        }
        foreach (var c in this.Descriptor.GetTradeAllowanceCharges())
        {
            BG23 bg23 = BG23List.Find(x => x.CategoryCode == c.Tax.CategoryCode);
            if (bg23 == null)
            {
                // cannot be!
                throw (new Exception("BG23List error"));
            }
            if (c.ChargeIndicator == true)
            {
                bg23.BT99 += c.ActualAmount;
                bg23.BT116 += c.ActualAmount;
            }
            else
            {
                bg23.BT92 += c.ActualAmount;
                bg23.BT116 -= c.ActualAmount;
            }
            //Console.WriteLine(string.Format("2 BG-23 BT-116:{0} BT-131:{1} BT-117:{2})", bg23.BT116, bg23.BT131, bg23.TaxAmount));
        }
        foreach (BG23 bg23 in BG23List)
        {

#if DEBUG
//Console.WriteLine(string.Format("BG-23 BT-116: {0}/BT-131: {1})", bg23.BT116, bg23.BT131));
#endif
writer.WriteStartElement("ram:ApplicableTradeTax");

            writer.WriteStartElement("ram:CalculatedAmount");
            writer.WriteValue(_formatDecimal(bg23.TaxAmount));
            writer.WriteEndElement(); // !CalculatedAmount

            writer.WriteElementString("ram:TypeCode", bg23.TypeCode.EnumToString());
            writer.WriteOptionalElementString("ram:ExemptionReason", bg23.ExemptionReason);
            writer.WriteStartElement("ram:BasisAmount");
            // sum of Invoice line net amounts (BT-131) plus the sum of document level charge amounts (BT-99) minus the sum of document level allowance amounts (BT-92) 
            writer.WriteValue(_formatDecimal(bg23.BT131));
            //writer.WriteValue(_formatDecimal(bg23.BT116));
            writer.WriteEndElement(); // !BasisAmount

            if (bg23.CategoryCode.HasValue)
            {
                writer.WriteElementString("ram:CategoryCode", bg23.CategoryCode?.EnumToString());
            }
            
            if (bg23.ExemptionReasonCode.HasValue)
            {
                writer.WriteElementString("ram:ExemptionReasonCode", bg23.ExemptionReasonCode?.EnumToString());
            }

            writer.WriteElementString("ram:RateApplicablePercent", _formatDecimal(bg23.Percent));
            writer.WriteEndElement(); // !RateApplicablePercent
        }
    } // !_writeOptionalTaxesNew()

@stephanstapel
Copy link
Owner Author

The code above can't be correct. E.g. it wouldn't work with two VAT rates.
Also, if different type codes or exemption reasons are used, they also would be wiped out.

@goedo
Copy link

goedo commented May 21, 2024

Hi, I prototyped this having to output my actual invoices ... I will now verify with a standard test case (three different taxes, like a Hotel bill 19% 7% 0%) and verify the code. I was not sure if developing it furthermore was desired... so please give me some day... TIA!

@goedo
Copy link

goedo commented May 22, 2024

Yep. IThanks for your patience! Instead of

            BG23 checkBG23 = BG23List.Find(x => x.CategoryCode == bg23.CategoryCode);

put

            BG23 checkBG23 = BG23List.Find(x => x.CategoryCode == bg23.CategoryCode && x.Percent == bg23.Percent && x.ExemptionReasonCode == bg23.ExemptionReasonCode

and it works.

Testcase was
image
giving
image

@stephanstapel
Copy link
Owner Author

cool, that sounds more reasonable now.
Could you test one last thing? Could you try two VATs with same category code, same percentage, same exemption reason code but different exemption reason messages? I wonder if this also needs to be a grouping criteria.

@stephanstapel
Copy link
Owner Author

I took another look at the code. I now understand better that you add/reduce the tax that is given in the TradeAllowanceCharge from the global Tax (of same category, percentage etc.).
However, how does one know if the user that fills in the values in his application did not already take the TradeAllowanceCharge into account when filling the document level Tax?

@goedo
Copy link

goedo commented May 23, 2024

Hi! Ich switch mal auf Deutsch, wird mir sonst zu mühsam... :-)
Wenn es um BT-92 geht: ich mache im aufrufenden Programm die Berechnung, um dem User die Mühe abzunehmen. Macht im Endeffekt das ERP, wo die Rechnung herkommt, ja auch.

                        if (allowancePerc > 0)
                        {
                            desc.AllowanceTotalAmount += rabatt;                                
                            decimal? preisrabatt = tradeLineItem.GrossUnitPrice - tradeLineItem.NetUnitPrice;
                            tradeLineItem.AddTradeAllowanceCharge(true, desc.Currency, neuesbrutto, (decimal)preisrabatt, allowancePerc, "Rabatt");
                            desc.AddTradeAllowanceCharge(true, altesbrutto, desc.Currency, (decimal)rabatt, allowancePerc, "Rabatt2", tradeLineItem.TaxType, tradeLineItem.TaxCategoryCode, tradeLineItem.TaxPercent);
                            desc.AddApplicableTradeTax(tax.BasisAmount, tax.Percent, tax.TypeCode, tax.CategoryCode, (decimal)rabatt, null, null);
                        }
                        if (allowancePerc == 0)
                        {
                            desc.AllowanceTotalAmount += rabatt;                                
                            decimal? preisrabatt = tradeLineItem.GrossUnitPrice - tradeLineItem.NetUnitPrice;
                            tax.BasisAmount = (decimal)neuesbrutto;
                            desc.AddApplicableTradeTax(tax.BasisAmount, tax.Percent, tax.TypeCode, tax.CategoryCode, (decimal)rabatt, null, null);
                        }

Hier könnte man - auf deine Frage hin - einen Fehler bei Abweichung der Eingabe zur Berechnung ausgeben-
Da der Validator sowieso den Gesamtwert mit der Summe der einzelnen Positionen abgleicht, müssen die Zahlen sowieso übereinstimmen.

Insofern: wenn überhaupt, dann den Fehler auf Anwendungsebene managen und nicht erst nach gescheiterter Validierung... ist sicher Geschmacksache.
VG

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants