feat(sales): add Excel exports
This commit is contained in:
parent
78ce263849
commit
bf02d37a33
|
|
@ -9,18 +9,21 @@ use App\Domains\Sale\Resources\AdminApp\SaleDetailResource;
|
||||||
use App\Domains\Sale\Resources\AdminApp\SaleModificationResource;
|
use App\Domains\Sale\Resources\AdminApp\SaleModificationResource;
|
||||||
use App\Domains\Sale\Resources\AdminApp\SaleResource;
|
use App\Domains\Sale\Resources\AdminApp\SaleResource;
|
||||||
use App\Domains\Sale\Resources\AdminApp\SaleTicketResource;
|
use App\Domains\Sale\Resources\AdminApp\SaleTicketResource;
|
||||||
|
use App\Domains\Sale\Services\AdminAppSaleExcelService;
|
||||||
use App\Domains\Sale\Services\AdminAppSalePdfService;
|
use App\Domains\Sale\Services\AdminAppSalePdfService;
|
||||||
use App\Domains\Sale\Services\AdminAppSaleService;
|
use App\Domains\Sale\Services\AdminAppSaleService;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
|
||||||
class SaleController extends Controller
|
class SaleController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected AdminAppSaleService $saleService,
|
protected AdminAppSaleService $saleService,
|
||||||
protected AdminAppSalePdfService $salePdfService,
|
protected AdminAppSalePdfService $salePdfService,
|
||||||
|
protected AdminAppSaleExcelService $saleExcelService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(AdminAppSaleIndexRequest $request): AnonymousResourceCollection
|
public function index(AdminAppSaleIndexRequest $request): AnonymousResourceCollection
|
||||||
|
|
@ -94,4 +97,27 @@ class SaleController extends Controller
|
||||||
$request->validated('timezone'),
|
$request->validated('timezone'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function downloadExcel(AdminAppSalePdfRequest $request): StreamedResponse
|
||||||
|
{
|
||||||
|
$tenant = $request->user()->tenant()->firstOrFail();
|
||||||
|
|
||||||
|
return $this->saleExcelService->downloadSales(
|
||||||
|
$tenant,
|
||||||
|
$this->saleService->salesForExport($tenant, $request->validated()),
|
||||||
|
$request->validated('timezone'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function downloadModificationsExcel(
|
||||||
|
AdminAppSaleModificationPdfRequest $request,
|
||||||
|
): StreamedResponse {
|
||||||
|
$tenant = $request->user()->tenant()->firstOrFail();
|
||||||
|
|
||||||
|
return $this->saleExcelService->downloadModifications(
|
||||||
|
$tenant,
|
||||||
|
$this->saleService->modificationsForExport($tenant),
|
||||||
|
$request->validated('timezone'),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Sale\Services;
|
||||||
|
|
||||||
|
use App\Domains\Logging\Models\ValueChange;
|
||||||
|
use App\Domains\Purchase\Models\Purchase;
|
||||||
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||||
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
|
||||||
|
class AdminAppSaleExcelService
|
||||||
|
{
|
||||||
|
/** @param Collection<int, Purchase> $sales */
|
||||||
|
public function downloadSales(Tenant $tenant, Collection $sales, string $timeZone): StreamedResponse
|
||||||
|
{
|
||||||
|
$generatedAt = now();
|
||||||
|
$spreadsheet = $this->spreadsheet($tenant, 'Historial de ventas');
|
||||||
|
$sheet = $spreadsheet->getActiveSheet();
|
||||||
|
$sheet->setTitle('Ventas');
|
||||||
|
$sheet->fromArray([
|
||||||
|
'ID',
|
||||||
|
'Fecha',
|
||||||
|
'Cliente',
|
||||||
|
'Cantidad',
|
||||||
|
'Estado',
|
||||||
|
'Importe',
|
||||||
|
'Tickets',
|
||||||
|
], null, 'A1');
|
||||||
|
|
||||||
|
foreach ($sales->values() as $index => $sale) {
|
||||||
|
$row = $index + 2;
|
||||||
|
$sheet->setCellValueExplicit("A{$row}", '#'.$sale->id, DataType::TYPE_STRING);
|
||||||
|
if ($sale->created_at) {
|
||||||
|
$sheet->setCellValue(
|
||||||
|
"B{$row}",
|
||||||
|
Date::dateTimeToExcel($sale->created_at->copy()->timezone($timeZone)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$sheet->setCellValueExplicit(
|
||||||
|
"C{$row}",
|
||||||
|
$sale->nombre_apellido ?: 'Sin nombre',
|
||||||
|
DataType::TYPE_STRING,
|
||||||
|
);
|
||||||
|
$sheet->setCellValue("D{$row}", (int) ($sale->quantity ?? 0));
|
||||||
|
$sheet->setCellValue("E{$row}", $this->saleStatus($sale->status));
|
||||||
|
$sheet->setCellValue("F{$row}", (float) $sale->total);
|
||||||
|
$sheet->setCellValue("G{$row}", (int) ($sale->tickets_count ?? 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastRow = max(2, $sales->count() + 1);
|
||||||
|
$sheet->getStyle("B2:B{$lastRow}")->getNumberFormat()->setFormatCode('dd/mm/yyyy hh:mm');
|
||||||
|
$sheet->getStyle("F2:F{$lastRow}")->getNumberFormat()->setFormatCode('$ #,##0.00');
|
||||||
|
$this->formatSheet($spreadsheet, 'A1:G1', "A1:G{$lastRow}", [
|
||||||
|
'A' => 13,
|
||||||
|
'B' => 20,
|
||||||
|
'C' => 32,
|
||||||
|
'D' => 12,
|
||||||
|
'E' => 22,
|
||||||
|
'F' => 16,
|
||||||
|
'G' => 12,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->download(
|
||||||
|
$spreadsheet,
|
||||||
|
'ventas_'.$tenant->codigo.'_'
|
||||||
|
.$generatedAt->copy()->timezone($timeZone)->format('Ymd_His').'.xlsx',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param Collection<int, ValueChange> $modifications */
|
||||||
|
public function downloadModifications(
|
||||||
|
Tenant $tenant,
|
||||||
|
Collection $modifications,
|
||||||
|
string $timeZone,
|
||||||
|
): StreamedResponse {
|
||||||
|
$generatedAt = now();
|
||||||
|
$spreadsheet = $this->spreadsheet($tenant, 'Historial de modificaciones de ventas');
|
||||||
|
$sheet = $spreadsheet->getActiveSheet();
|
||||||
|
$sheet->setTitle('Modificaciones');
|
||||||
|
$sheet->fromArray([
|
||||||
|
'Fecha',
|
||||||
|
'Hora',
|
||||||
|
'Venta',
|
||||||
|
'Cliente',
|
||||||
|
'Campo',
|
||||||
|
'Valor anterior',
|
||||||
|
'Valor nuevo',
|
||||||
|
'Modificado por',
|
||||||
|
], null, 'A1');
|
||||||
|
|
||||||
|
foreach ($modifications->values() as $index => $modification) {
|
||||||
|
$row = $index + 2;
|
||||||
|
$changedAt = $modification->changed_at->copy()->timezone($timeZone);
|
||||||
|
$sale = $modification->trackable;
|
||||||
|
$sheet->setCellValue("A{$row}", Date::dateTimeToExcel($changedAt));
|
||||||
|
$sheet->setCellValue("B{$row}", Date::dateTimeToExcel($changedAt));
|
||||||
|
$sheet->setCellValueExplicit(
|
||||||
|
"C{$row}",
|
||||||
|
'#'.$modification->trackable_id,
|
||||||
|
DataType::TYPE_STRING,
|
||||||
|
);
|
||||||
|
$sheet->setCellValueExplicit(
|
||||||
|
"D{$row}",
|
||||||
|
$sale?->nombre_apellido ?: 'Sin nombre',
|
||||||
|
DataType::TYPE_STRING,
|
||||||
|
);
|
||||||
|
$sheet->setCellValueExplicit(
|
||||||
|
"E{$row}",
|
||||||
|
$modification->attribute,
|
||||||
|
DataType::TYPE_STRING,
|
||||||
|
);
|
||||||
|
$sheet->setCellValueExplicit(
|
||||||
|
"F{$row}",
|
||||||
|
$modification->old_value ?? '-',
|
||||||
|
DataType::TYPE_STRING,
|
||||||
|
);
|
||||||
|
$sheet->setCellValueExplicit(
|
||||||
|
"G{$row}",
|
||||||
|
$modification->new_value ?? '-',
|
||||||
|
DataType::TYPE_STRING,
|
||||||
|
);
|
||||||
|
$sheet->setCellValueExplicit(
|
||||||
|
"H{$row}",
|
||||||
|
$modification->user?->nombre_apellido ?? 'Sistema',
|
||||||
|
DataType::TYPE_STRING,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastRow = max(2, $modifications->count() + 1);
|
||||||
|
$sheet->getStyle("A2:A{$lastRow}")->getNumberFormat()->setFormatCode('dd/mm/yyyy');
|
||||||
|
$sheet->getStyle("B2:B{$lastRow}")->getNumberFormat()->setFormatCode('hh:mm:ss');
|
||||||
|
$this->formatSheet($spreadsheet, 'A1:H1', "A1:H{$lastRow}", [
|
||||||
|
'A' => 14,
|
||||||
|
'B' => 12,
|
||||||
|
'C' => 13,
|
||||||
|
'D' => 32,
|
||||||
|
'E' => 20,
|
||||||
|
'F' => 24,
|
||||||
|
'G' => 24,
|
||||||
|
'H' => 28,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->download(
|
||||||
|
$spreadsheet,
|
||||||
|
'historial_modificaciones_'.$tenant->codigo.'_'
|
||||||
|
.$generatedAt->copy()->timezone($timeZone)->format('Ymd_His').'.xlsx',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function spreadsheet(Tenant $tenant, string $title): Spreadsheet
|
||||||
|
{
|
||||||
|
$spreadsheet = new Spreadsheet;
|
||||||
|
$spreadsheet->getProperties()
|
||||||
|
->setCreator('Shopit')
|
||||||
|
->setTitle($title)
|
||||||
|
->setSubject($tenant->nombre);
|
||||||
|
|
||||||
|
return $spreadsheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, int> $widths */
|
||||||
|
private function formatSheet(
|
||||||
|
Spreadsheet $spreadsheet,
|
||||||
|
string $headerRange,
|
||||||
|
string $filterRange,
|
||||||
|
array $widths,
|
||||||
|
): void {
|
||||||
|
$sheet = $spreadsheet->getActiveSheet();
|
||||||
|
$sheet->getStyle($headerRange)->applyFromArray([
|
||||||
|
'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']],
|
||||||
|
'fill' => [
|
||||||
|
'fillType' => Fill::FILL_SOLID,
|
||||||
|
'startColor' => ['rgb' => '26382E'],
|
||||||
|
],
|
||||||
|
'alignment' => ['vertical' => Alignment::VERTICAL_CENTER],
|
||||||
|
]);
|
||||||
|
$sheet->getRowDimension(1)->setRowHeight(24);
|
||||||
|
$sheet->freezePane('A2');
|
||||||
|
$sheet->setAutoFilter($filterRange);
|
||||||
|
|
||||||
|
foreach ($widths as $column => $width) {
|
||||||
|
$sheet->getColumnDimension($column)->setWidth($width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function download(Spreadsheet $spreadsheet, string $filename): StreamedResponse
|
||||||
|
{
|
||||||
|
return response()->streamDownload(function () use ($spreadsheet): void {
|
||||||
|
(new Xlsx($spreadsheet))->save('php://output');
|
||||||
|
$spreadsheet->disconnectWorksheets();
|
||||||
|
}, $filename, [
|
||||||
|
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function saleStatus(string $status): string
|
||||||
|
{
|
||||||
|
return match ($status) {
|
||||||
|
Purchase::STATUS_PAID => 'Confirmado',
|
||||||
|
Purchase::STATUS_CREATED => 'Por completar datos',
|
||||||
|
Purchase::STATUS_PENDING_PAYMENT, Purchase::STATUS_IN_REVIEW => 'Esperando pago',
|
||||||
|
default => 'Anulado',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ Provee consultas administrativas y exportaciones de ventas confirmadas, además
|
||||||
|
|
||||||
- `AdminAppSaleService`: pagina ventas, calcula totales y obtiene colecciones para exportación; también consulta modificaciones.
|
- `AdminAppSaleService`: pagina ventas, calcula totales y obtiene colecciones para exportación; también consulta modificaciones.
|
||||||
- `AdminAppSalePdfService`: genera descargas PDF de ventas y de cambios.
|
- `AdminAppSalePdfService`: genera descargas PDF de ventas y de cambios.
|
||||||
|
- `AdminAppSaleExcelService`: genera descargas Excel de ventas y de cambios.
|
||||||
- `AdminAppSaleIndexRequest`: valida filtros del listado y la exportación.
|
- `AdminAppSaleIndexRequest`: valida filtros del listado y la exportación.
|
||||||
- `SaleResource` y `SaleModificationResource`: representan ventas e historial para AdminApp.
|
- `SaleResource` y `SaleModificationResource`: representan ventas e historial para AdminApp.
|
||||||
- `SaleController`: entrada HTTP del panel.
|
- `SaleController`: entrada HTTP del panel.
|
||||||
|
|
@ -16,8 +17,8 @@ Provee consultas administrativas y exportaciones de ventas confirmadas, además
|
||||||
|
|
||||||
Bajo `/v1/adminapp/tenant`, protegidos por `auth:sanctum` y `adminapp.tenant`:
|
Bajo `/v1/adminapp/tenant`, protegidos por `auth:sanctum` y `adminapp.tenant`:
|
||||||
|
|
||||||
- `GET /sales` y `GET /sales/pdf`.
|
- `GET /sales`, `GET /sales/pdf` y `GET /sales/excel`.
|
||||||
- `GET /sales/modifications` y `GET /sales/modifications/pdf`.
|
- `GET /sales/modifications`, `GET /sales/modifications/pdf` y `GET /sales/modifications/excel`.
|
||||||
|
|
||||||
## Dependencias
|
## Dependencias
|
||||||
|
|
||||||
|
|
@ -25,4 +26,4 @@ Consume compras de `Purchase`, datos del tenant y entradas de `Logging`. No es d
|
||||||
|
|
||||||
## Consideraciones
|
## Consideraciones
|
||||||
|
|
||||||
La consulta paginada y la colección de exportación deben aplicar los mismos filtros para evitar diferencias entre pantalla y PDF.
|
La consulta paginada y la colección de exportación deben aplicar los mismos filtros para evitar diferencias entre pantalla, PDF y Excel.
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,10 @@ Route::prefix('v1/adminapp/tenant')
|
||||||
->group(function (): void {
|
->group(function (): void {
|
||||||
Route::get('sales', [SaleController::class, 'index']);
|
Route::get('sales', [SaleController::class, 'index']);
|
||||||
Route::get('sales/pdf', [SaleController::class, 'downloadPdf']);
|
Route::get('sales/pdf', [SaleController::class, 'downloadPdf']);
|
||||||
|
Route::get('sales/excel', [SaleController::class, 'downloadExcel']);
|
||||||
Route::get('sales/modifications', [SaleController::class, 'modifications']);
|
Route::get('sales/modifications', [SaleController::class, 'modifications']);
|
||||||
Route::get('sales/modifications/pdf', [SaleController::class, 'downloadModificationsPdf']);
|
Route::get('sales/modifications/pdf', [SaleController::class, 'downloadModificationsPdf']);
|
||||||
|
Route::get('sales/modifications/excel', [SaleController::class, 'downloadModificationsExcel']);
|
||||||
Route::post('sales/{sale}/confirm', [SaleController::class, 'confirm'])->whereNumber('sale');
|
Route::post('sales/{sale}/confirm', [SaleController::class, 'confirm'])->whereNumber('sale');
|
||||||
Route::post('sales/{sale}/cancel', [SaleController::class, 'cancel'])->whereNumber('sale');
|
Route::post('sales/{sale}/cancel', [SaleController::class, 'cancel'])->whereNumber('sale');
|
||||||
Route::get('sales/{sale}/tickets', [SaleController::class, 'tickets'])->whereNumber('sale');
|
Route::get('sales/{sale}/tickets', [SaleController::class, 'tickets'])->whereNumber('sale');
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,16 @@
|
||||||
"keywords": ["laravel", "framework"],
|
"keywords": ["laravel", "framework"],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"ext-gd": "*",
|
|
||||||
"php": "^8.3",
|
"php": "^8.3",
|
||||||
|
"ext-gd": "*",
|
||||||
"barryvdh/laravel-dompdf": "^3.1",
|
"barryvdh/laravel-dompdf": "^3.1",
|
||||||
"endroid/qr-code": "^6.1",
|
"endroid/qr-code": "^6.1",
|
||||||
"laravel/framework": "^13.8",
|
"laravel/framework": "^13.8",
|
||||||
"laravel/sanctum": "^4.3",
|
"laravel/sanctum": "^4.3",
|
||||||
"laravel/socialite": "^5.29",
|
"laravel/socialite": "^5.29",
|
||||||
"laravel/tinker": "^3.0",
|
"laravel/tinker": "^3.0",
|
||||||
"league/flysystem-aws-s3-v3": "^3.0"
|
"league/flysystem-aws-s3-v3": "^3.0",
|
||||||
|
"phpoffice/phpspreadsheet": "^5.9"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "ce185c60c617846be30ae694f0cf6e9c",
|
"content-hash": "a593ab47d99b233f75851dbb7ea50479",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "aws/aws-crt-php",
|
"name": "aws/aws-crt-php",
|
||||||
|
|
@ -417,6 +417,82 @@
|
||||||
],
|
],
|
||||||
"time": "2024-02-09T16:56:22+00:00"
|
"time": "2024-02-09T16:56:22+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "composer/pcre",
|
||||||
|
"version": "3.4.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/composer/pcre.git",
|
||||||
|
"reference": "d5a341b3fb61f3001970940afb1d332968a183ed"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed",
|
||||||
|
"reference": "d5a341b3fb61f3001970940afb1d332968a183ed",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.4 || ^8.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"phpstan/phpstan": "<2.2.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "^2",
|
||||||
|
"phpstan/phpstan-deprecation-rules": "^2",
|
||||||
|
"phpstan/phpstan-strict-rules": "^2",
|
||||||
|
"phpunit/phpunit": "^9"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"phpstan": {
|
||||||
|
"includes": [
|
||||||
|
"extension.neon"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "3.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Composer\\Pcre\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jordi Boggiano",
|
||||||
|
"email": "j.boggiano@seld.be",
|
||||||
|
"homepage": "http://seld.be"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
|
||||||
|
"keywords": [
|
||||||
|
"PCRE",
|
||||||
|
"preg",
|
||||||
|
"regex",
|
||||||
|
"regular expression"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/composer/pcre/issues",
|
||||||
|
"source": "https://github.com/composer/pcre/tree/3.4.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://packagist.com",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/composer",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-06-07T11:47:49+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "dasprid/enum",
|
"name": "dasprid/enum",
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
|
|
@ -2924,6 +3000,191 @@
|
||||||
],
|
],
|
||||||
"time": "2026-03-08T20:05:35+00:00"
|
"time": "2026-03-08T20:05:35+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "maennchen/zipstream-php",
|
||||||
|
"version": "3.2.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/maennchen/ZipStream-PHP.git",
|
||||||
|
"reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e",
|
||||||
|
"reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"ext-zlib": "*",
|
||||||
|
"php-64bit": "^8.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"brianium/paratest": "^7.7",
|
||||||
|
"ext-zip": "*",
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.86",
|
||||||
|
"guzzlehttp/guzzle": "^7.5",
|
||||||
|
"mikey179/vfsstream": "^1.6",
|
||||||
|
"php-coveralls/php-coveralls": "^2.5",
|
||||||
|
"phpunit/phpunit": "^12.0",
|
||||||
|
"vimeo/psalm": "^6.0"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"guzzlehttp/psr7": "^2.4",
|
||||||
|
"psr/http-message": "^2.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"ZipStream\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Paul Duncan",
|
||||||
|
"email": "pabs@pablotron.org"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jonatan Männchen",
|
||||||
|
"email": "jonatan@maennchen.ch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jesse Donat",
|
||||||
|
"email": "donatj@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "András Kolesár",
|
||||||
|
"email": "kolesar@kolesar.hu"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
|
||||||
|
"keywords": [
|
||||||
|
"stream",
|
||||||
|
"zip"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
|
||||||
|
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.2"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/maennchen",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-04-11T18:38:28+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "markbaker/complex",
|
||||||
|
"version": "3.0.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/MarkBaker/PHPComplex.git",
|
||||||
|
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
|
||||||
|
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.2 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
|
||||||
|
"phpcompatibility/php-compatibility": "^9.3",
|
||||||
|
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.7"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Complex\\": "classes/src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Mark Baker",
|
||||||
|
"email": "mark@lange.demon.co.uk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP Class for working with complex numbers",
|
||||||
|
"homepage": "https://github.com/MarkBaker/PHPComplex",
|
||||||
|
"keywords": [
|
||||||
|
"complex",
|
||||||
|
"mathematics"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/MarkBaker/PHPComplex/issues",
|
||||||
|
"source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
|
||||||
|
},
|
||||||
|
"time": "2022-12-06T16:21:08+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "markbaker/matrix",
|
||||||
|
"version": "3.0.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/MarkBaker/PHPMatrix.git",
|
||||||
|
"reference": "728434227fe21be27ff6d86621a1b13107a2562c"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
|
||||||
|
"reference": "728434227fe21be27ff6d86621a1b13107a2562c",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
|
||||||
|
"phpcompatibility/php-compatibility": "^9.3",
|
||||||
|
"phpdocumentor/phpdocumentor": "2.*",
|
||||||
|
"phploc/phploc": "^4.0",
|
||||||
|
"phpmd/phpmd": "2.*",
|
||||||
|
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
|
||||||
|
"sebastian/phpcpd": "^4.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.7"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Matrix\\": "classes/src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Mark Baker",
|
||||||
|
"email": "mark@demon-angel.eu"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP Class for working with matrices",
|
||||||
|
"homepage": "https://github.com/MarkBaker/PHPMatrix",
|
||||||
|
"keywords": [
|
||||||
|
"mathematics",
|
||||||
|
"matrix",
|
||||||
|
"vector"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/MarkBaker/PHPMatrix/issues",
|
||||||
|
"source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
|
||||||
|
},
|
||||||
|
"time": "2022-12-02T22:17:43+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "masterminds/html5",
|
"name": "masterminds/html5",
|
||||||
"version": "2.10.1",
|
"version": "2.10.1",
|
||||||
|
|
@ -3686,6 +3947,115 @@
|
||||||
},
|
},
|
||||||
"time": "2020-10-15T08:29:30+00:00"
|
"time": "2020-10-15T08:29:30+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "phpoffice/phpspreadsheet",
|
||||||
|
"version": "5.9.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
|
||||||
|
"reference": "05e99ebf61238a70227b4d9cc02d0030d34f6339"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/05e99ebf61238a70227b4d9cc02d0030d34f6339",
|
||||||
|
"reference": "05e99ebf61238a70227b4d9cc02d0030d34f6339",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"composer/pcre": "^1||^2||^3",
|
||||||
|
"ext-ctype": "*",
|
||||||
|
"ext-dom": "*",
|
||||||
|
"ext-fileinfo": "*",
|
||||||
|
"ext-filter": "*",
|
||||||
|
"ext-gd": "*",
|
||||||
|
"ext-iconv": "*",
|
||||||
|
"ext-libxml": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"ext-simplexml": "*",
|
||||||
|
"ext-xml": "*",
|
||||||
|
"ext-xmlreader": "*",
|
||||||
|
"ext-xmlwriter": "*",
|
||||||
|
"ext-zip": "*",
|
||||||
|
"ext-zlib": "*",
|
||||||
|
"maennchen/zipstream-php": "^2.1 || ^3.0",
|
||||||
|
"markbaker/complex": "^3.0",
|
||||||
|
"markbaker/matrix": "^3.0",
|
||||||
|
"php": "^8.2",
|
||||||
|
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
|
||||||
|
"dompdf/dompdf": "^2.0 || ^3.0",
|
||||||
|
"ext-intl": "*",
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.2",
|
||||||
|
"mitoteam/jpgraph": "^10.5",
|
||||||
|
"mpdf/mpdf": "^8.1.1",
|
||||||
|
"phpcompatibility/php-compatibility": "^9.3",
|
||||||
|
"phpstan/phpstan": "^1.1 || ^2.0",
|
||||||
|
"phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0",
|
||||||
|
"phpstan/phpstan-phpunit": "^1.0 || ^2.0",
|
||||||
|
"phpunit/phpunit": "^10.5 || ^11.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.7",
|
||||||
|
"tecnickcom/tcpdf": "^6.5"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
|
||||||
|
"ext-intl": "PHP Internationalization Functions, required for NumberFormat Wizard and StringHelper::setLocale()",
|
||||||
|
"mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
|
||||||
|
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
|
||||||
|
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Maarten Balliauw",
|
||||||
|
"homepage": "https://blog.maartenballiauw.be"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Mark Baker",
|
||||||
|
"homepage": "https://markbakeruk.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Franck Lefevre",
|
||||||
|
"homepage": "https://rootslabs.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Erik Tilt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Adrien Crivelli"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Owen Leibman"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
|
||||||
|
"homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
|
||||||
|
"keywords": [
|
||||||
|
"OpenXML",
|
||||||
|
"excel",
|
||||||
|
"gnumeric",
|
||||||
|
"ods",
|
||||||
|
"php",
|
||||||
|
"spreadsheet",
|
||||||
|
"xls",
|
||||||
|
"xlsx"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
|
||||||
|
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.9.0"
|
||||||
|
},
|
||||||
|
"time": "2026-07-12T19:17:39+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "phpoption/phpoption",
|
"name": "phpoption/phpoption",
|
||||||
"version": "1.9.5",
|
"version": "1.9.5",
|
||||||
|
|
@ -9838,8 +10208,8 @@
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": {
|
"platform": {
|
||||||
"ext-gd": "*",
|
"php": "^8.3",
|
||||||
"php": "^8.3"
|
"ext-gd": "*"
|
||||||
},
|
},
|
||||||
"platform-dev": {},
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.9.0"
|
"plugin-api-version": "2.9.0"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Sale;
|
||||||
|
|
||||||
|
use App\Domains\Auth\Models\User;
|
||||||
|
use App\Domains\Logging\Models\ValueChange;
|
||||||
|
use App\Domains\Purchase\Models\Purchase;
|
||||||
|
use App\Domains\Sale\Services\AdminAppSaleExcelService;
|
||||||
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||||
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class AdminAppSaleExcelServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
Carbon::setTestNow(Carbon::parse('2026-08-24 17:53:00', 'UTC'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
Carbon::setTestNow();
|
||||||
|
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_downloads_filtered_sales_as_an_excel_file(): void
|
||||||
|
{
|
||||||
|
$response = app(AdminAppSaleExcelService::class)->downloadSales(
|
||||||
|
$this->tenant(),
|
||||||
|
collect([$this->sale()]),
|
||||||
|
'America/La_Paz',
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertExcelResponse(
|
||||||
|
$response,
|
||||||
|
'ventas_acme_20260824_135300.xlsx',
|
||||||
|
function (string $path): void {
|
||||||
|
$sheet = IOFactory::load($path)->getActiveSheet();
|
||||||
|
|
||||||
|
$this->assertSame('Ventas', $sheet->getTitle());
|
||||||
|
$this->assertSame('Cliente Test', $sheet->getCell('C2')->getValue());
|
||||||
|
$this->assertSame('Confirmado', $sheet->getCell('E2')->getValue());
|
||||||
|
$this->assertSame(25000.0, $sheet->getCell('F2')->getValue());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_downloads_the_modification_history_as_an_excel_file(): void
|
||||||
|
{
|
||||||
|
$sale = $this->sale();
|
||||||
|
$admin = (new User)->forceFill([
|
||||||
|
'id' => 10,
|
||||||
|
'nombre_apellido' => 'Admin Test',
|
||||||
|
'email' => 'admin@example.test',
|
||||||
|
]);
|
||||||
|
$modification = (new ValueChange)->forceFill([
|
||||||
|
'id' => 1,
|
||||||
|
'trackable_id' => $sale->id,
|
||||||
|
'attribute' => 'status',
|
||||||
|
'old_value' => Purchase::STATUS_PENDING_PAYMENT,
|
||||||
|
'new_value' => Purchase::STATUS_PAID,
|
||||||
|
'changed_at' => now(),
|
||||||
|
'actor_type' => 'user',
|
||||||
|
]);
|
||||||
|
$modification->setRelation('trackable', $sale);
|
||||||
|
$modification->setRelation('user', $admin);
|
||||||
|
|
||||||
|
$response = app(AdminAppSaleExcelService::class)->downloadModifications(
|
||||||
|
$this->tenant(),
|
||||||
|
collect([$modification]),
|
||||||
|
'America/La_Paz',
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertExcelResponse(
|
||||||
|
$response,
|
||||||
|
'historial_modificaciones_acme_20260824_135300.xlsx',
|
||||||
|
function (string $path): void {
|
||||||
|
$sheet = IOFactory::load($path)->getActiveSheet();
|
||||||
|
|
||||||
|
$this->assertSame('Modificaciones', $sheet->getTitle());
|
||||||
|
$this->assertSame('#15', $sheet->getCell('C2')->getValue());
|
||||||
|
$this->assertSame('pending_payment', $sheet->getCell('F2')->getValue());
|
||||||
|
$this->assertSame('paid', $sheet->getCell('G2')->getValue());
|
||||||
|
$this->assertSame('Admin Test', $sheet->getCell('H2')->getValue());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param callable(string): void $assertSpreadsheet */
|
||||||
|
private function assertExcelResponse(
|
||||||
|
StreamedResponse $response,
|
||||||
|
string $filename,
|
||||||
|
callable $assertSpreadsheet,
|
||||||
|
): void {
|
||||||
|
$this->assertSame(
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
$response->headers->get('content-type'),
|
||||||
|
);
|
||||||
|
$this->assertStringContainsString(
|
||||||
|
"attachment; filename={$filename}",
|
||||||
|
(string) $response->headers->get('content-disposition'),
|
||||||
|
);
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
($response->getCallback())();
|
||||||
|
$contents = ob_get_clean();
|
||||||
|
$this->assertIsString($contents);
|
||||||
|
$this->assertStringStartsWith('PK', $contents);
|
||||||
|
|
||||||
|
$path = tempnam(sys_get_temp_dir(), 'shopit_excel_');
|
||||||
|
$this->assertNotFalse($path);
|
||||||
|
|
||||||
|
try {
|
||||||
|
file_put_contents($path, $contents);
|
||||||
|
$assertSpreadsheet($path);
|
||||||
|
} finally {
|
||||||
|
@unlink($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function tenant(): Tenant
|
||||||
|
{
|
||||||
|
return (new Tenant)->forceFill([
|
||||||
|
'codigo' => 'acme',
|
||||||
|
'nombre' => 'Acme Eventos',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sale(): Purchase
|
||||||
|
{
|
||||||
|
return (new Purchase)->forceFill([
|
||||||
|
'id' => 15,
|
||||||
|
'created_at' => now(),
|
||||||
|
'nombre_apellido' => 'Cliente Test',
|
||||||
|
'quantity' => 2,
|
||||||
|
'status' => Purchase::STATUS_PAID,
|
||||||
|
'total' => '25000.00',
|
||||||
|
'tickets_count' => 2,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue