diff --git a/src/Danfse/DanfseTemplate.php b/src/Danfse/DanfseTemplate.php
index f671dbf..eaf033d 100644
--- a/src/Danfse/DanfseTemplate.php
+++ b/src/Danfse/DanfseTemplate.php
@@ -103,7 +103,12 @@ public function buildData(array $nfse): array
'numero_dps' => $this->val($infDps, 'nDPS') ?: '-',
'serie_dps' => $this->val($infDps, 'serie') ?: '-',
'emissao_dps' => $this->formatter->dateTime($this->val($infDps, 'dhEmi')),
- 'ambiente' => Ambiente::fromValue($this->val($infDps, 'tpAmb'))->value,
+ 'ambiente' => Ambiente::fromValue($this->val($infDps, 'tpAmb'))->value,
+ 'ambiente_gerador' => $this->val($infNfse, 'ambGer') ?: '-',
+ 'municipio_emissao' => $this->val($infNfse, 'xLocEmi') ?: '-',
+ 'emitente_nfse' => $this->emitterLabel($this->val($infDps, 'tpEmit')),
+ 'situacao_nfse' => $this->statusLabel($this->val($infNfse, 'cStat')),
+ 'finalidade_nfse' => $this->purposeLabel($this->val($infDps, 'IBSCBS', 'finNFSe')),
'emitente' => [
'nome' => $this->val($emit, 'xNome') ?: '-',
@@ -164,9 +169,12 @@ public function buildData(array $nfse): array
'tributacao_federal' => [
'irrf' => $this->currencyOrDash($this->val($tribFed, 'vRetIRRF')),
'cp' => $this->currencyOrDash($this->val($tribFed, 'vRetCP')),
- 'csll' => $this->currencyOrDash($this->val($tribFed, 'vRetCSLL')),
+ 'contribuicoes_sociais' => $this->currencyOrDash($this->val($tribFed, 'vRetCSLL')),
'pis' => $this->currencyOrDash($this->val($tribFed, 'piscofins', 'vPis')),
'cofins' => $this->currencyOrDash($this->val($tribFed, 'piscofins', 'vCofins')),
+ 'descricao_retencao' => $this->pisCofinsRetentionLabel(
+ $this->val($tribFed, 'piscofins', 'tpRetPisCofins'),
+ ),
],
'totais' => [
@@ -194,20 +202,39 @@ public function buildData(array $nfse): array
'municipais' => $this->percentOrDash($this->val($totTrib, 'pTotTribMun')),
],
- 'ibs_cbs' => $ibsCbs === [] ? null : [
+ 'ibs_cbs' => [
+ 'cst_classificacao' => $this->joinedOrDash(
+ $this->val($infDps, 'IBSCBS', 'valores', 'trib', 'gIBSCBS', 'CST'),
+ $this->val($infDps, 'IBSCBS', 'valores', 'trib', 'gIBSCBS', 'cClassTrib'),
+ ),
+ 'indicador_operacao' => $this->val($infDps, 'IBSCBS', 'cIndOp') ?: '-',
'localidade_incidencia' => $this->val($ibsCbs, 'xLocalidadeIncid')
?: $this->val($ibsCbs, 'cLocalidadeIncid')
?: '-',
+ 'exclusoes_reducoes' => $this->sumCurrency(
+ $this->val($infNfse, 'valores', 'vISSQN'),
+ $this->val($tribFed, 'piscofins', 'vPis'),
+ $this->val($tribFed, 'piscofins', 'vCofins'),
+ ),
'base_calculo' => $this->currencyOrDash($this->val($ibsCbsValores, 'vBC')),
'aliquota_ibs_uf' => $this->percentOrDash($this->val($ibsCbsValores, 'uf', 'pIBSUF')),
'aliquota_ibs_municipal' => $this->percentOrDash($this->val($ibsCbsValores, 'mun', 'pIBSMun')),
'aliquota_cbs' => $this->percentOrDash($this->val($ibsCbsValores, 'fed', 'pCBS')),
'total_ibs' => $this->currencyOrDash($this->val($ibsCbsTotais, 'gIBS', 'vIBSTot')),
'total_cbs' => $this->currencyOrDash($this->val($ibsCbsTotais, 'gCBS', 'vCBS')),
+ 'total_ibs_cbs' => $ibsCbs === [] ? '-' : $this->sumCurrency(
+ $this->val($ibsCbsTotais, 'gIBS', 'vIBSTot'),
+ $this->val($ibsCbsTotais, 'gCBS', 'vCBS'),
+ ),
'valor_total_nfse' => $this->currencyOrDash($this->val($ibsCbsTotais, 'vTotNF')),
],
- 'informacoes_complementares' => $this->val($serv, 'infoCompl', 'xInfComp'),
+ 'informacoes_complementares' => $this->complementaryInformation(
+ $this->val($serv, 'infoCompl', 'xInfComp'),
+ $this->percentOrDash($this->val($totTrib, 'pTotTribFed')),
+ $this->percentOrDash($this->val($totTrib, 'pTotTribEst')),
+ $this->percentOrDash($this->val($totTrib, 'pTotTribMun')),
+ ),
];
}
@@ -318,6 +345,69 @@ private function sumCurrency(string ...$values): string
return $hasValue ? $this->formatter->currency((string) $total) : '-';
}
+ private function emitterLabel(string $value): string
+ {
+ return match ($value) {
+ '1' => 'Prestador',
+ '2' => 'Tomador',
+ '3' => 'Intermediário',
+ default => '-',
+ };
+ }
+
+ private function statusLabel(string $value): string
+ {
+ return $value === '100' ? 'NFS-e Gerada' : ($value !== '' ? $value : '-');
+ }
+
+ private function purposeLabel(string $value): string
+ {
+ return match ($value) {
+ '0' => 'NFS-e regular',
+ '1' => 'NFS-e de crédito',
+ '2' => 'NFS-e de débito',
+ default => '-',
+ };
+ }
+
+ private function pisCofinsRetentionLabel(string $value): string
+ {
+ return match ($value) {
+ '0' => '0 - PIS/COFINS/CSLL Não Retidos',
+ '1' => '1 - PIS/COFINS Retidos',
+ '2' => '2 - PIS/COFINS Não Retidos',
+ '3' => '3 - PIS/COFINS/CSLL Retidos',
+ '4' => '4 - PIS/COFINS Retidos, CSLL Não Retido',
+ '5' => '5 - PIS Retido, COFINS/CSLL Não Retido',
+ default => '-',
+ };
+ }
+
+ private function joinedOrDash(string ...$values): string
+ {
+ if (array_filter($values, static fn (string $value): bool => $value !== '') === []) {
+ return '-';
+ }
+
+ return implode(' / ', array_map(static fn (string $value): string => $value !== '' ? $value : '-', $values));
+ }
+
+ private function complementaryInformation(
+ string $existing,
+ string $federal,
+ string $state,
+ string $municipal,
+ ): string {
+ $taxes = sprintf(
+ 'Totais aproximados dos Tributos cfe. Lei n° 12.741/2012: Federais: %s; Estaduais: %s; Municipais: %s;',
+ $federal,
+ $state,
+ $municipal,
+ );
+
+ return trim($existing) === '' ? $taxes : trim($existing) . "\n" . $taxes;
+ }
+
private function generateQrCode(string $chaveAcesso, Ambiente $ambiente): string
{
$renderer = new ImageRenderer(
diff --git a/src/Danfse/HTMLTemplate.php b/src/Danfse/HTMLTemplate.php
index a55bf11..35e531f 100644
--- a/src/Danfse/HTMLTemplate.php
+++ b/src/Danfse/HTMLTemplate.php
@@ -151,24 +151,12 @@
-
-
-
- logoDataUri): ?>
-  ?>) |
-
-
- = htmlspecialchars($municipality->name) ?>
- department): ?>
- = htmlspecialchars($municipality->department) ?>
-
- email): ?>
- = htmlspecialchars($municipality->email) ?>
-
- |
-
-
+ logoDataUri): ?>
+  ?>)
+ Município: = $data['municipio_emissao'] ?>
+ Ambiente Gerador: = $data['ambiente_gerador'] ?>
+ Tipo de Ambiente: = $data['ambiente'] ?>
|
@@ -219,6 +207,22 @@
+
+
+ |
+ Emitente da NFS-e
+ = $data['emitente_nfse'] ?>
+ |
+
+ Situação da NFS-e
+ = $data['situacao_nfse'] ?>
+ |
+
+ Finalidade
+ = $data['finalidade_nfse'] ?>
+ |
+
+
@@ -479,26 +483,29 @@
= $data['tributacao_federal']['cp'] ?? '-' ?>
- CSLL - Retida
- = $data['tributacao_federal']['csll'] ?? '-' ?>
+ Contribuições Sociais - Retidas
+ = $data['tributacao_federal']['contribuicoes_sociais'] ?? '-' ?>
|
|
- |
+ |
PIS - Débito Apuração Própria
= $data['tributacao_federal']['pis'] ?? '-' ?>
|
-
+ |
COFINS - Débito Apuração Própria
= $data['tributacao_federal']['cofins'] ?? '-' ?>
|
+
+ Descrição Contrib. Sociais - Retidas
+ = $data['tributacao_federal']['descricao_retencao'] ?? '-' ?>
+ |
-
@@ -507,10 +514,24 @@
+ |
+ CST / cClassTrib
+ = $data['ibs_cbs']['cst_classificacao'] ?>
+ |
+
+ Indicador de Operação
+ = $data['ibs_cbs']['indicador_operacao'] ?>
+ |
Localidade de Incidência
= $data['ibs_cbs']['localidade_incidencia'] ?>
|
+
+ Exclusões e Reduções da Base de Cálculo
+ = $data['ibs_cbs']['exclusoes_reducoes'] ?>
+ |
+
+
|
Base de Cálculo
= $data['ibs_cbs']['base_calculo'] ?>
@@ -523,6 +544,10 @@
Alíquota IBS Município
= $data['ibs_cbs']['aliquota_ibs_municipal'] ?>
|
+
+ Reduções de Alíquota IBS/CBS
+ -
+ |
|
@@ -544,7 +569,6 @@
|
-
@@ -577,38 +601,17 @@
Total das Retenções Federais
= $data['totais']['retencoes_federais'] ?? '-' ?>
-
- PIS/COFINS - Débito Apur. Própria
- = $data['totais']['pis_cofins'] ?? '-' ?>
- |
Valor Líquido da NFS-e
= $data['totais']['valor_liquido'] ?>
|
-
-
-
-
-
-
-
-
-
-
-
- |
- Federais
- = $data['totais_tributos']['federais'] ?? '-' ?>
- |
-
- Estaduais
- = $data['totais_tributos']['estaduais'] ?? '-' ?>
+ |
+ Total do IBS/CBS
+ = $data['ibs_cbs']['total_ibs_cbs'] ?>
|
-
- Municipais
- = $data['totais_tributos']['municipais'] ?? '-' ?>
+ |
+ Valor Líquido da NFS-e + IBS/CBS
+ = $data['ibs_cbs']['valor_total_nfse'] ?>
|
diff --git a/src/Dto/DpsData.php b/src/Dto/DpsData.php
index 9475ea7..ea4ab4b 100644
--- a/src/Dto/DpsData.php
+++ b/src/Dto/DpsData.php
@@ -33,6 +33,12 @@ public function __construct(
/** Descrição do serviço prestado. */
public string $discriminacao,
+ /** Optional provider phone published in the DPS. */
+ public string $prestadorTelefone = '',
+
+ /** Optional provider e-mail published in the DPS. */
+ public string $prestadorEmail = '',
+
/** Tipo de ambiente (1-Produção | 2-Homologação). */
public int $tipoAmbiente = 2,
diff --git a/src/Xml/XmlBuilder.php b/src/Xml/XmlBuilder.php
index ed6499d..9b53e7b 100644
--- a/src/Xml/XmlBuilder.php
+++ b/src/Xml/XmlBuilder.php
@@ -55,6 +55,15 @@ public function buildDps(DpsData $dps): string
$prest = $doc->createElement('prest');
$cnpj = $doc->createElement('CNPJ', $dps->cnpjPrestador);
$prest->appendChild($cnpj);
+
+ if ($dps->prestadorTelefone !== '') {
+ $prest->appendChild($doc->createElement('fone', $dps->prestadorTelefone));
+ }
+
+ if ($dps->prestadorEmail !== '') {
+ $prest->appendChild($doc->createElement('email', htmlspecialchars($dps->prestadorEmail, ENT_XML1)));
+ }
+
$prest->appendChild($this->buildRegTrib($doc, $dps));
$infDps->appendChild($prest);
diff --git a/tests/Unit/Danfse/DanfseGoldenReferenceTest.php b/tests/Unit/Danfse/DanfseGoldenReferenceTest.php
index 55d7ec5..4cccbf0 100644
--- a/tests/Unit/Danfse/DanfseGoldenReferenceTest.php
+++ b/tests/Unit/Danfse/DanfseGoldenReferenceTest.php
@@ -104,7 +104,7 @@ public function testIbsCbsReferenceRendersOnlyAuthorizedTotals(): void
self::assertStringContainsString('TRIBUTAÇÃO IBS / CBS', $html);
}
- public function testLegacyAuthorizedXmlWithoutIbsCbsKeepsLegacyLayout(): void
+ public function testAuthorizedXmlWithoutIbsCbsStillRendersRequiredV2Section(): void
{
$nfse = $this->fixtureData();
unset($nfse['infNFSe']['IBSCBS']);
@@ -113,11 +113,32 @@ public function testLegacyAuthorizedXmlWithoutIbsCbsKeepsLegacyLayout(): void
$data = $template->buildData($nfse);
$html = $template->render($nfse, new DanfseConfig());
- self::assertNull($data['ibs_cbs']);
- self::assertStringNotContainsString('TRIBUTAÇÃO IBS / CBS', $html);
+ self::assertIsArray($data['ibs_cbs']);
+ self::assertSame('-', $data['ibs_cbs']['base_calculo']);
+ self::assertSame('-', $data['ibs_cbs']['total_ibs_cbs']);
+ self::assertStringContainsString('TRIBUTAÇÃO IBS / CBS', $html);
self::assertStringContainsString('DANFSe v2.0', $html);
}
+ public function testFederalRetentionUsesOfficialV2Semantics(): void
+ {
+ $nfse = $this->fixtureData();
+ $nfse['infNFSe']['DPS']['infDPS']['valores']['trib']['tribFed']['piscofins']['tpRetPisCofins'] = '4';
+ $nfse['infNFSe']['DPS']['infDPS']['valores']['trib']['tribFed']['vRetCSLL'] = '1149.75';
+
+ $template = new DanfseTemplate();
+ $data = $template->buildData($nfse);
+ $html = $template->render($nfse, new DanfseConfig());
+
+ self::assertSame('R$ 1.149,75', $data['tributacao_federal']['contribuicoes_sociais']);
+ self::assertSame(
+ '4 - PIS/COFINS Retidos, CSLL Não Retido',
+ $data['tributacao_federal']['descricao_retencao'],
+ );
+ self::assertStringContainsString('Contribuições Sociais - Retidas', $html);
+ self::assertStringContainsString('Descrição Contrib. Sociais - Retidas', $html);
+ }
+
public function testHomologationReferenceCarriesVisibleMarker(): void
{
$nfse = $this->fixtureData();
diff --git a/tests/Unit/Danfse/DanfseTemplateTest.php b/tests/Unit/Danfse/DanfseTemplateTest.php
index 25d10cc..dfbc875 100644
--- a/tests/Unit/Danfse/DanfseTemplateTest.php
+++ b/tests/Unit/Danfse/DanfseTemplateTest.php
@@ -95,6 +95,11 @@ public function testRenderProducesHtmlWithQrCodeAndAccessKey(): void
self::assertStringContainsString('', $html);
self::assertStringContainsString('DANFSe v2.0', $html);
+ self::assertStringContainsString('Município:', $html);
+ self::assertStringContainsString('Ambiente Gerador:', $html);
+ self::assertStringContainsString('Tipo de Ambiente:', $html);
+ self::assertStringContainsString('Situação da NFS-e', $html);
+ self::assertStringContainsString('Finalidade', $html);
self::assertStringContainsString('3303302112233450000195000000000000100000000001', $html);
self::assertStringContainsString('data:image/svg+xml;base64,', $html);
// Production environment: no homologação watermark
@@ -133,11 +138,12 @@ public function testBuildDataMapsAuthorizedIbsCbsTotalsWithoutRecalculating(): v
self::assertSame('R$ 1.515,75', $data['ibs_cbs']['valor_total_nfse']);
}
- public function testRenderShowsIbsCbsSectionOnlyWhenAuthorizedXmlContainsIt(): void
+ public function testRenderShowsRequiredIbsCbsSectionEvenWithoutAuthorizedIbsCbsGroup(): void
{
$withoutIbsCbs = (new DanfseTemplate())->render($this->fixtureNfseData(), new DanfseConfig());
- self::assertStringNotContainsString('TRIBUTAÇÃO IBS / CBS', $withoutIbsCbs);
+ self::assertStringContainsString('TRIBUTAÇÃO IBS / CBS', $withoutIbsCbs);
+ self::assertStringContainsString('Exclusões e Reduções da Base de Cálculo', $withoutIbsCbs);
$nfseData = $this->fixtureNfseData();
$nfseData['infNFSe']['IBSCBS'] = [
diff --git a/tests/Unit/Xml/XmlBuilderTest.php b/tests/Unit/Xml/XmlBuilderTest.php
index 48da978..2e1a196 100644
--- a/tests/Unit/Xml/XmlBuilderTest.php
+++ b/tests/Unit/Xml/XmlBuilderTest.php
@@ -87,6 +87,21 @@ public function testBuildDpsContainsCnpjPrestador(): void
self::assertStringContainsString('11222333000181', $xml);
}
+ public function testBuildDpsPublishesOptionalProviderContact(): void
+ {
+ $xml = $this->builder->buildDps($this->makeDps(
+ prestadorTelefone: '21969203370',
+ prestadorEmail: 'adm@librecode.coop',
+ ));
+
+ $normalized = str_replace(["\n", ' '], '', $xml);
+
+ self::assertStringContainsString(
+ '
1122233300018121969203370adm@librecode.coop',
+ $normalized,
+ );
+ }
+
public function testBuildDpsContainsMunicipioIbge(): void
{
$dps = $this->makeDps(municipioIbge: '3303302');
@@ -896,6 +911,8 @@ private function makeDps(
string $valorServico = '1000.00',
string $aliquota = '5.00',
string $discriminacao = 'Consultoria em TI',
+ string $prestadorTelefone = '',
+ string $prestadorEmail = '',
string $serie = '00001',
string $numeroDps = '1',
bool $issRetido = false,
@@ -951,6 +968,8 @@ private function makeDps(
valorServico: $valorServico,
aliquota: $aliquota,
discriminacao: $discriminacao,
+ prestadorTelefone: $prestadorTelefone,
+ prestadorEmail: $prestadorEmail,
serie: $serie,
numeroDps: $numeroDps,
codigoTributacaoNacional: $codigoTributacaoNacional,