stephanstapel / ZUGFeRD-csharp

C# assembly for creating and reading ZUGFeRD invoices
Apache License 2.0
169 stars 93 forks source link

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

Open stephanstapel opened 4 months ago

stephanstapel commented 4 months ago

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 commented 4 months ago

sum up taxes, probably easier with LINQ

goedo commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

Hi! No I'm not sure, a bit confused, see

https://docs.peppol.eu/poacc/billing/3.0/syntax/ubl-invoice/cac-TaxTotal/cac-TaxSubtotal/

However, with my current output, the invoices are accepted by DB. Could be shortsighted... With the original code was not accepted... I'll try it again, later, a bit busy today

Stephan @.***> schrieb am Di., 14. Mai 2024, 21:04:

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.

— Reply to this email directly, view it on GitHub https://github.com/stephanstapel/ZUGFeRD-csharp/issues/261#issuecomment-2110951230, or unsubscribe https://github.com/notifications/unsubscribe-auth/AEYZBNOBYYCJ7TDUOOWAYCDZCJN4VAVCNFSM6AAAAABHJELPFKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCMJQHE2TCMRTGA . You are receiving this because you were mentioned.Message ID: @.***>

stephanstapel commented 4 months ago

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

goedo commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

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