diff --git a/NFe.Classes/Informacoes/Detalhe/Tributacao/IS.cs b/NFe.Classes/Informacoes/Detalhe/Tributacao/IS.cs index 763fbf60..fa6eb151 100644 --- a/NFe.Classes/Informacoes/Detalhe/Tributacao/IS.cs +++ b/NFe.Classes/Informacoes/Detalhe/Tributacao/IS.cs @@ -1,4 +1,6 @@ using System.Xml.Serialization; +using System; +using System.ComponentModel; namespace NFe.Classes.Informacoes.Detalhe.Tributacao { @@ -6,7 +8,7 @@ public class IS { private decimal _vBcIs; private decimal _pIs; - private decimal? _pIsEspec; + private decimal? _adRemIS; private decimal _qTrib; private decimal _vIs; private string _cClassTribIS; @@ -39,32 +41,58 @@ public decimal pIS set => _pIs = value.Arredondar(4); } - // UB07 + /// + /// UB07 - Alíquota do Imposto Seletivo (por valor), conforme NT 2025.002 v1.40. + /// [XmlElement(Order = 5)] + public decimal? adRemIS + { + get => _adRemIS.Arredondar(4); + set => _adRemIS = value.Arredondar(4); + } + public bool ShouldSerializeadRemIS() => adRemIS.HasValue; + + /// + /// Nome anterior da alíquota específica, mantido para compatibilidade de código. + /// + [Obsolete("Utilize adRemIS (NT 2025.002 v1.40).")] + [XmlIgnore] public decimal? pISEspec { - get => _pIsEspec.Arredondar(4); - set => _pIsEspec = value.Arredondar(4); + get => adRemIS; + set => adRemIS = value; } - public bool ShouldSerializepISEspec() + public bool ShouldSerializepISEspec() => false; + + /// + /// Lê a tag de XMLs antigos; a emissão utiliza exclusivamente adRemIS. + /// O XmlSerializer ignora membros Obsolete, por isso a leitura usa este membro separado. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlElement("pISEspec", Order = 6)] + public decimal? pISEspecXml { - return pISEspec.HasValue; + get => adRemIS; + set => adRemIS = value; } + public bool ShouldSerializepISEspecXml() => false; // UB09 - [XmlElement(Order = 6)] + [XmlElement(Order = 7)] public string uTrib { get; set; } + public bool ShouldSerializeuTrib() => !string.IsNullOrEmpty(uTrib); // UB10 - [XmlElement(Order = 7)] + [XmlElement(Order = 8)] public decimal qTrib { get => _qTrib.Arredondar(4); set => _qTrib = value.Arredondar(4); } + public bool ShouldSerializeqTrib() => ShouldSerializeuTrib(); // UB11 - [XmlElement(Order = 8)] + [XmlElement(Order = 9)] public decimal vIS { get => _vIs.Arredondar(2); @@ -88,4 +116,4 @@ public void SetCSTIS(int intValue) CSTIS = intValue.ToString("D3"); } } -} \ No newline at end of file +} diff --git a/NFe.Utils.Testes/ISTestes.cs b/NFe.Utils.Testes/ISTestes.cs new file mode 100644 index 00000000..88640fd8 --- /dev/null +++ b/NFe.Utils.Testes/ISTestes.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using System.Xml.Linq; +using System.Xml.Schema; +using System.Xml.Serialization; +using NFe.Classes.Informacoes.Detalhe.Tributacao; +using Xunit; + +namespace NFe.Utils.Testes +{ + public class ISTestes + { + private const string NamespaceNFe = "http://www.portalfiscal.inf.br/nfe"; + private static readonly XmlSerializer Serializer = new XmlSerializer( + typeof(IS), new XmlRootAttribute("IS") { Namespace = NamespaceNFe }); + + [Theory] + [InlineData(null)] + [InlineData("")] + public void Serializar_AdValoremSemUnidade_ValidaNoXsd(string unidade) + { + var imposto = CriarIS(); + imposto.uTrib = unidade; + var xml = Serializar(imposto); + + AssertValido(xml); + Assert.Null(xml.Element(XName.Get("uTrib", NamespaceNFe))); + Assert.Null(xml.Element(XName.Get("qTrib", NamespaceNFe))); + Assert.Null(xml.Element(XName.Get("adRemIS", NamespaceNFe))); + } + + [Fact] + public void Serializar_AliquotaEspecifica_ValidaNoXsd() + { + var imposto = CriarIS(); + imposto.adRemIS = 1.0000m; + imposto.uTrib = "UN"; + imposto.qTrib = 100m; + + var xml = Serializar(imposto); + + AssertValido(xml); + Assert.Equal(1m, (decimal)xml.Element(XName.Get("adRemIS", NamespaceNFe))); + Assert.Null(xml.Element(XName.Get("pISEspec", NamespaceNFe))); + Assert.Equal(new[] { "CSTIS", "cClassTribIS", "vBCIS", "pIS", "adRemIS", "uTrib", "qTrib", "vIS" }, + xml.Elements().Select(element => element.Name.LocalName)); + } + + [Fact] + public void Desserializar_PISEspecAntigo_LeAliquotaEEmiteSomenteAdRemIS() + { + var xmlAntigo = XElement.Parse("" + + "00000000110001" + + "1.0000UN10010"); + + var imposto = Desserializar(xmlAntigo); + + Assert.Equal(1m, imposto.adRemIS); + Assert.Equal("UN", imposto.uTrib); + Assert.Equal(100m, imposto.qTrib); + Assert.Equal(10m, imposto.vIS); + var xmlAtual = Serializar(imposto); + AssertValido(xmlAtual); + Assert.Null(xmlAtual.Element(XName.Get("pISEspec", NamespaceNFe))); + Assert.Equal(1m, (decimal)xmlAtual.Element(XName.Get("adRemIS", NamespaceNFe))); + } + + [Fact] + public void SerializarEDesserializar_PreservaAdRemISEDemaisCampos() + { + var imposto = CriarIS(); + imposto.adRemIS = 1.2345m; + imposto.uTrib = "UN"; + imposto.qTrib = 100m; + var primeiroXml = Serializar(imposto); + + var restaurado = Desserializar(primeiroXml); + var segundoXml = Serializar(restaurado); + + Assert.Equal(1.2345m, restaurado.adRemIS); + Assert.True(XNode.DeepEquals(primeiroXml, segundoXml)); + AssertValido(segundoXml); + } + + [Theory] + [InlineData(1.23456, 1.2346)] + [InlineData(0, 0)] + public void AdRemIS_ArredondaQuatroCasasEEmiteZero(decimal valor, decimal esperado) + { + var imposto = CriarIS(); + imposto.adRemIS = valor; + + var xml = Serializar(imposto); + + Assert.Equal(esperado, imposto.adRemIS); + Assert.Equal(esperado, (decimal)xml.Element(XName.Get("adRemIS", NamespaceNFe))); + AssertValido(xml); + } + + [Fact] + public void PISEspec_AliasDeCodigo_CompartilhaValorComAdRemIS() + { + var imposto = CriarIS(); + // A API obsoleta é exercitada intencionalmente para garantir compatibilidade de código. +#pragma warning disable CS0618 + imposto.pISEspec = 1.23456m; + Assert.Equal(1.2346m, imposto.adRemIS); + imposto.adRemIS = 2m; + Assert.Equal(2m, imposto.pISEspec); + imposto.pISEspec = null; +#pragma warning restore CS0618 + Assert.Null(imposto.adRemIS); + + var xml = Serializar(imposto); + Assert.Null(xml.Element(XName.Get("pISEspec", NamespaceNFe))); + Assert.Null(xml.Element(XName.Get("adRemIS", NamespaceNFe))); + AssertValido(xml); + } + + private static IS Desserializar(XElement xml) + { + using (var reader = xml.CreateReader()) + return (IS)Serializer.Deserialize(reader); + } + + private static void AssertValido(XElement xml) + { + var erros = Validar(xml); + Assert.True(erros.Count == 0, string.Join(Environment.NewLine, erros)); + } + + private static IS CriarIS() + { + return new IS { CSTIS = "000", cClassTribIS = "000001", vBCIS = 1000m, pIS = 1m, vIS = 10m }; + } + + private static XElement Serializar(IS imposto) + { + using (var writer = new StringWriter()) + { + Serializer.Serialize(writer, imposto); + return XElement.Parse(writer.ToString()); + } + } + + private static List Validar(XElement xml) + { + // O wrapper declara somente a raiz; o contrato TIS vem do XSD da biblioteca. + var schema = new XmlSchema { TargetNamespace = NamespaceNFe, ElementFormDefault = XmlSchemaForm.Qualified }; + schema.Includes.Add(new XmlSchemaInclude + { + SchemaLocation = Path.Combine(AppContext.BaseDirectory, "Schemas", "DFeTiposBasicos_v1.00.xsd") + }); + schema.Items.Add(new XmlSchemaElement { Name = "IS", SchemaTypeName = new XmlQualifiedName("TIS", NamespaceNFe) }); + var schemas = new XmlSchemaSet { XmlResolver = new XmlUrlResolver() }; + schemas.Add(schema); + schemas.Compile(); + + var erros = new List(); + new XDocument(xml).Validate(schemas, (sender, args) => erros.Add(args.Message)); + return erros; + } + } +}