stephanstapel / ZUGFeRD-csharp

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

XRechnung UBL PaymentTerms with LineBreak #388

Closed quotschmacher closed 4 weeks ago

quotschmacher commented 1 month ago

Should the UBL-Writer respect Business Rules?

image

So it should look like

  <cac:PaymentTerms>
    <cbc:Note>
      #SKONTO#TAGE=14#PROZENT=0.00#BASISBETRAG=219.80#
    </cbc:Note>
  </cac:PaymentTerms>

and the actual implementation produces:

  <cac:PaymentTerms>
    <cbc:Note>#SKONTO#TAGE=14#PROZENT=0.00#BASISBETRAG=219.80#</cbc:Note>
  </cac:PaymentTerms>

If I add a verbatim string with line break at the beginning and end, the result looks like

  <cac:PaymentTerms>
    <cbc:Note>
#SKONTO#TAGE=14#PROZENT=0.00#BASISBETRAG=219.80#</cbc:Note>
  </cac:PaymentTerms>

So the line break at the end is ignored.

Do you think this is a thing? Is the Writer used able to produce this?

stephanstapel commented 1 month ago

good question. As far as I understand the xml generation capabilities of .net, this is not feasible. We could implement a dirty way however, inserting a custom string CUSTOMLINEBREAK and replace this value in IInvoiceDescriptorWriter by Environment.NewLine.

Quite a hack. What do you think?

quotschmacher commented 1 month ago

It is not done with newline I think. The indentation is a thing, too. And this could be multiline given multiple Skonto-terms. And I am not a friend of such string-replacements ;-) But if it could work...

stephanstapel commented 1 month ago

I switch to XmlWriter as a base class for generating XML. I didn't push the change yet, but if you could do this:

grafik

could you solve the problem mentioned above?

quotschmacher commented 1 month ago

i am getting the desired result

  <cac:PaymentTerms>
    <cbc:Note>
      #SKONTO#TAGE=14#PROZENT=0.00#BASISBETRAG=219.80#
      #SKONTO#TAGE=14#PROZENT=0.00#BASISBETRAG=219.80#
    </cbc:Note>
  </cac:PaymentTerms

with something like this in the UBL-Writer:

            // PaymentTerms (optional)
            if (this.Descriptor.GetTradePaymentTerms().Where(x => !string.IsNullOrEmpty(x.Description)).ToList().Count > 0)
            {
                Writer.WriteStartElement("cac", "PaymentTerms");                
                var sbPaymentNotes = new StringBuilder();

                Writer.WriteStartElement("cbc", "Note");
                foreach (PaymentTerms paymentTerms in this.Descriptor.GetTradePaymentTerms().Where(x => !string.IsNullOrEmpty(x.Description)))
                {
                    //if (paymentTerms.Description.StartsWith("#") && paymentTerms.Description.EndsWith("#"))
                    {
                        Writer.WriteRawStringWithIndentation(Environment.NewLine);
                        Writer.WriteRawStringWithIndentation(paymentTerms.Description, 6);
                    }
                    //sbPaymentNotes.AppendLine(paymentTerms.Description);
                }
                //Writer.WriteOptionalElementString("cbc", "Note", sbPaymentNotes.ToString().TrimEnd());
                //Writer.WriteRawStringWithIndentation(Environment.NewLine);
                //Writer.WriteRawStringWithIndentation(sbPaymentNotes.ToString(), 6);
                Writer.WriteRawStringWithIndentation(Environment.NewLine);
                Writer.WriteEndElement(4);
                Writer.WriteEndElement();
            }

One new Method in the XML-Writer:

        public void WriteRawStringWithIndentation(string data, int indentation = 0)
        {
            for (int i = 0; i < indentation; i++)
            {
                this.TextWriter?.WriteWhitespace(" ");
            }
            this.TextWriter?.WriteString(data);
        }

and extending the WriteEndElement method like this:

        public void WriteEndElement(int indentation = 0)
        {
            StackInfo infoForCurrentXmlLevel = this.XmlStack.Pop();
            if (_DoesProfileFitToCurrentProfile(infoForCurrentXmlLevel.Profile) && _IsNodeVisible())
            {
                for (int i = 0; i < indentation; i++)
                {
                    this.TextWriter?.WriteWhitespace(" ");
                }
                this.TextWriter?.WriteEndElement();
            }
        }

But I am not completely happy with it (because of the fixed indentation values) - but it works.

stephanstapel commented 1 month ago

I agree with your unhappiness :) Would it be possible to use the XmlStack for indention? And where is WriteEndElement() put if you don't add indention?

quotschmacher commented 1 month ago

Without the manual indentation the result is the following:

  <cac:PaymentTerms>
    <cbc:Note>
      #SKONTO#TAGE=14#PROZENT=0.00#BASISBETRAG=209.53#
</cbc:Note>
  </cac:PaymentTerms>

Would it be possible to use the XmlStack for indention?

Surely. Then one could pass a bool that there should be an additional indentation which is 2 * this.XmlStack.Count()...

stephanstapel commented 1 month ago

I guess this is as slick as possible: https://github.com/stephanstapel/ZUGFeRD-csharp/commit/014e180fe001777851538c41808a78ecc384a35a Unfortunately it leaves open how to properly indent the closing tag:

grafik

How about modifying WriteEndElement() and take into account if the previous line was a raw line?

quotschmacher commented 1 month ago

So when writing a raw text string setting a private boolean, that then could be evaluated in the WriteEndElement method (newline, indentation) and be reset?

stephanstapel commented 1 month ago

So when writing a raw text string setting a private boolean, that then could be evaluated in the WriteEndElement method (newline, indentation) and be reset?

yes, like this. Will do that later.

stephanstapel commented 1 month ago

Ignore the content that is displayed here, but is this the intended behaviour:

image

?

Code is committed. Does the requirement only count for UBL format or also for CII? Could you check this?

quotschmacher commented 1 month ago

Just tested the CII-export and it is coming out like this (I did not insert it with a linebreak):

      <ram:SpecifiedTradePaymentTerms>
        <ram:Description>#SKONTO#TAGE=14#PROZENT=0.00#BASISBETRAG=219.80#
</ram:Description>
      </ram:SpecifiedTradePaymentTerms>

But the validator did not say anything about this. So it just does not look very good...

stephanstapel commented 1 month ago

@quotschmacher : thanks! Could you share the generation code here? I'd like to check if the library can do better.

stephanstapel commented 1 month ago

@quotschmacher : I have checked in some code and added a test case (UBL only). Can you please check this?

quotschmacher commented 1 month ago

@quotschmacher : thanks! Could you share the generation code here? I'd like to check if the library can do better.

no special generation code - simply adding

result.AddTradePaymentTerms(Invariant($"#SKONTO#TAGE={zahlungsziel}#PROZENT={skonto:F2}#BASISBETRAG={basisbetrag:F2}#"));

TradePaymentTerms than than outputting and CII-invoice.

@quotschmacher : I have checked in some code and added a test case (UBL only). Can you please check this?

I hope I will get it done tomorrow

jochenkirstaetter commented 1 month ago

Hmm, interesting. I implemented that output already here: https://github.com/stephanstapel/ZUGFeRD-csharp/blob/9279a5c66f965a77f5b017dea351f3f2d7e2d6af/ZUGFeRD/InvoiceDescriptor23CIIWriter.cs#L985-L1001

Wouldn't that work for UBL writer?

stephanstapel commented 1 month ago

Hmm, interesting. I implemented that output already here:

https://github.com/stephanstapel/ZUGFeRD-csharp/blob/9279a5c66f965a77f5b017dea351f3f2d7e2d6af/ZUGFeRD/InvoiceDescriptor23CIIWriter.cs#L985-L1001

Wouldn't that work for UBL writer?

basically yes. The new thing here is correct indention of the node content like here:

https://github.com/stephanstapel/ZUGFeRD-csharp/issues/388#issuecomment-2413207892

jochenkirstaetter commented 1 month ago

Um, OK. :-) Not that I would be bothered about indentation as it is XML, and primarily machine-read.

I get the point though, although it is not a technical constraint but optics for the eyes. Any XML viewer has a "beautify XML" feature.

I'd rather would have an option to completely avoid indentation and produces linear, minified XML output. Hence, we have visualizer or renderer to display the XML content for humans.

Cheers, JoKi

stephanstapel commented 1 month ago

Um, OK. :-) Not that I would be bothered about indentation as it is XML, and primarily machine-read.

I get the point though, although it is not a technical constraint but optics for the eyes. Any XML viewer has a "beautify XML" feature.

I'd rather would have an option to completely avoid indentation and produces linear, minified XML output. Hence, we have visualizer or renderer to display the XML content for humans.

Cheers, JoKi

it appeared to be necessary for the validator.

jochenkirstaetter commented 1 month ago

Nope,

I tested CII output with 2-3 validators online and it passed. In case of an indentation-focused format like YAML I could understand. However, it's XML and could be without any indentation at all.

Cheers, JoKi

jochenkirstaetter commented 1 month ago

What I painfully noticed is that the sequence of elements is of importance which led to the weird way of implementation in the CII writer regarding the DueDate.

jochenkirstaetter commented 1 month ago

Hi,

I switch to XmlWriter as a base class for generating XML. I didn't push the change yet, but if you could do this:

grafik

could you solve the problem mentioned above?

The new line needs to be at the end only. The terminator is "#{Environment. NewLine}".

jochenkirstaetter commented 1 month ago

Just tested the CII-export and it is coming out like this (I did not insert it with a linebreak):

      <ram:SpecifiedTradePaymentTerms>
        <ram:Description>#SKONTO#TAGE=14#PROZENT=0.00#BASISBETRAG=219.80#
</ram:Description>
      </ram:SpecifiedTradePaymentTerms>

But the validator did not say anything about this. So it just does not look very good...

Exactly, that's the code I contributed. The indentation does not matter.

jochenkirstaetter commented 1 month ago

Something else.

If you take care of the indentation to look good, it would be part of the actual node value. Meaning, instead of "...#{newline}" the resulting value would include the spaces of the indentation from the next line, and read like this "...#{newline} " which seems to be more likely problematic than an optical glitch for humans.

To my opinion, this should be wrapped inside a CDATA section in order to cater properly for all value and optical constraints. And it would be clear.

      <ram:SpecifiedTradePaymentTerms>
        <ram:Description><![CDATA[#SKONTO#TAGE=14#PROZENT=0.00#BASISBETRAG=219.80#
]]>
        </ram:Description>
      </ram:SpecifiedTradePaymentTerms>

Reference: https://developer.mozilla.org/en-US/docs/Web/API/CDATASection