quinta-feira, 30 de outubro de 2008

XmlBuilder para NF-e

Esta semana estava escrevendo uma rotina que gravava um arquivo xml para a Nota Fiscal eletrônica. Eu estava usando Delphi 7 pra isso. Começei com o Wizard XML Data Binding e ía tudo bem até que eu comecei a validar o arquivo gerado. Me deu uma dor de cabeça danada! Eu não consegui gerar o arquivo conforme o layout exigido. Daí eu decidi por fazer uma classe pra gerar o XML ao meu gosto e me lembrei de ter lido algo sobre isso mas não sabia exatamente onde. Depois de alguma pesquisa encontrei: Refactoring to Patterns de Addison Wesley.

Ele mostra como simplificar a implementação de um Composite usando o Pattern Builder. Eu peguei a idéia e construí uma classe personalizada. É uma classe simples. Não adicionei muita funcionalidade. Eu queria apenas gerar o Xml para ser validado e enviado ao webservice da SEFAZ. Vejam o resultado:


unit uXmlBuilder;

interface

uses
SysUtils, Windows, Classes, Contnrs;

type
TTagNode = class
private
FName: string;
FValue: string;
FAttributes: TStrings;
FChildren: TObjectList;
FParent: TTagNode;
function GetAttributes: string;
function GetChildrenXml: string;
procedure SetParent(const Value: TTagNode);
public
constructor Create(TagName: string);
destructor Destroy; override;
function ToString: string;
function AddChild(Child: TTagNode): TTagNode; overload;
function AddChild(const ChildName: string): TTagNode; overload;
function RemoveChild(Child: TTagNode): TTagNode;
procedure SetValue(const Value: string);
procedure AddAttribute(const Name, Value: string);
property Name: string read FName;
property Parent: TTagNode read FParent write SetParent;
end;

TXmlBuilder = class
private
FEncoding: string;
FRootNode: TTagNode;
FVersion: string;
public
constructor Create(Version, Encoding, rootTagName: string);
destructor Destroy; override;
function ToXml: string;
property Version: string read FVersion;
property Encoding: string read FEncoding;
property RootNode: TTagNode read FRootNode;
end;

implementation

uses TestFramework;

{ TXmlBuilder }

constructor TXmlBuilder.Create(Version, Encoding, rootTagName: string);
begin
FVersion := Version;
FEncoding := Encoding;
FRootNode := TTagNode.Create(rootTagName);
end;

destructor TXmlBuilder.Destroy;
begin
FRootNode.Free;
inherited;
end;

function TXmlBuilder.ToXml: string;
begin
Result := '<?xml version="'+FVersion+'" encoding="'+FEncoding+'"?>'+FRootNode.ToString;
end;

{ TTagNode }

procedure TTagNode.AddAttribute(const Name, Value: string);
begin
FAttributes.Add(Format('%s=%s',[Name, Value]));
end;

constructor TTagNode.Create(TagName: string);
begin
FAttributes := TStringList.Create;
FChildren := TObjectList.Create(True);
FName := TagName;
end;

destructor TTagNode.Destroy;
begin
FChildren.Free;
FAttributes.Free;
inherited;
end;

procedure TTagNode.SetValue(const Value: string);
begin
FValue := Value;
end;

function TTagNode.ToString: string;
begin
if FValue <> '' then
Result := Format('<%s%s>%s%s</%s>',[FName, GetAttributes, FValue, GetChildrenXml, FName])
else if FChildren.Count > 0 then
Result := Format('<%s%s>%s</%s>',[FName, GetAttributes, GetChildrenXml, FName])
else
Result := Format('<%s%s/>',[FName, GetAttributes]);
end;

function TTagNode.GetChildrenXml: string;
var
i: Integer;
begin
for i := 0 to FChildren.Count - 1 do
Result := Result + TTagNode(FChildren[i]).ToString;
end;

function TTagNode.GetAttributes: string;
var
i: Integer;
begin
for i := 0 to FAttributes.Count - 1 do
begin
Result := Result + ' ' + FAttributes.Names[i]+'="'+FAttributes.ValueFromIndex[i]+'"';
end;
end;


type
TTestXmlBuilder = class(TTestCase)
private
FXmlBuilder: TXmlBuilder;
protected
procedure SetUp; override;
procedure TearDown; override;
published
procedure TestCreate;
procedure TestBuildRootNode;
procedure TestBuildRootNodeWithAttrib;
end;

TTestTagNode = class(TTestCase)
private
FTagNode: TTagNode;
protected
procedure SetUp; override;
procedure TearDown; override;
published
procedure TestCreate;
procedure TestTagSemValor;
procedure TestSetValue;
procedure TestAddAttribute;
procedure TestAddAttributes;
procedure TestAddChild;
procedure TestRemoveChild;
procedure TestSetParent;
procedure TestAddChildren;
end;

{ TXmlBuilderTest }

procedure TTestXmlBuilder.SetUp;
begin
inherited;
FXmlBuilder := TXmlBuilder.Create('1.0', 'utf-8', 'nfe');
end;

procedure TTestXmlBuilder.TearDown;
begin
FXmlBuilder.Free;
inherited;
end;

procedure TTestXmlBuilder.TestCreate;
begin
CheckEqualsString('1.0',FXmlBuilder.Version);
CheckEqualsString('utf-8',FXmlBuilder.Encoding);
end;

procedure TTestXmlBuilder.TestBuildRootNode;
var
sExpectedXml: string;
sActualXml: string;
begin
sExpectedXml := '<?xml version="1.0" encoding="utf-8"?><nfe/>';
sActualXml := FXmlBuilder.ToXml;
CheckEqualsString(sExpectedXml, sActualXml);
end;

function TTagNode.AddChild(Child: TTagNode): TTagNode;
begin
FChildren.Add(Child);
Child.FParent := Self;
Result := Child;
end;

function TTagNode.AddChild(const ChildName: string): TTagNode;
begin
Result := AddChild(TTagNode.Create(ChildName));
end;

procedure TTagNode.SetParent(const Value: TTagNode);
begin
if Assigned(FParent) then FParent.RemoveChild(Self);
if Assigned(Value) then Value.AddChild(Self);
FParent := Value;
end;

function TTagNode.RemoveChild(Child: TTagNode): TTagNode;
begin
Result := TTagNode(FChildren.Extract(Child));
Result.FParent := nil;
end;

{ TTestTagNode }

const
PRECO = '10.25';

procedure TTestTagNode.SetUp;
begin
inherited;
FTagNode := TTagNode.Create('preco');
end;

procedure TTestTagNode.TearDown;
begin
FTagNode.Free;
inherited;
end;

procedure TTestTagNode.TestAddAttribute;
var
sExpected: string;
sActual: string;
begin
sExpected := '<preco moeda="real">'+PRECO+'</preco>';
FTagNode.AddAttribute('moeda', 'real');
FTagNode.SetValue(PRECO);
sActual := FTagNode.ToString;
CheckEqualsString(sExpected, sActual);
end;

procedure TTestTagNode.TestAddAttributes;
var
sExpected: string;
sActual: string;
begin
sExpected := '<preco moeda="real" data_cot="10/10/2008">'+PRECO+'</preco>';
FTagNode.AddAttribute('moeda', 'real');
FTagNode.AddAttribute('data_cot', '10/10/2008');
FTagNode.SetValue(PRECO);
sActual := FTagNode.ToString;
CheckEqualsString(sExpected, sActual);
end;

procedure TTestTagNode.TestAddChild;
var
sExpected: string;
sActual: string;
begin
sExpected := '<preco><moeda/></preco>';
FTagNode.AddChild(TTagNode.Create('moeda'));
sActual := FTagNode.ToString;
CheckEqualsString(sExpected, sActual);
end;

procedure TTestTagNode.TestAddChildren;
var
sExpected: string;
sActual: string;
moedaTag: TTagNode;
begin
sExpected := '<preco><moeda><teste1>valor_teste1</teste1><teste2/><teste3/></moeda></preco>';
moedaTag := FTagNode.AddChild(TTagNode.Create('moeda'));
moedaTag.AddChild(TTagNode.Create('teste1')).SetValue('valor_teste1');
moedaTag.AddChild(TTagNode.Create('teste2'));
moedaTag.AddChild(TTagNode.Create('teste3'));
sActual := FTagNode.ToString;
CheckEqualsString(sExpected, sActual);
end;

procedure TTestTagNode.TestCreate;
var
sExpected: string;
sActual: string;
begin
sExpected := 'preco';
sActual := FTagNode.Name;
CheckEqualsString(sExpected, sActual);
end;

procedure TTestTagNode.TestRemoveChild;
var
sExpected: string;
sActual: string;
moedaTag: TTagNode;
teste3Tag: TTagNode;
begin
sExpected := '<preco><moeda><teste1/><teste2/></moeda></preco>';
moedaTag := FTagNode.AddChild(TTagNode.Create('moeda'));
moedaTag.AddChild(TTagNode.Create('teste1'));
moedaTag.AddChild(TTagNode.Create('teste2'));
teste3Tag := moedaTag.AddChild(TTagNode.Create('teste3'));
moedaTag.RemoveChild(teste3Tag);
sActual := FTagNode.ToString;
CheckEqualsString(sExpected, sActual);
end;

procedure TTestTagNode.TestSetParent;
var
sExpected: string;
sActual: string;
moedaTag: TTagNode;
teste2Tag: TTagNode;
begin
sExpected := '<preco><moeda><teste1/><teste3/></moeda><teste2/></preco>';
moedaTag := FTagNode.AddChild(TTagNode.Create('moeda'));
moedaTag.AddChild(TTagNode.Create('teste1'));
teste2Tag := moedaTag.AddChild(TTagNode.Create('teste2'));
moedaTag.AddChild(TTagNode.Create('teste3'));
teste2Tag.SetParent(FTagNode);
sActual := FTagNode.ToString;
CheckEqualsString(sExpected, sActual);
end;

procedure TTestTagNode.TestSetValue;
var
sExpected: string;
sActual: string;
begin
sExpected := '<preco>'+PRECO+'</preco>';
FTagNode.SetValue(PRECO);
sActual := FTagNode.ToString;
CheckEqualsString(sExpected, sActual);
end;

procedure TTestTagNode.TestTagSemValor;
var
sExpected: string;
sActual: string;
begin
sExpected := '<preco/>';
sActual := FTagNode.ToString;
CheckEqualsString(sExpected, sActual);
end;

procedure TTestXmlBuilder.TestBuildRootNodeWithAttrib;
var
sExpectedXml: string;
sActualXml: string;
begin
sExpectedXml := '<?xml version="1.0" encoding="utf-8"?><nfe atributo="valor_atrib"/>';
FXmlBuilder.RootNode.AddAttribute('atributo','valor_atrib');
sActualXml := FXmlBuilder.ToXml;
CheckEqualsString(sExpectedXml, sActualXml);
end;

initialization
TestFramework.RegisterTest('TagNode Test', TTestTagNode.Suite);
TestFramework.RegisterTest('XmlBuilder Test', TTestXmlBuilder.Suite);

end.


No desenvolvimento desta classe eu usei o DUnit como ferramenta para testes unitários. O desenvolvimento foi rápido e a descoberta de erros ocorreu logo cedo no desenvolvimento da classe. Portanto, dentro mais ou menos 30 minutos eu estava com ela funcionando a todo vapor.

Usá-la ficou muito fácil:


var
XmlBuilder: TXmlBuilder;
InfNfeTag: TTagNode;
IdeTag: TTagNode;
begin
XmlBuilder := TXmlBuilder.Create('1.0', 'utf-8', 'NFe');
XmlBuilder.RootNode.AddAttribute('xmlns','http://www.portalfiscal.inf.br/nfe');

InfNfeTag := XmlBuilder.RootNode.AddChild('infNFe');
InfNfeTag.AddAttribute('Id','NFe'+NotaFiscal.Chave_Acesso);
InfNfeTag.AddAttribute('versao','1.10');

IdeTag := InfNfeTag.AddChild('ide');
IdeTag.AddChild('cUF').SetValue('23');

// O código continua...


Espero que esta dica seja útil pra vocês.

7 comentários:

Vinicius disse...

O que é esta unit TestFramework?
Peguei o seu codigo mas deu erro com ela ao compilar...

Marcos George disse...

TestFramework é uma unit que faz parte do DUnit - um framework para testes unitários. Baixe-o daqui: http://dunit.sourceforge.net/

Vinicius disse...

Como ficaria o codigo sem este DUnit?
eu posso tirar as partes do codigo referente ao dunit?

obrigado.

Vinicius disse...

como eu faço tambem para salvar o arquivo gerado?

Marcos George disse...

Sim, você pode remover toda a classe de testes. Ela serve apenas a dois objetivos:
1. Mostrar como se faz uma classe de testes unitários;
2. Assegurar q a classe está funcionando corretamente.

Mas se você não tiver interesse pode removê-la.

Sobre gravar pra arquivo vc pode obter o Xml da função ToXml e gravar num arquivo usando uma TFileStream, por exemplo. Eu não precisei de nenhum método pra gravar em arquivo, por isso não implementei-o.

Vinicius disse...

Só mais uma dúvida...
como eu faria um loop para todos os itens? posso usar a mesma variavel?

nItem := 0;
while not Produtos.Eof do
begin
nItem := nItem + 1
DetTag := InfNfeTag.AddChild('det');
DetTag.AddAttribute('nItem', nItem);

end;

Muito obrigado pela atenção

Marcos George disse...

Pode. Cada AddChild retorna uma nova instancia. Portanto, não terá problemas.

 

web analytics software