Test Setup Failed
Push — master ( 99ee47...d58c12 )
by JAIME ELMER
01:53
created

DocumentGenerator::generateDocCode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * FACTURA ELECTRÓNICA SUNAT
5
 * UBL 2.1
6
 * Version 1.0
7
 * 
8
 * Copyright 2018, Jaime Cruz
9
 */
10
11
namespace F72X\Sunat;
12
13
use InvalidArgumentException;
14
use DateTime;
15
16
use F72X\Tools\XmlService;
17
use F72X\Tools\XmlDSig;
18
use F72X\Tools\FileService;
19
use F72X\Company;
20
use F72X\Sunat\Document\SunatDocument;
21
use F72X\Sunat\Document\Factura;
22
use F72X\Sunat\SunatVars;
23
24
use F72X\UblComponent\OrderReference;
25
use F72X\UblComponent\Signature;
26
use F72X\UblComponent\SignatoryParty;
27
use F72X\UblComponent\Party;
28
use F72X\UblComponent\PartyIdentification;
29
use F72X\UblComponent\PartyName;
30
use F72X\UblComponent\DigitalSignatureAttachment;
31
use F72X\UblComponent\ExternalReference;
32
use F72X\UblComponent\AccountingSupplierParty;
33
use F72X\UblComponent\AccountingCustomerParty;
34
use F72X\UblComponent\PartyLegalEntity;
35
use F72X\UblComponent\TaxTotal;
36
use F72X\UblComponent\TaxSubTotal;
37
use F72X\UblComponent\TaxCategory;
38
use F72X\UblComponent\TaxScheme;
39
use F72X\UblComponent\LegalMonetaryTotal;
40
use F72X\UblComponent\InvoiceLine;
41
use F72X\UblComponent\AllowanceCharge;
42
use F72X\UblComponent\PricingReference;
43
use F72X\UblComponent\AlternativeConditionPrice;
44
use F72X\UblComponent\Item;
45
use F72X\UblComponent\SellersItemIdentification;
46
use F72X\UblComponent\CommodityClassification;
47
use F72X\UblComponent\Price;
48
49
class DocumentGenerator {
50
51
    public static function generateFactura($data, $currencyID = 'PEN') {
52
53
        if (!is_array($data)) {
54
            throw new InvalidArgumentException('$data, Se esperá un array!');
55
        }
56
57
        // Documento XML para la factura
58
        $Document = new Factura();
59
60
        // Tipo de operación
61
        $Document->setProfileID($data['operationTypeCode']);
62
63
        // ID Serie + Numero
64
        $ID = 'F' . str_pad($data['voucherSeries'], 3, '0', STR_PAD_LEFT) . '-' . str_pad($data['voucherNumber'], 8, '0', STR_PAD_LEFT);
65
        $Document->setID($ID);
66
67
        // Fecha de emisión
68
        $IssueDate = isset($data['date']) ? $data['date'] : new DateTime();
69
        $Document->setIssueDate($IssueDate);
70
71
        // Currency code
72
        $Document->setDocumentCurrencyCode($currencyID);
73
74
        // LineCountNumeric: Total items
75
        $Document->setLineCountNumeric(count($data['items']));
76
77
        // Reference order
78
        $orderRef = new OrderReference();
79
            $orderRef->setID($data['purchaseOrder']);
80
        $Document->setOrderReference($orderRef);
81
82
        // AccountingSupplierParty: Company info
83
        self::addAccountingSupplierParty($Document);
84
85
        // 11. AccountingCustomerParty: Customer info
86
        self::addAccountingCustomerParty($Document, $data);
87
88
        // Detalle
89
        self::addInvoiceLines($Document, $data['items']);
90
        
91
        // Detail opertions matrix
92
        $DOM = $Document->getDetailMatrix();
93
94
        // Descuentos globales
95
        if (isset($data['allowances'])) {
96
            foreach ($data['allowances'] as $discount) {
97
                $baseAmount = $DOM->getTotalTaxableAmount();
98
                $k = $discount['multiplierFactor'];
99
                $amount = $baseAmount * $k;
100
                self::addAllowanceCharge($Document, $currencyID, 'false', $discount['reasonCode'], $discount['multiplierFactor'], $amount, $baseAmount);
101
            }
102
        }
103
104
        // Impuestos
105
        self::addTaxes($Document);
106
107
        // Totales
108
        self::addLegalMonetaryTotal($Document);
109
        
110
        // Save Document
111
        self::saveInvoice($Document);
112
        // Sign Document
113
        self::singInvoice($Document);
114
        // Compress Document
115
        self::zipInvoice($Document);
116
    }
117
    private static function singInvoice(SunatDocument $Document){
118
        $xmlFile = $Document->getFileName();
119
        XmlDSig::sign($xmlFile);
120
    }
121
    private static function zipInvoice(SunatDocument $Document){
122
        $xmlFile = $Document->getFileName();
123
        FileService::doZip($xmlFile);
124
    }
125
    private static function addLegalMonetaryTotal(SunatDocument $Invoice) {
126
127
        $DOM                = $Invoice->getDetailMatrix();
128
        $currencyID         = $Invoice->getDocumentCurrencyCode();  // Tipo de moneda
129
        $itemsAllowance     = $DOM->getTotalAllowances();           // Total descuentos por item
130
        $taxableOperations  = $DOM->getTotalTaxableOperations();    // Total operaciones gravadas
131
        $exemptedOperations = $DOM->getTotalExcemptedOperations();  // Total operaciones exoneradas
132
133
        // Total descuentos globales
134
        $gloabalAllowance = 0;
135
        $AllowanceCharges = $Invoice->getAllowanceCharges();
136
        foreach ($AllowanceCharges as $AllowanceCharge) {
137
            if ($AllowanceCharge->getChargeIndicator() === 'false') {
138
                $gloabalAllowance += $AllowanceCharge->getAmount();
139
            }
140
        }
141
        // Descuentos totales = Total descuentos globales + Total descuentos por item
142
        $totalAllowance = $gloabalAllowance + $itemsAllowance;
143
        
144
        // Total a pagar = (Operaciones gravadas - descuentos + cargos)*(1+IGV) + (Operaciones exóneradas - descuentos + cargos)
145
        $payableAmount = self::applyAllowancesAndCharges($Invoice, $taxableOperations) * (1 + SunatVars::IGV) + self::applyAllowancesAndCharges($Invoice, $exemptedOperations);
146
        // LegalMonetaryTotal
147
        $LegalMonetaryTotal = new LegalMonetaryTotal();
148
        $LegalMonetaryTotal
149
                ->setCurrencyID($currencyID)
150
                ->setLineExtensionAmount($DOM->getTotalPayableValue()) // Valor de la venta
151
                ->setTaxInclusiveAmount($payableAmount)         // Valor de la venta incluido impuestos
152
                ->setAllowanceTotalAmount($totalAllowance)
153
                ->setPayableAmount($payableAmount);
154
155
        $Invoice->setLegalMonetaryTotal($LegalMonetaryTotal);
156
    }
157
158
    /**
159
     * 
160
     * @param SunatDocument|InvoiceLine $source
161
     * @param float $baseAmount
162
     * @return float
163
     */
164
    private static function applyAllowancesAndCharges($source, $baseAmount) {
165
        $AllowanceCharges = $source->getAllowanceCharges();
166
        $allowance = 0;
167
        $charge = 0;
168
        foreach ($AllowanceCharges as $AllowanceCharge) {
169
            $k = $AllowanceCharge->getMultiplierFactorNumeric();
170
            if ($AllowanceCharge->getChargeIndicator() === 'false') {
171
                $allowance += $baseAmount * $k;
172
            } else {
173
                $charge += $baseAmount * $k;
174
            }
175
        }
176
        return $baseAmount - $allowance + $charge;
177
    }
178
179
    private static function addInvoiceLines(SunatDocument $Invoice, $items) {
180
        $currencyID = $Invoice->getDocumentCurrencyCode();  // Tipo de moneda
181
        $DetailMatrix = new DetailMatrix();                 // Matriz de calculos
182
        $DetailMatrix->populate($items, $currencyID);
183
        $Invoice->setDetailMatrix($DetailMatrix);
184
        $ln = count($items);
185
        // Loop
186
        for ($i = 0; $i < $ln; $i++) {
187
            self::addInvoiceLine($Invoice, $i);
188
        }
189
    }
190
191
    /**
192
     * 
193
     * @param SunatDocument $Invoice
194
     * @param int $itemIndex Index del item
195
     */
196
    private static function addInvoiceLine(SunatDocument $Invoice, $itemIndex) {
197
198
        // XML Nodes
199
        $InvoiceLine      = new InvoiceLine();
200
        $PricingReference = new PricingReference();
201
        $TaxTotal         = new TaxTotal();
202
        $TaxSubTotal      = new TaxSubTotal();
203
        $TaxCategory      = new TaxCategory();
204
        $TaxCategory
205
                ->setElementAttributes('ID', [
206
                    'schemeID' => 'UN/ECE 5305',
207
                    'schemeName' => 'Tax Category Identifier',
208
                    'schemeAgencyName' => 'United Nations Economic Commission for Europe'])
209
                ->setElementAttributes('TaxExemptionReasonCode', [
210
                    'listAgencyName' => 'PE:SUNAT',
211
                    'listName' => 'SUNAT:Codigo de Tipo de Afectación del IGV',
212
                    'listURI' => 'urn:pe:gob:sunat:cpe:see:gem:catalogos:catalogo07']);
213
214
        $TaxScheme = new TaxScheme();
215
        $TaxScheme
216
                ->setElementAttributes('ID', [
217
                    'schemeID' => 'UN/ECE 5153',
218
                    'schemeName' => 'Tax Scheme Identifier',
219
                    'schemeAgencyName' => 'United Nations Economic Commission for Europe']);
220
221
        $AlternativeConditionPrice  = new AlternativeConditionPrice();
222
        $Item                       = new Item();
223
        $SellersItemIdentification  = new SellersItemIdentification();
224
        $CommodityClassification    = new CommodityClassification();
225
        $Price                      = new Price();
226
        // Detail Operation Matrix
227
        $DOM = $Invoice->getDetailMatrix();
228
        // Vars
229
        $productCode        = $DOM->getProductCode($itemIndex);
230
        $sunatProductCode   = $DOM->getUNPSC($itemIndex);
231
        $unitCode           = $DOM->getUnitCode($itemIndex);
232
        $quantity           = $DOM->getQunatity($itemIndex);
233
        $description        = $DOM->getDescription($itemIndex);
234
        $currencyID         = $DOM->getCurrencyCode($itemIndex);
235
        $unitBillableValue  = $DOM->getUnitBillableValue($itemIndex);
236
        $priceTypeCode      = $DOM->getPriceTypeCode($itemIndex);
237
        $taxTypeCode        = $DOM->getTaxTypeCode($itemIndex);
238
        $igvAffectationCode = $DOM->getIgvAffectationCode($itemIndex);
239
        
240
        $itemValue          = $DOM->getItemValue($itemIndex);
241
        $allowances         = $DOM->getAllowances($itemIndex);
242
        $charges            = $DOM->getCharges($itemIndex);
243
        $itemTaxableAmount  = $DOM->getTaxableAmount($itemIndex);
244
        $itemTaxAmount      = $DOM->getIgv($itemIndex);
245
        $unitPrice          = $DOM->getUnitPayableAmount($itemIndex);
246
247
        // Catálogo 5 Ipuesto aplicable
248
        $cat5Item = Catalogo::getCatItem(5, $taxTypeCode);
249
250
        // Descuentos/Cargos
251
        // Descuentos
252
        foreach ($allowances as $item) {
253
            $multFactor = $item['multiplierFactor'];
254
            $amount = $itemValue * $multFactor;
255
            self::addAllowanceCharge($InvoiceLine, $currencyID, 'false', $item['reasonCode'], $multFactor, $amount, $itemValue);
256
        }
257
        // Cargos
258
        foreach ($charges as $item) {
259
            $multFactor = $item['multiplierFactor'];
260
            $amount = $itemValue * $multFactor;
261
            self::addAllowanceCharge($InvoiceLine, $currencyID, 'true', $item['reasonCode'], $multFactor, $amount, $itemValue);
262
        }
263
264
        $InvoiceLine
265
                ->setCurrencyID($currencyID)                    // Tipo de moneda
266
                ->setID($itemIndex + 1)                          // Número de orden
267
                ->setUnitCode($unitCode)                        // Codigo de unidad de medida
268
                ->setInvoicedQuantity($quantity)                // Cantidad
269
                ->setLineExtensionAmount($itemTaxableAmount)    // Valor de venta del ítem, sin impuestos
270
                ->setPricingReference($PricingReference
271
                        ->setAlternativeConditionPrice($AlternativeConditionPrice
272
                                ->setCurrencyID($currencyID)            // Tipo de moneda
273
                                ->setPriceAmount($unitPrice)            // Precio de venta unitario
274
                                ->setPriceTypeCode($priceTypeCode)))    // Price
275
                ->setTaxTotal($TaxTotal
276
                        ->setCurrencyID($currencyID)
277
                        ->setTaxAmount($itemTaxAmount)
278
                        ->addTaxSubTotal($TaxSubTotal
279
                                ->setCurrencyID($currencyID)            // Tipo de moneda
280
                                ->setTaxableAmount($itemTaxableAmount)  // Valor de venta del item sin impuestos
281
                                ->setTaxAmount($itemTaxAmount)          // IGV
282
                                ->setTaxCategory($TaxCategory
283
                                        ->setID($cat5Item['categoria'])                     // Codigo de categoria de immpuestos @CAT5
284
                                        ->setPercent(SunatVars::IGV_PERCENT)                // Porcentaje de IGV (18.00)
285
                                        ->setTaxExemptionReasonCode($igvAffectationCode)    // Código de afectación del IGV
286
                                        ->setTaxScheme($TaxScheme
287
                                                ->setID($taxTypeCode)                       // Codigo de categoria de impuesto
288
                                                ->setName($cat5Item['name'])
289
                                                ->setTaxTypeCode($cat5Item['UN_ECE_5153'])))))
290
                ->setItem($Item
291
                        ->setDescription($description)                              // Descripción
292
                        ->setSellersItemIdentification($SellersItemIdentification
293
                                ->setID($productCode))                              // Código de producto
294
                        ->setCommodityClassification($CommodityClassification
295
                                ->setItemClassificationCode($sunatProductCode)))    // Código de producto SUNAT
296
                ->setPrice($Price
297
                        ->setCurrencyID($currencyID)    // Tipo de moneda
298
                        ->setPriceAmount($unitBillableValue)    // Precio unitario del item
299
        );
300
        // Añade item
301
        $Invoice->addInvoiceLine($InvoiceLine);
302
    }
303
304
    /**
305
     * 
306
     * @param SunatDocument|InvoiceLine $target
307
     * @param string $currencyID
308
     * @param string $ChargeIndicator
309
     * @param string $AllowanceChargeReasonCode
310
     * @param float $Amount
311
     * @param float $BaseAmount
312
     */
313
    private static function addAllowanceCharge($target, $currencyID, $ChargeIndicator, $AllowanceChargeReasonCode, $MultiplierFactorNumeric, $Amount, $BaseAmount) {
314
        $AllowanceCharge = new AllowanceCharge();
315
        $AllowanceCharge
316
                ->setCurrencyID($currencyID)
317
                ->setChargeIndicator($ChargeIndicator)
318
                ->setAllowanceChargeReasonCode($AllowanceChargeReasonCode)
319
                ->setMultiplierFactorNumeric($MultiplierFactorNumeric)
320
                ->setAmount($Amount)
321
                ->setBaseAmount($BaseAmount);
322
        // Add AllowanceCharge
323
        $target
324
                ->addAllowanceCharge($AllowanceCharge);
325
    }
326
327
    private static function addTaxes(SunatDocument $Invoice) {
328
        $DOM                  = $Invoice->getDetailMatrix();
329
        $currencyID           = $Invoice->getDocumentCurrencyCode();  // Tipo de moneda
330
        $taxableOperations    = $DOM->getTotalTaxableOperations();    // Total operaciones gravadas
331
        $exemptedOperations   = $DOM->getTotalExcemptedOperations();  // Total operaciones exoneradas
332
        $unaffectedOperations = $DOM->getTotalUnaffectedOperations(); // Total operaciones inafectas
333
334
        // XML nodes
335
        $TaxTotal = new TaxTotal();
336
337
338
        // Operaciones gravadas - aplicando cargos y descuetos
339
        $igvBaseAmount = self::applyAllowancesAndCharges($Invoice, $taxableOperations);
340
        $taxAmount = $igvBaseAmount * SunatVars::IGV;
341
        self::addTaxSubtotal($TaxTotal, $currencyID, $taxAmount, $igvBaseAmount,    DetailMatrix::TAX_IGV);
342
        
343
        // Total operaciones exoneradas - aplicando cargos y descuetos
344
        $exoBaseAmount = self::applyAllowancesAndCharges($Invoice, $exemptedOperations);
345
        self::addTaxSubtotal($TaxTotal, $currencyID, 0, $exoBaseAmount,   DetailMatrix::TAX_EXO);
346
347
        // Total operaciones inafectas
348
        self::addTaxSubtotal($TaxTotal, $currencyID, 0, $unaffectedOperations, DetailMatrix::TAX_INA);
349
        
350
        // Total impuestos
351
        $TaxTotal
352
                ->setCurrencyID($currencyID)
353
                ->setTaxAmount($taxAmount);
354
        // Anadir al documento
355
        $Invoice->setTaxTotal($TaxTotal);
356
    }
357
358
    private static function addTaxSubtotal(TaxTotal $TaxTotal, $currencyID, $taxAmount, $taxableAmount, $taxTypeCode) {
359
        // XML nodes
360
        $TaxSubTotal = new TaxSubTotal();
361
        $TaxCategory = new TaxCategory();
362
        $TaxCategory->setElementAttributes('ID', [
363
            'schemeID'          => 'UN/ECE 5305',
364
            'schemeName'        => 'Tax Category Identifier',
365
            'schemeAgencyName'  => 'United Nations Economic Commission for Europe'
366
        ]);
367
        $TaxScheme = new TaxScheme();
368
        $TaxScheme->setElementAttributes('ID', [
369
            'schemeID'          => 'UN/ECE 5153',
370
            'schemeAgencyID'    => '6']);
371
372
        $cat5Item = Catalogo::getCatItem(5, $taxTypeCode);
373
        
374
        $TaxSubTotal
375
                ->setCurrencyID($currencyID)
376
                ->setTaxAmount($taxAmount)
377
                ->setTaxableAmount($taxableAmount)
378
                ->setTaxCategory($TaxCategory
379
                        ->setID($cat5Item['categoria'])
380
                        ->setTaxScheme($TaxScheme
381
                                ->setID($taxTypeCode)
382
                                ->setName($cat5Item['name'])
383
                                ->setTaxTypeCode($cat5Item['UN_ECE_5153'])));
384
        $TaxTotal->addTaxSubTotal($TaxSubTotal);
385
    }
386
387
    public static function addAccountingSupplierParty(SunatDocument $Document) {
388
        // Info
389
        $partyName  = Company::getBusinessName();
390
        $regName    = Company::getCompanyName();
391
        $docNumber  = Company::getRUC();
392
        $docType    = Catalogo::IDENTIFICATION_DOC_RUC;
393
        
394
        // XML nodes
395
        $AccountingSupplierParty    = new AccountingSupplierParty();
396
        $Party                      = new Party();
397
        $PartyIdentification        = new PartyIdentification();
398
        $PartyIdentification
399
                ->setElementAttributes('ID', [
400
                    'schemeAgencyName'  => 'PE:SUNAT',
401
                    'schemeID'          => $docType,
402
                    'schemeName'        => 'Documento de Identidad',
403
                    'schemeURI'         => 'urn:pe:gob:sunat:cpe:see:gem:catalogos:catalogo06']);
404
        $PartyName                  = new PartyName();
405
        $PartyLegalEntity           = new PartyLegalEntity();
406
        
407
        $AccountingSupplierParty
408
                ->setParty($Party
409
                        ->setPartyIdentification($PartyIdentification
410
                                ->setID($docNumber))
411
                        ->setPartyName($PartyName
412
                                ->setName($partyName))
413
                        ->setPartyLegalEntity($PartyLegalEntity
414
                                ->setRegistrationName($regName)));
415
        // Add to Document
416
        $Document->setAccountingSupplierParty($AccountingSupplierParty);
417
    }
418
419
    public static function addAccountingCustomerParty(SunatDocument $Document, $data) {
420
        // Info
421
        $regName     = $data['customerName'];
422
        $docNumber   = $data['customerDocNumber'];
423
        $docType     = $data['customerDocType'];
424
//        $taxSchemeID = '-';
425
        
426
        // XML nodes
427
        $AccountingCustomerParty    = new AccountingCustomerParty();
428
        $Party                      = new Party();
429
        $PartyIdentification        = new PartyIdentification();
430
        $PartyLegalEntity           = new PartyLegalEntity();
431
        $PartyIdentification
432
                ->setElementAttributes('ID', [
433
                    'schemeAgencyName'  => 'PE:SUNAT',
434
                    'schemeID'          => $docType,
435
                    'schemeName'        => 'Documento de Identidad',
436
                    'schemeURI'         => 'urn:pe:gob:sunat:cpe:see:gem:catalogos:catalogo06']);
437
        
438
        $AccountingCustomerParty
439
                ->setParty($Party
440
                        ->setPartyIdentification($PartyIdentification
441
                                ->setID($docNumber))
442
                        ->setPartyLegalEntity($PartyLegalEntity
443
                                ->setRegistrationName($regName)));
444
        // Add to Document
445
        $Document->setAccountingCustomerParty($AccountingCustomerParty);
446
    }
447
448
    public static function saveInvoice(SunatDocument $invoice) {
449
        $repository = Company::getRepositoryPath();
450
        $xmlService = new XmlService('1.0', 'ISO-8859-1');
451
452
        $xmlService->namespaceMap = [
453
            "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"                        => '',
454
            "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"      => 'cac',
455
            "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"          => 'cbc',
456
            "urn:un:unece:uncefact:documentation:2"                                         => 'ccts',
457
            "http://www.w3.org/2000/09/xmldsig#"                                            => 'ds',
458
            "urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2"      => 'ext',
459
            "urn:oasis:names:specification:ubl:schema:xsd:QualifiedDatatypes-2"             => 'qdt',
460
            "urn:un:unece:uncefact:data:specification:UnqualifiedDataTypesSchemaModule:2"   => 'udt',
461
            "http://www.w3.org/2001/XMLSchema-instance"                                     => 'xsi'
462
        ];
463
        $fileName = $invoice->getFileName();
464
        file_put_contents("$repository/xml/$fileName", $xmlService->write('Invoice', $invoice));
465
    }
466
467
}
468