bump
35
.github/dev_bug_report.md
vendored
|
|
@ -1,35 +0,0 @@
|
|||
---
|
||||
name: Dev Bug Report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: stat_backlog, type_bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
# Bug Description
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
# How to Reproduce
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
## Minimal Code Example
|
||||
|
||||
```
|
||||
// your code ...
|
||||
```
|
||||
|
||||
# Expected Behavior
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
# Screenshots
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
# Additional Information
|
||||
Add any other context about the problem here.
|
||||
18
.github/dev_feature_request.md
vendored
|
|
@ -1,18 +0,0 @@
|
|||
---
|
||||
name: Dev Feature Request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: stat_backlog, type_feature
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
# What is the feature you request
|
||||
* A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
* A clear and concise description of what you want to happen.
|
||||
|
||||
# Alternatives
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
# Additional Information
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
[
|
||||
{
|
||||
"type": "setting",
|
||||
"name": "1005100003",
|
||||
"content": "[\"en\", \"de\"]",
|
||||
"pattern": "",
|
||||
"module": "Billing"
|
||||
}
|
||||
]
|
||||
26
Admin/Install/Admin.install.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package Modules\Billing\Admin
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
use Modules\Billing\Controller\ApiController;
|
||||
use Modules\Billing\Models\SettingsEnum;
|
||||
|
||||
return [
|
||||
[
|
||||
'type' => 'setting',
|
||||
'name' => SettingsEnum::VALID_BILL_LANGUAGES,
|
||||
'content' => '["en","de"]',
|
||||
'pattern' => '',
|
||||
'module' => ApiController::NAME,
|
||||
],
|
||||
];
|
||||
|
|
@ -38,6 +38,6 @@ class Admin
|
|||
*/
|
||||
public static function install(ApplicationAbstract $app, string $path) : void
|
||||
{
|
||||
\Modules\Admin\Admin\Installer::installExternal($app, ['path' => __DIR__ . '/Admin.install.json']);
|
||||
\Modules\Admin\Admin\Installer::installExternal($app, ['path' => __DIR__ . '/Admin.install.php']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,14 +36,28 @@
|
|||
},
|
||||
{
|
||||
"type": "type",
|
||||
"name": "original",
|
||||
"name": "internal",
|
||||
"l11n": [
|
||||
{
|
||||
"title": "Original",
|
||||
"title": "Internal",
|
||||
"lang": "en"
|
||||
},
|
||||
{
|
||||
"title": "Original",
|
||||
"title": "Intern",
|
||||
"lang": "de"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "type",
|
||||
"name": "external",
|
||||
"l11n": [
|
||||
{
|
||||
"title": "External",
|
||||
"lang": "en"
|
||||
},
|
||||
{
|
||||
"title": "Extern",
|
||||
"lang": "de"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -54,11 +54,18 @@ class Media
|
|||
],
|
||||
[
|
||||
'type' => 'setting',
|
||||
'name' => SettingsEnum::ORIGINAL_MEDIA_TYPE,
|
||||
'name' => SettingsEnum::INTERNAL_MEDIA_TYPE,
|
||||
'content' => (string) $media['type'][1]['id'],
|
||||
'pattern' => '\\d+',
|
||||
'module' => 'Billing',
|
||||
],
|
||||
[
|
||||
'type' => 'setting',
|
||||
'name' => SettingsEnum::EXTERNAL_MEDIA_TYPE,
|
||||
'content' => (string) $media['type'][2]['id'],
|
||||
'pattern' => '\\d+',
|
||||
'module' => 'Billing',
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ declare(strict_types=1);
|
|||
use Modules\Billing\Models\NullBill;
|
||||
use phpOMS\Localization\ISO3166NameEnum;
|
||||
use phpOMS\Localization\Money;
|
||||
use phpOMS\Stdlib\Base\FloatInt;
|
||||
|
||||
/** @var \phpOMS\Views\View $this */
|
||||
|
||||
|
|
@ -118,7 +119,7 @@ $pdf->setTextColor(255, 162, 7);
|
|||
|
||||
$pdf->setXY($rightPos, $tempY = $pdf->getY() + 10, true);
|
||||
$pdf->MultiCell(
|
||||
26, 30,
|
||||
29, 30,
|
||||
$lang[$pdf->language]['InvoiceNo'] . "\n"
|
||||
. $lang[$pdf->language]['InvoiceDate'] . "\n"
|
||||
. $lang[$pdf->language]['ServiceDate'] . "\n"
|
||||
|
|
@ -131,20 +132,22 @@ $pdf->MultiCell(
|
|||
//$pdf->setFont('helvetica', '', 10);
|
||||
$pdf->setTextColor(0, 0, 0);
|
||||
|
||||
$pdf->setXY($rightPos + 26 + 2, $tempY, true);
|
||||
$pdf->setXY($rightPos + 29 + 2, $tempY, true);
|
||||
$pdf->MultiCell(
|
||||
25, 30,
|
||||
$bill->number . "\n"
|
||||
. ($bill->billDate?->format('Y-m-d') ?? '0') . "\n"
|
||||
. ($bill->performanceDate?->format('Y-m-d') ?? '0') . "\n"
|
||||
. $bill->accountNumber . "\n"
|
||||
. $bill->externalReferral . "\n"
|
||||
. $bill->external . "\n"
|
||||
. ($bill->billDate?->format('Y-m-d') ?? '0'), /* Consider to add dueDate in addition */
|
||||
0, 'L'
|
||||
);
|
||||
$pdf->Ln();
|
||||
|
||||
$pdf->setY($pdf->getY() - 20);
|
||||
$tempY = $pdf->getY();
|
||||
$height = 0;
|
||||
$pdf->setY($tempY - 20);
|
||||
|
||||
$header = [
|
||||
$lang[$pdf->language]['Item'],
|
||||
|
|
@ -153,8 +156,6 @@ $header = [
|
|||
$lang[$pdf->language]['Total'],
|
||||
];
|
||||
|
||||
$lines = $bill->elements;
|
||||
|
||||
// Header
|
||||
$headerCount = \count($header);
|
||||
$w = [$pageWidth - 20 - 20 - 20 - 2 * 15, 20, 20, 20];
|
||||
|
|
@ -166,7 +167,7 @@ $first = true;
|
|||
|
||||
// Data
|
||||
$fill = false;
|
||||
foreach($lines as $line) {
|
||||
foreach($bill->elements as $line) {
|
||||
// @todo depending on amount of lines, there is a solution (html, or use backtracking of tcpdf)
|
||||
if ($first || $pdf->getY() > $pageHeight - 40) {
|
||||
$pdf->setFillColor(255, 162, 7);
|
||||
|
|
@ -218,87 +219,93 @@ foreach($lines as $line) {
|
|||
$fill = !$fill;
|
||||
|
||||
// get taxes
|
||||
if (!isset($taxes[$line->taxR->value / 10000])) {
|
||||
$taxes[$line->taxR->value / 10000] = $line->taxP;
|
||||
if (!isset($taxes[$line->taxR->value / FloatInt::DIVISOR])) {
|
||||
$taxes[$line->taxR->value / FloatInt::DIVISOR] = $line->taxP;
|
||||
} else {
|
||||
$taxes[$line->taxR->value / 10000]->add($line->taxP);
|
||||
$taxes[$line->taxR->value / FloatInt::DIVISOR]->add($line->taxP);
|
||||
}
|
||||
}
|
||||
|
||||
$pdf->Cell(\array_sum($w), 0, '', 'T');
|
||||
$pdf->Ln();
|
||||
// We have to do the following because in some cases it doesn't set the Y position correctly after the table
|
||||
// I assume it is related to if/else above. A html multicell might not correctly set the y position.
|
||||
if (!empty($bill->elements)) {
|
||||
$pdf->setY($tempY + $height);
|
||||
|
||||
if ($pdf->getY() > $pageHeight - 40) {
|
||||
$pdf->AddPage();
|
||||
}
|
||||
$pdf->Cell(\array_sum($w), 0, '', 'T');
|
||||
$pdf->Ln();
|
||||
|
||||
$pdf->setFillColor(240, 240, 240);
|
||||
$pdf->setTextColor(0);
|
||||
$pdf->setDrawColor(240, 240, 240);
|
||||
$pdf->setFont('helvetica', 'B', 10);
|
||||
if ($pdf->getY() > $pageHeight - 40) {
|
||||
$pdf->AddPage();
|
||||
}
|
||||
|
||||
$tempY = $pdf->getY();
|
||||
$pdf->setFillColor(240, 240, 240);
|
||||
$pdf->setTextColor(0);
|
||||
$pdf->setDrawColor(240, 240, 240);
|
||||
$pdf->setFont('helvetica', 'B', 10);
|
||||
|
||||
$netSales = Money::fromFloatInt($bill->netSales);
|
||||
$tempY = $pdf->getY();
|
||||
|
||||
$pdf->setX($w[0] + $w[1] + 12);
|
||||
$pdf->Cell($w[2], 7, $lang[$pdf->language]['Subtotal'], 0, 0, 'L', false);
|
||||
$pdf->Cell($w[3], 7, $netSales->getCurrency(2, symbol: ''), 0, 0, 'L', false);
|
||||
$pdf->Ln();
|
||||
|
||||
foreach ($taxes as $rate => $tax) {
|
||||
$tax = Money::fromFloatInt($tax);
|
||||
$netSales = Money::fromFloatInt($bill->netSales);
|
||||
|
||||
$pdf->setX($w[0] + $w[1] + 12);
|
||||
$pdf->Cell($w[2], 7, $lang[$pdf->language]['Taxes'] . ' (' . $rate . '%)', 0, 0, 'L', false);
|
||||
$pdf->Cell($w[3], 7, $tax->getCurrency(2, symbol: ''), 0, 0, 'L', false);
|
||||
$pdf->Cell($w[2], 7, $lang[$pdf->language]['Subtotal'], 0, 0, 'L', false);
|
||||
$pdf->Cell($w[3], 7, $netSales->getCurrency(2, symbol: ''), 0, 0, 'L', false);
|
||||
$pdf->Ln();
|
||||
|
||||
foreach ($taxes as $rate => $tax) {
|
||||
$tax = Money::fromFloatInt($tax);
|
||||
|
||||
$pdf->setX($w[0] + $w[1] + 12);
|
||||
$pdf->Cell($w[2], 7, $lang[$pdf->language]['Taxes'] . ' (' . $rate . '%)', 0, 0, 'L', false);
|
||||
$pdf->Cell($w[3], 7, $tax->getCurrency(2, symbol: ''), 0, 0, 'L', false);
|
||||
$pdf->Ln();
|
||||
}
|
||||
|
||||
$pdf->setFillColor(255, 162, 7);
|
||||
$pdf->setTextColor(255);
|
||||
$pdf->setDrawColor(255, 162, 7);
|
||||
//$pdf->setFont('helvetica', 'B', 10);
|
||||
|
||||
$grossSales = Money::fromFloatInt($bill->grossSales);
|
||||
|
||||
$pdf->setX($w[0] + $w[1] + 12);
|
||||
$pdf->Cell($w[2], 7, \strtoupper($lang[$pdf->language]['Total']), 1, 0, 'L', true);
|
||||
$pdf->Cell($w[3] + 3, 7, $grossSales->getCurrency(2, symbol: ''), 1, 0, 'L', true);
|
||||
$pdf->Ln();
|
||||
|
||||
$tempY2 = $pdf->getY();
|
||||
|
||||
// @todo fix payment terms
|
||||
$pdf->setTextColor(0);
|
||||
$pdf->setFont('helvetica', 'B', 8);
|
||||
$pdf->setY($tempY);
|
||||
$pdf->Write(0, $lang[$pdf->language]['PaymentTerms'] . ': CreditCard', '', false, 'L', false, 0, false, false, 0);
|
||||
|
||||
$pdf->setFont('helvetica', '', 8);
|
||||
$pdf->Write(0, $bill->paymentText, '', false, 'L', false, 0, false, false, 0);
|
||||
$pdf->Ln();
|
||||
|
||||
// @todo fix terms
|
||||
$pdf->setFont('helvetica', 'B', 8);
|
||||
$pdf->Write(0, $lang[$pdf->language]['Terms'] . ': ' . $pdf->attributes['terms'], '', false, 'L', false, 0, false, false, 0);
|
||||
$pdf->Ln();
|
||||
|
||||
//$pdf->setFont('helvetica', 'B', 8);
|
||||
$pdf->Write(0, $lang[$pdf->language]['Currency'] . ': ' . $bill->currency, '', false, 'L', false, 0, false, false, 0);
|
||||
$pdf->Ln();
|
||||
|
||||
//$pdf->setFont('helvetica', 'B', 8);
|
||||
$pdf->Write(0, $lang[$pdf->language]['TaxRemark'], '', false, 'L', false, 0, false, false, 0);
|
||||
$pdf->Ln();
|
||||
|
||||
$pdf->setFont('helvetica', '', 8);
|
||||
$pdf->Write(0, $bill->termsText, '', false, 'L', false, 0, false, false, 0);
|
||||
//$pdf->Ln();
|
||||
|
||||
//$pdf->setY($tempY2);
|
||||
//$pdf->Ln();
|
||||
}
|
||||
|
||||
$pdf->setFillColor(255, 162, 7);
|
||||
$pdf->setTextColor(255);
|
||||
$pdf->setDrawColor(255, 162, 7);
|
||||
//$pdf->setFont('helvetica', 'B', 10);
|
||||
|
||||
$grossSales = Money::fromFloatInt($bill->grossSales);
|
||||
|
||||
$pdf->setX($w[0] + $w[1] + 12);
|
||||
$pdf->Cell($w[2], 7, \strtoupper($lang[$pdf->language]['Total']), 1, 0, 'L', true);
|
||||
$pdf->Cell($w[3] + 3, 7, $grossSales->getCurrency(2, symbol: ''), 1, 0, 'L', true);
|
||||
$pdf->Ln();
|
||||
|
||||
$tempY2 = $pdf->getY();
|
||||
|
||||
// @todo fix payment terms
|
||||
$pdf->setTextColor(0);
|
||||
$pdf->setFont('helvetica', 'B', 8);
|
||||
$pdf->setY($tempY);
|
||||
$pdf->Write(0, $lang[$pdf->language]['PaymentTerms'] . ': CreditCard', '', false, 'L', false, 0, false, false, 0);
|
||||
|
||||
$pdf->setFont('helvetica', '', 8);
|
||||
$pdf->Write(0, $bill->paymentText, '', false, 'L', false, 0, false, false, 0);
|
||||
$pdf->Ln();
|
||||
|
||||
// @todo fix terms
|
||||
$pdf->setFont('helvetica', 'B', 8);
|
||||
$pdf->Write(0, $lang[$pdf->language]['Terms'] . ': ' . $pdf->attributes['terms'], '', false, 'L', false, 0, false, false, 0);
|
||||
$pdf->Ln();
|
||||
|
||||
//$pdf->setFont('helvetica', 'B', 8);
|
||||
$pdf->Write(0, $lang[$pdf->language]['Currency'] . ': ' . $bill->currency, '', false, 'L', false, 0, false, false, 0);
|
||||
$pdf->Ln();
|
||||
|
||||
//$pdf->setFont('helvetica', 'B', 8);
|
||||
$pdf->Write(0, $lang[$pdf->language]['TaxRemark'], '', false, 'L', false, 0, false, false, 0);
|
||||
$pdf->Ln();
|
||||
|
||||
$pdf->setFont('helvetica', '', 8);
|
||||
$pdf->Write(0, $bill->termsText, '', false, 'L', false, 0, false, false, 0);
|
||||
//$pdf->Ln();
|
||||
|
||||
//$pdf->setY($tempY2);
|
||||
//$pdf->Ln();
|
||||
|
||||
//Close and output PDF document
|
||||
$path = (string) ($this->data['path'] ?? (($bill->billDate?->format('Y-m-d') ?? '0') . '_' . $bill->number . '.pdf'));
|
||||
$pdf->Output($path, 'I');
|
||||
|
|
|
|||
|
|
@ -15,7 +15,28 @@
|
|||
"de": {
|
||||
"subject": "Rechnungsstellung",
|
||||
"body": "<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Abrechnung</title></head><body style=\"font-family: Arial, sans-serif; font-size: 14px; line-height: 1.5;\"><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\"><tr><td><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"600\" style=\"margin: 0 auto; background-color: #ffffff;\"><tr><td style=\"padding: 40px 0;\"><h1 style=\"margin-top: 0; color: #000000; font-size: 24px; text-align: center;\">Abrechnung</h1><p style=\"margin-bottom: 20px;\">Sehr geehrte/r {user_name},</p><p style=\"margin-bottom: 20px;\">Vielen Dank für Ihre Geschäftsbeziehung mit uns.</p><p style=\"margin-bottom: 20px;\">Im Anhang finden Sie Ihre Rechnung.</p><p style=\"margin-top: 40px;\">Jingga e.K. - www.jingga.app - CEO Dennis Eichhorn - Amtsgericht Friedberg HRA 5058</p></td></tr></table></td></tr></table></body></html>",
|
||||
"bodyalt": "Sehr geehrte/r {user_name},\n\nvielen Dank für Ihre Geschäftsbeziehung mit uns.\n\nIm Anhang finden Sie freundlicherweise Ihre Rechnung.\n\n\nJingga e.K. - www.jingga.app - CEO Dennis Eichhorn - Amtsgericht Friedberg HRA 5058"
|
||||
"bodyalt": "Sehr geehrte/r {user_name},\n\nVielen Dank für Ihre Geschäftsbeziehung mit uns.\n\nIm Anhang finden Sie freundlicherweise Ihre Rechnung.\n\n\nJingga e.K. - www.jingga.app - CEO Dennis Eichhorn - Amtsgericht Friedberg HRA 5058"
|
||||
}
|
||||
},
|
||||
"send": false
|
||||
},
|
||||
{
|
||||
"type": "email_template",
|
||||
"from": "",
|
||||
"to": "",
|
||||
"cc": "",
|
||||
"bcc": "",
|
||||
"ishtml": true,
|
||||
"l11n": {
|
||||
"en": {
|
||||
"subject": "Order",
|
||||
"body": "<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Order</title></head><body style=\"font-family: Arial, sans-serif; font-size: 14px; line-height: 1.5;\"><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\"><tr><td><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"600\" style=\"margin: 0 auto; background-color: #ffffff;\"><tr><td style=\"padding: 40px 0;\"><h1 style=\"margin-top: 0; color: #000000; font-size: 24px; text-align: center;\">Order</h1><p style=\"margin-bottom: 20px;\">Dear {user_name},</p><p style=\"margin-bottom: 20px;\">We are looking forward to doing business with you.</p><p style=\"margin-bottom: 20px;\">Attached kindly find our order.</p><p style=\"margin-top: 40px;\">Jingga e.K. - www.jingga.app - CEO Dennis Eichhorn - Amtsgericht Friedberg HRA 5058</p></td></tr></table></td></tr></table></body></html>",
|
||||
"bodyalt": "Dear {user_name},\n\nWe are looking forward to doing business with you.\n\nAttached kindly find our order.\n\n\nJingga e.K. - www.jingga.app - CEO Dennis Eichhorn - Amtsgericht Friedberg HRA 5058"
|
||||
},
|
||||
"de": {
|
||||
"subject": "Bestellung",
|
||||
"body": "<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Bestellung</title></head><body style=\"font-family: Arial, sans-serif; font-size: 14px; line-height: 1.5;\"><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\"><tr><td><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"600\" style=\"margin: 0 auto; background-color: #ffffff;\"><tr><td style=\"padding: 40px 0;\"><h1 style=\"margin-top: 0; color: #000000; font-size: 24px; text-align: center;\">Bestellung</h1><p style=\"margin-bottom: 20px;\">Sehr geehrte/r {user_name},</p><p style=\"margin-bottom: 20px;\">Wir freuen uns eine Bestellung bei Ihnen aufgeben zu können.</p><p style=\"margin-bottom: 20px;\">Im Anhang finden Sie unsere Bestellung.</p><p style=\"margin-top: 40px;\">Jingga e.K. - www.jingga.app - CEO Dennis Eichhorn - Amtsgericht Friedberg HRA 5058</p></td></tr></table></td></tr></table></body></html>",
|
||||
"bodyalt": "Sehr geehrte/r {user_name},\n\nWir freuen uns eine Bestellung bei Ihnen aufgeben zu können.\n\nIm Anhang finden Sie unsere Bestellung.\n\n\nJingga e.K. - www.jingga.app - CEO Dennis Eichhorn - Amtsgericht Friedberg HRA 5058"
|
||||
}
|
||||
},
|
||||
"send": false
|
||||
|
|
|
|||
|
|
@ -53,6 +53,12 @@ class Messages
|
|||
'content' => (string) $messages['email_template'][0]['id'],
|
||||
'module' => 'Billing',
|
||||
],
|
||||
[
|
||||
'id' => null,
|
||||
'name' => SettingsEnum::BILLING_SUPPLIER_EMAIL_TEMPLATE,
|
||||
'content' => (string) $messages['email_template'][1]['id'],
|
||||
'module' => 'Billing',
|
||||
],
|
||||
];
|
||||
|
||||
$response = new HttpResponse();
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"type": 2,
|
||||
"subtype": 1,
|
||||
"name": "Billing",
|
||||
"uri": "{/base}/sales/bill/list",
|
||||
"uri": "{/base}/sales/bill/list?{?}",
|
||||
"target": "self",
|
||||
"icon": null,
|
||||
"order": 10,
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
"type": 3,
|
||||
"subtype": 1,
|
||||
"name": "Open",
|
||||
"uri": "{/base}/sales/bill/list",
|
||||
"uri": "{/base}/sales/bill/list?{?}",
|
||||
"target": "self",
|
||||
"icon": null,
|
||||
"order": 1,
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
"type": 3,
|
||||
"subtype": 1,
|
||||
"name": "Archive",
|
||||
"uri": "{/base}/sales/bill/archive",
|
||||
"uri": "{/base}/sales/bill/archive?{?}",
|
||||
"target": "self",
|
||||
"icon": null,
|
||||
"order": 5,
|
||||
|
|
@ -96,7 +96,7 @@
|
|||
"type": 2,
|
||||
"subtype": 1,
|
||||
"name": "Billing",
|
||||
"uri": "{/base}/purchase/bill/list",
|
||||
"uri": "{/base}/purchase/bill/list?{?}",
|
||||
"target": "self",
|
||||
"icon": null,
|
||||
"order": 10,
|
||||
|
|
@ -110,7 +110,7 @@
|
|||
"type": 3,
|
||||
"subtype": 1,
|
||||
"name": "Open",
|
||||
"uri": "{/base}/purchase/bill/list",
|
||||
"uri": "{/base}/purchase/bill/list?{?}",
|
||||
"target": "self",
|
||||
"icon": null,
|
||||
"order": 1,
|
||||
|
|
@ -125,7 +125,7 @@
|
|||
"type": 3,
|
||||
"subtype": 1,
|
||||
"name": "Archive",
|
||||
"uri": "{/base}/purchase/bill/archive",
|
||||
"uri": "{/base}/purchase/bill/archive?{?}",
|
||||
"target": "self",
|
||||
"icon": null,
|
||||
"order": 5,
|
||||
|
|
@ -172,7 +172,7 @@
|
|||
"type": 2,
|
||||
"subtype": 1,
|
||||
"name": "Billing",
|
||||
"uri": "{/base}/warehouse/bill/list",
|
||||
"uri": "{/base}/warehouse/bill/list?{?}",
|
||||
"target": "self",
|
||||
"icon": null,
|
||||
"order": 5,
|
||||
|
|
@ -186,7 +186,7 @@
|
|||
"type": 3,
|
||||
"subtype": 1,
|
||||
"name": "List",
|
||||
"uri": "{/base}/warehouse/bill/list",
|
||||
"uri": "{/base}/warehouse/bill/list?{?}",
|
||||
"target": "self",
|
||||
"icon": null,
|
||||
"order": 1,
|
||||
|
|
@ -201,7 +201,7 @@
|
|||
"type": 3,
|
||||
"subtype": 1,
|
||||
"name": "Archive",
|
||||
"uri": "{/base}/warehouse/bill/archive",
|
||||
"uri": "{/base}/warehouse/bill/archive?{?}",
|
||||
"target": "self",
|
||||
"icon": null,
|
||||
"order": 1,
|
||||
|
|
@ -233,7 +233,7 @@
|
|||
"type": 3,
|
||||
"subtype": 1,
|
||||
"name": "Bill",
|
||||
"uri": "{/base}/purchase/analysis/bill",
|
||||
"uri": "{/base}/purchase/analysis/bill?{?}",
|
||||
"target": "self",
|
||||
"icon": null,
|
||||
"order": 15,
|
||||
|
|
|
|||
|
|
@ -242,6 +242,11 @@
|
|||
"type": "TINYINT(1)",
|
||||
"null": false
|
||||
},
|
||||
"billing_price_status": {
|
||||
"name": "billing_price_status",
|
||||
"type": "TINYINT(1)",
|
||||
"null": false
|
||||
},
|
||||
"billing_price_quantity": {
|
||||
"name": "billing_price_quantity",
|
||||
"type": "BIGINT",
|
||||
|
|
@ -440,6 +445,12 @@
|
|||
"type": "TINYINT(1)",
|
||||
"null": false
|
||||
},
|
||||
"billing_type_email": {
|
||||
"description": "send email on archive",
|
||||
"name": "billing_type_email",
|
||||
"type": "TINYINT(1)",
|
||||
"null": false
|
||||
},
|
||||
"billing_type_account_format": {
|
||||
"name": "billing_type_account_format",
|
||||
"type": "VARCHAR(255)",
|
||||
|
|
@ -535,6 +546,11 @@
|
|||
"type": "VARCHAR(255)",
|
||||
"null": false
|
||||
},
|
||||
"billing_bill_external": {
|
||||
"name": "billing_bill_external",
|
||||
"type": "VARCHAR(255)",
|
||||
"null": false
|
||||
},
|
||||
"billing_bill_info": {
|
||||
"name": "billing_bill_info",
|
||||
"type": "TEXT",
|
||||
|
|
@ -715,21 +731,11 @@
|
|||
"type": "BIGINT",
|
||||
"null": false
|
||||
},
|
||||
"billing_bill_grossprofit": {
|
||||
"name": "billing_bill_grossprofit",
|
||||
"type": "BIGINT",
|
||||
"null": false
|
||||
},
|
||||
"billing_bill_netcosts": {
|
||||
"name": "billing_bill_netcosts",
|
||||
"type": "BIGINT",
|
||||
"null": false
|
||||
},
|
||||
"billing_bill_grosscosts": {
|
||||
"name": "billing_bill_grosscosts",
|
||||
"type": "BIGINT",
|
||||
"null": false
|
||||
},
|
||||
"billing_bill_netsales": {
|
||||
"name": "billing_bill_netsales",
|
||||
"type": "BIGINT",
|
||||
|
|
@ -745,11 +751,6 @@
|
|||
"type": "BIGINT",
|
||||
"null": false
|
||||
},
|
||||
"billing_bill_grossdiscount": {
|
||||
"name": "billing_bill_grossdiscount",
|
||||
"type": "BIGINT",
|
||||
"null": false
|
||||
},
|
||||
"billing_bill_taxp": {
|
||||
"name": "billing_bill_taxp",
|
||||
"type": "BIGINT",
|
||||
|
|
@ -781,12 +782,6 @@
|
|||
"foreignTable": "account",
|
||||
"foreignKey": "account_id"
|
||||
},
|
||||
"billing_bill_referral_name": {
|
||||
"name": "billing_bill_referral_name",
|
||||
"type": "VARCHAR(255)",
|
||||
"default": null,
|
||||
"null": true
|
||||
},
|
||||
"billing_bill_reference": {
|
||||
"name": "billing_bill_reference",
|
||||
"type": "INT",
|
||||
|
|
@ -1061,48 +1056,24 @@
|
|||
"null": true,
|
||||
"default": null
|
||||
},
|
||||
"billing_bill_element_single_grosspurchaseprice": {
|
||||
"name": "billing_bill_element_single_grosspurchaseprice",
|
||||
"type": "BIGINT",
|
||||
"null": true,
|
||||
"default": null
|
||||
},
|
||||
"billing_bill_element_total_netpurchaseprice": {
|
||||
"name": "billing_bill_element_total_netpurchaseprice",
|
||||
"type": "BIGINT",
|
||||
"null": true,
|
||||
"default": null
|
||||
},
|
||||
"billing_bill_element_total_grosspurchaseprice": {
|
||||
"name": "billing_bill_element_total_grosspurchaseprice",
|
||||
"type": "BIGINT",
|
||||
"null": true,
|
||||
"default": null
|
||||
},
|
||||
"billing_bill_element_single_netprofit": {
|
||||
"name": "billing_bill_element_single_netprofit",
|
||||
"type": "BIGINT",
|
||||
"null": true,
|
||||
"default": null
|
||||
},
|
||||
"billing_bill_element_single_grossprofit": {
|
||||
"name": "billing_bill_element_single_grossprofit",
|
||||
"type": "BIGINT",
|
||||
"null": true,
|
||||
"default": null
|
||||
},
|
||||
"billing_bill_element_total_netprofit": {
|
||||
"name": "billing_bill_element_total_netprofit",
|
||||
"type": "BIGINT",
|
||||
"null": true,
|
||||
"default": null
|
||||
},
|
||||
"billing_bill_element_total_grossprofit": {
|
||||
"name": "billing_bill_element_total_grossprofit",
|
||||
"type": "BIGINT",
|
||||
"null": true,
|
||||
"default": null
|
||||
},
|
||||
"billing_bill_element_price_discount_single": {
|
||||
"name": "billing_bill_element_price_discount_single",
|
||||
"type": "BIGINT",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
"numberFormat": "{y}{type}-{m}{sequence}",
|
||||
"transferType": 1,
|
||||
"sign": 1,
|
||||
"email": true,
|
||||
"isAccounting": false,
|
||||
"transferStock": false,
|
||||
"isTemplate": false,
|
||||
|
|
@ -17,6 +18,7 @@
|
|||
"numberFormat": "{y}{type}-{m}{sequence}",
|
||||
"transferType": 1,
|
||||
"sign": 1,
|
||||
"email": true,
|
||||
"isAccounting": false,
|
||||
"transferStock": false,
|
||||
"isTemplate": false,
|
||||
|
|
@ -30,6 +32,7 @@
|
|||
"numberFormat": "{y}{type}-{m}{sequence}",
|
||||
"transferType": 1,
|
||||
"sign": 1,
|
||||
"email": false,
|
||||
"isAccounting": false,
|
||||
"transferStock": true,
|
||||
"isTemplate": false,
|
||||
|
|
@ -43,6 +46,7 @@
|
|||
"numberFormat": "{y}{type}-{m}{sequence}",
|
||||
"transferType": 1,
|
||||
"sign": 1,
|
||||
"email": true,
|
||||
"isAccounting": true,
|
||||
"transferStock": false,
|
||||
"isTemplate": false,
|
||||
|
|
@ -56,6 +60,7 @@
|
|||
"numberFormat": "{y}{type}-{m}{sequence}",
|
||||
"transferType": 1,
|
||||
"sign": 1,
|
||||
"email": true,
|
||||
"isAccounting": false,
|
||||
"transferStock": false,
|
||||
"isTemplate": false,
|
||||
|
|
@ -69,6 +74,7 @@
|
|||
"numberFormat": "{y}{type}-{m}{sequence}",
|
||||
"transferType": 1,
|
||||
"sign": -1,
|
||||
"email": true,
|
||||
"isAccounting": true,
|
||||
"transferStock": false,
|
||||
"isTemplate": false,
|
||||
|
|
@ -82,6 +88,7 @@
|
|||
"numberFormat": "{y}{type}-{m}{sequence}",
|
||||
"transferType": 1,
|
||||
"sign": -1,
|
||||
"email": true,
|
||||
"isAccounting": true,
|
||||
"transferStock": false,
|
||||
"isTemplate": false,
|
||||
|
|
@ -95,6 +102,7 @@
|
|||
"numberFormat": "{y}{type}-{m}{sequence}",
|
||||
"transferType": 2,
|
||||
"sign": -1,
|
||||
"email": false,
|
||||
"isAccounting": false,
|
||||
"transferStock": false,
|
||||
"isTemplate": false,
|
||||
|
|
@ -108,6 +116,7 @@
|
|||
"numberFormat": "{y}{type}-{m}{sequence}",
|
||||
"transferType": 2,
|
||||
"sign": -1,
|
||||
"email": true,
|
||||
"isAccounting": false,
|
||||
"transferStock": false,
|
||||
"isTemplate": false,
|
||||
|
|
@ -121,6 +130,7 @@
|
|||
"numberFormat": "{y}{type}-{m}{sequence}",
|
||||
"transferType": 2,
|
||||
"sign": -1,
|
||||
"email": false,
|
||||
"isAccounting": false,
|
||||
"transferStock": false,
|
||||
"isTemplate": false,
|
||||
|
|
@ -134,6 +144,7 @@
|
|||
"numberFormat": "{y}{type}-{m}{sequence}",
|
||||
"transferType": 2,
|
||||
"sign": -1,
|
||||
"email": false,
|
||||
"isAccounting": false,
|
||||
"transferStock": true,
|
||||
"isTemplate": false,
|
||||
|
|
@ -147,6 +158,7 @@
|
|||
"numberFormat": "{y}{type}-{m}{sequence}",
|
||||
"transferType": 2,
|
||||
"sign": -1,
|
||||
"email": false,
|
||||
"isAccounting": true,
|
||||
"transferStock": false,
|
||||
"isTemplate": false,
|
||||
|
|
@ -160,6 +172,7 @@
|
|||
"numberFormat": "{y}{type}-{m}{sequence}",
|
||||
"transferType": 2,
|
||||
"sign": -1,
|
||||
"email": false,
|
||||
"isAccounting": false,
|
||||
"transferStock": false,
|
||||
"isTemplate": false,
|
||||
|
|
@ -173,6 +186,7 @@
|
|||
"numberFormat": "{y}{type}-{m}{sequence}",
|
||||
"transferType": 2,
|
||||
"sign": 1,
|
||||
"email": false,
|
||||
"isAccounting": true,
|
||||
"transferStock": false,
|
||||
"isTemplate": false,
|
||||
|
|
@ -186,6 +200,7 @@
|
|||
"numberFormat": "{y}{type}-{m}{sequence}",
|
||||
"transferType": 2,
|
||||
"sign": 1,
|
||||
"email": false,
|
||||
"isAccounting": true,
|
||||
"transferStock": false,
|
||||
"isTemplate": false,
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ final class Installer extends InstallerAbstract
|
|||
$billAttrType = [];
|
||||
|
||||
/** @var \Modules\Billing\Controller\ApiAttributeController $module */
|
||||
$module = $app->moduleManager->getModuleInstance('Billing', 'ApiAttribute');
|
||||
$module = $app->moduleManager->get('Billing', 'ApiAttribute');
|
||||
|
||||
/** @var array $attribute */
|
||||
foreach ($attributes as $attribute) {
|
||||
|
|
@ -205,7 +205,7 @@ final class Installer extends InstallerAbstract
|
|||
$billAttrValue = [];
|
||||
|
||||
/** @var \Modules\Billing\Controller\ApiAttributeController $module */
|
||||
$module = $app->moduleManager->getModuleInstance('Billing', 'ApiAttribute');
|
||||
$module = $app->moduleManager->get('Billing', 'ApiAttribute');
|
||||
|
||||
foreach ($attributes as $attribute) {
|
||||
$billAttrValue[$attribute['name']] = [];
|
||||
|
|
@ -277,7 +277,7 @@ final class Installer extends InstallerAbstract
|
|||
$result = [];
|
||||
|
||||
/** @var \Modules\Billing\Controller\ApiTaxController $module */
|
||||
$module = $app->moduleManager->getModuleInstance('Billing', 'ApiTax');
|
||||
$module = $app->moduleManager->get('Billing', 'ApiTax');
|
||||
|
||||
/** @var \Modules\Attribute\Models\AttributeType $itemAttributeSales */
|
||||
$itemAttributeSales = ItemAttributeTypeMapper::get()
|
||||
|
|
@ -345,7 +345,7 @@ final class Installer extends InstallerAbstract
|
|||
$billTypes = [];
|
||||
|
||||
/** @var \Modules\Billing\Controller\ApiBillTypeController $module */
|
||||
$module = $app->moduleManager->getModuleInstance('Billing', 'ApiBillType');
|
||||
$module = $app->moduleManager->get('Billing', 'ApiBillType');
|
||||
|
||||
foreach ($types as $type) {
|
||||
$response = new HttpResponse();
|
||||
|
|
@ -357,6 +357,7 @@ final class Installer extends InstallerAbstract
|
|||
$request->setData('language', \array_keys($type['l11n'])[0] ?? 'en');
|
||||
$request->setData('number_format', $type['numberFormat'] ?? '{id}');
|
||||
$request->setData('sign', $type['sign'] ?? 1);
|
||||
$request->setData('email', $type['email'] ?? false);
|
||||
$request->setData('transfer_stock', $type['transferStock'] ?? false);
|
||||
$request->setData('is_template', $type['isTemplate'] ?? false);
|
||||
$request->setData('is_accounting', $type['isAccounting'] ?? false);
|
||||
|
|
@ -414,7 +415,7 @@ final class Installer extends InstallerAbstract
|
|||
$paymentTerms = [];
|
||||
|
||||
/** @var \Modules\Billing\Controller\ApiController $module */
|
||||
$module = $app->moduleManager->getModuleInstance('Billing', 'Api');
|
||||
$module = $app->moduleManager->get('Billing', 'Api');
|
||||
|
||||
/** @var array $type */
|
||||
foreach ($types as $type) {
|
||||
|
|
@ -474,7 +475,7 @@ final class Installer extends InstallerAbstract
|
|||
$shippingTerms = [];
|
||||
|
||||
/** @var \Modules\Billing\Controller\ApiController $module */
|
||||
$module = $app->moduleManager->getModuleInstance('Billing', 'Api');
|
||||
$module = $app->moduleManager->get('Billing', 'Api');
|
||||
|
||||
/** @var array $type */
|
||||
foreach ($types as $type) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
use phpOMS\Router\RouteVerb;
|
||||
|
||||
return [
|
||||
'^/billing/bill/purchase/parse(\?.*$|$)' => [
|
||||
'^/billing/bill/purchase/parse( .*$|$)' => [
|
||||
[
|
||||
'dest' => '\Modules\Billing\Controller\CliController:cliParseSupplierBill',
|
||||
'verb' => RouteVerb::ANY,
|
||||
|
|
|
|||
|
|
@ -51,4 +51,15 @@ return [
|
|||
],
|
||||
],
|
||||
],
|
||||
'^.*/bill/parse(\?.*$|$)' => [
|
||||
[
|
||||
'dest' => '\Modules\Billing\Controller\ApiPurchaseController:apiInvoiceParse',
|
||||
'verb' => RouteVerb::SET,
|
||||
'permission' => [
|
||||
'module' => BackendController::NAME,
|
||||
'type' => PermissionType::MODIFY,
|
||||
'state' => PermissionCategory::PURCHASE_INVOICE,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ use phpOMS\Localization\ISO4217CharEnum;
|
|||
use phpOMS\Localization\ISO639x1Enum;
|
||||
use phpOMS\Message\Http\HttpRequest;
|
||||
use phpOMS\Message\Http\RequestStatusCode;
|
||||
use phpOMS\Message\Mail\Email;
|
||||
use phpOMS\Message\NotificationLevel;
|
||||
use phpOMS\Message\RequestAbstract;
|
||||
use phpOMS\Message\ResponseAbstract;
|
||||
|
|
@ -94,6 +93,105 @@ final class ApiBillController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
public function apiBillEmail(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
|
||||
{
|
||||
$bill = $data['bill'] ?? BillMapper::get()
|
||||
->with('type')
|
||||
->with('files')
|
||||
->with('files/types')
|
||||
->where('id', $request->getDataInt('bill') ?? 0)
|
||||
->execute();
|
||||
|
||||
$media = $data['media'] ?? $bill->getFileByTypeName('internal');;
|
||||
|
||||
if ($bill->status === BillStatus::ARCHIVED
|
||||
&& $bill->type->email
|
||||
) {
|
||||
$email = $request->getDataString('email');
|
||||
$billingTemplate = null;
|
||||
|
||||
if (!empty($email)) {
|
||||
/** @var \Model\Setting $billingTemplate */
|
||||
$billingTemplate = $this->app->appSettings->get(
|
||||
names: SettingsEnum::BILLING_CUSTOMER_EMAIL_TEMPLATE,
|
||||
module: 'Billing'
|
||||
);
|
||||
} elseif (($bill->client?->id ?? 0) !== 0) {
|
||||
$client = ClientMapper::get()
|
||||
->with('account')
|
||||
->with('attributes')
|
||||
->with('attributes/type')
|
||||
->with('attributes/value')
|
||||
->where('id', $bill->client?->id ?? 0)
|
||||
->where('attributes/type/name', ['bill_emails', 'bill_email_address'], 'IN')
|
||||
->execute();
|
||||
|
||||
/** @var \Model\Setting $billingTemplate */
|
||||
$billingTemplate = $this->app->appSettings->get(
|
||||
names: SettingsEnum::BILLING_CUSTOMER_EMAIL_TEMPLATE,
|
||||
module: 'Billing'
|
||||
);
|
||||
|
||||
if ($client->getAttribute('bill_emails')->value->getValue() === 1) {
|
||||
// @todo should this really be a string or an ID for a contact element?
|
||||
$email ??= empty($tmp = $client->getAttribute('bill_email_address')->value->getValue())
|
||||
? $client->account->email
|
||||
: (string) $tmp;
|
||||
}
|
||||
} elseif (($bill->supplier?->id ?? 0) !== 0) {
|
||||
$supplier = SupplierMapper::get()
|
||||
->with('account')
|
||||
->with('attributes')
|
||||
->with('attributes/type')
|
||||
->with('attributes/value')
|
||||
->where('id', $bill->supplier?->id ?? 0)
|
||||
->where('attributes/type/name', ['bill_emails', 'bill_email_address'], 'IN')
|
||||
->execute();
|
||||
|
||||
/** @var \Model\Setting $billingTemplate */
|
||||
$billingTemplate = $this->app->appSettings->get(
|
||||
names: SettingsEnum::BILLING_SUPPLIER_EMAIL_TEMPLATE,
|
||||
module: 'Billing'
|
||||
);
|
||||
|
||||
if ($supplier->getAttribute('bill_emails')->value->getValue() === 1) {
|
||||
// @todo should this really be a string or an ID for a contact element?
|
||||
$email ??= empty($tmp = $supplier->getAttribute('bill_email_address')->value->getValue())
|
||||
? $supplier->account->email
|
||||
: (string) $tmp;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($email)) {
|
||||
$this->sendBillEmail($media, $email, (int) $billingTemplate->content, $bill->language);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function apiBillFinalize(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
|
||||
{
|
||||
// Archive bill
|
||||
/** @var \Modules\Billing\Models\Bill $bill */
|
||||
$old = BillMapper::get()
|
||||
->with('type')
|
||||
->where('id', $request->getDataInt('bill') ?? 0)
|
||||
->execute();
|
||||
|
||||
$new = clone $old;
|
||||
$new->status = BillStatus::ARCHIVED;
|
||||
|
||||
$this->updateModel($request->header->account, $old, $new, BillMapper::class, 'bill', $request->getOrigin());
|
||||
|
||||
// Create final pdf
|
||||
$this->apiBillPdfArchiveCreate($request, $response, $data);
|
||||
$media = $response->getDataArray($request->uri->__toString())['response'];
|
||||
|
||||
// Send bill via email
|
||||
$this->apiBillEmail($request, $response, ['bill' => $old, 'media' => $media]);
|
||||
|
||||
$this->createStandardUpdateResponse($request, $response, $new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Api method to update a bill
|
||||
*
|
||||
|
|
@ -244,11 +342,8 @@ final class ApiBillController extends Controller
|
|||
* Maybe needs to add a status when last validated, we don't want to validate every time
|
||||
* https://github.com/Karaka-Management/oms-Billing/issues/44
|
||||
*
|
||||
* @todo Use bill and shipping address instead of main address if available
|
||||
*
|
||||
* @todo Implement allowed invoice languages and a default invoice language if none match
|
||||
*
|
||||
* @todo Implement client invoice language (allowing for different invoice languages than invoice address)
|
||||
* @todo Add custom tax id for bill to manually overwrite the client_sales_tax_code
|
||||
* https://github.com/Karaka-Management/oms-Billing/issues/65
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
|
@ -260,7 +355,7 @@ final class ApiBillController extends Controller
|
|||
$bill->billDate = $request->getDataDateTime('bill_date') ?? new \DateTime('now');
|
||||
$bill->performanceDate = $request->getDataDateTime('performancedate') ?? new \DateTime('now');
|
||||
$bill->accountNumber = $account->number;
|
||||
$bill->externalReferral = $request->getDataString('externalreferral') ?? '';
|
||||
$bill->external = $request->getDataString('externalreferral') ?? '';
|
||||
$bill->status = BillStatus::tryFromValue($request->getDataInt('status')) ?? BillStatus::DRAFT;
|
||||
|
||||
$bill->shippingTerms = null;
|
||||
|
|
@ -272,6 +367,7 @@ final class ApiBillController extends Controller
|
|||
// @todo Handle payment due
|
||||
// Careful, there can be multiple due dates
|
||||
// Example: payment plan or discounted and none-discounted date
|
||||
// https://github.com/Karaka-Management/oms-Billing/issues/53
|
||||
|
||||
if ($account instanceof Client) {
|
||||
$bill->client = $account;
|
||||
|
|
@ -286,6 +382,7 @@ final class ApiBillController extends Controller
|
|||
}
|
||||
|
||||
// @todo use bill and shipping address instead of main address if available
|
||||
// https://github.com/Karaka-Management/oms-Billing/issues/45
|
||||
$bill->billTo = $request->getDataString('billto') ?? $account->account->name1;
|
||||
$bill->billAddress = $request->getDataString('billaddress') ?? $account->mainAddress->address;
|
||||
$bill->billCity = $request->getDataString('billtocity') ?? $account->mainAddress->city;
|
||||
|
|
@ -294,6 +391,26 @@ final class ApiBillController extends Controller
|
|||
|
||||
$bill->currency = ISO4217CharEnum::_EUR;
|
||||
|
||||
$bill->language = $this->findBillLanguage($account);
|
||||
|
||||
$typeMapper = BillTypeMapper::get()
|
||||
->with('l11n')
|
||||
->where('l11n/language', $bill->language)
|
||||
->limit(1);
|
||||
|
||||
if ($request->hasData('type')) {
|
||||
$typeMapper->where('id', $request->getDataInt('type'));
|
||||
} else {
|
||||
$typeMapper->where('name', 'sales_invoice');
|
||||
}
|
||||
|
||||
$bill->type = $typeMapper->execute();
|
||||
|
||||
return $bill;
|
||||
}
|
||||
|
||||
private function findBillLanguage(Client|Supplier $account) : string
|
||||
{
|
||||
/** @var \Model\Setting $settings */
|
||||
$settings = $this->app->appSettings->get(null,
|
||||
SettingsEnum::VALID_BILL_LANGUAGES,
|
||||
|
|
@ -328,29 +445,18 @@ final class ApiBillController extends Controller
|
|||
$billLanguage = $accountBillLanguage;
|
||||
} else {
|
||||
$accountLanguages = ISO639x1Enum::languageFromCountry($account->mainAddress->country);
|
||||
$accountLanguage = empty($accountLanguages) ? '' : $accountLanguages[0];
|
||||
$accountLanguage = '';
|
||||
|
||||
if (\in_array($accountLanguage, $validLanguages)) {
|
||||
$billLanguage = $accountLanguage;
|
||||
foreach ($accountLanguages as $accountLanguage) {
|
||||
if (\in_array($accountLanguage, $validLanguages)) {
|
||||
$billLanguage = $accountLanguage;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$bill->language = $billLanguage;
|
||||
|
||||
$typeMapper = BillTypeMapper::get()
|
||||
->with('l11n')
|
||||
->where('l11n/language', $billLanguage)
|
||||
->limit(1);
|
||||
|
||||
if ($request->hasData('type')) {
|
||||
$typeMapper->where('id', $request->getDataInt('type'));
|
||||
} else {
|
||||
$typeMapper->where('name', 'sales_invoice');
|
||||
}
|
||||
|
||||
$bill->type = $typeMapper->execute();
|
||||
|
||||
return $bill;
|
||||
return $billLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -424,7 +530,7 @@ final class ApiBillController extends Controller
|
|||
$element = BillElement::fromItem(
|
||||
$item,
|
||||
$taxCombination,
|
||||
FloatInt::toInt($request->getDataString('quantity') ?? 1),
|
||||
FloatInt::toInt($request->getDataString('quantity') ?? '1'),
|
||||
$bill,
|
||||
$container
|
||||
);
|
||||
|
|
@ -856,7 +962,7 @@ final class ApiBillController extends Controller
|
|||
'sales_tax_code', 'purchase_tax_code', 'costcenter', 'costobject',
|
||||
'default_purchase_container', 'default_sales_container',
|
||||
], 'IN')
|
||||
->where('l11n/type/title', ['name1', 'name2', 'name3'], 'IN')
|
||||
->where('l11n/type/title', ['name1', 'name2'], 'IN')
|
||||
->where('l11n/language', $bill->language)
|
||||
->execute();
|
||||
|
||||
|
|
@ -904,7 +1010,27 @@ final class ApiBillController extends Controller
|
|||
*/
|
||||
public function apiMediaRender(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
|
||||
{
|
||||
// @todo check if has permission
|
||||
if (!$this->app->accountManager->get($request->header->account)->hasPermission(
|
||||
PermissionType::READ,
|
||||
$this->app->unitId,
|
||||
null,
|
||||
self::NAME,
|
||||
PermissionCategory::SALES_INVOICE
|
||||
)
|
||||
&& !$this->app->accountManager->get($request->header->account)->hasPermission(
|
||||
PermissionType::READ,
|
||||
$this->app->unitId,
|
||||
null,
|
||||
self::NAME,
|
||||
PermissionCategory::PURCHASE_INVOICE
|
||||
)
|
||||
) {
|
||||
$this->fillJsonResponse($request, $response, NotificationLevel::HIDDEN, '', '', []);
|
||||
$response->header->status = RequestStatusCode::R_403;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->app->moduleManager->get('Media', 'Api')->apiMediaExport($request, $response, ['ignorePermission' => true]);
|
||||
}
|
||||
|
||||
|
|
@ -923,6 +1049,27 @@ final class ApiBillController extends Controller
|
|||
*/
|
||||
public function apiPreviewRender(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
|
||||
{
|
||||
if (!$this->app->accountManager->get($request->header->account)->hasPermission(
|
||||
PermissionType::READ,
|
||||
$this->app->unitId,
|
||||
null,
|
||||
self::NAME,
|
||||
PermissionCategory::SALES_INVOICE
|
||||
)
|
||||
&& !$this->app->accountManager->get($request->header->account)->hasPermission(
|
||||
PermissionType::READ,
|
||||
$this->app->unitId,
|
||||
null,
|
||||
self::NAME,
|
||||
PermissionCategory::PURCHASE_INVOICE
|
||||
)
|
||||
) {
|
||||
$this->fillJsonResponse($request, $response, NotificationLevel::HIDDEN, '', '', []);
|
||||
$response->header->status = RequestStatusCode::R_403;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Autoloader::addPath(__DIR__ . '/../../../Resources/');
|
||||
|
||||
/** @var \Modules\Billing\Models\Bill $bill */
|
||||
|
|
@ -1068,10 +1215,33 @@ final class ApiBillController extends Controller
|
|||
*/
|
||||
public function apiBillPdfArchiveCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
|
||||
{
|
||||
if (!$this->app->accountManager->get($request->header->account)->hasPermission(
|
||||
PermissionType::READ,
|
||||
$this->app->unitId,
|
||||
null,
|
||||
self::NAME,
|
||||
PermissionCategory::SALES_INVOICE
|
||||
)
|
||||
&& !$this->app->accountManager->get($request->header->account)->hasPermission(
|
||||
PermissionType::READ,
|
||||
$this->app->unitId,
|
||||
null,
|
||||
self::NAME,
|
||||
PermissionCategory::PURCHASE_INVOICE
|
||||
)
|
||||
) {
|
||||
$this->fillJsonResponse($request, $response, NotificationLevel::HIDDEN, '', '', []);
|
||||
$response->header->status = RequestStatusCode::R_403;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Autoloader::addPath(__DIR__ . '/../../../Resources/');
|
||||
|
||||
/** @var \Modules\Billing\Models\Bill $bill */
|
||||
$bill = BillMapper::get()
|
||||
->with('files')
|
||||
->with('files/types')
|
||||
->with('elements')
|
||||
->with('elements/container')
|
||||
->with('type')
|
||||
|
|
@ -1168,6 +1338,15 @@ final class ApiBillController extends Controller
|
|||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
/** @var \Model\Setting $internalType */
|
||||
$internalType = $this->app->appSettings->get(
|
||||
names: SettingsEnum::INTERNAL_MEDIA_TYPE,
|
||||
module: self::NAME
|
||||
);
|
||||
|
||||
// @todo Check if old file exists -> update media
|
||||
$oldFile = $bill->getFileByType((int) $internalType->content);
|
||||
|
||||
$billFileName = ($bill->billDate?->format('Y-m-d') ?? '0') . '_' . $bill->number . '.pdf';
|
||||
|
||||
\file_put_contents($pdfDir . '/' . $billFileName, $pdf);
|
||||
|
|
@ -1177,73 +1356,60 @@ final class ApiBillController extends Controller
|
|||
return;
|
||||
}
|
||||
|
||||
$media = $this->app->moduleManager->get('Media', 'Api')->createDbEntry(
|
||||
status: [
|
||||
'status' => UploadStatus::OK,
|
||||
'name' => $billFileName,
|
||||
'path' => $pdfDir,
|
||||
'filename' => $billFileName,
|
||||
'size' => \filesize($pdfDir . '/' . $billFileName),
|
||||
'extension' => 'pdf',
|
||||
],
|
||||
account: $request->header->account,
|
||||
virtualPath: $path,
|
||||
ip: $request->getOrigin(),
|
||||
app: $this->app,
|
||||
readContent: true,
|
||||
unit: $this->app->unitId
|
||||
);
|
||||
$media = null;
|
||||
if ($oldFile->id === 0) {
|
||||
$media = $this->app->moduleManager->get('Media', 'Api')->createDbEntry(
|
||||
status: [
|
||||
'status' => UploadStatus::OK,
|
||||
'name' => $billFileName,
|
||||
'path' => $pdfDir,
|
||||
'filename' => $billFileName,
|
||||
'size' => \filesize($pdfDir . '/' . $billFileName),
|
||||
'extension' => 'pdf',
|
||||
],
|
||||
account: $request->header->account,
|
||||
virtualPath: $path,
|
||||
ip: $request->getOrigin(),
|
||||
app: $this->app,
|
||||
readContent: true,
|
||||
unit: $this->app->unitId
|
||||
);
|
||||
|
||||
// Send bill via email
|
||||
// @bug Not all bills should be sent as email
|
||||
// Depends on bill type and status (i.e. draft, deleted)
|
||||
// https://github.com/Karaka-Management/oms-Billing/issues/50
|
||||
$client = ClientMapper::get()
|
||||
->with('account')
|
||||
->with('attributes')
|
||||
->with('attributes/type')
|
||||
->with('attributes/value')
|
||||
->where('id', $bill->client?->id ?? 0)
|
||||
->where('attributes/type/name', ['bill_emails', 'bill_email_address'], 'IN')
|
||||
->execute();
|
||||
// Add type to media
|
||||
$this->createModelRelation(
|
||||
$request->header->account,
|
||||
$media->id,
|
||||
(int) $internalType->content,
|
||||
MediaMapper::class,
|
||||
'types',
|
||||
'',
|
||||
$request->getOrigin()
|
||||
);
|
||||
|
||||
if ($client->getAttribute('bill_emails')->value->getValue() === 1) {
|
||||
// @todo should this really be a string or an ID for a contact element?
|
||||
$email = empty($tmp = $client->getAttribute('bill_email_address')->value->getValue())
|
||||
? $client->account->email
|
||||
: (string) $tmp;
|
||||
// Add media to bill
|
||||
$this->createModelRelation(
|
||||
$request->header->account,
|
||||
$bill->id,
|
||||
$media->id,
|
||||
BillMapper::class,
|
||||
'files',
|
||||
'',
|
||||
$request->getOrigin()
|
||||
);
|
||||
} else {
|
||||
$media = clone $oldFile;
|
||||
if (\realpath($pdfDir . '/' . $billFileName) !== \realpath($oldFile->getAbsolutePath())) {
|
||||
\unlink($oldFile->getAbsolutePath());
|
||||
}
|
||||
|
||||
$this->sendBillEmail($media, $email, $response->header->l11n->language);
|
||||
|
||||
$media->setPath(\Modules\Media\Controller\ApiController::normalizeDbPath($pdfDir . '/' . $billFileName));
|
||||
$media->setVirtualPath($path);
|
||||
$media->size = \filesize($media->getAbsolutePath());
|
||||
|
||||
$this->updateModel($request->header->account, $oldFile, $media, MediaMapper::class, 'media', $request->getOrigin());
|
||||
}
|
||||
|
||||
// Add type to media
|
||||
/** @var \Model\Setting $originalType */
|
||||
$originalType = $this->app->appSettings->get(
|
||||
names: SettingsEnum::ORIGINAL_MEDIA_TYPE,
|
||||
module: self::NAME
|
||||
);
|
||||
|
||||
$this->createModelRelation(
|
||||
$request->header->account,
|
||||
$media->id,
|
||||
(int) $originalType->content,
|
||||
MediaMapper::class,
|
||||
'types',
|
||||
'',
|
||||
$request->getOrigin()
|
||||
);
|
||||
|
||||
// Add media to bill
|
||||
$this->createModelRelation(
|
||||
$request->header->account,
|
||||
$bill->id,
|
||||
$media->id,
|
||||
BillMapper::class,
|
||||
'files',
|
||||
'',
|
||||
$request->getOrigin()
|
||||
);
|
||||
|
||||
$this->createStandardCreateResponse($request, $response, $media);
|
||||
}
|
||||
|
||||
|
|
@ -1252,44 +1418,47 @@ final class ApiBillController extends Controller
|
|||
*
|
||||
* @param Media $media Media to send
|
||||
* @param string $email Email address
|
||||
* @param string $template Email template
|
||||
* @param string $language Message language
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @question Maybe we should move this entire function to the Messages module
|
||||
* There is nothing bill specific in here.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function sendBillEmail(Media $media, string $email, string $language = 'en') : void
|
||||
public function sendBillEmail(Media $media, string $email, int $template, string $language = 'en') : void
|
||||
{
|
||||
$handler = $this->app->moduleManager->get('Admin', 'Api')->setUpServerMailHandler();
|
||||
|
||||
/** @var \Model\Setting $emailFrom */
|
||||
$emailFrom = $this->app->appSettings->get(
|
||||
names: AdminSettingsEnum::MAIL_SERVER_ADDR,
|
||||
module: 'Admin'
|
||||
);
|
||||
|
||||
if (empty($emailFrom->content)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var \Model\Setting $billingTemplate */
|
||||
$billingTemplate = $this->app->appSettings->get(
|
||||
names: SettingsEnum::BILLING_CUSTOMER_EMAIL_TEMPLATE,
|
||||
module: 'Billing'
|
||||
);
|
||||
|
||||
$mail = EmailMapper::get()
|
||||
->with('l11n')
|
||||
->where('id', (int) $billingTemplate->content)
|
||||
->where('id', $template)
|
||||
->where('l11n/language', $language)
|
||||
->execute();
|
||||
|
||||
$mail = new Email();
|
||||
$mail->setFrom($emailFrom->content);
|
||||
$status = false;
|
||||
if ($mail->id !== 0) {
|
||||
$status = $this->app->moduleManager->get('Admin', 'Api')->setupEmailDefaults($mail, $language);
|
||||
}
|
||||
|
||||
$mail->addTo($email);
|
||||
$mail->addAttachment($media->getAbsolutePath(), $media->name);
|
||||
|
||||
$handler->send($mail);
|
||||
if ($status) {
|
||||
$status = $handler->send($mail);
|
||||
}
|
||||
|
||||
if (!$status) {
|
||||
\phpOMS\Log\FileLogger::getInstance()->error(
|
||||
\phpOMS\Log\FileLogger::MSG_FULL, [
|
||||
'message' => 'Couldn\'t send bill media: ' . $media->id,
|
||||
'line' => __LINE__,
|
||||
'file' => self::class,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ final class ApiBillTypeController extends Controller
|
|||
);
|
||||
$billType->numberFormat = $request->getDataString('number_format') ?? '{id}';
|
||||
$billType->sign = $request->getDataInt('sign') ?? 1;
|
||||
$billType->email = $request->getDataBool('email') ?? false;
|
||||
$billType->transferStock = $request->getDataBool('transfer_stock') ?? false;
|
||||
$billType->isTemplate = $request->getDataBool('is_template') ?? false;
|
||||
$billType->isAccounting = $request->getDataBool('is_accounting') ?? false;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ use Modules\Attribute\Models\NullAttributeValue;
|
|||
use Modules\Billing\Models\Price\NullPrice;
|
||||
use Modules\Billing\Models\Price\Price;
|
||||
use Modules\Billing\Models\Price\PriceMapper;
|
||||
use Modules\Billing\Models\Price\PriceStatus;
|
||||
use Modules\Billing\Models\Price\PriceType;
|
||||
use Modules\ClientManagement\Models\Client;
|
||||
use Modules\ClientManagement\Models\ClientMapper;
|
||||
|
|
@ -28,6 +29,7 @@ use Modules\ItemManagement\Models\ItemMapper;
|
|||
use Modules\ItemManagement\Models\NullItem;
|
||||
use Modules\SupplierManagement\Models\NullSupplier;
|
||||
use Modules\SupplierManagement\Models\Supplier;
|
||||
use Modules\SupplierManagement\Models\SupplierMapper;
|
||||
use phpOMS\Localization\ISO4217CharEnum;
|
||||
use phpOMS\Message\Http\RequestStatusCode;
|
||||
use phpOMS\Message\RequestAbstract;
|
||||
|
|
@ -46,8 +48,12 @@ final class ApiPriceController extends Controller
|
|||
{
|
||||
public function findBestPrice(RequestAbstract $request, ?Item $item = null, ?Client $client = null, ?Supplier $supplier = null)
|
||||
{
|
||||
$item ??= new NullItem();
|
||||
$client ??= new NullClient();
|
||||
$supplier ??= new NullSupplier();
|
||||
|
||||
// Get item
|
||||
if ($item === null && $request->hasData('price_item')) {
|
||||
if ($item->id === 0 && $request->hasData('price_item')) {
|
||||
/** @var null|\Modules\ItemManagement\Models\Item $item */
|
||||
$item = ItemMapper::get()
|
||||
->with('attributes')
|
||||
|
|
@ -59,9 +65,10 @@ final class ApiPriceController extends Controller
|
|||
}
|
||||
|
||||
// Get client
|
||||
if ($client === null && $request->hasData('client')) {
|
||||
if ($client->id === 0 && $request->hasData('client')) {
|
||||
/** @var \Modules\ClientManagement\Models\Client $client */
|
||||
$client = ClientMapper::get()
|
||||
->with('mainAddress')
|
||||
->with('attributes')
|
||||
->with('attributes/type')
|
||||
->with('attributes/value')
|
||||
|
|
@ -71,39 +78,125 @@ final class ApiPriceController extends Controller
|
|||
}
|
||||
|
||||
// Get supplier
|
||||
if ($supplier === null && $request->hasData('supplier')) {
|
||||
$supplier = new NullSupplier($request->getDataInt('supplier'));
|
||||
if ($supplier->id === 0 && $request->hasData('supplier')) {
|
||||
$supplier = SupplierMapper::get()
|
||||
->where('id', $request->getDataInt('supplier'))
|
||||
->execute();
|
||||
}
|
||||
|
||||
$quantity = new FloatInt($request->getDataString('price_quantity') ?? 10000);
|
||||
$quantity = new FloatInt($request->getDataString('price_quantity') ?? FloatInt::DIVISOR);
|
||||
$quantity->value = $quantity->value === 0 ? FloatInt::DIVISOR : $quantity->value;
|
||||
|
||||
// Get all relevant prices
|
||||
$queryMapper = PriceMapper::getAll();
|
||||
$queryMapper = PriceMapper::getAll()
|
||||
->where('status', PriceStatus::ACTIVE);
|
||||
|
||||
if ($request->hasData('price_name')) {
|
||||
$queryMapper->where('name', $request->getData('name'));
|
||||
}
|
||||
|
||||
$queryMapper->where('promocode', \array_unique([$request->getData('promocode'), null]), 'IN');
|
||||
$queryMapper->where('promocode', \array_unique([$request->getDataString('promocode') ?? '', '']), 'IN');
|
||||
|
||||
$queryMapper->where('item', \array_unique([$request->getDataInt('item'), $item?->id, null]), 'IN');
|
||||
$queryMapper->where('itemsalesgroup', \array_unique([$request->getDataInt('sales_group'), $item?->getAttribute('sales_group')->value->getValue(), null]), 'IN');
|
||||
$queryMapper->where('itemproductgroup', \array_unique([$request->getDataInt('product_group'), $item?->getAttribute('product_group')->value->getValue(), null]), 'IN');
|
||||
$queryMapper->where('itemsegment', \array_unique([$request->getDataInt('item_segment'), $item?->getAttribute('segment')->value->getValue(), null]), 'IN');
|
||||
$queryMapper->where('itemsection', \array_unique([$request->getDataInt('item_section'), $item?->getAttribute('section')->value->getValue(), null]), 'IN');
|
||||
$queryMapper->where('itemtype', \array_unique([$request->getDataInt('product_type'), $item?->getAttribute('product_type')->value->getValue(), null]), 'IN');
|
||||
// Item
|
||||
if ($item->id !== 0) {
|
||||
$queryMapper->where('item', $item->id);
|
||||
} elseif ($request->hasData('item')) {
|
||||
$queryMapper->where('item', $request->getDataInt('item'));
|
||||
}
|
||||
|
||||
$queryMapper->where('client', \array_unique([$request->getDataInt('client'), $client?->id, null]), 'IN');
|
||||
$queryMapper->where('clientgroup', \array_unique([$request->getDataInt('client_group'), $client?->getAttribute('client_group')->value->getValue(), null]), 'IN');
|
||||
$queryMapper->where('clientsegment', \array_unique([$request->getDataInt('client_segment'), $client?->getAttribute('segment')->value->getValue(), null]), 'IN');
|
||||
$queryMapper->where('clientsection', \array_unique([$request->getDataInt('client_section'), $client?->getAttribute('section')->value->getValue(), null]), 'IN');
|
||||
$queryMapper->where('clienttype', \array_unique([$request->getDataInt('client_type'), $client?->getAttribute('client_type')->value->getValue(), null]), 'IN');
|
||||
$queryMapper->where('clientcountry', \array_unique([$request->getData('client_region'), $client?->mainAddress->country, null]), 'IN');
|
||||
// Item segment
|
||||
$itemSegment = [null, $request->getDataInt('item_segment')];
|
||||
if ($item->getAttribute('segment')->value->id !== 0) {
|
||||
$itemSegment[] = $item->getAttribute('segment')->value->getValue();
|
||||
}
|
||||
$queryMapper->where('itemsegment', \array_unique($itemSegment), 'IN');
|
||||
|
||||
$queryMapper->where('supplier', \array_unique([$supplier?->id, null]), 'IN');
|
||||
$queryMapper->where('unit', \array_unique([$request->getDataInt('price_unit'), null]), 'IN');
|
||||
$queryMapper->where('type', $request->getDataInt('price_type') ?? (($supplier?->id ?? 0) === 0 ? PriceType::SALES : PriceType::PURCHASE));
|
||||
$queryMapper->where('currency', \array_unique([$request->getDataString('currency'), null]), 'IN');
|
||||
// Item section
|
||||
$itemSection = [null, $request->getDataInt('item_section')];
|
||||
if ($item->getAttribute('section')->value->id !== 0) {
|
||||
$itemSection[] = $item->getAttribute('section')->value->getValue();
|
||||
}
|
||||
$queryMapper->where('itemsection', \array_unique($itemSection), 'IN');
|
||||
|
||||
// Item sales group
|
||||
$itemSalesGroups = [null, $request->getDataInt('sales_group')];
|
||||
if ($item->getAttribute('sales_group')->value->id !== 0) {
|
||||
$itemSalesGroups[] = $item->getAttribute('sales_group')->value->getValue();
|
||||
}
|
||||
$queryMapper->where('itemsalesgroup', \array_unique($itemSalesGroups), 'IN');
|
||||
|
||||
// Item product group
|
||||
$itemProductGroups = [null, $request->getDataInt('product_group')];
|
||||
if ($item->getAttribute('product_group')->value->id !== 0) {
|
||||
$itemProductGroups[] = $item->getAttribute('product_group')->value->getValue();
|
||||
}
|
||||
$queryMapper->where('itemproductgroup', \array_unique($itemProductGroups), 'IN');
|
||||
|
||||
// Item product type
|
||||
$itemProductType = [null, $request->getDataInt('product_type')];
|
||||
if ($item->getAttribute('product_type')->value->id !== 0) {
|
||||
$itemProductType[] = $item->getAttribute('product_type')->value->getValue();
|
||||
}
|
||||
$queryMapper->where('itemtype', \array_unique($itemProductType), 'IN');
|
||||
|
||||
// Client
|
||||
if ($client->id !== 0) {
|
||||
$queryMapper->where('client', $client->id);
|
||||
} elseif ($request->hasData('client')) {
|
||||
$queryMapper->where('client', $request->getDataInt('client'));
|
||||
}
|
||||
|
||||
// Client segment
|
||||
$clientSegment = [null, $request->getDataInt('client_segment')];
|
||||
if ($client->getAttribute('segment')->value->id !== 0) {
|
||||
$clientSegment[] = $client->getAttribute('segment')->value->getValue();
|
||||
}
|
||||
$queryMapper->where('clientsegment', \array_unique($clientSegment), 'IN');
|
||||
|
||||
// Client section
|
||||
$clientSection = [null, $request->getDataInt('client_section')];
|
||||
if ($client->getAttribute('section')->value->id !== 0) {
|
||||
$clientSection[] = $client->getAttribute('section')->value->getValue();
|
||||
}
|
||||
$queryMapper->where('clientsection', \array_unique($clientSection), 'IN');
|
||||
|
||||
// Client group
|
||||
$clientGroup = [null, $request->getDataInt('client_group')];
|
||||
if ($client->getAttribute('client_group')->value->id !== 0) {
|
||||
$clientGroup[] = $client->getAttribute('client_group')->value->getValue();
|
||||
}
|
||||
$queryMapper->where('clientgroup', \array_unique($clientGroup), 'IN');
|
||||
|
||||
// Client type
|
||||
$clientType = [null, $request->getDataInt('client_type')];
|
||||
if ($client->getAttribute('client_type')->value->id !== 0) {
|
||||
$clientType[] = $client->getAttribute('client_type')->value->getValue();
|
||||
}
|
||||
$queryMapper->where('clienttype', \array_unique($clientType), 'IN');
|
||||
|
||||
// Client type
|
||||
$clientCountry = [null, $request->getDataInt('client_region')];
|
||||
if ($client->mainAddress->id !== 0) {
|
||||
$clientCountry[] = $client->mainAddress->country;
|
||||
}
|
||||
$queryMapper->where('clientcountry', \array_unique($clientCountry), 'IN');
|
||||
|
||||
// Supplier
|
||||
if ($supplier->id !== 0) {
|
||||
$queryMapper->where('supplier', $supplier->id);
|
||||
} elseif ($request->hasData('supplier')) {
|
||||
$queryMapper->where('supplier', $request->getDataInt('supplier'));
|
||||
}
|
||||
|
||||
if ($request->hasData('price_unit')) {
|
||||
$queryMapper->where('unit', $request->getDataInt('price_unit'));
|
||||
}
|
||||
|
||||
$queryMapper->where('type', $request->getDataInt('price_type') ?? ($supplier->id === 0 ? PriceType::SALES : PriceType::PURCHASE));
|
||||
|
||||
if ($request->hasData('currency')) {
|
||||
$queryMapper->where('currency', $request->getData('currency'));
|
||||
}
|
||||
|
||||
// @todo implement start and end
|
||||
|
||||
|
|
@ -124,7 +217,8 @@ final class ApiPriceController extends Controller
|
|||
// Find base price
|
||||
$basePrice = null;
|
||||
foreach ($prices as $price) {
|
||||
if ($price->priceNew > 0
|
||||
if (/*$price->priceNew->value > 0 */ // Price could be 0
|
||||
$price->id !== 0
|
||||
&& $price->item->id !== 0
|
||||
&& $price->itemsalesgroup->id === 0
|
||||
&& $price->itemproductgroup->id === 0
|
||||
|
|
@ -177,8 +271,8 @@ final class ApiPriceController extends Controller
|
|||
// 3. subtract bonus effect
|
||||
|
||||
$newPrice -= $price->discount->value;
|
||||
$newPrice = (int) ($newPrice - $price->bonus->value / 10000 * $price->priceNew->value / $quantity->value);
|
||||
$newPrice = (int) ((1000000 - $price->discountPercentage->value) / 1000000 * $newPrice);
|
||||
$newPrice = (int) ($newPrice - $price->bonus->value / FloatInt::DIVISOR * $price->priceNew->value / $quantity->value);
|
||||
$newPrice = (int) (((FloatInt::DIVISOR * 100) - $price->discountPercentage->value) / (FloatInt::DIVISOR * 100) * $newPrice);
|
||||
|
||||
// @todo If a customer receives 1+1 but purchases 2, then he gets 2+2 (if multiply === true) which is better than 1+1 with multiply false.
|
||||
// Same goes for amount discounts?
|
||||
|
|
@ -189,13 +283,13 @@ final class ApiPriceController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
if ($bestPrice->price->value === 0) {
|
||||
if ($bestPrice->priceNew->value === 0) {
|
||||
$discounts[] = clone $bestPrice;
|
||||
$bestPrice = $basePrice;
|
||||
}
|
||||
|
||||
// Actual price calculation
|
||||
$bestActualPriceValue = $bestPrice?->price->value ?? \PHP_INT_MAX;
|
||||
$bestActualPriceValue = $bestPrice?->priceNew->value ?? \PHP_INT_MAX;
|
||||
|
||||
$discountAmount = $bestPrice->discount->value;
|
||||
$discountPercentage = $bestPrice->discountPercentage->value;
|
||||
|
|
@ -210,11 +304,12 @@ final class ApiPriceController extends Controller
|
|||
}
|
||||
|
||||
$bestActualPriceValue -= $discountAmount;
|
||||
$bestActualPriceValue = (int) \round((1000000 - $discountPercentage) / 1000000 * $bestActualPriceValue, 0);
|
||||
$bestActualPriceValue = (int) \round(((FloatInt::DIVISOR * 100) - $discountPercentage) / (FloatInt::DIVISOR * 100) * $bestActualPriceValue, 0);
|
||||
|
||||
return [
|
||||
'basePrice' => $basePrice->price,
|
||||
'bestPrice' => $bestPrice->price,
|
||||
'basePrice' => $basePrice->priceNew,
|
||||
'bestPrice' => $bestPrice->priceNew,
|
||||
'supplier' => $bestPrice->supplier->id,
|
||||
'bestActualPrice' => new FloatInt($bestActualPriceValue),
|
||||
'discounts' => $discounts,
|
||||
'discountPercent' => new FloatInt($discountPercentage),
|
||||
|
|
@ -379,6 +474,8 @@ final class ApiPriceController extends Controller
|
|||
? ($request->getDataString('name') ?? $new->name)
|
||||
: $new->name;
|
||||
|
||||
$new->status = PriceStatus::tryFromValue($request->getDataInt('type')) ?? $new->status;
|
||||
|
||||
$new->promocode = $request->getDataString('promocode') ?? $new->promocode;
|
||||
|
||||
$new->itemsalesgroup = $request->hasData('itemsalesgroup') ? new NullAttributeValue((int) $request->getData('itemsalesgroup')) : $new->itemsalesgroup;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
|
|
@ -15,6 +14,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Modules\Billing\Controller;
|
||||
|
||||
use Modules\Billing\Models\BillMapper;
|
||||
use Modules\Billing\Models\BillStatus;
|
||||
use Modules\Billing\Models\BillTransferType;
|
||||
use Modules\Billing\Models\BillTypeMapper;
|
||||
|
|
@ -51,17 +51,54 @@ final class ApiPurchaseController extends Controller
|
|||
*
|
||||
* @throws \Exception
|
||||
*
|
||||
* apiSupplierBillUpload
|
||||
* -> apiBillCreate
|
||||
* -> [createBill]
|
||||
* -> apiMediaAddToBill
|
||||
* -> apiInvoiceParse
|
||||
* -> cliParseSupplierBill
|
||||
* -> [updateBill]
|
||||
* -> apiBillPdfArchiveCreate
|
||||
* -> eventBillArchive
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function apiSupplierBillUpload(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
|
||||
{
|
||||
if (!empty($val = $this->validateSupplierBillUpload($request))) {
|
||||
$response->header->status = RequestStatusCode::R_400;
|
||||
$this->createInvalidCreateResponse($request, $response, $val);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$bills = $this->createSupplierBillUploadFromRequest($request, $response, $data);
|
||||
if (empty($bills)) {
|
||||
$response->header->status = RequestStatusCode::R_400;
|
||||
$this->createInvalidCreateResponse($request, $response, $val);
|
||||
}
|
||||
|
||||
$this->createStandardCreateResponse($request, $response, $bills);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to create item attribute from request.
|
||||
*
|
||||
* @param RequestAbstract $request Request
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function createSupplierBillUploadFromRequest(RequestAbstract $request, ResponseAbstract $response, $data) : array
|
||||
{
|
||||
/** @var \Model\Setting $setting */
|
||||
$setting = $this->app->appSettings->get(
|
||||
names: SettingsEnum::ORIGINAL_MEDIA_TYPE,
|
||||
names: SettingsEnum::EXTERNAL_MEDIA_TYPE,
|
||||
module: self::NAME
|
||||
);
|
||||
|
||||
$originalType = $request->getDataInt('type') ?? ((int) $setting->content);
|
||||
$internalType = $request->getDataInt('type') ?? ((int) $setting->content);
|
||||
|
||||
/** @var \Modules\Billing\Models\BillType $purchaseTransferType */
|
||||
$purchaseTransferType = BillTypeMapper::get()
|
||||
|
|
@ -105,7 +142,7 @@ final class ApiPurchaseController extends Controller
|
|||
}
|
||||
|
||||
$mediaRequest->setData('bill', $billId);
|
||||
$mediaRequest->setData('type', $originalType);
|
||||
$mediaRequest->setData('type', $internalType);
|
||||
$mediaRequest->setData('parse_content', true, true);
|
||||
$this->app->moduleManager->get('Billing', 'ApiBill')->apiMediaAddToBill($mediaRequest, $mediaResponse, $data);
|
||||
|
||||
|
|
@ -113,46 +150,121 @@ final class ApiPurchaseController extends Controller
|
|||
/** @var \Modules\Media\Models\Media[] $uploaded */
|
||||
$uploaded = $mediaResponse->getDataArray('')['response']['upload'];
|
||||
if (empty($uploaded)) {
|
||||
$response->header->status = RequestStatusCode::R_400;
|
||||
throw new \Exception();
|
||||
return [];
|
||||
}
|
||||
|
||||
$in = \reset($uploaded)->getAbsolutePath();
|
||||
if (!\is_file($in)) {
|
||||
$response->header->status = RequestStatusCode::R_400;
|
||||
throw new \Exception();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Create internal document
|
||||
$billResponse = new HttpResponse();
|
||||
$billRequest = new HttpRequest();
|
||||
$request->setData('id', $billId, true);
|
||||
$request->setData('bill', $billId, true);
|
||||
|
||||
$billRequest->header->account = $request->header->account;
|
||||
$billRequest->setData('bill', $billId);
|
||||
|
||||
$this->app->moduleManager->get('Billing', 'ApiBill')->apiBillPdfArchiveCreate($billRequest, $billResponse);
|
||||
|
||||
// Offload bill parsing to cli
|
||||
$cliPath = \realpath(__DIR__ . '/../../../cli.php');
|
||||
if ($cliPath === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SystemUtils::runProc(
|
||||
OperatingSystem::getSystem() === SystemType::WIN ? 'php.exe' : 'php',
|
||||
\escapeshellarg($cliPath)
|
||||
. ' /billing/bill/purchase/parse '
|
||||
. '-i ' . \escapeshellarg((string) $billId),
|
||||
$request->getDataBool('async') ?? true
|
||||
);
|
||||
} catch (\Throwable $t) {
|
||||
$response->header->status = RequestStatusCode::R_400;
|
||||
$this->app->logger->error($t->getMessage());
|
||||
}
|
||||
|
||||
$this->createStandardCreateResponse($request, $response, $bills);
|
||||
$this->apiInvoiceParse($request, $response, $data);
|
||||
}
|
||||
|
||||
return $bills;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate item attribute create request
|
||||
*
|
||||
* @param RequestAbstract $request Request
|
||||
*
|
||||
* @return array<string, bool>
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function validateSupplierBillUpload(RequestAbstract $request) : array
|
||||
{
|
||||
$val = [];
|
||||
if (($val['files'] = empty($request->files))) {
|
||||
return $val;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Api method to create bill files
|
||||
*
|
||||
* @param RequestAbstract $request Request
|
||||
* @param ResponseAbstract $response Response
|
||||
* @param array $data Generic data
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @throws \Exception
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function apiInvoiceParse(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
|
||||
{
|
||||
if (!empty($val = $this->validateInvoiceParse($request))) {
|
||||
$response->header->status = RequestStatusCode::R_400;
|
||||
$this->createInvalidCreateResponse($request, $response, $val);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$bill = BillMapper::get()
|
||||
->where('id', (int) $request->getData('id'))
|
||||
->execute();
|
||||
|
||||
// After a bill is "closed" its values shouldn't change
|
||||
if ($bill->status !== BillStatus::DRAFT
|
||||
&& $bill->status !== BillStatus::UNPARSED
|
||||
&& $bill->status !== BillStatus::ACTIVE
|
||||
) {
|
||||
$response->header->status = RequestStatusCode::R_423;
|
||||
$this->createInvalidCreateResponse($request, $response, $val);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Offload bill parsing to cli
|
||||
$cliPath = \realpath(__DIR__ . '/../../../cli.php');
|
||||
if ($cliPath === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SystemUtils::runProc(
|
||||
OperatingSystem::getSystem() === SystemType::WIN ? 'php.exe' : 'php',
|
||||
'-dxdebug.remote_enable=1 -dxdebug.start_with_request=yes -dxdebug.mode=coverage,develop,debug ' .
|
||||
\escapeshellarg($cliPath)
|
||||
. ' /billing/bill/purchase/parse '
|
||||
. '-i ' . \escapeshellarg((string) $bill->id),
|
||||
$request->getDataBool('async') ?? true
|
||||
);
|
||||
} catch (\Throwable $t) {
|
||||
$response->header->status = RequestStatusCode::R_400;
|
||||
$this->app->logger->error($t->getMessage());
|
||||
}
|
||||
|
||||
$this->createStandardUpdateResponse($request, $response, $bill);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate item attribute create request
|
||||
*
|
||||
* @param RequestAbstract $request Request
|
||||
*
|
||||
* @return array<string, bool>
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function validateInvoiceParse(RequestAbstract $request) : array
|
||||
{
|
||||
$val = [];
|
||||
if (($val['id'] = !$request->hasData('id'))) {
|
||||
return $val;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,12 +22,14 @@ use Modules\Billing\Models\BillTransferType;
|
|||
use Modules\Billing\Models\BillTypeMapper;
|
||||
use Modules\Billing\Models\PaymentTermL11nMapper;
|
||||
use Modules\Billing\Models\PaymentTermMapper;
|
||||
use Modules\Billing\Models\PermissionCategory;
|
||||
use Modules\Billing\Models\PurchaseBillMapper;
|
||||
use Modules\Billing\Models\SalesBillMapper;
|
||||
use Modules\Billing\Models\SettingsEnum;
|
||||
use Modules\Billing\Models\ShippingTermL11nMapper;
|
||||
use Modules\Billing\Models\ShippingTermMapper;
|
||||
use Modules\Billing\Models\StockBillMapper;
|
||||
use phpOMS\Account\PermissionType;
|
||||
use phpOMS\Contract\RenderableInterface;
|
||||
use phpOMS\DataStorage\Database\Query\OrderType;
|
||||
use phpOMS\Message\RequestAbstract;
|
||||
|
|
@ -71,6 +73,7 @@ final class BackendController extends Controller
|
|||
->where('type/transferType', BillTransferType::SALES)
|
||||
->where('type/l11n/language', $response->header->l11n->language)
|
||||
->sort('id', OrderType::DESC)
|
||||
->where('unit', $this->app->unitId)
|
||||
->limit(25);
|
||||
|
||||
if ($request->getData('ptype') === 'p') {
|
||||
|
|
@ -131,26 +134,38 @@ final class BackendController extends Controller
|
|||
|
||||
$view->data['billtypes'] = $billTypes;
|
||||
|
||||
/** @var \Modules\Auditor\Models\Audit[] $logsBill */
|
||||
$logs = AuditMapper::getAll()
|
||||
->with('createdBy')
|
||||
->where('module', 'Billing')
|
||||
->where('type', StringUtils::intHash(BillMapper::class))
|
||||
->where('ref', $bill->id)
|
||||
->execute();
|
||||
|
||||
if (!empty($bill->elements)) {
|
||||
/** @var \Modules\Auditor\Models\Audit[] $logsElements */
|
||||
$logsElements = AuditMapper::getAll()
|
||||
$logs = [];
|
||||
if ($this->app->accountManager->get($request->header->account)->hasPermission(
|
||||
PermissionType::READ,
|
||||
$this->app->unitId,
|
||||
null,
|
||||
self::NAME,
|
||||
PermissionCategory::BILL_LOG,
|
||||
)
|
||||
) {
|
||||
/** @var \Modules\Auditor\Models\Audit[] $logsBill */
|
||||
$logs = AuditMapper::getAll()
|
||||
->with('createdBy')
|
||||
->where('module', 'Billing')
|
||||
->where('type', StringUtils::intHash(BillElementMapper::class))
|
||||
->where('ref', \array_keys($bill->elements), 'IN')
|
||||
->where('type', StringUtils::intHash(BillMapper::class))
|
||||
->where('ref', $bill->id)
|
||||
->execute();
|
||||
|
||||
$logs = \array_merge($logs, $logsElements);
|
||||
if (!empty($bill->elements)) {
|
||||
/** @var \Modules\Auditor\Models\Audit[] $logsElements */
|
||||
$logsElements = AuditMapper::getAll()
|
||||
->with('createdBy')
|
||||
->where('module', 'Billing')
|
||||
->where('type', StringUtils::intHash(BillElementMapper::class))
|
||||
->where('ref', \array_keys($bill->elements), 'IN')
|
||||
->execute();
|
||||
|
||||
$logs = \array_merge($logs, $logsElements);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
$view->data['logs'] = $logs;
|
||||
$view->data['media-upload'] = new \Modules\Media\Theme\Backend\Components\Upload\BaseView($this->app->l11nManager, $request, $response);
|
||||
|
||||
|
|
@ -255,6 +270,7 @@ final class BackendController extends Controller
|
|||
->with('supplier')
|
||||
->where('type/transferType', BillTransferType::PURCHASE)
|
||||
->sort('id', OrderType::DESC)
|
||||
->where('unit', $this->app->unitId)
|
||||
->limit(25);
|
||||
|
||||
if ($request->getData('ptype') === 'p') {
|
||||
|
|
@ -313,32 +329,34 @@ final class BackendController extends Controller
|
|||
->where('l11n/language', $request->header->l11n->language)
|
||||
->execute();
|
||||
|
||||
/** @var \Model\Setting $originalType */
|
||||
$originalType = $this->app->appSettings->get(
|
||||
names: SettingsEnum::ORIGINAL_MEDIA_TYPE,
|
||||
module: self::NAME
|
||||
);
|
||||
|
||||
$view->data['originalType'] = (int) $originalType->content;
|
||||
|
||||
/** @var \Modules\Auditor\Models\Audit[] $logs */
|
||||
$logs = AuditMapper::getAll()
|
||||
->with('createdBy')
|
||||
->where('module', 'Billing')
|
||||
->where('type', StringUtils::intHash(BillMapper::class))
|
||||
->where('ref', $view->data['bill']->id)
|
||||
->execute();
|
||||
|
||||
if (!empty($view->data['bill']->elements)) {
|
||||
/** @var \Modules\Auditor\Models\Audit[] $logsElements */
|
||||
$logsElements = AuditMapper::getAll()
|
||||
$logs = [];
|
||||
if ($this->app->accountManager->get($request->header->account)->hasPermission(
|
||||
PermissionType::READ,
|
||||
$this->app->unitId,
|
||||
null,
|
||||
self::NAME,
|
||||
PermissionCategory::BILL_LOG,
|
||||
)
|
||||
) {
|
||||
/** @var \Modules\Auditor\Models\Audit[] $logs */
|
||||
$logs = AuditMapper::getAll()
|
||||
->with('createdBy')
|
||||
->where('module', 'Billing')
|
||||
->where('type', StringUtils::intHash(BillElementMapper::class))
|
||||
->where('ref', \array_keys($view->data['bill']->elements), 'IN')
|
||||
->where('type', StringUtils::intHash(BillMapper::class))
|
||||
->where('ref', $view->data['bill']->id)
|
||||
->execute();
|
||||
|
||||
$logs = \array_merge($logs, $logsElements);
|
||||
if (!empty($view->data['bill']->elements)) {
|
||||
/** @var \Modules\Auditor\Models\Audit[] $logsElements */
|
||||
$logsElements = AuditMapper::getAll()
|
||||
->with('createdBy')
|
||||
->where('module', 'Billing')
|
||||
->where('type', StringUtils::intHash(BillElementMapper::class))
|
||||
->where('ref', \array_keys($view->data['bill']->elements), 'IN')
|
||||
->execute();
|
||||
|
||||
$logs = \array_merge($logs, $logsElements);
|
||||
}
|
||||
}
|
||||
|
||||
$view->data['logs'] = $logs;
|
||||
|
|
@ -366,11 +384,11 @@ final class BackendController extends Controller
|
|||
$view->data['nav'] = $this->app->moduleManager->get('Navigation')->createNavigationMid(1005106001, $request, $response);
|
||||
|
||||
if ($request->getData('ptype') === 'p') {
|
||||
$view->data['bills'] = StockBillMapper::getAll()->where('id', $request->getDataInt('id') ?? 0, '<')->limit(25)->execute();
|
||||
$view->data['bills'] = StockBillMapper::getAll()->where('id', $request->getDataInt('id') ?? 0, '<')->where('unit', $this->app->unitId)->limit(25)->execute();
|
||||
} elseif ($request->getData('ptype') === 'n') {
|
||||
$view->data['bills'] = StockBillMapper::getAll()->where('id', $request->getDataInt('id') ?? 0, '>')->limit(25)->execute();
|
||||
$view->data['bills'] = StockBillMapper::getAll()->where('id', $request->getDataInt('id') ?? 0, '>')->where('unit', $this->app->unitId)->limit(25)->execute();
|
||||
} else {
|
||||
$view->data['bills'] = StockBillMapper::getAll()->where('id', 0, '>')->limit(25)->execute();
|
||||
$view->data['bills'] = StockBillMapper::getAll()->where('id', 0, '>')->where('unit', $this->app->unitId)->limit(25)->execute();
|
||||
}
|
||||
|
||||
return $view;
|
||||
|
|
@ -459,7 +477,7 @@ final class BackendController extends Controller
|
|||
public function viewPrivatePurchaseBillDashboard(RequestAbstract $request, ResponseAbstract $response, array $data = []) : RenderableInterface
|
||||
{
|
||||
$view = new View($this->app->l11nManager, $request, $response);
|
||||
$view->setTemplate('/Modules/Billing/Theme/Backend/user-purchase-bill-dashboard');
|
||||
$view->setTemplate('/Modules/Billing/Theme/Backend/purchase-bill-list');
|
||||
$view->data['nav'] = $this->app->moduleManager->get('Navigation')->createNavigationMid(1005109001, $request, $response);
|
||||
|
||||
$mapperQuery = PurchaseBillMapper::getAll()
|
||||
|
|
@ -469,6 +487,7 @@ final class BackendController extends Controller
|
|||
->where('type/transferType', BillTransferType::PURCHASE)
|
||||
->where('status', BillStatus::UNPARSED)
|
||||
->sort('id', OrderType::DESC)
|
||||
->where('unit', $this->app->unitId)
|
||||
->limit(25);
|
||||
|
||||
if ($request->getData('ptype') === 'p') {
|
||||
|
|
@ -504,7 +523,7 @@ final class BackendController extends Controller
|
|||
public function viewPrivateBillingPurchaseInvoice(RequestAbstract $request, ResponseAbstract $response, array $data = []) : RenderableInterface
|
||||
{
|
||||
$view = new View($this->app->l11nManager, $request, $response);
|
||||
$view->setTemplate('/Modules/Billing/Theme/Backend/user-purchase-bill');
|
||||
$view->setTemplate('/Modules/Billing/Theme/Backend/purchase-bill');
|
||||
$view->data['nav'] = $this->app->moduleManager->get('Navigation')->createNavigationMid(1005109001, $request, $response);
|
||||
|
||||
$bill = PurchaseBillMapper::get()
|
||||
|
|
@ -525,13 +544,13 @@ final class BackendController extends Controller
|
|||
|
||||
$view->data['previewType'] = (int) $previewType->content;
|
||||
|
||||
/** @var \Model\Setting $originalType */
|
||||
$originalType = $this->app->appSettings->get(
|
||||
names: SettingsEnum::ORIGINAL_MEDIA_TYPE,
|
||||
/** @var \Model\Setting $externalType */
|
||||
$externalType = $this->app->appSettings->get(
|
||||
names: SettingsEnum::EXTERNAL_MEDIA_TYPE,
|
||||
module: self::NAME
|
||||
);
|
||||
|
||||
$view->data['originalType'] = (int) $originalType->content;
|
||||
$view->data['externalType'] = (int) $externalType->content;
|
||||
$view->data['media-upload'] = new \Modules\Media\Theme\Backend\Components\Upload\BaseView($this->app->l11nManager, $request, $response);
|
||||
|
||||
return $view;
|
||||
|
|
|
|||
|
|
@ -14,8 +14,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace Modules\Billing\Controller;
|
||||
|
||||
use Modules\Billing\Models\BillElement;
|
||||
use Modules\Billing\Models\BillElementMapper;
|
||||
use Modules\Billing\Models\BillMapper;
|
||||
use Modules\Billing\Models\BillTypeMapper;
|
||||
use Modules\Billing\Models\InvoiceRecognition;
|
||||
use Modules\Billing\Models\NullBillType;
|
||||
use Modules\Billing\Models\SettingsEnum;
|
||||
use Modules\Payment\Models\PaymentType;
|
||||
|
|
@ -23,7 +26,11 @@ use Modules\SupplierManagement\Models\NullSupplier;
|
|||
use Modules\SupplierManagement\Models\Supplier;
|
||||
use Modules\SupplierManagement\Models\SupplierMapper;
|
||||
use phpOMS\Contract\RenderableInterface;
|
||||
use phpOMS\Localization\LanguageDetection\Language;
|
||||
use phpOMS\Localization\ISO4217CharEnum;
|
||||
use phpOMS\Localization\ISO4217DecimalEnum;
|
||||
use phpOMS\Localization\Localization;
|
||||
use phpOMS\Message\Http\HttpRequest;
|
||||
use phpOMS\Message\Http\HttpResponse;
|
||||
use phpOMS\Message\RequestAbstract;
|
||||
use phpOMS\Message\ResponseAbstract;
|
||||
use phpOMS\Stdlib\Base\FloatInt;
|
||||
|
|
@ -53,31 +60,53 @@ final class CliController extends Controller
|
|||
*/
|
||||
public function cliParseSupplierBill(RequestAbstract $request, ResponseAbstract $response, array $data = []) : RenderableInterface
|
||||
{
|
||||
$view = new View($this->app->l11nManager, $request, $response);
|
||||
$view->setTemplate('/Modules/Billing/Theme/Cli/bill-parsed');
|
||||
|
||||
/** @var \Model\Setting $setting */
|
||||
$setting = $this->app->appSettings->get(
|
||||
names: SettingsEnum::ORIGINAL_MEDIA_TYPE,
|
||||
names: SettingsEnum::EXTERNAL_MEDIA_TYPE,
|
||||
module: self::NAME
|
||||
);
|
||||
|
||||
$originalType = $request->getDataInt('type') ?? (int) $setting->content;
|
||||
$externalType = $request->getDataInt('-t') ?? (int) $setting->content;
|
||||
|
||||
/** @var \Modules\Billing\Models\Bill $bill */
|
||||
$bill = BillMapper::get()
|
||||
->with('elements')
|
||||
->with('files')
|
||||
->with('media/types')
|
||||
->with('media/content')
|
||||
->where('id', (int) $request->getData('i'))
|
||||
->where('media/types/id', $originalType)
|
||||
->with('files/types')
|
||||
->with('files/content')
|
||||
->where('id', (int) $request->getData('-i'))
|
||||
->where('files/types', $externalType)
|
||||
->execute();
|
||||
|
||||
if ($bill->id === 0) {
|
||||
return $view;
|
||||
}
|
||||
|
||||
$old = clone $bill;
|
||||
|
||||
$content = \strtolower($bill->getFileByType($originalType)->content->content ?? '');
|
||||
$content = \strtolower($bill->getFileByType($externalType)->content->content ?? '');
|
||||
$lines = \explode("\n", $content);
|
||||
foreach ($lines as $line => $value) {
|
||||
if (empty(\trim($value))) {
|
||||
unset($lines[$line]);
|
||||
}
|
||||
}
|
||||
|
||||
$lines = \array_values($lines);
|
||||
|
||||
$language = InvoiceRecognition::detectLanguage($content);
|
||||
|
||||
if (!\in_array($language, ['en', 'de'])) {
|
||||
$language = 'en';
|
||||
}
|
||||
|
||||
$language = $this->detectLanguage($content);
|
||||
$bill->language = $language;
|
||||
|
||||
$l11n = Localization::fromLanguage($language);
|
||||
|
||||
$identifierContent = \file_get_contents(__DIR__ . '/../Models/bill_identifier.json');
|
||||
if ($identifierContent === false) {
|
||||
$identifierContent = '{}';
|
||||
|
|
@ -99,11 +128,32 @@ final class CliController extends Controller
|
|||
$bill->supplier = new NullSupplier($supplierId);
|
||||
$supplier = $suppliers[$supplierId] ?? new NullSupplier();
|
||||
|
||||
$bill->billTo = $supplier->account->name1;
|
||||
$bill->billAddress = $supplier->mainAddress->address;
|
||||
$bill->billCity = $supplier->mainAddress->city;
|
||||
$bill->billZip = $supplier->mainAddress->postal;
|
||||
$bill->billCountry = $supplier->mainAddress->country;
|
||||
if ($supplier->id !== 0) {
|
||||
$bill->billTo = $supplier->account->name1;
|
||||
$bill->billAddress = $supplier->mainAddress->address;
|
||||
$bill->billCity = $supplier->mainAddress->city;
|
||||
$bill->billZip = $supplier->mainAddress->postal;
|
||||
$bill->billCountry = $supplier->mainAddress->country;
|
||||
} else {
|
||||
$bill->billCountry = InvoiceRecognition::findCountry($lines, $identifiers, $language);
|
||||
}
|
||||
|
||||
$currency = InvoiceRecognition::findCurrency($lines);
|
||||
$countryCurrency = ISO4217CharEnum::currencyFromCountry($bill->billCountry);
|
||||
|
||||
// Identified currency has to be country currency or one of the top globally used currencies
|
||||
if ($currency !== \in_array($currency, [
|
||||
$countryCurrency, ISO4217CharEnum::_USD, ISO4217CharEnum::_EUR, ISO4217CharEnum::_JPY,
|
||||
ISO4217CharEnum::_GBP, ISO4217CharEnum::_AUD, ISO4217CharEnum::_CAD, ISO4217CharEnum::_CHF,
|
||||
ISO4217CharEnum::_CNH, ISO4217CharEnum::_CNY
|
||||
])
|
||||
) {
|
||||
$currency = $countryCurrency;
|
||||
}
|
||||
|
||||
$bill->currency = $currency;
|
||||
|
||||
$rd = -FloatInt::MAX_DECIMALS + ISO4217DecimalEnum::getByName('_' . $bill->currency);
|
||||
|
||||
/* Type */
|
||||
$type = $this->findSupplierInvoiceType($content, $identifiers['type'], $language);
|
||||
|
|
@ -116,65 +166,282 @@ final class CliController extends Controller
|
|||
$bill->type = new NullBillType($billType->id);
|
||||
|
||||
/* Number */
|
||||
$billNumber = $this->findBillNumber($lines, $identifiers['bill_no'][$language]);
|
||||
$bill->number = $billNumber;
|
||||
$billNumber = InvoiceRecognition::findBillNumber($lines, $identifiers['bill_no'][$language]);
|
||||
$bill->external = $billNumber;
|
||||
|
||||
/* Reference / PO */
|
||||
// @todo implement
|
||||
|
||||
/* Date */
|
||||
$billDateTemp = $this->findBillDate($lines, $identifiers['bill_date'][$language]);
|
||||
$billDate = $this->parseDate($billDateTemp, $supplier, $identifiers['date_format']);
|
||||
$billDateTemp = InvoiceRecognition::findBillDate($lines, $identifiers['bill_date'][$language]);
|
||||
$billDate = InvoiceRecognition::parseDate($billDateTemp, $identifiers['date_format'], $supplier->getAttribute('bill_date_format')->value->valueStr ?? '');
|
||||
|
||||
$bill->billDate = $billDate;
|
||||
|
||||
/* Due */
|
||||
$billDueTemp = $this->findBillDue($lines, $identifiers['bill_date'][$language]);
|
||||
$billDue = $this->parseDate($billDueTemp, $supplier, $identifiers['date_format']);
|
||||
$billDueTemp = InvoiceRecognition::findBillDue($lines, $identifiers['bill_due'][$language]);
|
||||
$billDue = InvoiceRecognition::parseDate($billDueTemp, $identifiers['date_format'], $supplier->getAttribute('bill_date_format')->value->valueStr ?? '');
|
||||
// @todo implement multiple due dates for bills
|
||||
|
||||
/* Total Net */
|
||||
$totalNet = $this->findBillNet($lines, $identifiers['total_net'][$language]);
|
||||
$bill->netCosts = new FloatInt($totalNet);
|
||||
/* Total */
|
||||
$totalGross = InvoiceRecognition::findBillGross($lines, $identifiers['total_gross'][$language]);
|
||||
$totalNet = InvoiceRecognition::findBillNet($lines, $identifiers['total_net'][$language]);
|
||||
|
||||
// The number format needs to be corrected:
|
||||
// Languages don't always respect the l11n number format
|
||||
// Sometimes parsing errors can happen
|
||||
$format = FloatInt::identifyNumericFormat($totalGross);
|
||||
|
||||
if ($format !== null) {
|
||||
$l11n->thousands = $format['thousands'];
|
||||
$l11n->decimal = $format['decimal'];
|
||||
}
|
||||
|
||||
$bill->grossSales = new FloatInt($totalGross, $l11n->thousands, $l11n->decimal);
|
||||
$bill->netSales = new FloatInt($totalNet, $l11n->thousands, $l11n->decimal);
|
||||
|
||||
/* Total Tax */
|
||||
$totalTaxAmount = $this->findBillTaxAmount($lines, $identifiers['total_net'][$language]);
|
||||
// @todo taxes depend on local tax id (if company in Germany but invoice from US -> only gross amount important, there is no net)
|
||||
$totalTaxAmount = InvoiceRecognition::findBillTaxAmount($lines, $identifiers['total_tax'][$language]);
|
||||
$taxRates = InvoiceRecognition::findBillTaxRates($lines, $identifiers['tax_rate'][$language]);
|
||||
|
||||
/* Total Gross */
|
||||
$totalGross = $this->findBillGross($lines, $identifiers['total_gross'][$language]);
|
||||
$bill->grossCosts = new FloatInt($totalGross);
|
||||
if ($bill->netSales->value === 0) {
|
||||
$bill->netSales->value = $taxRates === 0
|
||||
? $bill->grossSales->value
|
||||
: (int) \round($bill->grossSales->value / (1.0 + $taxRates / (FloatInt::DIVISOR * 100)), $rd);
|
||||
}
|
||||
|
||||
if ($bill->grossSales->value === 0) {
|
||||
$bill->grossSales->value = $taxRates === 0
|
||||
? $bill->netSales->value
|
||||
: $bill->netSales->value + ((int) \round($bill->netSales->value * $taxRates / (FloatInt::DIVISOR * 100), $rd));
|
||||
}
|
||||
|
||||
// We just assume that finding the net sales value is more likely
|
||||
// If this turns out to be false, we need to recalculate the netSales from the grossSales instead
|
||||
if ($bill->grossSales->value === $bill->netSales->value) {
|
||||
$bill->grossSales->value = $bill->netSales->value + ((int) \round($bill->netSales->value * $taxRates / (FloatInt::DIVISOR * 100), $rd));
|
||||
}
|
||||
|
||||
if ($taxRates === 0 && $bill->netSales->value !== $bill->grossSales->value) {
|
||||
$taxRates = ((int) ($bill->grossSales->value / ($bill->grossSales->value / FloatInt::DIVISOR))) - FloatInt::DIVISOR;
|
||||
}
|
||||
|
||||
/* Item lines */
|
||||
$itemLines = $this->findBillItemLines($lines, $identifiers['item_table'][$language]);
|
||||
$itemLines = InvoiceRecognition::findBillItemLines($lines, $identifiers['item_table'][$language]);
|
||||
|
||||
// @todo Try to find item from item database
|
||||
// @todo Some of the element value setting is unnecessary as it happens also in the recalculatePrices()
|
||||
// Same goes for the bill element creations further down below
|
||||
if (empty($bill->elements)) {
|
||||
$itemLineEnd = 0;
|
||||
foreach ($itemLines as $line => $itemLine) {
|
||||
$itemLineEnd = $line;
|
||||
|
||||
$billElement = new BillElement();
|
||||
$billElement->bill = $bill;
|
||||
|
||||
$billElement->taxR->value = $taxRates;
|
||||
|
||||
if (isset($itemLine['description'])) {
|
||||
$billElement->itemName = \trim($itemLine['description']);
|
||||
}
|
||||
|
||||
if (isset($itemLine['quantity'])) {
|
||||
$billElement->quantity = new FloatInt($itemLine['quantity'], $l11n->thousands, $l11n->decimal);
|
||||
}
|
||||
|
||||
// Unit
|
||||
if (isset($itemLine['price'])) {
|
||||
$billElement->singleListPriceNet = new FloatInt($itemLine['price'], $l11n->thousands, $l11n->decimal);
|
||||
|
||||
$billElement->singleSalesPriceNet = $billElement->singleListPriceNet;
|
||||
$billElement->singlePurchasePriceNet = $billElement->singleSalesPriceNet;
|
||||
|
||||
if ($billElement->taxR->value > 0) {
|
||||
$billElement->singleListPriceGross->value = $billElement->singleListPriceNet->value + ((int) \round($billElement->singleSalesPriceNet->value * $billElement->taxR->value / (FloatInt::DIVISOR * 100), $rd));
|
||||
$billElement->singleSalesPriceGross = $billElement->singleListPriceGross;
|
||||
} else {
|
||||
$billElement->singleListPriceGross = $billElement->singleListPriceNet;
|
||||
$billElement->singleSalesPriceGross = $billElement->singleListPriceGross;
|
||||
}
|
||||
}
|
||||
|
||||
// Total
|
||||
if (isset($itemLine['total'])) {
|
||||
$billElement->totalListPriceNet = new FloatInt($itemLine['total'], $l11n->thousands, $l11n->decimal);
|
||||
|
||||
$billElement->totalSalesPriceNet = $billElement->totalListPriceNet;
|
||||
$billElement->totalPurchasePriceNet = $billElement->totalSalesPriceNet;
|
||||
|
||||
if ($billElement->taxR->value > 0) {
|
||||
$billElement->totalListPriceGross->value = $billElement->totalListPriceNet->value + ((int) \round($billElement->totalSalesPriceNet->value * $billElement->taxR->value / (FloatInt::DIVISOR * 100), $rd));
|
||||
$billElement->totalSalesPriceGross = $billElement->totalListPriceGross;
|
||||
} else {
|
||||
$billElement->totalListPriceGross = $billElement->totalListPriceNet;
|
||||
$billElement->totalSalesPriceGross = $billElement->totalListPriceGross;
|
||||
}
|
||||
}
|
||||
|
||||
$billElement->taxP->value = $billElement->totalSalesPriceGross->value - $billElement->totalSalesPriceNet->value;
|
||||
|
||||
$billElement->recalculatePrices();
|
||||
$bill->elements[] = $billElement;
|
||||
|
||||
$this->createModel($request->header->account, $billElement, BillElementMapper::class, 'bill_element', $request->getOrigin());
|
||||
}
|
||||
|
||||
/* Total Special */
|
||||
// @question How do we want to apply total discounts?
|
||||
// Option 1: Apply in relation to the amount per line item (this would be correct for stock evaluation)
|
||||
// Option 2: Additional element (For correct stock evaluation we could do a internal/backend correction in the lot price calculation)
|
||||
//
|
||||
// Option 2 seems nicer from a user perspective!
|
||||
$totalSpecial = InvoiceRecognition::findBillSpecial($lines, $identifiers, $language, $itemLineEnd);
|
||||
foreach ($totalSpecial as $key => $amount) {
|
||||
if ($amount === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = \str_replace('total_', '', $key);
|
||||
|
||||
$billElement = new BillElement();
|
||||
$billElement->bill = $bill;
|
||||
|
||||
$billElement->taxR->value = $taxRates;
|
||||
|
||||
$internalRequest = new HttpRequest();
|
||||
$internalResponse = new HttpResponse();
|
||||
|
||||
$internalRequest->header->account = $request->header->account;
|
||||
$internalRequest->header->l11n = $request->header->l11n;
|
||||
|
||||
$internalRequest->setData('search', $key);
|
||||
$internalRequest->setData('limit', 1);
|
||||
|
||||
$internalResponse->header->l11n = clone $response->header->l11n;
|
||||
$internalResponse->header->l11n->language = $bill->language;
|
||||
|
||||
$this->app->moduleManager->get('ItemManagement', 'Api')->apiItemFind($internalRequest, $internalResponse);
|
||||
$item = $internalResponse->getDataArray('')[0];
|
||||
|
||||
$billElement->itemName = $key;
|
||||
|
||||
if ($item->id !== 0) {
|
||||
$billElement->item = $item;
|
||||
$billElement->itemNumber = $item->number;
|
||||
$billElement->itemName = $item->getL11n('name1')->content;
|
||||
}
|
||||
|
||||
$billElement->quantity->value = FloatInt::DIVISOR;
|
||||
|
||||
// Unit
|
||||
$billElement->singleListPriceNet = new FloatInt($amount, $l11n->thousands, $l11n->decimal);
|
||||
|
||||
$billElement->singleSalesPriceNet = $billElement->singleListPriceNet;
|
||||
$billElement->singlePurchasePriceNet = $billElement->singleSalesPriceNet;
|
||||
|
||||
if ($billElement->taxR->value > 0) {
|
||||
$billElement->singleListPriceGross->value = $billElement->singleListPriceNet->value + ((int) \round($billElement->singleSalesPriceNet->value * $billElement->taxR->value / (FloatInt::DIVISOR * 100), $rd));
|
||||
$billElement->singleSalesPriceGross = $billElement->singleListPriceGross;
|
||||
} else {
|
||||
$billElement->singleListPriceGross = $billElement->singleListPriceNet;
|
||||
$billElement->singleSalesPriceGross = $billElement->singleListPriceGross;
|
||||
}
|
||||
|
||||
// Total
|
||||
$billElement->totalListPriceNet = $billElement->singleListPriceNet;
|
||||
$billElement->totalSalesPriceNet = $billElement->singleSalesPriceNet;
|
||||
$billElement->totalPurchasePriceNet = $billElement->singlePurchasePriceNet;
|
||||
$billElement->totalListPriceGross = $billElement->singleListPriceGross;
|
||||
$billElement->totalSalesPriceGross = $billElement->singleSalesPriceGross;
|
||||
|
||||
$billElement->taxP->value = $billElement->totalSalesPriceGross->value - $billElement->totalSalesPriceNet->value;
|
||||
|
||||
$billElement->recalculatePrices();
|
||||
$bill->elements[] = $billElement;
|
||||
|
||||
$this->createModel($request->header->account, $billElement, BillElementMapper::class, 'bill_element', $request->getOrigin());
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($bill->elements)) {
|
||||
// Calculate totals from elements
|
||||
$totalNet = 0;
|
||||
$totalGross = 0;
|
||||
foreach ($bill->elements as $element) {
|
||||
$totalNet += $element->totalSalesPriceNet->value;
|
||||
$totalGross += $element->totalSalesPriceGross->value;
|
||||
}
|
||||
|
||||
$bill->grossSales = new FloatInt($totalGross);
|
||||
$bill->netCosts = new FloatInt($totalNet);
|
||||
$bill->netSales = $bill->netCosts;
|
||||
}
|
||||
|
||||
$bill->taxP->value = $bill->grossSales->value - $bill->netSales->value;
|
||||
|
||||
// No elements could be identified -> make total a bill element
|
||||
if (empty($itemLines) && empty($bill->elements)) {
|
||||
$billElement = new BillElement();
|
||||
$billElement->bill = $bill;
|
||||
|
||||
// List price
|
||||
$billElement->singleListPriceNet->value = $bill->netSales->value;
|
||||
$billElement->totalListPriceNet->value = $bill->netSales->value;
|
||||
|
||||
$billElement->singleListPriceGross->value = $bill->grossSales->value;
|
||||
$billElement->totalListPriceGross->value = $bill->grossSales->value;
|
||||
|
||||
// Unit price
|
||||
$billElement->singleSalesPriceNet->value = $bill->netSales->value;
|
||||
$billElement->singlePurchasePriceNet->value = $bill->netSales->value;
|
||||
|
||||
$billElement->singleSalesPriceGross->value = $bill->grossSales->value;
|
||||
|
||||
// Total
|
||||
$billElement->totalSalesPriceNet->value = $bill->netSales->value;
|
||||
$billElement->totalPurchasePriceNet->value = $bill->netSales->value;
|
||||
|
||||
$billElement->totalSalesPriceGross->value = $bill->grossSales->value;
|
||||
|
||||
$billElement->taxP->value = $bill->taxP->value;
|
||||
$billElement->taxR->value = $taxRates;
|
||||
|
||||
$billElement->recalculatePrices();
|
||||
$bill->elements[] = $billElement;
|
||||
|
||||
$this->createModel($request->header->account, $billElement, BillElementMapper::class, 'bill_element', $request->getOrigin());
|
||||
}
|
||||
|
||||
// Re-calculate totals from elements due to change
|
||||
$totalNet = 0;
|
||||
$totalGross = 0;
|
||||
foreach ($bill->elements as $element) {
|
||||
$totalNet += $element->totalSalesPriceNet->value;
|
||||
$totalGross += $element->totalSalesPriceGross->value;
|
||||
}
|
||||
|
||||
$bill->grossSales = new FloatInt($totalGross);
|
||||
$bill->netCosts = new FloatInt($totalNet);
|
||||
$bill->netSales = $bill->netCosts;
|
||||
|
||||
$bill->taxP->value = $bill->grossSales->value - $bill->netSales->value;
|
||||
|
||||
$this->updateModel($request->header->account, $old, $bill, BillMapper::class, 'bill_parsing', $request->getOrigin());
|
||||
|
||||
$view = new View($this->app->l11nManager, $request, $response);
|
||||
$view->setTemplate('/Modules/Billing/Theme/Cli/bill-parsed');
|
||||
// @todo change tax code during/after bill parsing
|
||||
|
||||
$view->data['bill'] = $bill;
|
||||
|
||||
// @todo change tax code during/after bill parsing
|
||||
// Fix internal document
|
||||
$request->setData('bill', $bill->id, true);
|
||||
$billResponse = new HttpResponse();
|
||||
$this->app->moduleManager->get('Billing', 'ApiBill')->apiBillPdfArchiveCreate($request, $billResponse);
|
||||
|
||||
return $view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect language from content
|
||||
*
|
||||
* @param string $content String to analyze
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function detectLanguage(string $content) : string
|
||||
{
|
||||
$detector = new Language();
|
||||
$language = $detector->detect($content)->bestResults()->close();
|
||||
|
||||
if (!\is_array($language) || \count($language) < 1) {
|
||||
return 'en';
|
||||
}
|
||||
|
||||
return \substr(\array_keys($language)[0], 0, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the supplier bill type
|
||||
|
|
@ -206,291 +473,6 @@ final class CliController extends Controller
|
|||
return empty($bestMatch) ? 'purchase_invoice' : $bestMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the supplier bill number
|
||||
*
|
||||
* @param string[] $lines Bill lines
|
||||
* @param array $matches Number match patterns
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function findBillNumber(array $lines, array $matches) : string
|
||||
{
|
||||
$bestPos = \count($lines);
|
||||
$bestMatch = '';
|
||||
|
||||
$found = [];
|
||||
|
||||
foreach ($matches as $match) {
|
||||
foreach ($lines as $row => $line) {
|
||||
if (\preg_match($match, $line, $found) === 1) {
|
||||
if ($row < $bestPos) {
|
||||
$bestPos = $row;
|
||||
$bestMatch = \trim($found['bill_no']);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $bestMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the supplier bill due date
|
||||
*
|
||||
* @param string[] $lines Bill lines
|
||||
* @param array $matches Due match patterns
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function findBillDue(array $lines, array $matches) : string
|
||||
{
|
||||
$bestPos = \count($lines);
|
||||
$bestMatch = '';
|
||||
|
||||
$found = [];
|
||||
|
||||
foreach ($matches as $match) {
|
||||
foreach ($lines as $row => $line) {
|
||||
if (\preg_match($match, $line, $found) === 1) {
|
||||
if ($row < $bestPos) {
|
||||
// @todo don't many invoices have the due date at the bottom? bestPos doesn't make sense?!
|
||||
$bestPos = $row;
|
||||
$bestMatch = \trim($found['bill_due']);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $bestMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the supplier bill date
|
||||
*
|
||||
* @param string[] $lines Bill lines
|
||||
* @param array $matches Date match patterns
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function findBillDate(array $lines, array $matches) : string
|
||||
{
|
||||
$bestPos = \count($lines);
|
||||
$bestMatch = '';
|
||||
|
||||
$found = [];
|
||||
|
||||
foreach ($matches as $match) {
|
||||
foreach ($lines as $row => $line) {
|
||||
if (\preg_match($match, $line, $found) === 1) {
|
||||
if ($row < $bestPos) {
|
||||
$bestPos = $row;
|
||||
$bestMatch = \trim($found['bill_date']);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $bestMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the supplier bill gross amount
|
||||
*
|
||||
* @param string[] $lines Bill lines
|
||||
* @param array $matches Tax match patterns
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @todo Handle multiple tax lines
|
||||
* Example: 19% and 7%
|
||||
*/
|
||||
private function findBillTaxAmount(array $lines, array $matches) : int
|
||||
{
|
||||
$bestMatch = 0;
|
||||
|
||||
$found = [];
|
||||
|
||||
foreach ($matches as $match) {
|
||||
foreach ($lines as $line) {
|
||||
if (\preg_match($match, $line, $found) === 1) {
|
||||
$temp = \trim($found['total_tax']);
|
||||
|
||||
$posD = \stripos($temp, '.');
|
||||
$posK = \stripos($temp, ',');
|
||||
|
||||
$hasDecimal = ($posD !== false || $posK !== false)
|
||||
&& \max((int) $posD, (int) $posK) + 3 >= \strlen($temp);
|
||||
|
||||
$gross = ((int) \str_replace(['.', ','], ['', ''], $temp)) * ($hasDecimal
|
||||
? 100
|
||||
: 10000);
|
||||
|
||||
if ($gross > $bestMatch) {
|
||||
$bestMatch = $gross;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $bestMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the supplier bill gross amount
|
||||
*
|
||||
* @param string[] $lines Bill lines
|
||||
* @param array $matches Net match patterns
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @todo maybe check with taxes
|
||||
* @todo maybe make sure text position is before total_gross
|
||||
*/
|
||||
private function findBillNet(array $lines, array $matches) : int
|
||||
{
|
||||
$bestMatch = 0;
|
||||
|
||||
$found = [];
|
||||
|
||||
foreach ($matches as $match) {
|
||||
foreach ($lines as $line) {
|
||||
if (\preg_match($match, $line, $found) === 1) {
|
||||
$temp = \trim($found['total_net']);
|
||||
|
||||
$posD = \stripos($temp, '.');
|
||||
$posK = \stripos($temp, ',');
|
||||
|
||||
$hasDecimal = ($posD !== false || $posK !== false)
|
||||
&& \max((int) $posD, (int) $posK) + 3 >= \strlen($temp);
|
||||
|
||||
$gross = ((int) \str_replace(['.', ','], ['', ''], $temp)) * ($hasDecimal
|
||||
? 100
|
||||
: 10000);
|
||||
|
||||
if ($gross > $bestMatch) {
|
||||
$bestMatch = $gross;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $bestMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the supplier bill gross amount
|
||||
*
|
||||
* @param string[] $lines Bill lines
|
||||
* @param array $matches Gross match patterns
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function findBillGross(array $lines, array $matches) : int
|
||||
{
|
||||
$bestMatch = 0;
|
||||
|
||||
$found = [];
|
||||
|
||||
foreach ($matches as $match) {
|
||||
foreach ($lines as $line) {
|
||||
if (\preg_match($match, $line, $found) === 1) {
|
||||
$temp = \trim($found['total_gross']);
|
||||
|
||||
$posD = \stripos($temp, '.');
|
||||
$posK = \stripos($temp, ',');
|
||||
|
||||
$hasDecimal = ($posD !== false || $posK !== false)
|
||||
&& \max((int) $posD, (int) $posK) + 3 >= \strlen($temp);
|
||||
|
||||
$gross = ((int) \str_replace(['.', ','], ['', ''], $temp)) * ($hasDecimal
|
||||
? 100
|
||||
: 10000);
|
||||
|
||||
if ($gross > $bestMatch) {
|
||||
$bestMatch = $gross;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $bestMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the supplier bill gross amount
|
||||
*
|
||||
* @param string[] $lines Bill lines
|
||||
* @param array $matches Item lines match patterns
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function findBillItemLines(array $lines, array $matches) : array
|
||||
{
|
||||
// Find start for item list (should be a headline containing certain words)
|
||||
$startLine = 0;
|
||||
$bestMatch = 0;
|
||||
|
||||
foreach ($lines as $idx => $line) {
|
||||
$headlineMatches = 0;
|
||||
|
||||
foreach ($matches['headline'] as $match) {
|
||||
foreach ($match as $headline) {
|
||||
if (\stripos($line, $headline) !== false) {
|
||||
++$headlineMatches;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($headlineMatches > $bestMatch && $headlineMatches > 1) {
|
||||
$bestMatch = $headlineMatches;
|
||||
$startLine = $idx;
|
||||
}
|
||||
}
|
||||
|
||||
if ($startLine === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get headline structure = item list structure
|
||||
$headlineStructure = [];
|
||||
foreach ($matches['headline'] as $type => $match) {
|
||||
foreach ($match as $headline) {
|
||||
if (($pos = \stripos($line, $headline)) !== false) {
|
||||
$headlineStructure[$type] = $pos;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
\asort($headlineStructure);
|
||||
|
||||
// Get item list until end of item list/table is reached
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find possible supplier id
|
||||
*
|
||||
|
|
@ -498,7 +480,6 @@ final class CliController extends Controller
|
|||
* 1. bill_match_pattern
|
||||
* 2. name1 + IBAN
|
||||
* 3. name1 + city || address
|
||||
* 4. name1
|
||||
*
|
||||
* @param string $content Content to analyze
|
||||
* @param Supplier[] $suppliers Suppliers
|
||||
|
|
@ -544,44 +525,6 @@ final class CliController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
// name1
|
||||
foreach ($suppliers as $supplier) {
|
||||
if (\stripos($content, $supplier->account->name1) !== false) {
|
||||
return $supplier->id;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create DateTime from date string
|
||||
*
|
||||
* @param string $date Date string
|
||||
* @param Supplier $supplier Supplier
|
||||
* @param string[] $formats Date formats
|
||||
*
|
||||
* @return null|\DateTime
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function parseDate(string $date, Supplier $supplier, array $formats) : ?\DateTime
|
||||
{
|
||||
if ((!empty($supplier->getAttribute('bill_date_format')->value->valueStr))) {
|
||||
$dt = \DateTime::createFromFormat(
|
||||
$supplier->getAttribute('bill_date_format')->value->valueStr ?? '',
|
||||
$date
|
||||
);
|
||||
|
||||
return $dt === false ? new \DateTime('1970-01-01') : $dt;
|
||||
}
|
||||
|
||||
foreach ($formats as $format) {
|
||||
if (($obj = \DateTime::createFromFormat($format, $date)) !== false) {
|
||||
return $obj === false ? null : $obj;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
117
Models/Bill.php
|
|
@ -65,6 +65,8 @@ class Bill implements \JsonSerializable
|
|||
*/
|
||||
public string $number = '';
|
||||
|
||||
public string $external = '';
|
||||
|
||||
/**
|
||||
* Bill type.
|
||||
*
|
||||
|
|
@ -247,8 +249,6 @@ class Bill implements \JsonSerializable
|
|||
*/
|
||||
public Account $referral;
|
||||
|
||||
public string $externalReferral = '';
|
||||
|
||||
/**
|
||||
* Net amount.
|
||||
*
|
||||
|
|
@ -257,14 +257,6 @@ class Bill implements \JsonSerializable
|
|||
*/
|
||||
public FloatInt $netProfit;
|
||||
|
||||
/**
|
||||
* Gross amount.
|
||||
*
|
||||
* @var FloatInt
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public FloatInt $grossProfit;
|
||||
|
||||
/**
|
||||
* Costs in net.
|
||||
*
|
||||
|
|
@ -273,14 +265,6 @@ class Bill implements \JsonSerializable
|
|||
*/
|
||||
public FloatInt $netCosts;
|
||||
|
||||
/**
|
||||
* Profit in net.
|
||||
*
|
||||
* @var FloatInt
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public FloatInt $grossCosts;
|
||||
|
||||
/**
|
||||
* Costs in net.
|
||||
*
|
||||
|
|
@ -305,14 +289,6 @@ class Bill implements \JsonSerializable
|
|||
*/
|
||||
public FloatInt $netDiscount;
|
||||
|
||||
/**
|
||||
* Profit in net.
|
||||
*
|
||||
* @var FloatInt
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public FloatInt $grossDiscount;
|
||||
|
||||
/**
|
||||
* Tax amount
|
||||
*
|
||||
|
|
@ -443,13 +419,10 @@ class Bill implements \JsonSerializable
|
|||
public function __construct()
|
||||
{
|
||||
$this->netProfit = new FloatInt(0);
|
||||
$this->grossProfit = new FloatInt(0);
|
||||
$this->netCosts = new FloatInt(0);
|
||||
$this->grossCosts = new FloatInt(0);
|
||||
$this->netSales = new FloatInt(0);
|
||||
$this->grossSales = new FloatInt(0);
|
||||
$this->netDiscount = new FloatInt(0);
|
||||
$this->grossDiscount = new FloatInt(0);
|
||||
$this->taxP = new FloatInt(0);
|
||||
|
||||
$this->billDate = new \DateTime('now');
|
||||
|
|
@ -551,15 +524,92 @@ class Bill implements \JsonSerializable
|
|||
$this->elements[] = $element;
|
||||
|
||||
$this->netProfit->value += $element->totalProfitNet->value;
|
||||
$this->grossProfit->value += $element->totalProfitGross->value;
|
||||
$this->netCosts->value += $element->totalPurchasePriceNet->value;
|
||||
$this->grossCosts->value += $element->totalPurchasePriceGross->value;
|
||||
$this->netSales->value += $element->totalSalesPriceNet->value;
|
||||
$this->grossSales->value += $element->totalSalesPriceGross->value;
|
||||
$this->netDiscount->value += $element->totalDiscountP->value;
|
||||
}
|
||||
|
||||
// @todo Discount might be in quantities
|
||||
$this->grossDiscount->value += (int) ($element->taxR->value * $element->totalDiscountP->value / 10000);
|
||||
// @todo also consider rounding similarly to recalculatePrices in elements
|
||||
public function isValid() : bool
|
||||
{
|
||||
return $this->validateTaxAmountElements()
|
||||
&& $this->validateProfit()
|
||||
&& $this->validateGrossElements()
|
||||
&& $this->validatePriceQuantityElements()
|
||||
&& $this->validateNetElements()
|
||||
&& $this->validateNetGross()
|
||||
&& $this->areElementsValid();
|
||||
}
|
||||
|
||||
public function areElementsValid() : bool
|
||||
{
|
||||
foreach ($this->elements as $element) {
|
||||
if (!$element->isValid()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function validateNetGross() : bool
|
||||
{
|
||||
return $this->netSales->value <= $this->grossSales->value;
|
||||
}
|
||||
|
||||
public function validateProfit() : bool
|
||||
{
|
||||
return $this->netSales->value - $this->netCosts->value === $this->netProfit->value;
|
||||
}
|
||||
|
||||
public function validateTax() : bool
|
||||
{
|
||||
return \abs($this->netSales->value + $this->taxP->value - $this->grossSales->value) === 0;
|
||||
}
|
||||
|
||||
public function validateTaxAmountElements() : bool
|
||||
{
|
||||
$taxes = 0;
|
||||
foreach ($this->elements as $element) {
|
||||
$taxes += $element->taxP->value;
|
||||
}
|
||||
|
||||
return $taxes === $this->taxP->value;
|
||||
}
|
||||
|
||||
public function validateNetElements() : bool
|
||||
{
|
||||
$net = 0;
|
||||
foreach ($this->elements as $element) {
|
||||
$net += $element->totalSalesPriceNet->value;
|
||||
}
|
||||
|
||||
return $net === $this->netSales->value;
|
||||
}
|
||||
|
||||
public function validateGrossElements()
|
||||
{
|
||||
$gross = 0;
|
||||
foreach ($this->elements as $element) {
|
||||
$gross += $element->totalSalesPriceGross->value;
|
||||
}
|
||||
|
||||
return $gross === $this->grossSales->value;
|
||||
}
|
||||
|
||||
public function validatePriceQuantityElements()
|
||||
{
|
||||
foreach ($this->elements as $element) {
|
||||
if ($element->discountQ->value === 0
|
||||
&& $element->totalDiscountP->value === 0
|
||||
&& ($element->quantity->value / FloatInt::DIVISOR) * $element->singleSalesPriceNet->value - $element->totalSalesPriceNet->value > 1.0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -570,6 +620,7 @@ class Bill implements \JsonSerializable
|
|||
return [
|
||||
'id' => $this->id,
|
||||
'number' => $this->number,
|
||||
'external' => $this->external,
|
||||
'type' => $this->type,
|
||||
'shipTo' => $this->shipTo,
|
||||
'shipFAO' => $this->shipFAO,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ use Modules\Finance\Models\TaxCode;
|
|||
use Modules\ItemManagement\Models\Container;
|
||||
use Modules\ItemManagement\Models\Item;
|
||||
use Modules\ItemManagement\Models\NullItem;
|
||||
use phpOMS\Localization\ISO4217DecimalEnum;
|
||||
use phpOMS\Stdlib\Base\FloatInt;
|
||||
use phpOMS\Stdlib\Base\SmartDateTime;
|
||||
|
||||
|
|
@ -100,20 +101,12 @@ class BillElement implements \JsonSerializable
|
|||
|
||||
public FloatInt $singlePurchasePriceNet;
|
||||
|
||||
public FloatInt $singlePurchasePriceGross;
|
||||
|
||||
public FloatInt $totalPurchasePriceNet;
|
||||
|
||||
public FloatInt $totalPurchasePriceGross;
|
||||
|
||||
public FloatInt $singleProfitNet;
|
||||
|
||||
public FloatInt $singleProfitGross;
|
||||
|
||||
public FloatInt $totalProfitNet;
|
||||
|
||||
public FloatInt $totalProfitGross;
|
||||
|
||||
public ?int $itemSegment = null;
|
||||
|
||||
public ?int $itemSection = null;
|
||||
|
|
@ -178,7 +171,7 @@ class BillElement implements \JsonSerializable
|
|||
{
|
||||
$this->bill = new NullBill();
|
||||
|
||||
$this->quantity = new FloatInt();
|
||||
$this->quantity = new FloatInt(FloatInt::DIVISOR);
|
||||
|
||||
$this->singleListPriceNet = new FloatInt();
|
||||
$this->singleListPriceGross = new FloatInt();
|
||||
|
|
@ -195,16 +188,10 @@ class BillElement implements \JsonSerializable
|
|||
$this->totalSalesPriceGross = new FloatInt();
|
||||
|
||||
$this->singlePurchasePriceNet = new FloatInt();
|
||||
$this->singlePurchasePriceGross = new FloatInt();
|
||||
|
||||
$this->totalPurchasePriceNet = new FloatInt();
|
||||
$this->totalPurchasePriceGross = new FloatInt();
|
||||
|
||||
$this->singleProfitNet = new FloatInt();
|
||||
$this->singleProfitGross = new FloatInt();
|
||||
|
||||
$this->totalProfitNet = new FloatInt();
|
||||
$this->totalProfitGross = new FloatInt();
|
||||
|
||||
$this->singleDiscountP = new FloatInt();
|
||||
$this->totalDiscountP = new FloatInt();
|
||||
|
|
@ -237,30 +224,95 @@ class BillElement implements \JsonSerializable
|
|||
|
||||
public function recalculatePrices() : void
|
||||
{
|
||||
$this->totalListPriceNet->value = (int) \round(($this->quantity->getNormalizedValue() - $this->discountQ->getNormalizedValue()) * $this->singleListPriceNet->value, 0);
|
||||
$this->totalSalesPriceNet->value = (int) \round(($this->quantity->getNormalizedValue() - $this->discountQ->getNormalizedValue()) * $this->singleListPriceNet->value, 0);
|
||||
$rd = -FloatInt::MAX_DECIMALS + ISO4217DecimalEnum::getByName('_' . $this->bill->currency);
|
||||
|
||||
$this->totalListPriceNet->value = (int) \round($this->quantity->getNormalizedValue() * $this->singleListPriceNet->value, $rd);
|
||||
$this->totalSalesPriceNet->value = (int) \round(($this->quantity->getNormalizedValue() - $this->discountQ->getNormalizedValue()) * $this->singleListPriceNet->value, $rd);
|
||||
|
||||
// @todo Check if this is correct, this should maybe happen after applying the discounts?!
|
||||
// This depends on if the single price is already discounted or not
|
||||
$this->singleProfitNet->value = $this->singleSalesPriceNet->value - $this->singlePurchasePriceNet->value;
|
||||
$this->totalProfitNet->value = $this->totalSalesPriceNet->value - $this->totalPurchasePriceNet->value;
|
||||
|
||||
$this->taxP->value = (int) \round($this->taxR->value / 1000000 * $this->totalSalesPriceNet->value, 0);
|
||||
$this->taxP->value = (int) \round($this->taxR->value / (FloatInt::DIVISOR * 100) * $this->totalSalesPriceNet->value, $rd);
|
||||
|
||||
$this->singleListPriceGross->value = (int) \round($this->singleListPriceNet->value + $this->singleListPriceNet->value * $this->taxR->value / 10000, 0);
|
||||
$this->totalListPriceGross->value = (int) \round($this->totalListPriceNet->value + $this->totalListPriceNet->value * $this->taxR->value / 10000, 0);
|
||||
$this->singleSalesPriceGross->value = (int) \round($this->singleSalesPriceNet->value + $this->singleSalesPriceNet->value * $this->taxR->value / 10000, 0);
|
||||
$this->totalSalesPriceGross->value = (int) \round($this->totalSalesPriceNet->value + $this->totalSalesPriceNet->value * $this->taxR->value / 10000, 0);
|
||||
|
||||
$this->singleProfitGross->value = $this->singleSalesPriceGross->value - $this->singlePurchasePriceGross->value;
|
||||
$this->totalProfitGross->value = (int) \round(($this->quantity->getNormalizedValue() - $this->discountQ->getNormalizedValue()) * ($this->totalSalesPriceGross->value - $this->totalPurchasePriceGross->value), 0);
|
||||
$this->singleListPriceGross->value = (int) \round($this->singleListPriceNet->value + $this->singleListPriceNet->value * $this->taxR->value / (FloatInt::DIVISOR * 100), $rd);
|
||||
$this->totalListPriceGross->value = (int) \round($this->totalListPriceNet->value + $this->totalListPriceNet->value * $this->taxR->value / (FloatInt::DIVISOR * 100), $rd);
|
||||
$this->singleSalesPriceGross->value = (int) \round($this->singleSalesPriceNet->value + $this->singleSalesPriceNet->value * $this->taxR->value / (FloatInt::DIVISOR * 100), $rd);
|
||||
$this->totalSalesPriceGross->value = (int) \round($this->totalSalesPriceNet->value + $this->totalSalesPriceNet->value * $this->taxR->value / (FloatInt::DIVISOR * 100), $rd);
|
||||
|
||||
$this->singleDiscountP->value = $this->quantity->value - $this->discountQ->value === 0
|
||||
? 0
|
||||
: (int) \round($this->totalDiscountP->value / ($this->quantity->getNormalizedValue() - $this->discountQ->getNormalizedValue()));
|
||||
|
||||
// important because the quantity includes $discountQ
|
||||
$this->effectiveSingleSalesPriceNet->value = (int) \round($this->totalSalesPriceNet->value / ($this->quantity->value / 10000));
|
||||
$this->effectiveSingleSalesPriceNet->value = (int) \round($this->totalSalesPriceNet->value / ($this->quantity->value / FloatInt::DIVISOR), $rd);
|
||||
}
|
||||
|
||||
// @todo also consider rounding similarly to recalculatePrices
|
||||
public function isValid() : bool
|
||||
{
|
||||
return $this->validateNetGross()
|
||||
&& $this->validateProfit()
|
||||
&& $this->validateTax()
|
||||
&& $this->validateTaxRate()
|
||||
&& $this->validateSingleTotal()
|
||||
&& $this->validateEffectiveSinglePrice()
|
||||
&& $this->validateTotalPrice();
|
||||
}
|
||||
|
||||
public function validateNetGross() : bool
|
||||
{
|
||||
return $this->singleListPriceNet->value <= $this->singleListPriceGross->value
|
||||
&& $this->singleSalesPriceNet->value <= $this->singleSalesPriceGross->value
|
||||
&& $this->totalListPriceNet->value <= $this->totalListPriceGross->value
|
||||
&& $this->totalSalesPriceNet->value <= $this->totalSalesPriceGross->value;
|
||||
}
|
||||
|
||||
public function validateProfit() : bool
|
||||
{
|
||||
return $this->totalSalesPriceNet->value - $this->totalPurchasePriceNet->value === $this->totalProfitNet->value;
|
||||
}
|
||||
|
||||
public function validateTax() : bool
|
||||
{
|
||||
$paidQuantity = $this->quantity->value - $this->discountQ->value;
|
||||
|
||||
return \abs($this->singleListPriceNet->value + ((int) \round($this->taxP->value / ($paidQuantity / FloatInt::DIVISOR), 0)) - $this->singleListPriceGross->value) === 0
|
||||
&& \abs($this->singleSalesPriceNet->value + ((int) \round($this->taxP->value / ($paidQuantity / FloatInt::DIVISOR), 0)) - $this->singleSalesPriceGross->value) === 0
|
||||
&& \abs($this->totalListPriceNet->value +$this->taxP->value - $this->totalListPriceGross->value) === 0
|
||||
&& \abs($this->totalSalesPriceNet->value + $this->taxP->value - $this->totalSalesPriceGross->value) === 0;
|
||||
}
|
||||
|
||||
public function validateTaxRate() : bool
|
||||
{
|
||||
return (($this->taxP->value === 0 && $this->taxR->value === 0)
|
||||
|| (\abs($this->taxP->value / $this->totalSalesPriceNet->value - $this->taxR->value / (FloatInt::DIVISOR * 100)) < 0.001)
|
||||
&& \abs($this->totalSalesPriceGross->value / $this->totalSalesPriceNet->value - 1.0 - $this->taxR->value / (FloatInt::DIVISOR * 100)) < 0.001);
|
||||
}
|
||||
|
||||
public function validateSingleTotal() : bool
|
||||
{
|
||||
$paidQuantity = $this->quantity->value - $this->discountQ->value;
|
||||
|
||||
// Only possible for sales, costs may be different for different lots
|
||||
return ((int) \round($this->singleListPriceNet->value * ($this->quantity->value / FloatInt::DIVISOR), 0)) === $this->totalListPriceNet->value
|
||||
&& ((int) \round($this->singleSalesPriceNet->value * ($paidQuantity / FloatInt::DIVISOR), 0)) === $this->totalSalesPriceNet->value
|
||||
&& ((int) \round($this->singleDiscountP->value * ($this->quantity->value / FloatInt::DIVISOR), 0)) === $this->totalDiscountP->value;
|
||||
}
|
||||
|
||||
public function validateEffectiveSinglePrice() : bool
|
||||
{
|
||||
return $this->effectiveSingleSalesPriceNet->value === (int) \round($this->totalSalesPriceNet->value / ($this->quantity->value / FloatInt::DIVISOR));
|
||||
}
|
||||
|
||||
public function validateTotalPrice() : bool
|
||||
{
|
||||
return ((int) \round($this->singleListPriceNet->value * ($this->quantity->value / FloatInt::DIVISOR)
|
||||
- $this->singleListPriceNet->value * ($this->quantity->value / FloatInt::DIVISOR) * $this->singleDiscountR->value / (FloatInt::DIVISOR * 100)
|
||||
- $this->totalDiscountP->value * ($this->quantity->value / FloatInt::DIVISOR)
|
||||
- $this->singleListPriceNet->value * ($this->discountQ->value / FloatInt::DIVISOR), 0))
|
||||
=== $this->totalSalesPriceNet->value;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -292,7 +344,7 @@ class BillElement implements \JsonSerializable
|
|||
public static function fromItem(
|
||||
Item $item,
|
||||
TaxCombination $taxCombination,
|
||||
int $quantity = 10000,
|
||||
int $quantity = FloatInt::DIVISOR,
|
||||
Bill $bill = null,
|
||||
?Container $container = null
|
||||
) : self
|
||||
|
|
@ -314,9 +366,6 @@ class BillElement implements \JsonSerializable
|
|||
$element->singlePurchasePriceNet->value = $item->purchasePrice->value;
|
||||
$element->totalPurchasePriceNet->value = (int) ($element->quantity->getNormalizedValue() * $item->purchasePrice->value);
|
||||
|
||||
$element->singlePurchasePriceGross->value = (int) \round($element->singlePurchasePriceNet->value + $element->singlePurchasePriceNet->value * $element->taxR->value / 10000, 0);
|
||||
$element->totalPurchasePriceGross->value = (int) \round($element->totalPurchasePriceNet->value + $element->totalPurchasePriceNet->value * $element->taxR->value / 10000, 0);
|
||||
|
||||
if ($element->bill->id !== 0
|
||||
&& $item->getAttribute('subscription')->value->getValue() === 1
|
||||
&& $element->item !== null
|
||||
|
|
|
|||
|
|
@ -59,14 +59,10 @@ final class BillElementMapper extends DataMapperFactory
|
|||
'billing_bill_element_total_grosssalesprice' => ['name' => 'billing_bill_element_total_grosssalesprice', 'type' => 'Serializable', 'internal' => 'totalSalesPriceGross'],
|
||||
|
||||
'billing_bill_element_single_netprofit' => ['name' => 'billing_bill_element_single_netprofit', 'type' => 'Serializable', 'internal' => 'singleProfitNet'],
|
||||
'billing_bill_element_single_grossprofit' => ['name' => 'billing_bill_element_single_grossprofit', 'type' => 'Serializable', 'internal' => 'singleProfitGross'],
|
||||
'billing_bill_element_total_netprofit' => ['name' => 'billing_bill_element_total_netprofit', 'type' => 'Serializable', 'internal' => 'totalProfitNet'],
|
||||
'billing_bill_element_total_grossprofit' => ['name' => 'billing_bill_element_total_grossprofit', 'type' => 'Serializable', 'internal' => 'totalProfitGross'],
|
||||
|
||||
'billing_bill_element_single_netpurchaseprice' => ['name' => 'billing_bill_element_single_netpurchaseprice', 'type' => 'Serializable', 'internal' => 'singlePurchasePriceNet'],
|
||||
'billing_bill_element_single_grosspurchaseprice' => ['name' => 'billing_bill_element_single_grosspurchaseprice', 'type' => 'Serializable', 'internal' => 'singlePurchasePriceGross'],
|
||||
'billing_bill_element_total_netpurchaseprice' => ['name' => 'billing_bill_element_total_netpurchaseprice', 'type' => 'Serializable', 'internal' => 'totalPurchasePriceNet'],
|
||||
'billing_bill_element_total_grosspurchaseprice' => ['name' => 'billing_bill_element_total_grosspurchaseprice', 'type' => 'Serializable', 'internal' => 'totalPurchasePriceGross'],
|
||||
'billing_bill_element_bill' => ['name' => 'billing_bill_element_bill', 'type' => 'int', 'internal' => 'bill'],
|
||||
|
||||
'billing_bill_element_tax_type' => ['name' => 'billing_bill_element_tax_type', 'type' => 'string', 'internal' => 'taxCode'],
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ class BillMapper extends DataMapperFactory
|
|||
'billing_bill_id' => ['name' => 'billing_bill_id', 'type' => 'int', 'internal' => 'id'],
|
||||
'billing_bill_sequence' => ['name' => 'billing_bill_sequence', 'type' => 'int', 'internal' => 'sequence'],
|
||||
'billing_bill_number' => ['name' => 'billing_bill_number', 'type' => 'string', 'internal' => 'number'],
|
||||
'billing_bill_external' => ['name' => 'billing_bill_external', 'type' => 'string', 'internal' => 'external'],
|
||||
'billing_bill_type' => ['name' => 'billing_bill_type', 'type' => 'int', 'internal' => 'type'],
|
||||
'billing_bill_template' => ['name' => 'billing_bill_template', 'type' => 'bool', 'internal' => 'isTemplate'],
|
||||
'billing_bill_archived' => ['name' => 'billing_bill_archived', 'type' => 'bool', 'internal' => 'isArchived'],
|
||||
|
|
@ -69,19 +70,15 @@ class BillMapper extends DataMapperFactory
|
|||
'billing_bill_billZip' => ['name' => 'billing_bill_billZip', 'type' => 'string', 'internal' => 'billZip'],
|
||||
'billing_bill_billCountry' => ['name' => 'billing_bill_billCountry', 'type' => 'string', 'internal' => 'billCountry'],
|
||||
'billing_bill_netprofit' => ['name' => 'billing_bill_netprofit', 'type' => 'Serializable', 'internal' => 'netProfit'],
|
||||
'billing_bill_grossprofit' => ['name' => 'billing_bill_grossprofit', 'type' => 'Serializable', 'internal' => 'grossProfit'],
|
||||
'billing_bill_netcosts' => ['name' => 'billing_bill_netcosts', 'type' => 'Serializable', 'internal' => 'netCosts'],
|
||||
'billing_bill_grosscosts' => ['name' => 'billing_bill_grosscosts', 'type' => 'Serializable', 'internal' => 'grossCosts'],
|
||||
'billing_bill_netsales' => ['name' => 'billing_bill_netsales', 'type' => 'Serializable', 'internal' => 'netSales'],
|
||||
'billing_bill_grosssales' => ['name' => 'billing_bill_grosssales', 'type' => 'Serializable', 'internal' => 'grossSales'],
|
||||
'billing_bill_netdiscount' => ['name' => 'billing_bill_netdiscount', 'type' => 'Serializable', 'internal' => 'netDiscount'],
|
||||
'billing_bill_grossdiscount' => ['name' => 'billing_bill_grossdiscount', 'type' => 'Serializable', 'internal' => 'grossDiscount'],
|
||||
'billing_bill_taxp' => ['name' => 'billing_bill_taxp', 'type' => 'Serializable', 'internal' => 'taxP'],
|
||||
'billing_bill_fiaccount' => ['name' => 'billing_bill_fiaccount', 'type' => 'string', 'internal' => 'fiAccount'],
|
||||
'billing_bill_currency' => ['name' => 'billing_bill_currency', 'type' => 'string', 'internal' => 'currency'],
|
||||
'billing_bill_language' => ['name' => 'billing_bill_language', 'type' => 'string', 'internal' => 'language'],
|
||||
'billing_bill_referral' => ['name' => 'billing_bill_referral', 'type' => 'int', 'internal' => 'referral'],
|
||||
'billing_bill_referral_name' => ['name' => 'billing_bill_referral_name', 'type' => 'string', 'internal' => 'externalReferral'],
|
||||
'billing_bill_reference' => ['name' => 'billing_bill_reference', 'type' => 'int', 'internal' => 'reference'],
|
||||
'billing_bill_accsegment' => ['name' => 'billing_bill_accsegment', 'type' => 'int', 'internal' => 'accSegment'],
|
||||
'billing_bill_accsection' => ['name' => 'billing_bill_accsection', 'type' => 'int', 'internal' => 'accSection'],
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ class BillType implements \JsonSerializable
|
|||
|
||||
public int $sign = 1;
|
||||
|
||||
public bool $email = false;
|
||||
|
||||
/**
|
||||
* Localization
|
||||
*
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ final class BillTypeMapper extends DataMapperFactory
|
|||
'billing_type_transfer_stock' => ['name' => 'billing_type_transfer_stock', 'type' => 'bool', 'internal' => 'transferStock'],
|
||||
'billing_type_accounting' => ['name' => 'billing_type_accounting', 'type' => 'bool', 'internal' => 'isAccounting'],
|
||||
'billing_type_transfer_sign' => ['name' => 'billing_type_transfer_sign', 'type' => 'int', 'internal' => 'sign'],
|
||||
'billing_type_email' => ['name' => 'billing_type_email', 'type' => 'bool', 'internal' => 'email'],
|
||||
'billing_type_is_template' => ['name' => 'billing_type_is_template', 'type' => 'bool', 'internal' => 'isTemplate'],
|
||||
];
|
||||
|
||||
|
|
|
|||
1328
Models/InvoiceRecognition.php
Normal file
|
|
@ -43,4 +43,6 @@ abstract class PermissionCategory extends Enum
|
|||
public const PAYMENT_TERM = 9;
|
||||
|
||||
public const SHIPPING_TERM = 10;
|
||||
|
||||
public const BILL_LOG = 101;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ class Price implements \JsonSerializable
|
|||
|
||||
public Item $item;
|
||||
|
||||
public int $status = PriceStatus::ACTIVE;
|
||||
|
||||
public AttributeValue $itemsalesgroup;
|
||||
|
||||
public AttributeValue $itemproductgroup;
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ final class PriceMapper extends DataMapperFactory
|
|||
'billing_price_supplier' => ['name' => 'billing_price_supplier', 'type' => 'int', 'internal' => 'supplier'],
|
||||
'billing_price_unit' => ['name' => 'billing_price_unit', 'type' => 'int', 'internal' => 'unit'],
|
||||
'billing_price_type' => ['name' => 'billing_price_type', 'type' => 'int', 'internal' => 'type'],
|
||||
'billing_price_status' => ['name' => 'billing_price_status', 'type' => 'int', 'internal' => 'status'],
|
||||
'billing_price_quantity' => ['name' => 'billing_price_quantity', 'type' => 'Serializable', 'internal' => 'quantity'],
|
||||
'billing_price_price' => ['name' => 'billing_price_price', 'type' => 'Serializable', 'internal' => 'price'],
|
||||
'billing_price_price_new' => ['name' => 'billing_price_price_new', 'type' => 'Serializable', 'internal' => 'priceNew'],
|
||||
|
|
|
|||
32
Models/Price/PriceStatus.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package Modules\Billing\Models\Price
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Billing\Models\Price;
|
||||
|
||||
use phpOMS\Stdlib\Base\Enum;
|
||||
|
||||
/**
|
||||
* Module settings enum.
|
||||
*
|
||||
* @package Modules\Billing\Models\Price
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
abstract class PriceStatus extends Enum
|
||||
{
|
||||
public const ACTIVE = 1;
|
||||
|
||||
public const INACTIVE = 2;
|
||||
}
|
||||
|
|
@ -364,6 +364,10 @@ final class SalesBillMapper extends BillMapper
|
|||
*/
|
||||
public static function getItemMonthlySalesCosts(array $items, \DateTime $start, \DateTime $end) : array
|
||||
{
|
||||
if (empty($items)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$item = \implode(',', $items);
|
||||
|
||||
$sql = <<<SQL
|
||||
|
|
@ -391,11 +395,15 @@ final class SalesBillMapper extends BillMapper
|
|||
|
||||
public static function getItemMonthlySalesQuantity(array $items, \DateTime $start, \DateTime $end) : array
|
||||
{
|
||||
if (empty($items)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$item = \implode(',', $items);
|
||||
|
||||
$sql = <<<SQL
|
||||
SELECT
|
||||
billing_bill_element_item,
|
||||
billing_bill_element_item as item,
|
||||
SUM(billing_bill_element_quantity) as quantity,
|
||||
YEAR(billing_bill_performance_date) as year,
|
||||
MONTH(billing_bill_performance_date) as month
|
||||
|
|
@ -405,8 +413,8 @@ final class SalesBillMapper extends BillMapper
|
|||
billing_bill_element_item IN ({$item})
|
||||
AND billing_bill_performance_date >= '{$start->format('Y-m-d H:i:s')}'
|
||||
AND billing_bill_performance_date <= '{$end->format('Y-m-d H:i:s')}'
|
||||
GROUP BY billing_bill_element_item, year, month
|
||||
ORDER BY billing_bill_element_item, year ASC, month ASC;
|
||||
GROUP BY item, year, month
|
||||
ORDER BY item, year ASC, month ASC;
|
||||
SQL;
|
||||
|
||||
$query = new Builder(self::$db);
|
||||
|
|
|
|||
|
|
@ -28,9 +28,13 @@ abstract class SettingsEnum extends Enum
|
|||
{
|
||||
public const PREVIEW_MEDIA_TYPE = '1005100001'; // internally generated preview
|
||||
|
||||
public const ORIGINAL_MEDIA_TYPE = '1005100002'; // original document (client = invoice sent to client, supplier = invoice from supplier)
|
||||
public const INTERNAL_MEDIA_TYPE = '1005100002'; // original document (client = invoice sent to client, supplier = invoice from supplier)
|
||||
|
||||
public const EXTERNAL_MEDIA_TYPE = '1005100006'; // original document (client = invoice sent to client, supplier = invoice from supplier)
|
||||
|
||||
public const VALID_BILL_LANGUAGES = '1005100003'; // List of valid languages for bills
|
||||
|
||||
public const BILLING_CUSTOMER_EMAIL_TEMPLATE = '1005100004'; // Email template for customer billing
|
||||
|
||||
public const BILLING_SUPPLIER_EMAIL_TEMPLATE = '1005100005'; // Email template for supplier billing
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,129 +35,216 @@
|
|||
"Auftragsbestätigung"
|
||||
]
|
||||
},
|
||||
"purchase_offer": {
|
||||
"en": [
|
||||
"Offer"
|
||||
],
|
||||
"de": [
|
||||
"Angebot"
|
||||
]
|
||||
},
|
||||
"purchase_reverse_invoice": {
|
||||
"en": [],
|
||||
"de": [
|
||||
"Gutschrift"
|
||||
]
|
||||
},
|
||||
"purchase_proforma_invoice": {
|
||||
"en": [
|
||||
"Proforma"
|
||||
],
|
||||
"de": [
|
||||
"Proforma"
|
||||
]
|
||||
}
|
||||
},
|
||||
"tax_id": {
|
||||
"en": [
|
||||
"/(tax[\\. \\-]*id|tin|ssn|ein)(.*? {1,})(?<tax_id>(.{6,15}))( |$)/i",
|
||||
"/(vat[\\. \\-]*id)(.*? {1,})(?<tax_id>([^a-zA-Z]{6,15}))( |$)/i"
|
||||
],
|
||||
"de": [
|
||||
"/(Steuern|Steuer[\\. \\-]*Nr|St[\\. \\-]*Nr)(.*? {1,})(?<tax_id>(.{8,15}))( |$)/i",
|
||||
"/(USt[\\. \\-]*Id|Umsatzst.*?Id)(.*? {1,})(?<tax_id>([^a-zA-Z]{8,15}))( |$)/i"
|
||||
]
|
||||
},
|
||||
"vat_id": {
|
||||
"en": [
|
||||
"/(vat[\\. \\-]*id)(.*? {1,})(?<vat_id>([a-zA-Z]{2})(.*?){7,13})( |$)/i"
|
||||
],
|
||||
"de": [
|
||||
"/(USt[\\. \\-]*Id|Umsatzst.*?Id)(.*? {1,})(?<vat_id>([a-zA-Z]{2})(.*?){7,13})( |$)/i"
|
||||
]
|
||||
},
|
||||
"iban": ["/(IBAN)(.*? {1,})(?<iban>([a-zA-Z]{2,}[ 0-9]{14,}))( |$)/i"],
|
||||
"email": ["/(^| )(?<email>([a-zA-Z0-9\\-]+@[a-zA-Z0-9\\-]+\\.[a-zA-Z]{2,}))( |$)/i"],
|
||||
"website": ["/(^| )(?<website>(https:\\/\\/|www\\.)([a-zA-Z0-9\\-]+\\.[a-zA-Z]{2,}))( |$)/i"],
|
||||
"phone": {
|
||||
"en": [
|
||||
"/(phone)(.*? {1,})(?<phone>([+0-9 \\/\\-\\(\\)]*[0-9]+[+0-9 \\/\\-\\(\\)]*){4,})( |[^ 0-9\\/\\(\\)+\\-]|$)/i"
|
||||
],
|
||||
"de": [
|
||||
"/(Tel|Rufn)(.*? {1,})(?<phone>([+0-9 \\/\\-\\(\\)]*[0-9]+[+0-9 \\/\\-\\(\\)]*){4,})( |[^ 0-9\\/\\(\\)+\\-]|$)/i"
|
||||
]
|
||||
},
|
||||
"date_format": [
|
||||
"Y-m-d",
|
||||
"Y.m.d",
|
||||
"Y/m/d",
|
||||
"d-m-Y",
|
||||
"d.m.Y",
|
||||
"d/m/Y",
|
||||
"m-d-Y",
|
||||
"m.d.Y",
|
||||
"m/d/Y",
|
||||
"M. d.Y",
|
||||
"M. d. Y",
|
||||
"M. d Y",
|
||||
"M. d,Y",
|
||||
"M. d, Y",
|
||||
"M d.Y",
|
||||
"M d. Y",
|
||||
"M d Y",
|
||||
"M d,Y",
|
||||
"M d, Y",
|
||||
"M, d.Y",
|
||||
"M, d. Y",
|
||||
"M, d Y",
|
||||
"M, d,Y",
|
||||
"M, d, Y"
|
||||
"Y-m-d", "Y.m.d", "Y/m/d",
|
||||
"d-m-Y", "d.m.Y", "d/m/Y",
|
||||
"m-d-Y", "m.d.Y", "m/d/Y",
|
||||
"M. d.Y", "M. d. Y", "M. d Y",
|
||||
"M. d,Y", "M. d, Y", "M d.Y",
|
||||
"M d. Y", "M d Y", "M d,Y",
|
||||
"M d, Y", "M, d.Y", "M, d. Y",
|
||||
"M, d Y", "M, d,Y", "M, d, Y",
|
||||
"d-m-y", "d.m.y", "d/m/y",
|
||||
"m-d-y", "m.d.y", "m/d/y"
|
||||
],
|
||||
"bill_no": {
|
||||
"en": [
|
||||
"/(inv.*?)(no|\\s|,|:|\\.|#)+(?<bill_no>.*?)( |$)/i",
|
||||
"/(#)(?<bill_no>.*?)( |$)/i"
|
||||
"/(invoice|inv.*? no)(.*? {1,})(?<bill_no>\\S*?)( |$)/i",
|
||||
"/(#)(?<bill_no>\\S*?)( |$)/i"
|
||||
],
|
||||
"de": [
|
||||
"/(rechnungsn.*?|beleg.*?)(?<bill_no>.*?)( |$)/i"
|
||||
"/(rechnungsn|rechnung n|belegn|beleg n)(.*? {1,})(?<bill_no>\\S*?)( |$)/i"
|
||||
]
|
||||
},
|
||||
"bill_date": {
|
||||
"en": [
|
||||
"/(inv.*?)(date.*?)(\\s|,|:|\\.)+(?<bill_date>.*?)( |$)/i",
|
||||
"/(date.*?)(\\s|,|:|\\.)+(?<bill_date>.*?)( |$)/i"
|
||||
"/(inv.*? )(date.*? )(?<bill_date>.{8,}?)( |$)/i",
|
||||
"/(date.*? )(?<bill_date>.{8,}?)( |$)/i"
|
||||
],
|
||||
"de": [
|
||||
"/(rechnungsdat.*?|belegdat.*?)(\\s|,|:|\\.)+(?<bill_date>.*?)( |$)/i"
|
||||
"/(rechnungsdat|belegdat|datum)(.*? )(?<bill_date>\\S{8,}?)( |$)/i"
|
||||
]
|
||||
},
|
||||
"bill_due": {
|
||||
"en": [
|
||||
"/(due date.*?)(\\s|,|:|\\.)+(?<bill_due>.*?)( |$)/i",
|
||||
"/(due.*?)(\\s|,|:|\\.)+(?<bill_due>.*?)( |$)/i"
|
||||
"/(due date.*? )(?<bill_due>\\S{8,}?)( |$)/i",
|
||||
"/(due.*? )(?<bill_due>\\S{8,}?)( |$)/i"
|
||||
],
|
||||
"de": [
|
||||
"/(fällig.*?)(\\s|,|:|\\.)+(?<bill_due>.*?)( |$)/i"
|
||||
"/(fällig.*? )(?<bill_due>\\S{8,}?)( |$)/i"
|
||||
]
|
||||
},
|
||||
"total_net": {
|
||||
"en": [
|
||||
"/(subtotal.*?|net.*?)(?<total_net>([0-9]+,*\\.*)+)/i"
|
||||
"/(subtotal|net)(.*? {1,})(?<total_net>([0-9]+,*\\.*)+)(?! *%)/i"
|
||||
],
|
||||
"de": [
|
||||
"/(netto.*?|zwischensumme.*?)(?<total_net>([0-9]+,*\\.*)+)/i"
|
||||
"/(netto|zwischensumme|betrag exk)(.*? {1,})(?<total_net>([0-9]+,*\\.*)+)(?! *%)/i"
|
||||
]
|
||||
},
|
||||
"total_discount": {
|
||||
"en": [
|
||||
"/(discount)(.*? {1,})(?<total_discount>([0-9]+,*\\.*)+)(?! *%)/i"
|
||||
],
|
||||
"de": [
|
||||
"/(rabatt)(.*? {1,})(?<total_discount>([0-9]+,*\\.*)+)(?! *%)/i"
|
||||
]
|
||||
},
|
||||
"total_shipping": {
|
||||
"en": [
|
||||
"/(fuel|handling|fright|shipping)(.*? {1,})(?<total_shipping>([0-9]+,*\\.*)+)(?! *%)/i"
|
||||
],
|
||||
"de": [
|
||||
"/(versand|transport|fracht)(.*? {1,})(?<total_shipping>([0-9]+,*\\.*)+)(?! *%)/i"
|
||||
]
|
||||
},
|
||||
"total_customs": {
|
||||
"en": [
|
||||
"/(customs)(.*? {1,})(?<total_customs>([0-9]+,*\\.*)+)(?! *%)/i"
|
||||
],
|
||||
"de": [
|
||||
"/(Einfuhr)(\\S* )(?<total_customs>([0-9]+,*\\.*)+)(?! *%)/i"
|
||||
]
|
||||
},
|
||||
"total_insurance": {
|
||||
"en": [
|
||||
"/(insurance)(.*? {1,})(?<total_insurance>([0-9]+,*\\.*)+)(?! *%)/i"
|
||||
],
|
||||
"de": [
|
||||
"/(versicherung)(.*? {1,})(?<total_insurance>([0-9]+,*\\.*)+)(?! *%)/i"
|
||||
]
|
||||
},
|
||||
"total_surcharge": {
|
||||
"en": [
|
||||
"/(fee|surcharge)(.*? {1,})(?<total_surcharge>([0-9]+,*\\.*)+)(?! *%)/i"
|
||||
],
|
||||
"de": [
|
||||
"/(gebühr)(.*? {1,})(?<total_surcharge>([0-9]+,*\\.*)+)(?! *%)/i"
|
||||
]
|
||||
},
|
||||
"total_tax": {
|
||||
"en": [
|
||||
"/(tax.*?)(?<total_tax>([0-9]+,*\\.*)+)/i"
|
||||
"/(VAT|tax)(.*? {1,})(?<total_tax>([0-9]+,*\\.*)+)(?! *%)/i"
|
||||
],
|
||||
"de": [
|
||||
"/(USt.*?|Mwst.*?|Umsatzsteuer.*?|Mehrwehrtsteuer.*?)(?<total_tax>([0-9]+,*\\.*)+)/i"
|
||||
"/(USt|Mwst|Umsatzst|Mehrwertst)(.*? {1,})(?<total_tax>([0-9]+,*\\.*)+)(?! *%)/i",
|
||||
"/( {1,})(USt|Mwst|Umsatzst|Mehrwertst)(?<total_tax>([0-9]+,*\\.*)+)(?! *%)/i"
|
||||
]
|
||||
},
|
||||
"tax_rate": {
|
||||
"en": [
|
||||
"/(VAT|tax)(.*? {1,})(?<tax_rate>([0-9]+,*\\.*)+)(?= *%)/i"
|
||||
],
|
||||
"de": [
|
||||
"/(USt|Mwst|Umsatzst|Mehrwertst)(.*? {1,})(?<tax_rate>([0-9]+,*\\.*)+)(?= *%)/i",
|
||||
"/( {1,})(?<tax_rate>([0-9]+,*\\.*)+)(?= *%)(.*?)(USt|Mwst|Umsatzst|Mehrwertst)/i"
|
||||
]
|
||||
},
|
||||
"total_gross": {
|
||||
"en": [
|
||||
"/(total.*?|gross.*?)(?<total_gross>([0-9]+,*\\.*)+)/i"
|
||||
"/(total|gross)(.*? {1,})(?<total_gross>([0-9]+,*\\.*)+)(?! *%)/i"
|
||||
],
|
||||
"de": [
|
||||
"/(betrag.*?|gesamt.*?|brutto|rechnungsbetrag.*?|summe.*?)(?<total_gross>([0-9]+,*\\.*)+)/i"
|
||||
"/(betrag|gesamt|brutto|summe)(.*? {1,})(?<total_gross>([0-9]+,*\\.*)+)(?! *%)/i"
|
||||
]
|
||||
},
|
||||
"item_table": {
|
||||
"en": {
|
||||
"headline": {
|
||||
"order": ["no.", "#"],
|
||||
"description": ["description", "name", "service", "product"],
|
||||
"quantity": ["qty", "quantity"],
|
||||
"price": ["price", "net", "gross"],
|
||||
"order": ["no.", "#", "pos"],
|
||||
"number": ["number"],
|
||||
"description": ["description", "name", "service", "product", "item"],
|
||||
"quantity": ["qty", "quantity", "hours"],
|
||||
"price": ["price", "rate"],
|
||||
"unit": ["unit"],
|
||||
"total": ["amount", "total", "price", "net", "gross"],
|
||||
"total": ["amount", "total", "gross"],
|
||||
"tax": ["tax"]
|
||||
},
|
||||
"parts": "/( *)(.+?)(\\s{3,}|$)/i",
|
||||
"row": {
|
||||
"order": "\\d+",
|
||||
"description": ".*?",
|
||||
"quantity": "[+-]?([0-9]{1,3}([,\\.][0-9]{3})*(,\\.[0-9]+)?|\\d*[,\\.]\\d+|\\d+)",
|
||||
"price": "[+-]?([0-9]{1,3}([,\\.][0-9]{3})*(,\\.[0-9]+)?|\\d*[,\\.]\\d+|\\d+)",
|
||||
"unit": ".*?",
|
||||
"total": "[+-]?([0-9]{1,3}([,\\.][0-9]{3})*(,\\.[0-9]+)?|\\d*[,\\.]\\d+|\\d+)",
|
||||
"tax": "[+-]?([0-9]{1,3}([,\\.][0-9]{3})*(,\\.[0-9]+)?|\\d*[,\\.]\\d+|\\d+)"
|
||||
"order": "/\\d+/i",
|
||||
"number": "/.*/i",
|
||||
"description": "/.*/i",
|
||||
"quantity": "/[+-]?([0-9]+,*\\.*)+/i",
|
||||
"price": "/[+-]?([0-9]+,*\\.*)+/i",
|
||||
"unit": "/.*/i",
|
||||
"total": "/[+-]?([0-9]+,*\\.*)+/i",
|
||||
"tax": "/[+-]?([0-9]+,*\\.*)+/i"
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"headline": {
|
||||
"order": ["Pos", "#", "Position"],
|
||||
"description": ["Beschreibung", "Bez", "Bezeichnung", "Leistung", "Produkt"],
|
||||
"quantity": ["Menge", "Anzahl"],
|
||||
"price": ["Einzel", "Preis", "Netto", "Net", "Brutto"],
|
||||
"number": ["Nr.", "Nummer"],
|
||||
"description": ["Beschreibung", "Bez", "Bezeichnung", "Leistung", "Produkt", "Artikel", "Name"],
|
||||
"quantity": ["Menge", "Anzahl", "Stunden"],
|
||||
"price": ["Einzel", "Preis", "Preis pro"],
|
||||
"unit": ["Einheit", "Einh"],
|
||||
"total": ["Gesamt", "Preis", "Netto", "Net", "Brutto"],
|
||||
"tax": ["MwSt", "USt"]
|
||||
"total": ["Gesamt", "Brutto", "Summe"],
|
||||
"tax": ["MwSt", "USt", "Steuer"]
|
||||
},
|
||||
"parts": "/( *)(.+?)(\\s{2,}|$)/i",
|
||||
"row": {
|
||||
"order": "\\d+",
|
||||
"description": ".*?",
|
||||
"quantity": "[+-]?([0-9]{1,3}([,\\.][0-9]{3})*(,\\.[0-9]+)?|\\d*[,\\.]\\d+|\\d+)",
|
||||
"price": "[+-]?([0-9]{1,3}([,\\.][0-9]{3})*(,\\.[0-9]+)?|\\d*[,\\.]\\d+|\\d+)",
|
||||
"unit": ".*?",
|
||||
"total": "[+-]?([0-9]{1,3}([,\\.][0-9]{3})*(,\\.[0-9]+)?|\\d*[,\\.]\\d+|\\d+)",
|
||||
"tax": "[+-]?([0-9]{1,3}([,\\.][0-9]{3})*(,\\.[0-9]+)?|\\d*[,\\.]\\d+|\\d+)"
|
||||
"order": "/\\d+/i",
|
||||
"number": "/.*/i",
|
||||
"description": "/.*/i",
|
||||
"quantity": "/[+-]?([0-9]+,*\\.*)+/i",
|
||||
"price": "/[+-]?([0-9]+,*\\.*)+/i",
|
||||
"unit": "/.*/i",
|
||||
"total": "/[+-]?([0-9]+,*\\.*)+/i",
|
||||
"tax": "/[+-]?([0-9]+,*\\.*)+/i"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@ return ['Billing' => [
|
|||
'AlreadyPaid' => 'Bereits bezahlt',
|
||||
'Amount' => 'Betrag',
|
||||
'Archive' => 'Archiev',
|
||||
'Internal' => 'Intern',
|
||||
'Error' => 'Fehler',
|
||||
'Billing' => 'Rechnungsstellung',
|
||||
'External' => 'Extern',
|
||||
'Bills' => 'Rechnungen',
|
||||
'Bonus' => 'Bonus',
|
||||
'Cashback' => 'Kennzeichnen',
|
||||
|
|
@ -51,6 +54,7 @@ return ['Billing' => [
|
|||
'MoneyTransfer' => 'Überweisung',
|
||||
'Name' => 'Name',
|
||||
'Net' => 'Netz',
|
||||
'Parse' => 'Rechnungserkennung',
|
||||
'Offer' => 'Angebot',
|
||||
'Original' => 'Original',
|
||||
'Payment' => 'Zahlung',
|
||||
|
|
@ -81,4 +85,9 @@ return ['Billing' => [
|
|||
'ShippingTerms' => 'Lieferbedingungen',
|
||||
'PaymentTerm' => 'Zahlungsbedingung',
|
||||
'ShippingTerm' => 'Lieferbedingung',
|
||||
'E_bill_items' => 'Es gibt ein Problem mit Ihren Artikeln.',
|
||||
'E_bill_taxes' => 'Der Gesamtsteuerbetrag stimmt nicht mit dem Steuerbetrag der Elemente überein. Möglicherweise gibt es ein Problem mit den Steuersätzen oder zusätzlichen Posten.',
|
||||
'E_bill_net' => 'Der Gesamtnettobetrag stimmt nicht mit dem Nettobetrag der Elemente überein. Mögliches Problem mit Steuersätzen oder zusätzlichen Positionen.',
|
||||
'E_bill_gross' => 'Der Gesamtbruttobetrag stimmt nicht mit dem Bruttobetrag der Elemente überein. Mögliches Problem mit Steuersätzen oder zusätzlichen Positionen.',
|
||||
'E_bill_unit' => 'Stückpreis stimmt nicht mit Gesamtpreis überein. Mögliche Probleme mit Preis, Menge oder Rabatten.',
|
||||
]];
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@ return ['Billing' => [
|
|||
'AlreadyPaid' => 'Already Paid',
|
||||
'Amount' => 'Amount',
|
||||
'Archive' => 'Archive',
|
||||
'Internal' => 'Internal',
|
||||
'Error' => 'Error',
|
||||
'Billing' => 'Billing',
|
||||
'External' => 'External',
|
||||
'Bills' => 'Bills',
|
||||
'Bonus' => 'Bonus',
|
||||
'Cashback' => 'Cash Back',
|
||||
|
|
@ -51,6 +54,7 @@ return ['Billing' => [
|
|||
'MoneyTransfer' => 'Money Transfer',
|
||||
'Name' => 'Name',
|
||||
'Net' => 'Net',
|
||||
'Parse' => 'Invoice recognition',
|
||||
'Offer' => 'Offer',
|
||||
'Original' => 'Original',
|
||||
'Payment' => 'Payment',
|
||||
|
|
@ -81,4 +85,10 @@ return ['Billing' => [
|
|||
'ShippingTerms' => 'Shipping Terms',
|
||||
'PaymentTerm' => 'Payment Term',
|
||||
'ShippingTerm' => 'Shipping Term',
|
||||
'ShippingTerm' => 'Shipping Term',
|
||||
'E_bill_items' => 'There is an issue with your bill items.',
|
||||
'E_bill_taxes' => 'The total tax amount doesn\'t match the tax amount of the elements. Potential issue with tax rates or additional items.',
|
||||
'E_bill_net' => 'The total net amount doesn\'t match the net amount of the elements. Potential issue with tax rates or additional items.',
|
||||
'E_bill_gross' => 'The total gross amount doesn\'t match the gross amount of the elements. Potential issue with tax rates or additional items.',
|
||||
'E_bill_unit' => 'Unit price doesn\'t match total price. Potential issues with price, quantity or discounts.',
|
||||
]];
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ $elements = $bill->elements;
|
|||
|
||||
$billTypes = $this->data['billtypes'] ?? [];
|
||||
|
||||
$archive = $bill->getFileByTypeName('original');
|
||||
$archive = $bill->getFileByTypeName('internal');
|
||||
|
||||
/** @var \Modules\Auditor\Models\Audit */
|
||||
$logs = $this->data['logs'] ?? [];
|
||||
|
|
@ -47,6 +47,33 @@ $disabled = $editable ? '' : ' disabled';
|
|||
$isNew = $archive->id === 0;
|
||||
|
||||
echo $this->data['nav']->render(); ?>
|
||||
<?php if (!$bill->isValid()) : ?>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<section class="portlet hl-1">
|
||||
<article class="hl-1">
|
||||
<ul>
|
||||
<?php if (!$bill->areElementsValid()) : ?>
|
||||
<li><?= $this->getHtml('E_bill_items'); ?></li>
|
||||
<?php endif; ?>
|
||||
<?php if (!$bill->validateTaxAmountElements()) : ?>
|
||||
<li><?= $this->getHtml('E_bill_taxes'); ?></li>
|
||||
<?php endif; ?>
|
||||
<?php if (!$bill->validateNetElements()) : ?>
|
||||
<li><?= $this->getHtml('E_bill_net'); ?></li>
|
||||
<?php endif; ?>
|
||||
<?php if (!$bill->validateGrossElements()) : ?>
|
||||
<li><?= $this->getHtml('E_bill_gross'); ?></li>
|
||||
<?php endif; ?>
|
||||
<?php if (!$bill->validatePriceQuantityElements()) : ?>
|
||||
<li><?= $this->getHtml('E_bill_unit'); ?></li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="tabview tab-2 col-simple">
|
||||
<div class="box">
|
||||
|
|
@ -55,9 +82,9 @@ echo $this->data['nav']->render(); ?>
|
|||
<li><label for="c-tab-2"><?= $this->getHtml('Items'); ?></label>
|
||||
<li><label for="c-tab-3"><?= $this->getHtml('Preview'); ?></label>
|
||||
<?php if (!$isNew) : ?><li><label for="c-tab-4"><?= $this->getHtml('Archive'); ?></label><?php endif; ?>
|
||||
<li><label for="c-tab-5"><?= $this->getHtml('Payment'); ?></label>
|
||||
<!--<li><label for="c-tab-5"><?= $this->getHtml('Payment'); ?></label>-->
|
||||
<li><label for="c-tab-6"><?= $this->getHtml('Files'); ?></label>
|
||||
<?php if (!$isNew) : ?><li><label for="c-tab-7"><?= $this->getHtml('Logs'); ?></label><?php endif; ?>
|
||||
<?php if (!$isNew && !empty($logs)) : ?><li><label for="c-tab-7"><?= $this->getHtml('Logs'); ?></label><?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content col-simple">
|
||||
|
|
@ -101,7 +128,7 @@ echo $this->data['nav']->render(); ?>
|
|||
<label for="iBillType"><?= $this->getHtml('Type'); ?></label>
|
||||
<select id="iBillType" name="bill_type"<?= $disabled; ?>>
|
||||
<?php foreach ($billTypes as $type) : ?>
|
||||
<option value="<?= $type->id; ?>"><?= $this->printHtml($type->getL11n()); ?>
|
||||
<option value="<?= $type->id; ?>"<?= $type->id === $bill->type->id ? ' selected' : ''; ?>><?= $this->printHtml($type->getL11n()); ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
|
@ -356,7 +383,7 @@ echo $this->data['nav']->render(); ?>
|
|||
<td>
|
||||
<?php endif; ?>
|
||||
<tfoot>
|
||||
<tr class="highlight-2">
|
||||
<tr class="hl-2">
|
||||
<td colspan="3"><?= $this->getHtml('Total'); ?>
|
||||
<td>
|
||||
<td><?= $bill->netDiscount->getAmount(2); ?>
|
||||
|
|
@ -385,7 +412,7 @@ echo $this->data['nav']->render(); ?>
|
|||
<select id="iBillPreviewType" name="bill_preview_type"
|
||||
data-action='[{"listener": "change", "action": [{"key": 1, "type": "dom.reload", "src": "iPreviewBill"}]}]'>
|
||||
<?php foreach ($billTypes as $type) : ?>
|
||||
<option value="<?= $type->id; ?>"><?= $this->printHtml($type->getL11n()); ?>
|
||||
<option value="<?= $type->id; ?>"<?= $type->id === $bill->type->id ? ' selected' : ''; ?>><?= $this->printHtml($type->getL11n()); ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
|
@ -417,6 +444,7 @@ echo $this->data['nav']->render(); ?>
|
|||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!--
|
||||
<input type="radio" id="c-tab-5" name="tabular-2">
|
||||
<div class="tab">
|
||||
<div class="row">
|
||||
|
|
@ -515,12 +543,14 @@ echo $this->data['nav']->render(); ?>
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<input type="radio" id="c-tab-6" name="tabular-2">
|
||||
<div class="tab col-simple">
|
||||
<?= $this->data['media-upload']->render('bill-file', 'files', '', $media); ?>
|
||||
</div>
|
||||
|
||||
<?php if (!$isNew) : ?>
|
||||
<?php if (!$isNew && !empty($bill)) : ?>
|
||||
<input type="radio" id="c-tab-7" name="tabular-2">
|
||||
<div class="tab">
|
||||
<div class="row">
|
||||
|
|
|
|||
|
|
@ -42,6 +42,18 @@ echo $this->data['nav']->render(); ?>
|
|||
<label>
|
||||
<i class="filter g-icon">filter_alt</i>
|
||||
</label>
|
||||
<td><?= $this->getHtml('External'); ?>
|
||||
<label for="billList-sort-1">
|
||||
<input type="radio" name="billList-sort" id="billList-sort-1">
|
||||
<i class="sort-asc g-icon">expand_less</i>
|
||||
</label>
|
||||
<label for="billList-sort-2">
|
||||
<input type="radio" name="billList-sort" id="billList-sort-2">
|
||||
<i class="sort-desc g-icon">expand_more</i>
|
||||
</label>
|
||||
<label>
|
||||
<i class="filter g-icon">filter_alt</i>
|
||||
</label>
|
||||
<td><?= $this->getHtml('Type'); ?>
|
||||
<label for="billList-sort-3">
|
||||
<input type="radio" name="billList-sort" id="billList-sort-3">
|
||||
|
|
@ -162,6 +174,7 @@ echo $this->data['nav']->render(); ?>
|
|||
<span class="checkmark"></span>
|
||||
</label>
|
||||
<td><a href="<?= $url; ?>"><?= $value->getNumber(); ?></a>
|
||||
<td><a href="<?= $url; ?>"><?= $value->external; ?></a>
|
||||
<td><a href="<?= $url; ?>"><?= $value->type->getL11n(); ?></a>
|
||||
<td><a class="content" href="<?= $supplier = UriFactory::build('purchase/supplier/view?{?}&id=' . $value->supplier->id); ?>"><?= $value->supplier->number; ?></a>
|
||||
<td><a class="content" href="<?= $supplier; ?>"><?= $this->printHtml($value->billTo); ?></a>
|
||||
|
|
@ -170,7 +183,7 @@ echo $this->data['nav']->render(); ?>
|
|||
<td><a href="<?= $url; ?>"><?= $value->billZip; ?></a>
|
||||
<td><a href="<?= $url; ?>"><?= $value->billCity; ?></a>
|
||||
<td><a href="<?= $url; ?>"><?= $value->billCountry; ?></a>
|
||||
<td><a href="<?= $url; ?>"><?= $this->getCurrency($value->netSales); ?></a>
|
||||
<td><a href="<?= $url; ?>"><?= $value->netSales->getAmount(); ?></a>
|
||||
<td><a href="<?= $url; ?>"><?= $value->createdAt->format('Y-m-d'); ?></a>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($count === 0) : ?>
|
||||
|
|
|
|||
|
|
@ -12,10 +12,17 @@
|
|||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
use Modules\Billing\Models\BillStatus;
|
||||
use phpOMS\Localization\ISO3166NameEnum;
|
||||
use phpOMS\Localization\ISO3166TwoEnum;
|
||||
use phpOMS\Localization\ISO4217Enum;
|
||||
use phpOMS\Localization\ISO639Enum;
|
||||
use phpOMS\Uri\UriFactory;
|
||||
|
||||
// Media helper functions (e.g. file icon generator)
|
||||
include __DIR__ . '/../../../Media/Theme/Backend/template-functions.php';
|
||||
$countryCodes = ISO3166TwoEnum::getConstants();
|
||||
$countries = ISO3166NameEnum::getConstants();
|
||||
$languages = ISO639Enum::getConstants();
|
||||
$currencies = ISO4217Enum::getConstants();
|
||||
|
||||
/**
|
||||
* @var \phpOMS\Views\View $this
|
||||
|
|
@ -27,24 +34,55 @@ $elements = $bill->elements;
|
|||
|
||||
$billTypes = $this->data['billtypes'] ?? [];
|
||||
|
||||
$originalType = $this->data['originalType'];
|
||||
$original = $bill->getFileByType($originalType);
|
||||
$archive = $bill->getFileByTypeName('external');
|
||||
|
||||
/** @var \Modules\Auditor\Models\Audit */
|
||||
$logs = $this->data['logs'] ?? [];
|
||||
|
||||
$editable = $bill->id === 0 || \in_array($bill->status, [BillStatus::DRAFT, BillStatus::UNPARSED]);
|
||||
$disabled = $editable ? '' : ' disabled';
|
||||
|
||||
$isNew = $archive->id === 0;
|
||||
|
||||
echo $this->data['nav']->render(); ?>
|
||||
<?php if (!$bill->isValid()) : ?>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<section class="portlet hl-1">
|
||||
<article class="hl-1">
|
||||
<ul>
|
||||
<?php if (!$bill->areElementsValid()) : ?>
|
||||
<li><?= $this->getHtml('E_bill_items'); ?></li>
|
||||
<?php endif; ?>
|
||||
<?php if (!$bill->validateTaxAmountElements()) : ?>
|
||||
<li><?= $this->getHtml('E_bill_taxes'); ?></li>
|
||||
<?php endif; ?>
|
||||
<?php if (!$bill->validateNetElements()) : ?>
|
||||
<li><?= $this->getHtml('E_bill_net'); ?></li>
|
||||
<?php endif; ?>
|
||||
<?php if (!$bill->validateGrossElements()) : ?>
|
||||
<li><?= $this->getHtml('E_bill_gross'); ?></li>
|
||||
<?php endif; ?>
|
||||
<?php if (!$bill->validatePriceQuantityElements()) : ?>
|
||||
<li><?= $this->getHtml('E_bill_unit'); ?></li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="tabview tab-2 col-simple">
|
||||
<div class="box">
|
||||
<ul class="tab-links">
|
||||
<li><label for="c-tab-1"><?= $this->getHtml('Invoice'); ?></label>
|
||||
<li><label for="c-tab-2"><?= $this->getHtml('Items'); ?></label>
|
||||
<li><label for="c-tab-3"><?= $this->getHtml('Preview'); ?></label>
|
||||
<li><label for="c-tab-4"><?= $this->getHtml('Original'); ?></label>
|
||||
<li><label for="c-tab-5"><?= $this->getHtml('Payment'); ?></label>
|
||||
<li><label for="c-tab-6"><?= $this->getHtml('Media'); ?></label>
|
||||
<li><label for="c-tab-7"><?= $this->getHtml('Logs'); ?></label>
|
||||
<li><label for="c-tab-3"><?= $this->getHtml('Internal'); ?></label>
|
||||
<?php if (!$isNew) : ?><li><label for="c-tab-4"><?= $this->getHtml('Archive'); ?></label><?php endif; ?>
|
||||
<!--<li><label for="c-tab-5"><?= $this->getHtml('Payment'); ?></label>-->
|
||||
<li><label for="c-tab-6"><?= $this->getHtml('Files'); ?></label>
|
||||
<?php if (!$isNew && !empty($logs)) : ?><li><label for="c-tab-7"><?= $this->getHtml('Logs'); ?></label><?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content col-simple">
|
||||
|
|
@ -56,61 +94,148 @@ echo $this->data['nav']->render(); ?>
|
|||
<form>
|
||||
<div class="portlet-head"><?= $this->getHtml('Invoice'); ?></div>
|
||||
<div class="portlet-body">
|
||||
<table class="layout wf-100">
|
||||
<tr><td><label for="iSource"><?= $this->getHtml('Source'); ?></label>
|
||||
<tr><td><span class="input"><button type="button" formaction=""><i class="g-icon">book</i></button><input type="text" id="iSource" name="source"></span>
|
||||
<tr><td><label for="iType"><?= $this->getHtml('Type'); ?></label>
|
||||
<tr><td><select id="iType" name="type">
|
||||
<option><?= $this->getHtml('Invoice'); ?>
|
||||
<option><?= $this->getHtml('Offer'); ?>
|
||||
<option><?= $this->getHtml('Confirmation'); ?>
|
||||
<option><?= $this->getHtml('DeliveryNote'); ?>
|
||||
<option><?= $this->getHtml('CreditNote'); ?>
|
||||
</select>
|
||||
<tr><td><label for="iClient"><?= $this->getHtml('Client'); ?></label>
|
||||
<tr><td><span class="input"><button type="button" formaction=""><i class="g-icon">book</i></button><input type="text" id="iClient" name="client"></span>
|
||||
<tr><td><label for="iDelivery"><?= $this->getHtml('Delivery'); ?></label>
|
||||
<tr><td><input type="datetime-local" id="iDelivery" name="delivery">
|
||||
<tr><td><label for="iDue"><?= $this->getHtml('Due'); ?></label>
|
||||
<tr><td><input type="datetime-local" id="iDue" name="due">
|
||||
<tr><td><label for="iFreightage"><?= $this->getHtml('Freightage'); ?></label>
|
||||
<tr><td><input type="number" id="iFreightage" name="freightage">
|
||||
<tr><td><label for="iShipment"><?= $this->getHtml('Shipment'); ?></label>
|
||||
<tr><td><select id="iShipment" name="shipment">
|
||||
<option>
|
||||
</select>
|
||||
<tr><td><label for="iTermsOfDelivery"><?= $this->getHtml('TermsOfDelivery'); ?></label>
|
||||
<tr><td><select id="iTermsOfDelivery" name="termsofdelivery">
|
||||
<option>
|
||||
</select>
|
||||
</table>
|
||||
<div class="form-group">
|
||||
<label for="iLanguage"><?= $this->getHtml('Language'); ?></label>
|
||||
<select id="iLanguage" name="bill_language"<?= $disabled; ?>>
|
||||
<?php foreach ($languages as $code => $language) : $code = \strtolower(\substr($code, 1)); ?>
|
||||
<option value="<?= $this->printHtml($code); ?>"<?= $code === $bill->language ? ' selected' : ''; ?>><?= $this->printHtml($language); ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iCurrency"><?= $this->getHtml('Currency'); ?></label>
|
||||
<select id="iCurrency" name="bill_currency"<?= $disabled; ?>>
|
||||
<?php foreach ($currencies as $code => $currency) : $code = \substr($code, 1); ?>
|
||||
<option value="<?= $this->printHtml($code); ?>"<?= $code === $bill->currency ? ' selected' : ''; ?>><?= $this->printHtml($currency); ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iSource"><?= $this->getHtml('Source'); ?></label>
|
||||
<span class="input">
|
||||
<button type="button" formaction="">
|
||||
<i class="g-icon">book</i>
|
||||
</button>
|
||||
<input type="text" id="iSource" name="bill_source"<?= $disabled; ?>>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iBillType"><?= $this->getHtml('Type'); ?></label>
|
||||
<select id="iBillType" name="bill_type"<?= $disabled; ?>>
|
||||
<?php foreach ($billTypes as $type) : ?>
|
||||
<option value="<?= $type->id; ?>"<?= $type->id === $bill->type->id ? ' selected' : ''; ?>><?= $this->printHtml($type->getL11n()); ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iClient"><?= $this->getHtml('Supplier'); ?></label>
|
||||
<div class="ipt-wrap">
|
||||
<div class="ipt-first">
|
||||
<span class="input">
|
||||
<button type="button" formaction="">
|
||||
<i class="g-icon">book</i>
|
||||
</button>
|
||||
<input type="text" id="iClient" name="bill_client" value="<?= $bill->client?->number ?? $bill->supplier?->number; ?>"<?= $disabled; ?>>
|
||||
</span>
|
||||
</div>
|
||||
<?php if (($bill->client?->id ?? 0) > 0) : ?>
|
||||
<div class="ipt-second">
|
||||
<a class="button" href="<?= UriFactory::build('{/base}/sales/client/view?id=' . $bill->client->id); ?>"><?= $this->getHtml('Client'); ?></a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iInvoiceDate"><?= $this->getHtml('Invoice'); ?></label>
|
||||
<input type="datetime-local" id="iInvoiceDate" name="bill_invoice_date"
|
||||
value="<?= $bill->createdAt->format('Y-m-d\TH:i'); ?>"<?= $disabled; ?>>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iDeliveryDate"><?= $this->getHtml('Delivery'); ?></label>
|
||||
<input type="datetime-local" id="iDeliveryDate" name="bill_delivery_date"
|
||||
value="<?= $bill->createdAt->format('Y-m-d\TH:i'); ?>"<?= $disabled; ?>>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iDueDate"><?= $this->getHtml('Due'); ?></label>
|
||||
<input type="datetime-local" id="iDueDate" name="bill_due"
|
||||
value="<?= (new \DateTime('now'))->format('Y-m-d\TH:i'); ?>"<?= $disabled; ?>>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iShipment"><?= $this->getHtml('Shipment'); ?></label>
|
||||
<select id="iShipment" name="bill_shipment_type"<?= $disabled; ?>>
|
||||
<option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iTermsOfDelivery"><?= $this->getHtml('TermsOfDelivery'); ?></label>
|
||||
<select id="iTermsOfDelivery" name="bill_termsofdelivery"<?= $disabled; ?>>
|
||||
<option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="portlet-foot"><input type="submit" value="<?= $this->getHtml('Create', '0', '0'); ?>" name="create-bill"></div>
|
||||
<?php if ($editable) : ?>
|
||||
<div class="portlet-foot">
|
||||
<input type="submit" value="<?= $this->getHtml('Create', '0', '0'); ?>" name="create-invoice">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-md-6 col-lg-4">
|
||||
<section class="portlet">
|
||||
<div class="portlet-head"><?= $this->getHtml('Invoice'); ?></div>
|
||||
<div class="portlet-head"><?= $this->getHtml('Billing'); ?></div>
|
||||
<div class="portlet-body">
|
||||
<form>
|
||||
<table class="layout wf-100">
|
||||
<tr><td><label for="iAddressS"><?= $this->getHtml('Addresses'); ?></label>
|
||||
<tr><td><select id="iAddressS" name="addressS">
|
||||
<option>
|
||||
</select>
|
||||
<tr><td><label for="iIRecipient"><?= $this->getHtml('Recipient'); ?></label>
|
||||
<tr><td><input type="text" id="iIRecipient" name="irecipient">
|
||||
<tr><td><label for="iAddress"><?= $this->getHtml('Address'); ?></label>
|
||||
<tr><td><input type="text" id="iAddress" name="address">
|
||||
<tr><td><label for="iZip"><?= $this->getHtml('Zip'); ?></label>
|
||||
<tr><td><input type="text" id="iZip" name="zip">
|
||||
<tr><td><label for="iCity"><?= $this->getHtml('City'); ?></label>
|
||||
<tr><td><input type="text" id="iCity" name="city">
|
||||
<tr><td><label for="iCountry"><?= $this->getHtml('Country'); ?></label>
|
||||
<tr><td><input type="text" id="iCountry" name="country">
|
||||
</table>
|
||||
<div class="form-group">
|
||||
<label for="iAddressListBill"><?= $this->getHtml('Addresses'); ?></label>
|
||||
<select id="iAddressListBill" name="bill_address_bill_list"<?= $disabled; ?>>
|
||||
<option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iRecipientBill"><?= $this->getHtml('Recipient'); ?></label>
|
||||
<input type="text" id="iRecipientBill" name="bill_recipient_bill" value="<?= $this->printHtml($bill->billTo); ?>"<?= $disabled; ?>>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iAddressBill"><?= $this->getHtml('Address'); ?></label>
|
||||
<input type="text" id="iAddressBill" name="bill_address_bill" value="<?= $this->printHtml($bill->billAddress); ?>"<?= $disabled; ?>>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iZipBill"><?= $this->getHtml('Zip'); ?></label>
|
||||
<input type="text" id="iZipBill" name="bill_address_bill" value="<?= $this->printHtml($bill->billZip); ?>"<?= $disabled; ?>>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iCityBill"><?= $this->getHtml('City'); ?></label>
|
||||
<input type="text" id="iCityBill" name="bill_city_bill" value="<?= $this->printHtml($bill->billCity); ?>"<?= $disabled; ?>>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iCountryBill"><?= $this->getHtml('Country'); ?></label>
|
||||
<select id="iCountryBill" name="bill_country_bill"<?= $disabled; ?>>
|
||||
<?php foreach ($countryCodes as $code3 => $code2) : ?>
|
||||
<option value="<?= $this->printHtml($code2); ?>"<?= $code2 === $bill->billCountry ? ' selected' : ''; ?>><?= $this->printHtml($countries[$code3]); ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iEmailBill"><?= $this->getHtml('Email'); ?></label>
|
||||
<input type="text" id="iEmailBill" name="bill_email_bill" value="<?= $this->printHtml($bill->billEmail); ?>"<?= $disabled; ?>>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -121,22 +246,42 @@ echo $this->data['nav']->render(); ?>
|
|||
<div class="portlet-head"><?= $this->getHtml('Delivery'); ?></div>
|
||||
<div class="portlet-body">
|
||||
<form>
|
||||
<table class="layout wf-100">
|
||||
<tr><td><label for="iAddressS"><?= $this->getHtml('Addresses'); ?></label>
|
||||
<tr><td><select id="iAddressS" name="addressS">
|
||||
<option>
|
||||
</select>
|
||||
<tr><td><label for="iDRecipient"><?= $this->getHtml('Recipient'); ?></label>
|
||||
<tr><td><input type="text" id="iDRecipient" name="drecipient">
|
||||
<tr><td><label for="iAddress"><?= $this->getHtml('Address'); ?></label>
|
||||
<tr><td><input type="text" id="iAddress" name="address">
|
||||
<tr><td><label for="iZip"><?= $this->getHtml('Zip'); ?></label>
|
||||
<tr><td><input type="text" id="iZip" name="zip">
|
||||
<tr><td><label for="iCity"><?= $this->getHtml('City'); ?></label>
|
||||
<tr><td><input type="text" id="iCity" name="city">
|
||||
<tr><td><label for="iCountry"><?= $this->getHtml('Country'); ?></label>
|
||||
<tr><td><input type="text" id="iCountry" name="country">
|
||||
</table>
|
||||
<div class="form-group">
|
||||
<label for="iAddressListDelivery"><?= $this->getHtml('Addresses'); ?></label>
|
||||
<select id="iAddressListDelivery" name="bill_address_delivery_list"<?= $disabled; ?>>
|
||||
<option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iRecipientDelivery"><?= $this->getHtml('Recipient'); ?></label>
|
||||
<input type="text" id="iRecipientDelivery" name="bill_recipient_delivery" value="<?= $this->printHtml($bill->shipTo); ?>"<?= $disabled; ?>>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iAddressDelivery"><?= $this->getHtml('Address'); ?></label>
|
||||
<input type="text" id="iAddressDelivery" name="bill_address_delivery" value="<?= $this->printHtml($bill->shipAddress); ?>"<?= $disabled; ?>>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iZipDelivery"><?= $this->getHtml('Zip'); ?></label>
|
||||
<input type="text" id="iZipDelivery" name="bill_zip_delivery" value="<?= $this->printHtml($bill->shipZip); ?>"<?= $disabled; ?>>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iCityDelivery"><?= $this->getHtml('City'); ?></label>
|
||||
<input type="text" id="iCityDelivery" name="bill_city_delivery" value="<?= $this->printHtml($bill->shipCity); ?>"<?= $disabled; ?>>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iCountryDelivery"><?= $this->getHtml('Country'); ?></label>
|
||||
<select id="iCountryDelivery" name="bill_country_delivery"<?= $disabled; ?>>
|
||||
<option value="" <?= $bill->shipTo === '' ? 'selected ' : ''; ?>><?= $this->getHtml('Select', '0', '0'); ?>
|
||||
<?php foreach ($countryCodes as $code3 => $code2) : ?>
|
||||
<option value="<?= $this->printHtml($code2); ?>"<?= $code2 === $bill->shipCountry ? ' selected' : ''; ?>><?= $this->printHtml($countries[$code3]); ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -147,56 +292,114 @@ echo $this->data['nav']->render(); ?>
|
|||
<div class="tab">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="portlet">
|
||||
<section class="portlet">
|
||||
<div class="portlet-head"><?= $this->getHtml('Invoice'); ?><i class="g-icon download btn end-xs">download</i></div>
|
||||
<div class="slider">
|
||||
<table class="default sticky" id="invoice-item-list">
|
||||
<table
|
||||
id="invoiceElements"
|
||||
class="default sticky"
|
||||
data-action="<?= \phpOMS\Uri\UriFactory::build('{/api}billing/bill/element?{?}&csrf={$CSRF}'); ?>"
|
||||
data-tag="form"
|
||||
data-ui-container="tbody"
|
||||
data-ui-element="tr"
|
||||
data-on-change="1"
|
||||
data-add-tpl=".oms-invoice-add">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<td style="min-width:150px"><?= $this->getHtml('Item'); ?>
|
||||
<td class="wf-100" style="min-width:150px"><?= $this->getHtml('Name'); ?>
|
||||
<td style="min-width:50px"><?= $this->getHtml('Quantity'); ?>
|
||||
<td style="min-width:90px"><?= $this->getHtml('Price'); ?>
|
||||
<td style="min-width:75px"><?= $this->getHtml('Quantity'); ?>
|
||||
<td style="min-width:90px"><?= $this->getHtml('Discount'); ?>
|
||||
<td style="min-width:90px"><?= $this->getHtml('DiscountP'); ?>
|
||||
<td style="min-width:90px"><?= $this->getHtml('Bonus'); ?>
|
||||
<td style="min-width:90px"><?= $this->getHtml('Tax'); ?>
|
||||
<td style="min-width:90px"><?= $this->getHtml('Net'); ?>
|
||||
<tbody>
|
||||
<?php foreach ($elements as $element) : ?>
|
||||
<tr>
|
||||
<td><i class="g-icon add">add</i> <i class="g-icon order-up">expand_less</i> <i class="g-icon order-down">expand_more</i>
|
||||
<td><span class="input"><button type="button" formaction=""><i class="g-icon">book</i></button><input name="" type="text" value="<?= $element->itemNumber; ?>" required></span>
|
||||
<td><textarea required><?= $element->itemName; ?></textarea>
|
||||
<td><input name="" type="number" min="0" value="<?= $element->quantity->getAmount(); ?>" required>
|
||||
<td><input name="" type="text" value="<?= $this->getCurrency($element->singleSalesPriceNet, ''); ?>">
|
||||
<td><input name="" type="number" min="0">
|
||||
<td><input name="" type="number" min="0" max="100" step="any">
|
||||
<td><input name="" type="number" min="0" step="any">
|
||||
<td><input name="" type="number" min="0" step="any">
|
||||
<td><?= $this->getCurrency($element->totalSalesPriceNet); ?>
|
||||
<?php endforeach; ?>
|
||||
<tr>
|
||||
<td><i class="g-icon">add</i> <i class="g-icon order-up">expand_less</i> <i class="g-icon order-down">expand_more</i>
|
||||
<td><span class="input"><button type="button" formaction=""><i class="g-icon">book</i></button><input name="" type="text" required></span>
|
||||
<td><textarea required></textarea>
|
||||
<td><input name="" type="number" min="0" value="0" required>
|
||||
<td><input name="" type="text">
|
||||
<td><input name="" type="number" min="0">
|
||||
<td><input name="" type="number" min="0" max="100" step="any">
|
||||
<td><input name="" type="number" min="0" step="any">
|
||||
<td><input name="" type="number" min="0" step="any">
|
||||
<td>
|
||||
<td style="min-width:75px"><?= $this->getHtml('Bonus'); ?>
|
||||
<td style="min-width:90px"><?= $this->getHtml('Price'); ?>
|
||||
<td style="min-width:75px"><?= $this->getHtml('TaxP'); ?>
|
||||
<td><?= $this->getHtml('Net'); ?>
|
||||
<td><?= $this->getHtml('Margin'); ?>
|
||||
<tbody class="oms-ordercontainer">
|
||||
<?php if ($editable) : ?>
|
||||
<template class="oms-invoice-add">
|
||||
<tr data-id="">
|
||||
<td>
|
||||
<i class="g-icon order-up">expand_less</i>
|
||||
<i class="g-icon order-down">expand_more</i>
|
||||
<i class="g-icon btn remove-form">close</i>
|
||||
<td><span class="input">
|
||||
<button type="button" formaction="">
|
||||
<label><i class="g-icon">book</i></label>
|
||||
</button><input name="item_number" type="text" autocomplete="off"></span>
|
||||
<td><textarea name="item_description" autocomplete="off"></textarea>
|
||||
<td><input name="item_quantity" type="number" step="any" value="" autocomplete="off">
|
||||
<td><input name="item_discountp" type="number" step="0.01" value="" autocomplete="off">
|
||||
<td><input name="item_discountr" type="number" min="-100" max="100" step="0.01" value="" autocomplete="off">
|
||||
<td><input name="item_bonus" type="number" step="0.01" value="" autocomplete="off">
|
||||
<td><input name="item_price" type="number" step="0.01" value="" autocomplete="off">
|
||||
<td><input name="item_taxr" type="number" step="0.01" value="" autocomplete="off">
|
||||
<td>
|
||||
<td>
|
||||
</tr>
|
||||
</template>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($elements as $element) : ?>
|
||||
<tr>
|
||||
<td><?php if ($editable) : ?>
|
||||
<i class="g-icon order-up">expand_less</i>
|
||||
<i class="g-icon order-down">expand_more</i>
|
||||
<i class="g-icon btn remove-form">close</i>
|
||||
<?php endif; ?>
|
||||
<td><span class="input">
|
||||
<button type="button" formaction="">
|
||||
<i class="g-icon">book</i>
|
||||
</button><input name="item_number" autocomplete="off" type="text" value="<?= $element->itemNumber; ?>"<?= $disabled; ?>></span>
|
||||
<td><textarea name="item_description" autocomplete="off"<?= $disabled; ?>><?= $element->itemName; ?></textarea>
|
||||
<td><input name="item_quantity" autocomplete="off" type="number" step="any" value="<?= $element->quantity->sub($element->discountQ)->getAmount($element->container->quantityDecimals); ?>"<?= $disabled; ?>>
|
||||
<td><input name="item_discountp" autocomplete="off" type="number" step="0.01" value="<?= $element->singleDiscountP->getAmount(); ?>"<?= $disabled; ?>>
|
||||
<td><input name="item_discountr" autocomplete="off" type="number" step="0.01" value="<?= $element->singleDiscountR->getAmount(); ?>"<?= $disabled; ?>>
|
||||
<td><input name="item_bonus" autocomplete="off" type="number" min="-100" max="100" step="0.01" value="<?= $element->discountQ->getAmount($element->container->quantityDecimals); ?>"<?= $disabled; ?>>
|
||||
<td><input name="item_price" autocomplete="off" type="number" step="0.01" value="<?= $element->singleSalesPriceNet->getFloat(); ?>"<?= $disabled; ?>>
|
||||
<td><input name="item_taxr" autocomplete="off" type="number" step="0.01" value="<?= $element->taxR->getAmount(); ?>"<?= $disabled; ?>>
|
||||
<td><?= $this->getCurrency($element->totalSalesPriceNet, symbol: ''); ?>
|
||||
<td><?= \number_format($element->totalSalesPriceNet->value === 0 ? 0 : (1 - $element->totalPurchasePriceNet->value / $element->totalSalesPriceNet->value) * 100, 2); ?>%
|
||||
<?php endforeach; ?>
|
||||
<?php if ($editable) : ?>
|
||||
<tr data-id="0">
|
||||
<td><i class="g-icon order-up">expand_less</i>
|
||||
<i class="g-icon order-down">expand_more</i>
|
||||
<i class="g-icon btn remove-form">close</i>
|
||||
<td><span class="input">
|
||||
<button type="button" formaction="">
|
||||
<i class="g-icon">book</i></button><input name="item_number" type="text" autocomplete="off"></span>
|
||||
<td><textarea name="item_description" autocomplete="off"></textarea>
|
||||
<td><input name="item_quantity" type="number" step="any" value="" autocomplete="off">
|
||||
<td><input name="item_discountp" type="number" step="0.01" value="" autocomplete="off">
|
||||
<td><input name="item_discountr" type="number" min="-100" max="100" step="0.01" value="" autocomplete="off">
|
||||
<td><input name="item_bonus" type="number" step="0.01" value="" autocomplete="off">
|
||||
<td><input name="item_price" type="number" step="0.01" value="" autocomplete="off">
|
||||
<td><input name="item_taxr" type="number" step="0.01" value="" autocomplete="off">
|
||||
<td>
|
||||
<td>
|
||||
<?php endif; ?>
|
||||
<tfoot>
|
||||
<tr class="hl-2">
|
||||
<td colspan="3"><?= $this->getHtml('Total'); ?>
|
||||
<td>
|
||||
<td><?= $bill->netDiscount->getAmount(2); ?>
|
||||
<td><?= \number_format($bill->netDiscount->value === 0 ? 0 : ($bill->netDiscount->value / ($bill->netSales->value + $bill->netDiscount->value)) * 100, 2); ?>%
|
||||
<td>
|
||||
<td>
|
||||
<td><?= $bill->taxP->getAmount(2); ?>
|
||||
<td><?= $bill->netSales->getAmount(2); ?>
|
||||
<td><?= \number_format($bill->netSales->value === 0 ? 0 : (1 - $bill->netCosts->value / $bill->netSales->value) * 100, 2); ?>%
|
||||
</table>
|
||||
</div>
|
||||
<div class="portlet-foot">
|
||||
<?= $this->getHtml('Freightage'); ?>: 0.00 -
|
||||
<?= $this->getHtml('Net'); ?>: <?= $this->getCurrency($bill->netSales); ?> -
|
||||
<?= $this->getHtml('Tax'); ?>: 0.00 -
|
||||
<?= $this->getHtml('Total'); ?>: <?= $this->getCurrency($bill->grossSales); ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if ($editable) : ?>
|
||||
<div class="box">
|
||||
<input type="submit" class="add-form" value="<?= $this->getHtml('Add', '0', '0'); ?>" form="invoiceElements">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -208,7 +411,7 @@ echo $this->data['nav']->render(); ?>
|
|||
<select id="iBillPreviewType" name="bill_preview_type"
|
||||
data-action='[{"listener": "change", "action": [{"key": 1, "type": "dom.reload", "src": "iPreviewBill"}]}]'>
|
||||
<?php foreach ($billTypes as $type) : ?>
|
||||
<option value="<?= $type->id; ?>"><?= $this->printHtml($type->getL11n()); ?>
|
||||
<option value="<?= $type->id; ?>"<?= $type->id === $bill->type->id ? ' selected' : ''; ?>><?= $this->printHtml($type->getL11n()); ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
|
@ -225,66 +428,147 @@ echo $this->data['nav']->render(); ?>
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!$isNew) : ?>
|
||||
<input type="radio" id="c-tab-4" name="tabular-2">
|
||||
<div class="tab col-simple">
|
||||
<div class="row col-simple">
|
||||
<?php if ($bill->status === BillStatus::DRAFT
|
||||
|| $bill->status === BillStatus::UNPARSED
|
||||
|| $bill->status === BillStatus::ACTIVE
|
||||
) : ?>
|
||||
<div>
|
||||
<div class="col-xs-12 col-sm-3 box">
|
||||
<form id="iInvoiceRecognition"
|
||||
action="<?= UriFactory::build('{/api}bill/parse?id=' . $bill->id . '&async=0'); ?>"
|
||||
method="post"
|
||||
data-redirect="<?= UriFactory::build('{%}'); ?>">
|
||||
<input type="submit" value="<?= $this->getHtml('Parse') ?>">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="col-simple">
|
||||
<div class="col-xs-12 col-simple">
|
||||
<section id="mediaFile" class="portlet col-simple">
|
||||
<div class="portlet-body col-simple">
|
||||
<?php if ($original->id > 0) : ?>
|
||||
<iframe class="col-simple" id="iOriginal" src="<?= UriFactory::build('Resources/mozilla/Pdf/web/viewer.html?file=' . \urlencode(UriFactory::build('{/api}media/export?id=' . $original->id))); ?>" allowfullscreen></iframe>
|
||||
<?php endif; ?>
|
||||
<iframe id="iBillArchive"
|
||||
class="col-simple"
|
||||
src="<?= UriFactory::build('{/api}media/export') . '?id=' . $archive->id; ?>"
|
||||
loading="lazy" allowfullscreen></iframe>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!--
|
||||
<input type="radio" id="c-tab-5" name="tabular-2">
|
||||
<div class="tab">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-6 col-lg-4">
|
||||
<section class="box wf-100">
|
||||
<header><h1><?= $this->getHtml('Payment'); ?></h1></header>
|
||||
<div class="inner">
|
||||
<section class="portlet">
|
||||
<div class="portlet-head"><?= $this->getHtml('Payment'); ?></div>
|
||||
<div class="portlet-body">
|
||||
<form>
|
||||
<table class="layout wf-100">
|
||||
<tr><td><label for="iType"><?= $this->getHtml('Type'); ?></label>
|
||||
<tr><td><select id="iType" name="type">
|
||||
<option>
|
||||
</select>
|
||||
<tr><td><label for="iType"><?= $this->getHtml('Type'); ?></label>
|
||||
<tr><td><select id="iType" name="type">
|
||||
<option><?= $this->getHtml('MoneyTransfer'); ?>
|
||||
<option><?= $this->getHtml('Prepaid'); ?>
|
||||
<option><?= $this->getHtml('AlreadyPaid'); ?>
|
||||
<option><?= $this->getHtml('CreditCard'); ?>
|
||||
<option><?= $this->getHtml('DirectDebit'); ?>
|
||||
</select>
|
||||
<tr><td><label for="iDue"><?= $this->getHtml('Due'); ?></label>
|
||||
<tr><td><input type="datetime-local" id="iDue" name="due">
|
||||
<tr><td><label for="iDue"><?= $this->getHtml('Due'); ?> - <?= $this->getHtml('Cashback'); ?></label>
|
||||
<tr><td><input type="datetime-local" id="iDue" name="due">
|
||||
<tr><td><label for="iCashBack"><?= $this->getHtml('Cashback'); ?></label>
|
||||
<tr><td><input type="number" id="iCashBack" name="cashback">
|
||||
<tr><td><label for="iDue"><?= $this->getHtml('Due'); ?> - <?= $this->getHtml('Cashback'); ?> 2</label>
|
||||
<tr><td><input type="datetime-local" id="iDue" name="due">
|
||||
<tr><td><label for="iCashBack2"><?= $this->getHtml('Cashback'); ?> 2</label>
|
||||
<tr><td><input type="number" id="iCashBack2" name="cashback2">
|
||||
<tr><td><input type="submit" value="<?= $this->getHtml('Create', '0', '0'); ?>" name="create-bill">
|
||||
</table>
|
||||
<div class="form-group">
|
||||
<label for="iPaymentTypeList"><?= $this->getHtml('Types'); ?></label>
|
||||
<input type="text" id="iPaymentTypeList" name="bill_payment_type_list"<?= $disabled; ?>>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iPaymentType"><?= $this->getHtml('Type'); ?></label>
|
||||
<select id="iPaymentType" name="bill_payment_type"<?= $disabled; ?>>
|
||||
<option><?= $this->getHtml('MoneyTransfer'); ?>
|
||||
<option><?= $this->getHtml('Prepaid'); ?>
|
||||
<option><?= $this->getHtml('AlreadyPaid'); ?>
|
||||
<option><?= $this->getHtml('CreditCard'); ?>
|
||||
<option><?= $this->getHtml('DirectDebit'); ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iPaymentDueDate"><?= $this->getHtml('Due'); ?></label>
|
||||
<input type="datetime-local" id="iPaymentDueDate" name="bill_payment_due_date"<?= $disabled; ?>>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="portlet-separator"></div>
|
||||
|
||||
<div class="portlet-body">
|
||||
<div class="form-group">
|
||||
<label for="iPaymentCashbackDate1"><?= $this->getHtml('Cashback'); ?></label>
|
||||
<input type="datetime-local" id="iPaymentCashbackDate1" name="bill_payment_cashback_date1"<?= $disabled; ?>>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iPaymentCashbackAmount1"><?= $this->getHtml('Cashback'); ?></label>
|
||||
<input type="number" id="iPaymentCashbackAmount1" name="bill_payment_cashback_amount1"<?= $disabled; ?>>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="portlet-separator"></div>
|
||||
|
||||
<div class="portlet-body">
|
||||
<div class="form-group">
|
||||
<label for="iPaymentCashbackDate2"><?= $this->getHtml('Cashback'); ?></label>
|
||||
<input type="datetime-local" id="iPaymentCashbackDate2" name="bill_payment_cashback_date2"<?= $disabled; ?>>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="iPaymentCashbackAmount2"><?= $this->getHtml('Cashback'); ?></label>
|
||||
<input type="number" id="iPaymentCashbackAmount2" name="bill_payment_cashback_amount2"<?= $disabled; ?>>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-md-6 col-lg-8">
|
||||
<section class="portlet">
|
||||
<div class="portlet-head"><?= $this->getHtml('PaymentPlan'); ?></div>
|
||||
<table id="paymentPlan"
|
||||
class="default sticky"
|
||||
data-tag="form"
|
||||
data-ui-container="tbody"
|
||||
data-ui-element="tr"
|
||||
data-add-tpl=".oms-payment-add">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<td class="wf-100"><?= $this->getHtml('Date'); ?>
|
||||
<td><?= $this->getHtml('Amount'); ?>
|
||||
<tbody class="oms-ordercontainer">
|
||||
<template class="oms-payment-add">
|
||||
<tr data-id="">
|
||||
<td><?php if ($editable) : ?>
|
||||
<i class="g-icon order-up">expand_less</i>
|
||||
<i class="g-icon order-down">expand_more</i>
|
||||
<i class="g-icon btn remove-form">close</i>
|
||||
<?php endif; ?>
|
||||
<td><input type="datetime-local" autocomplete="off" required>
|
||||
<td><input type="number" value="" autocomplete="off" required>
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<?php if ($editable) : ?>
|
||||
<div class="box">
|
||||
<input type="submit" class="add-payment-form" value="<?= $this->getHtml('Add', '0', '0'); ?>" form="paymentPlan">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<input type="radio" id="c-tab-6" name="tabular-2">
|
||||
<div class="tab col-simple">
|
||||
<?= $this->data['media-upload']->render('bill-file', 'files', '', $bill->files); ?>
|
||||
</div>
|
||||
|
||||
<?php if (!$isNew && !empty($logs)) : ?>
|
||||
<input type="radio" id="c-tab-7" name="tabular-2">
|
||||
<div class="tab">
|
||||
<div class="row">
|
||||
|
|
@ -295,8 +579,8 @@ echo $this->data['nav']->render(); ?>
|
|||
<thead>
|
||||
<tr>
|
||||
<td><?= $this->getHtml('ID', '0', '0'); ?>
|
||||
<td><?= $this->getHtml('Trigger', 'Auditor', 'Backend'); ?>
|
||||
<td><?= $this->getHtml('Action', 'Auditor', 'Backend'); ?>
|
||||
<td class="wf-100"><?= $this->getHtml('Trigger', 'Auditor', 'Backend'); ?>
|
||||
<td><?= $this->getHtml('CreatedBy', 'Auditor', 'Backend'); ?>
|
||||
<td><?= $this->getHtml('CreatedAt', 'Auditor', 'Backend'); ?>
|
||||
<tbody>
|
||||
|
|
@ -306,22 +590,24 @@ echo $this->data['nav']->render(); ?>
|
|||
?>
|
||||
<tr data-href="<?= $url; ?>">
|
||||
<td><a href="<?= $url; ?>"><?= $audit->id; ?></a>
|
||||
<td><a href="<?= $url; ?>"><?= $audit->trigger; ?></a>
|
||||
<td><?php if ($audit->old === null) : echo $this->getHtml('CREATE', 'Auditor', 'Backend'); ?>
|
||||
<?php elseif ($audit->old !== null && $audit->new !== null) : echo $this->getHtml('UPDATE', 'Auditor', 'Backend'); ?>
|
||||
<?php elseif ($audit->new === null) : echo $this->getHtml('DELETE', 'Auditor', 'Backend'); ?>
|
||||
<?php else : echo $this->getHtml('UNKNOWN', 'Auditor', 'Backend'); ?>
|
||||
<?php endif; ?>
|
||||
<td><a href="<?= $url; ?>"><?= $audit->trigger; ?></a>
|
||||
<td><a class="content"
|
||||
href="<?= UriFactory::build('{/base}/admin/account/settings?id=' . $audit->createdBy->id); ?>"><?= $this->printHtml(
|
||||
$this->renderUserName('%3$s %2$s %1$s', [$audit->createdBy->name1, $audit->createdBy->name2, $audit->createdBy->name3, $audit->createdBy->login])
|
||||
); ?></a>
|
||||
<td><a href="<?= $url; ?>"><?= $audit->createdAt->format('Y-m-d'); ?></a>
|
||||
<td><a href="<?= $url; ?>"><?= $audit->createdAt->format('Y-m-d H:i'); ?></a>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,18 @@ echo $this->data['nav']->render(); ?>
|
|||
<label>
|
||||
<i class="filter g-icon">filter_alt</i>
|
||||
</label>
|
||||
<td><?= $this->getHtml('External'); ?>
|
||||
<label for="billList-sort-1">
|
||||
<input type="radio" name="billList-sort" id="billList-sort-1">
|
||||
<i class="sort-asc g-icon">expand_less</i>
|
||||
</label>
|
||||
<label for="billList-sort-2">
|
||||
<input type="radio" name="billList-sort" id="billList-sort-2">
|
||||
<i class="sort-desc g-icon">expand_more</i>
|
||||
</label>
|
||||
<label>
|
||||
<i class="filter g-icon">filter_alt</i>
|
||||
</label>
|
||||
<td><?= $this->getHtml('Type'); ?>
|
||||
<label for="billList-sort-3">
|
||||
<input type="radio" name="billList-sort" id="billList-sort-3">
|
||||
|
|
@ -164,6 +176,7 @@ echo $this->data['nav']->render(); ?>
|
|||
<span class="checkmark"></span>
|
||||
</label>
|
||||
<td><a href="<?= $url; ?>"><?= $value->getNumber(); ?></a>
|
||||
<td><a href="<?= $url; ?>"><?= $value->external; ?></a>
|
||||
<td><a href="<?= $url; ?>"><?= $value->type->getL11n(); ?></a>
|
||||
<td><a class="content" href="<?= $supplier = UriFactory::build('purchase/supplier/view?{?}&id=' . $value->supplier->id); ?>"><?= $value->supplier->number; ?></a>
|
||||
<td><a class="content" href="<?= $supplier; ?>"><?= $this->printHtml($value->billTo); ?></a>
|
||||
|
|
@ -176,7 +189,7 @@ echo $this->data['nav']->render(); ?>
|
|||
: ISO3166NameEnum::getByName(
|
||||
ISO3166TwoEnum::getName($value->billCountry)
|
||||
); ?></a>
|
||||
<td><a href="<?= $url; ?>"><?= $value->grossCosts->getAmount(); ?></a>
|
||||
<td><a href="<?= $url; ?>"><?= $value->grossSales->getAmount(); ?></a>
|
||||
<td><a href="<?= $url; ?>"><?= $value->billDate?->format('Y-m-d'); ?></a>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($count === 0) : ?>
|
||||
|
|
|
|||
|
|
@ -1,350 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package Modules\Billing
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
use phpOMS\System\File\FileUtils;
|
||||
use phpOMS\Uri\UriFactory;
|
||||
|
||||
// Media helper functions (e.g. file icon generator)
|
||||
include __DIR__ . '/../../../Media/Theme/Backend/template-functions.php';
|
||||
|
||||
/**
|
||||
* @var \phpOMS\Views\View $this
|
||||
*/
|
||||
|
||||
$bill = $this->data['bill'];
|
||||
$elements = $bill->elements;
|
||||
|
||||
$previewType = $this->data['previewType'];
|
||||
$originalType = $this->data['originalType'];
|
||||
$billPdf = $bill->getFileByType($previewType);
|
||||
$original = $bill->getFileByType($originalType);
|
||||
$media = $bill->files;
|
||||
|
||||
echo $this->data['nav']->render(); ?>
|
||||
|
||||
<div class="tabview tab-2">
|
||||
<div class="box">
|
||||
<ul class="tab-links">
|
||||
<li><label for="c-tab-1"><?= $this->getHtml('Invoice'); ?></label>
|
||||
<li><label for="c-tab-2"><?= $this->getHtml('Items'); ?></label>
|
||||
<li><label for="c-tab-3"><?= $this->getHtml('Preview'); ?></label>
|
||||
<li><label for="c-tab-4"><?= $this->getHtml('Original'); ?></label>
|
||||
<li><label for="c-tab-5"><?= $this->getHtml('Payment'); ?></label>
|
||||
<li><label for="c-tab-6"><?= $this->getHtml('Media'); ?></label>
|
||||
<li><label for="c-tab-7"><?= $this->getHtml('Logs'); ?></label>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<input type="radio" id="c-tab-1" name="tabular-2" checked>
|
||||
<div class="tab">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-6 col-lg-4">
|
||||
<section class="portlet">
|
||||
<form>
|
||||
<div class="portlet-head"><?= $this->getHtml('Invoice'); ?></div>
|
||||
<div class="portlet-body">
|
||||
<table class="layout wf-100">
|
||||
<tr><td><label for="iSource"><?= $this->getHtml('Source'); ?></label>
|
||||
<tr><td><span class="input"><button type="button" formaction=""><i class="g-icon">book</i></button><input type="text" id="iSource" name="source"></span>
|
||||
<tr><td><label for="iType"><?= $this->getHtml('Type'); ?></label>
|
||||
<tr><td><select id="iType" name="type">
|
||||
<option><?= $this->getHtml('Invoice'); ?>
|
||||
<option><?= $this->getHtml('Offer'); ?>
|
||||
<option><?= $this->getHtml('Confirmation'); ?>
|
||||
<option><?= $this->getHtml('DeliveryNote'); ?>
|
||||
<option><?= $this->getHtml('CreditNote'); ?>
|
||||
</select>
|
||||
<tr><td><label for="iClient"><?= $this->getHtml('Client'); ?></label>
|
||||
<tr><td><span class="input"><button type="button" formaction=""><i class="g-icon">book</i></button><input type="text" id="iClient" name="client"></span>
|
||||
<tr><td><label for="iDelivery"><?= $this->getHtml('Delivery'); ?></label>
|
||||
<tr><td><input type="datetime-local" id="iDelivery" name="delivery">
|
||||
<tr><td><label for="iDue"><?= $this->getHtml('Due'); ?></label>
|
||||
<tr><td><input type="datetime-local" id="iDue" name="due">
|
||||
<tr><td><label for="iFreightage"><?= $this->getHtml('Freightage'); ?></label>
|
||||
<tr><td><input type="number" id="iFreightage" name="freightage">
|
||||
<tr><td><label for="iShipment"><?= $this->getHtml('Shipment'); ?></label>
|
||||
<tr><td><select id="iShipment" name="shipment">
|
||||
<option>
|
||||
</select>
|
||||
<tr><td><label for="iTermsOfDelivery"><?= $this->getHtml('TermsOfDelivery'); ?></label>
|
||||
<tr><td><select id="iTermsOfDelivery" name="termsofdelivery">
|
||||
<option>
|
||||
</select>
|
||||
</table>
|
||||
</div>
|
||||
<div class="portlet-foot"><input type="submit" value="<?= $this->getHtml('Create', '0', '0'); ?>" name="create-bill"></div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-md-6 col-lg-4">
|
||||
<section class="portlet">
|
||||
<div class="portlet-head"><?= $this->getHtml('Invoice'); ?></div>
|
||||
<div class="portlet-body">
|
||||
<form>
|
||||
<table class="layout wf-100">
|
||||
<tr><td><label for="iAddressS"><?= $this->getHtml('Addresses'); ?></label>
|
||||
<tr><td><select id="iAddressS" name="addressS">
|
||||
<option>
|
||||
</select>
|
||||
<tr><td><label for="iIRecipient"><?= $this->getHtml('Recipient'); ?></label>
|
||||
<tr><td><input type="text" id="iIRecipient" name="irecipient">
|
||||
<tr><td><label for="iAddress"><?= $this->getHtml('Address'); ?></label>
|
||||
<tr><td><input type="text" id="iAddress" name="address">
|
||||
<tr><td><label for="iZip"><?= $this->getHtml('Zip'); ?></label>
|
||||
<tr><td><input type="text" id="iZip" name="zip">
|
||||
<tr><td><label for="iCity"><?= $this->getHtml('City'); ?></label>
|
||||
<tr><td><input type="text" id="iCity" name="city">
|
||||
<tr><td><label for="iCountry"><?= $this->getHtml('Country'); ?></label>
|
||||
<tr><td><input type="text" id="iCountry" name="country">
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-md-6 col-lg-4">
|
||||
<section class="portlet">
|
||||
<div class="portlet-head"><?= $this->getHtml('Delivery'); ?></div>
|
||||
<div class="portlet-body">
|
||||
<form>
|
||||
<table class="layout wf-100">
|
||||
<tr><td><label for="iAddressS"><?= $this->getHtml('Addresses'); ?></label>
|
||||
<tr><td><select id="iAddressS" name="addressS">
|
||||
<option>
|
||||
</select>
|
||||
<tr><td><label for="iDRecipient"><?= $this->getHtml('Recipient'); ?></label>
|
||||
<tr><td><input type="text" id="iDRecipient" name="drecipient">
|
||||
<tr><td><label for="iAddress"><?= $this->getHtml('Address'); ?></label>
|
||||
<tr><td><input type="text" id="iAddress" name="address">
|
||||
<tr><td><label for="iZip"><?= $this->getHtml('Zip'); ?></label>
|
||||
<tr><td><input type="text" id="iZip" name="zip">
|
||||
<tr><td><label for="iCity"><?= $this->getHtml('City'); ?></label>
|
||||
<tr><td><input type="text" id="iCity" name="city">
|
||||
<tr><td><label for="iCountry"><?= $this->getHtml('Country'); ?></label>
|
||||
<tr><td><input type="text" id="iCountry" name="country">
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="radio" id="c-tab-2" name="tabular-2">
|
||||
<div class="tab">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="portlet">
|
||||
<div class="portlet-head"><?= $this->getHtml('Invoice'); ?><i class="g-icon download btn end-xs">download</i></div>
|
||||
<table class="default sticky" id="invoice-item-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<td><?= $this->getHtml('Item'); ?>
|
||||
<td class="wf-100"><?= $this->getHtml('Name'); ?>
|
||||
<td><?= $this->getHtml('Quantity'); ?>
|
||||
<td><?= $this->getHtml('Price'); ?>
|
||||
<td><?= $this->getHtml('Discount'); ?>
|
||||
<td><?= $this->getHtml('DiscountP'); ?>
|
||||
<td><?= $this->getHtml('Bonus'); ?>
|
||||
<td><?= $this->getHtml('Tax'); ?>
|
||||
<td><?= $this->getHtml('Net'); ?>
|
||||
<tbody>
|
||||
<?php foreach ($elements as $element) : ?>
|
||||
<tr>
|
||||
<td><i class="g-icon add">add</i> <i class="g-icon order-up">expand_less</i> <i class="g-icon order-down">expand_more</i>
|
||||
<td><span class="input"><button type="button" formaction=""><i class="g-icon">book</i></button><input name="" type="text" value="<?= $element->itemNumber; ?>" required></span>
|
||||
<td><textarea required><?= $element->itemName; ?></textarea>
|
||||
<td><input name="" type="number" min="0" value="<?= $element->quantity; ?>" required>
|
||||
<td><input name="" type="text" value="<?= $this->getCurrency($element->singleSalesPriceNet); ?>">
|
||||
<td><input name="" type="number" min="0">
|
||||
<td><input name="" type="number" min="0" max="100" step="any">
|
||||
<td><input name="" type="number" min="0" step="any">
|
||||
<td><input name="" type="number" min="0" step="any">
|
||||
<td><?= $this->getCurrency($element->totalSalesPriceNet); ?>
|
||||
<?php endforeach; ?>
|
||||
<tr>
|
||||
<td><i class="g-icon">add</i> <i class="g-icon order-up">expand_less</i> <i class="g-icon order-down">expand_more</i>
|
||||
<td><span class="input"><button type="button" formaction=""><i class="g-icon">book</i></button><input name="" type="text" required></span>
|
||||
<td><textarea required></textarea>
|
||||
<td><input name="" type="number" min="0" value="0" required>
|
||||
<td><input name="" type="text">
|
||||
<td><input name="" type="number" min="0">
|
||||
<td><input name="" type="number" min="0" max="100" step="any">
|
||||
<td><input name="" type="number" min="0" step="any">
|
||||
<td><input name="" type="number" min="0" step="any">
|
||||
<td>
|
||||
</table>
|
||||
<div class="portlet-foot">
|
||||
<?= $this->getHtml('Freightage'); ?>: 0.00 -
|
||||
<?= $this->getHtml('Net'); ?>: <?= $this->getCurrency($bill->netSales); ?> -
|
||||
<?= $this->getHtml('Tax'); ?>: 0.00 -
|
||||
<?= $this->getHtml('Total'); ?>: <?= $this->getCurrency($bill->grossSales); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="radio" id="c-tab-3" name="tabular-2">
|
||||
<div class="tab">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<section id="mediaFile" class="portlet">
|
||||
<div class="portlet-body">
|
||||
<?php if ($billPdf->id > 0) : ?>
|
||||
<iframe style="min-height: 600px;" data-form="iUiSettings" data-name="iframeHelper" id="iHelperFrame" src="<?= UriFactory::build('{/backend}Resources/mozilla/Pdf/web/viewer.html{?}&file=' . \urlencode(($billPdf->isAbsolute ? '' : '/../../../../') . $billPdf->getPath())); ?>" allowfullscreen></iframe>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="radio" id="c-tab-4" name="tabular-2">
|
||||
<div class="tab">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<section id="mediaFile" class="portlet">
|
||||
<div class="portlet-body">
|
||||
<?php if ($original->id > 0) : ?>
|
||||
<iframe style="min-height: 600px;" data-form="iUiSettings" data-name="iframeHelper" id="iHelperFrame" src="<?= UriFactory::build('{/backend}Resources/mozilla/Pdf/web/viewer.html{?}&file=' . \urlencode(($original->isAbsolute ? '' : '/../../../../') . $original->getPath())); ?>" allowfullscreen></iframe>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="radio" id="c-tab-5" name="tabular-2">
|
||||
<div class="tab">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-6 col-lg-4">
|
||||
<section class="box wf-100">
|
||||
<header><h1><?= $this->getHtml('Payment'); ?></h1></header>
|
||||
<div class="inner">
|
||||
<form>
|
||||
<table class="layout wf-100">
|
||||
<tr><td><label for="iType"><?= $this->getHtml('Type'); ?></label>
|
||||
<tr><td><select id="iType" name="type">
|
||||
<option>
|
||||
</select>
|
||||
<tr><td><label for="iType"><?= $this->getHtml('Type'); ?></label>
|
||||
<tr><td><select id="iType" name="type">
|
||||
<option><?= $this->getHtml('MoneyTransfer'); ?>
|
||||
<option><?= $this->getHtml('Prepaid'); ?>
|
||||
<option><?= $this->getHtml('AlreadyPaid'); ?>
|
||||
<option><?= $this->getHtml('CreditCard'); ?>
|
||||
<option><?= $this->getHtml('DirectDebit'); ?>
|
||||
</select>
|
||||
<tr><td><label for="iDue"><?= $this->getHtml('Due'); ?></label>
|
||||
<tr><td><input type="datetime-local" id="iDue" name="due">
|
||||
<tr><td><label for="iDue"><?= $this->getHtml('Due'); ?> - <?= $this->getHtml('Cashback'); ?></label>
|
||||
<tr><td><input type="datetime-local" id="iDue" name="due">
|
||||
<tr><td><label for="iCashBack"><?= $this->getHtml('Cashback'); ?></label>
|
||||
<tr><td><input type="number" id="iCashBack" name="cashback">
|
||||
<tr><td><label for="iDue"><?= $this->getHtml('Due'); ?> - <?= $this->getHtml('Cashback'); ?> 2</label>
|
||||
<tr><td><input type="datetime-local" id="iDue" name="due">
|
||||
<tr><td><label for="iCashBack2"><?= $this->getHtml('Cashback'); ?> 2</label>
|
||||
<tr><td><input type="number" id="iCashBack2" name="cashback2">
|
||||
<tr><td><input type="submit" value="<?= $this->getHtml('Create', '0', '0'); ?>" name="create-bill">
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="radio" id="c-tab-6" name="tabular-2">
|
||||
<div class="tab">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-6 col-lg-4">
|
||||
<section class="box wf-100">
|
||||
<header><h1><?= $this->getHtml('Media'); ?></h1></header>
|
||||
|
||||
<div class="inner">
|
||||
<form>
|
||||
<table class="layout wf-100">
|
||||
<tbody>
|
||||
<tr><td colspan="2"><label for="iMedia"><?= $this->getHtml('Media'); ?></label>
|
||||
<tr><td><input type="text" id="iMedia" placeholder="File"><td><button><?= $this->getHtml('Select'); ?></button>
|
||||
<tr><td colspan="2"><label for="iUpload"><?= $this->getHtml('Upload'); ?></label>
|
||||
<tr><td><input type="file" id="iUpload" form="fTask"><input form="fTask" type="hidden" name="type"><td>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-md-6 col-lg-8">
|
||||
<div class="portlet">
|
||||
<div class="portlet-head"><?= $this->getHtml('Media'); ?><i class="g-icon download btn end-xs">download</i></div>
|
||||
<table class="default sticky" id="invoice-item-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<td>
|
||||
<td class="wf-100"><?= $this->getHtml('Name'); ?>
|
||||
<td><?= $this->getHtml('Type'); ?>
|
||||
<tbody>
|
||||
<?php foreach ($media as $file) :
|
||||
$url = $file->extension === 'collection'
|
||||
? UriFactory::build('{/base}/media/list?path=' . \rtrim($file->getVirtualPath(), '/') . '/' . $file->name)
|
||||
: UriFactory::build('{/base}/media/view?id=' . $file->id
|
||||
. '&path={?path}' . (
|
||||
$file->id === 0
|
||||
? '/' . $file->name
|
||||
: ''
|
||||
)
|
||||
);
|
||||
|
||||
$icon = $fileIconFunction(FileUtils::getExtensionType($file->extension));
|
||||
?>
|
||||
<tr data-href="<?= $url; ?>">
|
||||
<td>
|
||||
<td data-label="<?= $this->getHtml('Type'); ?>"><a href="<?= $url; ?>"><i class="g-icon"><?= $this->printHtml($icon); ?></i></a>
|
||||
<td><a href="<?= $url; ?>"><?= $file->name; ?></a>
|
||||
<td><a href="<?= $url; ?>"><?= $file->extension; ?></a>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="radio" id="c-tab-7" name="tabular-2">
|
||||
<div class="tab">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="box wf-100">
|
||||
<table class="default sticky">
|
||||
<caption><?= $this->getHtml('Logs'); ?><i class="g-icon end-xs download btn">download</i></caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>IP
|
||||
<td><?= $this->getHtml('ID', '0', '0'); ?>
|
||||
<td><?= $this->getHtml('Name'); ?>
|
||||
<td class="wf-100"><?= $this->getHtml('Log'); ?>
|
||||
<td><?= $this->getHtml('Date'); ?>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><?= $this->printHtml($this->request->getOrigin()); ?>
|
||||
<td><?= $this->printHtml((string) $this->request->header->account); ?>
|
||||
<td><?= $this->printHtml((string) $this->request->header->account); ?>
|
||||
<td>Create Invoice
|
||||
<td><?= $this->printHtml((new \DateTime('now'))->format('Y-m-d H:i:s')); ?>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -36,6 +36,7 @@ final class Autoloader
|
|||
__DIR__ . '/../',
|
||||
__DIR__ . '/../MainRepository/',
|
||||
__DIR__ . '/../../',
|
||||
__DIR__ . '/../../../',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
352
tests/Models/InvoiceRecognitionTest.php
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package tests
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Billing\tests\Models;
|
||||
|
||||
use Modules\Billing\Models\Bill;
|
||||
use Modules\Billing\Models\InvoiceRecognition;
|
||||
use phpOMS\Ai\Ocr\Tesseract\TesseractOcr;
|
||||
use phpOMS\Utils\Parser\Pdf\PdfParser;
|
||||
|
||||
require_once __DIR__ . '/../Autoloader.php';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class InvoiceRecognitionTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider billList
|
||||
*/
|
||||
public function testNetSales($json, $content) : void
|
||||
{
|
||||
$billObj = new Bill();
|
||||
InvoiceRecognition::detect($billObj, $content);
|
||||
|
||||
$test = \json_decode(\file_get_contents($json), true);
|
||||
|
||||
self::assertEquals($test['netSales'], $billObj->netSales->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider billList
|
||||
*/
|
||||
public function testTaxRate($json, $content) : void
|
||||
{
|
||||
$billObj = new Bill();
|
||||
InvoiceRecognition::detect($billObj, $content);
|
||||
|
||||
$test = \json_decode(\file_get_contents($json), true);
|
||||
|
||||
self::assertEquals($test['tax_rate'], \reset($billObj->elements)->taxR->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider billList
|
||||
*/
|
||||
public function testGrossSales($json, $content) : void
|
||||
{
|
||||
$billObj = new Bill();
|
||||
InvoiceRecognition::detect($billObj, $content);
|
||||
|
||||
$test = \json_decode(\file_get_contents($json), true);
|
||||
|
||||
self::assertEquals($test['grossSales'], $billObj->grossSales->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider billList
|
||||
*/
|
||||
public function testTaxAmount($json, $content) : void
|
||||
{
|
||||
$billObj = new Bill();
|
||||
InvoiceRecognition::detect($billObj, $content);
|
||||
|
||||
$test = \json_decode(\file_get_contents($json), true);
|
||||
|
||||
self::assertEquals($test['tax_amount'], $billObj->taxP->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider billList
|
||||
*/
|
||||
public function testBillDate($json, $content) : void
|
||||
{
|
||||
$billObj = new Bill();
|
||||
InvoiceRecognition::detect($billObj, $content);
|
||||
|
||||
$test = \json_decode(\file_get_contents($json), true);
|
||||
|
||||
self::assertEquals($test['bill_date'], $billObj->billDate?->format('Y-m-d'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider billList
|
||||
*/
|
||||
public function testBillLanguage($json, $content) : void
|
||||
{
|
||||
$billObj = new Bill();
|
||||
InvoiceRecognition::detect($billObj, $content);
|
||||
|
||||
$test = \json_decode(\file_get_contents($json), true);
|
||||
|
||||
self::assertEquals($test['language'], $billObj->language);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider billList
|
||||
*/
|
||||
public function testBillCurrency($json, $content) : void
|
||||
{
|
||||
$billObj = new Bill();
|
||||
InvoiceRecognition::detect($billObj, $content);
|
||||
|
||||
$test = \json_decode(\file_get_contents($json), true);
|
||||
|
||||
self::assertEquals($test['currency'], $billObj->currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider billList
|
||||
*/
|
||||
public function testIban($json, $content) : void
|
||||
{
|
||||
$identifierContent = \file_get_contents(__DIR__ . '/../../Models/bill_identifier.json');
|
||||
if ($identifierContent === false) {
|
||||
$identifierContent = '{}';
|
||||
}
|
||||
|
||||
/** @var array $identifiers */
|
||||
$identifiers = \json_decode($identifierContent, true);
|
||||
|
||||
$test = \json_decode(\file_get_contents($json), true);
|
||||
|
||||
$lines = \explode("\n", $content);
|
||||
foreach ($lines as $line => $value) {
|
||||
if (empty(\trim($value))) {
|
||||
unset($lines[$line]);
|
||||
}
|
||||
}
|
||||
$lines = \array_values($lines);
|
||||
|
||||
self::assertEquals(
|
||||
\str_replace(' ', '', $test['iban']),
|
||||
\str_replace(' ', '', InvoiceRecognition::findIban($lines, $identifiers['iban']))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider billList
|
||||
*/
|
||||
public function testVATId($json, $content) : void
|
||||
{
|
||||
$identifierContent = \file_get_contents(__DIR__ . '/../../Models/bill_identifier.json');
|
||||
if ($identifierContent === false) {
|
||||
$identifierContent = '{}';
|
||||
}
|
||||
|
||||
/** @var array $identifiers */
|
||||
$identifiers = \json_decode($identifierContent, true);
|
||||
|
||||
$test = \json_decode(\file_get_contents($json), true);
|
||||
|
||||
$lines = \explode("\n", $content);
|
||||
foreach ($lines as $line => $value) {
|
||||
if (empty(\trim($value))) {
|
||||
unset($lines[$line]);
|
||||
}
|
||||
}
|
||||
$lines = \array_values($lines);
|
||||
|
||||
self::assertEquals(
|
||||
$test['vat_id'],
|
||||
InvoiceRecognition::findVat($lines, $identifiers['vat_id'][$test['language']])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider billList
|
||||
*/
|
||||
public function testTaxId($json, $content) : void
|
||||
{
|
||||
$identifierContent = \file_get_contents(__DIR__ . '/../../Models/bill_identifier.json');
|
||||
if ($identifierContent === false) {
|
||||
$identifierContent = '{}';
|
||||
}
|
||||
|
||||
/** @var array $identifiers */
|
||||
$identifiers = \json_decode($identifierContent, true);
|
||||
|
||||
$test = \json_decode(\file_get_contents($json), true);
|
||||
|
||||
$lines = \explode("\n", $content);
|
||||
foreach ($lines as $line => $value) {
|
||||
if (empty(\trim($value))) {
|
||||
unset($lines[$line]);
|
||||
}
|
||||
}
|
||||
$lines = \array_values($lines);
|
||||
|
||||
self::assertEquals(
|
||||
$test['tax_id'],
|
||||
InvoiceRecognition::findTaxId($lines, $identifiers['tax_id'][$test['language']])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider billList
|
||||
*/
|
||||
public function testWebsite($json, $content) : void
|
||||
{
|
||||
$identifierContent = \file_get_contents(__DIR__ . '/../../Models/bill_identifier.json');
|
||||
if ($identifierContent === false) {
|
||||
$identifierContent = '{}';
|
||||
}
|
||||
|
||||
/** @var array $identifiers */
|
||||
$identifiers = \json_decode($identifierContent, true);
|
||||
|
||||
$test = \json_decode(\file_get_contents($json), true);
|
||||
|
||||
$lines = \explode("\n", $content);
|
||||
foreach ($lines as $line => $value) {
|
||||
if (empty(\trim($value))) {
|
||||
unset($lines[$line]);
|
||||
}
|
||||
}
|
||||
$lines = \array_values($lines);
|
||||
|
||||
self::assertEquals(
|
||||
$test['website'],
|
||||
InvoiceRecognition::findWebsite($lines, $identifiers['website'])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider billList
|
||||
*/
|
||||
public function testEmail($json, $content) : void
|
||||
{
|
||||
$identifierContent = \file_get_contents(__DIR__ . '/../../Models/bill_identifier.json');
|
||||
if ($identifierContent === false) {
|
||||
$identifierContent = '{}';
|
||||
}
|
||||
|
||||
/** @var array $identifiers */
|
||||
$identifiers = \json_decode($identifierContent, true);
|
||||
|
||||
$test = \json_decode(\file_get_contents($json), true);
|
||||
|
||||
$lines = \explode("\n", $content);
|
||||
foreach ($lines as $line => $value) {
|
||||
if (empty(\trim($value))) {
|
||||
unset($lines[$line]);
|
||||
}
|
||||
}
|
||||
$lines = \array_values($lines);
|
||||
|
||||
self::assertEquals(
|
||||
$test['email'],
|
||||
InvoiceRecognition::findEmail($lines, $identifiers['email'])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider billList
|
||||
*/
|
||||
public function testPhone($json, $content) : void
|
||||
{
|
||||
$identifierContent = \file_get_contents(__DIR__ . '/../../Models/bill_identifier.json');
|
||||
if ($identifierContent === false) {
|
||||
$identifierContent = '{}';
|
||||
}
|
||||
|
||||
/** @var array $identifiers */
|
||||
$identifiers = \json_decode($identifierContent, true);
|
||||
|
||||
$test = \json_decode(\file_get_contents($json), true);
|
||||
|
||||
$lines = \explode("\n", $content);
|
||||
foreach ($lines as $line => $value) {
|
||||
if (empty(\trim($value))) {
|
||||
unset($lines[$line]);
|
||||
}
|
||||
}
|
||||
$lines = \array_values($lines);
|
||||
|
||||
self::assertEquals(
|
||||
\str_replace(' ', '', $test['phone']),
|
||||
\str_replace(' ', '', InvoiceRecognition::findPhone($lines, $identifiers['phone'][$test['language']]))
|
||||
);
|
||||
}
|
||||
|
||||
public static array $billList = [];
|
||||
|
||||
public function billList()
|
||||
{
|
||||
/*
|
||||
if (\str_ends_with(__DIR__ . '/bills/12.png', 'pdf')) {
|
||||
$content = PdfParser::pdf2text(__DIR__ . '/bills/12.png');
|
||||
} else {
|
||||
$ocr = new TesseractOcr();
|
||||
|
||||
$content = $ocr->parseImage(__DIR__ . '/bills/12.png');
|
||||
}
|
||||
|
||||
return [
|
||||
[
|
||||
__DIR__ . '/bills/12.json',
|
||||
$content
|
||||
]
|
||||
];
|
||||
*/
|
||||
|
||||
if (!empty(self::$billList)) {
|
||||
return self::$billList;
|
||||
}
|
||||
|
||||
$files = \scandir(__DIR__ . '/bills/');
|
||||
foreach ($files as $bill) {
|
||||
if ($bill === '.' || $bill === '..' || \str_ends_with($bill, '.json')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts = \explode('.', $bill);
|
||||
$count = \count($parts);
|
||||
unset($parts[$count - 1]);
|
||||
|
||||
if (\str_ends_with(__DIR__ . '/bills/' . $bill, 'pdf')) {
|
||||
$content = PdfParser::pdf2text(__DIR__ . '/bills/' . $bill);
|
||||
} else {
|
||||
$ocr = new TesseractOcr();
|
||||
|
||||
$content = $ocr->parseImage(__DIR__ . '/bills/' . $bill);
|
||||
}
|
||||
|
||||
$element = [
|
||||
__DIR__ . '/bills/' . \implode('', $parts) . '.json',
|
||||
$content
|
||||
];
|
||||
|
||||
self::$billList[] = $element;
|
||||
}
|
||||
|
||||
return self::$billList;
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass() : void
|
||||
{
|
||||
self::$billList = [];
|
||||
}
|
||||
}
|
||||
34
tests/Models/bills/1.json
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"invoice_number": "20191242",
|
||||
"grossSales": 2800000,
|
||||
"netSales": 2800000,
|
||||
"tax_amount": 0,
|
||||
"tax_rate": 0,
|
||||
"bill_date": "2019-09-12",
|
||||
"delivery_date": "",
|
||||
"due_date": "2019-09-12",
|
||||
"vat_id": "DE12345678",
|
||||
"tax_id": "12/112233/44221",
|
||||
"address": {
|
||||
"name": "fortytools gmbh",
|
||||
"address": "Georgsplatz 10",
|
||||
"city": "20099 Hamburg",
|
||||
"country": ""
|
||||
},
|
||||
"iban": "",
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"description": "",
|
||||
"quantity": 10000,
|
||||
"unit": "pro Woche",
|
||||
"price": 280000,
|
||||
"total": 280000
|
||||
}
|
||||
],
|
||||
"language": "de",
|
||||
"currency": "EUR",
|
||||
"email": "info@fortytools-cleaning.com",
|
||||
"website": "",
|
||||
"phone": "+49-40-609 407 89 - 0"
|
||||
}
|
||||
BIN
tests/Models/bills/1.png
Normal file
|
After Width: | Height: | Size: 171 KiB |
26
tests/Models/bills/10.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"invoice_number": "12345",
|
||||
"grossSales": 11200000,
|
||||
"netSales": 11200000,
|
||||
"tax_amount": 0,
|
||||
"tax_rate": 0,
|
||||
"bill_date": "",
|
||||
"delivery_date": "",
|
||||
"service_date": "",
|
||||
"due_date": "",
|
||||
"vat_id": "",
|
||||
"tax_id": "",
|
||||
"address": {
|
||||
"name": "",
|
||||
"address": "",
|
||||
"city": "",
|
||||
"country": ""
|
||||
},
|
||||
"iban": "DE34233004333401",
|
||||
"elements": [],
|
||||
"language": "de",
|
||||
"currency": "EUR",
|
||||
"email": "mail@musterfirma.com",
|
||||
"website": "www.musterfirma.com",
|
||||
"phone": "(+49) 1234/98 76 54"
|
||||
}
|
||||
BIN
tests/Models/bills/10.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
tests/Models/bills/11.jpg
Normal file
|
After Width: | Height: | Size: 33 KiB |
26
tests/Models/bills/11.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"invoice_number": "",
|
||||
"grossSales": 20440000,
|
||||
"netSales": 0,
|
||||
"tax_amount": 0,
|
||||
"tax_rate": 0,
|
||||
"bill_date": "",
|
||||
"delivery_date": "",
|
||||
"service_date": "",
|
||||
"due_date": "",
|
||||
"vat_id": "",
|
||||
"tax_id": "",
|
||||
"address": {
|
||||
"name": "",
|
||||
"address": "",
|
||||
"city": "",
|
||||
"country": ""
|
||||
},
|
||||
"iban": "",
|
||||
"elements": [],
|
||||
"language": "de",
|
||||
"currency": "EUR",
|
||||
"email": "",
|
||||
"website": "",
|
||||
"phone": ""
|
||||
}
|
||||
26
tests/Models/bills/12.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"invoice_number": "10005",
|
||||
"grossSales": 1160000,
|
||||
"netSales": 1000000,
|
||||
"tax_amount": 160000,
|
||||
"tax_rate": 160000,
|
||||
"bill_date": "2020-09-22",
|
||||
"delivery_date": "",
|
||||
"service_date": "",
|
||||
"due_date": "",
|
||||
"vat_id": "DE814878557",
|
||||
"tax_id": "122/5719/4368",
|
||||
"address": {
|
||||
"name": "easzbill GmbH",
|
||||
"address": "Düsselstr. 21",
|
||||
"city": "41564 Kaarst",
|
||||
"country": ""
|
||||
},
|
||||
"iban": "DE58300700240509944500",
|
||||
"elements": [],
|
||||
"language": "de",
|
||||
"currency": "EUR",
|
||||
"email": "support@easybill.de",
|
||||
"website": "www.easybil.de",
|
||||
"phone": "+49 2154 897 01 - 20"
|
||||
}
|
||||
BIN
tests/Models/bills/12.png
Normal file
|
After Width: | Height: | Size: 333 KiB |
26
tests/Models/bills/14.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"invoice_number": "2021011",
|
||||
"grossSales": 15969800,
|
||||
"netSales": 13420000,
|
||||
"tax_amount": 2549800,
|
||||
"tax_rate": 190000,
|
||||
"bill_date": "2021-03-29",
|
||||
"delivery_date": "",
|
||||
"service_date": "",
|
||||
"due_date": "2021-04-28",
|
||||
"vat_id": "DE234567891",
|
||||
"tax_id": "",
|
||||
"address": {
|
||||
"name": "Invoice Office GmbH",
|
||||
"address": "Inge-Meysel Str. 8a",
|
||||
"city": "85053 Ingolstadt",
|
||||
"country": ""
|
||||
},
|
||||
"iban": "DE20000111122223332103",
|
||||
"elements": [],
|
||||
"language": "de",
|
||||
"currency": "EUR",
|
||||
"email": "demo@invoiceoffice.de",
|
||||
"website": "",
|
||||
"phone": "0049123456789"
|
||||
}
|
||||
BIN
tests/Models/bills/14.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
26
tests/Models/bills/15.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"invoice_number": "2021012",
|
||||
"grossSales": 92391600,
|
||||
"netSales": 77640000,
|
||||
"tax_amount": 14751600,
|
||||
"tax_rate": 190000,
|
||||
"bill_date": "2021-03-29",
|
||||
"delivery_date": "",
|
||||
"service_date": "",
|
||||
"due_date": "2021-04-28",
|
||||
"vat_id": "DE123456789",
|
||||
"tax_id": "",
|
||||
"address": {
|
||||
"name": "Invoice Office GmbH",
|
||||
"address": "Inge-Meysel Str. 8a",
|
||||
"city": "85053 Ingolstadt",
|
||||
"country": ""
|
||||
},
|
||||
"iban": "DE20000111122223332103",
|
||||
"elements": [],
|
||||
"language": "de",
|
||||
"currency": "EUR",
|
||||
"email": "demo@invoiceoffice.de",
|
||||
"website": "",
|
||||
"phone": "0049123456789"
|
||||
}
|
||||
BIN
tests/Models/bills/15.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
26
tests/Models/bills/16.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"invoice_number": "1001",
|
||||
"grossSales": 4658900,
|
||||
"netSales": 3915000,
|
||||
"tax_amount": 743900,
|
||||
"tax_rate": 190000,
|
||||
"bill_date": "2021-02-17",
|
||||
"delivery_date": "",
|
||||
"service_date": "",
|
||||
"due_date": "",
|
||||
"vat_id": "",
|
||||
"tax_id": "",
|
||||
"address": {
|
||||
"name": "Ihre Firma",
|
||||
"address": "Musterstraße X",
|
||||
"city": "12345 Musterstadt",
|
||||
"country": ""
|
||||
},
|
||||
"iban": "DE85123456789012345678",
|
||||
"elements": [],
|
||||
"language": "de",
|
||||
"currency": "EUR",
|
||||
"email": "firmaxyz@gmail.de",
|
||||
"website": "www.firmaxyz.de",
|
||||
"phone": "+49 1234/12 34 56"
|
||||
}
|
||||
BIN
tests/Models/bills/16.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
26
tests/Models/bills/17.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"invoice_number": "DE-001",
|
||||
"grossSales": 1654100,
|
||||
"netSales": 1390000,
|
||||
"tax_amount": 264100,
|
||||
"tax_rate": 190000,
|
||||
"bill_date": "2019-01-29",
|
||||
"delivery_date": "",
|
||||
"service_date": "",
|
||||
"due_date": "2019-01-29",
|
||||
"vat_id": "",
|
||||
"tax_id": "",
|
||||
"address": {
|
||||
"name": "European Auto Parts",
|
||||
"address": "Schillingbrücke 58",
|
||||
"city": "73111 Berlin",
|
||||
"country": ""
|
||||
},
|
||||
"iban": "DE9112345678",
|
||||
"elements": [],
|
||||
"language": "de",
|
||||
"currency": "EUR",
|
||||
"email": "",
|
||||
"website": "",
|
||||
"phone": ""
|
||||
}
|
||||
BIN
tests/Models/bills/17.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
tests/Models/bills/18.jpg
Normal file
|
After Width: | Height: | Size: 144 KiB |
26
tests/Models/bills/18.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"invoice_number": "1234",
|
||||
"grossSales": 36533000,
|
||||
"netSales": 30700000,
|
||||
"tax_amount": 5833000,
|
||||
"tax_rate": 190000,
|
||||
"bill_date": "2020-01-01",
|
||||
"delivery_date": "",
|
||||
"service_date": "",
|
||||
"due_date": "",
|
||||
"vat_id": "DE123456789",
|
||||
"tax_id": "",
|
||||
"address": {
|
||||
"name": "pixa",
|
||||
"address": "Musterstraße 1",
|
||||
"city": "12345 Berlin",
|
||||
"country": ""
|
||||
},
|
||||
"iban": "DE07123412341234123412",
|
||||
"elements": [],
|
||||
"language": "de",
|
||||
"currency": "EUR",
|
||||
"email": "mail@example.com",
|
||||
"website": "",
|
||||
"phone": "+49 123 456789"
|
||||
}
|
||||
26
tests/Models/bills/19.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"invoice_number": "2019-1001",
|
||||
"grossSales": 357000,
|
||||
"netSales": 300000,
|
||||
"tax_amount": 57000,
|
||||
"tax_rate": 190000,
|
||||
"bill_date": "2019-01-01",
|
||||
"delivery_date": "",
|
||||
"service_date": "",
|
||||
"due_date": "",
|
||||
"vat_id": "DE24324567",
|
||||
"tax_id": "",
|
||||
"address": {
|
||||
"name": "Musterfirma GmbH",
|
||||
"address": "Musterstraße 23",
|
||||
"city": "12345 Musterhausen",
|
||||
"country": ""
|
||||
},
|
||||
"iban": "DE3423456234356765",
|
||||
"elements": [],
|
||||
"language": "de",
|
||||
"currency": "EUR",
|
||||
"email": "info@muster.de",
|
||||
"website": "www.firma.de",
|
||||
"phone": "+40 (0)30 12345678"
|
||||
}
|
||||
BIN
tests/Models/bills/19.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
26
tests/Models/bills/2.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"invoice_number": "12345",
|
||||
"grossSales": 16838500,
|
||||
"netSales": 14150000,
|
||||
"tax_amount": 2688500,
|
||||
"tax_rate": 190000,
|
||||
"bill_date": "2022-01-01",
|
||||
"delivery_date": "",
|
||||
"service_date": "",
|
||||
"due_date": "2022-01-15",
|
||||
"vat_id": "",
|
||||
"tax_id": "123456789",
|
||||
"address": {
|
||||
"name": "Vorname Name",
|
||||
"address": "Musterstraße 123",
|
||||
"city": "12345 Musterstadt",
|
||||
"country": ""
|
||||
},
|
||||
"iban": "",
|
||||
"elements": [],
|
||||
"language": "de",
|
||||
"currency": "EUR",
|
||||
"email": "hallo@superduperseite.de",
|
||||
"website": "www.superduperseite.de",
|
||||
"phone": ""
|
||||
}
|
||||
BIN
tests/Models/bills/2.png
Normal file
|
After Width: | Height: | Size: 415 KiB |
26
tests/Models/bills/3.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"invoice_number": "01234",
|
||||
"grossSales": 21420000,
|
||||
"netSales": 18000000,
|
||||
"tax_amount": 3420000,
|
||||
"tax_rate": 190000,
|
||||
"bill_date": "2024-08-13",
|
||||
"delivery_date": "",
|
||||
"service_date": "",
|
||||
"due_date": "2024-09-15",
|
||||
"vat_id": "",
|
||||
"tax_id": "0123 4567 8901",
|
||||
"address": {
|
||||
"name": "Werbeagentur Kluger",
|
||||
"address": "Jede Straße 123",
|
||||
"city": "12345 Jede Stadt",
|
||||
"country": ""
|
||||
},
|
||||
"iban": "",
|
||||
"elements": [],
|
||||
"language": "de",
|
||||
"currency": "EUR",
|
||||
"email": "hallo@superduperseite.de",
|
||||
"website": "",
|
||||
"phone": "(0221) 1234-56"
|
||||
}
|
||||
BIN
tests/Models/bills/3.png
Normal file
|
After Width: | Height: | Size: 476 KiB |
26
tests/Models/bills/4.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"invoice_number": "2021021",
|
||||
"grossSales": 15113000,
|
||||
"netSales": 12700000,
|
||||
"tax_amount": 2413000,
|
||||
"tax_rate": 190000,
|
||||
"bill_date": "2021-04-01",
|
||||
"delivery_date": "",
|
||||
"service_date": "",
|
||||
"due_date": "2021-05-01",
|
||||
"vat_id": "DE123456789",
|
||||
"tax_id": "",
|
||||
"address": {
|
||||
"name": "Invoice Office GmbH",
|
||||
"address": "Inge-Mezsel Str. 8a",
|
||||
"city": "85053 Ingolstadt",
|
||||
"country": ""
|
||||
},
|
||||
"iban": "DE20000111122223332103",
|
||||
"elements": [],
|
||||
"language": "de",
|
||||
"currency": "EUR",
|
||||
"email": "demo@invoiceoffice.de",
|
||||
"website": "",
|
||||
"phone": "0049123456789"
|
||||
}
|
||||
BIN
tests/Models/bills/4.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
26
tests/Models/bills/5.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"invoice_number": "323525",
|
||||
"grossSales": 7809300,
|
||||
"netSales": 6562400,
|
||||
"tax_amount": 1246900,
|
||||
"tax_rate": 190000,
|
||||
"bill_date": "2022-08-10",
|
||||
"delivery_date": "",
|
||||
"service_date": "",
|
||||
"due_date": "",
|
||||
"vat_id": "DE101778899",
|
||||
"tax_id": "200/100/10001",
|
||||
"address": {
|
||||
"name": "Handwerker Muster",
|
||||
"address": "Musterstraße 1",
|
||||
"city": "123456 Musterstadt",
|
||||
"country": ""
|
||||
},
|
||||
"iban": "DE02345611110000100020",
|
||||
"elements": [],
|
||||
"language": "de",
|
||||
"currency": "EUR",
|
||||
"email": "",
|
||||
"website": "",
|
||||
"phone": "+49 123 456 789-0"
|
||||
}
|
||||
BIN
tests/Models/bills/5.png
Normal file
|
After Width: | Height: | Size: 238 KiB |
26
tests/Models/bills/6.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"invoice_number": "2021010",
|
||||
"grossSales": 22848000,
|
||||
"netSales": 19200000,
|
||||
"tax_amount": 3648000,
|
||||
"tax_rate": 190000,
|
||||
"bill_date": "2021-03-29",
|
||||
"delivery_date": "",
|
||||
"service_date": "",
|
||||
"due_date": "2021-04-28",
|
||||
"vat_id": "DE123456789",
|
||||
"tax_id": "",
|
||||
"address": {
|
||||
"name": "Invoice Office GmbH",
|
||||
"address": "Inge-Meysel Str. 8a",
|
||||
"city": "85053 Ingolstadt",
|
||||
"country": ""
|
||||
},
|
||||
"iban": "DE20000111122223332103",
|
||||
"elements": [],
|
||||
"language": "de",
|
||||
"currency": "EUR",
|
||||
"email": "demo@invoiceoffice.de",
|
||||
"website": "",
|
||||
"phone": "0049123456789"
|
||||
}
|
||||
BIN
tests/Models/bills/6.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
26
tests/Models/bills/7.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"invoice_number": "220002",
|
||||
"grossSales": 6021400,
|
||||
"netSales": 5060000,
|
||||
"tax_amount": 961400,
|
||||
"tax_rate": 190000,
|
||||
"bill_date": "2022-11-04",
|
||||
"delivery_date": "",
|
||||
"service_date": "",
|
||||
"due_date": "2022-11-18",
|
||||
"vat_id": "",
|
||||
"tax_id": "45/123/12345",
|
||||
"address": {
|
||||
"name": "Handwerksmeister Frity Blau",
|
||||
"address": "Musterstraße 123",
|
||||
"city": "12345 Berlin",
|
||||
"country": ""
|
||||
},
|
||||
"iban": "DE100023145678",
|
||||
"elements": [],
|
||||
"language": "de",
|
||||
"currency": "EUR",
|
||||
"email": "info@mustermann.de",
|
||||
"website": "www.mustermann.de",
|
||||
"phone": "+49 (30) 12 34 56 01"
|
||||
}
|
||||
BIN
tests/Models/bills/7.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
26
tests/Models/bills/8.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"invoice_number": "INV0041-1",
|
||||
"grossSales": 56283100,
|
||||
"netSales": 52601000,
|
||||
"tax_amount": 3939800,
|
||||
"tax_rate": 70000,
|
||||
"bill_date": "2022-02-20",
|
||||
"delivery_date": "",
|
||||
"service_date": "",
|
||||
"due_date": "2022-02-27",
|
||||
"vat_id": "",
|
||||
"tax_id": "",
|
||||
"address": {
|
||||
"name": "Company ABC",
|
||||
"address": "Some Street 4",
|
||||
"city": "12345 Rome",
|
||||
"country": ""
|
||||
},
|
||||
"iban": "",
|
||||
"elements": [],
|
||||
"language": "en",
|
||||
"currency": "USD",
|
||||
"email": "",
|
||||
"website": "",
|
||||
"phone": ""
|
||||
}
|
||||
BIN
tests/Models/bills/8.pdf
Normal file
26
tests/Models/bills/9.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"invoice_number": "INV0041-1",
|
||||
"grossSales": 56283100,
|
||||
"netSales": 52601000,
|
||||
"tax_amount": 3939800,
|
||||
"tax_rate": 70000,
|
||||
"bill_date": "2022-02-20",
|
||||
"delivery_date": "",
|
||||
"service_date": "",
|
||||
"due_date": "2022-02-27",
|
||||
"vat_id": "",
|
||||
"tax_id": "",
|
||||
"address": {
|
||||
"name": "Company ABC",
|
||||
"address": "Some Street 4",
|
||||
"city": "12345 Rome",
|
||||
"country": ""
|
||||
},
|
||||
"iban": "",
|
||||
"elements": [],
|
||||
"language": "en",
|
||||
"currency": "USD",
|
||||
"email": "",
|
||||
"website": "",
|
||||
"phone": ""
|
||||
}
|
||||