Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 71 additions & 17 deletions src/Danfse/DanfseTemplate.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ public function buildData(array $nfse): array
$ibsCbs = $this->node($infNfse, 'IBSCBS');
$ibsCbsValores = $this->node($ibsCbs, 'valores');
$ibsCbsTotais = $this->node($ibsCbs, 'totCIBS');
$ibsTotal = $this->val($ibsCbsTotais, 'gIBS', 'vIBSTot');
$cbsTotal = $this->val($ibsCbsTotais, 'gCBS', 'vCBS');
$incidenceCode = $this->val($ibsCbs, 'cLocalidadeIncid');
$dpsDiscounts = $this->node($valores, 'vDescCondIncond');

$id = $this->val($infNfse, 'Id');
$chaveAcesso = str_starts_with($id, 'NFS') ? substr($id, 3) : $id;
Expand Down Expand Up @@ -190,12 +194,19 @@ public function buildData(array $nfse): array
],

'totais' => [
'valor_servico' => $this->formatter->currency($this->val($valores, 'vServPrest', 'vServ')),
'desconto_condicionado' => $this->currencyOrDash($this->val($tribMun, 'vDescCond')),
'desconto_incondicionado' => $this->currencyOrDash($this->val($tribMun, 'vDescIncond')),
'issqn_retido' => ($this->val($authorizedValues, 'vISSQN') !== '' && ($this->val($tribMun, 'tpRetISSQN') ?: '1') !== '1')
? $this->formatter->currency($this->val($authorizedValues, 'vISSQN'))
: '-',
'valor_servico' => $this->formatter->currency($this->val($valores, 'vServPrest', 'vServ')),
'desconto_condicionado' => $this->currencyOrDash($this->val($dpsDiscounts, 'vDescCond')),
'desconto_incondicionado' => $this->currencyOrDash($this->val($dpsDiscounts, 'vDescIncond')),
// This field is calculated by SEFIN and exists in the authorized
// NFS-e. Do not recalculate it from partially provided retentions.
'total_retencoes' => $this->currencyOrDash($this->val($authorizedValues, 'vTotalRet')),
// Historical buildData() keys are retained for consumers and
// golden contracts. The DANFSe itself prints vTotalRet as the
// authoritative total rather than any partial DPS sum.
'issqn_retido' => (
$this->val($authorizedValues, 'vISSQN') !== ''
&& ($this->val($tribMun, 'tpRetISSQN') ?: '1') !== '1'
) ? $this->currencyOrDash($this->val($authorizedValues, 'vISSQN')) : '-',
'retencoes_federais' => $this->sumCurrency(
$this->val($tribFed, 'vRetIRRF'),
$this->val($tribFed, 'vRetCP'),
Expand All @@ -205,7 +216,7 @@ public function buildData(array $nfse): array
$this->val($tribFed, 'piscofins', 'vPis'),
$this->val($tribFed, 'piscofins', 'vCofins'),
),
'valor_liquido' => $this->formatter->currency($this->val($infNfse, 'valores', 'vLiq')),
'valor_liquido' => $this->currencyOrDash($this->val($authorizedValues, 'vLiq')),
],

'totais_tributos' => [
Expand All @@ -219,25 +230,51 @@ public function buildData(array $nfse): array
$this->val($infDps, 'IBSCBS', 'valores', 'trib', 'gIBSCBS', 'CST'),
$this->val($infDps, 'IBSCBS', 'valores', 'trib', 'gIBSCBS', 'cClassTrib'),
),
'indicador_operacao' => $this->val($infDps, 'IBSCBS', 'cIndOp') ?: '-',
'indicador_operacao' => $this->joinedOrDash(
$this->val($infDps, 'IBSCBS', 'cIndOp'),
$incidenceCode,
$this->val($ibsCbs, 'xLocalidadeIncid'),
$this->ufForIbge($incidenceCode),
),
// Public buildData() key retained for consumers of the data
// model; HTML uses the complete official indicator field.
'localidade_incidencia' => $this->val($ibsCbs, 'xLocalidadeIncid')
?: $this->val($ibsCbs, 'cLocalidadeIncid')
?: $incidenceCode
?: '-',
'exclusoes_reducoes' => $this->sumCurrency(
$this->val($infNfse, 'valores', 'vISSQN'),
// NT008 explicitly displays this sum of XML components. It
// does not authorize fresh IBS/CBS or ISS tax calculation.
'exclusoes_reducoes' => $ibsCbs === [] ? '-' : $this->sumCurrency(
$this->val($dpsDiscounts, 'vDescIncond'),
$this->val($ibsCbsValores, 'vCalcReeRepRes'),
$this->val($authorizedValues, 'vISSQN'),
$this->val($tribFed, 'piscofins', 'vPis'),
$this->val($tribFed, 'piscofins', 'vCofins'),
),
'base_calculo' => $this->currencyOrDash($this->val($ibsCbsValores, 'vBC')),
'reducao_aliquotas' => $this->joinedOrDash(
$this->percentOrDash($this->val($ibsCbsValores, 'uf', 'pRedAliqUF')),
$this->percentOrDash($this->val($ibsCbsValores, 'mun', 'pRedAliqMun')),
$this->percentOrDash($this->val($ibsCbsValores, 'fed', 'pRedAliqCBS')),
),
'aliquota_ibs' => $this->joinedOrDash(
$this->percentOrDash($this->val($ibsCbsValores, 'uf', 'pIBSUF')),
$this->percentOrDash($this->val($ibsCbsValores, 'mun', 'pIBSMun')),
),
'aliquota_ibs_uf' => $this->percentOrDash($this->val($ibsCbsValores, 'uf', 'pIBSUF')),
'aliquota_ibs_municipal' => $this->percentOrDash($this->val($ibsCbsValores, 'mun', 'pIBSMun')),
'aliquota_efetiva_municipal' => $this->percentOrDash($this->val($ibsCbsValores, 'mun', 'pAliqEfetMun')),
'valor_ibs_municipal' => $this->currencyOrDash($this->val($ibsCbsTotais, 'gIBS', 'gIBSMunTot', 'vIBSMun')),
'aliquota_efetiva_estadual' => $this->percentOrDash($this->val($ibsCbsValores, 'uf', 'pAliqEfetUF')),
'valor_ibs_estadual' => $this->currencyOrDash($this->val($ibsCbsTotais, 'gIBS', 'gIBSUFTot', 'vIBSUF')),
'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'),
),
'aliquota_efetiva_cbs' => $this->percentOrDash($this->val($ibsCbsValores, 'fed', 'pAliqEfetCBS')),
'total_ibs' => $this->currencyOrDash($ibsTotal),
'total_cbs' => $this->currencyOrDash($cbsTotal),
// An absent amount is not proof of zero. Two explicit amounts
// (including literal zero) are required for a displayed sum.
'total_ibs_cbs' => $ibsTotal !== '' && $cbsTotal !== ''
? $this->sumCurrency($ibsTotal, $cbsTotal)
: '-',
'valor_total_nfse' => $this->currencyOrDash($this->val($ibsCbsTotais, 'vTotNF')),
],

Expand Down Expand Up @@ -343,6 +380,23 @@ private function locationWithUfAndCountry(string $city, string $ibge, string $co
. ($country !== '' ? $country : '-');
}

/**
* Derive the UF only from an explicitly authorized, recognized IBGE code.
*/
private function ufForIbge(string $code): string
{
if (preg_match('/^\\d{7}$/D', $code) !== 1) {
return '-';
}

$name = Municipios::lookup($code);
if ($name === $code || !str_contains($name, ' - ')) {
return '-';
}

return substr($name, (int) strrpos($name, ' - ') + 3);
}

private function formattedNbs(string $code): string
{
if (preg_match('/^\\d{9}$/D', $code) !== 1) {
Expand Down
73 changes: 18 additions & 55 deletions src/Danfse/HTMLTemplate.php
Original file line number Diff line number Diff line change
Expand Up @@ -374,68 +374,31 @@
</table>
</div>

<!-- Tributação IBS / CBS (NT 008 v1.02) -->
<!-- Tributação IBS/CBS: Anexo I grid and official authorized XML fields -->
<div class="bordered-section">
<table>
<tr>
<td class="section-header">
<span class="section-title">TRIBUTAÇÃO IBS / CBS</span>
</td>
<td>
<span class="label">CST / cClassTrib</span>
<span class="value"><?= $data['ibs_cbs']['cst_classificacao'] ?></span>
</td>
<td>
<span class="label">Indicador de Operação</span>
<span class="value"><?= $data['ibs_cbs']['indicador_operacao'] ?></span>
</td>
<td>
<span class="label">Localidade de Incidência</span>
<span class="value"><?= $data['ibs_cbs']['localidade_incidencia'] ?></span>
</td>
<td class="section-header"><span class="section-title">TRIBUTAÇÃO IBS / CBS</span></td>
<td><span class="label">CST / cClassTrib</span><span class="value"><?= $data['ibs_cbs']['cst_classificacao'] ?></span></td>
<td colspan="2"><span class="label">Indicador de Operação / Código IBGE / Município Incidência / Sigla UF</span><span class="value"><?= $data['ibs_cbs']['indicador_operacao'] ?></span></td>
</tr>
<tr>
<td>
<span class="label">Exclusões e Reduções da Base de Cálculo</span>
<span class="value"><?= $data['ibs_cbs']['exclusoes_reducoes'] ?></span>
</td>
<td>
<span class="label">Base de Cálculo</span>
<span class="value"><?= $data['ibs_cbs']['base_calculo'] ?></span>
</td>
<td>
<span class="label">Alíquota IBS UF</span>
<span class="value"><?= $data['ibs_cbs']['aliquota_ibs_uf'] ?></span>
</td>
<td>
<span class="label">Alíquota IBS Município</span>
<span class="value"><?= $data['ibs_cbs']['aliquota_ibs_municipal'] ?></span>
</td>
<td><span class="label">Exclusões e Reduções da Base de Cálculo</span><span class="value"><?= $data['ibs_cbs']['exclusoes_reducoes'] ?></span></td>
<td><span class="label">Base de Cálculo Após Exclusões e Reduções</span><span class="value"><?= $data['ibs_cbs']['base_calculo'] ?></span></td>
<td><span class="label">Red. Alíquota IBS / Red. Alíquota CBS</span><span class="value"><?= $data['ibs_cbs']['reducao_aliquotas'] ?></span></td>
<td><span class="label">Alíquota - IBS UF / IBS Mun</span><span class="value"><?= $data['ibs_cbs']['aliquota_ibs'] ?></span></td>
</tr>
<tr>
<td>
<span class="label">Reduções de Alíquota IBS/CBS</span>
<span class="value">-</span>
</td>
<td>
<span class="label">Alíquota CBS</span>
<span class="value"><?= $data['ibs_cbs']['aliquota_cbs'] ?></span>
</td>
<td>
<span class="label">Total IBS</span>
<span class="value"><?= $data['ibs_cbs']['total_ibs'] ?></span>
</td>
<td>
<span class="label">Total CBS</span>
<span class="value"><?= $data['ibs_cbs']['total_cbs'] ?></span>
</td>
<td><span class="label">Alíq. Efetiva Municipal - IBS</span><span class="value"><?= $data['ibs_cbs']['aliquota_efetiva_municipal'] ?></span></td>
<td><span class="label">Valor Apurado Municipal - IBS</span><span class="value"><?= $data['ibs_cbs']['valor_ibs_municipal'] ?></span></td>
<td><span class="label">Alíq. Efetiva Estadual - IBS</span><span class="value"><?= $data['ibs_cbs']['aliquota_efetiva_estadual'] ?></span></td>
<td><span class="label">Valor Apurado Estadual - IBS</span><span class="value"><?= $data['ibs_cbs']['valor_ibs_estadual'] ?></span></td>
</tr>
<tr>
<td colspan="3"></td>
<td>
<span class="label">Valor Total da NFS-e c/ IBS/CBS</span>
<span class="value"><?= $data['ibs_cbs']['valor_total_nfse'] ?></span>
</td>
<td><span class="label">Valor Total Apurado - IBS</span><span class="value"><?= $data['ibs_cbs']['total_ibs'] ?></span></td>
<td><span class="label">Alíquota - CBS</span><span class="value"><?= $data['ibs_cbs']['aliquota_cbs'] ?></span></td>
<td><span class="label">Alíquota Efetiva - CBS</span><span class="value"><?= $data['ibs_cbs']['aliquota_efetiva_cbs'] ?></span></td>
<td><span class="label">Valor Total Apurado - CBS</span><span class="value"><?= $data['ibs_cbs']['total_cbs'] ?></span></td>
</tr>
</table>
</div>
Expand All @@ -450,9 +413,9 @@
<td><span class="label">Desconto Condicionado</span><span class="value"><?= $data['totais']['desconto_condicionado'] ?></span></td>
</tr>
<tr>
<td><span class="label">Retenções Federais</span><span class="value"><?= $data['totais']['retencoes_federais'] ?></span></td>
<td><span class="label">Total das Retenções (ISSQN / Federais)</span><span class="value"><?= $data['totais']['total_retencoes'] ?></span></td>
<td><span class="label">VALOR LÍQUIDO DA NFS-e</span><span class="value"><?= $data['totais']['valor_liquido'] ?></span></td>
<td><span class="label">ISSQN Retido / Total IBS/CBS</span><span class="value"><?= $data['totais']['issqn_retido'] ?> / <?= $data['ibs_cbs']['total_ibs_cbs'] ?></span></td>
<td><span class="label">Total do IBS/CBS</span><span class="value"><?= $data['ibs_cbs']['total_ibs_cbs'] ?></span></td>
<td class="total-emphasis"><span class="label">VALOR LÍQUIDO DA NFS-e + IBS/CBS</span><span class="value value-highlight"><?= $data['ibs_cbs']['valor_total_nfse'] ?></span></td>
</tr>
</table>
Expand Down
93 changes: 93 additions & 0 deletions tests/Unit/Danfse/DanfseTemplateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,99 @@ public function testAuthorizedMunicipalTotalsUseNfseValuesNotDpsTaxFlags(): void
self::assertSame('R$ 630,00', $data['tributacao_municipal']['issqn_apurado']);
}

public function testIbsCbsFullGridUsesOnlyAuthorizedAmountsAndRates(): void
{
$nfse = $this->fixtureNfseData();
$authorized = & $nfse['infNFSe'];
$dps = & $authorized['DPS']['infDPS'];
$dps['valores']['vDescCondIncond'] = ['vDescIncond' => '10.00', 'vDescCond' => '5.00'];
$authorized['valores']['vISSQN'] = '7.00';
$authorized['valores']['vTotalRet'] = '123.45';
$authorized['IBSCBS'] = [
'cLocalidadeIncid' => '3303302',
'xLocalidadeIncid' => 'Niterói',
'valores' => [
'vBC' => '1450.00',
'vCalcReeRepRes' => '20.00',
'uf' => ['pRedAliqUF' => '0.00', 'pIBSUF' => '0.10', 'pAliqEfetUF' => '0.10'],
'mun' => ['pRedAliqMun' => '0.00', 'pIBSMun' => '0.05', 'pAliqEfetMun' => '0.05'],
'fed' => ['pRedAliqCBS' => '0.00', 'pCBS' => '0.90', 'pAliqEfetCBS' => '0.90'],
],
'totCIBS' => [
'vTotNF' => '1515.75',
'gIBS' => [
'vIBSTot' => '2.25',
'gIBSMunTot' => ['vIBSMun' => '0.75'],
'gIBSUFTot' => ['vIBSUF' => '1.50'],
],
'gCBS' => ['vCBS' => '13.50'],
],
];
$dps['IBSCBS']['cIndOp'] = '010101';

$template = new DanfseTemplate();
$data = $template->buildData($nfse);
self::assertSame('010101 / 3303302 / Niterói / RJ', $data['ibs_cbs']['indicador_operacao']);
self::assertSame('R$ 91,75', $data['ibs_cbs']['exclusoes_reducoes']);
self::assertSame('0.00% / 0.00% / 0.00%', $data['ibs_cbs']['reducao_aliquotas']);
self::assertSame('0.10% / 0.05%', $data['ibs_cbs']['aliquota_ibs']);
self::assertSame('0.05%', $data['ibs_cbs']['aliquota_efetiva_municipal']);
self::assertSame('R$ 0,75', $data['ibs_cbs']['valor_ibs_municipal']);
self::assertSame('0.10%', $data['ibs_cbs']['aliquota_efetiva_estadual']);
self::assertSame('R$ 1,50', $data['ibs_cbs']['valor_ibs_estadual']);
self::assertSame('0.90%', $data['ibs_cbs']['aliquota_efetiva_cbs']);
self::assertSame('R$ 15,75', $data['ibs_cbs']['total_ibs_cbs']);
self::assertSame('R$ 123,45', $data['totais']['total_retencoes']);
self::assertSame('R$ 10,00', $data['totais']['desconto_incondicionado']);
self::assertSame('R$ 5,00', $data['totais']['desconto_condicionado']);

$html = $template->render($nfse, new DanfseConfig());
self::assertStringContainsString('Valor Apurado Municipal - IBS', $html);
self::assertStringContainsString('Valor Apurado Estadual - IBS', $html);
self::assertStringContainsString('Alíquota Efetiva - CBS', $html);
self::assertStringContainsString('Total das Retenções (ISSQN / Federais)', $html);
}

public function testAbsentIbsOrCbsAmountIsNotTreatedAsRealZero(): void
{
$nfse = $this->fixtureNfseData();
$nfse['infNFSe']['IBSCBS']['totCIBS']['gIBS']['vIBSTot'] = '0.00';
$template = new DanfseTemplate();
self::assertSame('-', $template->buildData($nfse)['ibs_cbs']['total_ibs_cbs']);

$nfse['infNFSe']['IBSCBS']['totCIBS']['gCBS']['vCBS'] = '0.00';
self::assertSame('R$ 0,00', $template->buildData($nfse)['ibs_cbs']['total_ibs_cbs']);
}

public function testLegacyXmlWithoutIbsCbsDoesNotInventIbsExclusions(): void
{
$nfse = $this->fixtureNfseData();
unset($nfse['infNFSe']['IBSCBS']);
// The old DPS can still contain PIS/COFINS amounts. Without any
// authorized IBS/CBS block they are not a new IBS base reduction.
$nfse['infNFSe']['DPS']['infDPS']['valores']['trib']['tribFed']['piscofins']['vPis'] = '12.25';

$model = (new DanfseTemplate())->buildData($nfse);
self::assertSame('-', $model['ibs_cbs']['exclusoes_reducoes']);
self::assertSame('-', $model['ibs_cbs']['base_calculo']);
}

public function testAuthorizedTotalRetentionNeverComesFromPartialDpsSum(): void
{
$nfse = $this->fixtureNfseData();
$dps = & $nfse['infNFSe']['DPS']['infDPS'];
$dps['valores']['trib']['tribFed']['vRetIRRF'] = '99.99';

$template = new DanfseTemplate();
self::assertSame('-', $template->buildData($nfse)['totais']['total_retencoes']);

$nfse['infNFSe']['valores']['vTotalRet'] = '0.00';
self::assertSame('R$ 0,00', $template->buildData($nfse)['totais']['total_retencoes']);

$nfse['infNFSe']['valores']['vTotalRet'] = '78.90';
self::assertSame('R$ 78,90', $template->buildData($nfse)['totais']['total_retencoes']);
}

public function testDescriptionPreservesNewlinesWithoutRenderingMarkup(): void
{
$nfse = $this->fixtureNfseData();
Expand Down
Loading