This commit is contained in:
Dennis Eichhorn 2024-02-28 05:09:09 +00:00
parent 8eaaacac0a
commit b43a167361
81 changed files with 4204 additions and 1446 deletions

View File

@ -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.

View File

@ -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.

View File

@ -1,9 +0,0 @@
[
{
"type": "setting",
"name": "1005100003",
"content": "[\"en\", \"de\"]",
"pattern": "",
"module": "Billing"
}
]

View 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,
],
];

View File

@ -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']);
}
}

View File

@ -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"
}
]

View File

@ -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',
],
],
]
);

View File

@ -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->Cell(\array_sum($w), 0, '', 'T');
$pdf->Ln();
if ($pdf->getY() > $pageHeight - 40) {
$pdf->AddPage();
}
}
$pdf->setFillColor(240, 240, 240);
$pdf->setTextColor(0);
$pdf->setDrawColor(240, 240, 240);
$pdf->setFont('helvetica', 'B', 10);
$pdf->setFillColor(240, 240, 240);
$pdf->setTextColor(0);
$pdf->setDrawColor(240, 240, 240);
$pdf->setFont('helvetica', 'B', 10);
$tempY = $pdf->getY();
$tempY = $pdf->getY();
$netSales = Money::fromFloatInt($bill->netSales);
$netSales = Money::fromFloatInt($bill->netSales);
$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();
$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) {
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');

View File

@ -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

View File

@ -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();

View File

@ -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,

View File

@ -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",

View File

@ -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,

View File

@ -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) {

View File

@ -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,

View File

@ -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,
],
],
],
];

View File

@ -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 = '';
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,6 +1356,8 @@ final class ApiBillController extends Controller
return;
}
$media = null;
if ($oldFile->id === 0) {
$media = $this->app->moduleManager->get('Media', 'Api')->createDbEntry(
status: [
'status' => UploadStatus::OK,
@ -1194,39 +1375,11 @@ final class ApiBillController extends Controller
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();
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;
$this->sendBillEmail($media, $email, $response->header->l11n->language);
}
// 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,
(int) $internalType->content,
MediaMapper::class,
'types',
'',
@ -1243,6 +1396,19 @@ final class ApiBillController extends Controller
'',
$request->getOrigin()
);
} else {
$media = clone $oldFile;
if (\realpath($pdfDir . '/' . $billFileName) !== \realpath($oldFile->getAbsolutePath())) {
\unlink($oldFile->getAbsolutePath());
}
$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());
}
$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,
]
);
}
}
/**

View File

@ -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;

View File

@ -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;

View File

@ -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,25 +150,81 @@ 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)) {
return [];
}
}
$request->setData('id', $billId, true);
$request->setData('bill', $billId, true);
$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;
throw new \Exception();
}
$this->createInvalidCreateResponse($request, $response, $val);
return;
}
// Create internal document
$billResponse = new HttpResponse();
$billRequest = new HttpRequest();
$bill = BillMapper::get()
->where('id', (int) $request->getData('id'))
->execute();
$billRequest->header->account = $request->header->account;
$billRequest->setData('bill', $billId);
// 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);
$this->app->moduleManager->get('Billing', 'ApiBill')->apiBillPdfArchiveCreate($billRequest, $billResponse);
return;
}
// Offload bill parsing to cli
$cliPath = \realpath(__DIR__ . '/../../../cli.php');
@ -142,9 +235,10 @@ final class ApiPurchaseController extends Controller
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) $billId),
. '-i ' . \escapeshellarg((string) $bill->id),
$request->getDataBool('async') ?? true
);
} catch (\Throwable $t) {
@ -152,7 +246,25 @@ final class ApiPurchaseController extends Controller
$this->app->logger->error($t->getMessage());
}
$this->createStandardCreateResponse($request, $response, $bills);
$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 [];
}
}

View File

@ -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,6 +134,15 @@ final class BackendController extends Controller
$view->data['billtypes'] = $billTypes;
$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')
@ -150,6 +162,9 @@ final class BackendController extends Controller
$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,14 +329,15 @@ 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;
$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')
@ -340,6 +357,7 @@ final class BackendController extends Controller
$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);
@ -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;

View File

@ -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();
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;
}
}

View File

@ -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,

View File

@ -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

View File

@ -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'],

View File

@ -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'],

View File

@ -54,6 +54,8 @@ class BillType implements \JsonSerializable
public int $sign = 1;
public bool $email = false;
/**
* Localization
*

View File

@ -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'],
];

File diff suppressed because it is too large Load Diff

View 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;
}

View File

@ -55,6 +55,8 @@ class Price implements \JsonSerializable
public Item $item;
public int $status = PriceStatus::ACTIVE;
public AttributeValue $itemsalesgroup;
public AttributeValue $itemproductgroup;

View File

@ -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'],

View 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;
}

View File

@ -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);

View File

@ -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
}

View File

@ -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"
}
}
}

View File

@ -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.',
]];

View File

@ -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.',
]];

View File

@ -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">

View File

@ -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) : ?>

View File

@ -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'); ?>
<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>
<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>
<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>
<?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">
<div class="form-group">
<label for="iAddressListBill"><?= $this->getHtml('Addresses'); ?></label>
<select id="iAddressListBill" name="bill_address_bill_list"<?= $disabled; ?>>
<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>
<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">
<div class="form-group">
<label for="iAddressListDelivery"><?= $this->getHtml('Addresses'); ?></label>
<select id="iAddressListDelivery" name="bill_address_delivery_list"<?= $disabled; ?>>
<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>
<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>
<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><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); ?>
<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; ?>
<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">
<?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">
<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>
<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>
<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>

View File

@ -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) : ?>

View File

@ -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>

View File

@ -36,6 +36,7 @@ final class Autoloader
__DIR__ . '/../',
__DIR__ . '/../MainRepository/',
__DIR__ . '/../../',
__DIR__ . '/../../../',
];
/**

View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
tests/Models/bills/11.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View 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": ""
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
tests/Models/bills/18.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View 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"
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

26
tests/Models/bills/2.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

26
tests/Models/bills/3.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

26
tests/Models/bills/4.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

26
tests/Models/bills/5.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

26
tests/Models/bills/6.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

26
tests/Models/bills/7.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

26
tests/Models/bills/8.json Normal file
View 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

Binary file not shown.

26
tests/Models/bills/9.json Normal file
View 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/9.pdf Normal file

Binary file not shown.