Skip to content
Open
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
121 changes: 121 additions & 0 deletions bin/lms-ksef.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env php
<?php

/*
* LMS version 1.11-git
*
* (C) Copyright 2001-2026 LMS Developers
*
* Please, see the doc/AUTHORS for more information about authors!
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License Version 2 as
* published by the Free Software Foundation.
*/

use Lms\KSeF\KSeF;
use Lms\KSeF\KSeFConfig;
use Lms\KSeF\KSeFRepository;
use Lms\KSeF\KSeFSubmissionService;
use Lms\KSeF\N1ebieskiKSeFGateway;

$script_parameters = [
'send' => null,
'sync' => null,
'test' => 't',
'section:' => 's:',
'division:' => null,
'customerid:' => null,
];

$script_help = <<<EOF
--send send eligible sales invoices to KSeF;
--sync synchronize pending KSeF invoice statuses and UPO files;
-t, --test dry run; print candidate counts only;
-s, --section=<section-name> configuration section name, default: ksef;
--division=<shortname> limit sending candidates to selected division;
--customerid=<id> limit sending candidates to selected customer;
EOF;

require_once('script-options.php');

$send = isset($options['send']);
$sync = isset($options['sync']);

if (!$send && !$sync) {
die('Use --send and/or --sync.' . PHP_EOL);
}

$SYSLOG = SYSLOG::getInstance();
$AUTH = null;
$LMS = new LMS($DB, $AUTH, $SYSLOG);

$plugin_manager = LMSPluginManager::getInstance();
$LMS->setPluginManager($plugin_manager);

$section = isset($options['section']) && preg_match('/^[a-z0-9-_]+$/i', $options['section'])
? $options['section']
: 'ksef';
$repository = new KSeFRepository($DB);

$divisionId = null;
if (!empty($options['division'])) {
$divisionId = $LMS->getDivisionIdByShortName($options['division']);
if (empty($divisionId)) {
die('Unknown division: ' . $options['division'] . PHP_EOL);
}
ConfigHelper::setFilter($divisionId);
}
$customerId = isset($options['customerid']) ? intval($options['customerid']) : null;
$configProvider = function (?int $selectedDivisionId = null) use ($section, $options) {
if ($selectedDivisionId !== null) {
ConfigHelper::setFilter($selectedDivisionId);
}

return KSeFConfig::fromConfigHelper($section, !isset($options['test']));
};
$config = KSeFConfig::fromConfigHelper($section, false);

if (isset($options['test'])) {
if ($send) {
$eligible = $repository->getEligibleInvoices($config->getMaxDocuments(), $divisionId, $customerId);
echo 'KSeF send candidates: ' . count($eligible) . PHP_EOL;
}
if ($sync) {
$pending = $repository->getPendingDocuments($config->getMaxDocuments(), $divisionId, $customerId);
echo 'KSeF pending documents: ' . count($pending) . PHP_EOL;
}
exit(0);
}

$gateway = new N1ebieskiKSeFGateway();
$ksef = new KSeF($DB, $LMS);
$service = new KSeFSubmissionService(
$repository,
$gateway,
function (array $invoice) use ($LMS, $ksef) {
$invoiceContent = $LMS->GetInvoiceContent((int) $invoice['id']);
if (empty($invoiceContent)) {
return ['error' => 'Invoice not found.'];
}

return $ksef->getInvoiceXml($invoiceContent);
},
$configProvider
);

if ($send) {
$result = $service->send($config, $divisionId, $customerId);
echo 'KSeF submitted: ' . $result['submitted'] . ', skipped: ' . $result['skipped'] . PHP_EOL;
foreach ($result['errors'] as $error) {
echo 'Document ' . $error['docid'] . ': ' . $error['error'] . PHP_EOL;
}
}

if ($sync) {
$result = $service->sync($config, $divisionId, $customerId);
echo 'KSeF status updates: ' . $result['updated'] . PHP_EOL;
foreach ($result['errors'] as $error) {
echo 'KSeF document ' . $error['id'] . ': ' . $error['error'] . PHP_EOL;
}
}
4 changes: 4 additions & 0 deletions js/locale/pl_PL.js
Original file line number Diff line number Diff line change
Expand Up @@ -6361,6 +6361,10 @@ $_LANG['<!ksef>corrective PEF invoice'] = 'korekta faktury PEF';
$_LANG['<!ksef>RR invoice'] = 'faktura RR';
$_LANG['<!ksef>corrective RR invoice'] = 'korekta faktury RR';

$_LANG['Send invoice $a to KSeF?'] = 'Wysłać fakturę $a do KSeF?';
$_LANG['Send selected invoices to KSeF?'] = 'Wysłać zaznaczone faktury do KSeF?';
$_LANG['Sending invoices to KSeF. Please wait.'] = 'Wysyłanie faktur do KSeF. Proszę czekać.';

$_LANG['selection from filter'] = 'wybór z filtra';
$_LANG['all recipient of this message'] = 'wszyscy odbiorcy tej wiadomości';
$_LANG['recipients who haven\'t received this message'] = 'odbiorcy, którzy nie otrzymali tej wiadomości';
Expand Down
100 changes: 82 additions & 18 deletions lib/KSeF/KSeF.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,35 @@ class KSeF

private $smartNumberFormatter;

public static function formatStatusDetails($statusDetails)
{
if (!is_string($statusDetails) || $statusDetails === '') {
return $statusDetails;
}

$decoded = json_decode($statusDetails, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return $statusDetails;
}

if (is_array($decoded)) {
return implode(', ', array_map([self::class, 'formatStatusDetailValue'], $decoded));
}

return is_scalar($decoded) ? strval($decoded) : $statusDetails;
}

private static function formatStatusDetailValue($value)
{
if (is_scalar($value) || $value === null) {
return strval($value);
}

$encoded = json_encode($value, JSON_UNESCAPED_UNICODE);

return $encoded === false ? '' : $encoded;
}

public function __construct($db, $lms)
{
$this->db = $db;
Expand Down Expand Up @@ -384,6 +413,8 @@ private function smartFormatNumber($number)

public function getInvoiceXml(array $invoice)
{
$invoiceType = $invoice['type'] ?? $invoice['doctype'] ?? null;

if (!isset($this->divisions[$invoice['divisionid']])) {
$this->divisions[$invoice['divisionid']] = $this->lms->GetDivision($invoice['divisionid']);
}
Expand Down Expand Up @@ -604,7 +635,7 @@ public function getInvoiceXml(array $invoice)
$taxFree = false;
$diffTotal = 0;

if ($invoice['type'] == DOC_CNOTE) {
if ($invoiceType == DOC_CNOTE) {
if (isset($invoice['taxest']['23.00']) || isset($invoice['invoice']['taxest']['23.00'])) {
$taxRate = '23.00';
} elseif (isset($invoice['taxest']['22.00']) || isset($invoice['invoice']['taxest']['22.00'])) {
Expand Down Expand Up @@ -650,7 +681,7 @@ public function getInvoiceXml(array $invoice)
}
}

if ($invoice['type'] == DOC_CNOTE) {
if ($invoiceType == DOC_CNOTE) {
if (isset($invoice['taxest']['8.00']) || isset($invoice['invoice']['taxest']['8.00'])) {
$taxRate = '8.00';
} elseif (isset($invoice['taxest']['7.00']) || isset($invoice['invoice']['taxest']['7.00'])) {
Expand Down Expand Up @@ -696,7 +727,7 @@ public function getInvoiceXml(array $invoice)
}
}

if ($invoice['type'] == DOC_CNOTE) {
if ($invoiceType == DOC_CNOTE) {
if (isset($invoice['taxest']['5.00']) || isset($invoice['invoice']['taxest']['5.00'])) {
$taxRate = '5.00';
} else {
Expand Down Expand Up @@ -739,7 +770,7 @@ public function getInvoiceXml(array $invoice)
}

if (!$foreign) {
if ($invoice['type'] == DOC_CNOTE) {
if ($invoiceType == DOC_CNOTE) {
if (isset($invoice['taxest']['0.00']) || isset($invoice['invoice']['taxest']['0.00'])) {
$taxRate = '0.00';
if (isset($invoice['taxest'][$taxRate])) {
Expand All @@ -759,7 +790,7 @@ public function getInvoiceXml(array $invoice)
}
}

if ($invoice['type'] == DOC_CNOTE) {
if ($invoiceType == DOC_CNOTE) {
if (isset($invoice['taxest']['-1']) || isset($invoice['invoice']['taxest']['-1'])) {
$taxRate = '-1';
if (isset($invoice['taxest'][$taxRate])) {
Expand Down Expand Up @@ -793,7 +824,7 @@ public function getInvoiceXml(array $invoice)
}
}

if ($invoice['type'] == DOC_CNOTE) {
if ($invoiceType == DOC_CNOTE) {
if (isset($invoice['taxest']['-2']) || isset($invoice['invoice']['taxest']['-2'])) {
$taxRate = '-2';
if (isset($invoice['taxest'][$taxRate])) {
Expand All @@ -814,7 +845,7 @@ public function getInvoiceXml(array $invoice)
}
}

if ($invoice['type'] == DOC_CNOTE) {
if ($invoiceType == DOC_CNOTE) {
$xml .= "\t\t<P_15>" . sprintf('%.2f', $diffTotal) . "</P_15>" . PHP_EOL;
} else {
$xml .= "\t\t<P_15>" . sprintf('%.2f', $invoice['total']) . "</P_15>" . PHP_EOL;
Expand Down Expand Up @@ -852,7 +883,7 @@ public function getInvoiceXml(array $invoice)
$xml .= "\t\t\t</PMarzy>" . PHP_EOL;
$xml .= "\t\t</Adnotacje>" . PHP_EOL;

if ($invoice['type'] == DOC_CNOTE) {
if ($invoiceType == DOC_CNOTE) {
$xml .= "\t\t<RodzajFaktury>KOR</RodzajFaktury>" . PHP_EOL;
if (!empty($invoice['reason'])) {
$xml .= "\t\t<PrzyczynaKorekty>" . htmlspecialchars($invoice['reason']) . "</PrzyczynaKorekty>" . PHP_EOL;
Expand Down Expand Up @@ -922,7 +953,7 @@ public function getInvoiceXml(array $invoice)
foreach ($invoice['content'] as $position) {
$itemId = $position['itemid'];

if ($invoice['type'] == DOC_CNOTE && !empty($refInvoiceContent[$itemId])) {
if ($invoiceType == DOC_CNOTE && !empty($refInvoiceContent[$itemId])) {
$description = htmlspecialchars($refInvoiceContent[$itemId]['description']);
if (mb_strlen($description) > 512) {
$description = mb_substr($description, 0, 512 - strlen(' [...]')) . ' [...]';
Expand Down Expand Up @@ -1062,7 +1093,7 @@ public function getInvoiceXml(array $invoice)
}

if (!empty($invoice['ksefshowbalancesummary'])) {
if ($invoice['type'] == DOC_CNOTE) {
if ($invoiceType == DOC_CNOTE) {
$total = $diffTotal;
} else {
$total = $invoice['total'];
Expand Down Expand Up @@ -1624,11 +1655,7 @@ public static function getCertificateQrCodeUrl(array $params): string

public static function downloadUpoFile($invoiceStatus)
{
if (!isset(self::$upoStorage)) {
self::$upoStorage = is_dir(self::KSEF_UPO_DIR) && is_readable(self::KSEF_UPO_DIR);
}

if (!self::$upoStorage) {
if (!self::ensureUpoStorageDirectory()) {
return false;
}

Expand All @@ -1642,7 +1669,20 @@ public static function downloadUpoFile($invoiceStatus)
return 'Couldn\'t download UPO file for KSeF invoice \'' . $invoiceStatus->ksefNumber . '\'!';
}

[$ten, $date] = explode('-', $invoiceStatus->ksefNumber);
return self::saveUpoContent($invoiceStatus->ksefNumber, $upoContent);
}

public static function saveUpoContent($ksefNumber, $upoContent)
{
if (!self::ensureUpoStorageDirectory()) {
return false;
}

if (!is_string($upoContent) || $upoContent === '') {
return 'Empty UPO file content for KSeF invoice \'' . $ksefNumber . '\'!';
}

[$ten, $date] = explode('-', $ksefNumber);

$ksefUpoTenDir = self::KSEF_UPO_DIR . DIRECTORY_SEPARATOR . $ten;
if (!is_dir($ksefUpoTenDir)) {
Expand All @@ -1666,7 +1706,7 @@ public static function downloadUpoFile($invoiceStatus)
@chgrp($ksefUpoTenDateDir, filegroup(self::KSEF_UPO_DIR));
}

$upoFile = $ksefUpoTenDateDir . DIRECTORY_SEPARATOR . $invoiceStatus->ksefNumber . '.xml';
$upoFile = $ksefUpoTenDateDir . DIRECTORY_SEPARATOR . $ksefNumber . '.xml';
if (file_put_contents($upoFile, $upoContent) !== false) {
@chmod(
$upoFile,
Expand All @@ -1675,12 +1715,36 @@ public static function downloadUpoFile($invoiceStatus)
@chown($upoFile, fileowner(self::KSEF_UPO_DIR));
@chgrp($upoFile, filegroup(self::KSEF_UPO_DIR));
} else {
return 'Couldn\'t write UPO file for KSeF invoice \'' . $invoiceStatus->ksefNumber . '\'!';
return 'Couldn\'t write UPO file for KSeF invoice \'' . $ksefNumber . '\'!';
}

return true;
}

private static function ensureUpoStorageDirectory()
{
if (!is_dir(self::KSEF_UPO_DIR)) {
$permissions = is_dir(STORAGE_DIR) ? fileperms(STORAGE_DIR) & 0xfff : 0775;
@mkdir(self::KSEF_UPO_DIR, $permissions, true);

if (is_dir(STORAGE_DIR)) {
$ksefDir = dirname(self::KSEF_UPO_DIR);
@chmod($ksefDir, $permissions);
@chmod(self::KSEF_UPO_DIR, $permissions);
@chown($ksefDir, fileowner(STORAGE_DIR));
@chown(self::KSEF_UPO_DIR, fileowner(STORAGE_DIR));
@chgrp($ksefDir, filegroup(STORAGE_DIR));
@chgrp(self::KSEF_UPO_DIR, filegroup(STORAGE_DIR));
}
}

self::$upoStorage = is_dir(self::KSEF_UPO_DIR)
&& is_readable(self::KSEF_UPO_DIR)
&& is_writable(self::KSEF_UPO_DIR);

return self::$upoStorage;
}

private static function getUpoFilePath($ksefNumber)
{
if (!isset(self::$upoStorage)) {
Expand Down
Loading