Skip to content

Commit e5070cf

Browse files
committed
feat(perception): soportar sac:ExceptionalIndicator (campo 7)
1 parent c9faa40 commit e5070cf

6 files changed

Lines changed: 228 additions & 0 deletions

File tree

packages/core/src/Core/Model/Perception/Perception.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,18 @@ class Perception implements DocumentInterface
7676
*/
7777
private $observacion;
7878

79+
/**
80+
* Indicador de emision excepcional.
81+
*
82+
* Campo 7 de la hoja «Percepciones1_0» de las reglas de validacion de
83+
* SUNAT. Su unico valor admitido es '01' (regla 3322) y, cuando se
84+
* consigna, el comprobante solo puede llevar un documento relacionado
85+
* (3323) y ese documento debe ser una factura (3324).
86+
*
87+
* @var string
88+
*/
89+
private $exceptionalIndicator;
90+
7991
/**
8092
* Dato del Comprobante relacionado.
8193
*
@@ -283,6 +295,25 @@ public function setObservacion(?string $observacion): Perception
283295
return $this;
284296
}
285297

298+
/**
299+
* @return string|null
300+
*/
301+
public function getExceptionalIndicator(): ?string
302+
{
303+
return $this->exceptionalIndicator;
304+
}
305+
306+
/**
307+
* @param string|null $exceptionalIndicator
308+
* @return Perception
309+
*/
310+
public function setExceptionalIndicator(?string $exceptionalIndicator): Perception
311+
{
312+
$this->exceptionalIndicator = $exceptionalIndicator;
313+
314+
return $this;
315+
}
316+
286317
/**
287318
* @return PerceptionDetail[]
288319
*/

packages/xml-parser/src/Xml/Parser/PerceptionParser.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public function parse($value): ?DocumentInterface
5757
$perception->setSerie($idNum[0])
5858
->setCorrelativo($idNum[1])
5959
->setFechaEmision(new DateTime($xml->getValue('cbc:IssueDate')))
60+
->setExceptionalIndicator($xml->getValue('sac:ExceptionalIndicator', $root, null))
6061
->setCompany($this->getCompany())
6162
->setProveedor($this->getClient())
6263
->setRegimen($xml->getValue('sac:SUNATPerceptionSystemCode'))
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Perception xmlns="urn:sunat:names:specification:ubl:peru:schema:xsd:Perception-1" xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns:ccts="urn:un:unece:uncefact:documentation:2" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ext="urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2" xmlns:qdt="urn:oasis:names:specification:ubl:schema:xsd:QualifiedDatatypes-2" xmlns:sac="urn:sunat:names:specification:ubl:peru:schema:xsd:SunatAggregateComponents-1" xmlns:udt="urn:un:unece:uncefact:data:specification:UnqualifiedDataTypesSchemaModule:2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
3+
<ext:UBLExtensions>
4+
<ext:UBLExtension>
5+
<ext:ExtensionContent>
6+
<ds:Signature Id="NubeFacTSign"/>
7+
</ext:ExtensionContent>
8+
</ext:UBLExtension>
9+
</ext:UBLExtensions>
10+
<cbc:UBLVersionID>2.0</cbc:UBLVersionID>
11+
<cbc:CustomizationID>1.0</cbc:CustomizationID>
12+
<cac:Signature>
13+
<cbc:ID>20000000001</cbc:ID>
14+
<cac:SignatoryParty>
15+
<cac:PartyIdentification>
16+
<cbc:ID>20000000001</cbc:ID>
17+
</cac:PartyIdentification>
18+
<cac:PartyName>
19+
<cbc:Name>TECNOMAQ V &amp; L SRL</cbc:Name>
20+
</cac:PartyName>
21+
</cac:SignatoryParty>
22+
<cac:DigitalSignatureAttachment>
23+
<cac:ExternalReference>
24+
<cbc:URI>20000000001</cbc:URI>
25+
</cac:ExternalReference>
26+
</cac:DigitalSignatureAttachment>
27+
</cac:Signature>
28+
<cbc:ID>P001-2</cbc:ID>
29+
<cbc:IssueDate>2018-01-26</cbc:IssueDate>
30+
<sac:ExceptionalIndicator>01</sac:ExceptionalIndicator>
31+
<cac:AgentParty>
32+
<cac:PartyIdentification>
33+
<cbc:ID schemeID="6">20000000001</cbc:ID>
34+
</cac:PartyIdentification>
35+
<cac:PartyName>
36+
<cbc:Name>TECNOMAQ V &amp; L SRL</cbc:Name>
37+
</cac:PartyName>
38+
<cac:PostalAddress>
39+
<cbc:ID>150122</cbc:ID>
40+
<cbc:StreetName>Miraflore</cbc:StreetName>
41+
<cbc:CitySubdivisionName>SANTIAGO DE CHUCO</cbc:CitySubdivisionName>
42+
<cbc:CityName>SANTIAGO DE CHUCO</cbc:CityName>
43+
<cbc:CountrySubentity>LIMA</cbc:CountrySubentity>
44+
<cbc:District>MIRAFLORES</cbc:District>
45+
<cac:Country>
46+
<cbc:IdentificationCode>PE</cbc:IdentificationCode>
47+
</cac:Country>
48+
</cac:PostalAddress>
49+
<cac:PartyLegalEntity>
50+
<cbc:RegistrationName>TECNOMAQ V &amp; L SRL</cbc:RegistrationName>
51+
</cac:PartyLegalEntity>
52+
</cac:AgentParty>
53+
<cac:ReceiverParty>
54+
<cac:PartyIdentification>
55+
<cbc:ID schemeID="6">1724754054</cbc:ID>
56+
</cac:PartyIdentification>
57+
<cac:PartyLegalEntity>
58+
<cbc:RegistrationName>TECNOMAQ V &amp; L SRL</cbc:RegistrationName>
59+
</cac:PartyLegalEntity>
60+
</cac:ReceiverParty>
61+
<sac:SUNATPerceptionSystemCode>01</sac:SUNATPerceptionSystemCode>
62+
<sac:SUNATPerceptionPercent>2.0</sac:SUNATPerceptionPercent>
63+
<cbc:Note>NOTA</cbc:Note>
64+
<cbc:TotalInvoiceAmount currencyID="PEN">0.02</cbc:TotalInvoiceAmount>
65+
<sac:SUNATTotalCashed currencyID="PEN">1.02</sac:SUNATTotalCashed>
66+
<sac:SUNATPerceptionDocumentReference>
67+
<cbc:ID schemeID="01">F001-12</cbc:ID>
68+
<cbc:IssueDate>2018-01-26</cbc:IssueDate>
69+
<cbc:TotalInvoiceAmount currencyID="PEN">12.00</cbc:TotalInvoiceAmount>
70+
<cac:Payment>
71+
<cbc:ID>212</cbc:ID>
72+
<cbc:PaidAmount currencyID="PEN">1.00</cbc:PaidAmount>
73+
<cbc:PaidDate>2018-01-26</cbc:PaidDate>
74+
</cac:Payment>
75+
<sac:SUNATPerceptionInformation>
76+
<sac:SUNATPerceptionAmount currencyID="PEN">0.02</sac:SUNATPerceptionAmount>
77+
<sac:SUNATPerceptionDate>2018-01-26</sac:SUNATPerceptionDate>
78+
<sac:SUNATNetTotalCashed currencyID="PEN">1.02</sac:SUNATNetTotalCashed>
79+
<cac:ExchangeRate>
80+
<cbc:SourceCurrencyCode>PEN</cbc:SourceCurrencyCode>
81+
<cbc:TargetCurrencyCode>PEN</cbc:TargetCurrencyCode>
82+
<cbc:CalculationRate>1.0</cbc:CalculationRate>
83+
<cbc:Date>2018-01-26</cbc:Date>
84+
</cac:ExchangeRate>
85+
</sac:SUNATPerceptionInformation>
86+
</sac:SUNATPerceptionDocumentReference>
87+
</Perception>

packages/xml-parser/tests/Xml/Parser/PerceptionParserTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,29 @@ public function testParseDoc($filename)
4343
}
4444
}
4545

46+
/**
47+
* Field 7 of SUNAT's "Percepciones1_0" sheet, and conditional: absent
48+
* from a document that is not exceptionally issued.
49+
*/
50+
public function testParseExceptionalIndicator()
51+
{
52+
$withIndicator = $this->parseResource('20000000001-40-P001-2.xml');
53+
$this->assertSame('01', $withIndicator->getExceptionalIndicator());
54+
55+
$without = $this->parseResource('20000000001-40-P001-1.xml');
56+
$this->assertNull($without->getExceptionalIndicator());
57+
}
58+
59+
private function parseResource(string $filename): Perception
60+
{
61+
$path = __DIR__.'/../../Resources/perception/'.$filename;
62+
63+
/**@var $obj Perception */
64+
$obj = $this->getParser()->parse(file_get_contents($path));
65+
66+
return $obj;
67+
}
68+
4669
public function providerDocs()
4770
{
4871
$files = glob(__DIR__.'/../../Resources/perception/*.xml');

packages/xml/src/Xml/Templates/perception.xml.twig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
<cbc:ID>{{ doc.serie }}-{{ doc.correlativo }}</cbc:ID>
2929
<cbc:IssueDate>{{ doc.fechaEmision|date('Y-m-d') }}</cbc:IssueDate>
3030
<cbc:IssueTime>{{ doc.fechaEmision|date('H:i:s') }}</cbc:IssueTime>
31+
{% if doc.exceptionalIndicator %}
32+
<sac:ExceptionalIndicator>{{ doc.exceptionalIndicator }}</sac:ExceptionalIndicator>
33+
{% endif %}
3134
<cac:AgentParty>
3235
<cac:PartyIdentification>
3336
<cbc:ID schemeID="6">{{ emp.ruc }}</cbc:ID>
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Greenter\Xml\Builder;
6+
7+
use DOMDocument;
8+
use DOMElement;
9+
use Greenter\Data\Generator\PerceptionStore;
10+
use Greenter\Data\SharedStore;
11+
use Greenter\Model\Perception\Perception;
12+
use Greenter\Xml\Builder\PerceptionBuilder;
13+
use PHPUnit\Framework\TestCase;
14+
15+
/**
16+
* Field 7 of SUNAT's "Percepciones1_0" validation sheet:
17+
* /Perception/sac:ExceptionalIndicator
18+
*
19+
* It is a conditional field (an2). Its only accepted value is '01' (rule
20+
* 3322); when present, the document may reference a single related document
21+
* (3323) and that document must be an invoice (3324).
22+
*/
23+
class PerceptionExceptionalIndicatorTest extends TestCase
24+
{
25+
public function testIsOmittedWhenNotSet(): void
26+
{
27+
$xml = $this->build(null);
28+
29+
self::assertStringNotContainsString('ExceptionalIndicator', $xml);
30+
}
31+
32+
public function testIsWrittenWithItsValue(): void
33+
{
34+
$xml = $this->build('01');
35+
36+
self::assertStringContainsString(
37+
'<sac:ExceptionalIndicator>01</sac:ExceptionalIndicator>',
38+
$xml
39+
);
40+
}
41+
42+
/**
43+
* The sheet numbers the fields in document order: 6 is cbc:IssueTime and
44+
* 8 is the agent's identity document, so field 7 belongs between them.
45+
*/
46+
public function testIsPlacedBetweenIssueTimeAndAgentParty(): void
47+
{
48+
$names = $this->childNames($this->build('01'));
49+
50+
$indicator = array_search('sac:ExceptionalIndicator', $names, true);
51+
self::assertNotFalse($indicator);
52+
53+
self::assertGreaterThan(array_search('cbc:IssueTime', $names, true), $indicator);
54+
self::assertLessThan(array_search('cac:AgentParty', $names, true), $indicator);
55+
}
56+
57+
private function build(?string $indicator): string
58+
{
59+
/** @var Perception $perception */
60+
$perception = (new PerceptionStore(new SharedStore()))->create();
61+
$perception->setExceptionalIndicator($indicator);
62+
63+
return (new PerceptionBuilder())->build($perception);
64+
}
65+
66+
/**
67+
* @return string[]
68+
*/
69+
private function childNames(string $xml): array
70+
{
71+
$doc = new DOMDocument();
72+
$doc->loadXML($xml);
73+
74+
$names = [];
75+
foreach ($doc->documentElement->childNodes as $node) {
76+
if ($node instanceof DOMElement) {
77+
$names[] = $node->nodeName;
78+
}
79+
}
80+
81+
return $names;
82+
}
83+
}

0 commit comments

Comments
 (0)