stephanstapel / ZUGFeRD-csharp

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

X-Rechnung 3.1 UBL for Deutsche Bahn #254

Closed goedo closed 5 months ago

goedo commented 5 months ago

First a big THANK you for your work! Amazing...

I was playing around to send some invoices to the DB, validating with https://xre.deutschebahn.com... I succeeded doing some changes with you possibily have alread in mind, so I list them here.

Interesting: the Kosit validator gives some errors; if I fix them, the DB validator does not run OK: image

The DB validator gives only a warning about payment means, but this seems to be a bug of the validator, the invoices are accepted.

Dont kill me not sending a pull request; I'm not sure if my issue is of public interest, so before boring you, I'd like you to have a short overview to the following changes I had to do. Possibly they are completely against your best practice... or already on your to-do list... or I'm inventing the UBL format for you :-)

Just let me know, if I can contribute, I'd be more than happy!

I send some lines with comments an I attach a current diff.

Hopefully this helps in any way... Take care, Roger


InvoiceDescriptor22Writer.cs:

263
                   // BT-148                 
                   if (tradeLineItem.GrossUnitPrice.HasValue || (tradeLineItem.GetTradeAllowanceCharges().Count > 0)
                        && !tradeLineItem.NetUnitPrice.HasValue // roger

268
                        //_writeOptionalAmount(Writer, "ram:ChargeAmount", tradeLineItem.GrossUnitPrice, 4); 
                        _writeOptionalAmount(Writer, "ram:ChargeAmount", tradeLineItem.GrossUnitPrice, 2); //roger

285
                            #region ChargePercentage
                            /*
                            //roger not allowed
                            if (tradeAllowanceCharge.ChargePercentage.HasValue)
                            {
                                Writer.WriteStartElement("ram:CalculationPercent", profile: Profile.Extended | Profile.XRechnung); 
                                Writer.WriteValue(_formatDecimal(tradeAllowanceCharge.ChargePercentage.Value, 2));
                                Writer.WriteEndElement();
                            }
                            */
                            #endregion

340
                    //_writeOptionalAmount(Writer, "ram:ChargeAmount", tradeLineItem.NetUnitPrice, 4);
                    _writeOptionalAmount(Writer, "ram:ChargeAmount", tradeLineItem.NetUnitPrice, 2);//roger
(I had to group by VAT, otherwise I had one VAT line per allowance on the invoice...)

404
                if (tradeLineItem.TaxCategoryCode != TaxCategoryCodes.O) // notwendig, damit die Validierung klappt
                {
                    Writer.WriteElementString("ram:RateApplicablePercent", _formatDecimal(tradeLineItem.TaxPercent));
                }
                //roger
                if (tradeLineItem.LineTotalAmount.HasValue) 
               // -->I had to add LineTotalAmount to desc.addtradeLineItem in InvoiceDescriptor.cs
                {
                    Writer.WriteStartElement("ram:BasisAmount", profile: Profile.Extended); // not in XRechnung, according to CII-SR-123
                    Writer.WriteValue(_formatDecimal(tradeLineItem.LineTotalAmount.Value, 2));
                    Writer.WriteEndElement();
                } // !roger

later...
                    //_writeOptionalAmount(Writer, "ram:ChargeAmount", tradeLineItem.NetUnitPrice, 4);
                    _writeOptionalAmount(Writer, "ram:ChargeAmount", tradeLineItem.NetUnitPrice, 2);//roger

and
                if (tradeLineItem.TaxCategoryCode != TaxCategoryCodes.O) // notwendig, damit die Validierung klappt
                {
                    Writer.WriteElementString("ram:RateApplicablePercent", _formatDecimal(tradeLineItem.TaxPercent));
                }
                //roger
                if (tradeLineItem.LineTotalAmount.HasValue)
                {
                    Writer.WriteStartElement("ram:BasisAmount", profile: Profile.Extended); // not in XRechnung, according to CII-SR-123
                    Writer.WriteValue(_formatDecimal(tradeLineItem.LineTotalAmount.Value, 2));
                    Writer.WriteEndElement();
                }

-----------------------------------------
        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()

diff.txt

goedo commented 5 months ago

.. I forgot: in Tax.cs: return System.Math.Round(0.01m this.Percent this.BasisAmount, 2, MidpointRounding.AwayFromZero);

stephanstapel commented 5 months ago

Thanks for sharing. I will take a deeper look in the coming days. Happy to accept e.g. the Tax correction - did you find the rounding rule in the documentation?

Concerning the validators: I made the same observations. Generally, one would say that the KOSIT validator is key but if your customer doesn't accept your invoice because of his own (different) validator, you are busted.

goedo commented 5 months ago

Hi! Thanks, I was afraid of doing something wrong. However, I saw that my amends shall be corrected, however; as soon I will get correct results, I'll try to apply your mechanisms to write data depending on the profile and doing calcs in the subclasses...

See you...


{
    lastvalidationmessage = "";
    try
    {
        IList<string> messages = new List<string>();
        string assemblyFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
        string schemaroot = assemblyFolder + "/xsd/" + xrechnungAngaben.xmlInvoiceSchemaroot;

        Console.WriteLine("Validating: " + target);
        XmlReaderSettings validationSettings = new XmlReaderSettings();
        validationSettings.Schemas.Add("urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100", @schemaroot + @"FACTUR-X_EN16931.xsd");
        validationSettings.Schemas.Add("urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100", @schemaroot + @"FACTUR-X_EN16931_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd");
        validationSettings.Schemas.Add("urn:un:unece:uncefact:data:standard:QualifiedDataType:100", @schemaroot + @"FACTUR-X_EN16931_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd");
        validationSettings.Schemas.Add("urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100", @schemaroot + @"FACTUR-X_EN16931_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd");
        validationSettings.ValidationType = ValidationType.Schema;
        XmlReader reader = XmlReader.Create(target, validationSettings);
        XmlDocument document = new XmlDocument();
        document.Load(reader);
        XPathNavigator navigator = document.CreateNavigator();
        ValidationEventHandler validation = new ValidationEventHandler(ValidationEventHandler);

        validationSettings.ValidationEventHandler += new ValidationEventHandler(ValidationEventHandler);
        validationSettings.ValidationFlags |= XmlSchemaValidationFlags.ProcessInlineSchema;
        validationSettings.ValidationFlags |= XmlSchemaValidationFlags.ReportValidationWarnings;
        validationSettings.ValidationFlags |= XmlSchemaValidationFlags.AllowXmlAttributes;
        validationSettings.ValidationFlags |= XmlSchemaValidationFlags.ProcessInlineSchema;
        document.Validate(validation);
        Console.WriteLine();
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
        return 1;
    }
    return 0;
}

static void ValidationEventHandler(object sender, ValidationEventArgs e)
{
    if (e.Severity == XmlSeverityType.Warning)
    {
        Tools.WriteLog(String.Format("E=>{0}", e.Message), "E");
        validateRC = 1;
    }
    else if (e.Severity == XmlSeverityType.Error)
    {
        Tools.WriteLog(String.Format("E=>{0}", e.Message), "E");
        lastvalidationmessage = e.Message;
        validateRC = 2;
    }
}
stephanstapel commented 5 months ago

broken down into

255, #256, #257, #258, #259, #260, #261, #262

stephanstapel commented 5 months ago

Closing this ticket in favor to more specific tickets. I'd be more than happy to add the validator code as soon you have it ready!

stephanstapel commented 5 months ago

@goedo : I added some comments to the new "broken down" tickets. Could you please answer there?

goedo commented 5 months ago

Hi Stephan, I will review all changes, to avoid that they are residual from early testing.

Will take me some time, I expect to be done tomorrow evening.

BRGDS

Roger

Von: Stephan @.> Gesendet: Montag, 6. Mai 2024 17:07 An: stephanstapel/ZUGFeRD-csharp @.> Cc: goedo @.>; Mention @.> Betreff: Re: [stephanstapel/ZUGFeRD-csharp] X-Rechnung 3.1 UBL for Deutsche Bahn (Issue #254)

@goedo https://github.com/goedo : I added some comments to the new "broken down" tickets. Could you please answer there?

— Reply to this email directly, view it on GitHub https://github.com/stephanstapel/ZUGFeRD-csharp/issues/254#issuecomment-2096258360 , or unsubscribe https://github.com/notifications/unsubscribe-auth/AEYZBNJOOIGZEWKZK5XP6ATZA6MDBAVCNFSM6AAAAABHHKHA3CVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDAOJWGI2TQMZWGA . You are receiving this because you were mentioned. https://github.com/notifications/beacon/AEYZBNIDILKDA7LE44WMKJLZA6MDBA5CNFSM6AAAAABHHKHA3CWGG33NNVSW45C7OR4XAZNMJFZXG5LFINXW23LFNZ2KUY3PNVWWK3TUL5UWJTT46JOTQ.gif Message ID: @. @.> >