diff --git a/.github/dev_bug_report.md b/.github/dev_bug_report.md deleted file mode 100755 index ef93e56..0000000 --- a/.github/dev_bug_report.md +++ /dev/null @@ -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. diff --git a/.github/dev_feature_request.md b/.github/dev_feature_request.md deleted file mode 100755 index 9573c35..0000000 --- a/.github/dev_feature_request.md +++ /dev/null @@ -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. diff --git a/.github/user_bug_report.md b/.github/user_bug_report.md deleted file mode 100755 index 4b92a8e..0000000 --- a/.github/user_bug_report.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -name: User 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 - -# Expected Behavior - -A clear and concise description of what you expected to happen. - -# Screenshots - -If applicable, add screenshots to help explain your problem. - -# System Information - -- System: [e.g. PC or iPhone11, ...] -- OS: [e.g. iOS] -- Browser [e.g. chrome, safari] -- KarakaVersion [e.g. 22] - -# Additional Information - -Add any other context about the problem here. diff --git a/.github/user_feature_request.md b/.github/user_feature_request.md deleted file mode 100755 index c9595e8..0000000 --- a/.github/user_feature_request.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -name: User 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. diff --git a/Admin/Install/Admin.install.json b/Admin/Install/Admin.install.json deleted file mode 100755 index 1b8d939..0000000 --- a/Admin/Install/Admin.install.json +++ /dev/null @@ -1,9 +0,0 @@ -[ - { - "type": "setting", - "name": "1005100003", - "content": "[\"en\", \"de\"]", - "pattern": "", - "module": "Billing" - } -] \ No newline at end of file diff --git a/Admin/Install/Admin.install.php b/Admin/Install/Admin.install.php new file mode 100644 index 0000000..0fd52fb --- /dev/null +++ b/Admin/Install/Admin.install.php @@ -0,0 +1,26 @@ + 'setting', + 'name' => SettingsEnum::VALID_BILL_LANGUAGES, + 'content' => '["en","de"]', + 'pattern' => '', + 'module' => ApiController::NAME, + ], +]; diff --git a/Admin/Install/Admin.php b/Admin/Install/Admin.php index ea8cd1f..a21cb13 100755 --- a/Admin/Install/Admin.php +++ b/Admin/Install/Admin.php @@ -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']); } } diff --git a/Admin/Install/Media.install.json b/Admin/Install/Media.install.json index 2f42254..9f59e9c 100755 --- a/Admin/Install/Media.install.json +++ b/Admin/Install/Media.install.json @@ -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" } ] diff --git a/Admin/Install/Media.php b/Admin/Install/Media.php index 274c55a..3dae6c2 100755 --- a/Admin/Install/Media.php +++ b/Admin/Install/Media.php @@ -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', + ], ], ] ); diff --git a/Admin/Install/Media/bill.pdf.php b/Admin/Install/Media/bill.pdf.php index e60edad..cb92fd3 100755 --- a/Admin/Install/Media/bill.pdf.php +++ b/Admin/Install/Media/bill.pdf.php @@ -13,15 +13,17 @@ declare(strict_types=1); use Modules\Billing\Models\NullBill; +use Modules\Media\Models\NullCollection; use phpOMS\Localization\ISO3166NameEnum; -use phpOMS\Localization\ISO3166TwoEnum; use phpOMS\Localization\Money; +use phpOMS\Stdlib\Base\FloatInt; /** @var \phpOMS\Views\View $this */ -/** @var \Modules\Media\Models\Collection $media */ -$media = $this->getData('defaultTemplates'); -require_once $media->findFile('.pdf.php')->getAbsolutePath(); +/** @var \Modules\Media\Models\Collection $collection */ +$collection = $this->data['defaultTemplates'] ?? new NullCollection(); + +require_once $collection->findFile('.pdf.php')->getAbsolutePath(); /** @var \Modules\Billing\Models\Bill $bill */ $bill = $this->data['bill'] ?? new NullBill(); @@ -32,8 +34,8 @@ $pdf = new DefaultPdf(); $lang = include __DIR__ . '/lang.php'; -$pdf->attributes['title_name'] = (string) ($this->data['bill_logo_name'] ?? 'Jingga'); -$pdf->attributes['slogan'] = (string) ($this->data['bill_slogan'] ?? 'Business solutions made simple.'); +$pdf->attributes['title_name'] = $this->data['bill_logo_name'] ?? 'Jingga'; +$pdf->attributes['slogan'] = $this->data['bill_slogan'] ?? 'Business solutions made simple.'; $pdf->setHeaderData( __DIR__ . '/logo.png', 15, @@ -47,22 +49,22 @@ $pdf->setSubject((string) ($this->data['bill_subtitle'] ?? '')); $pdf->setKeywords(\implode(', ', (array) ($this->data['keywords'] ?? []))); $pdf->language = $bill->language; -$pdf->attributes['legal_name'] = (string) ($this->data['legal_company_name'] ?? 'Jingga e. K.'); -$pdf->attributes['address'] = (string) ($this->data['bill_company_address'] ?? 'Kirchstr. 33'); -$pdf->attributes['city'] = (string) ($this->data['bill_company_city'] ?? '61191 Rosbach'); +$pdf->attributes['legal_name'] = $this->data['legal_company_name'] ?? 'Jingga e. K.'; +$pdf->attributes['address'] = $this->data['bill_company_address'] ?? 'Kirchstr. 33'; +$pdf->attributes['city'] = $this->data['bill_company_city'] ?? '61191 Rosbach'; -$pdf->attributes['ceo'] = (string) ($this->data['bill_company_ceo'] ?? 'Dennis Eichhorn'); -$pdf->attributes['tax_office'] = (string) ($this->data['bill_company_tax_office'] ?? 'HRA 5058'); -$pdf->attributes['tax_number'] = (string) ($this->data['bill_company_tax_id'] ?? 'DE362646968'); -$pdf->attributes['terms'] = (string) ($this->data['bill_company_terms'] ?? 'https://jingga.app/terms'); +$pdf->attributes['ceo'] = $this->data['bill_company_ceo'] ?? 'Dennis Eichhorn'; +$pdf->attributes['tax_office'] = $this->data['bill_company_tax_office'] ?? 'HRA 5058'; +$pdf->attributes['tax_number'] = $this->data['bill_company_tax_id'] ?? 'DE362646968'; +$pdf->attributes['terms'] = $this->data['bill_company_terms'] ?? 'https://jingga.app/terms'; -$pdf->attributes['bank_name'] = (string) ($this->data['bill_company_bank_name'] ?? 'Volksbank Mittelhessen'); -$pdf->attributes['swift'] = (string) ($this->data['bill_company_swift'] ?? 'VBMHDE5F'); -$pdf->attributes['bank_account'] = (string) ($this->data['bill_company_bank_account'] ?? 'DE62 5139 0000 0084 8044 10'); +$pdf->attributes['bank_name'] = $this->data['bill_company_bank_name'] ?? 'Volksbank Mittelhessen'; +$pdf->attributes['swift'] = $this->data['bill_company_swift'] ?? 'VBMHDE5F'; +$pdf->attributes['bank_account'] = $this->data['bill_company_bank_account'] ?? 'DE62 5139 0000 0084 8044 10'; -$pdf->attributes['website'] = (string) ($this->data['bill_company_website'] ?? 'www.jingga.app'); -$pdf->attributes['email'] = (string) ($this->data['bill_company_email'] ?? 'info@jingga.app'); -$pdf->attributes['phone'] = (string) ($this->data['bill_company_phone'] ?? '+49 152 04337728'); +$pdf->attributes['website'] = $this->data['bill_company_website'] ?? 'www.jingga.app'; +$pdf->attributes['email'] = $this->data['bill_company_email'] ?? 'info@jingga.app'; +$pdf->attributes['phone'] = $this->data['bill_company_phone'] ?? '+49 152 04337728'; $pdf->setImageScale(PDF_IMAGE_SCALE_RATIO); @@ -73,17 +75,11 @@ $topPos = $pdf->getY(); // Set up default bill template $billTypeName = \strtoupper($bill->type->getL11n()); -// @todo: depending on amount of lines, there is a solution (html, or use backtracking of tcpdf) - // Address $pdf->setY(50); $pdf->setFont('helvetica', '', 10); -$countries = ISO3166NameEnum::getConstants(); -$countryEnumName = ISO3166TwoEnum::getName($bill->billCountry); -$toCountry = \is_string($countryEnumName) && ($country = ISO3166NameEnum::getByName($countryEnumName)) !== null - ? $country - : ''; +$toCountry = ISO3166NameEnum::getBy2Code($bill->billCountry); $addressString = \trim( $bill->billTo . "\n" @@ -104,64 +100,58 @@ $pdf->Write( ); $lineHeight = ($lineHeight - $pdf->getY()) / $addressLineCount; +$pageWidth = $pdf->getPageWidth(); +$pageHeight = $pdf->getPageHeight(); + // Bill head $pdf->setFont('helvetica', 'B', 16); $titleWidth = $pdf->getStringWidth($billTypeName, 'helvetica', 'B', 16); $titleWidth = \is_array($titleWidth) ? \array_sum($titleWidth) : $titleWidth; $pdf->setXY( - $rightPos = ($pdf->getPageWidth() - $titleWidth - \max(60 - $titleWidth, 0) - 15 - 2), + $rightPos = ($pageWidth - $titleWidth - \max(60 - $titleWidth, 0) - 15 - 2), $topPos + 50 + $lineHeight * $addressLineCount - 38, true ); $pdf->setTextColor(255, 255, 255); $pdf->setFillColor(255, 162, 7); -$pdf->Cell($pdf->getPageWidth() - $rightPos - 15, 0, $billTypeName, 0, 0, 'L', true); +$pdf->Cell($pageWidth - $rightPos - 15, 0, $billTypeName, 0, 0, 'L', true); $pdf->setFont('helvetica', '', 10); $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" . $lang[$pdf->language]['CustomerNo'] . "\n" - . $lang[$pdf->language]['PO'] . "\n" + . $lang[$pdf->language]['REF'] . "\n" . $lang[$pdf->language]['DueDate'], 0, 'L' ); -$pdf->setFont('helvetica', '', 10); +//$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" - . '' . "\n" /* @todo: implement customer / supplier reference as string */ + . $bill->external . "\n" . ($bill->billDate?->format('Y-m-d') ?? '0'), /* Consider to add dueDate in addition */ 0, 'L' ); $pdf->Ln(); -$pdf->setY($pdf->getY() - 30); - -/* -$pdf->writeHTMLCell( - $pdf->getPageWidth() - 15 * 2, 0, null, null, - "Lorem ipsum dolor sit amet,

Consectetur adipiscing elit. Vivamus ac massa sit amet eros posuere accumsan feugiat vel est. Maecenas ultricies enim eu eros rhoncus, volutpat cursus enim imperdiet. Aliquam et odio ipsum. Quisque dapibus scelerisque tempor. Phasellus purus lorem, venenatis eget pretium ac, convallis et ante. Aenean pulvinar justo consectetur mi tincidunt venenatis. Suspendisse ultricies enim id nulla facilisis lacinia.

Nam congue nunc nunc, eu pellentesque eros aliquam ac. Nunc placerat elementum turpis, quis facilisis diam volutpat at. Suspendisse enim leo, convallis nec ornare eu, auctor nec purus. Nunc neque metus, feugiat quis justo nec, mollis dignissim risus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In at ornare sem. Cras placerat, sapien sed ornare lacinia, mauris nulla volutpat nisl, eget dapibus nisl ipsum non est. Suspendisse ut nisl a ipsum rhoncus sodales.", - 0, 0, false, true, 'J' -); -$pdf->Ln(); -*/ - -$pdf->setY($pdf->getY() + 10); +$tempY = $pdf->getY(); +$height = 0; +$pdf->setY($tempY - 20); $header = [ $lang[$pdf->language]['Item'], @@ -170,11 +160,9 @@ $header = [ $lang[$pdf->language]['Total'], ]; -$lines = $bill->getElements(); - // Header $headerCount = \count($header); -$w = [$pdf->getPageWidth() - 20 - 20 - 20 - 2 * 15, 20, 20, 20]; +$w = [$pageWidth - 20 - 20 - 20 - 2 * 15, 20, 20, 20]; $pdf->setCellPadding(1); @@ -183,9 +171,9 @@ $first = true; // Data $fill = false; -foreach($lines as $line) { - // @todo: add support for empty lines (row = line) - if (/*$row === null || */$first || $pdf->getY() > $pdf->getPageHeight() - 40) { +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); $pdf->setTextColor(255); $pdf->setDrawColor(255, 162, 7); @@ -209,110 +197,119 @@ foreach($lines as $line) { $first = false; } + // Discounts are shown below the original price -> additional line + // We don't want discount columns because that hints at customers they might be able to get discounts. + $lines = 1 + + ((int) ($line->discountQ->value > 0)) + + ((int) ($line->singleDiscountP->value > 0)) + + ((int) ($line->singleDiscountR->value > 0)); + $tempY = $pdf->getY(); - $pdf->writeHTMLCell($w[0], 10, null, null, $line->itemNumber . ' ' . $line->itemName, 0, 2, $fill); + //$pdf->writeHTMLCell($w[0], 10, null, null, $line->itemNumber . ' ' . $line->itemName, 0, 2, $fill); + $pdf->MultiCell($w[0], 10 * $lines, \trim($line->itemNumber . ' ' . $line->itemName), 0, 'L', $fill, 2, null, null, true, 0, true, true, 0, 'M', false); $height = $pdf->getY() - $tempY; - $singleSalesPriceNet = Money::fromFloatInt($line->singleSalesPriceNet); - $totalSalesPriceNet = Money::fromFloatInt($line->totalSalesPriceNet); + $singleListPriceNet = Money::fromFloatInt($line->singleListPriceNet); + $totalSalesPriceNet = Money::fromFloatInt($line->totalSalesPriceNet); - $pdf->MultiCell($w[1], $height, (string) $line->getQuantity(), 0, 'L', $fill, 0, 15 + $w[0], $tempY, true, 0, false, true, 0, 'M', true); - $pdf->MultiCell($w[2], $height, $singleSalesPriceNet->getCurrency(2, symbol: ''), 0, 'L', $fill, 0, 15 + $w[0] + $w[1], $tempY, true, 0, false, true, 0, 'M', true); - $pdf->MultiCell($w[3], $height, $totalSalesPriceNet->getCurrency(2, symbol: ''), 0, 'L', $fill, 1, 15 + $w[0] + $w[1] + $w[2], $tempY, true, 0, false, true, 0, 'M', true); + if ($line->quantity->value === 0) { + $pdf->MultiCell($w[1] + $w[2] + $w[3], $height, '', 0, 'L', $fill, 0, 15 + $w[0], $tempY, true, 0, false, true, 0, 'M', true); + } else { + $pdf->MultiCell($w[1], $height, (string) $line->quantity->getAmount($line->container?->quantityDecimals ?? 0), 0, 'L', $fill, 0, 15 + $w[0], $tempY, true, 0, false, true, 0, 'M', true); + $pdf->MultiCell($w[2], $height, $singleListPriceNet->getCurrency(2, symbol: ''), 0, 'L', $fill, 0, 15 + $w[0] + $w[1], $tempY, true, 0, false, true, 0, 'M', true); + $pdf->MultiCell($w[3], $height, $totalSalesPriceNet->getCurrency(2, symbol: ''), 0, 'L', $fill, 1, 15 + $w[0] + $w[1] + $w[2], $tempY, true, 0, false, true, 0, 'M', true); + } $fill = !$fill; // get taxes - if (!isset($taxes[$line->taxR->getInt() / 100])) { - $taxes[$line->taxR->getInt() / 100] = $line->taxP; + if (!isset($taxes[$line->taxR->value / FloatInt::DIVISOR])) { + $taxes[$line->taxR->value / FloatInt::DIVISOR] = $line->taxP; } else { - $taxes[$line->taxR->getInt() / 100]->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() > $pdf->getPageHeight() - 40) { - $pdf->AddPage(); -} - -$pdf->setFillColor(240, 240, 240); -$pdf->setTextColor(0); -$pdf->setDrawColor(240, 240, 240); -$pdf->setFont('helvetica', 'B', 10); - -$tempY = $pdf->getY(); - -$netSales = Money::fromFloatInt($bill->netSales); - -$pdf->setX($w[0] + $w[1] + 15); -$pdf->Cell($w[2], 7, $lang[$pdf->language]['Subtotal'], 0, 0, 'L', false); -$pdf->Cell($w[3], 7, $netSales->getCurrency(2, symbol: ''), 0, 0, 'L', false); -$pdf->Ln(); - -foreach ($taxes as $rate => $tax) { - $tax = Money::fromFloatInt($tax); - - $pdf->setX($w[0] + $w[1] + 15); - $pdf->Cell($w[2], 7, $lang[$pdf->language]['Taxes'] . ' (' . $rate . '%)', 0, 0, 'L', false); - $pdf->Cell($w[3], 7, $tax->getCurrency(2, symbol: ''), 0, 0, 'L', false); + $pdf->Cell(\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); + + $tempY = $pdf->getY(); + + $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(); + + 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] + 15); -$pdf->Cell($w[2], 7, \strtoupper($lang[$pdf->language]['Total']), 1, 0, 'L', true); -$pdf->Cell($w[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->writeHTMLCell( - $pdf->getPageWidth() - 15 * 2, 0, null, null, - "Consectetur adipiscing elit. Vivamus ac massa sit amet eros posuere accumsan feugiat vel est. Maecenas ultricies enim eu eros rhoncus, volutpat cursus enim imperdiet. Aliquam et odio ipsum. Quisque dapibus scelerisque tempor. Phasellus purus lorem, venenatis eget pretium ac, convallis et ante. Aenean pulvinar justo consectetur mi tincidunt venenatis. Suspendisse ultricies enim id nulla facilisis lacinia. Nam congue nunc nunc, eu pellentesque eros aliquam ac.

Nunc placerat elementum turpis, quis facilisis diam volutpat at. Suspendisse enim leo, convallis nec ornare eu, auctor nec purus. Nunc neque metus, feugiat quis justo nec, mollis dignissim risus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In at ornare sem. Cras placerat, sapien sed ornare lacinia, mauris nulla volutpat nisl, eget dapibus nisl ipsum non est. Suspendisse ut nisl a ipsum rhoncus sodales.", - 0, 0, false, true, 'J' -); -$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'); diff --git a/Admin/Install/Media/lang.php b/Admin/Install/Media/lang.php index 38f26e0..7c11b32 100755 --- a/Admin/Install/Media/lang.php +++ b/Admin/Install/Media/lang.php @@ -20,7 +20,7 @@ return [ 'InvoiceDate' => 'Invoice Date', 'ServiceDate' => 'Service Date', 'CustomerNo' => 'Customer No.', - 'PO' => 'PO', + 'REF' => 'REF', 'DueDate' => 'Due Date', 'Item' => 'Item', 'Currency' => 'Currency', @@ -40,7 +40,7 @@ return [ 'InvoiceDate' => 'Belegdatum', 'ServiceDate' => 'Leistungsdatum', 'CustomerNo' => 'Kundennummer', - 'PO' => 'Kundenreferenz', + 'REF' => 'REF', 'DueDate' => 'Fälligkeitsdatum', 'Item' => 'Artikel', 'Currency' => 'Währung', diff --git a/Admin/Install/Messages.install.json b/Admin/Install/Messages.install.json index 7608fba..2a50782 100755 --- a/Admin/Install/Messages.install.json +++ b/Admin/Install/Messages.install.json @@ -9,13 +9,34 @@ "l11n": { "en": { "subject": "Billing", - "body": "Billing

Billing

Dear {user_name},

Thank you for for doing business with us.

Attached kindly find your bill.

Jingga e.K. - www.jingga.app - CEO Dennis Eichhorn - Amtsgericht Friedberg HRA 5058

", + "body": "Billing

Billing

Dear {user_name},

Thank you for for doing business with us.

Attached kindly find your bill.

Jingga e.K. - www.jingga.app - CEO Dennis Eichhorn - Amtsgericht Friedberg HRA 5058

", "bodyalt": "Dear {user_name},\n\nThank you for doing business with us.\n\nAttached kindly find your bill.\n\n\nJingga e.K. - www.jingga.app - CEO Dennis Eichhorn - Amtsgericht Friedberg HRA 5058" }, "de": { "subject": "Rechnungsstellung", - "body": "Abrechnung

Abrechnung

Sehr geehrte/r {user_name},

Vielen Dank für Ihre Geschäftsbeziehung mit uns.

Im Anhang finden Sie Ihre Rechnung.

Jingga 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" + "body": "Abrechnung

Abrechnung

Sehr geehrte/r {user_name},

Vielen Dank für Ihre Geschäftsbeziehung mit uns.

Im Anhang finden Sie Ihre Rechnung.

Jingga 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": "Order

Order

Dear {user_name},

We are looking forward to doing business with you.

Attached kindly find our order.

Jingga e.K. - www.jingga.app - CEO Dennis Eichhorn - Amtsgericht Friedberg HRA 5058

", + "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": "Bestellung

Bestellung

Sehr geehrte/r {user_name},

Wir freuen uns eine Bestellung bei Ihnen aufgeben zu können.

Im Anhang finden Sie unsere Bestellung.

Jingga e.K. - www.jingga.app - CEO Dennis Eichhorn - Amtsgericht Friedberg HRA 5058

", + "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 diff --git a/Admin/Install/Messages.php b/Admin/Install/Messages.php index ddf2e5c..670cff7 100755 --- a/Admin/Install/Messages.php +++ b/Admin/Install/Messages.php @@ -18,7 +18,6 @@ use Modules\Billing\Models\SettingsEnum; use phpOMS\Application\ApplicationAbstract; use phpOMS\Message\Http\HttpRequest; use phpOMS\Message\Http\HttpResponse; -use phpOMS\Uri\HttpUri; /** * Media class. @@ -54,10 +53,16 @@ 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(); - $request = new HttpRequest(new HttpUri('')); + $request = new HttpRequest(); $request->header->account = 1; $request->setData('settings', \json_encode($settings)); diff --git a/Admin/Install/Navigation.install.json b/Admin/Install/Navigation.install.json index 2845692..61df907 100755 --- a/Admin/Install/Navigation.install.json +++ b/Admin/Install/Navigation.install.json @@ -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, @@ -57,6 +57,36 @@ "permission": { "permission": 4, "category": null, "element": null }, "parent": 1005104001, "children": [] + }, + { + "id": 1005104401, + "pid": "/sales/bill", + "type": 3, + "subtype": 1, + "name": "PaymentTerms", + "uri": "{/base}/bill/payment/list?{?}", + "target": "self", + "icon": null, + "order": 15, + "from": "Billing", + "permission": { "permission": 4, "category": null, "element": null }, + "parent": 1005104001, + "children": [] + }, + { + "id": 1005104501, + "pid": "/sales/bill", + "type": 3, + "subtype": 1, + "name": "ShippingTerms", + "uri": "{/base}/bill/shipping/list?{?}", + "target": "self", + "icon": null, + "order": 20, + "from": "Billing", + "permission": { "permission": 4, "category": null, "element": null }, + "parent": 1005104001, + "children": [] } ] }, @@ -66,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, @@ -80,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, @@ -95,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, @@ -142,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, @@ -156,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, @@ -171,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, @@ -203,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, @@ -272,7 +302,7 @@ "order": 5, "from": "Billing", "permission": { "permission": 2, "category": null, "element": null }, - "parent": 1003401001, + "parent": 1002101001, "children": [ { "id": 1005110101, diff --git a/Admin/Install/Taxes/de_small_business.json b/Admin/Install/Taxes/de_small_business.json index 691de90..9c03a6e 100644 --- a/Admin/Install/Taxes/de_small_business.json +++ b/Admin/Install/Taxes/de_small_business.json @@ -1,4 +1,184 @@ [ + { + "type": 1, + "item_code": "GENERAL", + "account_code": "EU", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "AT", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "BE", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "BG", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "HR", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "CY", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "CZ", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "DK", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "EE", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "FI", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "FR", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "DE", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "GR", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "HU", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "IE", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "IT", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "LV", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "LT", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "LU", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "MT", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "NL", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "PL", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "PT", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "RO", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "SK", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "SI", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "ES", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "SE", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "GB", + "tax_code": "SBIZ_0" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "INT", + "tax_code": "SBIZ_0" + }, { "type": 1, "item_code": "SOFTWARE", diff --git a/Admin/Install/Taxes/taxes.json b/Admin/Install/Taxes/taxes.json index 8e057f1..68e2d49 100644 --- a/Admin/Install/Taxes/taxes.json +++ b/Admin/Install/Taxes/taxes.json @@ -1,4 +1,207 @@ [ + { + "type": 1, + "item_code": "GENERAL", + "account_code": "EU", + "tax_code": "EU_S0", + "account": "8400" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "AT", + "tax_code": "AT_S20" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "BE", + "tax_code": "BE_S21" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "BG", + "tax_code": "BG_S20" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "HR", + "tax_code": "HR_S25" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "CY", + "tax_code": "CY_S19" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "CZ", + "tax_code": "CZ_S21" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "DK", + "tax_code": "DK_S25" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "EE", + "tax_code": "EE_S20" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "FI", + "tax_code": "FI_S24" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "FR", + "tax_code": "FR_S20" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "DE", + "tax_code": "DE_M19", + "account": "8400" + }, + { + "type": 2, + "item_code": "GENERAL", + "account_code": "DE", + "tax_code": "DE_V19", + "account": "3400" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "DE_0", + "tax_code": "SBIZ_0", + "account": "8195" + }, + { + "type": 2, + "item_code": "GENERAL", + "account_code": "DE_0", + "tax_code": "SBIZ_0", + "account": "3349" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "GR", + "tax_code": "GR_S24" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "HU", + "tax_code": "HU_S27" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "IE", + "tax_code": "IE_S23" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "IT", + "tax_code": "IT_S22" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "LV", + "tax_code": "LV_S21" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "LT", + "tax_code": "LT_S21" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "LU", + "tax_code": "LU_S17" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "MT", + "tax_code": "MT_S18" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "NL", + "tax_code": "NL_S21" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "PL", + "tax_code": "PL_S23" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "PT", + "tax_code": "PT_S23" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "RO", + "tax_code": "RO_S19" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "SK", + "tax_code": "SK_S20" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "SI", + "tax_code": "SI_S22" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "ES", + "tax_code": "ES_S21" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "SE", + "tax_code": "SE_S25" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "GB", + "tax_code": "GB_S20" + }, + { + "type": 1, + "item_code": "GENERAL", + "account_code": "INT", + "tax_code": "S0" + }, { "type": 1, "item_code": "SOFTWARE", @@ -69,7 +272,29 @@ "type": 1, "item_code": "SOFTWARE", "account_code": "DE", - "tax_code": "DE_S19" + "tax_code": "DE_M19", + "account": "8400" + }, + { + "type": 2, + "item_code": "SOFTWARE", + "account_code": "DE", + "tax_code": "DE_V19", + "account": "3400" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "DE_0", + "tax_code": "SBIZ_0", + "account": "8195" + }, + { + "type": 2, + "item_code": "SOFTWARE", + "account_code": "DE_0", + "tax_code": "SBIZ_0", + "account": "3349" }, { "type": 1, @@ -249,7 +474,29 @@ "type": 1, "item_code": "SERVICE", "account_code": "DE", - "tax_code": "DE_S19" + "tax_code": "DE_M19", + "account": "8400" + }, + { + "type": 2, + "item_code": "SERVICE", + "account_code": "DE", + "tax_code": "DE_V19", + "account": "3400" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "DE_0", + "tax_code": "SBIZ_0", + "account": "8195" + }, + { + "type": 2, + "item_code": "SERVICE", + "account_code": "DE_0", + "tax_code": "SBIZ_0", + "account": "3349" }, { "type": 1, diff --git a/Admin/Install/db.json b/Admin/Install/db.json index 7e9fa21..66c1067 100755 --- a/Admin/Install/db.json +++ b/Admin/Install/db.json @@ -1,4 +1,112 @@ { + "billing_payment_term": { + "name": "billing_payment_term", + "fields": { + "billing_payment_term_id": { + "name": "billing_payment_term_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "billing_payment_term_code": { + "description": "In days", + "name": "billing_payment_term_code", + "type": "VARCHAR(100)", + "null": false, + "unique": true + }, + "billing_payment_term_due": { + "description": "In days", + "name": "billing_payment_term_due", + "type": "INT", + "null": false + } + } + }, + "billing_payment_term_l11n": { + "name": "billing_payment_term_l11n", + "fields": { + "billing_payment_term_l11n_id": { + "name": "billing_payment_term_l11n_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "billing_payment_term_l11n_name": { + "name": "billing_payment_term_l11n_name", + "type": "VARCHAR(255)", + "null": false + }, + "billing_payment_term_l11n_term": { + "name": "billing_payment_term_l11n_term", + "type": "INT", + "null": false, + "foreignTable": "billing_payment_term", + "foreignKey": "billing_payment_term_id" + }, + "billing_payment_term_l11n_language": { + "name": "billing_payment_term_l11n_language", + "type": "VARCHAR(2)", + "default": null, + "null": true, + "foreignTable": "language", + "foreignKey": "language_639_1" + } + } + }, + "billing_shipping_term": { + "name": "billing_shipping_term", + "fields": { + "billing_shipping_term_id": { + "name": "billing_shipping_term_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "billing_shipping_term_code": { + "description": "In days", + "name": "billing_shipping_term_code", + "type": "VARCHAR(100)", + "null": false, + "unique": true + } + } + }, + "billing_shipping_term_l11n": { + "name": "billing_shipping_term_l11n", + "fields": { + "billing_shipping_term_l11n_id": { + "name": "billing_shipping_term_l11n_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "billing_shipping_term_l11n_name": { + "name": "billing_shipping_term_l11n_name", + "type": "VARCHAR(255)", + "null": false + }, + "billing_shipping_term_l11n_term": { + "name": "billing_shipping_term_l11n_term", + "type": "INT", + "null": false, + "foreignTable": "billing_shipping_term", + "foreignKey": "billing_shipping_term_id" + }, + "billing_shipping_term_l11n_language": { + "name": "billing_shipping_term_l11n_language", + "type": "VARCHAR(2)", + "default": null, + "null": true, + "foreignTable": "language", + "foreignKey": "language_639_1" + } + } + }, "billing_price": { "name": "billing_price", "fields": { @@ -27,8 +135,16 @@ "foreignTable": "itemmgmt_item", "foreignKey": "itemmgmt_item_id" }, - "billing_price_itemgroup": { - "name": "billing_price_itemgroup", + "billing_price_itemsalesgroup": { + "name": "billing_price_itemsalesgroup", + "type": "INT", + "null": true, + "default": null, + "foreignTable": "itemmgmt_attr_value", + "foreignKey": "itemmgmt_attr_value_id" + }, + "billing_price_itemproductgroup": { + "name": "billing_price_itemproductgroup", "type": "INT", "null": true, "default": null, @@ -126,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", @@ -234,6 +355,18 @@ "null": false, "default": null }, + "billing_tax_tax1_account": { + "name": "billing_tax_tax1_account", + "type": "VARCHAR(10)", + "null": false, + "default": null + }, + "billing_tax_tax2_account": { + "name": "billing_tax_tax2_account", + "type": "VARCHAR(10)", + "null": false, + "default": null + }, "billing_tax_refund_account": { "name": "billing_tax_refund_account", "type": "VARCHAR(10)", @@ -246,6 +379,24 @@ "null": false, "default": null }, + "billing_tax_cashback_account": { + "name": "billing_tax_cashback_account", + "type": "VARCHAR(10)", + "null": false, + "default": null + }, + "billing_tax_overpayment_account": { + "name": "billing_tax_overpayment_account", + "type": "VARCHAR(10)", + "null": false, + "default": null + }, + "billing_tax_underpayment_account": { + "name": "billing_tax_underpayment_account", + "type": "VARCHAR(10)", + "null": false, + "default": null + }, "billing_tax_min_price": { "name": "billing_tax_min_price", "type": "BIGINT", @@ -312,12 +463,24 @@ "type": "TINYINT(1)", "null": false }, + "billing_type_accounting": { + "description": "Is this bill relevant for accounting", + "name": "billing_type_accounting", + "type": "TINYINT(1)", + "null": false + }, "billing_type_transfer_sign": { "description": "1 = from->to direction, -1 = to->from direction = credit note", "name": "billing_type_transfer_sign", "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)", @@ -413,6 +576,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", @@ -437,17 +605,25 @@ }, "billing_bill_template": { "name": "billing_bill_template", - "type": "INT", - "null": true, - "default": null, - "foreignTable": "media", - "foreignKey": "media_id" + "type": "TINYINT(1)", + "null": false + }, + "billing_bill_archived": { + "name": "billing_bill_archived", + "type": "TINYINT(1)", + "null": false }, "billing_bill_account_no": { "name": "billing_bill_account_no", "type": "VARCHAR(50)", "null": false }, + "billing_bill_tax_type": { + "name": "billing_bill_tax_type", + "type": "INT", + "null": true, + "default": null + }, "billing_bill_supplier": { "name": "billing_bill_supplier", "type": "INT", @@ -585,21 +761,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", @@ -615,8 +781,8 @@ "type": "BIGINT", "null": false }, - "billing_bill_grossdiscount": { - "name": "billing_bill_grossdiscount", + "billing_bill_taxp": { + "name": "billing_bill_taxp", "type": "BIGINT", "null": false }, @@ -633,6 +799,11 @@ "foreignTable": "language", "foreignKey": "language_639_1" }, + "billing_bill_fiaccount": { + "name": "billing_bill_fiaccount", + "type": "VARCHAR(10)", + "null": false + }, "billing_bill_referral": { "name": "billing_bill_referral", "type": "INT", @@ -641,18 +812,41 @@ "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", "null": false }, + "billing_bill_accsegment": { + "description": "attribute values", + "name": "billing_bill_accsegment", + "type": "INT", + "null": true, + "default": null + }, + "billing_bill_accsection": { + "description": "attribute values", + "name": "billing_bill_accsection", + "type": "INT", + "null": true, + "default": null + }, + "billing_bill_accgroup": { + "description": "attribute values", + "name": "billing_bill_accgroup", + "type": "INT", + "null": true, + "default": null + }, + "billing_bill_acctype": { + "description": "attribute values", + "name": "billing_bill_acctype", + "type": "INT", + "null": true, + "default": null + }, "billing_bill_payment": { + "description": "should this handle the dues?", "name": "billing_bill_payment", "type": "INT", "null": false @@ -666,7 +860,10 @@ "billing_bill_paymentterms": { "name": "billing_bill_paymentterms", "type": "INT", - "null": false + "null": true, + "default": null, + "foreignTable": "billing_payment_term", + "foreignKey": "billing_payment_term_id" }, "billing_bill_paymentterms_text": { "name": "billing_bill_paymentterms_text", @@ -677,7 +874,10 @@ "billing_bill_ship_type": { "name": "billing_bill_ship_type", "type": "INT", - "null": false + "null": true, + "default": null, + "foreignTable": "billing_shipping_term", + "foreignKey": "billing_shipping_term_id" }, "billing_bill_ship_text": { "name": "billing_bill_ship_text", @@ -784,6 +984,14 @@ "foreignTable": "itemmgmt_item", "foreignKey": "itemmgmt_item_id" }, + "billing_bill_element_container": { + "name": "billing_bill_element_container", + "type": "INT", + "null": false, + "default": null, + "foreignTable": "itemmgmt_item_container", + "foreignKey": "itemmgmt_item_container_id" + }, "billing_bill_element_subscription": { "name": "billing_bill_element_subscription", "type": "INT", @@ -815,7 +1023,7 @@ }, "billing_bill_element_quantity": { "name": "billing_bill_element_quantity", - "type": "INT", + "type": "BIGINT", "null": false }, "billing_bill_element_single_netlistprice": { @@ -848,6 +1056,12 @@ "null": true, "default": null }, + "billing_bill_element_single_effectivenetsalesprice": { + "name": "billing_bill_element_single_effectivenetsalesprice", + "type": "BIGINT", + "null": true, + "default": null + }, "billing_bill_element_single_grosssalesprice": { "name": "billing_bill_element_single_grosssalesprice", "type": "BIGINT", @@ -872,48 +1086,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", @@ -962,6 +1152,14 @@ "null": true, "default": null }, + "billing_bill_element_tax_combination": { + "name": "billing_bill_element_tax_combination", + "type": "INT", + "null": true, + "default": null, + "foreignTable": "billing_tax", + "foreignKey": "billing_tax_id" + }, "billing_bill_element_tax_type": { "name": "billing_bill_element_tax_type", "type": "VARCHAR(10)", @@ -986,6 +1184,51 @@ "null": true, "default": null }, + "billing_bill_element_segment": { + "description": "attribute values", + "name": "billing_bill_element_segment", + "type": "INT", + "null": true, + "default": null, + "foreignTable": "itemmgmt_attr_value", + "foreignKey": "itemmgmt_attr_value_id" + }, + "billing_bill_element_section": { + "description": "attribute values", + "name": "billing_bill_element_section", + "type": "INT", + "null": true, + "default": null, + "foreignTable": "itemmgmt_attr_value", + "foreignKey": "itemmgmt_attr_value_id" + }, + "billing_bill_element_salesgroup": { + "description": "attribute values", + "name": "billing_bill_element_salesgroup", + "type": "INT", + "null": true, + "default": null, + "foreignTable": "itemmgmt_attr_value", + "foreignKey": "itemmgmt_attr_value_id" + }, + "billing_bill_element_productgroup": { + "description": "attribute values", + "name": "billing_bill_element_productgroup", + "type": "INT", + "null": true, + "default": null, + "foreignTable": "itemmgmt_attr_value", + "foreignKey": "itemmgmt_attr_value_id" + }, + "billing_bill_element_itemtype": { + "description": "attribute values", + "name": "billing_bill_element_itemtype", + "type": "INT", + "null": true, + "default": null, + "foreignTable": "itemmgmt_attr_value", + "foreignKey": "itemmgmt_attr_value_id" + }, "billing_bill_element_bill": { "name": "billing_bill_element_bill", "type": "INT", @@ -993,9 +1236,24 @@ "foreignTable": "billing_bill", "foreignKey": "billing_bill_id" }, + "billing_bill_element_fiaccount": { + "name": "billing_bill_element_fiaccount", + "type": "VARCHAR(10)", + "null": false + }, + "billing_bill_element_costcenter": { + "name": "billing_bill_element_costcenter", + "type": "VARCHAR(10)", + "null": false + }, + "billing_bill_element_costobject": { + "name": "billing_bill_element_costobject", + "type": "VARCHAR(10)", + "null": false + }, "billing_bill_element_promotion": { "name": "billing_bill_element_promotion", - "type": "INT", + "type": "VARCHAR(10)", "default": null, "null": true }, @@ -1038,6 +1296,16 @@ "type": "TINYINT(1)", "null": false }, + "billing_attr_type_repeatable": { + "name": "billing_attr_type_repeatable", + "type": "TINYINT(1)", + "null": false + }, + "billing_attr_type_internal": { + "name": "billing_attr_type_internal", + "type": "TINYINT(1)", + "null": false + }, "billing_attr_type_required": { "description": "Every item must have this attribute type if set to true.", "name": "billing_attr_type_required", diff --git a/Admin/Install/paymentterms.json b/Admin/Install/paymentterms.json new file mode 100644 index 0000000..394d4ef --- /dev/null +++ b/Admin/Install/paymentterms.json @@ -0,0 +1,149 @@ +[ + { + "name": "PORI", + "l11n": { + "en" : "Payment on receipt of invoice", + "de" : "Zahlung fällig sofort nach Rechnungserhalt" + } + }, + { + "name": "Net 10", + "l11n": { + "en" : "Payment is due 10 days after the invoice date", + "de" : "Zahlung fällig 10 Tage nach Rechnungsdatum" + } + }, + { + "name": "Net 15", + "l11n": { + "en" : "Payment is due 15 days after the invoice date", + "de" : "Zahlung fällig 15 Tage nach Rechnungsdatum" + } + }, + { + "name": "Net 30", + "l11n": { + "en" : "Payment is due 30 days after the invoice date", + "de" : "Zahlung fällig 30 Tage nach Rechnungsdatum" + } + }, + { + "name": "Net 60", + "l11n": { + "en" : "Payment is due 60 days after the invoice date", + "de" : "Zahlung fällig 60 Tage nach Rechnungsdatum" + } + }, + { + "name": "Net 90", + "l11n": { + "en" : "Payment is due 90 days after the invoice date", + "de" : "Zahlung fällig 90 Tage nach Rechnungsdatum" + } + }, + { + "name": "COD", + "l11n": { + "en" : "Payment is made at the time of delivery", + "de" : "Zahlung fällig bei Lieferung" + } + }, + { + "name": "CWO", + "l11n": { + "en" : "Cash with order", + "de" : "Zahlung fällig bei Bestellung" + } + }, + { + "name": "CBS", + "l11n": { + "en" : "Cash before shipment", + "de" : "Zahlung fällig vor Lieferung" + } + }, + { + "name": "Monthly", + "l11n": { + "en" : "Due at the end of the month", + "de" : "Zahlung fällig zum Monatsende des Rechnungsdatums" + } + }, + { + "name": "PIA", + "l11n": { + "en" : "Payment in advance (pay-as-you-use)", + "de" : "Vorauszahlung (Pay-as-you-use)" + } + }, + { + "name": "2/10 Net 30", + "l11n": { + "en" : "2% discount for payment within 10 days after invoice date; otherwise, the full amount is due in 30 days after invoice date", + "de" : "2% Skonto innerhalb von 10 Tagen nach Rechnungsdatum, andernfalls fällig zum vollen Betrag nach 30 Tagen nach Rechnungsdatum" + } + }, + { + "name": "3/10 Net 30", + "l11n": { + "en" : "3% discount for payment within 10 days after invoice date; otherwise, the full amount is due in 30 days after invoice date", + "de" : "3% Skonto innerhalb von 10 Tagen nach Rechnungsdatum, andernfalls fällig zum vollen Betrag nach 30 Tagen nach Rechnungsdatum" + } + }, + { + "name": "2/15 Net 30", + "l11n": { + "en" : "2% discount for payment within 15 days after invoice date; otherwise, the full amount is due in 30 days after invoice date", + "de" : "2% Skonto innerhalb von 15 Tagen nach Rechnungsdatum, andernfalls fällig zum vollen Betrag nach 30 Tagen nach Rechnungsdatum" + } + }, + { + "name": "3/15 Net 30", + "l11n": { + "en" : "3% discount for payment within 15 days after invoice date; otherwise, the full amount is due in 30 days after invoice date", + "de" : "3% Skonto innerhalb von 15 Tagen nach Rechnungsdatum, andernfalls fällig zum vollen Betrag nach 30 Tagen nach Rechnungsdatum" + } + }, + { + "name": "Escrow", + "l11n": { + "en" : "Payment is held by a third party until certain conditions are met (Escrow)", + "de" : "Die Zahlung wird von einer dritten Partei zurückgehalten, bis bestimmte Bedingungen erfüllt sind (Escrow)" + } + }, + { + "name": "Open account", + "l11n": { + "en" : "There is a credit agreement and payment is expected within the agreed period", + "de" : "Es besteht eine Kreditvereinbarung, und die Zahlung wird innerhalb der vereinbarten Frist erwartet" + } + }, + { + "name": "Installment payments", + "l11n": { + "en" : "Payments are divided into installments over a defined period", + "de" : "Die Zahlungen werden in Raten über einen bestimmten Zeitraum verteilt" + } + }, + { + "name": "LC", + "l11n": { + "en" : "Payment is guaranteed by a bank upon presentation of specified documents", + "de" : "Die Zahlung wird von einer Bank gegen Vorlage bestimmter Dokumente garantiert" + } + }, + { + "name": "Consignment", + "l11n": { + "en" : "Payment is made only after the goods are sold by the buyer", + "de" : "Die Zahlung erfolgt erst nach dem Verkauf der Ware durch den Käufer" + } + }, + { + "name": "Retainer", + "l11n": { + "en" : "A partial upfront payment to secure services", + "de" : "Eine teilweise Vorauszahlung zur Sicherung der Dienstleistungen" + } + } +] \ No newline at end of file diff --git a/Admin/Install/shippingterms.json b/Admin/Install/shippingterms.json new file mode 100644 index 0000000..7c11ace --- /dev/null +++ b/Admin/Install/shippingterms.json @@ -0,0 +1,79 @@ +[ + { + "name": "EXW", + "l11n": { + "en": "Incoterms 2020 Ex Works", + "de": "Incotemrs 2020 Ex Works" + } + }, + { + "name": "FCA", + "l11n": { + "en": "Incoterms 2020 Free Carrier", + "de": "Incotemrs 2020 Free Carrier" + } + }, + { + "name": "CPT", + "l11n": { + "en": "Incoterms 2020 Carriage Paid to", + "de": "Incotemrs 2020 Carriage Paid to" + } + }, + { + "name": "CIP", + "l11n": { + "en": "Incoterms 2020 Carriage and Insurance Paid To", + "de": "Incotemrs 2020 Carriage and Insurance Paid To" + } + }, + { + "name": "DAP", + "l11n": { + "en": "Incoterms 2020 Delivered at Place", + "de": "Incotemrs 2020 Delivered at Place" + } + }, + { + "name": "DPU", + "l11n": { + "en": "Incoterms 2020 Delivered at Place Unloaded", + "de": "Incotemrs 2020 Delivered at Place Unloaded" + } + }, + { + "name": "DDP", + "l11n": { + "en": "Incoterms 2020 Delivered Duty Paid", + "de": "Incotemrs 2020 Delivered Duty Paid" + } + }, + { + "name": "FAS", + "l11n": { + "en": "Incoterms 2020 Free Alongside Ship", + "de": "Incotemrs 2020 Free Alongside Ship" + } + }, + { + "name": "FOB", + "l11n": { + "en": "Incoterms 2020 Free on Board", + "de": "Incotemrs 2020 Free on Board" + } + }, + { + "name": "CFR", + "l11n": { + "en": "Incoterms 2020 Cost and Freight", + "de": "Incotemrs 2020 Cost and Freight" + } + }, + { + "name": "CIF", + "l11n": { + "en": "Incoterms 2020 Cost Insurance and Freight", + "de": "Incotemrs 2020 Cost Insurance and Freight" + } + } +] \ No newline at end of file diff --git a/Admin/Install/types.json b/Admin/Install/types.json index 165483c..575b93b 100755 --- a/Admin/Install/types.json +++ b/Admin/Install/types.json @@ -1,8 +1,11 @@ [ { "name": "sales_offer", - "numberFormat": "{y}-{sequence}", + "numberFormat": "{y}{type}-{m}{sequence}", "transferType": 1, + "sign": 1, + "email": true, + "isAccounting": false, "transferStock": false, "isTemplate": false, "l11n": { @@ -12,8 +15,11 @@ }, { "name": "sales_order_confirmation", - "numberFormat": "{y}-{sequence}", + "numberFormat": "{y}{type}-{m}{sequence}", "transferType": 1, + "sign": 1, + "email": true, + "isAccounting": false, "transferStock": false, "isTemplate": false, "l11n": { @@ -23,8 +29,11 @@ }, { "name": "sales_delivery_note", - "numberFormat": "{y}-{sequence}", + "numberFormat": "{y}{type}-{m}{sequence}", "transferType": 1, + "sign": 1, + "email": false, + "isAccounting": false, "transferStock": true, "isTemplate": false, "l11n": { @@ -34,8 +43,11 @@ }, { "name": "sales_invoice", - "numberFormat": "{y}-{sequence}", + "numberFormat": "{y}{type}-{m}{sequence}", "transferType": 1, + "sign": 1, + "email": true, + "isAccounting": true, "transferStock": false, "isTemplate": false, "l11n": { @@ -45,8 +57,11 @@ }, { "name": "sales_proforma_invoice", - "numberFormat": "{y}-{sequence}", + "numberFormat": "{y}{type}-{m}{sequence}", "transferType": 1, + "sign": 1, + "email": true, + "isAccounting": false, "transferStock": false, "isTemplate": false, "l11n": { @@ -56,8 +71,11 @@ }, { "name": "sales_credit_note", - "numberFormat": "{y}-{sequence}", + "numberFormat": "{y}{type}-{m}{sequence}", "transferType": 1, + "sign": -1, + "email": true, + "isAccounting": true, "transferStock": false, "isTemplate": false, "l11n": { @@ -67,8 +85,11 @@ }, { "name": "sales_reverse_invoice", - "numberFormat": "{y}-{sequence}", + "numberFormat": "{y}{type}-{m}{sequence}", "transferType": 1, + "sign": -1, + "email": true, + "isAccounting": true, "transferStock": false, "isTemplate": false, "l11n": { @@ -78,8 +99,11 @@ }, { "name": "purchase_offer", - "numberFormat": "{y}-{sequence}", + "numberFormat": "{y}{type}-{m}{sequence}", "transferType": 2, + "sign": -1, + "email": false, + "isAccounting": false, "transferStock": false, "isTemplate": false, "l11n": { @@ -88,9 +112,26 @@ } }, { - "name": "purchase_order_confirmation", - "numberFormat": "{y}-{sequence}", + "name": "purchase_order", + "numberFormat": "{y}{type}-{m}{sequence}", "transferType": 2, + "sign": -1, + "email": true, + "isAccounting": false, + "transferStock": false, + "isTemplate": false, + "l11n": { + "en": "Order", + "de": "Bestellung" + } + }, + { + "name": "purchase_order_confirmation", + "numberFormat": "{y}{type}-{m}{sequence}", + "transferType": 2, + "sign": -1, + "email": false, + "isAccounting": false, "transferStock": false, "isTemplate": false, "l11n": { @@ -100,8 +141,11 @@ }, { "name": "purchase_delivery_note", - "numberFormat": "{y}-{sequence}", + "numberFormat": "{y}{type}-{m}{sequence}", "transferType": 2, + "sign": -1, + "email": false, + "isAccounting": false, "transferStock": true, "isTemplate": false, "l11n": { @@ -111,8 +155,11 @@ }, { "name": "purchase_invoice", - "numberFormat": "{y}-{sequence}", + "numberFormat": "{y}{type}-{m}{sequence}", "transferType": 2, + "sign": -1, + "email": false, + "isAccounting": true, "transferStock": false, "isTemplate": false, "l11n": { @@ -122,8 +169,11 @@ }, { "name": "purchase_proforma_invoice", - "numberFormat": "{y}-{sequence}", + "numberFormat": "{y}{type}-{m}{sequence}", "transferType": 2, + "sign": -1, + "email": false, + "isAccounting": false, "transferStock": false, "isTemplate": false, "l11n": { @@ -133,8 +183,11 @@ }, { "name": "purchase_credit_note", - "numberFormat": "{y}-{sequence}", + "numberFormat": "{y}{type}-{m}{sequence}", "transferType": 2, + "sign": 1, + "email": false, + "isAccounting": true, "transferStock": false, "isTemplate": false, "l11n": { @@ -144,8 +197,11 @@ }, { "name": "purchase_reverse_invoice", - "numberFormat": "{y}-{sequence}", + "numberFormat": "{y}{type}-{m}{sequence}", "transferType": 2, + "sign": 1, + "email": false, + "isAccounting": true, "transferStock": false, "isTemplate": false, "l11n": { @@ -155,8 +211,10 @@ }, { "name": "stock_movement", - "numberFormat": "{y}-{sequence}", + "numberFormat": "{y}{type}-{m}{sequence}", "transferType": 4, + "sign": 1, + "isAccounting": false, "transferStock": false, "isTemplate": false, "l11n": { @@ -165,20 +223,37 @@ } }, { - "name": "stock_scrapping", - "numberFormat": "{y}-{sequence}", + "name": "stock_decrease", + "numberFormat": "{y}{type}-{m}{sequence}", "transferType": 4, - "transferStock": false, + "sign": -1, + "isAccounting": false, + "transferStock": true, "isTemplate": false, "l11n": { - "en": "Scrapping", - "de": "Verschrottung" + "en": "Stock Decrease", + "de": "Lagerausbuchung" } }, { - "name": "sales_subscritpion", - "numberFormat": "{y}-{sequence}", + "name": "stock_increase", + "numberFormat": "{y}{type}-{m}{sequence}", + "transferType": 4, + "sign": 1, + "isAccounting": false, + "transferStock": true, + "isTemplate": false, + "l11n": { + "en": "Stock Increase", + "de": "Lagereinbuchung" + } + }, + { + "name": "sales_subscription", + "numberFormat": "{y}{type}-{m}{sequence}", "transferType": 1, + "sign": 1, + "isAccounting": true, "transferStock": false, "isTemplate": false, "l11n": { @@ -187,36 +262,16 @@ } }, { - "name": "sales_template", - "numberFormat": "{y}-{sequence}", - "transferType": 1, - "transferStock": false, - "isTemplate": true, - "l11n": { - "en": "Template", - "de": "Vorlage" - } - }, - { - "name": "purchase_subscritpion", - "numberFormat": "{y}-{sequence}", + "name": "purchase_subscription", + "numberFormat": "{y}{type}-{m}{sequence}", "transferType": 2, + "sign": -1, + "isAccounting": true, "transferStock": false, "isTemplate": false, "l11n": { "en": "Subscription", "de": "Abonnement" } - }, - { - "name": "purchase_template", - "numberFormat": "{y}-{sequence}", - "transferType": 2, - "transferStock": false, - "isTemplate": true, - "l11n": { - "en": "Template", - "de": "Vorlage" - } } ] \ No newline at end of file diff --git a/Admin/Installer.php b/Admin/Installer.php index 38d9f93..e5afc82 100755 --- a/Admin/Installer.php +++ b/Admin/Installer.php @@ -17,14 +17,13 @@ namespace Modules\Billing\Admin; use Modules\Billing\Models\BillTransferType; use Modules\ClientManagement\Models\Attribute\ClientAttributeTypeMapper; use Modules\ItemManagement\Models\Attribute\ItemAttributeTypeMapper; -use Modules\SupplierManagement\Models\SupplierAttributeTypeMapper; +use Modules\SupplierManagement\Models\Attribute\SupplierAttributeTypeMapper; use phpOMS\Application\ApplicationAbstract; use phpOMS\Config\SettingsInterface; use phpOMS\Message\Http\HttpRequest; use phpOMS\Message\Http\HttpResponse; use phpOMS\Module\InstallerAbstract; use phpOMS\Module\ModuleInfo; -use phpOMS\Uri\HttpUri; /** * Installer class. @@ -99,6 +98,26 @@ final class Installer extends InstallerAbstract $attrTypes = self::createBillAttributeTypes($app, $attributes); $attrValues = self::createBillAttributeValues($app, $attrTypes, $attributes); + + /* Payment terms */ + $fileContent = \file_get_contents(__DIR__ . '/Install/paymentterms.json'); + if ($fileContent === false) { + return; + } + + /** @var array $terms */ + $terms = \json_decode($fileContent, true); + $paymentTypeArray = self::createPaymentTerms($app, $terms); + + /* Shipping terms */ + $fileContent = \file_get_contents(__DIR__ . '/Install/shippingterms.json'); + if ($fileContent === false) { + return; + } + + /** @var array $terms */ + $terms = \json_decode($fileContent, true); + $shippingTypeArray = self::createShippingTerms($app, $terms); } /** @@ -116,19 +135,21 @@ final class Installer extends InstallerAbstract /** @var array $billAttrType */ $billAttrType = []; - /** @var \Modules\Billing\Controller\ApiController $module */ - $module = $app->moduleManager->getModuleInstance('Billing'); + /** @var \Modules\Billing\Controller\ApiAttributeController $module */ + $module = $app->moduleManager->get('Billing', 'ApiAttribute'); /** @var array $attribute */ foreach ($attributes as $attribute) { $response = new HttpResponse(); - $request = new HttpRequest(new HttpUri('')); + $request = new HttpRequest(); $request->header->account = 1; $request->setData('name', $attribute['name'] ?? ''); $request->setData('title', \reset($attribute['l11n'])); $request->setData('language', \array_keys($attribute['l11n'])[0] ?? 'en'); $request->setData('is_required', $attribute['is_required'] ?? false); + $request->setData('repeatable', $attribute['repeatable'] ?? false); + $request->setData('internal', $attribute['internal'] ?? false); $request->setData('custom', $attribute['is_custom_allowed'] ?? false); $request->setData('validation_pattern', $attribute['validation_pattern'] ?? ''); $request->setData('datatype', (int) $attribute['value_type']); @@ -153,7 +174,7 @@ final class Installer extends InstallerAbstract } $response = new HttpResponse(); - $request = new HttpRequest(new HttpUri('')); + $request = new HttpRequest(); $request->header->account = 1; $request->setData('title', $l11n); @@ -183,8 +204,8 @@ final class Installer extends InstallerAbstract /** @var array $billAttrValue */ $billAttrValue = []; - /** @var \Modules\Billing\Controller\ApiController $module */ - $module = $app->moduleManager->getModuleInstance('Billing'); + /** @var \Modules\Billing\Controller\ApiAttributeController $module */ + $module = $app->moduleManager->get('Billing', 'ApiAttribute'); foreach ($attributes as $attribute) { $billAttrValue[$attribute['name']] = []; @@ -192,7 +213,7 @@ final class Installer extends InstallerAbstract /** @var array $value */ foreach ($attribute['values'] as $value) { $response = new HttpResponse(); - $request = new HttpRequest(new HttpUri('')); + $request = new HttpRequest(); $request->header->account = 1; $request->setData('value', $value['value'] ?? ''); @@ -226,7 +247,7 @@ final class Installer extends InstallerAbstract } $response = new HttpResponse(); - $request = new HttpRequest(new HttpUri('')); + $request = new HttpRequest(); $request->header->account = 1; $request->setData('title', $l11n); @@ -255,8 +276,8 @@ final class Installer extends InstallerAbstract { $result = []; - /** @var \Modules\Billing\Controller\ApiController $module */ - $module = $app->moduleManager->getModuleInstance('Billing'); + /** @var \Modules\Billing\Controller\ApiTaxController $module */ + $module = $app->moduleManager->get('Billing', 'ApiTax'); /** @var \Modules\Attribute\Models\AttributeType $itemAttributeSales */ $itemAttributeSales = ItemAttributeTypeMapper::get() @@ -283,13 +304,14 @@ final class Installer extends InstallerAbstract : $supplierAttributeSales->getDefaultByValue($tax['account_code']); $response = new HttpResponse(); - $request = new HttpRequest(new HttpUri('')); + $request = new HttpRequest(); $request->header->account = 1; $request->setData('tax_type', $tax['type']); $request->setData('tax_code', $tax['tax_code']); $request->setData('item_code', $itemValue->id); $request->setData('account_code', $accountValue->id); + $request->setData('account', $tax['account'] ?? null); $module->apiTaxCombinationCreate($request, $response); @@ -323,23 +345,23 @@ final class Installer extends InstallerAbstract { $billTypes = []; - /** @var \Modules\Billing\Controller\ApiController $module */ - $module = $app->moduleManager->getModuleInstance('Billing'); - - // @todo: allow multiple alternative bill templates - // @todo: implement ordering of templates + /** @var \Modules\Billing\Controller\ApiBillTypeController $module */ + $module = $app->moduleManager->get('Billing', 'ApiBillType'); foreach ($types as $type) { $response = new HttpResponse(); - $request = new HttpRequest(new HttpUri('')); + $request = new HttpRequest(); $request->header->account = 1; $request->setData('name', $type['name'] ?? ''); $request->setData('title', \reset($type['l11n'])); $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); $request->setData('transfer_type', $type['transferType'] ?? BillTransferType::SALES); $request->setData('template', $template); @@ -364,7 +386,7 @@ final class Installer extends InstallerAbstract } $response = new HttpResponse(); - $request = new HttpRequest(new HttpUri('')); + $request = new HttpRequest(); $request->header->account = 1; $request->setData('title', $l11n); @@ -377,4 +399,124 @@ final class Installer extends InstallerAbstract return $billTypes; } + + /** + * Install default payment terms + * + * @param ApplicationAbstract $app Application + * @param array $types Payment term definitions + * + * @return array + * + * @since 1.0.0 + */ + private static function createPaymentTerms(ApplicationAbstract $app, array $types) : array + { + /** @var array $paymentTerms */ + $paymentTerms = []; + + /** @var \Modules\Billing\Controller\ApiController $module */ + $module = $app->moduleManager->get('Billing', 'Api'); + + /** @var array $type */ + foreach ($types as $type) { + $response = new HttpResponse(); + $request = new HttpRequest(); + + $request->header->account = 1; + $request->setData('name', $type['name'] ?? ''); + $request->setData('title', \reset($type['l11n'])); + + $module->apiPaymentTermCreate($request, $response); + + $responseData = $response->getData(''); + if (!\is_array($responseData)) { + continue; + } + + $paymentTerms[$type['name']] = \is_array($responseData['response']) + ? $responseData['response'] + : $responseData['response']->toArray(); + + $isFirst = true; + foreach ($type['l11n'] as $language => $l11n) { + if ($isFirst) { + $isFirst = false; + continue; + } + + $response = new HttpResponse(); + $request = new HttpRequest(); + + $request->header->account = 1; + $request->setData('title', $l11n); + $request->setData('language', $language); + $request->setData('type', $paymentTerms[$type['name']]['id']); + + $module->apiPaymentTermL11nCreate($request, $response); + } + } + + return $paymentTerms; + } + + /** + * Install default shipping terms + * + * @param ApplicationAbstract $app Application + * @param array $types Shipping term definitions + * + * @return array + * + * @since 1.0.0 + */ + private static function createShippingTerms(ApplicationAbstract $app, array $types) : array + { + /** @var array $shippingTerms */ + $shippingTerms = []; + + /** @var \Modules\Billing\Controller\ApiController $module */ + $module = $app->moduleManager->get('Billing', 'Api'); + + /** @var array $type */ + foreach ($types as $type) { + $response = new HttpResponse(); + $request = new HttpRequest(); + + $request->header->account = 1; + $request->setData('name', $type['name'] ?? ''); + $request->setData('title', \reset($type['l11n'])); + + $module->apiShippingTermCreate($request, $response); + + $responseData = $response->getData(''); + if (!\is_array($responseData)) { + continue; + } + + $shippingTerms[$type['name']] = \is_array($responseData['response']) + ? $responseData['response'] + : $responseData['response']->toArray(); + + $isFirst = true; + foreach ($type['l11n'] as $language => $l11n) { + if ($isFirst) { + $isFirst = false; + continue; + } + + $response = new HttpResponse(); + $request = new HttpRequest(); + + $request->header->account = 1; + $request->setData('title', $l11n); + $request->setData('language', $language); + $request->setData('type', $shippingTerms[$type['name']]['id']); + + $module->apiShippingTermL11nCreate($request, $response); + } + } + + return $shippingTerms; + } } diff --git a/Admin/Routes/Cli.php b/Admin/Routes/Cli.php index 278c7cc..3b81fd9 100755 --- a/Admin/Routes/Cli.php +++ b/Admin/Routes/Cli.php @@ -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, diff --git a/Admin/Routes/Web/Api.php b/Admin/Routes/Web/Api.php index f20525d..44996e2 100755 --- a/Admin/Routes/Web/Api.php +++ b/Admin/Routes/Web/Api.php @@ -18,7 +18,7 @@ use phpOMS\Account\PermissionType; use phpOMS\Router\RouteVerb; return [ - '^.*/bill/render.*$' => [ + '^.*/bill/render(\?.*$|$)' => [ [ 'dest' => '\Modules\Billing\Controller\ApiBillController:apiMediaRender', 'verb' => RouteVerb::GET, @@ -29,4 +29,37 @@ return [ ], ], ], + '^.*/bill/render/preview(\?.*$|$)' => [ + [ + 'dest' => '\Modules\Billing\Controller\ApiBillController:apiPreviewRender', + 'verb' => RouteVerb::GET, + 'permission' => [ + 'module' => BackendController::NAME, + 'type' => PermissionType::CREATE, + 'state' => PermissionCategory::SALES_INVOICE, + ], + ], + ], + '^.*/bill/price(\?.*$|$)' => [ + [ + 'dest' => '\Modules\Billing\Controller\ApiPriceController:apiPriceCreate', + 'verb' => RouteVerb::GET, + 'permission' => [ + 'module' => BackendController::NAME, + 'type' => PermissionType::CREATE, + 'state' => PermissionCategory::PRICE, + ], + ], + ], + '^.*/bill/parse(\?.*$|$)' => [ + [ + 'dest' => '\Modules\Billing\Controller\ApiPurchaseController:apiInvoiceParse', + 'verb' => RouteVerb::SET, + 'permission' => [ + 'module' => BackendController::NAME, + 'type' => PermissionType::MODIFY, + 'state' => PermissionCategory::PURCHASE_INVOICE, + ], + ], + ], ]; diff --git a/Admin/Routes/Web/Backend.php b/Admin/Routes/Web/Backend.php index 082061c..127427f 100755 --- a/Admin/Routes/Web/Backend.php +++ b/Admin/Routes/Web/Backend.php @@ -18,7 +18,7 @@ use phpOMS\Account\PermissionType; use phpOMS\Router\RouteVerb; return [ - '^.*/sales/bill/create.*$' => [ + '^.*/sales/bill/create(\?.*$|$)' => [ [ 'dest' => '\Modules\Billing\Controller\BackendController:viewBillingSalesInvoiceCreate', 'verb' => RouteVerb::GET, @@ -29,7 +29,7 @@ return [ ], ], ], - '^.*/sales/bill/list.*$' => [ + '^.*/sales/bill/list(\?.*$|$)' => [ [ 'dest' => '\Modules\Billing\Controller\BackendController:viewBillingSalesList', 'verb' => RouteVerb::GET, @@ -40,7 +40,18 @@ return [ ], ], ], - '^.*/sales/bill\?.*$' => [ + '^.*/sales/bill/archive(\?.*$|$)' => [ + [ + 'dest' => '\Modules\Billing\Controller\BackendController:viewBillingSalesArchive', + 'verb' => RouteVerb::GET, + 'permission' => [ + 'module' => BackendController::NAME, + 'type' => PermissionType::READ, + 'state' => PermissionCategory::SALES_INVOICE, + ], + ], + ], + '^.*/sales/bill(\?.*$|$)' => [ [ 'dest' => '\Modules\Billing\Controller\BackendController:viewBillingSalesInvoice', 'verb' => RouteVerb::GET, @@ -52,7 +63,7 @@ return [ ], ], - '^.*/purchase/bill/create.*$' => [ + '^.*/purchase/bill/create(\?.*$|$)' => [ [ 'dest' => '\Modules\Billing\Controller\BackendController:viewBillingPurchaseInvoiceCreate', 'verb' => RouteVerb::GET, @@ -63,7 +74,7 @@ return [ ], ], ], - '^.*/purchase/bill/list.*$' => [ + '^.*/purchase/bill/list(\?.*$|$)' => [ [ 'dest' => '\Modules\Billing\Controller\BackendController:viewBillingPurchaseList', 'verb' => RouteVerb::GET, @@ -74,7 +85,18 @@ return [ ], ], ], - '^.*/purchase/bill\?.*$' => [ + '^.*/purchase/bill/archive(\?.*$|$)' => [ + [ + 'dest' => '\Modules\Billing\Controller\BackendController:viewBillingPurchaseArchive', + 'verb' => RouteVerb::GET, + 'permission' => [ + 'module' => BackendController::NAME, + 'type' => PermissionType::READ, + 'state' => PermissionCategory::PURCHASE_INVOICE, + ], + ], + ], + '^.*/purchase/bill(\?.*$|$)' => [ [ 'dest' => '\Modules\Billing\Controller\BackendController:viewBillingPurchaseInvoice', 'verb' => RouteVerb::GET, @@ -85,7 +107,7 @@ return [ ], ], ], - '^.*/purchase/bill/upload\?.*$' => [ + '^.*/purchase/bill/upload(\?.*$|$)' => [ [ 'dest' => '\Modules\Billing\Controller\BackendController:viewBillingPurchaseInvoiceUpload', 'verb' => RouteVerb::GET, @@ -97,7 +119,7 @@ return [ ], ], - '^.*/warehouse/bill/create.*$' => [ + '^.*/warehouse/bill/create(\?.*$|$)' => [ [ 'dest' => '\Modules\Billing\Controller\BackendController:viewBillingStockInvoiceCreate', 'verb' => RouteVerb::GET, @@ -108,7 +130,7 @@ return [ ], ], ], - '^.*/warehouse/bill/list.*$' => [ + '^.*/warehouse/bill/list(\?.*$|$)' => [ [ 'dest' => '\Modules\Billing\Controller\BackendController:viewBillingStockList', 'verb' => RouteVerb::GET, @@ -119,7 +141,18 @@ return [ ], ], ], - '^.*/warehouse/bill\?.*$' => [ + '^.*/warehouse/bill/archive(\?.*$|$)' => [ + [ + 'dest' => '\Modules\Billing\Controller\BackendController:viewBillingStockArchive', + 'verb' => RouteVerb::GET, + 'permission' => [ + 'module' => BackendController::NAME, + 'type' => PermissionType::READ, + 'state' => PermissionCategory::PURCHASE_INVOICE, + ], + ], + ], + '^.*/warehouse/bill(\?.*$|$)' => [ [ 'dest' => '\Modules\Billing\Controller\BackendController:viewBillingStockInvoice', 'verb' => RouteVerb::GET, @@ -131,7 +164,7 @@ return [ ], ], - '^.*/private/purchase/recognition/dashboard.*$' => [ + '^.*/private/purchase/recognition/dashboard(\?.*$|$)' => [ [ 'dest' => '\Modules\Billing\Controller\BackendController:viewPrivatePurchaseBillDashboard', 'verb' => RouteVerb::GET, @@ -142,7 +175,7 @@ return [ ], ], ], - '^.*/private/purchase/recognition/upload.*$' => [ + '^.*/private/purchase/recognition/upload(\?.*$|$)' => [ [ 'dest' => '\Modules\Billing\Controller\BackendController:viewPrivatePurchaseBillUpload', 'verb' => RouteVerb::GET, @@ -153,7 +186,7 @@ return [ ], ], ], - '^.*/private/purchase/recognition/bill.*$' => [ + '^.*/private/purchase/recognition/bill(\?.*$|$)' => [ [ 'dest' => '\Modules\Billing\Controller\BackendController:viewPrivateBillingPurchaseInvoice', 'verb' => RouteVerb::GET, @@ -164,7 +197,7 @@ return [ ], ], ], - '^.*/purchase/recognition/dashboard.*$' => [ + '^.*/purchase/recognition/dashboard(\?.*$|$)' => [ [ 'dest' => '\Modules\Billing\Controller\BackendController:viewPrivatePurchaseBillDashboard', 'verb' => RouteVerb::GET, @@ -175,7 +208,7 @@ return [ ], ], ], - '^.*/purchase/recognition/upload.*$' => [ + '^.*/purchase/recognition/upload(\?.*$|$)' => [ [ 'dest' => '\Modules\Billing\Controller\BackendController:viewPrivatePurchaseBillUpload', 'verb' => RouteVerb::GET, @@ -186,7 +219,7 @@ return [ ], ], ], - '^.*/purchase/recognition/bill.*$' => [ + '^.*/purchase/recognition/bill(\?.*$|$)' => [ [ 'dest' => '\Modules\Billing\Controller\BackendController:viewPrivateBillingPurchaseInvoice', 'verb' => RouteVerb::GET, @@ -197,4 +230,49 @@ return [ ], ], ], + + '^.*/bill/payment/list(\?.*$|$)' => [ + [ + 'dest' => '\Modules\Billing\Controller\BackendController:viewPaymentList', + 'verb' => RouteVerb::GET, + 'permission' => [ + 'module' => BackendController::NAME, + 'type' => PermissionType::READ, + 'state' => PermissionCategory::PAYMENT_TERM, + ], + ], + ], + '^.*/bill/payment/view(\?.*$|$)' => [ + [ + 'dest' => '\Modules\Billing\Controller\BackendController:viewPaymentView', + 'verb' => RouteVerb::GET, + 'permission' => [ + 'module' => BackendController::NAME, + 'type' => PermissionType::READ, + 'state' => PermissionCategory::PAYMENT_TERM, + ], + ], + ], + '^.*/bill/shipping/list(\?.*$|$)' => [ + [ + 'dest' => '\Modules\Billing\Controller\BackendController:viewShippingList', + 'verb' => RouteVerb::GET, + 'permission' => [ + 'module' => BackendController::NAME, + 'type' => PermissionType::READ, + 'state' => PermissionCategory::SHIPPING_TERM, + ], + ], + ], + '^.*/bill/shipping/view(\?.*$|$)' => [ + [ + 'dest' => '\Modules\Billing\Controller\BackendController:viewShippingView', + 'verb' => RouteVerb::GET, + 'permission' => [ + 'module' => BackendController::NAME, + 'type' => PermissionType::READ, + 'state' => PermissionCategory::SHIPPING_TERM, + ], + ], + ], ]; diff --git a/Controller/ApiAttributeController.php b/Controller/ApiAttributeController.php index d61aa61..0fe467c 100755 --- a/Controller/ApiAttributeController.php +++ b/Controller/ApiAttributeController.php @@ -61,7 +61,26 @@ final class ApiAttributeController extends Controller return; } - $type = BillAttributeTypeMapper::get()->with('defaults')->where('id', (int) $request->getData('type'))->execute(); + $type = BillAttributeTypeMapper::get() + ->with('defaults') + ->where('id', (int) $request->getData('type')) + ->execute(); + + if (!$type->repeatable) { + $attr = BillAttributeMapper::count() + ->with('type') + ->where('type/id', $type->id) + ->where('ref', (int) $request->getData('ref')) + ->executeCount(); + + if ($attr > 0) { + $response->header->status = RequestStatusCode::R_409; + $this->createInvalidCreateResponse($request, $response, $val); + + return; + } + } + $attribute = $this->createAttributeFromRequest($request, $type); $this->createModel($request->header->account, $attribute, BillAttributeMapper::class, 'attribute', $request->getOrigin()); $this->createStandardCreateResponse($request, $response, $attribute); @@ -148,13 +167,20 @@ final class ApiAttributeController extends Controller ->where('id', $request->getDataInt('type') ?? 0) ->execute(); + if ($type->isInternal) { + $response->header->status = RequestStatusCode::R_403; + $this->createInvalidCreateResponse($request, $response, $val); + + return; + } + $attrValue = $this->createAttributeValueFromRequest($request, $type); $this->createModel($request->header->account, $attrValue, BillAttributeValueMapper::class, 'attr_value', $request->getOrigin()); if ($attrValue->isDefault) { $this->createModelRelation( $request->header->account, - (int) $request->getData('type'), + $type->id, $attrValue->id, BillAttributeTypeMapper::class, 'defaults', '', $request->getOrigin() ); @@ -377,7 +403,7 @@ final class ApiAttributeController extends Controller * * @api * - * @todo: implement + * @todo Implement API function * * @since 1.0.0 */ @@ -448,7 +474,7 @@ final class ApiAttributeController extends Controller */ public function apiBillAttributeValueDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void { - // @todo: I don't think values can be deleted? Only Attributes + // @todo I don't think values can be deleted? Only Attributes // However, It should be possible to remove UNUSED default values // either here or other function? // if (!empty($val = $this->validateAttributeValueDelete($request))) { diff --git a/Controller/ApiBillController.php b/Controller/ApiBillController.php index 984b8ba..2b84a91 100755 --- a/Controller/ApiBillController.php +++ b/Controller/ApiBillController.php @@ -16,38 +16,50 @@ namespace Modules\Billing\Controller; use Modules\Admin\Models\NullAccount; use Modules\Admin\Models\SettingsEnum as AdminSettingsEnum; +use Modules\Attribute\Models\NullAttribute; +use Modules\Attribute\Models\NullAttributeType; +use Modules\Attribute\Models\NullAttributeValue; use Modules\Billing\Models\Bill; use Modules\Billing\Models\BillElement; use Modules\Billing\Models\BillElementMapper; use Modules\Billing\Models\BillMapper; use Modules\Billing\Models\BillStatus; +use Modules\Billing\Models\BillTransferType; use Modules\Billing\Models\BillTypeMapper; use Modules\Billing\Models\NullBill; use Modules\Billing\Models\NullBillElement; +use Modules\Billing\Models\PermissionCategory; use Modules\Billing\Models\SettingsEnum; use Modules\ClientManagement\Models\Client; use Modules\ClientManagement\Models\ClientMapper; +use Modules\ItemManagement\Models\Attribute\ItemAttributeMapper; use Modules\ItemManagement\Models\Item; use Modules\ItemManagement\Models\ItemMapper; +use Modules\ItemManagement\Models\NullContainer; use Modules\Media\Models\CollectionMapper; use Modules\Media\Models\Media; +use Modules\Media\Models\MediaClass; use Modules\Media\Models\MediaMapper; +use Modules\Media\Models\NullCollection; use Modules\Media\Models\PathSettings; use Modules\Media\Models\UploadStatus; use Modules\Messages\Models\EmailMapper; use Modules\SupplierManagement\Models\NullSupplier; use Modules\SupplierManagement\Models\Supplier; use Modules\SupplierManagement\Models\SupplierMapper; +use phpOMS\Account\PermissionType; use phpOMS\Application\ApplicationAbstract; use phpOMS\Autoloader; +use phpOMS\DataStorage\Database\Query\ColumnName; 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; use phpOMS\Model\Message\FormValidation; +use phpOMS\Stdlib\Base\FloatInt; use phpOMS\System\MimeType; use phpOMS\Views\View; @@ -68,13 +80,173 @@ final class ApiBillController extends Controller * * @since 1.0.0 */ - public function __construct(ApplicationAbstract $app = null) + public function __construct(?ApplicationAbstract $app = null) { parent::__construct($app); if ($this->app->moduleManager->isActive('WarehouseManagement')) { $this->app->eventManager->importFromFile(__DIR__ . '/../../WarehouseManagement/Admin/Hooks/Manual.php'); } + + if ($this->app->moduleManager->isActive('Accounting')) { + $this->app->eventManager->importFromFile(__DIR__ . '/../../Accounting/Admin/Hooks/Manual.php'); + } + } + + /** + * Create email for/from bill + * + * @param RequestAbstract $request Request + * @param array $data Data + * + * @return void + * + * @since 1.0.0 + */ + public function apiBillEmail(RequestAbstract $request, 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->valueStr) + ? $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->valueStr) + ? $supplier->account->email + : (string) $tmp; + } + } + + if (!empty($email) && $billingTemplate !== null) { + $this->sendBillEmail($media, $email, (int) $billingTemplate->content, $bill->language); + } + } + } + + /** + * Api method to finalize a bill + * + * Finalization creates an archive and possibly sends the bill via email. + * Additionally, it triggers the event Billing-bill-finalize event which also finalizes the stock changes and possibly accounting postings + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiBillFinalize(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; + } + + // Archive bill + /** @var \Modules\Billing\Models\Bill $old */ + $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']; + + $this->app->eventManager->triggerSimilar('PRE:Module:' . self::NAME . '-bill-finalize', '', [ + $request->header->account, + null, $new, + null, self::NAME . '-bill-finalize', + self::NAME, + (string) $new->id, + null, + $request->getOrigin(), + ]); + + // Send bill via email + $this->apiBillEmail($request, ['bill' => $new, 'media' => $media]); + + $this->createStandardUpdateResponse($request, $response, $new); } /** @@ -101,6 +273,16 @@ final class ApiBillController extends Controller /** @var \Modules\Billing\Models\Bill $old */ $old = BillMapper::get()->where('id', (int) $request->getData('bill'))->execute(); + + // @feature Allow to update internal statistical fields + // Example: Referral account + if ($old->status === BillStatus::ARCHIVED) { + $response->header->status = RequestStatusCode::R_423; + $this->createInvalidUpdateResponse($request, $response, $val); + + return; + } + $new = $this->updateBillFromRequest($request, clone $old); $this->updateModel($request->header->account, $old, $new, BillMapper::class, 'bill', $request->getOrigin()); @@ -201,51 +383,100 @@ final class ApiBillController extends Controller /** * Create a base Bill object with default values * + * Client attributes required: + * 'segment', 'section', 'client_group', 'client_type', + * 'sales_tax_code' + * + * Supplier attributes required: + * 'purchase_tax_code' + * * @param Client|Supplier $account The client or supplier object for whom the bill is being created * @param RequestAbstract $request The request object that contains the header account * * @return Bill The new Bill object with default values * - * @todo Validate VAT before creation (maybe need to add a status when last validated, we don't want to validate every time) - * @todo Set the correct date of payment - * @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 Validate VAT before creation + * 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 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 */ public function createBaseBill(Client | Supplier $account, RequestAbstract $request) : Bill { - // @todo: validate vat before creation for clients $bill = new Bill(); $bill->createdBy = new NullAccount($request->header->account); $bill->unit = $account->unit ?? $this->app->unitId; - $bill->billDate = new \DateTime('now'); // @todo: Date of payment - $bill->performanceDate = $request->getDataDateTime('performancedate') ?? new \DateTime('now'); // @todo: Date of payment + $bill->billDate = $request->getDataDateTime('bill_date') ?? new \DateTime('now'); + $bill->performanceDate = $request->getDataDateTime('performancedate') ?? new \DateTime('now'); $bill->accountNumber = $account->number; - $bill->setStatus($request->getDataInt('status') ?? BillStatus::DRAFT); + $bill->external = $request->getDataString('externalreferral') ?? ''; + $bill->status = BillStatus::tryFromValue($request->getDataInt('status')) ?? BillStatus::DRAFT; - $bill->shipping = 0; - $bill->shippingText = ''; + $bill->shippingTerms = null; + $bill->shippingText = ''; - $bill->payment = 0; - $bill->paymentText = ''; + $bill->paymentTerms = null; + $bill->paymentText = ''; + + // @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; + $bill->client = $account; + $bill->accTaxCode = empty($temp = $bill->client->getAttribute('sales_tax_code')->value->id) ? null : $temp; + $bill->accSegment = empty($temp = $bill->client->getAttribute('segment')->value->id) ? null : $temp; + $bill->accSection = empty($temp = $bill->client->getAttribute('section')->value->id) ? null : $temp; + $bill->accGroup = empty($temp = $bill->client->getAttribute('client_group')->value->id) ? null : $temp; + $bill->accType = empty($temp = $bill->client->getAttribute('client_type')->value->id) ? null : $temp; } else { - $bill->supplier = $account; + $bill->supplier = $account; + $bill->accTaxCode = empty($temp = $bill->supplier->getAttribute('purchase_tax_code')->value->id) ? null : $temp; } - // @todo: use bill and shipping address instead of main address if available + // @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; $bill->billZip = $request->getDataString('billtopostal') ?? $account->mainAddress->postal; - $bill->billCountry = $request->getDataString('billtocountry') ?? $account->mainAddress->getCountry(); + $bill->billCountry = $request->getDataString('billtocountry') ?? $account->mainAddress->country; - $bill->setCurrency(ISO4217CharEnum::_EUR); + $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; + } + + /** + * Find bill language. + * + * @param Client|Supplier $account Account (with attributes!!!) + * + * @return string + * + * @since 1.0.0 + */ + private function findBillLanguage(Client|Supplier $account) : string + { /** @var \Model\Setting $settings */ $settings = $this->app->appSettings->get(null, SettingsEnum::VALID_BILL_LANGUAGES, @@ -256,7 +487,7 @@ final class ApiBillController extends Controller if (empty($settings)) { /** @var \Model\Setting $settings */ $settings = $this->app->appSettings->get(null, - SettingsEnum::VALID_BILL_LANGUAGES, + SettingsEnum::VALID_BILL_LANGUAGES, unit: null, module: 'Admin' ); @@ -279,36 +510,29 @@ final class ApiBillController extends Controller if (!empty($accountBillLanguage) && \in_array($accountBillLanguage, $validLanguages)) { $billLanguage = $accountBillLanguage; } else { - $accountLanguages = ISO639x1Enum::languageFromCountry($account->mainAddress->getCountry()); - $accountLanguage = empty($accountLanguages) ? '' : $accountLanguages[0]; + $accountLanguages = ISO639x1Enum::languageFromCountry($account->mainAddress->country); + $accountLanguage = ''; - if (\in_array($accountLanguage, $validLanguages)) { - $billLanguage = $accountLanguage; + foreach ($accountLanguages as $accountLanguage) { + if (\in_array($accountLanguage, $validLanguages)) { + $billLanguage = $accountLanguage; + + break; + } } } - $bill->language = $billLanguage; - - $typeMapper = BillTypeMapper::get() - ->with('l11n') - ->where('l11n/langauge', $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; } /** * Create a base BillElement object with default values * - * @param Client $client The client object for whom the bill is being created + * Item attributes required: + * 'segment', 'section', 'sales_group', 'product_group', 'product_type', + * 'sales_tax_code', 'purchase_tax_code', 'costcenter', 'costobject', + * 'default_purchase_container', 'default_sales_container', + * * @param Item $item The item object for which the bill element is being created * @param Bill $bill The bill object for which the bill element is being created * @param RequestAbstract $request The request object that contains the header account @@ -317,17 +541,95 @@ final class ApiBillController extends Controller * * @since 1.0.0 */ - public function createBaseBillElement(Client $client, Item $item, Bill $bill, RequestAbstract $request) : BillElement + public function createBaseBillElement(Item $item, Bill $bill, RequestAbstract $request) : BillElement { - $taxCode = $this->app->moduleManager->get('Billing', 'ApiTax') - ->getTaxCodeFromClientItem($client, $item, $request->header->l11n->country); + // Handle person tax code for finding tax combination below + $attr = new NullAttribute(); + $attrType = new NullAttributeType(); + $attrType->name = $bill->client !== null ? 'sales_tax_code' : 'purchase_tax_code'; + $attrValue = new NullAttributeValue($bill->accTaxCode ?? 0); + $attr->type = $attrType; + $attr->value = $attrValue; - return BillElement::fromItem( + $container = $request->hasData('container') + ? new NullContainer((int) $request->getData('container')) + : null; + + $attr = new NullAttribute(); + + if ($bill->type->transferType === BillTransferType::PURCHASE && $bill->supplier !== null) { + $bill->supplier->attributes[] = $attr; + + if ($container === null) { + $attr = $item->getAttribute('default_purchase_container'); + if ($attr->id === 0) { + /** @var \Modules\Attribute\Models\Attribute $attr */ + $attr = ItemAttributeMapper::get() + ->with('type') + ->with('value') + ->where('ref', $item->id) + ->where('type/name', 'default_purchase_container') + ->execute(); + } + } + } elseif ($bill->client !== null) { + $bill->client->attributes[] = $attr; + + if ($container === null) { + $attr = $item->getAttribute('default_sales_container'); + if ($attr->id === 0) { + /** @var \Modules\Attribute\Models\Attribute $attr */ + $attr = ItemAttributeMapper::get() + ->with('type') + ->with('value') + ->where('ref', $item->id) + ->where('type/name', 'default_sales_container') + ->execute(); + } + } + } + + $container = $container === null && $attr->id !== 0 + ? new NullContainer($attr->value->valueInt ?? 0) + : $container; + + $taxCombination = $this->app->moduleManager->get('Billing', 'ApiTax') + ->getTaxForPerson($item, $bill->client, $bill->supplier, $request->header->l11n->country); + + $element = BillElement::fromItem( $item, - $taxCode, - $request->getDataInt('quantity') ?? 1, - $bill->id + $taxCombination, + $bill, + FloatInt::toInt($request->getDataString('quantity') ?? '1'), + $container ); + + $element->itemSegment = empty($temp = $item->getAttribute('segment')->value->id) ? null : $temp; + $element->itemSection = empty($temp = $item->getAttribute('section')->value->id) ? null : $temp; + $element->itemSalesGroup = empty($temp = $item->getAttribute('sales_group')->value->id) ? null : $temp; + $element->itemProductGroup = empty($temp = $item->getAttribute('product_group')->value->id) ? null : $temp; + $element->itemType = empty($temp = $item->getAttribute('product_type')->value->id) ? null : $temp; + + $internalRequest = new HttpRequest($request->uri); + $internalRequest->header->account = $request->header->account; + + $price = $this->app->moduleManager->get('Billing', 'ApiPrice')->findBestPrice($internalRequest, $item, $bill->client, $bill->supplier); + + $element->singleListPriceNet->value = $price['bestPrice']->value === 0 + ? $item->salesPrice->value + : $price['bestPrice']->value; + + $element->singleSalesPriceNet->value = $price['bestActualPrice']->value === 0 + ? $item->salesPrice->value + : $price['bestActualPrice']->value; + + $element->totalDiscountP = new FloatInt($request->getDataString('discount_amount') ?? $price['discountAmount']->value); + $element->singleDiscountR = new FloatInt($request->getDataString('discount_percentage') ?? $price['discountPercent']->value); + $element->discountQ = new FloatInt($request->getDataString('bonus') ?? $price['bonus']->value); + + $element->recalculatePrices(); + + return $element; } /** @@ -350,7 +652,14 @@ final class ApiBillController extends Controller $account = ClientMapper::get() ->with('account') ->with('mainAddress') + ->with('attributes') + ->with('attributes/type') + ->with('attributes/value') ->where('id', (int) $request->getData('client')) + ->where('attributes/type/name', [ + 'segment', 'section', 'client_group', 'client_type', + 'sales_tax_code', + ], 'IN') ->execute(); } elseif (($request->getDataInt('supplier') ?? -1) === 0) { /** @var \Modules\SupplierManagement\Models\Supplier $account */ @@ -360,7 +669,13 @@ final class ApiBillController extends Controller $account = SupplierMapper::get() ->with('account') ->with('mainAddress') + ->with('attributes') + ->with('attributes/type') + ->with('attributes/value') ->where('id', (int) $request->getData('supplier')) + ->where('attributes/type/name', [ + 'purchase_tax_code', + ], 'IN') ->execute(); } @@ -419,7 +734,7 @@ final class ApiBillController extends Controller $uploaded = []; if (!empty($uploadedFiles = $request->files)) { - $uploaded = $this->app->moduleManager->get('Media')->uploadFiles( + $uploaded = $this->app->moduleManager->get('Media', 'Api')->uploadFiles( names: [], fileNames: [], files: $uploadedFiles, @@ -482,20 +797,20 @@ final class ApiBillController extends Controller } } - if (!empty($mediaFiles = $request->getDataJson('media'))) { - foreach ($mediaFiles as $media) { - $this->createModelRelation( - $request->header->account, - $bill->id, - (int) $media, - BillMapper::class, - 'files', - '', - $request->getOrigin() - ); - } + $mediaFiles = $request->getDataJson('media'); + foreach ($mediaFiles as $media) { + $this->createModelRelation( + $request->header->account, + $bill->id, + (int) $media, + BillMapper::class, + 'files', + '', + $request->getOrigin() + ); } + // @todo media should be an array of NullMedia elements $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Media', 'Media added to bill.', [ 'upload' => $uploaded, 'media' => $mediaFiles, @@ -517,7 +832,6 @@ final class ApiBillController extends Controller */ public function apiMediaRemoveFromBill(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void { - // @todo: check that it is not system generated media! if (!empty($val = $this->validateMediaRemoveFromBill($request))) { $response->header->status = RequestStatusCode::R_400; $this->createInvalidDeleteResponse($request, $response, $val); @@ -531,20 +845,35 @@ final class ApiBillController extends Controller /** @var \Modules\Billing\Models\Bill $bill */ $bill = BillMapper::get()->where('id', (int) $request->getData('bill'))->execute(); - $path = $this->createBillDir($bill); + // Cannot delete system generated bill + if (\stripos($media->name, $bill->number) !== false) { + $response->header->status = RequestStatusCode::R_423; + $this->createInvalidDeleteResponse($request, $response, $media); - /** @var \Modules\Media\Models\Collection[] */ - $billCollection = CollectionMapper::getAll() - ->where('virtual', $path) - ->execute(); - - if (\count($billCollection) !== 1) { - // @todo: For some reason there are multiple collections with the same virtual path? - // @todo: check if this is the correct way to handle it or if we need to make sure that it is a collection return; } - $collection = \reset($billCollection); + $path = \dirname($this->createBillDir($bill)); + + /** @var \Modules\Media\Models\Collection $collection */ + $collection = CollectionMapper::get() + ->where('name', (string) $bill->id) + ->where('virtual', $path) + ->where('class', MediaClass::COLLECTION) + ->limit(1) + ->execute(); + + if ($collection->id !== 0) { + $this->deleteModelRelation( + $request->header->account, + $collection->id, + $media->id, + CollectionMapper::class, + 'sources', + '', + $request->getOrigin() + ); + } $this->deleteModelRelation( $request->header->account, @@ -556,23 +885,9 @@ final class ApiBillController extends Controller $request->getOrigin() ); - $this->deleteModelRelation( - $request->header->account, - $collection->id, - $media->id, - CollectionMapper::class, - 'sources', - '', - $request->getOrigin() - ); - + // Check if media referenced by other media except the parent collection $referenceCount = MediaMapper::countInternalReferences($media->id); - - if ($referenceCount === 0) { - // Is not used anywhere else -> remove from db and file system - - // @todo: remove media types from media - + if ($referenceCount === 1) { $this->deleteModel($request->header->account, $media, MediaMapper::class, 'bill_media', $request->getOrigin()); if (\is_dir($media->getAbsolutePath())) { @@ -669,25 +984,21 @@ final class ApiBillController extends Controller /** @var \Modules\Billing\Models\Bill $old */ $old = BillMapper::get() ->with('client') - ->with('client/attributes') - ->with('client/attributes/type') - ->with('client/attributes/value') + ->with('supplier') ->where('id', $request->getDataInt('bill') ?? 0) ->execute(); $element = $this->createBillElementFromRequest($request, $response, $old, $data); $this->createModel($request->header->account, $element, BillElementMapper::class, 'bill_element', $request->getOrigin()); - // @todo: handle stock transaction here - // @todo: if transaction fails don't update below and send warning to user - // @todo: however mark transaction as reserved and only update when bill is finalized!!! - - // @todo: in BillElementUpdate do the same + // @todo Handle stock transaction here + // If the transaction fails don't perform the update below + // The same goes for BillElementUpdate $new = clone $old; $new->addElement($element); - $this->updateModel($request->header->account, $old, $new, BillMapper::class, 'bill_element', $request->getOrigin()); + $this->updateModel($request->header->account, $old, $new, BillMapper::class, 'bill', $request->getOrigin()); $this->createStandardCreateResponse($request, $response, $element); } @@ -705,6 +1016,8 @@ final class ApiBillController extends Controller */ private function createBillElementFromRequest(RequestAbstract $request, ResponseAbstract $response, Bill $bill, $data = null) : BillElement { + // @todo handle text element + /** @var \Modules\ItemManagement\Models\Item $item */ $item = ItemMapper::get() ->with('attributes') @@ -713,7 +1026,12 @@ final class ApiBillController extends Controller ->with('l11n') ->with('l11n/type') ->where('id', $request->getDataInt('item') ?? 0) - ->where('l11n/type/title', ['name1', 'name2', 'name3'], 'IN') + ->where('attributes/type/name', [ + 'segment', 'section', 'sales_group', 'product_group', 'product_type', + 'sales_tax_code', 'purchase_tax_code', 'costcenter', 'costobject', + 'default_purchase_container', 'default_sales_container', + ], 'IN') + ->where('l11n/type/title', ['name1', 'name2'], 'IN') ->where('l11n/language', $bill->language) ->execute(); @@ -721,16 +1039,9 @@ final class ApiBillController extends Controller return new NullBillElement(); } - $element = $this->createBaseBillElement($bill->client, $item, $bill, $request); + $element = $this->createBaseBillElement($item, $bill, $request); $element->bill = new NullBill($bill->id); - // discounts - // @todo: implement a addDiscount function - /* - if ($request->getData('discount_percentage') !== null) { - } - */ - return $element; } @@ -768,7 +1079,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]); } @@ -787,49 +1118,53 @@ final class ApiBillController extends Controller */ public function apiPreviewRender(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void { - /** @var \Modules\Billing\Models\Bill $bill */ - $bill = BillMapper::get() - ->with('type') - ->with('type/l11n') - ->with('elements') - ->where('id', $request->getDataInt('bill') ?? 0) - ->execute(); + 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/'); - $templateId = $request->getData('bill_template', 'int'); - if ($templateId === null) { - $billTypeId = $request->getData('bill_type', 'int'); - - if (empty($billTypeId)) { - $billTypeId = $bill->type->id; - } - - if (empty($billTypeId)) { - return; - } - - /** @var \Modules\Billing\Models\BillType $billType */ - $billType = BillTypeMapper::get() - ->with('defaultTemplate') - ->where('id', $billTypeId) - ->execute(); - - $templateId = $billType->defaultTemplate?->id; - } - - /** @var \Modules\Media\Models\Collection $template */ - $template = CollectionMapper::get() - ->with('sources') - ->where('id', $templateId) + /** @var \Modules\Billing\Models\Bill $bill */ + $bill = BillMapper::get() + ->with('elements') + ->with('elements/container') + ->where('id', $request->getDataInt('bill') ?? 0) ->execute(); - require_once __DIR__ . '/../../../Resources/tcpdf/TCPDF.php'; + // Load bill type + $billTypeId = $request->getDataInt('bill_type') ?? $bill->type->id; + if (empty($billTypeId)) { + return; + } - $response->header->set('Content-Type', MimeType::M_PDF, true); + /** @var \Modules\Billing\Models\BillType $billType */ + $billType = BillTypeMapper::get() + ->with('l11n') + ->where('id', $billTypeId) + ->where('l11n/language', $bill->language) + ->execute(); - $view = new View($this->app->l11nManager, $request, $response); - $view->setTemplate('/' . \substr($template->getSourceByName('bill.pdf.php')->getPath(), 0, -8), 'pdf.php'); + $templateId = $request->getDataInt('bill_template') ?? $billType->defaultTemplate?->id ?? 0; + + // Overriding actual bill type + $bill->type = $billType; /** @var \Model\Setting[] $settings */ $settings = $this->app->appSettings->get(null, @@ -853,17 +1188,36 @@ final class ApiBillController extends Controller ); } - /** @var \Modules\Media\Models\Collection $defaultTemplates */ - $defaultTemplates = CollectionMapper::get() + $template = new NullCollection(); + $defaultTemplates = new NullCollection(); + $defaultAssets = new NullCollection(); + + /** @var \Modules\Media\Models\Collection[] $collections */ + $collections = CollectionMapper::get() ->with('sources') - ->where('id', (int) $settings[AdminSettingsEnum::DEFAULT_TEMPLATES]->content) + ->where('id', [ + (int) $settings[AdminSettingsEnum::DEFAULT_TEMPLATES]->content, + (int) $settings[AdminSettingsEnum::DEFAULT_ASSETS]->content, + $templateId, + ], 'IN') ->execute(); - /** @var \Modules\Media\Models\Collection $defaultAssets */ - $defaultAssets = CollectionMapper::get() - ->with('sources') - ->where('id', (int) $settings[AdminSettingsEnum::DEFAULT_ASSETS]->content) - ->execute(); + foreach ($collections as $collection) { + if ($collection->id === $templateId) { + $template = $collection; + } elseif ($collection->id === (int) $settings[AdminSettingsEnum::DEFAULT_TEMPLATES]->content) { + $defaultTemplates = $collection; + } elseif ($collection->id === (int) $settings[AdminSettingsEnum::DEFAULT_ASSETS]->content) { + $defaultAssets = $collection; + } + } + + require_once __DIR__ . '/../../../Resources/tcpdf/TCPDF.php'; + + $response->header->set('Content-Type', MimeType::M_PDF, true); + + $view = new View($this->app->l11nManager, $request, $response); + $view->setTemplate('/' . \substr($template->getSourceByName('bill.pdf.php')->getPath(), 0, -8), 'pdf.php'); $view->data['defaultTemplates'] = $defaultTemplates; $view->data['defaultAssets'] = $defaultAssets; @@ -890,7 +1244,7 @@ final class ApiBillController extends Controller $view->data['bill_taxes'] = $request->getDataString('bill_taxes'); $view->data['bill_currency'] = $request->getDataString('bill_currency'); - // Unit specifc settings + // Unit specific settings $view->data['bill_logo_name'] = $request->getDataString('bill_logo_name'); $view->data['bill_slogan'] = $request->getDataString('bill_slogan'); $view->data['legal_company_name'] = $request->getDataString('legal_company_name'); @@ -930,46 +1284,49 @@ 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() - ->where('id', $request->getDataInt('bill') ?? 0) - ->execute(); - - // @todo: This is stupid to do twice but I need to get the langauge. - // For the future it should just be a join on the bill langauge!!! - // The problem is the where here is a model where and not a query - // builder where meaning it is always considered a value and not a column. - - /** @var \Modules\Billing\Models\Bill $bill */ - $bill = BillMapper::get() + ->with('files') + ->with('files/types') + ->with('elements') + ->with('elements/container') ->with('type') ->with('type/l11n') - ->with('type/defaultTemplate') - ->with('elements') ->where('id', $request->getDataInt('bill') ?? 0) - ->where('type/l11n/language', $bill->language) + ->where('type/l11n/language', new ColumnName(BillMapper::getColumnByMember('language') ?? '')) ->execute(); - $templateId = $request->getDataInt('bill_template'); - if ($templateId === null) { - $templateId = $bill->type->defaultTemplate?->id; - } + // Handle PDF generation + $templateId = $request->getDataInt('bill_template') ?? $bill->type->defaultTemplate?->id ?? 0; - /** @var \Modules\Media\Models\Collection $template */ - $template = CollectionMapper::get() - ->with('sources') - ->where('id', $templateId) - ->execute(); - - require_once __DIR__ . '/../../../Resources/tcpdf/TCPDF.php'; - - $view = new View($this->app->l11nManager, $request, $response); - $view->setTemplate('/' . \substr($template->getSourceByName('bill.pdf.php')->getPath(), 0, -8), 'pdf.php'); - - /** @var \Model\Setting[] $settings */ - $settings = $this->app->appSettings->get(null, + // @todo It would be nice if we could somehow make the two settings calls below in one go. + // Maybe always make with unit if defined AND with null (maybe also with app?) + // Then return none-empty strictest match + /** @var \Model\Setting[] $settings */ + $settings = $this->app->appSettings->get(null, [ AdminSettingsEnum::DEFAULT_TEMPLATES, AdminSettingsEnum::DEFAULT_ASSETS, @@ -990,23 +1347,40 @@ final class ApiBillController extends Controller ); } - /** @var \Modules\Media\Models\Collection $defaultTemplates */ - $defaultTemplates = CollectionMapper::get() + $template = new NullCollection(); + $defaultTemplates = new NullCollection(); + $defaultAssets = new NullCollection(); + + /** @var \Modules\Media\Models\Collection[] $collections */ + $collections = CollectionMapper::get() ->with('sources') - ->where('id', (int) $settings[AdminSettingsEnum::DEFAULT_TEMPLATES]->content) + ->where('id', [ + (int) $settings[AdminSettingsEnum::DEFAULT_TEMPLATES]->content, + (int) $settings[AdminSettingsEnum::DEFAULT_ASSETS]->content, + $templateId, + ], 'IN') ->execute(); - /** @var \Modules\Media\Models\Collection $defaultAssets */ - $defaultAssets = CollectionMapper::get() - ->with('sources') - ->where('id', (int) $settings[AdminSettingsEnum::DEFAULT_ASSETS]->content) - ->execute(); + foreach ($collections as $collection) { + if ($collection->id === $templateId) { + $template = $collection; + } elseif ($collection->id === (int) $settings[AdminSettingsEnum::DEFAULT_TEMPLATES]->content) { + $defaultTemplates = $collection; + } elseif ($collection->id === (int) $settings[AdminSettingsEnum::DEFAULT_ASSETS]->content) { + $defaultAssets = $collection; + } + } + + require_once __DIR__ . '/../../../Resources/tcpdf/TCPDF.php'; + + $view = new View($this->app->l11nManager, $request, $response); + $view->setTemplate('/' . \substr($template->getSourceByName('bill.pdf.php')->getPath(), 0, -8), 'pdf.php'); $view->data['defaultTemplates'] = $defaultTemplates; $view->data['defaultAssets'] = $defaultAssets; $view->data['bill'] = $bill; - // @todo: add bill data such as company name bank information, ..., etc. + // @todo add bill data such as company name bank information, ..., etc. $pdf = $view->render(); @@ -1019,83 +1393,100 @@ final class ApiBillController extends Controller $response->set($request->uri->__toString(), new FormValidation(['status' => $status])); $response->header->status = RequestStatusCode::R_400; + \phpOMS\Log\FileLogger::getInstance()->error( + \phpOMS\Log\FileLogger::MSG_FULL, [ + 'message' => 'Couldn\'t create bill path: ' . $bill->id, + 'line' => __LINE__, + 'file' => self::class, + ] + ); + return; // @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); if (!\is_file($pdfDir . '/' . $billFileName)) { $response->header->status = RequestStatusCode::R_400; + $response->set($request->uri->__toString(), []); + + \phpOMS\Log\FileLogger::getInstance()->error( + \phpOMS\Log\FileLogger::MSG_FULL, [ + 'message' => 'Couldn\'t render bill pdf: ' . $bill->id, + 'line' => __LINE__, + 'file' => self::class, + ] + ); return; } - $media = $this->app->moduleManager->get('Media', 'Api')->createDbEntry( - status: [ - 'status' => UploadStatus::OK, - 'name' => $billFileName, - 'path' => $pdfDir, - 'filename' => $billFileName, - 'size' => \filesize($pdfDir . '/' . $billFileName), - 'extension' => 'pdf', - ], - account: $request->header->account, - virtualPath: $path, - ip: $request->getOrigin(), - app: $this->app, - readContent: true, - unit: $this->app->unitId - ); + $media = null; + if ($oldFile->id === 0) { + // Creating new bill archive pdf + $media = $this->app->moduleManager->get('Media', 'Api')->createDbEntry( + status: [ + 'status' => UploadStatus::OK, + 'name' => $billFileName, + 'path' => $pdfDir, + 'filename' => $billFileName, + 'size' => \filesize($pdfDir . '/' . $billFileName), + 'extension' => 'pdf', + ], + account: $request->header->account, + virtualPath: $path, + ip: $request->getOrigin(), + app: $this->app, + readContent: true, + unit: $this->app->unitId + ); - // Send bill via email - // @todo: maybe not all bill types, and bill status (e.g. deleted should not be sent) - $client = ClientMapper::get() - ->with('account') - ->with('attributes') - ->with('attributes/type') - ->with('attributes/value') - ->where('id', $bill->client?->id ?? 0) - ->where('attributes/type/name', ['bill_emails', 'bill_email_address'], 'IN') - ->execute(); + // Add type to media + $this->createModelRelation( + $request->header->account, + $media->id, + (int) $internalType->content, + MediaMapper::class, + 'types', + '', + $request->getOrigin() + ); - if ($client->getAttribute('bill_emails')->value->getValue() === 1) { - $email = empty($tmp = $client->getAttribute('bill_email_address')->value->getValue()) - ? (string) $tmp - : $client->account->getEmail(); + // Add media to bill + $this->createModelRelation( + $request->header->account, + $bill->id, + $media->id, + BillMapper::class, + 'files', + '', + $request->getOrigin() + ); + } else { + // Updating existing bill archive pdf + $media = clone $oldFile; + if (\realpath($pdfDir . '/' . $billFileName) !== \realpath($oldFile->getAbsolutePath())) { + \unlink($oldFile->getAbsolutePath()); + } - $this->sendBillEmail($media, $email, $response->header->l11n->language); + $media->setPath(\Modules\Media\Controller\ApiController::normalizeDbPath($pdfDir . '/' . $billFileName)); + $media->setVirtualPath($path); + $media->size = (int) \filesize($media->getAbsolutePath()); + + $this->updateModel($request->header->account, $oldFile, $media, MediaMapper::class, 'media', $request->getOrigin()); } - // Add type to media - /** @var \Model\Setting $originalType */ - $originalType = $this->app->appSettings->get( - names: SettingsEnum::ORIGINAL_MEDIA_TYPE, - module: self::NAME - ); - - $this->createModelRelation( - $request->header->account, - $media->id, - (int) $originalType->content, - MediaMapper::class, - 'types', - '', - $request->getOrigin() - ); - - // Add media to bill - $this->createModelRelation( - $request->header->account, - $bill->id, - $media->id, - BillMapper::class, - 'files', - '', - $request->getOrigin() - ); - $this->createStandardCreateResponse($request, $response, $media); } @@ -1104,42 +1495,47 @@ final class ApiBillController extends Controller * * @param Media $media Media to send * @param string $email Email address + * @param int $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' - ); - - /** @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); + } - $this->app->moduleManager->get('Billing', 'Api')->sendMail($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, + ] + ); + } } /** @@ -1176,7 +1572,7 @@ final class ApiBillController extends Controller /** @var \Modules\Editor\Models\EditorDoc $model */ $model = $response->getDataArray($request->uri->__toString())['response']; - $this->createModelRelation($request->header->account, $request->getDataInt('id'), $model->id, BillMapper::class, 'bill_note', '', $request->getOrigin()); + $this->createModelRelation($request->header->account, $request->getDataInt('id'), $model->id, BillMapper::class, 'notes', '', $request->getOrigin()); } /** @@ -1223,8 +1619,8 @@ final class ApiBillController extends Controller /** @var \Modules\Billing\Models\Bill $old */ $old = BillMapper::get()->where('id', (int) $request->getData('id'))->execute(); - // @todo: check if bill can be deleted - // @todo: adjust stock transfer + // @todo check if bill can be deleted + // @todo adjust stock transfer $new = $this->deleteBillFromRequest($request, clone $old); $this->updateModel($request->header->account, $old, $new, BillMapper::class, 'bill', $request->getOrigin()); @@ -1255,8 +1651,6 @@ final class ApiBillController extends Controller * * @return array * - * @todo: implement - * * @since 1.0.0 */ private function validateBillDelete(RequestAbstract $request) : array @@ -1292,10 +1686,20 @@ final class ApiBillController extends Controller } /** @var BillElement $old */ - $old = BillElementMapper::get()->where('id', (int) $request->getData('id'))->execute(); + $old = BillElementMapper::get() + ->with('bill') + ->where('id', (int) $request->getData('id')) + ->execute(); - // @todo: can be edited? - // @todo: adjust transfer protocolls + if ($old->bill->status === BillStatus::ARCHIVED) { + $response->header->status = RequestStatusCode::R_423; + $this->createInvalidUpdateResponse($request, $response, $old); + + return; + } + + // @todo can be edited? + // @todo adjust transfer protocols $new = $this->updateBillElementFromRequest($request, clone $old); @@ -1311,7 +1715,7 @@ final class ApiBillController extends Controller * * @return BillElement * - * @todo: implement + * @todo Implement API update function * * @since 1.0.0 */ @@ -1327,7 +1731,7 @@ final class ApiBillController extends Controller * * @return array * - * @todo: implement + * @todo Implement API validation function * * @since 1.0.0 */ @@ -1363,8 +1767,8 @@ final class ApiBillController extends Controller return; } - // @todo: check if can be deleted - // @todo: handle transactions and bill update + // @todo check if can be deleted + // @todo handle transactions and bill update /** @var \Modules\Billing\Models\BillElement $billElement */ $billElement = BillElementMapper::get()->where('id', (int) $request->getData('id'))->execute(); @@ -1406,8 +1810,17 @@ final class ApiBillController extends Controller */ public function apiNoteUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void { - // @todo: check permissions - $this->app->moduleManager->get('Editor', 'Api')->apiEditorDocUpdate($request, $response, $data); + $accountId = $request->header->account; + if (!$this->app->accountManager->get($accountId)->hasPermission( + PermissionType::MODIFY, $this->app->unitId, $this->app->appId, self::NAME, PermissionCategory::BILL_NOTE, $request->getDataInt('id')) + ) { + $this->fillJsonResponse($request, $response, NotificationLevel::HIDDEN, '', '', []); + $response->header->status = RequestStatusCode::R_403; + + return; + } + + $this->app->moduleManager->get('Editor', 'Api')->apiEditorUpdate($request, $response, $data); } /** @@ -1425,7 +1838,16 @@ final class ApiBillController extends Controller */ public function apiNoteDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void { - // @todo: check permissions - $this->app->moduleManager->get('Editor', 'Api')->apiEditorDocDelete($request, $response, $data); + $accountId = $request->header->account; + if (!$this->app->accountManager->get($accountId)->hasPermission( + PermissionType::DELETE, $this->app->unitId, $this->app->appId, self::NAME, PermissionCategory::BILL_NOTE, $request->getDataInt('id')) + ) { + $this->fillJsonResponse($request, $response, NotificationLevel::HIDDEN, '', '', []); + $response->header->status = RequestStatusCode::R_403; + + return; + } + + $this->app->moduleManager->get('Editor', 'Api')->apiEditorDelete($request, $response, $data); } } diff --git a/Controller/ApiBillTypeController.php b/Controller/ApiBillTypeController.php index 0bad017..c754c62 100755 --- a/Controller/ApiBillTypeController.php +++ b/Controller/ApiBillTypeController.php @@ -75,11 +75,17 @@ final class ApiBillTypeController extends Controller private function createBillTypeFromRequest(RequestAbstract $request) : BillType { $billType = new BillType($request->getDataString('name') ?? ''); - $billType->setL11n($request->getDataString('title') ?? '', $request->getDataString('language') ?? ISO639x1Enum::_EN); + $billType->setL11n( + $request->getDataString('title') ?? '', + ISO639x1Enum::tryFromValue($request->getDataString('language')) ?? ISO639x1Enum::_EN + ); $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->transferType = $request->getDataInt('transfer_type') ?? BillTransferType::SALES; + $billType->isAccounting = $request->getDataBool('is_accounting') ?? false; + $billType->transferType = BillTransferType::tryFromValue($request->getDataInt('transfer_type')) ?? BillTransferType::SALES; $billType->defaultTemplate = $request->hasData('template') ? new NullCollection((int) $request->getData('template')) : null; @@ -150,12 +156,10 @@ final class ApiBillTypeController extends Controller */ private function createBillTypeL11nFromRequest(RequestAbstract $request) : BaseStringL11n { - $billTypeL11n = new BaseStringL11n(); - $billTypeL11n->ref = $request->getDataInt('type') ?? 0; - $billTypeL11n->setLanguage( - $request->getDataString('language') ?? $request->header->l11n->language - ); - $billTypeL11n->content = $request->getDataString('title') ?? ''; + $billTypeL11n = new BaseStringL11n(); + $billTypeL11n->ref = $request->getDataInt('type') ?? 0; + $billTypeL11n->language = ISO639x1Enum::tryFromValue($request->getDataString('language')) ?? $request->header->l11n->language; + $billTypeL11n->content = $request->getDataString('title') ?? ''; return $billTypeL11n; } @@ -219,7 +223,7 @@ final class ApiBillTypeController extends Controller * * @return BillType * - * @todo: implement + * @todo Implement API update function * * @since 1.0.0 */ @@ -228,7 +232,8 @@ final class ApiBillTypeController extends Controller $new->numberFormat = $request->getDataString('number_format') ?? $new->numberFormat; $new->transferStock = $request->getDataBool('transfer_stock') ?? $new->transferStock; $new->isTemplate = $request->getDataBool('is_template') ?? $new->isTemplate; - $new->transferType = $request->getDataInt('transfer_type') ?? $new->transferType; + $new->isAccounting = $request->getDataBool('is_accounting') ?? $new->isAccounting; + $new->transferType = BillTransferType::tryFromValue($request->getDataInt('transfer_type')) ?? $new->transferType; $new->defaultTemplate = $request->hasData('template') ? new NullCollection((int) $request->getData('template')) : $new->defaultTemplate; @@ -243,7 +248,7 @@ final class ApiBillTypeController extends Controller * * @return array * - * @todo: implement + * @todo Implement API validation function * * @since 1.0.0 */ @@ -346,11 +351,9 @@ final class ApiBillTypeController extends Controller */ public function updateBillTypeL11nFromRequest(RequestAbstract $request, BaseStringL11n $new) : BaseStringL11n { - $new->ref = $request->getDataInt('type') ?? $new->ref; - $new->setLanguage( - $request->getDataString('language') ?? $new->language - ); - $new->content = $request->getDataString('title') ?? $new->content; + $new->ref = $request->getDataInt('type') ?? $new->ref; + $new->language = ISO639x1Enum::tryFromValue($request->getDataString('language')) ?? $new->language; + $new->content = $request->getDataString('title') ?? $new->content; return $new; } diff --git a/Controller/ApiController.php b/Controller/ApiController.php index 0103c26..6a6ebe7 100755 --- a/Controller/ApiController.php +++ b/Controller/ApiController.php @@ -15,6 +15,14 @@ declare(strict_types=1); namespace Modules\Billing\Controller; +use Modules\Billing\Models\PaymentTermL11nMapper; +use Modules\Billing\Models\PaymentTermMapper; +use Modules\Billing\Models\ShippingTermL11nMapper; +use Modules\Billing\Models\ShippingTermMapper; +use phpOMS\Localization\BaseStringL11n; +use phpOMS\Localization\BaseStringL11nType; +use phpOMS\Localization\ISO639x1Enum; +use phpOMS\Message\Http\RequestStatusCode; use phpOMS\Message\RequestAbstract; use phpOMS\Message\ResponseAbstract; @@ -29,7 +37,7 @@ use phpOMS\Message\ResponseAbstract; final class ApiController extends Controller { /** - * Api method to update a bill + * Api method to create item payment type * * @param RequestAbstract $request Request * @param ResponseAbstract $response Response @@ -41,49 +49,63 @@ final class ApiController extends Controller * * @since 1.0.0 */ - public function apiBillUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + public function apiPaymentTermCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void { - $this->app->moduleManager->get('Billing', 'ApiBill')->apiBillUpdate($request, $response, $data); + if (!empty($val = $this->validatePaymentTermCreate($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidCreateResponse($request, $response, $val); + + return; + } + + $paymentTerm = $this->createPaymentTermFromRequest($request); + $this->createModel($request->header->account, $paymentTerm, PaymentTermMapper::class, 'payment_term', $request->getOrigin()); + $this->createStandardCreateResponse($request, $response, $paymentTerm); } /** - * Api method to create a bill + * Validate payment create request * - * @param RequestAbstract $request Request - * @param ResponseAbstract $response Response - * @param array $data Generic data + * @param RequestAbstract $request Request * - * @return void - * - * @api + * @return array * * @since 1.0.0 */ - public function apiBillCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + private function validatePaymentTermCreate(RequestAbstract $request) : array { - $this->app->moduleManager->get('Billing', 'ApiBill')->apiBillCreate($request, $response, $data); + $val = []; + if (($val['title'] = !$request->hasData('title')) + || ($val['name'] = !$request->hasData('name')) + ) { + return $val; + } + + return []; } /** - * Api method to create a bill + * Method to create payment from request. * - * @param RequestAbstract $request Request - * @param ResponseAbstract $response Response - * @param array $data Generic data + * @param RequestAbstract $request Request * - * @return void - * - * @api + * @return BaseStringL11nType * * @since 1.0.0 */ - public function apiMediaAddToBill(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + private function createPaymentTermFromRequest(RequestAbstract $request) : BaseStringL11nType { - $this->app->moduleManager->get('Billing', 'ApiBill')->apiMediaAddToBill($request, $response, $data); + $paymentTerm = new BaseStringL11nType($request->getDataString('name') ?? ''); + $paymentTerm->setL11n( + $request->getDataString('title') ?? '', + ISO639x1Enum::tryFromValue($request->getDataString('language')) ?? ISO639x1Enum::_EN + ); + + return $paymentTerm; } /** - * Api method to create a bill element + * Api method to create item payment l11n * * @param RequestAbstract $request Request * @param ResponseAbstract $response Response @@ -95,49 +117,62 @@ final class ApiController extends Controller * * @since 1.0.0 */ - public function apiBillElementCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + public function apiPaymentTermL11nCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void { - $this->app->moduleManager->get('Billing', 'ApiBill')->apiBillElementCreate($request, $response, $data); + if (!empty($val = $this->validatePaymentTermL11nCreate($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidCreateResponse($request, $response, $val); + + return; + } + + $paymentL11n = $this->createPaymentTermL11nFromRequest($request); + $this->createModel($request->header->account, $paymentL11n, PaymentTermL11nMapper::class, 'payment_term_l11n', $request->getOrigin()); + $this->createStandardCreateResponse($request, $response, $paymentL11n); } /** - * Api method to create a bill preview + * Validate payment l11n create request * - * @param RequestAbstract $request Request - * @param ResponseAbstract $response Response - * @param array $data Generic data + * @param RequestAbstract $request Request * - * @return void - * - * @api + * @return array * * @since 1.0.0 */ - public function apiPreviewRender(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + private function validatePaymentTermL11nCreate(RequestAbstract $request) : array { - $this->app->moduleManager->get('Billing', 'ApiBill')->apiPreviewRender($request, $response, $data); + $val = []; + if (($val['title'] = !$request->hasData('title')) + || ($val['type'] = !$request->hasData('type')) + ) { + return $val; + } + + return []; } /** - * Api method to create and archive a bill + * Method to create payment l11n from request. * - * @param RequestAbstract $request Request - * @param ResponseAbstract $response Response - * @param array $data Generic data + * @param RequestAbstract $request Request * - * @return void - * - * @api + * @return BaseStringL11n * * @since 1.0.0 */ - public function apiBillPdfArchiveCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + private function createPaymentTermL11nFromRequest(RequestAbstract $request) : BaseStringL11n { - $this->app->moduleManager->get('Billing', 'ApiBill')->apiBillPdfArchiveCreate($request, $response, $data); + $paymentL11n = new BaseStringL11n(); + $paymentL11n->ref = $request->getDataInt('type') ?? 0; + $paymentL11n->language = ISO639x1Enum::tryFromValue($request->getDataString('language')) ?? $request->header->l11n->language; + $paymentL11n->content = $request->getDataString('title') ?? ''; + + return $paymentL11n; } /** - * Api method to create a bill + * Api method to create shipping term type * * @param RequestAbstract $request Request * @param ResponseAbstract $response Response @@ -149,49 +184,63 @@ final class ApiController extends Controller * * @since 1.0.0 */ - public function apiBillPdfCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + public function apiShippingTermCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void { - $this->app->moduleManager->get('Billing', 'ApiBill')->apiBillPdfCreate($request, $response, $data); + if (!empty($val = $this->validateShippingTermCreate($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidCreateResponse($request, $response, $val); + + return; + } + + $shippingTerm = $this->createShippingTermFromRequest($request); + $this->createModel($request->header->account, $shippingTerm, ShippingTermMapper::class, 'shipping_term', $request->getOrigin()); + $this->createStandardCreateResponse($request, $response, $shippingTerm); } /** - * Api method to create bill files + * Validate shipping create request * - * @param RequestAbstract $request Request - * @param ResponseAbstract $response Response - * @param array $data Generic data + * @param RequestAbstract $request Request * - * @return void - * - * @api + * @return array * * @since 1.0.0 */ - public function apiNoteCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + private function validateShippingTermCreate(RequestAbstract $request) : array { - $this->app->moduleManager->get('Billing', 'ApiBill')->apiNoteCreate($request, $response, $data); + $val = []; + if (($val['title'] = !$request->hasData('title')) + || ($val['name'] = !$request->hasData('name')) + ) { + return $val; + } + + return []; } /** - * Api method to create bill files + * Method to create shipping from request. * - * @param RequestAbstract $request Request - * @param ResponseAbstract $response Response - * @param array $data Generic data + * @param RequestAbstract $request Request * - * @return void - * - * @api + * @return BaseStringL11nType * * @since 1.0.0 */ - public function apiSupplierBillUpload(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + private function createShippingTermFromRequest(RequestAbstract $request) : BaseStringL11nType { - $this->app->moduleManager->get('Billing', 'ApiPurchase')->apiSupplierBillUpload($request, $response, $data); + $shippingTerm = new BaseStringL11nType($request->getDataString('name') ?? ''); + $shippingTerm->setL11n( + $request->getDataString('title') ?? '', + ISO639x1Enum::tryFromValue($request->getDataString('language')) ?? ISO639x1Enum::_EN + ); + + return $shippingTerm; } /** - * Api method to create item bill type + * Api method to create shipping term l11n * * @param RequestAbstract $request Request * @param ResponseAbstract $response Response @@ -203,169 +252,57 @@ final class ApiController extends Controller * * @since 1.0.0 */ - public function apiBillTypeCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + public function apiShippingTermL11nCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void { - $this->app->moduleManager->get('Billing', 'ApiBillType')->apiBillTypeCreate($request, $response, $data); + if (!empty($val = $this->validateShippingTermL11nCreate($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidCreateResponse($request, $response, $val); + + return; + } + + $shippingL11n = $this->createShippingTermL11nFromRequest($request); + $this->createModel($request->header->account, $shippingL11n, ShippingTermL11nMapper::class, 'shipping_term_l11n', $request->getOrigin()); + $this->createStandardCreateResponse($request, $response, $shippingL11n); } /** - * Api method to create item attribute l11n + * Validate shipping l11n create request * - * @param RequestAbstract $request Request - * @param ResponseAbstract $response Response - * @param array $data Generic data + * @param RequestAbstract $request Request * - * @return void - * - * @api + * @return array * * @since 1.0.0 */ - public function apiBillTypeL11nCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + private function validateShippingTermL11nCreate(RequestAbstract $request) : array { - $this->app->moduleManager->get('Billing', 'ApiBillType')->apiBillTypeL11nCreate($request, $response, $data); + $val = []; + if (($val['title'] = !$request->hasData('title')) + || ($val['type'] = !$request->hasData('type')) + ) { + return $val; + } + + return []; } /** - * Api method to create item bill type + * Method to create shipping l11n from request. * - * @param RequestAbstract $request Request - * @param ResponseAbstract $response Response - * @param array $data Generic data + * @param RequestAbstract $request Request * - * @return void - * - * @api + * @return BaseStringL11n * * @since 1.0.0 */ - public function apiTaxCombinationCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + private function createShippingTermL11nFromRequest(RequestAbstract $request) : BaseStringL11n { - $this->app->moduleManager->get('Billing', 'ApiTax')->apiTaxCombinationCreate($request, $response, $data); - } + $shippingL11n = new BaseStringL11n(); + $shippingL11n->ref = $request->getDataInt('type') ?? 0; + $shippingL11n->language = ISO639x1Enum::tryFromValue($request->getDataString('language')) ?? $request->header->l11n->language; + $shippingL11n->content = $request->getDataString('title') ?? ''; - /** - * Api method to create item bill type - * - * @param RequestAbstract $request Request - * @param ResponseAbstract $response Response - * @param array $data Generic data - * - * @return void - * - * @api - * - * @since 1.0.0 - */ - public function apiPriceCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void - { - $this->app->moduleManager->get('Billing', 'ApiPrice')->apiPriceCreate($request, $response, $data); - } - - /** - * Api method to create item attribute - * - * @param RequestAbstract $request Request - * @param ResponseAbstract $response Response - * @param array $data Generic data - * - * @return void - * - * @api - * - * @since 1.0.0 - */ - public function apiBillAttributeCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void - { - $this->app->moduleManager->get('Billing', 'ApiAttribute')->apiBillAttributeCreate($request, $response, $data); - } - - /** - * Api method to create bill attribute l11n - * - * @param RequestAbstract $request Request - * @param ResponseAbstract $response Response - * @param array $data Generic data - * - * @return void - * - * @api - * - * @since 1.0.0 - */ - public function apiBillAttributeTypeL11nCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void - { - $this->app->moduleManager->get('Billing', 'ApiAttribute')->apiBillAttributeTypeL11nCreate($request, $response, $data); - } - - /** - * Api method to create bill attribute type - * - * @param RequestAbstract $request Request - * @param ResponseAbstract $response Response - * @param array $data Generic data - * - * @return void - * - * @api - * - * @since 1.0.0 - */ - public function apiBillAttributeTypeCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void - { - $this->app->moduleManager->get('Billing', 'ApiAttribute')->apiBillAttributeTypeCreate($request, $response, $data); - } - - /** - * Api method to create bill attribute value - * - * @param RequestAbstract $request Request - * @param ResponseAbstract $response Response - * @param array $data Generic data - * - * @return void - * - * @api - * - * @since 1.0.0 - */ - public function apiBillAttributeValueCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void - { - $this->app->moduleManager->get('Billing', 'ApiAttribute')->apiBillAttributeValueCreate($request, $response, $data); - } - - /** - * Api method to create bill attribute l11n - * - * @param RequestAbstract $request Request - * @param ResponseAbstract $response Response - * @param array $data Generic data - * - * @return void - * - * @api - * - * @since 1.0.0 - */ - public function apiBillAttributeValueL11nCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void - { - $this->app->moduleManager->get('Billing', 'ApiAttribute')->apiBillAttributeValueL11nCreate($request, $response, $data); - } - - /** - * Api method to find subscriptions - * - * @param RequestAbstract $request Request - * @param ResponseAbstract $response Response - * @param array $data Generic data - * - * @return void - * - * @api - * - * @since 1.0.0 - */ - public function apiSubscriptionFind(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void - { + return $shippingL11n; } } diff --git a/Controller/ApiPriceController.php b/Controller/ApiPriceController.php index 4ee4b38..eb42653 100755 --- a/Controller/ApiPriceController.php +++ b/Controller/ApiPriceController.php @@ -16,22 +16,25 @@ declare(strict_types=1); namespace Modules\Billing\Controller; 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\Billing\Models\Tax\TaxCombinationMapper; +use Modules\ClientManagement\Models\Client; use Modules\ClientManagement\Models\ClientMapper; use Modules\ClientManagement\Models\NullClient; +use Modules\ItemManagement\Models\Item; 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; use phpOMS\Message\ResponseAbstract; use phpOMS\Stdlib\Base\FloatInt; -use phpOMS\System\MimeType; /** * Billing class. @@ -44,98 +47,177 @@ use phpOMS\System\MimeType; final class ApiPriceController extends Controller { /** - * Api method to find items + * Find the best price for a client/supplier and an item * - * @param RequestAbstract $request Request - * @param ResponseAbstract $response Response - * @param array $data Generic data + * @param RequestAbstract $request Request + * @param null|Item $item Item to find the price for (alternatively, price_item in request) + * @param null|Client $client Client to find the price for (alternatively, client in request) + * @param null|Supplier $supplier Supplier to find the price for (alternatively, supplier in request) * - * @return void - * - * @api + * @return array * * @since 1.0.0 */ - public function apiPricingFind(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + public function findBestPrice( + RequestAbstract $request, + ?Item $item = null, + ?Client $client = null, ?Supplier $supplier = null + ) : array { + $item ??= new NullItem(); + $client ??= new NullClient(); + $supplier ??= new NullSupplier(); + // Get item - /** @var null|\Modules\ItemManagement\Models\Item $item */ - $item = null; - if ($request->hasData('price_item')) { - /** @var null|\Modules\ItemManagement\Models\Item $item */ + if ($item->id === 0 && $request->hasData('price_item')) { + /** @var \Modules\ItemManagement\Models\Item $item */ $item = ItemMapper::get() ->with('attributes') ->with('attributes/type') ->with('attributes/value') ->where('id', (int) $request->getData('price_item')) + ->where('attributes/type/name', ['segment', 'section', 'sales_group', 'product_group', 'product_type'], 'IN') ->execute(); } - // Get account - /** @var null|\Modules\ClientManagement\Models\Client|\Modules\SupplierManagement\Models\Supplier $account */ - $account = null; - - /** @var null|\Modules\ClientManagement\Models\Client $client */ - $client = null; - - /** @var null|\Modules\SupplierManagement\Models\Supplier $supplier */ - $supplier = null; - - if ($request->hasData('price_client')) { + // Get 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') - ->where('id', (int) $request->getData('price_client')) + ->where('id', (int) $request->getData('client')) + ->where('attributes/type/name', ['segment', 'section', 'client_group', 'client_type'], 'IN') ->execute(); - - /** @var \Modules\ClientManagement\Models\Client */ - $account = $client; - } else { - /** @var \Modules\SupplierManagement\Models\Supplier $supplier */ - $supplier = SupplierMapper::get() - ->with('attributes') - ->with('attributes/type') - ->with('attributes/value') - ->where('id', (int) $request->getData('price_supplier')) - ->execute(); - - /** @var \Modules\SupplierManagement\Models\Supplier $account */ - $account = $supplier; } + // Get supplier + if ($supplier->id === 0 && $request->hasData('supplier')) { + $supplier = SupplierMapper::get() + ->where('id', $request->getDataInt('supplier')) + ->execute(); + } + + $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('price_name')); + $queryMapper->where('name', $request->getData('name')); } - $queryMapper->where('promocode', \array_unique([$request->getData('price_promocode'), null]), 'IN'); + $queryMapper->where('promocode', \array_unique([$request->getDataString('promocode') ?? '', '']), 'IN'); - $queryMapper->where('item', \array_unique([$request->getData('price_item', 'int'), null]), 'IN'); - $queryMapper->where('itemgroup', \array_unique([$request->getData('price_itemgroup', 'int'), $item?->getAttribute('itemgroup')->id, null]), 'IN'); - $queryMapper->where('itemsegment', \array_unique([$request->getData('price_itemsegment', 'int'), $item?->getAttribute('itemsegment')->id, null]), 'IN'); - $queryMapper->where('itemsection', \array_unique([$request->getData('price_itemsection', 'int'), $item?->getAttribute('itemsection')->id, null]), 'IN'); - $queryMapper->where('itemtype', \array_unique([$request->getData('price_itemtype', 'int'), $item?->getAttribute('itemtype')->id, 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->getData('price_client', 'int'), null]), 'IN'); - $queryMapper->where('clientgroup', \array_unique([$request->getData('price_clientgroup', 'int'), $client?->getAttribute('clientgroup')->id, null]), 'IN'); - $queryMapper->where('clientsegment', \array_unique([$request->getData('price_clientsegment', 'int'), $client?->getAttribute('clientsegment')->id, null]), 'IN'); - $queryMapper->where('clientsection', \array_unique([$request->getData('price_clientsection', 'int'), $client?->getAttribute('clientsection')->id, null]), 'IN'); - $queryMapper->where('clienttype', \array_unique([$request->getData('price_clienttype', 'int'), $client?->getAttribute('clienttype')->id, null]), 'IN'); - $queryMapper->where('clientcountry', \array_unique([$request->getData('price_clientcountry'), $client?->mainAddress->getCountry(), 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([$request->getData('price_supplier', 'int'), null]), 'IN'); - $queryMapper->where('unit', \array_unique([$request->getData('price_unit', 'int'), null]), 'IN'); - $queryMapper->where('type', $request->getData('price_type', 'int') ?? PriceType::SALES); - $queryMapper->where('currency', \array_unique([$request->getData('price_currency', 'int'), 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'); - // @todo: implement start and end + // 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 /* - @todo: implement quantity + @todo implement quantity if ($request->hasData('price_quantity')) { $whereQuery = new Where(); $whereQuery->where('quantity', (int) $request->getData('price_quantity'), '<=') @@ -148,12 +230,14 @@ final class ApiPriceController extends Controller /** @var \Modules\Billing\Models\Price\Price[] $prices */ $prices = $queryMapper->execute(); - // Find base price (@todo: probably not a good solution) - $bestBasePrice = null; + // Find base price + $basePrice = null; foreach ($prices as $price) { - if ($price->price->value !== 0 && $price->priceNew === 0 + if (/*$price->priceNew->value > 0 */ // Price could be 0 + $price->id !== 0 && $price->item->id !== 0 - && $price->itemgroup->id === 0 + && $price->itemsalesgroup->id === 0 + && $price->itemproductgroup->id === 0 && $price->itemsegment->id === 0 && $price->itemsection->id === 0 && $price->itemtype->id === 0 @@ -163,35 +247,51 @@ final class ApiPriceController extends Controller && $price->clientsection->id === 0 && $price->clienttype->id === 0 && $price->promocode === '' - && $price->price->value < ($bestBasePrice?->price->value ?? \PHP_INT_MAX) + && $price->priceNew->value < ($basePrice?->priceNew->value ?? \PHP_INT_MAX) ) { - $bestBasePrice = $price; + $basePrice = $price; } } - // @todo: implement prices which cannot be improved even if there are better prices available (i.e. some customer groups may not get better prices, Dentagen Beispiel) - // alternatively set prices as 'improvable' => which whitelists a price as can be improved or 'alwaysimproces' which always overwrites other prices + $basePrice ??= new NullPrice(); + + // @todo implement prices which cannot be improved even if there are better prices available + // (i.e. some customer groups may not get better prices, Dentagen Beispiel) + // alternatively set prices as 'improvable' => which whitelists a price as can be improved + // or 'always_improves' which always overwrites other prices + // Find best price - $bestPrice = null; + $bestPrice = $basePrice; $bestPriceValue = \PHP_INT_MAX; + $discounts = []; + foreach ($prices as $price) { - $newPrice = $bestBasePrice?->price->value ?? \PHP_INT_MAX; - - if ($price->price->value < $newPrice) { - $newPrice = $price->price->value; + if ($price->isAdditive && $price->priceNew->value === 0) { + $discounts[] = $price; } - if ($price->priceNew < $newPrice) { - $newPrice = $price->priceNew; + $newPrice = $bestPrice->price->value ?? $basePrice->price->value; + + if ($price->priceNew->value > 0 && $price->priceNew->value < $newPrice) { + $newPrice = $price->priceNew->value; } - $newPrice -= $price->discount; - $newPrice = (int) ((10000 / $price->discountPercentage) * $newPrice); - $newPrice = (int) (($price->quantity === 0 ? 10000 : $price->quantity) / (10000 + $price->bonus) * $newPrice); + if ($price->priceNew->value > 0 && $price->priceNew->value < $newPrice) { + $newPrice = $price->priceNew->value; + } - // @todo: the calculation above regarding discount and bonus don't consider the purchased quantity. - // 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. + // Calculate the price EFFECT (this is the theoretical unit price) + // 1. subtract discount value + // 2. subtract discount percentage + // 3. subtract bonus effect + + $newPrice -= $price->discount->value; + $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? if ($newPrice < $bestPriceValue) { $bestPriceValue = $newPrice; @@ -199,23 +299,39 @@ final class ApiPriceController extends Controller } } - // Get tax definition - /** @var \Modules\Billing\Models\Tax\TaxCombination $tax */ - $tax = ($request->getDataInt('price_type') ?? PriceType::SALES) === PriceType::SALES - ? TaxCombinationMapper::get() - ->where('itemCode', $request->getDataInt('price_item')) - ->where('clientCode', $account->getAttribute('client_code')->value->id) - ->execute() - : TaxCombinationMapper::get() - ->where('itemCode', $request->getDataInt('price_item')) - ->where('supplierCode', $account->getAttribute('supplier_code')->value->id) - ->execute(); + if ($bestPrice->priceNew->value === 0) { + $discounts[] = clone $bestPrice; + $bestPrice = $basePrice; + } - $response->header->set('Content-Type', MimeType::M_JSON, true); - $response->set( - $request->uri->__toString(), - \array_values($prices) - ); + // Actual price calculation + $bestActualPriceValue = $bestPrice?->priceNew->value ?? \PHP_INT_MAX; + + $discountAmount = $bestPrice->discount->value; + $discountPercentage = $bestPrice->discountPercentage->value; + $bonus = $bestPrice->bonus->value; + + foreach ($discounts as $discount) { + $bestActualPriceValue -= $discount->discount->value; + + $discountAmount += $discount->discount->value; + $discountPercentage += $discount->discountPercentage->value; + $bonus += $discount->bonus->value; + } + + $bestActualPriceValue -= $discountAmount; + $bestActualPriceValue = (int) \round(((FloatInt::DIVISOR * 100) - $discountPercentage) / (FloatInt::DIVISOR * 100) * $bestActualPriceValue, 0); + + return [ + 'basePrice' => $basePrice->priceNew, + 'bestPrice' => $bestPrice->priceNew, + 'supplier' => $bestPrice->supplier->id, + 'bestActualPrice' => new FloatInt($bestActualPriceValue), + 'discounts' => $discounts, + 'discountPercent' => new FloatInt($discountPercentage), + 'discountAmount' => new FloatInt($discountAmount), + 'bonus' => new FloatInt($bonus), + ]; } /** @@ -260,31 +376,32 @@ final class ApiPriceController extends Controller $price->name = $request->getDataString('name') ?? ''; $price->promocode = $request->getDataString('promocode') ?? ''; - $price->item = new NullItem((int) $request->getData('item')); - $price->itemgroup = new NullAttributeValue((int) $request->getData('itemgroup')); - $price->itemsegment = new NullAttributeValue((int) $request->getData('itemsegment')); - $price->itemsection = new NullAttributeValue((int) $request->getData('itemsection')); - $price->itemtype = new NullAttributeValue((int) $request->getData('itemtype')); + $price->item = new NullItem((int) $request->getData('item')); + $price->itemsegment = new NullAttributeValue((int) $request->getData('itemsegment')); + $price->itemsection = new NullAttributeValue((int) $request->getData('itemsection')); + $price->itemsalesgroup = new NullAttributeValue((int) $request->getData('itemsalesgroup')); + $price->itemproductgroup = new NullAttributeValue((int) $request->getData('itemproductgroup')); + $price->itemtype = new NullAttributeValue((int) $request->getData('itemtype')); $price->client = new NullClient((int) $request->getData('client')); - $price->clientgroup = new NullAttributeValue((int) $request->getData('clientgroup')); $price->clientsegment = new NullAttributeValue((int) $request->getData('clientsegment')); $price->clientsection = new NullAttributeValue((int) $request->getData('clientsection')); + $price->clientgroup = new NullAttributeValue((int) $request->getData('clientgroup')); $price->clienttype = new NullAttributeValue((int) $request->getData('clienttype')); $price->supplier = new NullSupplier((int) $request->getData('supplier')); $price->unit = (int) $request->getData('unit'); - $price->type = $request->getDataInt('type') ?? PriceType::SALES; - $price->quantity = (int) $request->getData('quantity'); - $price->price = new FloatInt((int) $request->getData('price')); - $price->priceNew = (int) $request->getData('price_new'); - $price->discount = (int) $request->getData('discount'); - $price->discountPercentage = (int) $request->getData('discountPercentage'); - $price->bonus = (int) $request->getData('bonus'); + $price->type = PriceType::tryFromValue($request->getDataInt('type')) ?? PriceType::SALES; + $price->quantity = new FloatInt($request->getDataString('quantity') ?? 0); + $price->price = new FloatInt($request->getDataString('price') ?? 0); + $price->priceNew = new FloatInt($request->getDataString('price_new') ?? 0); + $price->discount = new FloatInt($request->getDataString('discount') ?? 0); + $price->discountPercentage = new FloatInt($request->getDataString('discountPercentage') ?? 0); + $price->bonus = new FloatInt($request->getDataString('bonus') ?? 0); $price->multiply = $request->getDataBool('multiply') ?? false; - $price->currency = $request->getDataString('currency') ?? ISO4217CharEnum::_EUR; - $price->start = $request->getDataDateTime('start') ?? null; - $price->end = $request->getDataDateTime('end') ?? null; + $price->currency = ISO4217CharEnum::tryFromValue($request->getDataString('currency')) ?? ISO4217CharEnum::_EUR; + $price->start = $request->getDataDateTime('start'); + $price->end = $request->getDataDateTime('end'); return $price; } @@ -296,7 +413,7 @@ final class ApiPriceController extends Controller * * @return array * - * @todo: consider to prevent name 'base'? + * @todo consider to prevent name 'default'? * Might not be possible because it is used internally as well (see apiItemCreate in ItemManagement) * * @since 1.0.0 @@ -337,6 +454,22 @@ final class ApiPriceController extends Controller $old = PriceMapper::get()->where('id', (int) $request->getData('id'))->execute(); $new = $this->updatePriceFromRequest($request, clone $old); + if ($new->name === 'default' + && $old->priceNew->value !== $new->priceNew->value + ) { + /** @var \Modules\ItemManagement\Models\Item $item */ + $item = ItemMapper::get()->where('id', $new->item)->execute(); + $itemNew = clone $item; + + if ($new->type === PriceType::SALES) { + $itemNew->salesPrice = $new->priceNew; + } else { + $itemNew->purchasePrice = $new->priceNew; + } + + $this->updateModel($request->header->account, $item, $itemNew, ItemMapper::class, 'price', $request->getOrigin()); + } + $this->updateModel($request->header->account, $old, $new, PriceMapper::class, 'price', $request->getOrigin()); $this->createStandardUpdateResponse($request, $response, $new); } @@ -353,17 +486,19 @@ final class ApiPriceController extends Controller */ public function updatePriceFromRequest(RequestAbstract $request, Price $new) : Price { - $new->name = $new->name !== 'base' + $new->name = $new->name !== 'default' ? ($request->getDataString('name') ?? $new->name) : $new->name; + $new->status = PriceStatus::tryFromValue($request->getDataInt('type')) ?? $new->status; + $new->promocode = $request->getDataString('promocode') ?? $new->promocode; - $new->item = $request->hasData('item') ? new NullItem((int) $request->getData('item')) : $new->item; - $new->itemgroup = $request->hasData('itemgroup') ? new NullAttributeValue((int) $request->getData('itemgroup')) : $new->itemgroup; - $new->itemsegment = $request->hasData('itemsegment') ? new NullAttributeValue((int) $request->getData('itemsegment')) : $new->itemsegment; - $new->itemsection = $request->hasData('itemsection') ? new NullAttributeValue((int) $request->getData('itemsection')) : $new->itemsection; - $new->itemtype = $request->hasData('itemtype') ? new NullAttributeValue((int) $request->getData('itemtype')) : $new->itemtype; + $new->itemsalesgroup = $request->hasData('itemsalesgroup') ? new NullAttributeValue((int) $request->getData('itemsalesgroup')) : $new->itemsalesgroup; + $new->itemproductgroup = $request->hasData('itemproductgroup') ? new NullAttributeValue((int) $request->getData('itemproductgroup')) : $new->itemproductgroup; + $new->itemsegment = $request->hasData('itemsegment') ? new NullAttributeValue((int) $request->getData('itemsegment')) : $new->itemsegment; + $new->itemsection = $request->hasData('itemsection') ? new NullAttributeValue((int) $request->getData('itemsection')) : $new->itemsection; + $new->itemtype = $request->hasData('itemtype') ? new NullAttributeValue((int) $request->getData('itemtype')) : $new->itemtype; $new->client = $request->hasData('client') ? new NullClient((int) $request->getData('client')) : $new->client; $new->clientgroup = $request->hasData('clientgroup') ? new NullAttributeValue((int) $request->getData('clientgroup')) : $new->clientgroup; @@ -373,15 +508,16 @@ final class ApiPriceController extends Controller $new->supplier = $request->hasData('supplier') ? new NullSupplier((int) $request->getData('supplier')) : $new->supplier; $new->unit = $request->getDataInt('unit') ?? $new->unit; - $new->type = $request->getDataInt('type') ?? $new->type; - $new->quantity = $request->getDataInt('quantity') ?? $new->quantity; - $new->price = $request->hasData('price') ? new FloatInt((int) $request->getData('price')) : $new->price; - $new->priceNew = $request->getDataInt('price_new') ?? $new->priceNew; - $new->discount = $request->getDataInt('discount') ?? $new->discount; - $new->discountPercentage = $request->getDataInt('discountPercentage') ?? $new->discountPercentage; - $new->bonus = $request->getDataInt('bonus') ?? $new->bonus; + + $new->quantity = new FloatInt($request->getDataString('quantity') ?? $new->quantity->value); + $new->price = new FloatInt($request->getDataString('price') ?? $new->price->value); + $new->priceNew = new FloatInt($request->getDataString('price_new') ?? $new->priceNew->value); + $new->discount = new FloatInt($request->getDataString('discount') ?? $new->discount->value); + $new->discountPercentage = new FloatInt($request->getDataString('discountPercentage') ?? $new->discountPercentage->value); + $new->bonus = new FloatInt($request->getDataString('bonus') ?? $new->bonus->value); + $new->multiply = $request->getDataBool('multiply') ?? $new->multiply; - $new->currency = $request->getDataString('currency') ?? $new->currency; + $new->currency = ISO4217CharEnum::tryFromValue($request->getDataString('currency')) ?? $new->currency; $new->start = $request->getDataDateTime('start') ?? $new->start; $new->end = $request->getDataDateTime('end') ?? $new->end; @@ -395,9 +531,6 @@ final class ApiPriceController extends Controller * * @return array * - * @todo: implement - * @todo: consider to block 'base' name - * * @since 1.0.0 */ private function validatePriceUpdate(RequestAbstract $request) : array @@ -435,7 +568,7 @@ final class ApiPriceController extends Controller /** @var \Modules\Billing\Models\Price\Price $price */ $price = PriceMapper::get()->where('id', (int) $request->getData('id'))->execute(); - if ($price->name === 'base') { + if ($price->name === 'default') { // default price cannot be deleted $this->createInvalidDeleteResponse($request, $response, []); diff --git a/Controller/ApiPurchaseController.php b/Controller/ApiPurchaseController.php index b5e7eea..d15f382 100755 --- a/Controller/ApiPurchaseController.php +++ b/Controller/ApiPurchaseController.php @@ -1,5 +1,4 @@ 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 + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @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() @@ -69,10 +108,12 @@ final class ApiPurchaseController extends Controller ->limit(1) ->execute(); - $files = $request->files; + $bills = []; + + $files = \array_merge($request->files, $request->getDataJson('media')); foreach ($files as $file) { // Create default bill - $billRequest = new HttpRequest(new HttpUri('')); + $billRequest = new HttpRequest(); $billRequest->header->account = $request->header->account; $billRequest->header->l11n = $request->header->l11n; $billRequest->setData('supplier', 0); @@ -82,61 +123,150 @@ final class ApiPurchaseController extends Controller $billResponse = new HttpResponse(); $billResponse->header->l11n = $response->header->l11n; - $this->app->moduleManager->get('Billing', 'Api')->apiBillCreate($billRequest, $billResponse, $data); + $this->app->moduleManager->get('Billing', 'ApiBill')->apiBillCreate($billRequest, $billResponse, $data); - $billId = $billResponse->getDataArray('')['response']->id; + $billId = $billResponse->getDataArray('')['response']->id; + $bills[] = $billId; // Upload and assign document to bill - $mediaRequest = new HttpRequest(); - $mediaRequest->header->account = $request->header->account; - $mediaRequest->header->l11n = $request->header->l11n; - $mediaRequest->addFile($file); + $mediaResponse = new HttpResponse(); + $mediaRequest = new HttpRequest(); - $mediaResponse = new HttpResponse(); $mediaResponse->header->l11n = $response->header->l11n; + $mediaRequest->header->account = $request->header->account; + $mediaRequest->header->l11n = $request->header->l11n; + + if (\is_array($file)) { + $mediaRequest->addFile($file); + } else { + $mediaRequest->setData('media', \json_encode($file)); + } + $mediaRequest->setData('bill', $billId); - $mediaRequest->setData('type', $originalType); + $mediaRequest->setData('type', $internalType); $mediaRequest->setData('parse_content', true, true); - $this->app->moduleManager->get('Billing', 'Api')->apiMediaAddToBill($mediaRequest, $mediaResponse, $data); + $this->app->moduleManager->get('Billing', 'ApiBill')->apiMediaAddToBill($mediaRequest, $mediaResponse, $data); - /** @var \Modules\Media\Models\Media[] $uploaded */ - $uploaded = $mediaResponse->getDataArray('')['response']['upload']; - if (empty($uploaded)) { - throw new \Exception(); + if (\is_array($file)) { + /** @var \Modules\Media\Models\Media[] $uploaded */ + $uploaded = $mediaResponse->getDataArray('')['response']['upload']; + if (empty($uploaded)) { + return []; + } + + $in = \reset($uploaded)->getAbsolutePath(); + if (!\is_file($in)) { + return []; + } } - $in = \reset($uploaded)->getAbsolutePath(); // pdf parsed content is available in $in->content - if (!\is_file($in)) { - throw new \Exception(); - } + $request->setData('id', $billId, true); + $request->setData('bill', $billId, true); - // Create internal document - $billResponse = new HttpResponse(); - $billRequest = new HttpRequest(new HttpUri('')); - - $billRequest->header->account = $request->header->account; - $billRequest->setData('bill', $billId); - - $this->app->moduleManager->get('Billing', 'Api')->apiBillPdfArchiveCreate($billRequest, $billResponse); - - // Offload bill parsing to cli - $cliPath = \realpath(__DIR__ . '/../../../cli.php'); - if ($cliPath === false) { - return; - } - - try { - SystemUtils::runProc( - OperatingSystem::getSystem() === SystemType::WIN ? 'php.exe' : 'php', - \escapeshellarg($cliPath) - . ' /billing/bill/purchase/parse ' - . '-i ' . \escapeshellarg((string) $billId), - true - ); - } catch (\Throwable $t) { - $this->app->logger->error($t->getMessage()); - } + $this->apiInvoiceParse($request, $response, $data); } + + return $bills; + } + + /** + * Validate item attribute create request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateSupplierBillUpload(RequestAbstract $request) : array + { + $val = []; + if (($val['files'] = empty($request->files))) { + return $val; + } + + return []; + } + + /** + * Api method to create bill files + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @throws \Exception + * + * @since 1.0.0 + */ + public function apiInvoiceParse(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateInvoiceParse($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidCreateResponse($request, $response, $val); + + return; + } + + $bill = BillMapper::get() + ->where('id', (int) $request->getData('id')) + ->execute(); + + // After a bill is "closed" its values shouldn't change + if ($bill->status !== BillStatus::DRAFT + && $bill->status !== BillStatus::UNPARSED + && $bill->status !== BillStatus::ACTIVE + ) { + $response->header->status = RequestStatusCode::R_423; + $this->createInvalidCreateResponse($request, $response, $val); + + return; + } + + // Offload bill parsing to cli + $cliPath = \realpath(__DIR__ . '/../../../cli.php'); + if ($cliPath === false) { + return; + } + + try { + SystemUtils::runProc( + OperatingSystem::getSystem() === SystemType::WIN ? 'php.exe' : 'php', + '-dxdebug.remote_enable=1 -dxdebug.start_with_request=yes -dxdebug.mode=coverage,develop,debug ' . + \escapeshellarg($cliPath) + . ' /billing/bill/purchase/parse ' + . '-i ' . \escapeshellarg((string) $bill->id), + $request->getDataBool('async') ?? true + ); + } catch (\Throwable $t) { + $response->header->status = RequestStatusCode::R_400; + $this->app->logger->error($t->getMessage()); + } + + $this->createStandardUpdateResponse($request, $response, $bill); + } + + /** + * Validate item attribute create request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateInvoiceParse(RequestAbstract $request) : array + { + $val = []; + if (($val['id'] = !$request->hasData('id'))) { + return $val; + } + + return []; } } diff --git a/Controller/ApiTaxController.php b/Controller/ApiTaxController.php index cd1b997..cb22186 100755 --- a/Controller/ApiTaxController.php +++ b/Controller/ApiTaxController.php @@ -14,22 +14,25 @@ declare(strict_types=1); namespace Modules\Billing\Controller; -use Modules\Admin\Models\Address; use Modules\Attribute\Models\AttributeValue; use Modules\Attribute\Models\NullAttributeValue; +use Modules\Billing\Models\Tax\NullTaxCombination; use Modules\Billing\Models\Tax\TaxCombination; use Modules\Billing\Models\Tax\TaxCombinationMapper; use Modules\ClientManagement\Models\Attribute\ClientAttributeTypeMapper; use Modules\ClientManagement\Models\Client; -use Modules\Finance\Models\TaxCode; +use Modules\Finance\Models\NullTaxCode; use Modules\Finance\Models\TaxCodeMapper; use Modules\ItemManagement\Models\Item; use Modules\Organization\Models\UnitMapper; -use phpOMS\Localization\ISO3166CharEnum; +use Modules\SupplierManagement\Models\Attribute\SupplierAttributeTypeMapper; +use Modules\SupplierManagement\Models\Supplier; +use phpOMS\Localization\ISO3166TwoEnum; use phpOMS\Message\Http\RequestStatusCode; use phpOMS\Message\RequestAbstract; use phpOMS\Message\ResponseAbstract; use phpOMS\Security\Guard; +use phpOMS\Stdlib\Base\Address; /** * Billing class. @@ -44,32 +47,49 @@ final class ApiTaxController extends Controller /** * Get tax code from client and item. * - * @param Client $client Client to get tax code from - * @param Item $item Item toget tax code from - * @param string $defaultCountry default country to use if no valid tax code could be found and if the unit country code shouldn't be used + * @param Item $item Item to get tax code from + * @param null|Client $client Client to get tax code from + * @param null|Supplier $supplier Supplier to get tax code from + * @param string $defaultCountry Default country to use if no valid tax code could be found + * and if the unit country code shouldn't be used * - * @return TaxCode + * @return TaxCombination * * @since 1.0.0 */ - public function getTaxCodeFromClientItem(Client $client, Item $item, string $defaultCountry = '') : TaxCode + public function getTaxForPerson( + Item $item, + ?Client $client = null, ?Supplier $supplier = null, + string $defaultCountry = '' + ) : TaxCombination { - // @todo: define default sales tax code if none available?! - // @todo: consider to actually use a ownsOne reference instead of only a string, this way the next line with the TaxCodeMapper can be removed + if ($client === null && $supplier === null) { + return new NullTaxCombination(); + } + + // @todo define default sales tax code if none available?! + $itemCode = 0; + $accountCode = 0; + $combinationType = 'clientCode'; + + if ($client !== null) { + $itemCode = $item->getAttribute('sales_tax_code')->value->id; + $accountCode = $client->getAttribute('sales_tax_code')->value->id; + } else { + $itemCode = $item->getAttribute('purchase_tax_code')->value->id; + $accountCode = $supplier->getAttribute('purchase_tax_code')->value->id; + $combinationType = 'supplierCode'; + } /** @var \Modules\Billing\Models\Tax\TaxCombination $taxCombination */ $taxCombination = TaxCombinationMapper::get() - ->where('itemCode', $item->getAttribute('sales_tax_code')->value->id) - ->where('clientCode', $client->getAttribute('sales_tax_code')->value->id) + ->with('taxCode') + ->where('itemCode', $itemCode) + ->where($combinationType, $accountCode) ->execute(); - /** @var \Modules\Finance\Models\TaxCode $taxCode */ - $taxCode = TaxCodeMapper::get() - ->where('abbr', $taxCombination->taxCode) - ->execute(); - - if ($taxCode->id !== 0) { - return $taxCode; + if ($taxCombination->taxCode->id !== 0) { + return $taxCombination; } /** @var \Modules\Organization\Models\Unit $unit */ @@ -78,28 +98,23 @@ final class ApiTaxController extends Controller ->where('id', $this->app->unitId) ->execute(); - // Create dummy client - $client = new Client(); - $client->mainAddress = $unit->mainAddress; + // Create dummy + $account = $client !== null ? new Client() : new Supplier(); + $account->mainAddress = $unit->mainAddress; if (!empty($defaultCountry)) { - $client->mainAddress->setCountry($defaultCountry); + $account->mainAddress->setCountry($defaultCountry); } - $taxCodeAttribute = $this->getClientTaxCode($client, $unit->mainAddress); + $taxCodeAttribute = $client !== null + ? $this->getClientTaxCode($account, $unit->mainAddress) + : $this->getSupplierTaxCode($account, $unit->mainAddress); - /** @var \Modules\Billing\Models\Tax\TaxCombination $taxCombination */ - $taxCombination = TaxCombinationMapper::get() - ->where('itemCode', $item->getAttribute('sales_tax_code')->value->id) - ->where('clientCode', $taxCodeAttribute->id) + return TaxCombinationMapper::get() + ->with('taxCode') + ->where('itemCode', $itemCode) + ->where($combinationType, $taxCodeAttribute->id) ->execute(); - - /** @var \Modules\Finance\Models\TaxCode $taxCode */ - $taxCode = TaxCodeMapper::get() - ->where('abbr', $taxCombination->taxCode) - ->execute(); - - return $taxCode; } /** @@ -138,10 +153,11 @@ final class ApiTaxController extends Controller */ private function createTaxCombinationFromRequest(RequestAbstract $request) : TaxCombination { - $tax = new TaxCombination(); - $tax->taxType = $request->getDataInt('tax_type') ?? 1; - $tax->taxCode = (string) $request->getData('tax_code'); - $tax->itemCode = new NullAttributeValue((int) $request->getData('item_code')); + $tax = new TaxCombination(); + $tax->taxType = $request->getDataInt('tax_type') ?? 1; + $tax->taxCode->abbr = (string) $request->getData('tax_code'); + $tax->itemCode = new NullAttributeValue((int) $request->getData('item_code')); + $tax->account = $request->getDataString('account') ?? ''; if ($tax->taxType === 1) { $tax->clientCode = new NullAttributeValue((int) $request->getData('account_code')); @@ -195,28 +211,72 @@ final class ApiTaxController extends Controller $taxCode = new NullAttributeValue(); - // @todo: need to consider own tax id as well - if ($taxOfficeAddress->getCountry() === $client->mainAddress->getCountry()) { - $taxCode = $codes->getDefaultByValue($client->mainAddress->getCountry()); - } elseif (\in_array($taxOfficeAddress->getCountry(), ISO3166CharEnum::getRegion('eu')) - && \in_array($client->mainAddress->getCountry(), ISO3166CharEnum::getRegion('eu')) + // @todo need to consider own tax id as well + // @todo consider delivery & invoice location (Reihengeschaeft) + if ($taxOfficeAddress->country === $client->mainAddress->country) { + // Same country as we (= local tax code) + return $codes->getDefaultByValue($client->mainAddress->country); + } elseif (\in_array($taxOfficeAddress->country, ISO3166TwoEnum::getRegion('eu')) + && \in_array($client->mainAddress->country, ISO3166TwoEnum::getRegion('eu')) ) { if (!empty($client->getAttribute('vat_id')->value->getValue())) { - // Is EU company - $taxCode = $codes->getDefaultByValue('EU'); + // Is EU company and we are EU company + return $codes->getDefaultByValue('EU'); } else { - // Is EU private customer - $taxCode = $codes->getDefaultByValue($client->mainAddress->getCountry()); + // Is EU private customer and we are EU company + return $codes->getDefaultByValue($client->mainAddress->country); } - } elseif (\in_array($taxOfficeAddress->getCountry(), ISO3166CharEnum::getRegion('eu'))) { + } elseif (\in_array($taxOfficeAddress->country, ISO3166TwoEnum::getRegion('eu'))) { // None EU company but we are EU company - $taxCode = $codes->getDefaultByValue('INT'); - } else { - // None EU company and we are also none EU company - $taxCode = $codes->getDefaultByValue('INT'); + return $codes->getDefaultByValue('INT'); } - return $taxCode; + // None EU company and we are also none EU company + return $codes->getDefaultByValue('INT'); + } + + /** + * Get the supplier's tax code based on their country and tax office address + * + * @param Supplier $supplier The supplier to get the tax code for + * @param Address $taxOfficeAddress The tax office address used to determine the tax code + * + * @return AttributeValue The supplier's tax code + * + * @since 1.0.0 + */ + public function getSupplierTaxCode(Supplier $supplier, Address $taxOfficeAddress) : AttributeValue + { + /** @var \Modules\Attribute\Models\AttributeType $codes */ + $codes = SupplierAttributeTypeMapper::get() + ->with('defaults') + ->where('name', 'purchase_tax_code') + ->execute(); + + $taxCode = new NullAttributeValue(); + + // @todo need to consider own tax id as well + // @todo consider delivery & invoice location (Reihengeschaeft) + if ($taxOfficeAddress->country === $supplier->mainAddress->country) { + // Same country as we (= local tax code) + return $codes->getDefaultByValue($supplier->mainAddress->country); + } elseif (\in_array($taxOfficeAddress->country, ISO3166TwoEnum::getRegion('eu')) + && \in_array($supplier->mainAddress->country, ISO3166TwoEnum::getRegion('eu')) + ) { + if (!empty($supplier->getAttribute('vat_id')->value->getValue())) { + // Is EU company and we are EU company + return $codes->getDefaultByValue('EU'); + } else { + // Is EU private customer and we are EU company + return $codes->getDefaultByValue($supplier->mainAddress->country); + } + } elseif (\in_array($taxOfficeAddress->country, ISO3166TwoEnum::getRegion('eu'))) { + // None EU company but we are EU company + return $codes->getDefaultByValue('INT'); + } + + // None EU company and we are also none EU company + return $codes->getDefaultByValue('INT'); } /** @@ -307,7 +367,7 @@ final class ApiTaxController extends Controller $old = \reset($old); $new = clone $old; - $new->taxCode = $combination['tax_code'] ?? ''; + $new->taxCode = TaxCodeMapper::get()->where('abbr', $combination['tax_code'] ?? '')->execute(); $this->updateModel($request->header->account, $old, $new, TaxCombinationMapper::class, 'tax_combination', $request->getOrigin()); } @@ -347,7 +407,7 @@ final class ApiTaxController extends Controller public function updateTaxCombinationFromRequest(RequestAbstract $request, TaxCombination $new) : TaxCombination { $new->taxType = $request->getDataInt('tax_type') ?? $new->taxType; - $new->taxCode = $request->getDataString('tax_code') ?? $new->taxCode; + $new->taxCode = $request->hasData('tax_code') ? new NullTaxCode((int) $request->getData('tax_code')) : $new->taxCode; $new->itemCode = $request->hasData('item_code') ? new NullAttributeValue((int) $request->getData('item_code')) : $new->itemCode; if ($new->taxType === 1) { @@ -413,8 +473,6 @@ final class ApiTaxController extends Controller * * @return array * - * @todo: implement - * * @since 1.0.0 */ private function validateTaxCombinationDelete(RequestAbstract $request) : array diff --git a/Controller/BackendController.php b/Controller/BackendController.php index 3969848..2d53acc 100755 --- a/Controller/BackendController.php +++ b/Controller/BackendController.php @@ -20,15 +20,18 @@ use Modules\Billing\Models\BillMapper; use Modules\Billing\Models\BillStatus; 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\Asset\AssetType; +use phpOMS\Account\PermissionType; use phpOMS\Contract\RenderableInterface; use phpOMS\DataStorage\Database\Query\OrderType; -use phpOMS\Localization\ISO3166CharEnum; -use phpOMS\Localization\ISO3166NameEnum; use phpOMS\Message\RequestAbstract; use phpOMS\Message\ResponseAbstract; use phpOMS\Utils\StringUtils; @@ -46,7 +49,7 @@ use phpOMS\Views\View; final class BackendController extends Controller { /** - * Routing end-point for application behaviour. + * Routing end-point for application behavior. * * @param RequestAbstract $request Request * @param ResponseAbstract $response Response @@ -67,9 +70,11 @@ final class BackendController extends Controller ->with('type') ->with('type/l11n') ->with('client') + ->where('status', BillStatus::DRAFT) ->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') { @@ -91,7 +96,54 @@ final class BackendController extends Controller } /** - * Routing end-point for application behaviour. + * Routing end-point for application behavior. + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return RenderableInterface + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + public function viewBillingSalesArchive(RequestAbstract $request, ResponseAbstract $response, array $data = []) : RenderableInterface + { + $view = new View($this->app->l11nManager, $request, $response); + $view->setTemplate('/Modules/Billing/Theme/Backend/sales-bill-list'); + $view->data['nav'] = $this->app->moduleManager->get('Navigation')->createNavigationMid(1005104001, $request, $response); + + $mapperQuery = SalesBillMapper::getAll() + ->with('type') + ->with('type/l11n') + ->with('client') + ->where('status', BillStatus::DRAFT, '!=') + ->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') { + $view->data['bills'] = $mapperQuery + ->where('id', $request->getDataInt('id') ?? 0, '<') + ->where('client', null, '!=') + ->execute(); + } elseif ($request->getData('ptype') === 'n') { + $view->data['bills'] = $mapperQuery->where('id', $request->getDataInt('id') ?? 0, '>') + ->where('client', null, '!=') + ->execute(); + } else { + $view->data['bills'] = $mapperQuery->where('id', 0, '>') + ->where('client', null, '!=') + ->execute(); + } + + return $view; + } + + /** + * Routing end-point for application behavior. * * @param RequestAbstract $request Request * @param ResponseAbstract $response Response @@ -112,6 +164,7 @@ final class BackendController extends Controller $bill = SalesBillMapper::get() ->with('client') ->with('elements') + ->with('elements/container') ->with('files') ->with('files/types') ->with('notes') @@ -120,31 +173,53 @@ final class BackendController extends Controller $view->data['bill'] = $bill; - /** @var \Modules\Auditor\Models\Audit[] $logsBill */ - $logsBill = AuditMapper::getAll() - ->with('createdBy') - ->where('module', 'Billing') - ->where('type', StringUtils::intHash(BillMapper::class)) - ->where('ref', $bill->id) + $billTypes = BillTypeMapper::getAll() + ->with('l11n') + ->where('isTemplate', false) + ->where('transferType', BillTransferType::SALES) + ->where('l11n/language', $request->header->l11n->language) ->execute(); - /** @var \Modules\Auditor\Models\Audit[] $logsElements */ - $logsElements = AuditMapper::getAll() - ->with('createdBy') - ->where('module', 'Billing') - ->where('type', StringUtils::intHash(BillElementMapper::class)) - ->where('ref', \array_keys($bill->getElements()), 'IN') - ->execute(); + $view->data['billtypes'] = $billTypes; - $logs = \array_merge($logsBill, $logsElements); + $logs = []; + if ($this->app->accountManager->get($request->header->account)->hasPermission( + PermissionType::READ, + $this->app->unitId, + null, + self::NAME, + PermissionCategory::BILL_LOG, + ) + ) { + /** @var \Modules\Auditor\Models\Audit[] $logs */ + $logs = AuditMapper::getAll() + ->with('createdBy') + ->where('module', 'Billing') + ->where('type', StringUtils::intHash(BillMapper::class)) + ->where('ref', $bill->id) + ->execute(); - $view->data['logs'] = $logs; + if (!empty($bill->elements)) { + /** @var \Modules\Auditor\Models\Audit[] $logsElements */ + $logsElements = AuditMapper::getAll() + ->with('createdBy') + ->where('module', 'Billing') + ->where('type', StringUtils::intHash(BillElementMapper::class)) + ->where('ref', \array_keys($bill->elements), 'IN') + ->execute(); + + $logs = \array_merge($logs, $logsElements); + } + } + + $view->data['logs'] = $logs; + $view->data['media-upload'] = new \Modules\Media\Theme\Backend\Components\Upload\BaseView($this->app->l11nManager, $request, $response); return $view; } /** - * Routing end-point for application behaviour. + * Routing end-point for application behavior. * * @param RequestAbstract $request Request * @param ResponseAbstract $response Response @@ -170,15 +245,13 @@ final class BackendController extends Controller $view->data['billtypes'] = $billTypes; - $mediaListView = new \Modules\Media\Theme\Backend\Components\Media\ListView($this->app->l11nManager, $request, $response); - $mediaListView->setTemplate('/Modules/Media/Theme/Backend/Components/Media/list'); - $view->data['medialist'] = $mediaListView; + $view->data['media-upload'] = new \Modules\Media\Theme\Backend\Components\Upload\BaseView($this->app->l11nManager, $request, $response); return $view; } /** - * Routing end-point for application behaviour. + * Routing end-point for application behavior. * * @param RequestAbstract $request Request * @param ResponseAbstract $response Response @@ -199,7 +272,7 @@ final class BackendController extends Controller } /** - * Routing end-point for application behaviour. + * Routing end-point for application behavior. * * @param RequestAbstract $request Request * @param ResponseAbstract $response Response @@ -220,7 +293,7 @@ final class BackendController extends Controller } /** - * Routing end-point for application behaviour. + * Routing end-point for application behavior. * * @param RequestAbstract $request Request * @param ResponseAbstract $response Response @@ -243,6 +316,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') { @@ -267,7 +341,7 @@ final class BackendController extends Controller } /** - * Routing end-point for application behaviour. + * Routing end-point for application behavior. * * @param RequestAbstract $request Request * @param ResponseAbstract $response Response @@ -284,37 +358,61 @@ final class BackendController extends Controller $view->setTemplate('/Modules/Billing/Theme/Backend/purchase-bill'); $view->data['nav'] = $this->app->moduleManager->get('Navigation')->createNavigationMid(1005105001, $request, $response); - $bill = PurchaseBillMapper::get() + $view->data['bill'] = PurchaseBillMapper::get() + ->with('supplier') ->with('elements') + ->with('elements/container') ->with('files') ->with('files/types') ->with('notes') ->where('id', (int) $request->getData('id')) ->execute(); - $view->data['bill'] = $bill; + $view->data['billtypes'] = BillTypeMapper::getAll() + ->with('l11n') + ->where('isTemplate', false) + ->where('transferType', BillTransferType::PURCHASE) + ->where('l11n/language', $request->header->l11n->language) + ->execute(); - /** @var \Model\Setting $previewType */ - $previewType = $this->app->appSettings->get( - names: SettingsEnum::PREVIEW_MEDIA_TYPE, - module: self::NAME - ); + $logs = []; + if ($this->app->accountManager->get($request->header->account)->hasPermission( + PermissionType::READ, + $this->app->unitId, + null, + self::NAME, + PermissionCategory::BILL_LOG, + ) + ) { + /** @var \Modules\Auditor\Models\Audit[] $logs */ + $logs = AuditMapper::getAll() + ->with('createdBy') + ->where('module', 'Billing') + ->where('type', StringUtils::intHash(BillMapper::class)) + ->where('ref', $view->data['bill']->id) + ->execute(); - $view->data['previewType'] = (int) $previewType->content; + if (!empty($view->data['bill']->elements)) { + /** @var \Modules\Auditor\Models\Audit[] $logsElements */ + $logsElements = AuditMapper::getAll() + ->with('createdBy') + ->where('module', 'Billing') + ->where('type', StringUtils::intHash(BillElementMapper::class)) + ->where('ref', \array_keys($view->data['bill']->elements), 'IN') + ->execute(); - /** @var \Model\Setting $originalType */ - $originalType = $this->app->appSettings->get( - names: SettingsEnum::ORIGINAL_MEDIA_TYPE, - module: self::NAME - ); + $logs = \array_merge($logs, $logsElements); + } + } - $view->data['originalType'] = (int) $originalType->content; + $view->data['logs'] = $logs; + $view->data['media-upload'] = new \Modules\Media\Theme\Backend\Components\Upload\BaseView($this->app->l11nManager, $request, $response); return $view; } /** - * Routing end-point for application behaviour. + * Routing end-point for application behavior. * * @param RequestAbstract $request Request * @param ResponseAbstract $response Response @@ -332,18 +430,18 @@ 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; } /** - * Routing end-point for application behaviour. + * Routing end-point for application behavior. * * @param RequestAbstract $request Request * @param ResponseAbstract $response Response @@ -362,13 +460,14 @@ final class BackendController extends Controller $bill = StockBillMapper::get()->where('id', (int) $request->getData('id'))->execute(); - $view->data['bill'] = $bill; + $view->data['bill'] = $bill; + $view->data['media-upload'] = new \Modules\Media\Theme\Backend\Components\Upload\BaseView($this->app->l11nManager, $request, $response); return $view; } /** - * Routing end-point for application behaviour. + * Routing end-point for application behavior. * * @param RequestAbstract $request Request * @param ResponseAbstract $response Response @@ -389,7 +488,7 @@ final class BackendController extends Controller } /** - * Routing end-point for application behaviour. + * Routing end-point for application behavior. * * @param RequestAbstract $request Request * @param ResponseAbstract $response Response @@ -410,7 +509,7 @@ final class BackendController extends Controller } /** - * Routing end-point for application behaviour. + * Routing end-point for application behavior. * * @param RequestAbstract $request Request * @param ResponseAbstract $response Response @@ -424,7 +523,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() @@ -434,6 +533,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') { @@ -455,7 +555,7 @@ final class BackendController extends Controller } /** - * Routing end-point for application behaviour. + * Routing end-point for application behavior. * * @param RequestAbstract $request Request * @param ResponseAbstract $response Response @@ -469,7 +569,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() @@ -490,13 +590,138 @@ 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; + } + + /** + * Routing end-point for application behavior. + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return RenderableInterface + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + public function viewPaymentList(RequestAbstract $request, ResponseAbstract $response, array $data = []) : RenderableInterface + { + $view = new View($this->app->l11nManager, $request, $response); + $view->setTemplate('/Modules/Billing/Theme/Backend/payment-type-list'); + $view->data['nav'] = $this->app->moduleManager->get('Navigation')->createNavigationMid(1002901101, $request, $response); + + $view->data['types'] = PaymentTermMapper::getAll() + ->with('l11n') + ->where('l11n/language', $response->header->l11n->language) + ->execute(); + + return $view; + } + + /** + * Routing end-point for application behavior. + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return RenderableInterface + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + public function viewPaymentView(RequestAbstract $request, ResponseAbstract $response, array $data = []) : RenderableInterface + { + $view = new View($this->app->l11nManager, $request, $response); + $view->setTemplate('/Modules/Billing/Theme/Backend/payment-view'); + $view->data['nav'] = $this->app->moduleManager->get('Navigation')->createNavigationMid(1002901101, $request, $response); + + $view->data['type'] = PaymentTermMapper::get() + ->with('l11n') + ->where('id', (int) $request->getData('id')) + ->where('l11n/language', $response->header->l11n->language) + ->execute(); + + $view->data['l11nView'] = new \Web\Backend\Views\L11nView($this->app->l11nManager, $request, $response); + + /** @var \phpOMS\Localization\BaseStringL11n[] $l11nValues */ + $l11nValues = PaymentTermL11nMapper::getAll() + ->where('ref', $view->data['type']->id) + ->execute(); + + $view->data['l11nValues'] = $l11nValues; + + return $view; + } + + /** + * Routing end-point for application behavior. + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return RenderableInterface + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + public function viewShippingList(RequestAbstract $request, ResponseAbstract $response, array $data = []) : RenderableInterface + { + $view = new View($this->app->l11nManager, $request, $response); + $view->setTemplate('/Modules/Billing/Theme/Backend/shipping-type-list'); + $view->data['nav'] = $this->app->moduleManager->get('Navigation')->createNavigationMid(1002901101, $request, $response); + + $view->data['types'] = ShippingTermMapper::getAll() + ->with('l11n') + ->where('l11n/language', $response->header->l11n->language) + ->execute(); + + return $view; + } + + /** + * Routing end-point for application behavior. + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return RenderableInterface + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + public function viewShippingView(RequestAbstract $request, ResponseAbstract $response, array $data = []) : RenderableInterface + { + $view = new View($this->app->l11nManager, $request, $response); + $view->setTemplate('/Modules/Billing/Theme/Backend/shipping-view'); + $view->data['nav'] = $this->app->moduleManager->get('Navigation')->createNavigationMid(1002901101, $request, $response); + + $view->data['type'] = ShippingTermMapper::get() + ->with('l11n') + ->where('id', (int) $request->getData('id')) + ->where('l11n/language', $response->header->l11n->language) + ->execute(); + + $view->data['l11nView'] = new \Web\Backend\Views\L11nView($this->app->l11nManager, $request, $response); + + /** @var \phpOMS\Localization\BaseStringL11n[] $l11nValues */ + $l11nValues = ShippingTermL11nMapper::getAll() + ->where('ref', $view->data['type']->id) + ->execute(); + + $view->data['l11nValues'] = $l11nValues; return $view; } diff --git a/Controller/CliController.php b/Controller/CliController.php index eb1121b..0271494 100755 --- a/Controller/CliController.php +++ b/Controller/CliController.php @@ -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; @@ -40,7 +47,7 @@ use phpOMS\Views\View; final class CliController extends Controller { /** - * Analyse supplier bill + * Analyze supplier bill * * @param RequestAbstract $request Request * @param ResponseAbstract $response Response @@ -53,32 +60,54 @@ 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('media') - ->with('media/types') - ->with('media/content') - ->where('id', (int) $request->getData('i')) - ->where('media/types/id', $originalType) + ->with('elements') + ->with('files') + ->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; - $identifierContent = \file_get_contents(__DIR__ . '/../Models/billIdentifier.json'); + $l11n = Localization::fromLanguage($language); + + $identifierContent = \file_get_contents(__DIR__ . '/../Models/bill_identifier.json'); if ($identifierContent === false) { $identifierContent = '{}'; } @@ -97,16 +126,37 @@ final class CliController extends Controller $supplierId = $this->matchSupplier($content, $suppliers); $bill->supplier = new NullSupplier($supplierId); - $supplier = $suppliers[$supplierId] ?? new NullSupplier(); + $supplier = $suppliers[$supplierId] ?? new NullSupplier(); - $bill->billTo = $supplier->account->name1; - $bill->billAddress = $supplier->mainAddress->address; - $bill->billCity = $supplier->mainAddress->city; - $bill->billZip = $supplier->mainAddress->postal; - $bill->billCountry = $supplier->mainAddress->getCountry(); + 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); + $type = InvoiceRecognition::findSupplierInvoiceType($content, $identifiers['type'], $language); /** @var \Modules\Billing\Models\BillType $billType */ $billType = BillTypeMapper::get() @@ -116,233 +166,289 @@ 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']); - // @todo: implement multiple due dates for bills + $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 Gross */ - $totalGross = $this->findBillGross($lines, $identifiers['total_gross'][$language]); - $bill->grossCosts = new FloatInt($totalGross); + /* 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 */ + // @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]); + + 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 = 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; + // 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 - * - * @param string $content String to analyze - * @param array $types Possible bill types - * @param string $language Bill language - * - * @return string - * - * @since 1.0.0 - */ - private function findSupplierInvoiceType(string $content, array $types, string $language) : string - { - $bestPos = \strlen($content); - $bestMatch = ''; - - foreach ($types as $name => $type) { - foreach ($type[$language] as $l11n) { - $found = \stripos($content, \strtolower($l11n)); - - if ($found !== false && $found < $bestPos) { - $bestPos = $found; - $bestMatch = $name; - } - } - } - - 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 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; - } - /** * Find possible supplier id * * Priorities: * 1. bill_match_pattern - * 2. name1 + iban + * 2. name1 + IBAN * 3. name1 + city || address - * 4. name1 * * @param string $content Content to analyze * @param Supplier[] $suppliers Suppliers @@ -355,7 +461,7 @@ final class CliController extends Controller { // bill_match_pattern foreach ($suppliers as $supplier) { - // @todo: consider to support regex? + // @todo consider to support regex? if ((!empty($supplier->getAttribute('bill_match_pattern')->value->valueStr) && \stripos($content, $supplier->getAttribute('bill_match_pattern')->value->valueStr) !== false) ) { @@ -363,7 +469,7 @@ final class CliController extends Controller } } - // name1 + iban + // name1 + IBAN foreach ($suppliers as $supplier) { if (\stripos($content, $supplier->account->name1) !== false) { $ibans = $supplier->getPaymentsByType(PaymentType::SWIFT); @@ -388,44 +494,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; - } } diff --git a/ICAL.txt b/ICAL.txt index 019045f..f1b55ed 100755 --- a/ICAL.txt +++ b/ICAL.txt @@ -1,6 +1,6 @@ # Individual Contributor License Agreement ("CLA") 1.0 -Thank you for your interest in Karaka-Management (the "Company"). In order to clarify the intellectual property license granted with Contributions from any person or entity, the Company must provide a Contributor License Agreement ("CLA") on file that has been made available to each Contributor. This license is for your protection as a Contributor as well as the protection of the Company and its users; it does not change your rights to use your own Contributions for any other purpose. +Thank you for your interest in Jingga e. K. (the "Company"). In order to clarify the intellectual property license granted with Contributions from any person or entity, the Company must provide a Contributor License Agreement ("CLA") on file that has been made available to each Contributor. This license is for your protection as a Contributor as well as the protection of the Company and its users; it does not change your rights to use your own Contributions for any other purpose. By contributing to the Company You accept and agree to the following terms and conditions for Your present and future Contributions submitted to the Company. In return, the Company shall not use Your Contributions in a way that is contrary to the public benefit or inconsistent with its bylaws in effect at the time of the Contribution. Except for the license granted herein to the Company and recipients of software distributed by the Company, You reserve all right, title, and interest in and to Your Contributions. diff --git a/Models/Attribute/BillAttributeTypeMapper.php b/Models/Attribute/BillAttributeTypeMapper.php index d41769b..3429716 100755 --- a/Models/Attribute/BillAttributeTypeMapper.php +++ b/Models/Attribute/BillAttributeTypeMapper.php @@ -42,6 +42,8 @@ final class BillAttributeTypeMapper extends DataMapperFactory 'billing_attr_type_datatype' => ['name' => 'billing_attr_type_datatype', 'type' => 'int', 'internal' => 'datatype'], 'billing_attr_type_fields' => ['name' => 'billing_attr_type_fields', 'type' => 'int', 'internal' => 'fields'], 'billing_attr_type_custom' => ['name' => 'billing_attr_type_custom', 'type' => 'bool', 'internal' => 'custom'], + 'billing_attr_type_repeatable' => ['name' => 'billing_attr_type_repeatable', 'type' => 'bool', 'internal' => 'repeatable'], + 'billing_attr_type_internal' => ['name' => 'billing_attr_type_internal', 'type' => 'bool', 'internal' => 'isInternal'], 'billing_attr_type_pattern' => ['name' => 'billing_attr_type_pattern', 'type' => 'string', 'internal' => 'validationPattern'], 'billing_attr_type_required' => ['name' => 'billing_attr_type_required', 'type' => 'bool', 'internal' => 'isRequired'], ]; diff --git a/Models/Attribute/BillAttributeValueL11nMapper.php b/Models/Attribute/BillAttributeValueL11nMapper.php index ade8ec8..939cd7e 100755 --- a/Models/Attribute/BillAttributeValueL11nMapper.php +++ b/Models/Attribute/BillAttributeValueL11nMapper.php @@ -37,10 +37,10 @@ final class BillAttributeValueL11nMapper extends DataMapperFactory * @since 1.0.0 */ public const COLUMNS = [ - 'billing_attr_value_l11n_id' => ['name' => 'billing_attr_value_l11n_id', 'type' => 'int', 'internal' => 'id'], - 'billing_attr_value_l11n_title' => ['name' => 'billing_attr_value_l11n_title', 'type' => 'string', 'internal' => 'content', 'autocomplete' => true], - 'billing_attr_value_l11n_value' => ['name' => 'billing_attr_value_l11n_value', 'type' => 'int', 'internal' => 'ref'], - 'billing_attr_value_l11n_lang' => ['name' => 'billing_attr_value_l11n_lang', 'type' => 'string', 'internal' => 'language'], + 'billing_attr_value_l11n_id' => ['name' => 'billing_attr_value_l11n_id', 'type' => 'int', 'internal' => 'id'], + 'billing_attr_value_l11n_title' => ['name' => 'billing_attr_value_l11n_title', 'type' => 'string', 'internal' => 'content', 'autocomplete' => true], + 'billing_attr_value_l11n_value' => ['name' => 'billing_attr_value_l11n_value', 'type' => 'int', 'internal' => 'ref'], + 'billing_attr_value_l11n_lang' => ['name' => 'billing_attr_value_l11n_lang', 'type' => 'string', 'internal' => 'language'], ]; /** diff --git a/Models/Attribute/BillAttributeValueMapper.php b/Models/Attribute/BillAttributeValueMapper.php index 22323b6..2e54b8b 100755 --- a/Models/Attribute/BillAttributeValueMapper.php +++ b/Models/Attribute/BillAttributeValueMapper.php @@ -37,15 +37,15 @@ final class BillAttributeValueMapper extends DataMapperFactory * @since 1.0.0 */ public const COLUMNS = [ - 'billing_attr_value_id' => ['name' => 'billing_attr_value_id', 'type' => 'int', 'internal' => 'id'], - 'billing_attr_value_default' => ['name' => 'billing_attr_value_default', 'type' => 'bool', 'internal' => 'isDefault'], - 'billing_attr_value_valueStr' => ['name' => 'billing_attr_value_valueStr', 'type' => 'string', 'internal' => 'valueStr'], - 'billing_attr_value_valueInt' => ['name' => 'billing_attr_value_valueInt', 'type' => 'int', 'internal' => 'valueInt'], - 'billing_attr_value_valueDec' => ['name' => 'billing_attr_value_valueDec', 'type' => 'float', 'internal' => 'valueDec'], - 'billing_attr_value_valueDat' => ['name' => 'billing_attr_value_valueDat', 'type' => 'DateTime', 'internal' => 'valueDat'], - 'billing_attr_value_unit' => ['name' => 'billing_attr_value_unit', 'type' => 'string', 'internal' => 'unit'], - 'billing_attr_value_deptype' => ['name' => 'billing_attr_value_deptype', 'type' => 'int', 'internal' => 'dependingAttributeType'], - 'billing_attr_value_depvalue' => ['name' => 'billing_attr_value_depvalue', 'type' => 'int', 'internal' => 'dependingAttributeValue'], + 'billing_attr_value_id' => ['name' => 'billing_attr_value_id', 'type' => 'int', 'internal' => 'id'], + 'billing_attr_value_default' => ['name' => 'billing_attr_value_default', 'type' => 'bool', 'internal' => 'isDefault'], + 'billing_attr_value_valueStr' => ['name' => 'billing_attr_value_valueStr', 'type' => 'string', 'internal' => 'valueStr'], + 'billing_attr_value_valueInt' => ['name' => 'billing_attr_value_valueInt', 'type' => 'int', 'internal' => 'valueInt'], + 'billing_attr_value_valueDec' => ['name' => 'billing_attr_value_valueDec', 'type' => 'float', 'internal' => 'valueDec'], + 'billing_attr_value_valueDat' => ['name' => 'billing_attr_value_valueDat', 'type' => 'DateTime', 'internal' => 'valueDat'], + 'billing_attr_value_unit' => ['name' => 'billing_attr_value_unit', 'type' => 'string', 'internal' => 'unit'], + 'billing_attr_value_deptype' => ['name' => 'billing_attr_value_deptype', 'type' => 'int', 'internal' => 'dependingAttributeType'], + 'billing_attr_value_depvalue' => ['name' => 'billing_attr_value_depvalue', 'type' => 'int', 'internal' => 'dependingAttributeValue'], ]; /** @@ -59,6 +59,7 @@ final class BillAttributeValueMapper extends DataMapperFactory 'mapper' => BillAttributeValueL11nMapper::class, 'table' => 'billing_attr_value_l11n', 'self' => 'billing_attr_value_l11n_value', + 'column' => 'content', 'external' => null, ], ]; diff --git a/Models/Bill.php b/Models/Bill.php index 5ed024f..2a6784a 100755 --- a/Models/Bill.php +++ b/Models/Bill.php @@ -17,7 +17,6 @@ namespace Modules\Billing\Models; use Modules\Admin\Models\Account; use Modules\Admin\Models\NullAccount; use Modules\ClientManagement\Models\Client; -use Modules\Media\Models\Collection; use Modules\SupplierManagement\Models\Supplier; use phpOMS\Localization\ISO4217CharEnum; use phpOMS\Localization\ISO639x1Enum; @@ -66,6 +65,8 @@ class Bill implements \JsonSerializable */ public string $number = ''; + public string $external = ''; + /** * Bill type. * @@ -74,7 +75,9 @@ class Bill implements \JsonSerializable */ public BillType $type; - public ?Collection $template = null; + public bool $isTemplate = false; + + public bool $isArchived = false; /** * Bill status. @@ -239,15 +242,13 @@ class Bill implements \JsonSerializable public string $billEmail = ''; /** - * Person refering for this order. + * Person referring for this order. * * @var Account * @since 1.0.0 */ public Account $referral; - public string $referralName = ''; - /** * Net amount. * @@ -256,14 +257,6 @@ class Bill implements \JsonSerializable */ public FloatInt $netProfit; - /** - * Gross amount. - * - * @var FloatInt - * @since 1.0.0 - */ - public FloatInt $grossProfit; - /** * Costs in net. * @@ -272,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,28 +290,14 @@ class Bill implements \JsonSerializable public FloatInt $netDiscount; /** - * Profit in net. + * Tax amount * * @var FloatInt * @since 1.0.0 */ - public FloatInt $grossDiscount; + public FloatInt $taxP; - /** - * Insurance fees in net. - * - * @var FloatInt - * @since 1.0.0 - */ - public FloatInt $insurance; - - /** - * Freight in net. - * - * @var FloatInt - * @since 1.0.0 - */ - public FloatInt $freight; + public ?int $accTaxCode = null; /** * Currency. @@ -360,12 +331,6 @@ class Bill implements \JsonSerializable */ public string $info = ''; - /** - * Payment type. - * - * @var int - * @since 1.0.0 - */ public int $payment = 0; /** @@ -384,6 +349,10 @@ class Bill implements \JsonSerializable */ public int $terms = 0; + public ?int $paymentTerms = null; + + public ?int $shippingTerms = null; + /** * Terms text. * @@ -392,14 +361,6 @@ class Bill implements \JsonSerializable */ public string $termsText = ''; - /** - * Shipping. - * - * @var int - * @since 1.0.0 - */ - public int $shipping = 0; - /** * Shipping text. * @@ -440,6 +401,18 @@ class Bill implements \JsonSerializable */ public int $reference = 0; + public ?int $accSegment = null; + + public ?int $accSection = null; + + public ?int $accGroup = null; + + public ?int $accType = null; + + public ?string $fiAccount = null; + + // @todo Implement reason for bill (especially useful for credit notes, warehouse bookings) + /** * Constructor. * @@ -447,14 +420,12 @@ 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->netProfit = new FloatInt(0); + $this->netCosts = new FloatInt(0); + $this->netSales = new FloatInt(0); + $this->grossSales = new FloatInt(0); + $this->netDiscount = new FloatInt(0); + $this->taxP = new FloatInt(0); $this->billDate = new \DateTime('now'); $this->createdAt = new \DateTimeImmutable(); @@ -463,18 +434,6 @@ class Bill implements \JsonSerializable $this->type = new NullBillType(); } - /** - * Get id. - * - * @return int Model id - * - * @since 1.0.0 - */ - public function getId() : int - { - return $this->id; - } - /** * Build the invoice number. * @@ -492,6 +451,9 @@ class Bill implements \JsonSerializable '{id}', '{sequence}', '{type}', + '{unit}', + '{account}', + '{country}', ], [ $this->createdAt->format('Y'), @@ -500,6 +462,9 @@ class Bill implements \JsonSerializable $this->id, $this->sequence, $this->type->id, + $this->unit, + $this->accountNumber, + $this->billCountry, ], $this->type->numberFormat ); @@ -521,32 +486,6 @@ class Bill implements \JsonSerializable return $this->number; } - /** - * Get status - * - * @return int - * - * @since 1.0.0 - */ - public function getStatus() : int - { - return $this->status; - } - - /** - * Set status - * - * @param int $status Status - * - * @return void - * - * @since 1.0.0 - */ - public function setStatus(int $status) : void - { - $this->status = $status; - } - /** * Get paymentStatus * @@ -573,96 +512,6 @@ class Bill implements \JsonSerializable $this->paymentStatus = $paymentStatus; } - /** - * Set currency. - * - * @param string $currency Currency - * - * @return void - * - * @since 1.0.0 - */ - public function setCurrency(string $currency) : void - { - $this->currency = $currency; - } - - /** - * Get currency. - * - * @return string - * - * @since 1.0.0 - */ - public function getCurrency() : string - { - return $this->currency; - } - - /** - * Get vouchers. - * - * @return array - * - * @since 1.0.0 - */ - public function getVouchers() : array - { - return $this->vouchers; - } - - /** - * Add voucher. - * - * @param string $voucher Voucher code - * - * @return void - * - * @since 1.0.0 - */ - public function addVoucher(string $voucher) : void - { - $this->vouchers[] = $voucher; - } - - /** - * Get tracking ids for shipment. - * - * @return array - * - * @since 1.0.0 - */ - public function getTrackings() : array - { - return $this->trackings; - } - - /** - * Add tracking id. - * - * @param string $tracking Tracking id - * - * @return void - * - * @since 1.0.0 - */ - public function addTracking(string $tracking) : void - { - $this->trackings[] = $tracking; - } - - /** - * Get Bill elements. - * - * @return BillElement[] - * - * @since 1.0.0 - */ - public function getElements() : array - { - return $this->elements; - } - /** * Add Bill element. * @@ -676,16 +525,157 @@ class Bill implements \JsonSerializable { $this->elements[] = $element; - $this->netProfit->add($element->totalProfitNet->getInt()); - $this->grossProfit->add($element->totalProfitGross->getInt()); - $this->netCosts->add($element->totalPurchasePriceNet->getInt()); - $this->grossCosts->add($element->totalPurchasePriceGross->getInt()); - $this->netSales->add($element->totalSalesPriceNet->getInt()); - $this->grossSales->add($element->totalSalesPriceGross->getInt()); - $this->netDiscount->add($element->totalDiscountP->getInt()); + $this->netProfit->value += $element->totalProfitNet->value; + $this->netCosts->value += $element->totalPurchasePriceNet->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->add((int) ($element->taxR->getInt() * $element->totalDiscountP->getInt() / 10000)); + /** + * Validate the correctness of the bill + * + * @return bool + * + * @todo also consider rounding similarly to recalculatePrices in elements + * + * @since 1.0.0 + */ + public function isValid() : bool + { + return $this->validateTaxAmountElements() + && $this->validateProfit() + && $this->validateGrossElements() + && $this->validatePriceQuantityElements() + && $this->validateNetElements() + && $this->validateNetGross() + && $this->areElementsValid(); + } + + /** + * Validate the correctness of the bill elements + * + * @return bool + * + * @since 1.0.0 + */ + public function areElementsValid() : bool + { + foreach ($this->elements as $element) { + if (!$element->isValid()) { + return false; + } + } + + return true; + } + + /** + * Validate the correctness of the net and gross values + * + * @return bool + * + * @since 1.0.0 + */ + public function validateNetGross() : bool + { + return $this->netSales->value <= $this->grossSales->value; + } + + /** + * Validate the correctness of the profit + * + * @return bool + * + * @since 1.0.0 + */ + public function validateProfit() : bool + { + return $this->netSales->value - $this->netCosts->value === $this->netProfit->value; + } + + /** + * Validate the correctness of the taxes + * + * @return bool + * + * @since 1.0.0 + */ + public function validateTax() : bool + { + return \abs($this->netSales->value + $this->taxP->value - $this->grossSales->value) === 0; + } + + /** + * Validate the correctness of the taxes for the elements + * + * @return bool + * + * @since 1.0.0 + */ + public function validateTaxAmountElements() : bool + { + $taxes = 0; + foreach ($this->elements as $element) { + $taxes += $element->taxP->value; + } + + return $taxes === $this->taxP->value; + } + + /** + * Validate the correctness of the net of the elements + * + * @return bool + * + * @since 1.0.0 + */ + public function validateNetElements() : bool + { + $net = 0; + foreach ($this->elements as $element) { + $net += $element->totalSalesPriceNet->value; + } + + return $net === $this->netSales->value; + } + + /** + * Validate the correctness of the gross of the elements + * + * @return bool + * + * @since 1.0.0 + */ + public function validateGrossElements() + { + $gross = 0; + foreach ($this->elements as $element) { + $gross += $element->totalSalesPriceGross->value; + } + + return $gross === $this->grossSales->value; + } + + /** + * Validate the correctness of the quantities and total price + * + * @return bool + * + * @since 1.0.0 + */ + 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; } /** @@ -694,21 +684,22 @@ class Bill implements \JsonSerializable public function toArray() : array { return [ - 'id' => $this->id, - 'number' => $this->number, - 'type' => $this->type, - 'shipTo' => $this->shipTo, - 'shipFAO' => $this->shipFAO, - 'shipAddress' => $this->shipAddress, - 'shipCity' => $this->shipCity, - 'shipZip' => $this->shipZip, - 'shipCountry' => $this->shipCountry, - 'billTo' => $this->billTo, - 'billFAO' => $this->billFAO, - 'billAddress' => $this->billAddress, - 'billCity' => $this->billCity, - 'billZip' => $this->billZip, - 'billCountry' => $this->billCountry, + 'id' => $this->id, + 'number' => $this->number, + 'external' => $this->external, + 'type' => $this->type, + 'shipTo' => $this->shipTo, + 'shipFAO' => $this->shipFAO, + 'shipAddress' => $this->shipAddress, + 'shipCity' => $this->shipCity, + 'shipZip' => $this->shipZip, + 'shipCountry' => $this->shipCountry, + 'billTo' => $this->billTo, + 'billFAO' => $this->billFAO, + 'billAddress' => $this->billAddress, + 'billCity' => $this->billCity, + 'billZip' => $this->billZip, + 'billCountry' => $this->billCountry, ]; } diff --git a/Models/BillElement.php b/Models/BillElement.php index 059ecf2..024c17a 100755 --- a/Models/BillElement.php +++ b/Models/BillElement.php @@ -14,8 +14,12 @@ declare(strict_types=1); namespace Modules\Billing\Models; +use Modules\Billing\Models\Tax\TaxCombination; 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; @@ -39,8 +43,9 @@ class BillElement implements \JsonSerializable public int $order = 0; - /** @todo: consider to reference the model instead of the int, this would make it much easier in other places like the shop */ - public ?int $item = null; + public ?Item $item = null; + + public ?Container $container = null; public string $itemNumber = ''; @@ -48,12 +53,30 @@ class BillElement implements \JsonSerializable public string $itemDescription = ''; - public int $quantity = 0; + /** + * Line quantity + * + * Careful this also includes the bonus items defined in $discountQ! + * + * @var FloatInt + * @since 1.0.0 + */ + public FloatInt $quantity; public ?Subscription $subscription = null; + /** + * Single unit price + * + * Careful this is NOT corrected by bonus items defined in $discountQ + * + * @var FloatInt + * @since 1.0.0 + */ public FloatInt $singleSalesPriceNet; + public FloatInt $effectiveSingleSalesPriceNet; + public FloatInt $singleSalesPriceGross; public FloatInt $totalSalesPriceNet; @@ -64,9 +87,9 @@ class BillElement implements \JsonSerializable public FloatInt $totalDiscountP; - public ?FloatInt $singleDiscountR = null; + public FloatInt $singleDiscountR; - public ?FloatInt $discountQ = null; + public FloatInt $discountQ; public FloatInt $singleListPriceNet; @@ -78,19 +101,29 @@ 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; + + public ?int $itemSalesGroup = null; + + public ?int $itemProductGroup = null; + + public ?int $itemType = null; + + public string $fiAccount = ''; + + public ?string $costcenter = null; + + public ?string $costobject = null; + + public ?TaxCombination $taxCombination = null; /** * Tax amount @@ -128,6 +161,9 @@ class BillElement implements \JsonSerializable public Bill $bill; + // Distribution of lots/sn and from which stock location + public array $identifiers = []; + /** * Constructor. * @@ -137,6 +173,8 @@ class BillElement implements \JsonSerializable { $this->bill = new NullBill(); + $this->quantity = new FloatInt(FloatInt::DIVISOR); + $this->singleListPriceNet = new FloatInt(); $this->singleListPriceGross = new FloatInt(); @@ -146,40 +184,26 @@ class BillElement implements \JsonSerializable $this->singleSalesPriceNet = new FloatInt(); $this->singleSalesPriceGross = new FloatInt(); + $this->effectiveSingleSalesPriceNet = new FloatInt(); + $this->totalSalesPriceNet = new FloatInt(); $this->totalSalesPriceGross = new FloatInt(); - $this->singlePurchasePriceNet = new FloatInt(); - $this->singlePurchasePriceGross = new FloatInt(); + $this->singlePurchasePriceNet = new FloatInt(); + $this->totalPurchasePriceNet = 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->singleProfitNet = new FloatInt(); + $this->totalProfitNet = new FloatInt(); $this->singleDiscountP = new FloatInt(); $this->totalDiscountP = new FloatInt(); + $this->singleDiscountR = new FloatInt(); + $this->discountQ = new FloatInt(); $this->taxP = new FloatInt(); $this->taxR = new FloatInt(); } - /** - * Get id. - * - * @return int Model id - * - * @since 1.0.0 - */ - public function getId() : int - { - return $this->id; - } - /** * Set the element quantity. * @@ -191,24 +215,172 @@ class BillElement implements \JsonSerializable */ public function setQuantity(int $quantity) : void { - if ($this->quantity === $quantity) { + if ($this->quantity->value === $quantity) { return; } - $this->quantity = $quantity; - // @todo: recalculate all the prices!!! + $this->quantity->value = $quantity; + + $this->recalculatePrices(); } /** - * Get quantity. + * Re-calculate prices. * - * @return int + * This function is very important to call after changing any prices/quantities + * + * @return void * * @since 1.0.0 */ - public function getQuantity() : int + public function recalculatePrices() : void { - return $this->quantity; + $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 / (FloatInt::DIVISOR * 100) * $this->totalSalesPriceNet->value, $rd); + + $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 / FloatInt::DIVISOR), $rd); + } + + /** + * Validate the correctness of the element + * + * @return bool + * + * @todo also consider rounding similarly to recalculatePrices + * + * @since 1.0.0 + */ + public function isValid() : bool + { + return $this->validateNetGross() + && $this->validateProfit() + && $this->validateTax() + && $this->validateTaxRate() + && $this->validateSingleTotal() + && $this->validateEffectiveSinglePrice() + && $this->validateTotalPrice(); + } + + /** + * Validate the correctness of the net and gross values + * + * @return bool + * + * @since 1.0.0 + */ + 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; + } + + /** + * Validate the correctness of the profit + * + * @return bool + * + * @since 1.0.0 + */ + public function validateProfit() : bool + { + return $this->totalSalesPriceNet->value - $this->totalPurchasePriceNet->value === $this->totalProfitNet->value; + } + + /** + * Validate the correctness of the taxes + * + * @return bool + * + * @since 1.0.0 + */ + 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; + } + + /** + * Validate the correctness of the tax rate + * + * @return bool + * + * @since 1.0.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); + } + + /** + * Validate the correctness of single and total prices + * + * @return bool + * + * @since 1.0.0 + */ + 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; + } + + /** + * Validate the correctness of the effective price + * + * @return bool + * + * @since 1.0.0 + */ + public function validateEffectiveSinglePrice() : bool + { + return $this->effectiveSingleSalesPriceNet->value === (int) \round($this->totalSalesPriceNet->value / ($this->quantity->value / FloatInt::DIVISOR)); + } + + /** + * Validate the correctness of the total price + * + * @return bool + * + * @since 1.0.0 + */ + 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; } /** @@ -222,56 +394,47 @@ class BillElement implements \JsonSerializable */ public function setItem(int $item) : void { - $this->item = $item; + $this->item = new NullItem($item); } /** * Create element from item * - * @param Item $item Item - * @param TaxCode $code Tax code used for gross amount calculation - * @param int $quantity Quantity - * @param int $bill Bill + * @param Item $item Item + * @param TaxCombination $taxCombination Tax combination + * @param Bill $bill Bill + * @param int $quantity Quantity (1.0 = 10000) + * @param null|Container $container Item container * * @return self * * @since 1.0.0 */ - public static function fromItem(Item $item, TaxCode $code, int $quantity = 1, int $bill = 0) : self + public static function fromItem( + Item $item, + TaxCombination $taxCombination, + Bill $bill, + int $quantity = FloatInt::DIVISOR, + ?Container $container = null + ) : self { $element = new self(); - $element->bill = new NullBill($bill); - $element->item = empty($item->id) ? null : $item->id; + $element->bill = $bill; + $element->item = empty($item->id) ? null : $item; + $element->container = empty($container->id) ? null : $container; $element->itemNumber = $item->number; $element->itemName = $item->getL11n('name1')->content; $element->itemDescription = $item->getL11n('description_short')->content; - $element->quantity = $quantity; + $element->quantity->value = $quantity; - // @todo: Use pricing instead of the default sales price - // @todo: discounts might be in quantities - $element->singleListPriceNet->setInt($item->salesPrice->getInt()); - $element->totalListPriceNet->setInt($element->quantity * $item->salesPrice->getInt()); - $element->singleSalesPriceNet->setInt($item->salesPrice->getInt()); - $element->totalSalesPriceNet->setInt($element->quantity * $item->salesPrice->getInt()); - $element->singlePurchasePriceNet->setInt($item->purchasePrice->getInt()); - $element->totalPurchasePriceNet->setInt($element->quantity * $item->purchasePrice->getInt()); + $element->taxR = new FloatInt($taxCombination->taxCode->percentageInvoice); + $element->taxCode = $taxCombination->taxCode->abbr; + $element->fiAccount = $taxCombination->account; + $element->taxCombination = $taxCombination; - $element->singleProfitNet->setInt($element->singleSalesPriceNet->getInt() - $element->singlePurchasePriceNet->getInt()); - $element->totalProfitNet->setInt($element->totalSalesPriceNet->getInt() - $element->totalPurchasePriceNet->getInt()); - - $element->taxP = new FloatInt((int) (($code->percentageInvoice * $element->totalSalesPriceNet->getInt()) / 10000)); - $element->taxR = new FloatInt($code->percentageInvoice); - $element->taxCode = $code->abbr; - - $element->singleListPriceGross->setInt((int) ($element->singleListPriceNet->getInt() + $element->singleListPriceNet->getInt() * $element->taxR->getInt() / 10000)); - $element->totalListPriceGross->setInt((int) ($element->totalListPriceNet->getInt() + $element->totalListPriceNet->getInt() * $element->taxR->getInt() / 10000)); - $element->singleSalesPriceGross->setInt((int) ($element->singleSalesPriceNet->getInt() + $element->singleSalesPriceNet->getInt() * $element->taxR->getInt() / 10000)); - $element->totalSalesPriceGross->setInt((int) ($element->totalSalesPriceNet->getInt() + $element->totalSalesPriceNet->getInt() * $element->taxR->getInt() / 10000)); - $element->singlePurchasePriceGross->setInt((int) ($element->singlePurchasePriceNet->getInt() + $element->singlePurchasePriceNet->getInt() * $element->taxR->getInt() / 10000)); - $element->totalPurchasePriceGross->setInt((int) ($element->totalPurchasePriceNet->getInt() + $element->totalPurchasePriceNet->getInt() * $element->taxR->getInt() / 10000)); - - $element->singleProfitGross->setInt($element->singleSalesPriceGross->getInt() - $element->singlePurchasePriceGross->getInt()); - $element->totalProfitGross->setInt($element->quantity * ($element->totalSalesPriceGross->getInt() - $element->totalPurchasePriceGross->getInt())); + // @todo the purchase price is based on lot/sn/avg prices if available + $element->singlePurchasePriceNet->value = $item->purchasePrice->value; + $element->totalPurchasePriceNet->value = (int) ($element->quantity->getNormalizedValue() * $item->purchasePrice->value); if ($element->bill->id !== 0 && $item->getAttribute('subscription')->value->getValue() === 1 @@ -279,10 +442,9 @@ class BillElement implements \JsonSerializable ) { $element->subscription = new Subscription(); $element->subscription->bill = $element->bill->id; - $element->subscription->item = $element->item; - $element->subscription->start = new \DateTime('now'); // @todo: change to bill performanceDate - $element->subscription->end = new SmartDateTime('now'); // @todo: depends on subscription type - $element->subscription->end->smartModify(m: 1); + $element->subscription->item = $element->item->id; + $element->subscription->start = $bill?->performanceDate ?? new \DateTime('now'); + $element->subscription->end = (new SmartDateTime('now'))->smartModify(m: 1); // @todo depends on subscription type $element->subscription->quantity = $element->quantity; $element->subscription->autoRenew = $item->getAttribute('subscription_renewal_type')->value->getValue() === 1; @@ -299,7 +461,7 @@ class BillElement implements \JsonSerializable return [ 'id' => $this->id, 'order' => $this->order, - 'item' => $this->item, + 'item' => $this->item?->id, 'itemNumber' => $this->itemNumber, 'itemName' => $this->itemName, 'itemDescription' => $this->itemDescription, diff --git a/Models/BillElementMapper.php b/Models/BillElementMapper.php index f397608..d8771e3 100755 --- a/Models/BillElementMapper.php +++ b/Models/BillElementMapper.php @@ -14,6 +14,9 @@ declare(strict_types=1); namespace Modules\Billing\Models; +use Modules\Billing\Models\Tax\TaxCombinationMapper; +use Modules\ItemManagement\Models\ContainerMapper; +use Modules\ItemManagement\Models\ItemMapper; use phpOMS\DataStorage\Database\Mapper\DataMapperFactory; /** @@ -36,38 +39,47 @@ final class BillElementMapper extends DataMapperFactory * @since 1.0.0 */ public const COLUMNS = [ - 'billing_bill_element_id' => ['name' => 'billing_bill_element_id', 'type' => 'int', 'internal' => 'id'], - 'billing_bill_element_order' => ['name' => 'billing_bill_element_order', 'type' => 'int', 'internal' => 'order'], - 'billing_bill_element_item' => ['name' => 'billing_bill_element_item', 'type' => 'int', 'internal' => 'item'], - 'billing_bill_element_item_number' => ['name' => 'billing_bill_element_item_number', 'type' => 'string', 'internal' => 'itemNumber'], - 'billing_bill_element_item_name' => ['name' => 'billing_bill_element_item_name', 'type' => 'string', 'internal' => 'itemName'], - 'billing_bill_element_item_desc' => ['name' => 'billing_bill_element_item_desc', 'type' => 'string', 'internal' => 'itemDescription'], - 'billing_bill_element_quantity' => ['name' => 'billing_bill_element_quantity', 'type' => 'int', 'internal' => 'quantity', 'private' => true], + 'billing_bill_element_id' => ['name' => 'billing_bill_element_id', 'type' => 'int', 'internal' => 'id'], + 'billing_bill_element_order' => ['name' => 'billing_bill_element_order', 'type' => 'int', 'internal' => 'order'], + 'billing_bill_element_item' => ['name' => 'billing_bill_element_item', 'type' => 'int', 'internal' => 'item'], + 'billing_bill_element_container' => ['name' => 'billing_bill_element_container', 'type' => 'int', 'internal' => 'container'], + 'billing_bill_element_item_number' => ['name' => 'billing_bill_element_item_number', 'type' => 'string', 'internal' => 'itemNumber'], + 'billing_bill_element_item_name' => ['name' => 'billing_bill_element_item_name', 'type' => 'string', 'internal' => 'itemName'], + 'billing_bill_element_item_desc' => ['name' => 'billing_bill_element_item_desc', 'type' => 'string', 'internal' => 'itemDescription'], + 'billing_bill_element_quantity' => ['name' => 'billing_bill_element_quantity', 'type' => 'Serializable', 'internal' => 'quantity', 'private' => true], - 'billing_bill_element_single_netlistprice' => ['name' => 'billing_bill_element_single_netlistprice', 'type' => 'Serializable', 'internal' => 'singleListPriceNet'], - 'billing_bill_element_single_grosslistprice' => ['name' => 'billing_bill_element_single_grosslistprice', 'type' => 'Serializable', 'internal' => 'singleListPriceGross'], - 'billing_bill_element_total_netlistprice' => ['name' => 'billing_bill_element_total_netlistprice', 'type' => 'Serializable', 'internal' => 'totalListPriceNet'], - 'billing_bill_element_total_grosslistprice' => ['name' => 'billing_bill_element_total_grosslistprice', 'type' => 'Serializable', 'internal' => 'totalListPriceGross'], + 'billing_bill_element_single_netlistprice' => ['name' => 'billing_bill_element_single_netlistprice', 'type' => 'Serializable', 'internal' => 'singleListPriceNet'], + 'billing_bill_element_single_grosslistprice' => ['name' => 'billing_bill_element_single_grosslistprice', 'type' => 'Serializable', 'internal' => 'singleListPriceGross'], + 'billing_bill_element_total_netlistprice' => ['name' => 'billing_bill_element_total_netlistprice', 'type' => 'Serializable', 'internal' => 'totalListPriceNet'], + 'billing_bill_element_total_grosslistprice' => ['name' => 'billing_bill_element_total_grosslistprice', 'type' => 'Serializable', 'internal' => 'totalListPriceGross'], - 'billing_bill_element_single_netsalesprice' => ['name' => 'billing_bill_element_single_netsalesprice', 'type' => 'Serializable', 'internal' => 'singleSalesPriceNet'], - 'billing_bill_element_single_grosssalesprice' => ['name' => 'billing_bill_element_single_grosssalesprice', 'type' => 'Serializable', 'internal' => 'singleSalesPriceGross'], - 'billing_bill_element_total_netsalesprice' => ['name' => 'billing_bill_element_total_netsalesprice', 'type' => 'Serializable', 'internal' => 'totalSalesPriceNet'], - 'billing_bill_element_total_grosssalesprice' => ['name' => 'billing_bill_element_total_grosssalesprice', 'type' => 'Serializable', 'internal' => 'totalSalesPriceGross'], + 'billing_bill_element_single_netsalesprice' => ['name' => 'billing_bill_element_single_netsalesprice', 'type' => 'Serializable', 'internal' => 'singleSalesPriceNet'], + 'billing_bill_element_single_effectivenetsalesprice' => ['name' => 'billing_bill_element_single_effectivenetsalesprice', 'type' => 'Serializable', 'internal' => 'effectiveSingleSalesPriceNet'], + 'billing_bill_element_single_grosssalesprice' => ['name' => 'billing_bill_element_single_grosssalesprice', 'type' => 'Serializable', 'internal' => 'singleSalesPriceGross'], + 'billing_bill_element_total_netsalesprice' => ['name' => 'billing_bill_element_total_netsalesprice', 'type' => 'Serializable', 'internal' => 'totalSalesPriceNet'], + '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_netprofit' => ['name' => 'billing_bill_element_single_netprofit', 'type' => 'Serializable', 'internal' => 'singleProfitNet'], + 'billing_bill_element_total_netprofit' => ['name' => 'billing_bill_element_total_netprofit', 'type' => 'Serializable', 'internal' => 'totalProfitNet'], - '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_single_netpurchaseprice' => ['name' => 'billing_bill_element_single_netpurchaseprice', 'type' => 'Serializable', 'internal' => 'singlePurchasePriceNet'], + 'billing_bill_element_total_netpurchaseprice' => ['name' => 'billing_bill_element_total_netpurchaseprice', 'type' => 'Serializable', 'internal' => 'totalPurchasePriceNet'], + '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'], - 'billing_bill_element_tax_price' => ['name' => 'billing_bill_element_tax_price', 'type' => 'Serializable', 'internal' => 'taxP'], - 'billing_bill_element_tax_percentage' => ['name' => 'billing_bill_element_tax_percentage', 'type' => 'Serializable', 'internal' => 'taxR'], + 'billing_bill_element_tax_combination' => ['name' => 'billing_bill_element_tax_combination', 'type' => 'int', 'internal' => 'taxCombination'], + 'billing_bill_element_tax_type' => ['name' => 'billing_bill_element_tax_type', 'type' => 'string', 'internal' => 'taxCode'], + 'billing_bill_element_tax_price' => ['name' => 'billing_bill_element_tax_price', 'type' => 'Serializable', 'internal' => 'taxP'], + 'billing_bill_element_tax_percentage' => ['name' => 'billing_bill_element_tax_percentage', 'type' => 'Serializable', 'internal' => 'taxR'], + + 'billing_bill_element_segment' => ['name' => 'billing_bill_element_segment', 'type' => 'int', 'internal' => 'itemSegment'], + 'billing_bill_element_section' => ['name' => 'billing_bill_element_section', 'type' => 'int', 'internal' => 'itemSection'], + 'billing_bill_element_salesgroup' => ['name' => 'billing_bill_element_salesgroup', 'type' => 'int', 'internal' => 'itemSalesGroup'], + 'billing_bill_element_productgroup' => ['name' => 'billing_bill_element_productgroup', 'type' => 'int', 'internal' => 'itemProductGroup'], + 'billing_bill_element_itemtype' => ['name' => 'billing_bill_element_itemtype', 'type' => 'int', 'internal' => 'itemType'], + + 'billing_bill_element_fiaccount' => ['name' => 'billing_bill_element_fiaccount', 'type' => 'string', 'internal' => 'fiAccount'], + 'billing_bill_element_costcenter' => ['name' => 'billing_bill_element_costcenter', 'type' => 'string', 'internal' => 'costcenter'], + 'billing_bill_element_costobject' => ['name' => 'billing_bill_element_costobject', 'type' => 'string', 'internal' => 'costobject'], ]; /** @@ -78,8 +90,8 @@ final class BillElementMapper extends DataMapperFactory */ public const BELONGS_TO = [ 'bill' => [ - 'mapper' => BillMapper::class, - 'external' => 'billing_bill_element_bill', + 'mapper' => BillMapper::class, + 'external' => 'billing_bill_element_bill', ], ]; @@ -91,8 +103,20 @@ final class BillElementMapper extends DataMapperFactory */ public const OWNS_ONE = [ 'subscription' => [ - 'mapper' => SubscriptionMapper::class, - 'external' => 'billing_bill_element_subscription', + 'mapper' => SubscriptionMapper::class, + 'external' => 'billing_bill_element_subscription', + ], + 'item' => [ + 'mapper' => ItemMapper::class, + 'external' => 'billing_bill_element_item', + ], + 'container' => [ + 'mapper' => ContainerMapper::class, + 'external' => 'billing_bill_element_container', + ], + 'taxCombination' => [ + 'mapper' => TaxCombinationMapper::class, + 'external' => 'billing_bill_element_tax_combination', ], ]; diff --git a/Models/BillMapper.php b/Models/BillMapper.php index 5fd58f3..2d94804 100755 --- a/Models/BillMapper.php +++ b/Models/BillMapper.php @@ -45,55 +45,60 @@ class BillMapper extends DataMapperFactory * @since 1.0.0 */ public const COLUMNS = [ - '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_type' => ['name' => 'billing_bill_type', 'type' => 'int', 'internal' => 'type'], - 'billing_bill_template' => ['name' => 'billing_bill_template', 'type' => 'int', 'internal' => 'template'], - 'billing_bill_header' => ['name' => 'billing_bill_header', 'type' => 'string', 'internal' => 'header'], - 'billing_bill_footer' => ['name' => 'billing_bill_footer', 'type' => 'string', 'internal' => 'footer'], - 'billing_bill_info' => ['name' => 'billing_bill_info', 'type' => 'string', 'internal' => 'info'], - 'billing_bill_status' => ['name' => 'billing_bill_status', 'type' => 'int', 'internal' => 'status'], - 'billing_bill_paymentstatus' => ['name' => 'billing_bill_paymentstatus', 'type' => 'int', 'internal' => 'paymentStatus'], - 'billing_bill_shipTo' => ['name' => 'billing_bill_shipTo', 'type' => 'string', 'internal' => 'shipTo'], - 'billing_bill_shipFAO' => ['name' => 'billing_bill_shipFAO', 'type' => 'string', 'internal' => 'shipFAO'], - 'billing_bill_shipAddr' => ['name' => 'billing_bill_shipAddr', 'type' => 'string', 'internal' => 'shipAddress'], - 'billing_bill_shipCity' => ['name' => 'billing_bill_shipCity', 'type' => 'string', 'internal' => 'shipCity'], - 'billing_bill_shipZip' => ['name' => 'billing_bill_shipZip', 'type' => 'string', 'internal' => 'shipZip'], - 'billing_bill_shipCountry' => ['name' => 'billing_bill_shipCountry', 'type' => 'string', 'internal' => 'shipCountry'], - 'billing_bill_billTo' => ['name' => 'billing_bill_billTo', 'type' => 'string', 'internal' => 'billTo'], - 'billing_bill_billFAO' => ['name' => 'billing_bill_billFAO', 'type' => 'string', 'internal' => 'billFAO'], - 'billing_bill_billAddr' => ['name' => 'billing_bill_billAddr', 'type' => 'string', 'internal' => 'billAddress'], - 'billing_bill_billCity' => ['name' => 'billing_bill_billCity', 'type' => 'string', 'internal' => 'billCity'], - '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_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' => 'referralName'], - 'billing_bill_reference' => ['name' => 'billing_bill_reference', 'type' => 'int', 'internal' => 'reference'], - 'billing_bill_payment' => ['name' => 'billing_bill_payment', 'type' => 'int', 'internal' => 'payment'], - 'billing_bill_payment_text' => ['name' => 'billing_bill_payment_text', 'type' => 'string', 'internal' => 'paymentText'], - 'billing_bill_paymentterms' => ['name' => 'billing_bill_paymentterms', 'type' => 'int', 'internal' => 'terms'], - 'billing_bill_paymentterms_text' => ['name' => 'billing_bill_paymentterms_text', 'type' => 'string', 'internal' => 'termsText'], - 'billing_bill_ship_type' => ['name' => 'billing_bill_ship_type', 'type' => 'int', 'internal' => 'shipping'], - 'billing_bill_ship_text' => ['name' => 'billing_bill_ship_text', 'type' => 'string', 'internal' => 'shippingText'], - 'billing_bill_account_no' => ['name' => 'billing_bill_account_no', 'type' => 'string', 'internal' => 'accountNumber'], - 'billing_bill_client' => ['name' => 'billing_bill_client', 'type' => 'int', 'internal' => 'client'], - 'billing_bill_supplier' => ['name' => 'billing_bill_supplier', 'type' => 'int', 'internal' => 'supplier'], - 'billing_bill_created_by' => ['name' => 'billing_bill_created_by', 'type' => 'int', 'internal' => 'createdBy', 'readonly' => true], - 'billing_bill_date' => ['name' => 'billing_bill_date', 'type' => 'DateTime', 'internal' => 'billDate'], - 'billing_bill_performance_date' => ['name' => 'billing_bill_performance_date', 'type' => 'DateTime', 'internal' => 'performanceDate', 'readonly' => true], - 'billing_bill_created_at' => ['name' => 'billing_bill_created_at', 'type' => 'DateTimeImmutable', 'internal' => 'createdAt', 'readonly' => true], - 'billing_bill_unit' => ['name' => 'billing_bill_unit', 'type' => 'int', 'internal' => 'unit'], + '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'], + 'billing_bill_header' => ['name' => 'billing_bill_header', 'type' => 'string', 'internal' => 'header'], + 'billing_bill_footer' => ['name' => 'billing_bill_footer', 'type' => 'string', 'internal' => 'footer'], + 'billing_bill_info' => ['name' => 'billing_bill_info', 'type' => 'string', 'internal' => 'info'], + 'billing_bill_status' => ['name' => 'billing_bill_status', 'type' => 'int', 'internal' => 'status'], + 'billing_bill_paymentstatus' => ['name' => 'billing_bill_paymentstatus', 'type' => 'int', 'internal' => 'paymentStatus'], + 'billing_bill_shipTo' => ['name' => 'billing_bill_shipTo', 'type' => 'string', 'internal' => 'shipTo'], + 'billing_bill_shipFAO' => ['name' => 'billing_bill_shipFAO', 'type' => 'string', 'internal' => 'shipFAO'], + 'billing_bill_shipAddr' => ['name' => 'billing_bill_shipAddr', 'type' => 'string', 'internal' => 'shipAddress'], + 'billing_bill_shipCity' => ['name' => 'billing_bill_shipCity', 'type' => 'string', 'internal' => 'shipCity'], + 'billing_bill_shipZip' => ['name' => 'billing_bill_shipZip', 'type' => 'string', 'internal' => 'shipZip'], + 'billing_bill_shipCountry' => ['name' => 'billing_bill_shipCountry', 'type' => 'string', 'internal' => 'shipCountry'], + 'billing_bill_billTo' => ['name' => 'billing_bill_billTo', 'type' => 'string', 'internal' => 'billTo'], + 'billing_bill_billFAO' => ['name' => 'billing_bill_billFAO', 'type' => 'string', 'internal' => 'billFAO'], + 'billing_bill_billAddr' => ['name' => 'billing_bill_billAddr', 'type' => 'string', 'internal' => 'billAddress'], + 'billing_bill_billCity' => ['name' => 'billing_bill_billCity', 'type' => 'string', 'internal' => 'billCity'], + '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_netcosts' => ['name' => 'billing_bill_netcosts', 'type' => 'Serializable', 'internal' => 'netCosts'], + '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_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_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'], + 'billing_bill_accgroup' => ['name' => 'billing_bill_accgroup', 'type' => 'int', 'internal' => 'accGroup'], + 'billing_bill_acctype' => ['name' => 'billing_bill_acctype', 'type' => 'int', 'internal' => 'accType'], + 'billing_bill_payment' => ['name' => 'billing_bill_payment', 'type' => 'int', 'internal' => 'payment'], + 'billing_bill_payment_text' => ['name' => 'billing_bill_payment_text', 'type' => 'string', 'internal' => 'paymentText'], + 'billing_bill_paymentterms' => ['name' => 'billing_bill_paymentterms', 'type' => 'int', 'internal' => 'paymentTerms'], + 'billing_bill_paymentterms_text' => ['name' => 'billing_bill_paymentterms_text', 'type' => 'string', 'internal' => 'termsText'], + 'billing_bill_ship_type' => ['name' => 'billing_bill_ship_type', 'type' => 'int', 'internal' => 'shippingTerms'], + 'billing_bill_ship_text' => ['name' => 'billing_bill_ship_text', 'type' => 'string', 'internal' => 'shippingText'], + 'billing_bill_account_no' => ['name' => 'billing_bill_account_no', 'type' => 'string', 'internal' => 'accountNumber'], + 'billing_bill_tax_type' => ['name' => 'billing_bill_tax_type', 'type' => 'int', 'internal' => 'accTaxCode'], + 'billing_bill_client' => ['name' => 'billing_bill_client', 'type' => 'int', 'internal' => 'client'], + 'billing_bill_supplier' => ['name' => 'billing_bill_supplier', 'type' => 'int', 'internal' => 'supplier'], + 'billing_bill_created_by' => ['name' => 'billing_bill_created_by', 'type' => 'int', 'internal' => 'createdBy', 'readonly' => true], + 'billing_bill_date' => ['name' => 'billing_bill_date', 'type' => 'DateTime', 'internal' => 'billDate'], + 'billing_bill_performance_date' => ['name' => 'billing_bill_performance_date', 'type' => 'DateTime', 'internal' => 'performanceDate', 'readonly' => true], + 'billing_bill_created_at' => ['name' => 'billing_bill_created_at', 'type' => 'DateTimeImmutable', 'internal' => 'createdAt', 'readonly' => true], + 'billing_bill_unit' => ['name' => 'billing_bill_unit', 'type' => 'int', 'internal' => 'unit'], ]; /** @@ -104,12 +109,12 @@ class BillMapper extends DataMapperFactory */ public const HAS_MANY = [ 'elements' => [ - 'mapper' => BillElementMapper::class, - 'table' => 'billing_bill_element', - 'self' => 'billing_bill_element_bill', - 'external' => null, + 'mapper' => BillElementMapper::class, + 'table' => 'billing_bill_element', + 'self' => 'billing_bill_element_bill', + 'external' => null, ], - 'files' => [ + 'files' => [ 'mapper' => MediaMapper::class, 'table' => 'billing_bill_media', 'external' => 'billing_bill_media_dst', @@ -130,17 +135,17 @@ class BillMapper extends DataMapperFactory * @since 1.0.0 */ public const OWNS_ONE = [ - 'type' => [ - 'mapper' => BillTypeMapper::class, - 'external' => 'billing_bill_type', + 'type' => [ + 'mapper' => BillTypeMapper::class, + 'external' => 'billing_bill_type', ], - 'referral' => [ - 'mapper' => AccountMapper::class, - 'external' => 'billing_bill_referral', + 'referral' => [ + 'mapper' => AccountMapper::class, + 'external' => 'billing_bill_referral', ], - 'template' => [ - 'mapper' => CollectionMapper::class, - 'external' => 'billing_bill_template', + 'template' => [ + 'mapper' => CollectionMapper::class, + 'external' => 'billing_bill_template', ], ]; @@ -152,16 +157,16 @@ class BillMapper extends DataMapperFactory */ public const BELONGS_TO = [ 'createdBy' => [ - 'mapper' => AccountMapper::class, - 'external' => 'billing_bill_created_by', + 'mapper' => AccountMapper::class, + 'external' => 'billing_bill_created_by', ], 'client' => [ - 'mapper' => ClientMapper::class, - 'external' => 'billing_bill_client', + 'mapper' => ClientMapper::class, + 'external' => 'billing_bill_client', ], 'supplier' => [ - 'mapper' => SupplierMapper::class, - 'external' => 'billing_bill_supplier', + 'mapper' => SupplierMapper::class, + 'external' => 'billing_bill_supplier', ], 'attributes' => [ 'mapper' => BillAttributeMapper::class, diff --git a/Models/BillTransferType.php b/Models/BillTransferType.php index c47b4a8..9b7bd59 100755 --- a/Models/BillTransferType.php +++ b/Models/BillTransferType.php @@ -30,5 +30,7 @@ abstract class BillTransferType extends Enum public const PURCHASE = 2; + public const PRODUCTION = 3; + public const STOCK = 4; } diff --git a/Models/BillType.php b/Models/BillType.php index 10e0574..8bfab10 100755 --- a/Models/BillType.php +++ b/Models/BillType.php @@ -50,8 +50,12 @@ class BillType implements \JsonSerializable public bool $transferStock = true; + public bool $isAccounting = false; + public int $sign = 1; + public bool $email = false; + /** * Localization * @@ -88,13 +92,13 @@ class BillType implements \JsonSerializable if ($l11n instanceof BaseStringL11n) { $this->l11n = $l11n; } elseif (isset($this->l11n) && $this->l11n instanceof BaseStringL11n) { - $this->l11n->content = $l11n; - $this->l11n->setLanguage($lang); + $this->l11n->content = $l11n; + $this->l11n->language = $lang; } else { - $this->l11n = new BaseStringL11n(); - $this->l11n->content = $l11n; - $this->l11n->ref = $this->id; - $this->l11n->setLanguage($lang); + $this->l11n = new BaseStringL11n(); + $this->l11n->content = $l11n; + $this->l11n->ref = $this->id; + $this->l11n->language = $lang; } } @@ -144,9 +148,9 @@ class BillType implements \JsonSerializable public function toArray() : array { return [ - 'id' => $this->id, - 'numberFormat' => $this->numberFormat, - 'transferType' => $this->transferType, + 'id' => $this->id, + 'numberFormat' => $this->numberFormat, + 'transferType' => $this->transferType, ]; } diff --git a/Models/BillTypeL11nMapper.php b/Models/BillTypeL11nMapper.php index 635d43e..be5b1d6 100755 --- a/Models/BillTypeL11nMapper.php +++ b/Models/BillTypeL11nMapper.php @@ -37,10 +37,10 @@ final class BillTypeL11nMapper extends DataMapperFactory * @since 1.0.0 */ public const COLUMNS = [ - 'billing_type_l11n_id' => ['name' => 'billing_type_l11n_id', 'type' => 'int', 'internal' => 'id'], - 'billing_type_l11n_name' => ['name' => 'billing_type_l11n_name', 'type' => 'string', 'internal' => 'content', 'autocomplete' => true], - 'billing_type_l11n_type' => ['name' => 'billing_type_l11n_type', 'type' => 'int', 'internal' => 'ref'], - 'billing_type_l11n_language' => ['name' => 'billing_type_l11n_language', 'type' => 'string', 'internal' => 'language'], + 'billing_type_l11n_id' => ['name' => 'billing_type_l11n_id', 'type' => 'int', 'internal' => 'id'], + 'billing_type_l11n_name' => ['name' => 'billing_type_l11n_name', 'type' => 'string', 'internal' => 'content', 'autocomplete' => true], + 'billing_type_l11n_type' => ['name' => 'billing_type_l11n_type', 'type' => 'int', 'internal' => 'ref'], + 'billing_type_l11n_language' => ['name' => 'billing_type_l11n_language', 'type' => 'string', 'internal' => 'language'], ]; /** diff --git a/Models/BillTypeMapper.php b/Models/BillTypeMapper.php index c147cbc..d3cccfe 100755 --- a/Models/BillTypeMapper.php +++ b/Models/BillTypeMapper.php @@ -37,15 +37,17 @@ final class BillTypeMapper extends DataMapperFactory * @since 1.0.0 */ public const COLUMNS = [ - 'billing_type_id' => ['name' => 'billing_type_id', 'type' => 'int', 'internal' => 'id'], - 'billing_type_name' => ['name' => 'billing_type_name', 'type' => 'string', 'internal' => 'name'], - 'billing_type_number_format' => ['name' => 'billing_type_number_format', 'type' => 'string', 'internal' => 'numberFormat'], - 'billing_type_account_format' => ['name' => 'billing_type_account_format', 'type' => 'string', 'internal' => 'accountFormat'], - 'billing_type_transfer_type' => ['name' => 'billing_type_transfer_type', 'type' => 'int', 'internal' => 'transferType'], - 'billing_type_default_template' => ['name' => 'billing_type_default_template', 'type' => 'int', 'internal' => 'defaultTemplate'], - 'billing_type_transfer_stock' => ['name' => 'billing_type_transfer_stock', 'type' => 'bool', 'internal' => 'transferStock'], - 'billing_type_transfer_sign' => ['name' => 'billing_type_transfer_sign', 'type' => 'int', 'internal' => 'sign'], - 'billing_type_is_template' => ['name' => 'billing_type_is_template', 'type' => 'bool', 'internal' => 'isTemplate'], + 'billing_type_id' => ['name' => 'billing_type_id', 'type' => 'int', 'internal' => 'id'], + 'billing_type_name' => ['name' => 'billing_type_name', 'type' => 'string', 'internal' => 'name'], + 'billing_type_number_format' => ['name' => 'billing_type_number_format', 'type' => 'string', 'internal' => 'numberFormat'], + 'billing_type_account_format' => ['name' => 'billing_type_account_format', 'type' => 'string', 'internal' => 'accountFormat'], + 'billing_type_transfer_type' => ['name' => 'billing_type_transfer_type', 'type' => 'int', 'internal' => 'transferType'], + 'billing_type_default_template' => ['name' => 'billing_type_default_template', 'type' => 'int', 'internal' => 'defaultTemplate'], + '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'], ]; /** @@ -56,13 +58,13 @@ final class BillTypeMapper extends DataMapperFactory */ public const HAS_MANY = [ 'l11n' => [ - 'mapper' => BillTypeL11nMapper::class, - 'table' => 'billing_type_l11n', - 'self' => 'billing_type_l11n_type', - 'column' => 'content', - 'external' => null, + 'mapper' => BillTypeL11nMapper::class, + 'table' => 'billing_type_l11n', + 'self' => 'billing_type_l11n_type', + 'column' => 'content', + 'external' => null, ], - 'templates' => [ + 'templates' => [ 'mapper' => CollectionMapper::class, 'table' => 'billing_bill_type_media_rel', 'external' => 'billing_bill_type_media_rel_dst', @@ -77,9 +79,9 @@ final class BillTypeMapper extends DataMapperFactory * @since 1.0.0 */ public const OWNS_ONE = [ - 'defaultTemplate' => [ - 'mapper' => CollectionMapper::class, - 'external' => 'billing_type_default_template', + 'defaultTemplate' => [ + 'mapper' => CollectionMapper::class, + 'external' => 'billing_type_default_template', ], ]; diff --git a/Models/InvoiceRecognition.php b/Models/InvoiceRecognition.php new file mode 100644 index 0000000..74c11be --- /dev/null +++ b/Models/InvoiceRecognition.php @@ -0,0 +1,1351 @@ + $value) { + if (empty(\trim($value))) { + unset($lines[$line]); + } + } + + $lines = \array_values($lines); + + $language = self::detectLanguage($content); + if (!\in_array($language, ['en', 'de'])) { + $language = 'en'; + } + + $bill->language = $language; + + $l11n = Localization::fromLanguage($language); + + $identifierContent = \file_get_contents(__DIR__ . '/bill_identifier.json'); + if ($identifierContent === false) { + $identifierContent = '{}'; + } + + /** @var array $identifiers */ + $identifiers = \json_decode($identifierContent, true); + + $bill->billCountry = self::findCountry($lines, $identifiers, $language); + + $currency = self::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 = self::findSupplierInvoiceType($content, $identifiers['type'], $language); + + /* + @var \Modules\Billing\Models\BillType $billType + $billType = BillTypeMapper::get() + ->where('name', $type) + ->execute(); + + $bill->type = new NullBillType($billType->id); + */ + + /* Number */ + $billNumber = self::findBillNumber($lines, $identifiers['bill_no'][$language]); + $bill->external = $billNumber; + + /* Reference / PO */ + // @todo implement + + /* Date */ + $billDateTemp = self::findBillDate($lines, $identifiers['bill_date'][$language]); + $billDate = self::parseDate($billDateTemp, $identifiers['date_format']); + + $bill->billDate = $billDate; + + /* Due */ + $billDueTemp = self::findBillDue($lines, $identifiers['bill_due'][$language]); + $billDue = self::parseDate($billDueTemp, $identifiers['date_format']); + // @todo implement multiple due dates for bills + + /* Total */ + $totalGross = self::findBillGross($lines, $identifiers['total_gross'][$language]); + $totalNet = self::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 */ + // @todo taxes depend on local tax id (if company in Germany but invoice from US -> only gross amount important, there is no net) + $totalTaxAmount = self::findBillTaxAmount($lines, $identifiers['total_tax'][$language]); + $taxRates = self::findBillTaxRates($lines, $identifiers['tax_rate'][$language]); + + 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 = self::findBillItemLines($lines, $identifiers['item_table'][$language]); + + // @todo Try to find item from item database + 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; + } + + /* 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 = self::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; + + $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; + } + } + + 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($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; + } + + // 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; + } + + /** + * Detect language from content + * + * @param string $content String to analyze + * + * @return string + * + * @since 1.0.0 + */ + public static function detectLanguage(string $content) : string + { + $detector = new Language(); + $language = $detector->detect($content)->bestResults()->close(); + + if (!\is_array($language) || empty($language)) { + return 'en'; + } + + return \substr(\array_keys($language)[0], 0, 2); + } + + /** + * Detect the supplier bill type + * + * @param string $content String to analyze + * @param array $types Possible bill types + * @param string $language Bill language + * + * @return string + * + * @since 1.0.0 + */ + public static function findSupplierInvoiceType(string $content, array $types, string $language) : string + { + $bestPos = \strlen($content); + $bestMatch = ''; + + foreach ($types as $name => $type) { + foreach ($type[$language] as $l11n) { + $found = \stripos($content, \strtolower($l11n)); + + if ($found !== false && $found < $bestPos) { + $bestPos = $found; + $bestMatch = $name; + } + } + } + + 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 + */ + public static 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 = $found['bill_no']; + } + + break; + } + } + } + + return \trim($bestMatch); + } + + /** + * Detect the supplier bill due date + * + * @param string[] $lines Bill lines + * @param array $matches Due match patterns + * + * @return string + * + * @since 1.0.0 + */ + public static 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 = $found['bill_due']; + } + + break; + } + } + } + + return \trim($bestMatch); + } + + /** + * Detect the supplier bill date + * + * @param string[] $lines Bill lines + * @param array $matches Date match patterns + * + * @return string + * + * @since 1.0.0 + */ + public static 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 = $found['bill_date']; + } + + break; + } + } + } + + return \trim($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% + */ + public static 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 + : FloatInt::DIVISOR); + + if ($gross > $bestMatch) { + $bestMatch = $gross; + } + } + } + } + + 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% + */ + public static function findBillTaxRates(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['tax_rate']); + + $posD = \stripos($temp, '.'); + $posK = \stripos($temp, ','); + + $hasDecimal = ($posD !== false || $posK !== false) + && \max((int) $posD, (int) $posK) + 3 >= \strlen($temp); + + $rate = ((int) \str_replace(['.', ','], ['', ''], $temp)) * ($hasDecimal + ? 100 + : FloatInt::DIVISOR); + + if ($rate > $bestMatch) { + $bestMatch = $rate; + } + } + } + } + + return $bestMatch; + } + + /** + * Detect the supplier bill gross amount + * + * @param string[] $lines Bill lines + * @param array $matches Net match patterns + * + * @return string + * + * @bug Issue with net/discount/gross in one line + * + * @since 1.0.0 + * @todo maybe check with taxes + * @todo maybe make sure text position is before total_gross + */ + public static function findBillNet(array $lines, array $matches) : string + { + $bestMatch = 0; + $bestMatchStr = ''; + + $found = []; + + foreach ($matches as $match) { + foreach ($lines as $line) { + if (\preg_match($match, $line, $found) === 1 + && \preg_match('/[,.]{1,1}[\d]{4}$/', $found['total_net']) !== 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); + + $net = ((int) \str_replace(['.', ','], ['', ''], $temp)) * ($hasDecimal + ? 100 + : FloatInt::DIVISOR); + + if ($net > $bestMatch) { + $bestMatch = $net; + $bestMatchStr = $temp; + } + } + } + } + + return $bestMatchStr; + } + + /** + * Detect the supplier bill gross amount + * + * @param string[] $lines Bill lines + * @param array $matches Gross match patterns + * + * @return string + * + * @bug Issue with net/discount/gross in one line + * + * @since 1.0.0 + */ + public static function findBillGross(array $lines, array $matches) : string + { + $bestMatch = 0; + $bestMatchStr = ''; + + $found = []; + + foreach ($matches as $match) { + foreach ($lines as $line) { + if (\preg_match($match, $line, $found) === 1 + && \preg_match('/[,.]{1,1}[\d]{4}$/', $found['total_gross']) !== 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 + : FloatInt::DIVISOR); + + if ($gross > $bestMatch) { + $bestMatch = $gross; + $bestMatchStr = $temp; + } + } + } + } + + return $bestMatchStr; + } + + /** + * Detect the supplier bill gross amount + * + * @param string[] $lines Bill lines + * @param array $matches Gross match patterns + * + * @return array + * + * @bug Issue with net/discount/gross in one line + * + * @since 1.0.0 + */ + public static function findBillSpecial(array $lines, array $matches, string $language, int $lineStart) : array + { + // Find discounts + $bestDiscount = 0; + $found = []; + + foreach ($matches['total_discount'][$language] as $match) { + foreach ($lines as $idx => $line) { + if ($idx < $lineStart) { + continue; + } + + if (\preg_match($match, $line, $found) === 1) { + $temp = \trim($found['total_discount']); + + $posD = \stripos($temp, '.'); + $posK = \stripos($temp, ','); + + $hasDecimal = ($posD !== false || $posK !== false) + && \max((int) $posD, (int) $posK) + 3 >= \strlen($temp); + + $discount = ((int) \str_replace(['.', ','], ['', ''], $temp)) * ($hasDecimal + ? 100 + : FloatInt::DIVISOR); + + $discount = \abs($discount); + + if ($discount > $bestDiscount) { + $bestDiscount = $discount; + $discountLine = $idx; + + break; + } + } + } + } + + // Find shipping + $bestShipping = 0; + $found = []; + + $shippingLine = 0; + + foreach ($matches['total_shipping'][$language] as $match) { + foreach ($lines as $idx => $line) { + if ($idx < $lineStart) { + continue; + } + + if (\preg_match($match, $line, $found) === 1) { + $temp = \trim($found['total_shipping']); + + $posD = \stripos($temp, '.'); + $posK = \stripos($temp, ','); + + $hasDecimal = ($posD !== false || $posK !== false) + && \max((int) $posD, (int) $posK) + 3 >= \strlen($temp); + + $shipping = ((int) \str_replace(['.', ','], ['', ''], $temp)) * ($hasDecimal + ? 100 + : FloatInt::DIVISOR); + + if ($shipping > $bestShipping) { + $bestShipping = $shipping; + $shippingLine = $idx; + + break; + } + } + } + } + + // Find customs + $bestCustoms = 0; + $found = []; + + $customsLine = 0; + + foreach ($matches['total_customs'][$language] as $match) { + foreach ($lines as $idx => $line) { + if ($idx < $lineStart) { + continue; + } + + if (\preg_match($match, $line, $found) === 1) { + $temp = \trim($found['total_customs']); + + $posD = \stripos($temp, '.'); + $posK = \stripos($temp, ','); + + $hasDecimal = ($posD !== false || $posK !== false) + && \max((int) $posD, (int) $posK) + 3 >= \strlen($temp); + + $customs = ((int) \str_replace(['.', ','], ['', ''], $temp)) * ($hasDecimal + ? 100 + : FloatInt::DIVISOR); + + if ($customs > $bestCustoms) { + $bestCustoms = $customs; + $customsLine = $idx; + + break; + } + } + } + } + + // Find insurance + $bestInsurance = 0; + $found = []; + + $insuranceLine = 0; + + foreach ($matches['total_insurance'][$language] as $match) { + foreach ($lines as $idx => $line) { + if ($idx < $lineStart) { + continue; + } + + if (\preg_match($match, $line, $found) === 1) { + $temp = \trim($found['total_insurance']); + + $posD = \stripos($temp, '.'); + $posK = \stripos($temp, ','); + + $hasDecimal = ($posD !== false || $posK !== false) + && \max((int) $posD, (int) $posK) + 3 >= \strlen($temp); + + $insurance = ((int) \str_replace(['.', ','], ['', ''], $temp)) * ($hasDecimal + ? 100 + : FloatInt::DIVISOR); + + if ($insurance > $bestInsurance) { + $bestInsurance = $insurance; + $insuranceLine = $idx; + + break; + } + } + } + } + + // Find surcharge + $bestSurcharge = 0; + $found = []; + + foreach ($matches['total_surcharge'][$language] as $match) { + foreach ($lines as $idx => $line) { + if ($idx < $lineStart) { + continue; + } + + if (\preg_match($match, $line, $found) === 1 + && $idx !== $shippingLine + && $idx !== $customsLine + && $idx !== $insuranceLine + ) { + $temp = \trim($found['total_surcharge']); + + $posD = \stripos($temp, '.'); + $posK = \stripos($temp, ','); + + $hasDecimal = ($posD !== false || $posK !== false) + && \max((int) $posD, (int) $posK) + 3 >= \strlen($temp); + + $surcharge = ((int) \str_replace(['.', ','], ['', ''], $temp)) * ($hasDecimal + ? 100 + : FloatInt::DIVISOR); + + if ($surcharge > $bestSurcharge) { + $bestSurcharge = $surcharge; + + break; + } + } + } + } + + return [ + 'total_discount' => -1 * $bestDiscount, + 'total_shipping' => $bestShipping, + 'total_customs' => $bestCustoms, + 'total_insurance' => $bestInsurance, + 'total_surcharge' => $bestSurcharge, + ]; + } + + /** + * Detect the supplier bill gross amount + * + * @param string[] $lines Bill lines + * @param array $matches Item lines match patterns + * + * @return array + * + * @since 1.0.0 + */ + public static 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 []; + } + + // Find end of item lines + $line = $lines[$startLine]; + + // Get headline structure = item list structure + $headlineStructure = []; + foreach ($matches['headline'] as $type => $match) { + foreach ($match as $headline) { + // We have to make sure that there are + if (\preg_match('/(\s{1,}' . $headline . '|' . $headline . '\s{1,})/', $line) === 1) { + $headlineStructure[$type] = true; + + continue; + } + } + } + + \asort($headlineStructure); + + $rows = []; + + // Get item list until end of item list/table is reached + $found = []; + $structureCount = \count($headlineStructure); + $linesSkipped = 0; + + foreach ($lines as $l => $line) { + // @todo find better way to identify end of item table + // @bug find way to handle multiple pages + // @bug find way to handle multi-line item description + if ($l <= $startLine) { + continue; + } + + if ($linesSkipped > 2) { + break; + } + + if (\preg_match_all($matches['parts'], $line, $found) !== $structureCount) { + ++$linesSkipped; + continue; + } + + $linesSkipped = 0; + + $temp = []; + $c = 0; + foreach ($headlineStructure as $idx => $_) { + $subFound = []; + + $temp[$idx] = \preg_match($matches['row'][$idx], $found[2][$c], $subFound) === 1 + ? $subFound[0] + : ''; + + ++$c; + } + + $rows[$l] = $temp; + } + + return $rows; + } + + /** + * Create DateTime from date string + * + * @param string $date Date string + * @param string[] $formats Date formats + * + * @return null|\DateTime + * + * @since 1.0.0 + */ + public static function parseDate(string $date, array $formats, string $supplierFormat = '') : ?\DateTime + { + if ((!empty($supplierFormat))) { + $dt = \DateTime::createFromFormat( + $supplierFormat, + $date + ); + + return $dt === false ? new \DateTime('1970-01-01') : $dt; + } + + $now = new \DateTime('now'); + $bestMatch = null; + + foreach ($formats as $format) { + if (($obj = \DateTime::createFromFormat($format, $date)) !== false) { + if (\abs($obj->getTimestamp() - $now->getTimestamp()) < 60 * 60 * 24 * 365 * 10) { + // The estimated date should be within 10 years + return $obj; + } + + $bestMatch = $obj; + } + } + + return $bestMatch; + } + + /** + * Detect the supplier bill number + * + * @param string[] $lines Bill lines + * @param array $matches Number match patterns + * + * @return string + * + * @since 1.0.0 + */ + public static function findEmail(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 = $found['email']; + } + + break; + } + } + } + + return \trim($bestMatch); + } + + /** + * Detect the supplier bill number + * + * @param string[] $lines Bill lines + * @param array $matches Number match patterns + * + * @return string + * + * @since 1.0.0 + */ + public static function findPhone(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 = $found['phone']; + } + + break; + } + } + } + + return \trim($bestMatch); + } + + /** + * Detect the supplier bill number + * + * @param string[] $lines Bill lines + * @param array $matches Number match patterns + * + * @return string + * + * @since 1.0.0 + */ + public static function findWebsite(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 = $found['website']; + } + + break; + } + } + } + + return \trim($bestMatch); + } + + /** + * Detect the supplier bill number + * + * @param string[] $lines Bill lines + * @param array $matches Number match patterns + * + * @return string + * + * @since 1.0.0 + */ + public static function findVat(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 = $found['vat_id']; + } + + break; + } + } + } + + if (\stripos($bestMatch, 'S') > 1 + || \stripos($bestMatch, 'O') > 1 + ) { + $subIban = \substr($bestMatch, 2); + $subIban = \str_replace(['S', 'O'], ['5', '0'], $subIban); + $bestMatch = \substr($bestMatch, 0, 2) . $subIban; + } + + return \str_replace([' ', '-'], '', \strtoupper(\trim($bestMatch))); + } + + /** + * Detect the supplier bill number + * + * @param string[] $lines Bill lines + * @param array $matches Number match patterns + * + * @return string + * + * @since 1.0.0 + */ + public static function findTaxId(array $lines, array $matches) : string + { + $bestPos = \count($lines); + $bestMatch = ''; + + $found = []; + + // @performance A lot of these loops (see other functions as well) can be optimized + // Go over the lines first this way we stop the loop much earlier. + foreach ($matches as $match) { + foreach ($lines as $row => $line) { + if (\preg_match($match, $line, $found) === 1) { + if ($row < $bestPos) { + $bestPos = $row; + $bestMatch = $found['tax_id']; + } + + // Break 2 is required because here we also support searching for VAT ID. + // We do this because some software may use the identifiers for VAT and Tax id interchangeably + // The highest priority $match use the actual identifier and afterwards the other identifiers follow. + break 2; + } + } + } + + return \trim($bestMatch); + } + + /** + * Detect the supplier bill number + * + * @param string[] $lines Bill lines + * @param array $matches Number match patterns + * + * @return string + * + * @since 1.0.0 + */ + public static function findIban(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 = $found['iban']; + } + + break; + } + } + } + + $bestMatch = \trim(\strtoupper($bestMatch)); + $bestMatch = \str_replace(' ', '', $bestMatch); + $bestMatch = \wordwrap($bestMatch, 4, ' ', true); + + // Trying to fix bad parsing + if (\stripos($bestMatch, 'S') > 1 + || \stripos($bestMatch, 'O') > 1 + ) { + /** @var string $format */ + $format = IbanEnum::getByName('_' . \substr($bestMatch, 0, 2)) ?? ''; + + $len = \strlen($bestMatch); + $formatLen = \strlen($format); + + for ($i = 0; $i < $len; ++$i) { + if ($i >= $formatLen) { + break; + } + + if ($format[$i] !== 'k' && $format[$i] !== 'n') { + continue; + } + + if ($bestMatch[$i] === 'O' + || $bestMatch[$i] === 'o' + ) { + $bestMatch[$i] = '0'; + } elseif ($bestMatch[$i] === 'S' + || $bestMatch[$i] === 's' + ) { + $bestMatch[$i] = '5'; + } + } + } + + return \trim($bestMatch); + } + + /** + * Find country from bill + * + * @param string[] $lines Lines + * @param array $matches Match patterns + * @param string $language Bill language + */ + public static function findCountry(array $lines, array $matches, string $language) : string + { + $iban = self::findIban($lines, $matches['iban']); + if (\phpOMS\Validation\Finance\Iban::isValid($iban)) { + $obj = new Iban($iban); + + if (ISO3166TwoEnum::isValidValue($obj->getCountry())) { + return \strtoupper($obj->getCountry()); + } + } + + $vatId = self::findVat($lines, $matches['vat_id'][$language]); + if (EUVat::isValid($vatId)) { + return \strtoupper(\substr($vatId, 0, 2)); + } + + $email = self::findEmail($lines, $matches['email']); + $country = \strtoupper(\substr($email, \strrpos($email, '.') + 1)); + + if (ISO3166TwoEnum::isValidValue($country)) { + return \strtoupper($country); + } + + $website = self::findWebsite($lines, $matches['website']); + $country = \strtoupper(\substr($website, \strrpos($website, '.') + 1)); + + if (ISO3166TwoEnum::isValidValue($country)) { + return \strtoupper($country); + } + + $countries = ISO3166TwoEnum::countryFromLanguage($language); + + return empty($countries) ? 'US' : \reset($countries); + } + + /** + * Find currency + * + * @param string[] $lines Lines + * + * @return string + * + * @since 1.0.0 + */ + public static function findCurrency(array $lines) : string + { + $symbols = ISO4217SymbolEnum::getConstants(); + $currency = ''; + + foreach ($lines as $line) { + foreach ($symbols as $symbol) { + $match = $symbol; + if (\preg_match('/[\x20-\x7e]/', $symbol) === 1) { + $match = ' ' . $symbol . ' '; + } + + if (\strpos($line, $match) !== false) { + /** @var string $currency */ + $currency = ISO4217SymbolEnum::getName($symbol); + + /** @var string $currency */ + $currency = ISO4217CharEnum::getByName($currency) ?? ''; + + break; + } + } + } + + if (!empty($currency)) { + return $currency; + } + + $symbols = ISO4217CharEnum::getConstants(); + + foreach ($lines as $line) { + foreach ($symbols as $symbol) { + if (\strpos($line, ' ' . $symbol . ' ') !== false) { + $currency = $symbol; + + break; + } + } + } + + return $currency; + } +} diff --git a/Models/PaymentTermL11nMapper.php b/Models/PaymentTermL11nMapper.php new file mode 100644 index 0000000..3510b12 --- /dev/null +++ b/Models/PaymentTermL11nMapper.php @@ -0,0 +1,69 @@ + + */ +final class PaymentTermL11nMapper extends DataMapperFactory +{ + /** + * Columns. + * + * @var array + * @since 1.0.0 + */ + public const COLUMNS = [ + 'billing_payment_term_l11n_id' => ['name' => 'billing_payment_term_l11n_id', 'type' => 'int', 'internal' => 'id'], + 'billing_payment_term_l11n_name' => ['name' => 'billing_payment_term_l11n_name', 'type' => 'string', 'internal' => 'content', 'autocomplete' => true], + 'billing_payment_term_l11n_term' => ['name' => 'billing_payment_term_l11n_term', 'type' => 'int', 'internal' => 'ref'], + 'billing_payment_term_l11n_language' => ['name' => 'billing_payment_term_l11n_language', 'type' => 'string', 'internal' => 'language'], + ]; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'billing_payment_term_l11n'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'billing_payment_term_l11n_id'; + + /** + * Model to use by the mapper. + * + * @var class-string + * @since 1.0.0 + */ + public const MODEL = BaseStringL11n::class; +} diff --git a/Models/PaymentTermMapper.php b/Models/PaymentTermMapper.php new file mode 100644 index 0000000..a8fcce5 --- /dev/null +++ b/Models/PaymentTermMapper.php @@ -0,0 +1,83 @@ + + */ +final class PaymentTermMapper extends DataMapperFactory +{ + /** + * Columns. + * + * @var array + * @since 1.0.0 + */ + public const COLUMNS = [ + 'billing_payment_term_id' => ['name' => 'billing_payment_term_id', 'type' => 'int', 'internal' => 'id'], + 'billing_payment_term_code' => ['name' => 'billing_payment_term_code', 'type' => 'string', 'internal' => 'title', 'autocomplete' => true], + ]; + + /** + * Has many relation. + * + * @var array + * @since 1.0.0 + */ + public const HAS_MANY = [ + 'l11n' => [ + 'mapper' => PaymentTermL11nMapper::class, + 'table' => 'billing_payment_term_l11n', + 'self' => 'billing_payment_term_l11n_term', + 'column' => 'content', + 'external' => null, + ], + ]; + + /** + * Model to use by the mapper. + * + * @var class-string + * @since 1.0.0 + */ + public const MODEL = BaseStringL11nType::class; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'billing_payment_term'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'billing_payment_term_id'; +} diff --git a/Models/PermissionCategory.php b/Models/PermissionCategory.php index 57498ec..3e1458c 100755 --- a/Models/PermissionCategory.php +++ b/Models/PermissionCategory.php @@ -17,7 +17,7 @@ namespace Modules\Billing\Models; use phpOMS\Stdlib\Base\Enum; /** - * Permision state enum. + * Permission category enum. * * @package Modules\Billing\Models * @license OMS License 2.0 @@ -35,4 +35,14 @@ abstract class PermissionCategory extends Enum public const PRIVATE_DASHBOARD = 5; public const PRIVATE_BILL_UPLOAD = 6; + + public const BILL_NOTE = 7; + + public const PRICE = 8; + + public const PAYMENT_TERM = 9; + + public const SHIPPING_TERM = 10; + + public const BILL_LOG = 101; } diff --git a/Models/Price/Price.php b/Models/Price/Price.php index 94f97b7..023a524 100755 --- a/Models/Price/Price.php +++ b/Models/Price/Price.php @@ -32,6 +32,12 @@ use phpOMS\Stdlib\Base\FloatInt; * @license OMS License 2.0 * @link https://jingga.app * @since 1.0.0 + * + * @todo Find a way to handle references to the total invoice amount and other items + * Example: If total invoice > $X no shipping expenses + * Maybe additional column referencing total value + * Example: If item Y quantity > Z no costs for item A (e.g. service fee) + * Maybe by referencing another price (i.e. if other price triggered than this is triggered as well) */ class Price implements \JsonSerializable { @@ -49,7 +55,11 @@ class Price implements \JsonSerializable public Item $item; - public AttributeValue $itemgroup; + public int $status = PriceStatus::ACTIVE; + + public AttributeValue $itemsalesgroup; + + public AttributeValue $itemproductgroup; public AttributeValue $itemsegment; @@ -75,20 +85,22 @@ class Price implements \JsonSerializable public int $type = PriceType::SALES; - public int $quantity = 0; + public FloatInt $quantity; public FloatInt $price; - public int $priceNew = 0; + public FloatInt $priceNew; - public int $discount = 0; + public FloatInt $discount; - public int $discountPercentage = 0; + public FloatInt $discountPercentage; - public int $bonus = 0; + public FloatInt $bonus; public bool $multiply = false; + public bool $isAdditive = false; + public string $currency = ISO4217CharEnum::_EUR; public ?\DateTime $start = null; @@ -102,11 +114,12 @@ class Price implements \JsonSerializable */ public function __construct() { - $this->item = new NullItem(); - $this->itemgroup = new NullAttributeValue(); - $this->itemsegment = new NullAttributeValue(); - $this->itemsection = new NullAttributeValue(); - $this->itemtype = new NullAttributeValue(); + $this->item = new NullItem(); + $this->itemsalesgroup = new NullAttributeValue(); + $this->itemproductgroup = new NullAttributeValue(); + $this->itemsegment = new NullAttributeValue(); + $this->itemsection = new NullAttributeValue(); + $this->itemtype = new NullAttributeValue(); $this->client = new NullClient(); $this->clientgroup = new NullAttributeValue(); @@ -116,19 +129,12 @@ class Price implements \JsonSerializable $this->supplier = new NullSupplier(); - $this->price = new FloatInt(); - } - - /** - * Get id. - * - * @return int Model id - * - * @since 1.0.0 - */ - public function getId() : int - { - return $this->id; + $this->price = new FloatInt(); + $this->quantity = new FloatInt(); + $this->priceNew = new FloatInt(); + $this->discount = new FloatInt(); + $this->discountPercentage = new FloatInt(); + $this->bonus = new FloatInt(); } /** diff --git a/Models/Price/PriceMapper.php b/Models/Price/PriceMapper.php index 595ad82..6d15044 100755 --- a/Models/Price/PriceMapper.php +++ b/Models/Price/PriceMapper.php @@ -42,33 +42,35 @@ final class PriceMapper extends DataMapperFactory * @since 1.0.0 */ public const COLUMNS = [ - 'billing_price_id' => ['name' => 'billing_price_id', 'type' => 'int', 'internal' => 'id'], - 'billing_price_name' => ['name' => 'billing_price_name', 'type' => 'string', 'internal' => 'name'], - 'billing_price_promocode' => ['name' => 'billing_price_promocode', 'type' => 'string', 'internal' => 'promocode'], - 'billing_price_item' => ['name' => 'billing_price_item', 'type' => 'int', 'internal' => 'item'], - 'billing_price_itemgroup' => ['name' => 'billing_price_itemgroup', 'type' => 'int', 'internal' => 'itemgroup'], - 'billing_price_itemsegment' => ['name' => 'billing_price_itemsegment', 'type' => 'int', 'internal' => 'itemsegment'], - 'billing_price_itemsection' => ['name' => 'billing_price_itemsection', 'type' => 'int', 'internal' => 'itemsection'], - 'billing_price_itemtype' => ['name' => 'billing_price_itemtype', 'type' => 'int', 'internal' => 'itemtype'], - 'billing_price_client' => ['name' => 'billing_price_client', 'type' => 'int', 'internal' => 'client'], - 'billing_price_clientgroup' => ['name' => 'billing_price_clientgroup', 'type' => 'int', 'internal' => 'clientgroup'], - 'billing_price_clientsegment' => ['name' => 'billing_price_clientsegment', 'type' => 'int', 'internal' => 'clientsegment'], - 'billing_price_clientsection' => ['name' => 'billing_price_clientsection', 'type' => 'int', 'internal' => 'clientsection'], - 'billing_price_clienttype' => ['name' => 'billing_price_clienttype', 'type' => 'int', 'internal' => 'clienttype'], - 'billing_price_clientcountry' => ['name' => 'billing_price_clientcountry', 'type' => 'string', 'internal' => 'clientcountry'], - '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_quantity' => ['name' => 'billing_price_quantity', 'type' => 'int', 'internal' => 'quantity'], - 'billing_price_price' => ['name' => 'billing_price_price', 'type' => 'Serializable', 'internal' => 'price'], - 'billing_price_price_new' => ['name' => 'billing_price_price_new', 'type' => 'int', 'internal' => 'priceNew'], - 'billing_price_discount' => ['name' => 'billing_price_discount', 'type' => 'int', 'internal' => 'discount'], - 'billing_price_discountp' => ['name' => 'billing_price_discountp', 'type' => 'int', 'internal' => 'discountPercentage'], - 'billing_price_bonus' => ['name' => 'billing_price_bonus', 'type' => 'int', 'internal' => 'bonus'], - 'billing_price_multiply' => ['name' => 'billing_price_multiply', 'type' => 'bool', 'internal' => 'multiply'], - 'billing_price_currency' => ['name' => 'billing_price_currency', 'type' => 'string', 'internal' => 'currency'], - 'billing_price_start' => ['name' => 'billing_price_start', 'type' => 'DateTime', 'internal' => 'start'], - 'billing_price_end' => ['name' => 'billing_price_end', 'type' => 'DateTime', 'internal' => 'end'], + 'billing_price_id' => ['name' => 'billing_price_id', 'type' => 'int', 'internal' => 'id'], + 'billing_price_name' => ['name' => 'billing_price_name', 'type' => 'string', 'internal' => 'name'], + 'billing_price_promocode' => ['name' => 'billing_price_promocode', 'type' => 'string', 'internal' => 'promocode'], + 'billing_price_item' => ['name' => 'billing_price_item', 'type' => 'int', 'internal' => 'item'], + 'billing_price_itemsegment' => ['name' => 'billing_price_itemsegment', 'type' => 'int', 'internal' => 'itemsegment'], + 'billing_price_itemsection' => ['name' => 'billing_price_itemsection', 'type' => 'int', 'internal' => 'itemsection'], + 'billing_price_itemsalesgroup' => ['name' => 'billing_price_itemsalesgroup', 'type' => 'int', 'internal' => 'itemsalesgroup'], + 'billing_price_itemproductgroup' => ['name' => 'billing_price_itemproductgroup', 'type' => 'int', 'internal' => 'itemproductgroup'], + 'billing_price_itemtype' => ['name' => 'billing_price_itemtype', 'type' => 'int', 'internal' => 'itemtype'], + 'billing_price_client' => ['name' => 'billing_price_client', 'type' => 'int', 'internal' => 'client'], + 'billing_price_clientsegment' => ['name' => 'billing_price_clientsegment', 'type' => 'int', 'internal' => 'clientsegment'], + 'billing_price_clientsection' => ['name' => 'billing_price_clientsection', 'type' => 'int', 'internal' => 'clientsection'], + 'billing_price_clientgroup' => ['name' => 'billing_price_clientgroup', 'type' => 'int', 'internal' => 'clientgroup'], + 'billing_price_clienttype' => ['name' => 'billing_price_clienttype', 'type' => 'int', 'internal' => 'clienttype'], + 'billing_price_clientcountry' => ['name' => 'billing_price_clientcountry', 'type' => 'string', 'internal' => 'clientcountry'], + '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'], + 'billing_price_discount' => ['name' => 'billing_price_discount', 'type' => 'Serializable', 'internal' => 'discount'], + 'billing_price_discountp' => ['name' => 'billing_price_discountp', 'type' => 'Serializable', 'internal' => 'discountPercentage'], + 'billing_price_bonus' => ['name' => 'billing_price_bonus', 'type' => 'Serializable', 'internal' => 'bonus'], + 'billing_price_multiply' => ['name' => 'billing_price_multiply', 'type' => 'bool', 'internal' => 'multiply'], + 'billing_price_currency' => ['name' => 'billing_price_currency', 'type' => 'string', 'internal' => 'currency'], + 'billing_price_start' => ['name' => 'billing_price_start', 'type' => 'DateTime', 'internal' => 'start'], + 'billing_price_end' => ['name' => 'billing_price_end', 'type' => 'DateTime', 'internal' => 'end'], ]; /** @@ -82,9 +84,13 @@ final class PriceMapper extends DataMapperFactory 'mapper' => ItemMapper::class, 'external' => 'billing_price_item', ], - 'itemgroup' => [ + 'itemsalesgroup' => [ 'mapper' => ItemAttributeValueMapper::class, - 'external' => 'billing_price_itemgroup', + 'external' => 'billing_price_itemsalesgroup', + ], + 'itemproductgroup' => [ + 'mapper' => ItemAttributeValueMapper::class, + 'external' => 'billing_price_itemproductgroup', ], 'itemsegment' => [ 'mapper' => ItemAttributeValueMapper::class, @@ -182,7 +188,7 @@ final class PriceMapper extends DataMapperFactory AND (unit = ? OR unit = null) */ - // @todo: allow nested where clause (already possible with the query builder, but not with the mappers) + // @todo allow nested where clause (already possible with the query builder, but not with the mappers) return []; } diff --git a/Models/Price/PriceStatus.php b/Models/Price/PriceStatus.php new file mode 100644 index 0000000..74e3a21 --- /dev/null +++ b/Models/Price/PriceStatus.php @@ -0,0 +1,32 @@ += '{$start->format('Y-m-d H:i:s')}' + AND billing_bill_performance_date <= '{$end->format('Y-m-d H:i:s')}'; + SQL; - /** @var array $result */ - $result = $query->select('SUM(billing_bill_element_single_netsalesprice)', 'COUNT(billing_bill_element_total_netsalesprice)') - ->from(self::TABLE) - ->leftJoin(BillElementMapper::TABLE) - ->on(self::TABLE . '.billing_bill_id', '=', BillElementMapper::TABLE . '.billing_bill_element_bill') - ->where(BillElementMapper::TABLE . '.billing_bill_element_item', '=', $id) - ->andWhere(self::TABLE . '.billing_bill_performance_date', '>=', $start) - ->andWhere(self::TABLE . '.billing_bill_performance_date', '<=', $end) - ->execute() - ?->fetch() ?? []; + $query = new Builder(self::$db); + $result = $query->raw($sql)->execute()?->fetchAll(\PDO::FETCH_ASSOC) ?? []; - return new FloatInt(((int) ($result[1] ?? 0)) === 0 ? 0 : (int) (((int) ($result[0] ?? 0)) / ((int) $result[1]))); + return isset($result[0]['net_count']) + ? new FloatInt((int) ($result[0]['net_sales'] ?? 0) / ($result[0]['net_count'])) + : new FloatInt(0); } /** * Placeholder + * @todo Implement */ public static function getLastOrderDateByItemId(int $id) : ?\DateTimeImmutable { - // @todo: only delivers/invoice/production (no offers ...) + // @todo only delivers/invoice/production (no offers ...) $query = new Builder(self::$db); /** @var false|array $result */ @@ -160,10 +170,11 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getLastOrderDateByClientId(int $id) : ?\DateTimeImmutable { - // @todo: only delivers/invoice/production (no offers ...) + // @todo only delivers/invoice/production (no offers ...) $query = new Builder(self::$db); /** @var false|array $result */ @@ -180,6 +191,7 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getItemRetentionRate(int $id, \DateTime $start, \DateTime $end) : float { @@ -188,6 +200,7 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getItemLivetimeValue(int $id, \DateTime $start, \DateTime $end) : FloatInt { @@ -196,6 +209,7 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getNewestItemInvoices(int $id, int $limit = 10) : array { @@ -217,6 +231,7 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getNewestClientInvoices(int $id, int $limit = 10) : array { @@ -236,6 +251,7 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getItemTopClients(int $id, \DateTime $start, \DateTime $end, int $limit = 10) : array { @@ -276,14 +292,19 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getItemBills(int $id, \DateTime $start, \DateTime $end) : array { - $query = self::getQuery(); + $query = self::reader() + ->with('type') + ->with('type/l11n') + ->where('type/l11n/language', 'en') + ->getQuery(); + $query->leftJoin(BillElementMapper::TABLE, BillElementMapper::TABLE . '_d1') ->on(self::TABLE . '_d1.billing_bill_id', '=', BillElementMapper::TABLE . '_d1.billing_bill_element_bill') - ->where(BillElementMapper::TABLE . '_d1.billing_bill_element_item', '=', $id) - ->limit($limit = 10); + ->where(BillElementMapper::TABLE . '_d1.billing_bill_element_item', '=', $id); /** @phpstan-ignore-next-line */ if (!empty(self::CREATED_AT)) { @@ -292,32 +313,45 @@ final class SalesBillMapper extends BillMapper $query->orderBy(self::TABLE . '_d1.' . self::COLUMNS[self::PRIMARYFIELD]['name'], 'DESC'); } - return self::getAll()->execute($query); + return self::getAll() + ->with('type') + ->with('type/l11n') + ->execute($query); } /** * Placeholder + * @todo Implement + */ + public static function getClientBills(int $id, string $language, \DateTime $start, \DateTime $end) : array + { + return self::getAll() + ->with('type') + ->with('type/l11n') + ->where('client', $id) + ->where('type/l11n/language', $language) + ->where('billDate', $start, '>=') + ->where('billDate', $end, '<=') + ->execute(); + } + + /** + * Placeholder + * @todo Implement */ public static function getClientItem(int $client, \DateTime $start, \DateTime $end) : array { - $query = BillElementMapper::getQuery(); - $query->leftJoin(self::TABLE, self::TABLE . '_d1') - ->on(BillElementMapper::TABLE . '_d1.billing_bill_element_bill', '=', self::TABLE . '_d1.billing_bill_id') - ->where(self::TABLE . '_d1.billing_bill_client', '=', $client) - ->limit($limit = 10); - - /** @phpstan-ignore-next-line */ - if (!empty(self::CREATED_AT)) { - $query->orderBy(self::TABLE . '_d1.' . self::COLUMNS[self::CREATED_AT]['name'], 'DESC'); - } else { - $query->orderBy(self::TABLE . '_d1.' . self::COLUMNS[self::PRIMARYFIELD]['name'], 'DESC'); - } - - return BillElementMapper::getAll()->execute($query); + return BillElementMapper::getAll() + ->with('bill') + ->with('bill/type') + ->where('bill/client', $client) + ->where('bill/type/transferStock', true) + ->execute(); } /** * Placeholder + * @todo Implement */ public static function getItemCountrySales(int $id, \DateTime $start, \DateTime $end, int $limit = 10) : array { @@ -343,47 +377,240 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ - public static function getItemMonthlySalesCosts(int $id, \DateTime $start, \DateTime $end) : array + public static function getItemMonthlySalesCosts(array $items, \DateTime $start, \DateTime $end) : array { + if (empty($items)) { + return []; + } + + $item = \implode(',', $items); + + $sql = <<= '{$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; + SQL; + $query = new Builder(self::$db); - $result = $query->selectAs('SUM(billing_bill_element_total_netsalesprice)', 'net_sales') - ->selectAs('SUM(billing_bill_element_total_netpurchaseprice)', 'net_costs') - ->selectAs('YEAR(billing_bill_performance_date)', 'year') - ->selectAs('MONTH(billing_bill_performance_date)', 'month') - ->from(self::TABLE) - ->leftJoin(BillElementMapper::TABLE) - ->on(self::TABLE . '.billing_bill_id', '=', BillElementMapper::TABLE . '.billing_bill_element_bill') - ->where(BillElementMapper::TABLE . '.billing_bill_element_item', '=', $id) - ->andWhere(self::TABLE . '.billing_bill_performance_date', '>=', $start) - ->andWhere(self::TABLE . '.billing_bill_performance_date', '<=', $end) - ->groupBy('year', 'month') - ->orderBy(['year', 'month'], ['ASC', 'ASC']) - ->execute() - ?->fetchAll(); + $result = $query->raw($sql)->execute()?->fetchAll(\PDO::FETCH_ASSOC) ?? []; return $result ?? []; } /** * Placeholder + * @todo Implement */ - public static function getClientMonthlySalesCosts(int $id, \DateTime $start, \DateTime $end) : array + public static function getItemMonthlySalesQuantity(array $items, \DateTime $start, \DateTime $end) : array { + if (empty($items)) { + return []; + } + + $item = \implode(',', $items); + + $sql = <<= '{$start->format('Y-m-d H:i:s')}' + AND billing_bill_performance_date <= '{$end->format('Y-m-d H:i:s')}' + GROUP BY item, year, month + ORDER BY item, year ASC, month ASC; + SQL; + $query = new Builder(self::$db); - $result = $query->selectAs('SUM(billing_bill_netsales)', 'net_sales') - ->selectAs('SUM(billing_bill_netcosts)', 'net_costs') - ->selectAs('YEAR(billing_bill_performance_date)', 'year') - ->selectAs('MONTH(billing_bill_performance_date)', 'month') - ->from(self::TABLE) - ->where(self::TABLE . '.billing_bill_client', '=', $id) - ->andWhere(self::TABLE . '.billing_bill_performance_date', '>=', $start) - ->andWhere(self::TABLE . '.billing_bill_performance_date', '<=', $end) - ->groupBy('year', 'month') - ->orderBy(['year', 'month'], ['ASC', 'ASC']) - ->execute() - ?->fetchAll(); + $result = $query->raw($sql)->execute()?->fetchAll(\PDO::FETCH_ASSOC) ?? []; return $result ?? []; } + + /** + * Placeholder + * @todo Implement + */ + public static function getClientMonthlySalesCosts(int $client, \DateTime $start, \DateTime $end) : array + { + $sql = <<= '{$start->format('Y-m-d H:i:s')}' + AND billing_bill_performance_date <= '{$end->format('Y-m-d H:i:s')}' + GROUP BY year, month + ORDER BY year ASC, month ASC; + SQL; + + $query = new Builder(self::$db); + $result = $query->raw($sql)->execute()?->fetchAll(\PDO::FETCH_ASSOC) ?? []; + + return $result ?? []; + } + + /** + * Placeholder + * @todo Implement + */ + public static function getItemNetSales(int $item, \DateTime $start, \DateTime $end) : FloatInt + { + $sql = <<= '{$start->format('Y-m-d H:i:s')}' + AND billing_bill_performance_date <= '{$end->format('Y-m-d H:i:s')}'; + SQL; + + $query = new Builder(self::$db); + $result = $query->raw($sql)->execute()?->fetchAll(\PDO::FETCH_ASSOC) ?? []; + + return new FloatInt((int) ($result[0]['net_sales'] ?? 0)); + } + + /** + * Placeholder + * @todo Implement + */ + public static function getILVHistoric(int $item) : FloatInt + { + $sql = <<raw($sql)->execute()?->fetchAll(\PDO::FETCH_ASSOC) ?? []; + + return new FloatInt((int) ($result[0]['net_sales'] ?? 0)); + } + + /** + * Placeholder + * @todo Implement + */ + public static function getItemMRR() : FloatInt + { + return new FloatInt(0); + } + + /** + * Placeholder + * @todo Implement + */ + public static function getItemLastOrder(int $item) : ?\DateTime + { + $sql = <<raw($sql)->execute()?->fetchAll(\PDO::FETCH_ASSOC) ?? []; + + return isset($result[0]['billing_bill_created_at']) + ? new \DateTime(($result[0]['billing_bill_created_at'])) + : null; + } + + /** + * Placeholder + * @todo Implement + */ + public static function getClientNetSales(int $client, \DateTime $start, \DateTime $end) : FloatInt + { + $sql = <<= '{$start->format('Y-m-d H:i:s')}' + AND billing_bill_performance_date <= '{$end->format('Y-m-d H:i:s')}'; + SQL; + + $query = new Builder(self::$db); + $result = $query->raw($sql)->execute()?->fetchAll(\PDO::FETCH_ASSOC) ?? []; + + return new FloatInt((int) ($result[0]['net_sales'] ?? 0)); + } + + /** + * Placeholder + * @todo Implement + */ + public static function getCLVHistoric(int $client) : FloatInt + { + $sql = <<raw($sql)->execute()?->fetchAll(\PDO::FETCH_ASSOC) ?? []; + + return new FloatInt((int) ($result[0]['net_sales'] ?? 0)); + } + + /** + * Placeholder + * @todo Implement + */ + public static function getClientMRR() : FloatInt + { + return new FloatInt(0); + } + + /** + * Placeholder + * @todo Implement + */ + public static function getClientLastOrder(int $client) : ?\DateTime + { + $sql = <<raw($sql)->execute()?->fetchAll(\PDO::FETCH_ASSOC) ?? []; + + return isset($result[0]['billing_bill_created_at']) + ? new \DateTime(($result[0]['billing_bill_created_at'])) + : null; + } } diff --git a/Models/SettingsEnum.php b/Models/SettingsEnum.php index 038c6bd..9edcade 100755 --- a/Models/SettingsEnum.php +++ b/Models/SettingsEnum.php @@ -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 } diff --git a/Models/ShippingTermL11nMapper.php b/Models/ShippingTermL11nMapper.php new file mode 100644 index 0000000..e67686a --- /dev/null +++ b/Models/ShippingTermL11nMapper.php @@ -0,0 +1,69 @@ + + */ +final class ShippingTermL11nMapper extends DataMapperFactory +{ + /** + * Columns. + * + * @var array + * @since 1.0.0 + */ + public const COLUMNS = [ + 'billing_shipping_term_l11n_id' => ['name' => 'billing_shipping_term_l11n_id', 'type' => 'int', 'internal' => 'id'], + 'billing_shipping_term_l11n_name' => ['name' => 'billing_shipping_term_l11n_name', 'type' => 'string', 'internal' => 'content', 'autocomplete' => true], + 'billing_shipping_term_l11n_term' => ['name' => 'billing_shipping_term_l11n_term', 'type' => 'int', 'internal' => 'ref'], + 'billing_shipping_term_l11n_language' => ['name' => 'billing_shipping_term_l11n_language', 'type' => 'string', 'internal' => 'language'], + ]; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'billing_shipping_term_l11n'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'billing_shipping_term_l11n_id'; + + /** + * Model to use by the mapper. + * + * @var class-string + * @since 1.0.0 + */ + public const MODEL = BaseStringL11n::class; +} diff --git a/Models/ShippingTermMapper.php b/Models/ShippingTermMapper.php new file mode 100644 index 0000000..5b55f9c --- /dev/null +++ b/Models/ShippingTermMapper.php @@ -0,0 +1,83 @@ + + */ +final class ShippingTermMapper extends DataMapperFactory +{ + /** + * Columns. + * + * @var array + * @since 1.0.0 + */ + public const COLUMNS = [ + 'billing_shipping_term_id' => ['name' => 'billing_shipping_term_id', 'type' => 'int', 'internal' => 'id'], + 'billing_shipping_term_code' => ['name' => 'billing_shipping_term_code', 'type' => 'string', 'internal' => 'title', 'autocomplete' => true], + ]; + + /** + * Has many relation. + * + * @var array + * @since 1.0.0 + */ + public const HAS_MANY = [ + 'l11n' => [ + 'mapper' => ShippingTermL11nMapper::class, + 'table' => 'billing_shipping_term_l11n', + 'self' => 'billing_shipping_term_l11n_term', + 'column' => 'content', + 'external' => null, + ], + ]; + + /** + * Model to use by the mapper. + * + * @var class-string + * @since 1.0.0 + */ + public const MODEL = BaseStringL11nType::class; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'billing_shipping_term'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'billing_shipping_term_id'; +} diff --git a/Models/StockBillMapper.php b/Models/StockBillMapper.php index a11467e..eda9236 100755 --- a/Models/StockBillMapper.php +++ b/Models/StockBillMapper.php @@ -39,10 +39,10 @@ final class StockBillMapper extends BillMapper */ public static function getStockBeforePivot( mixed $pivot, - string $column = null, + ?string $column = null, int $limit = 50, int $depth = 3, - Builder $query = null + ?Builder $query = null ) : array { return self::getAll() @@ -58,10 +58,10 @@ final class StockBillMapper extends BillMapper */ public static function getStockAfterPivot( mixed $pivot, - string $column = null, + ?string $column = null, int $limit = 50, int $depth = 3, - Builder $query = null + ?Builder $query = null ) : array { return self::getAll() diff --git a/Models/Subscription.php b/Models/Subscription.php index e704a6d..feccea0 100755 --- a/Models/Subscription.php +++ b/Models/Subscription.php @@ -50,7 +50,7 @@ class Subscription implements \JsonSerializable public int $client = 0; - public int $quantity = 0; + public FloatInt $quantity; /** * Constructor. @@ -59,19 +59,8 @@ class Subscription implements \JsonSerializable */ public function __construct() { - $price = new FloatInt(); - } - - /** - * Get id. - * - * @return int Model id - * - * @since 1.0.0 - */ - public function getId() : int - { - return $this->id; + $price = new FloatInt(); + $quantity = new FloatInt(); } /** @@ -80,7 +69,7 @@ class Subscription implements \JsonSerializable public function toArray() : array { return [ - 'id' => $this->id, + 'id' => $this->id, ]; } diff --git a/Models/SubscriptionMapper.php b/Models/SubscriptionMapper.php index 65fba7d..7f2a098 100755 --- a/Models/SubscriptionMapper.php +++ b/Models/SubscriptionMapper.php @@ -41,7 +41,7 @@ final class SubscriptionMapper extends DataMapperFactory 'billing_subscription_start' => ['name' => 'billing_subscription_start', 'type' => 'DateTime', 'internal' => 'start'], 'billing_subscription_end' => ['name' => 'billing_subscription_end', 'type' => 'DateTime', 'internal' => 'end'], 'billing_subscription_price' => ['name' => 'billing_subscription_price', 'type' => 'Serializable', 'internal' => 'price'], - 'billing_subscription_quantity' => ['name' => 'billing_subscription_quantity', 'type' => 'int', 'internal' => 'quantity'], + 'billing_subscription_quantity' => ['name' => 'billing_subscription_quantity', 'type' => 'Serializable', 'internal' => 'quantity'], 'billing_subscription_bill' => ['name' => 'billing_subscription_bill', 'type' => 'int', 'internal' => 'bill'], 'billing_subscription_item' => ['name' => 'billing_subscription_item', 'type' => 'int', 'internal' => 'item'], 'billing_subscription_autorenew' => ['name' => 'billing_subscription_autorenew', 'type' => 'bool', 'internal' => 'autoRenew'], diff --git a/Models/Tax/NullTaxCombination.php b/Models/Tax/NullTaxCombination.php index 5513ab2..13a142b 100755 --- a/Models/Tax/NullTaxCombination.php +++ b/Models/Tax/NullTaxCombination.php @@ -34,6 +34,7 @@ final class NullTaxCombination extends TaxCombination public function __construct(int $id = 0) { $this->id = $id; + parent::__construct(); } /** diff --git a/Models/Tax/TaxCombination.php b/Models/Tax/TaxCombination.php index edbfd88..56bd8cf 100755 --- a/Models/Tax/TaxCombination.php +++ b/Models/Tax/TaxCombination.php @@ -16,6 +16,8 @@ namespace Modules\Billing\Models\Tax; use Modules\Attribute\Models\AttributeValue; use Modules\Attribute\Models\NullAttributeValue; +use Modules\Finance\Models\NullTaxCode; +use Modules\Finance\Models\TaxCode; /** * Billing class. @@ -41,18 +43,30 @@ class TaxCombination implements \JsonSerializable public AttributeValue $itemCode; - public string $taxCode = ''; - - // @todo: consider to add the tax code object directly, it is annoying to make a manuall mapper call which is often required afterwards. + public TaxCode $taxCode; public int $taxType = BillTaxType::SALES; public string $account = ''; + // Tax accounts can be defined in: + // 1. Account (gross postings are automatically split) + // 2. Tax code + // 3. Tax combination + public string $taxAccount1 = ''; + + public string $taxAccount2 = ''; + public string $refundAccount = ''; public string $discountAccount = ''; + public string $cashbackAccount = ''; + + public string $overpaymentAccount = ''; + + public string $underpaymentAccount = ''; + public ?int $minPrice = null; public ?int $maxPrice = null; @@ -69,6 +83,7 @@ class TaxCombination implements \JsonSerializable public function __construct() { $this->itemCode = new NullAttributeValue(); + $this->taxCode = new NullTaxCode(); } /** @@ -77,7 +92,7 @@ class TaxCombination implements \JsonSerializable public function toArray() : array { return [ - 'id' => $this->id, + 'id' => $this->id, ]; } diff --git a/Models/Tax/TaxCombinationMapper.php b/Models/Tax/TaxCombinationMapper.php index e59fe9f..b4ad10f 100755 --- a/Models/Tax/TaxCombinationMapper.php +++ b/Models/Tax/TaxCombinationMapper.php @@ -15,8 +15,9 @@ declare(strict_types=1); namespace Modules\Billing\Models\Tax; use Modules\ClientManagement\Models\Attribute\ClientAttributeValueMapper; +use Modules\Finance\Models\TaxCodeMapper; use Modules\ItemManagement\Models\Attribute\ItemAttributeValueMapper; -use Modules\SupplierManagement\Models\SupplierAttributeValueMapper; +use Modules\SupplierManagement\Models\Attribute\SupplierAttributeValueMapper; use phpOMS\DataStorage\Database\Mapper\DataMapperFactory; /** @@ -39,19 +40,24 @@ final class TaxCombinationMapper extends DataMapperFactory * @since 1.0.0 */ public const COLUMNS = [ - 'billing_tax_id' => ['name' => 'billing_tax_id', 'type' => 'int', 'internal' => 'id'], - 'billing_tax_client_code' => ['name' => 'billing_tax_client_code', 'type' => 'int', 'internal' => 'clientCode'], - 'billing_tax_supplier_code' => ['name' => 'billing_tax_supplier_code', 'type' => 'int', 'internal' => 'supplierCode'], - 'billing_tax_item_code' => ['name' => 'billing_tax_item_code', 'type' => 'int', 'internal' => 'itemCode'], - 'billing_tax_code' => ['name' => 'billing_tax_code', 'type' => 'string', 'internal' => 'taxCode'], - 'billing_tax_type' => ['name' => 'billing_tax_type', 'type' => 'int', 'internal' => 'taxType'], - 'billing_tax_account' => ['name' => 'billing_tax_account', 'type' => 'string', 'internal' => 'account'], - 'billing_tax_refund_account' => ['name' => 'billing_tax_refund_account', 'type' => 'string', 'internal' => 'refundAccount'], - 'billing_tax_discount_account' => ['name' => 'billing_tax_discount_account', 'type' => 'string', 'internal' => 'discountAccount'], - 'billing_tax_min_price' => ['name' => 'billing_tax_min_price', 'type' => 'int', 'internal' => 'minPrice'], - 'billing_tax_max_price' => ['name' => 'billing_tax_max_price', 'type' => 'int', 'internal' => 'maxPrice'], - 'billing_tax_start' => ['name' => 'billing_tax_start', 'type' => 'DateTime', 'internal' => 'start'], - 'billing_tax_end' => ['name' => 'billing_tax_end', 'type' => 'DateTime', 'internal' => 'end'], + 'billing_tax_id' => ['name' => 'billing_tax_id', 'type' => 'int', 'internal' => 'id'], + 'billing_tax_client_code' => ['name' => 'billing_tax_client_code', 'type' => 'int', 'internal' => 'clientCode'], + 'billing_tax_supplier_code' => ['name' => 'billing_tax_supplier_code', 'type' => 'int', 'internal' => 'supplierCode'], + 'billing_tax_item_code' => ['name' => 'billing_tax_item_code', 'type' => 'int', 'internal' => 'itemCode'], + 'billing_tax_code' => ['name' => 'billing_tax_code', 'type' => 'string', 'internal' => 'taxCode'], + 'billing_tax_type' => ['name' => 'billing_tax_type', 'type' => 'int', 'internal' => 'taxType'], + 'billing_tax_account' => ['name' => 'billing_tax_account', 'type' => 'string', 'internal' => 'account'], + 'billing_tax_tax1_account' => ['name' => 'billing_tax_tax1_account', 'type' => 'string', 'internal' => 'taxAccount1'], + 'billing_tax_tax2_account' => ['name' => 'billing_tax_tax2_account', 'type' => 'string', 'internal' => 'taxAccount2'], + 'billing_tax_refund_account' => ['name' => 'billing_tax_refund_account', 'type' => 'string', 'internal' => 'refundAccount'], + 'billing_tax_discount_account' => ['name' => 'billing_tax_discount_account', 'type' => 'string', 'internal' => 'discountAccount'], + 'billing_tax_cashback_account' => ['name' => 'billing_tax_cashback_account', 'type' => 'string', 'internal' => 'cashbackAccount'], + 'billing_tax_overpayment_account' => ['name' => 'billing_tax_overpayment_account', 'type' => 'string', 'internal' => 'overpaymentAccount'], + 'billing_tax_underpayment_account' => ['name' => 'billing_tax_underpayment_account', 'type' => 'string', 'internal' => 'underpaymentAccount'], + 'billing_tax_min_price' => ['name' => 'billing_tax_min_price', 'type' => 'int', 'internal' => 'minPrice'], + 'billing_tax_max_price' => ['name' => 'billing_tax_max_price', 'type' => 'int', 'internal' => 'maxPrice'], + 'billing_tax_start' => ['name' => 'billing_tax_start', 'type' => 'DateTime', 'internal' => 'start'], + 'billing_tax_end' => ['name' => 'billing_tax_end', 'type' => 'DateTime', 'internal' => 'end'], ]; /** @@ -73,6 +79,11 @@ final class TaxCombinationMapper extends DataMapperFactory 'mapper' => ItemAttributeValueMapper::class, 'external' => 'billing_tax_item_code', ], + 'taxCode' => [ + 'mapper' => TaxCodeMapper::class, + 'external' => 'billing_tax_code', + 'by' => 'abbr', + ], ]; /** diff --git a/Models/billIdentifier.json b/Models/billIdentifier.json deleted file mode 100755 index dd133c9..0000000 --- a/Models/billIdentifier.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "type": { - "purchase_invoice": { - "en": [ - "Invoice" - ], - "de": [ - "Rechnung" - ] - }, - "purchase_credit_note": { - "en": [ - "Credit Note" - ], - "de": [ - "Rechnungskorrektur", - "Gutschrift" - ] - }, - "purchase_delivery_note": { - "en": [ - "Delivery Note" - ], - "de": [ - "Lieferschein" - ] - }, - "purchase_order_confirmation": { - "en": [ - "Order Confirmation" - ], - "de": [ - "Auftragsbestätigung" - ] - }, - "purchase_reverse_invoice": { - "en": [ - "Credit Note" - ], - "de": [ - "Gutschrift" - ] - } - }, - "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" - ], - "bill_no": { - "en": [ - "/(inv.*?)(no|\\s|,|:|\\.|#)+(?.*?)( |$)/i", - "/(#)(?.*?)( |$)/i" - ], - "de": [ - "/(rechnungsn.*?|beleg.*?)(?.*?)( |$)/i" - ] - }, - "bill_date": { - "en": [ - "/(inv.*?)(date.*?)(\\s|,|:|\\.)+(?.*?)( |$)/i", - "/(date.*?)(\\s|,|:|\\.)+(?.*?)( |$)/i" - ], - "de": [ - "/(rechnungsdat.*?|belegdat.*?)(\\s|,|:|\\.)+(?.*?)( |$)/i" - ] - }, - "bill_due": { - "en": [ - "/(due date.*?)(\\s|,|:|\\.)+(?.*?)( |$)/i", - "/(due.*?)(\\s|,|:|\\.)+(?.*?)( |$)/i" - ], - "de": [ - "/(fällig.*?)(\\s|,|:|\\.)+(?.*?)( |$)/i" - ] - }, - "total_gross": { - "en": [ - "/(total.*?|gross.*?)(?([0-9]+,*\\.*)+)/i" - ], - "de": [ - "/(betrag.*?|gesamt.*?|brutto.*?)(?([0-9]+,*\\.*)+)/i" - ] - } -} \ No newline at end of file diff --git a/Models/bill_identifier.json b/Models/bill_identifier.json new file mode 100644 index 0000000..285b32a --- /dev/null +++ b/Models/bill_identifier.json @@ -0,0 +1,251 @@ +{ + "type": { + "purchase_invoice": { + "en": [ + "Invoice", + "Receipt" + ], + "de": [ + "Rechnung", + "Quittung" + ] + }, + "purchase_credit_note": { + "en": [ + "Credit Note" + ], + "de": [ + "Rechnungskorrektur", + "Gutschrift" + ] + }, + "purchase_delivery_note": { + "en": [ + "Delivery Note" + ], + "de": [ + "Lieferschein" + ] + }, + "purchase_order_confirmation": { + "en": [ + "Order Confirmation" + ], + "de": [ + "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,})(?(.{6,15}))( |$)/i", + "/(vat[\\. \\-]*id)(.*? {1,})(?([^a-zA-Z]{6,15}))( |$)/i" + ], + "de": [ + "/(Steuern|Steuer[\\. \\-]*Nr|St[\\. \\-]*Nr)(.*? {1,})(?(.{8,15}))( |$)/i", + "/(USt[\\. \\-]*Id|Umsatzst.*?Id)(.*? {1,})(?([^a-zA-Z]{8,15}))( |$)/i" + ] + }, + "vat_id": { + "en": [ + "/(vat[\\. \\-]*id)(.*? {1,})(?([a-zA-Z]{2})(.*?){7,13})( |$)/i" + ], + "de": [ + "/(USt[\\. \\-]*Id|Umsatzst.*?Id)(.*? {1,})(?([a-zA-Z]{2})(.*?){7,13})( |$)/i" + ] + }, + "iban": ["/(IBAN)(.*? {1,})(?([a-zA-Z]{2,}[ 0-9]{14,}))( |$)/i"], + "email": ["/(^| )(?([a-zA-Z0-9\\-]+@[a-zA-Z0-9\\-]+\\.[a-zA-Z]{2,}))( |$)/i"], + "website": ["/(^| )(?(https:\\/\\/|www\\.)([a-zA-Z0-9\\-]+\\.[a-zA-Z]{2,}))( |$)/i"], + "phone": { + "en": [ + "/(phone)(.*? {1,})(?([+0-9 \\/\\-\\(\\)]*[0-9]+[+0-9 \\/\\-\\(\\)]*){4,})( |[^ 0-9\\/\\(\\)+\\-]|$)/i" + ], + "de": [ + "/(Tel|Rufn)(.*? {1,})(?([+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", + "d-m-y", "d.m.y", "d/m/y", + "m-d-y", "m.d.y", "m/d/y" + ], + "bill_no": { + "en": [ + "/(invoice|inv.*? no)(.*? {1,})(?\\S*?)( |$)/i", + "/(#)(?\\S*?)( |$)/i" + ], + "de": [ + "/(rechnungsn|rechnung n|belegn|beleg n)(.*? {1,})(?\\S*?)( |$)/i" + ] + }, + "bill_date": { + "en": [ + "/(inv.*? )(date.*? )(?.{8,}?)( |$)/i", + "/(date.*? )(?.{8,}?)( |$)/i" + ], + "de": [ + "/(rechnungsdat|belegdat|datum)(.*? )(?\\S{8,}?)( |$)/i" + ] + }, + "bill_due": { + "en": [ + "/(due date.*? )(?\\S{8,}?)( |$)/i", + "/(due.*? )(?\\S{8,}?)( |$)/i" + ], + "de": [ + "/(fällig.*? )(?\\S{8,}?)( |$)/i" + ] + }, + "total_net": { + "en": [ + "/(subtotal|net)(.*? {1,})(?([0-9]+,*\\.*)+)(?! *%)/i" + ], + "de": [ + "/(netto|zwischensumme|betrag exk)(.*? {1,})(?([0-9]+,*\\.*)+)(?! *%)/i" + ] + }, + "total_discount": { + "en": [ + "/(discount)(.*? {1,})(?([0-9]+,*\\.*)+)(?! *%)/i" + ], + "de": [ + "/(rabatt)(.*? {1,})(?([0-9]+,*\\.*)+)(?! *%)/i" + ] + }, + "total_shipping": { + "en": [ + "/(fuel|handling|fright|shipping)(.*? {1,})(?([0-9]+,*\\.*)+)(?! *%)/i" + ], + "de": [ + "/(versand|transport|fracht)(.*? {1,})(?([0-9]+,*\\.*)+)(?! *%)/i" + ] + }, + "total_customs": { + "en": [ + "/(customs)(.*? {1,})(?([0-9]+,*\\.*)+)(?! *%)/i" + ], + "de": [ + "/(Einfuhr)(\\S* )(?([0-9]+,*\\.*)+)(?! *%)/i" + ] + }, + "total_insurance": { + "en": [ + "/(insurance)(.*? {1,})(?([0-9]+,*\\.*)+)(?! *%)/i" + ], + "de": [ + "/(versicherung)(.*? {1,})(?([0-9]+,*\\.*)+)(?! *%)/i" + ] + }, + "total_surcharge": { + "en": [ + "/(fee|surcharge)(.*? {1,})(?([0-9]+,*\\.*)+)(?! *%)/i" + ], + "de": [ + "/(gebühr)(.*? {1,})(?([0-9]+,*\\.*)+)(?! *%)/i" + ] + }, + "total_tax": { + "en": [ + "/(VAT|tax)(.*? {1,})(?([0-9]+,*\\.*)+)(?! *%)/i" + ], + "de": [ + "/(USt|Mwst|Umsatzst|Mehrwertst)(.*? {1,})(?([0-9]+,*\\.*)+)(?! *%)/i", + "/( {1,})(USt|Mwst|Umsatzst|Mehrwertst)(?([0-9]+,*\\.*)+)(?! *%)/i" + ] + }, + "tax_rate": { + "en": [ + "/(VAT|tax)(.*? {1,})(?([0-9]+,*\\.*)+)(?= *%)/i" + ], + "de": [ + "/(USt|Mwst|Umsatzst|Mehrwertst)(.*? {1,})(?([0-9]+,*\\.*)+)(?= *%)/i", + "/( {1,})(?([0-9]+,*\\.*)+)(?= *%)(.*?)(USt|Mwst|Umsatzst|Mehrwertst)/i" + ] + }, + "total_gross": { + "en": [ + "/(total|gross)(.*? {1,})(?([0-9]+,*\\.*)+)(?! *%)/i" + ], + "de": [ + "/(betrag|gesamt|brutto|summe)(.*? {1,})(?([0-9]+,*\\.*)+)(?! *%)/i" + ] + }, + "item_table": { + "en": { + "headline": { + "order": ["no.", "#", "pos"], + "number": ["number"], + "description": ["description", "name", "service", "product", "item"], + "quantity": ["qty", "quantity", "hours"], + "price": ["price", "rate"], + "unit": ["unit"], + "total": ["amount", "total", "gross"], + "tax": ["tax"] + }, + "parts": "/( *)(.+?)(\\s{3,}|$)/i", + "row": { + "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"], + "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", "Brutto", "Summe"], + "tax": ["MwSt", "USt", "Steuer"] + }, + "parts": "/( *)(.+?)(\\s{2,}|$)/i", + "row": { + "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" + } + } + } +} diff --git a/Models/itemcategoryidentifier.json b/Models/item_category_identifier.json similarity index 50% rename from Models/itemcategoryidentifier.json rename to Models/item_category_identifier.json index d2b3eaf..2216771 100644 --- a/Models/itemcategoryidentifier.json +++ b/Models/item_category_identifier.json @@ -10,14 +10,26 @@ "cabinet labels", "paper towels", "cleaning supplies", "sticky tack", "adhesive putty", "wall calendar" ], - "de": [] + "de": [ + "Büromaterial", "Bleistift", "Kugelschreiber", "Notizbuch", "Notiz", "Textmarker", + "Radiergummi", "Büroklammer", "Foldback-Klammer", "Heftklammern", "Hefter", "Klebebandabroller", + "Tesafilm", "Gummibänder", "Marker", "Whiteboard-Marker", + "Whiteboard-Radiergummi", "Schreibtischorganizer", "Kalender", "Schreibtischkalender", + "Aktenordner", "Ordner", "Hängeregistratur", "Umschlag", "Briefmarke", + "Schere", "Taschenrechner", "Lineal", "Mülleimer", "Abfalleimer", + "Schranketiketten", "Papierhandtücher", "Putzmittel", "Knetklebe", "Haftmasse", + "Wandkalender" + ] }, "it_supplies": { "en": [ "keyboard", "mouse", "mice", "USB drive", "network cable", "toner", "printer paper", "ink", "cartridge" ], - "de": [] + "de": [ + "Tastatur", "Maus", "Mäuse", "USB-Laufwerk", "Netzwerkkabel", "Toner", + "Druckerpapier", "Tinte", "Patrone" + ] }, "it_equipment": { "en": [ @@ -25,13 +37,19 @@ "router", "webcam", "headset", "hard drive", "microphone", "macbook", "phone", "cellphone", "mobile phone" ], - "de": [] + "de": [ + "PC", "Computer", "Laptop", "Monitor", "Switch", "Netzteil", "Server", "Drucker", + "Router", "Webcam", "Headset", "Festplatte", "Mikrofon", "Macbook", "Telefon", + "Handy", "Mobiltelefon" + ] }, "internet_phone": { "en": [ "domain", "website", "web server", "phone", "internet", "cellphone" ], - "de": [] + "de": [ + "Domäne", "Website", "Webserver", "Telefon", "Internet", "Mobiltelefon" + ] }, "furniture": { "en": [ @@ -43,7 +61,15 @@ "bean bag", "stool", "cubby storage", "shoe rack", "coat rack", "futon", "crib", "playpen" ], - "de": [] + "de": [ + "Sofa", "Sessel", "Tisch", "Stuhl", "Bett", "Nachttisch", "Kommode", "Kleiderschrank", + "Bücherregal", "Schreibtisch", "Schrank", "Couch", "Sessel", "Fernsehtisch", "Kücheninsel", + "Barhocker", "Badezimmer-Waschtisch", "Esszimmerschrank", "Kommode", + "Bank", "Kredenz", "Sideboard", "Ottomane", "Kopfteil", "Fußteil", + "Chaiselongue", "Terrassenmöbel", "Essgarnitur für draußen", "Hängematte", + "Sitzsack", "Hocker", "Schrankmöbel", "Schuhregal", "Garderobe", "Futon", + "Kinderbett", "Laufstall" + ] }, "appliances": { "en": [ @@ -59,7 +85,19 @@ "portable air conditioner", "hot water dispenser", "skillet", "popcorn maker", "margarita machine", "deep fryer", "griddle", "scale" ], - "de": [] + "de": [ + "Kühlschrank", "Backofen", "Herd", "Mikrowelle", "Geschirrspüler", "Waschmaschine", + "Wäschetrockner", "Gefrierschrank", "Kaffeemaschine", "Toaster", "Mixer", + "Küchenmaschine", "Wasserkocher", "Reiskocher", "Langsamkocher", + "Toaster", "Klimaanlage", "Heizung", "Deckenventilator", "Staubsauger", + "Bügeleisen", "Haartrockner", "Lockenstab", "Rasierapparat", + "Luftreiniger", "Luftentfeuchter", "Luftbefeuchter", "Müllschlucker", + "Müllpresse", "Dunstabzugshaube", "Entsafter", "Mixer", "Grill", + "Brotbackautomat", "Eismaschine", "Wasserspender", "ductless mini-split system", + "Wandofen", "Induktionskochfeld", "Espressomaschine", "Dosenöffner", + "tragbares Klimagerät", "Heißwasserspender", "Bratpfanne", + "Popcornmaschine", "Margarita-Maschine", "Fritteuse", "Bratplatte", "Waage" + ] }, "cleaning": { "en": [ @@ -74,7 +112,18 @@ "furniture polish", "shoe polish", "stainless steel cleaner", "rust remover", "grill brush", "lime scale remover", "rug cleaner", "furniture stain remover" ], - "de": [] + "de": [ + "Besen", "Mopp", "Kehrblech", "Eimer", "Schwamm", "Schrubber", "Lappen", + "Papierhandtücher", "Müllbeutel", "Mülleimer", "Staubsauger", + "Teppichreiniger", "Glasreiniger", "Reinigungsmittel", "Desinfektionstücher", + "Toilettenbürste", "Toilettenschüsselreiniger", "Badreiniger", "Fliesenreiniger", + "Backofenreiniger", "Geschirrspülmittel", "Waschmittel", "Weichspüler", + "Fleckenentferner", "Mülleimerauskleidung", "Backpulver", "Essig", + "Staubmaske", "Putzkorb", "Fensterabzieher", "Staubwedel", + "Fusselrolle", "Lufterfrischer", "Handschuhe", "Entfetter", "Bodenwachs", + "Möbelpolitur", "Schuhcreme", "Edelstahlreiniger", "Rostlöser", + "Grillbürste", "Kalkentferner", "Teppichreiniger", "Möbelfleckenentferner" + ] }, "marketing": { "en": [ @@ -82,7 +131,11 @@ "advertising", "social media", "market research", "marketing", "catalogue", "catalog", "price list" ], - "de": [] + "de": [ + "Flugblatt", "Broschüre", "Artikel", "Messe", "Ausstellung", "Messe", + "Werbung", "Social Media", "Marktforschung", "Marketing", "Katalog", + "katalog", "preisliste" + ] }, "food": { "en": [ @@ -90,18 +143,26 @@ "cake", "ice cream", "pudding", "donut", "nudle", "steak", "fish", "salat", "burger", "fries", "potato", "soup" ], - "de": [] + "de": [ + "Wein", "Bier", "Schnaps", "Wasser", "Cola", "Fanta", "Saft", "Sprite", "Kaffee", + "Kuchen", "Eis", "Pudding", "Donut", + "Nudel", "Steak", "Fisch", "Salat", "Burger", "Pommes", "Kartoffel", "Suppe" + ] }, "hotel": { "en": [ "hotel", "motel", "hostel" ], - "de": [] + "de": [ + "Hotel", "Motel", "Herberge" + ] }, "travel": { "en": [ - "flight", "taxi", "train", "uber", "parking" + "flight", "taxi", "train", "uber", "parking", "garage" ], - "de": [] + "de": [ + "Flug", "Taxi", "Zug", "Uber", "Parken", "Garage" + ] } } \ No newline at end of file diff --git a/README.md b/README.md index af4f138..4cb40c2 100755 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Currently Karaka is still developing the first Alpha version. As soon as we have General updates can be found in our info section at https://jingga.app/info and developer updates can be found in our developer section at https://jingga.app/dev. In our developer section you can also check out the automatically generated reports such as code coverage, code style, static analysis etc. as well as our code style guide lines and developer documentation. -* [Project Status](https://github.com/Karaka-Management/Organization-Guide/blob/master/Project/PROJECT.md) +* [Project Status](https://github.com/orgs/Karaka-Management/projects/10) ## Tech stack @@ -62,7 +62,7 @@ General updates can be found in our info section at https://jingga.app/info and ## Become a contributor -Karaka has a very open culture and we always welcome new people who share our philosophy in providing create solutions which just work. You can find the development process description which also describes how to become a contributer in the [Organization documentation](https://github.com/Karaka-Management/Organization-Guide/blob/master/Processes/Development.md). +Karaka has a very open culture and we always welcome new people who share our philosophy in providing create solutions which just work. You can find the development process description which also describes how to become a contributer in the [Organization documentation](https://github.com/Karaka-Management/Organization-Guide/blob/master/Processes/01_Development.md). ## Misc diff --git a/Theme/Backend/Lang/Navigation.en.lang.php b/Theme/Backend/Lang/Navigation.en.lang.php index f446247..0d87942 100755 --- a/Theme/Backend/Lang/Navigation.en.lang.php +++ b/Theme/Backend/Lang/Navigation.en.lang.php @@ -19,4 +19,6 @@ return ['Navigation' => [ 'InvoiceRecognition' => 'Invoice Recognition', 'Open' => 'Open', 'Upload' => 'Upload', + 'PaymentTerms' => 'Payment Terms', + 'ShippingTerms' => 'Shipping Terms', ]]; diff --git a/Theme/Backend/Lang/ar.lang.php b/Theme/Backend/Lang/ar.lang.php index 1f81fe3..7e12b2b 100755 --- a/Theme/Backend/Lang/ar.lang.php +++ b/Theme/Backend/Lang/ar.lang.php @@ -15,33 +15,22 @@ declare(strict_types=1); return ['Billing' => [ 'Address' => 'تبوك', 'Addresses' => 'عناوين', - 'Africa' => '', 'AlreadyPaid' => 'مدفوع بالفعل', - 'America' => '', 'Amount' => '', - 'Analyse' => '', 'Archive' => '', - 'Articles' => '', - 'Asia' => '', - 'Attribute' => '', - 'BaseTime' => '', 'Billing' => '', 'Bills' => 'فواتير', 'Bonus' => 'علاوة', - 'CIS' => '', 'Cashback' => 'استرداد النقدي', 'City' => 'مدينة', 'Client' => 'عميل', 'ClientID' => 'معرف العميل', - 'ComparisonTime' => '', 'Confirmation' => 'تأكيد', 'Country' => 'دولة', - 'CreateBill' => '', 'Created' => 'خلقت', 'CreditCard' => 'بطاقة إئتمان', 'CreditNote' => 'اشعار دائن', 'Currency' => '', - 'Customers' => '', 'Date' => 'تاريخ', 'Delivery' => 'توصيل', 'DeliveryNote' => 'مذكرة تسليم', @@ -50,13 +39,9 @@ return ['Billing' => [ 'DiscountP' => 'خصم ٪', 'Due' => 'بسبب', 'Email' => '', - 'Europe' => '', - 'Filter' => '', 'Freightage' => 'شحن', - 'General' => '', 'Gross' => 'أزداد', 'Invoice' => 'فاتورة', - 'Invoices' => 'الفواتير', 'Item' => 'العنصر', 'Items' => 'أغراض', 'Language' => '', @@ -68,7 +53,6 @@ return ['Billing' => [ 'Net' => 'شبكة', 'Offer' => 'عرض', 'Original' => '', - 'Other' => '', 'Payment' => 'دفع', 'PaymentPlan' => '', 'Postal' => 'بريدي', @@ -78,9 +62,6 @@ return ['Billing' => [ 'Profit' => 'ربح', 'Quantity' => 'كمية', 'Recipient' => 'متلقي', - 'Region' => '', - 'Rep' => '', - 'Sales' => '', 'Select' => 'يختار', 'Shipment' => 'شحنة', 'Source' => 'مصدر', @@ -92,7 +73,5 @@ return ['Billing' => [ 'Type' => 'نوع', 'Types' => '', 'Upload' => 'تحميل', - 'Value' => '', - 'Variation' => 'تفاوت', 'Zip' => 'أزيز', ]]; diff --git a/Theme/Backend/Lang/cs.lang.php b/Theme/Backend/Lang/cs.lang.php index 0c5f3bd..e38fbb0 100755 --- a/Theme/Backend/Lang/cs.lang.php +++ b/Theme/Backend/Lang/cs.lang.php @@ -15,33 +15,22 @@ declare(strict_types=1); return ['Billing' => [ 'Address' => 'Adresa', 'Addresses' => 'Adresy', - 'Africa' => '', 'AlreadyPaid' => 'Již zaplatil', - 'America' => '', 'Amount' => '', - 'Analyse' => '', 'Archive' => '', - 'Articles' => '', - 'Asia' => '', - 'Attribute' => '', - 'BaseTime' => '', 'Billing' => '', 'Bills' => 'Účtenka', 'Bonus' => 'Bonus', - 'CIS' => '', 'Cashback' => 'Hotovost', 'City' => 'Město', 'Client' => 'Klienta', 'ClientID' => 'ID klienta', - 'ComparisonTime' => '', 'Confirmation' => 'potvrzení', 'Country' => 'Země', - 'CreateBill' => '', 'Created' => 'Vytvořený', 'CreditCard' => 'Kreditní karta', 'CreditNote' => 'Dobropis', 'Currency' => '', - 'Customers' => '', 'Date' => 'datum', 'Delivery' => 'dodávka', 'DeliveryNote' => 'Dodací list', @@ -50,13 +39,9 @@ return ['Billing' => [ 'DiscountP' => 'Sleva%', 'Due' => 'Způsoben', 'Email' => '', - 'Europe' => '', - 'Filter' => '', 'Freightage' => 'Dopravné', - 'General' => '', 'Gross' => 'Hrubý', 'Invoice' => 'Faktura', - 'Invoices' => 'Faktury', 'Item' => 'Položka', 'Items' => 'Položky', 'Language' => '', @@ -68,7 +53,6 @@ return ['Billing' => [ 'Net' => 'Síť', 'Offer' => 'Nabídka', 'Original' => '', - 'Other' => '', 'Payment' => 'Způsob platby', 'PaymentPlan' => '', 'Postal' => 'Poštovní', @@ -78,9 +62,6 @@ return ['Billing' => [ 'Profit' => 'Zisk', 'Quantity' => 'Množství', 'Recipient' => 'Příjemce', - 'Region' => '', - 'Rep' => '', - 'Sales' => '', 'Select' => 'Vybrat', 'Shipment' => 'náklad', 'Source' => 'Zdroj', @@ -92,7 +73,5 @@ return ['Billing' => [ 'Type' => 'Typ', 'Types' => '', 'Upload' => 'nahrát', - 'Value' => '', - 'Variation' => 'Variace', 'Zip' => 'Zip', ]]; diff --git a/Theme/Backend/Lang/da.lang.php b/Theme/Backend/Lang/da.lang.php index 3495b7f..3aaa037 100755 --- a/Theme/Backend/Lang/da.lang.php +++ b/Theme/Backend/Lang/da.lang.php @@ -15,33 +15,22 @@ declare(strict_types=1); return ['Billing' => [ 'Address' => 'Adresse', 'Addresses' => 'Adresser', - 'Africa' => '', 'AlreadyPaid' => 'Allerede betalt', - 'America' => '', 'Amount' => '', - 'Analyse' => '', 'Archive' => '', - 'Articles' => '', - 'Asia' => '', - 'Attribute' => '', - 'BaseTime' => '', 'Billing' => '', 'Bills' => 'Regninger.', 'Bonus' => 'Bonus', - 'CIS' => '', 'Cashback' => 'Penge tilbage', 'City' => 'City.', 'Client' => 'Klient', 'ClientID' => 'Klient ID.', - 'ComparisonTime' => '', 'Confirmation' => 'Bekræftelse', 'Country' => 'Land', - 'CreateBill' => '', 'Created' => 'Oprettet', 'CreditCard' => 'Kreditkort', 'CreditNote' => 'Kreditnota', 'Currency' => '', - 'Customers' => '', 'Date' => 'Dato', 'Delivery' => 'Levering', 'DeliveryNote' => 'Levering note', @@ -50,13 +39,9 @@ return ['Billing' => [ 'DiscountP' => 'Rabat%', 'Due' => 'På grund', 'Email' => '', - 'Europe' => '', - 'Filter' => '', 'Freightage' => 'Gods', - 'General' => '', 'Gross' => 'Brutto', 'Invoice' => 'Faktura', - 'Invoices' => 'Fakturaer.', 'Item' => 'Vare', 'Items' => 'Varer', 'Language' => '', @@ -68,7 +53,6 @@ return ['Billing' => [ 'Net' => 'Net', 'Offer' => 'Tilbud', 'Original' => '', - 'Other' => '', 'Payment' => 'Betaling', 'PaymentPlan' => '', 'Postal' => 'Postal.', @@ -78,9 +62,6 @@ return ['Billing' => [ 'Profit' => 'Profit', 'Quantity' => 'Antal', 'Recipient' => 'Modtager', - 'Region' => '', - 'Rep' => '', - 'Sales' => '', 'Select' => 'Vælg', 'Shipment' => 'Forsendelse', 'Source' => 'Kilde', @@ -92,7 +73,5 @@ return ['Billing' => [ 'Type' => 'Type', 'Types' => '', 'Upload' => 'Upload', - 'Value' => '', - 'Variation' => 'Variation', 'Zip' => 'Zip.', ]]; diff --git a/Theme/Backend/Lang/de.lang.php b/Theme/Backend/Lang/de.lang.php index 876e11e..d1b05de 100755 --- a/Theme/Backend/Lang/de.lang.php +++ b/Theme/Backend/Lang/de.lang.php @@ -15,84 +15,79 @@ declare(strict_types=1); return ['Billing' => [ 'Address' => 'Adresse', 'Addresses' => 'Adressen', - 'Africa' => 'Africa', 'AlreadyPaid' => 'Bereits bezahlt', - 'America' => 'Amerika', 'Amount' => 'Betrag', - 'Analyse' => 'Analyse', 'Archive' => 'Archiev', - 'Articles' => 'Artikel', - 'Asia' => 'Asien', - 'Attribute' => 'Attribute', - 'BaseTime' => 'Basiszeit', + 'Internal' => 'Intern', + 'Error' => 'Fehler', 'Billing' => 'Rechnungsstellung', + 'External' => 'Extern', 'Bills' => 'Rechnungen', 'Bonus' => 'Bonus', - 'CIS' => 'CIS', 'Cashback' => 'Kennzeichnen', 'City' => 'Stadt', 'Client' => 'Kunde', 'ClientID' => 'Kunden ID', - 'ComparisonTime' => 'Vergleichszeit', 'Confirmation' => 'Bestätigung', 'Country' => 'Land', - 'CreateBill' => 'Erstelle Rechnung', 'Created' => 'Erstellt', 'CreditCard' => 'Kreditkarte', - 'CreditNote' => 'Gutschrift', + 'CreditNote' => 'Rechnungskorrektur', 'Currency' => 'Währung', - 'Customers' => 'Kunden', 'Date' => 'Datum', - 'Delivery' => 'Die Zustellung', + 'Delivery' => 'Lieferung', 'DeliveryNote' => 'Lieferschein', - 'DirectDebit' => 'DirectDeBIT.', + 'DirectDebit' => 'Direktüberweisung.', 'Discount' => 'Rabatt', 'DiscountP' => 'Rabatt %', 'Due' => 'Fällig', 'Email' => 'Email', - 'Europe' => 'Europa', - 'Filter' => 'Filter', 'Freightage' => 'Fracht', - 'General' => 'Allgemein', - 'Gross' => 'Grob', + 'Gross' => 'Brutto', 'Invoice' => 'Rechnung', - 'Invoices' => 'Rechnungen', 'Item' => 'Artikel', 'Items' => 'Produkte', 'Language' => 'Sprache', 'Log' => 'Protokoll', - 'Logs' => 'Protokoll', + 'Logs' => 'Protokolle', 'Media' => 'Medien', - 'MoneyTransfer' => 'Geldüberweisung', + 'MoneyTransfer' => 'Überweisung', 'Name' => 'Name', 'Net' => 'Netz', + 'Parse' => 'Rechnungserkennung', 'Offer' => 'Angebot', 'Original' => 'Original', - 'Other' => 'Sonstige', 'Payment' => 'Zahlung', 'PaymentPlan' => 'Zahlungsplan', 'Postal' => 'Post', 'Prepaid' => 'Vorausbezahlt', 'Preview' => 'Vorschau', 'Price' => 'Preis', - 'Profit' => 'Profitieren', + 'Profit' => 'Gewinn', 'Quantity' => 'Menge', 'Recipient' => 'Empfänger', - 'Region' => 'Region', - 'Rep' => 'Vertreter', - 'Sales' => 'Umsatz', 'Select' => 'Wählen', 'Shipment' => 'Sendung', 'Source' => 'Quelle', 'Supplier' => 'Anbieter', 'SupplierID' => 'Lieferanten ID', 'Tax' => 'Steuer', + 'TaxP' => 'Steuer %', + 'Margin' => 'Marge', 'TermsOfDelivery' => 'Lieferbedingungen', 'Total' => 'Gesamt', 'Type' => 'Typ', 'Types' => 'Typen', 'Upload' => 'Hochladen', - 'Value' => 'Wert', - 'Variation' => 'Variation', 'Zip' => 'Zip', + 'Files' => 'Files', + 'PaymentTerms' => 'Zahlungsbedingungen', + '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.', ]]; diff --git a/Theme/Backend/Lang/el.lang.php b/Theme/Backend/Lang/el.lang.php index f79d7b5..8bedd08 100755 --- a/Theme/Backend/Lang/el.lang.php +++ b/Theme/Backend/Lang/el.lang.php @@ -15,33 +15,22 @@ declare(strict_types=1); return ['Billing' => [ 'Address' => 'Διεύθυνση', 'Addresses' => 'Διευθύνσεις', - 'Africa' => '', 'AlreadyPaid' => 'Ήδη πληρωθεί', - 'America' => '', 'Amount' => '', - 'Analyse' => '', 'Archive' => '', - 'Articles' => '', - 'Asia' => '', - 'Attribute' => '', - 'BaseTime' => '', 'Billing' => '', 'Bills' => 'Λογαριασμοί', 'Bonus' => 'Δώρο', - 'CIS' => '', 'Cashback' => 'Επιστροφή μετρητών', 'City' => 'Πόλη', 'Client' => 'Πελάτης', 'ClientID' => 'ταυτότητα πελάτη', - 'ComparisonTime' => '', 'Confirmation' => 'Επιβεβαίωση', 'Country' => 'Χώρα', - 'CreateBill' => '', 'Created' => 'Δημιουργήθηκε', 'CreditCard' => 'Πιστωτική κάρτα', 'CreditNote' => 'Πιστωτικό σημείωμα', 'Currency' => '', - 'Customers' => '', 'Date' => 'Ημερομηνία', 'Delivery' => 'Διανομή', 'DeliveryNote' => 'Δελτίο παράδοσης', @@ -50,13 +39,9 @@ return ['Billing' => [ 'DiscountP' => 'Έκπτωση%', 'Due' => 'Λόγω', 'Email' => '', - 'Europe' => '', - 'Filter' => '', 'Freightage' => 'Ναύλος', - 'General' => '', 'Gross' => 'Ακαθάριστο', 'Invoice' => 'Τιμολόγιο', - 'Invoices' => 'Τιμολόγια', 'Item' => 'Είδος', 'Items' => 'Αντικείμενα', 'Language' => '', @@ -68,7 +53,6 @@ return ['Billing' => [ 'Net' => 'Καθαρά', 'Offer' => 'Προσφορά', 'Original' => '', - 'Other' => '', 'Payment' => 'Πληρωμή', 'PaymentPlan' => '', 'Postal' => 'Ταχυδρομικός', @@ -78,9 +62,6 @@ return ['Billing' => [ 'Profit' => 'Κέρδος', 'Quantity' => 'Ποσότητα', 'Recipient' => 'Παραλήπτης', - 'Region' => '', - 'Rep' => '', - 'Sales' => '', 'Select' => 'Επιλέγω', 'Shipment' => 'Αποστολή', 'Source' => 'Πηγή', @@ -92,7 +73,5 @@ return ['Billing' => [ 'Type' => 'Τύπος', 'Types' => '', 'Upload' => 'Μεταφόρτω', - 'Value' => '', - 'Variation' => 'Παραλλαγή', 'Zip' => 'Φερμουάρ', ]]; diff --git a/Theme/Backend/Lang/en.lang.php b/Theme/Backend/Lang/en.lang.php index 5aa7212..a7bbb44 100755 --- a/Theme/Backend/Lang/en.lang.php +++ b/Theme/Backend/Lang/en.lang.php @@ -15,33 +15,25 @@ declare(strict_types=1); return ['Billing' => [ 'Address' => 'Address', 'Addresses' => 'Addresses', - 'Africa' => 'Africa', 'AlreadyPaid' => 'Already Paid', - 'America' => 'America', 'Amount' => 'Amount', - 'Analyse' => 'Analyse', 'Archive' => 'Archive', - 'Articles' => 'Articles', - 'Asia' => 'Asia', - 'Attribute' => 'Attribute', - 'BaseTime' => 'Base time', + 'Internal' => 'Internal', + 'Error' => 'Error', 'Billing' => 'Billing', + 'External' => 'External', 'Bills' => 'Bills', 'Bonus' => 'Bonus', - 'CIS' => 'CIS', 'Cashback' => 'Cash Back', 'City' => 'City', 'Client' => 'Client', 'ClientID' => 'Client ID', - 'ComparisonTime' => 'Comparison time', 'Confirmation' => 'Confirmation', 'Country' => 'Country', - 'CreateBill' => 'Create Bill', 'Created' => 'Created', 'CreditCard' => 'CreditCard', 'CreditNote' => 'Credit Note', 'Currency' => 'Currency', - 'Customers' => 'Customers', 'Date' => 'Date', 'Delivery' => 'Delivery', 'DeliveryNote' => 'Delivery Note', @@ -50,13 +42,9 @@ return ['Billing' => [ 'DiscountP' => 'Discount %', 'Due' => 'Due', 'Email' => 'Email', - 'Europe' => 'Europe', - 'Filter' => 'Filter', 'Freightage' => 'Freightage', - 'General' => 'General', 'Gross' => 'Gross', 'Invoice' => 'Invoice', - 'Invoices' => 'Invoices', 'Item' => 'Item', 'Items' => 'Items', 'Language' => 'Language', @@ -66,9 +54,9 @@ return ['Billing' => [ 'MoneyTransfer' => 'Money Transfer', 'Name' => 'Name', 'Net' => 'Net', + 'Parse' => 'Invoice recognition', 'Offer' => 'Offer', 'Original' => 'Original', - 'Other' => 'Other', 'Payment' => 'Payment', 'PaymentPlan' => 'Payment Plan', 'Postal' => 'Postal', @@ -78,21 +66,28 @@ return ['Billing' => [ 'Profit' => 'Profit', 'Quantity' => 'Quantity', 'Recipient' => 'Recipient', - 'Region' => 'Region', - 'Rep' => 'Rep.', - 'Sales' => 'Sales', 'Select' => 'Select', 'Shipment' => 'Shipment', 'Source' => 'Source', 'Supplier' => 'Supplier', 'SupplierID' => 'Supplier ID', 'Tax' => 'Tax', + 'TaxP' => 'Tax %', + 'Margin' => 'Margin', 'TermsOfDelivery' => 'Terms Of Delivery', 'Total' => 'Total', 'Type' => 'Type', 'Types' => 'Types', 'Upload' => 'Upload', - 'Value' => 'Value', - 'Variation' => 'Variation', 'Zip' => 'Zip', + 'Files' => 'Files', + 'PaymentTerms' => 'Payment Terms', + 'ShippingTerms' => 'Shipping Terms', + 'PaymentTerm' => 'Payment 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.', ]]; diff --git a/Theme/Backend/Lang/es.lang.php b/Theme/Backend/Lang/es.lang.php index 4338aa8..42e0a37 100755 --- a/Theme/Backend/Lang/es.lang.php +++ b/Theme/Backend/Lang/es.lang.php @@ -15,33 +15,22 @@ declare(strict_types=1); return ['Billing' => [ 'Address' => 'Habla a', 'Addresses' => 'Direcciones', - 'Africa' => '', 'AlreadyPaid' => 'Ya pagado', - 'America' => '', 'Amount' => '', - 'Analyse' => '', 'Archive' => '', - 'Articles' => '', - 'Asia' => '', - 'Attribute' => '', - 'BaseTime' => '', 'Billing' => '', 'Bills' => 'Facturas', 'Bonus' => 'Prima', - 'CIS' => '', 'Cashback' => 'Devolución de dinero', 'City' => 'Ciudad', 'Client' => 'Cliente', 'ClientID' => 'Identificación del cliente', - 'ComparisonTime' => '', 'Confirmation' => 'Confirmación', 'Country' => 'País', - 'CreateBill' => '', 'Created' => 'Creado', 'CreditCard' => 'Tarjeta de crédito', 'CreditNote' => 'Nota de crédito', 'Currency' => '', - 'Customers' => '', 'Date' => 'Fecha', 'Delivery' => 'Entrega', 'DeliveryNote' => 'Nota de entrega', @@ -50,13 +39,9 @@ return ['Billing' => [ 'DiscountP' => 'Descuento%', 'Due' => 'Vencer', 'Email' => '', - 'Europe' => '', - 'Filter' => '', 'Freightage' => 'Flete', - 'General' => '', 'Gross' => 'Bruto', 'Invoice' => 'Factura', - 'Invoices' => 'Facturas', 'Item' => 'Articulo', 'Items' => 'Elementos', 'Language' => '', @@ -68,7 +53,6 @@ return ['Billing' => [ 'Net' => 'Red', 'Offer' => 'Oferta', 'Original' => '', - 'Other' => '', 'Payment' => 'Pago', 'PaymentPlan' => '', 'Postal' => 'Postal', @@ -78,9 +62,6 @@ return ['Billing' => [ 'Profit' => 'Lucro', 'Quantity' => 'Cantidad', 'Recipient' => 'Recipiente', - 'Region' => '', - 'Rep' => '', - 'Sales' => '', 'Select' => 'Seleccione', 'Shipment' => 'Envío', 'Source' => 'Fuente', @@ -92,7 +73,5 @@ return ['Billing' => [ 'Type' => 'Escribe', 'Types' => '', 'Upload' => 'Subir', - 'Value' => '', - 'Variation' => 'Variación', 'Zip' => 'Cremallera', ]]; diff --git a/Theme/Backend/Lang/fi.lang.php b/Theme/Backend/Lang/fi.lang.php index f026bb7..bc65818 100755 --- a/Theme/Backend/Lang/fi.lang.php +++ b/Theme/Backend/Lang/fi.lang.php @@ -15,33 +15,22 @@ declare(strict_types=1); return ['Billing' => [ 'Address' => 'Osoite', 'Addresses' => 'Osoitteet', - 'Africa' => '', 'AlreadyPaid' => 'Jo maksettu', - 'America' => '', 'Amount' => '', - 'Analyse' => '', 'Archive' => '', - 'Articles' => '', - 'Asia' => '', - 'Attribute' => '', - 'BaseTime' => '', 'Billing' => '', 'Bills' => 'Laskut', 'Bonus' => 'Bonus', - 'CIS' => '', 'Cashback' => 'Vaihtorahat', 'City' => 'Kaupunki', 'Client' => 'Asiakas', 'ClientID' => 'Asiakastunnus', - 'ComparisonTime' => '', 'Confirmation' => 'Vahvistus', 'Country' => 'Maa', - 'CreateBill' => '', 'Created' => 'Luotu', 'CreditCard' => 'Luottokortti', 'CreditNote' => 'Luottoluotto', 'Currency' => '', - 'Customers' => '', 'Date' => 'Päivämäärä', 'Delivery' => 'Toimitus', 'DeliveryNote' => 'Saapumisilmoitus', @@ -50,13 +39,9 @@ return ['Billing' => [ 'DiscountP' => 'Alennus %', 'Due' => 'Takia', 'Email' => '', - 'Europe' => '', - 'Filter' => '', 'Freightage' => 'Rahti', - 'General' => '', 'Gross' => 'Ällöttävä', 'Invoice' => 'Lasku', - 'Invoices' => 'Laskut', 'Item' => 'Kohde', 'Items' => 'Tuotteet', 'Language' => '', @@ -68,7 +53,6 @@ return ['Billing' => [ 'Net' => 'Netto', 'Offer' => 'Tarjous', 'Original' => '', - 'Other' => '', 'Payment' => 'Maksu', 'PaymentPlan' => '', 'Postal' => 'Posti-', @@ -78,9 +62,6 @@ return ['Billing' => [ 'Profit' => 'Voitto', 'Quantity' => 'Määrä', 'Recipient' => 'Vastaanottaja', - 'Region' => '', - 'Rep' => '', - 'Sales' => '', 'Select' => 'Valitse', 'Shipment' => 'Lähetys', 'Source' => 'Lähde', @@ -92,7 +73,5 @@ return ['Billing' => [ 'Type' => 'Tyyppi', 'Types' => '', 'Upload' => 'Ladata', - 'Value' => '', - 'Variation' => 'Vaihtelu', 'Zip' => 'Postinumero', ]]; diff --git a/Theme/Backend/Lang/fr.lang.php b/Theme/Backend/Lang/fr.lang.php index bfeff4d..87caaf1 100755 --- a/Theme/Backend/Lang/fr.lang.php +++ b/Theme/Backend/Lang/fr.lang.php @@ -15,33 +15,22 @@ declare(strict_types=1); return ['Billing' => [ 'Address' => 'Adresse', 'Addresses' => 'Adresses', - 'Africa' => '', 'AlreadyPaid' => 'Déjà payé', - 'America' => '', 'Amount' => '', - 'Analyse' => '', 'Archive' => '', - 'Articles' => '', - 'Asia' => '', - 'Attribute' => '', - 'BaseTime' => '', 'Billing' => '', 'Bills' => 'Factures', 'Bonus' => 'Prime', - 'CIS' => '', 'Cashback' => 'Remise en argent', 'City' => 'Ville', 'Client' => 'Client', 'ClientID' => 'identité du client', - 'ComparisonTime' => '', 'Confirmation' => 'Confirmation', 'Country' => 'Pays', - 'CreateBill' => '', 'Created' => 'Établi', 'CreditCard' => 'Carte de crédit', 'CreditNote' => 'Note de crédit', 'Currency' => '', - 'Customers' => '', 'Date' => 'Date', 'Delivery' => 'Livraison', 'DeliveryNote' => 'Bon de livraison', @@ -50,13 +39,9 @@ return ['Billing' => [ 'DiscountP' => 'Remise %', 'Due' => 'Dû', 'Email' => '', - 'Europe' => '', - 'Filter' => '', 'Freightage' => 'Fret', - 'General' => '', 'Gross' => 'Brut', 'Invoice' => 'Facture d\'achat', - 'Invoices' => 'Factures', 'Item' => 'Article', 'Items' => 'Articles', 'Language' => '', @@ -68,7 +53,6 @@ return ['Billing' => [ 'Net' => 'Rapporter', 'Offer' => 'Offrir', 'Original' => '', - 'Other' => '', 'Payment' => 'Paiement', 'PaymentPlan' => '', 'Postal' => 'Postal', @@ -78,9 +62,6 @@ return ['Billing' => [ 'Profit' => 'Profit', 'Quantity' => 'Quantité', 'Recipient' => 'Destinataire', - 'Region' => '', - 'Rep' => '', - 'Sales' => '', 'Select' => 'Sélectionner', 'Shipment' => 'Expédition', 'Source' => 'La source', @@ -92,7 +73,5 @@ return ['Billing' => [ 'Type' => 'Taper', 'Types' => '', 'Upload' => 'Télécharger', - 'Value' => '', - 'Variation' => 'Variation', 'Zip' => 'Zipper', ]]; diff --git a/Theme/Backend/Lang/hu.lang.php b/Theme/Backend/Lang/hu.lang.php index f27a41b..67b5a86 100755 --- a/Theme/Backend/Lang/hu.lang.php +++ b/Theme/Backend/Lang/hu.lang.php @@ -15,33 +15,22 @@ declare(strict_types=1); return ['Billing' => [ 'Address' => 'Cím', 'Addresses' => 'Címek', - 'Africa' => '', 'AlreadyPaid' => 'Fizetve', - 'America' => '', 'Amount' => '', - 'Analyse' => '', 'Archive' => '', - 'Articles' => '', - 'Asia' => '', - 'Attribute' => '', - 'BaseTime' => '', 'Billing' => '', 'Bills' => 'Számlák', 'Bonus' => 'Bónusz', - 'CIS' => '', 'Cashback' => 'Pénzvisszafizetés', 'City' => 'Város', 'Client' => 'Ügyfél', 'ClientID' => 'Ügyfélazonosító', - 'ComparisonTime' => '', 'Confirmation' => 'Megerősítés', 'Country' => 'Ország', - 'CreateBill' => '', 'Created' => 'Létrehozott', 'CreditCard' => 'Hitelkártya', 'CreditNote' => 'Jóváírás', 'Currency' => '', - 'Customers' => '', 'Date' => 'Dátum', 'Delivery' => 'Szállítás', 'DeliveryNote' => 'Fuvarlevél', @@ -50,13 +39,9 @@ return ['Billing' => [ 'DiscountP' => 'Árengedmény%', 'Due' => 'Esedékes', 'Email' => '', - 'Europe' => '', - 'Filter' => '', 'Freightage' => 'Fuvardíj', - 'General' => '', 'Gross' => 'Bruttó', 'Invoice' => 'Számla', - 'Invoices' => 'Számlák', 'Item' => 'Tétel', 'Items' => 'Elemek', 'Language' => '', @@ -68,7 +53,6 @@ return ['Billing' => [ 'Net' => 'Háló', 'Offer' => 'Ajánlat', 'Original' => '', - 'Other' => '', 'Payment' => 'Fizetés', 'PaymentPlan' => '', 'Postal' => 'Postai', @@ -78,9 +62,6 @@ return ['Billing' => [ 'Profit' => 'Nyereség', 'Quantity' => 'Mennyiség', 'Recipient' => 'Befogadó', - 'Region' => '', - 'Rep' => '', - 'Sales' => '', 'Select' => 'Kiválaszt', 'Shipment' => 'Szállítmány', 'Source' => 'Forrás', @@ -92,7 +73,5 @@ return ['Billing' => [ 'Type' => 'típus', 'Types' => '', 'Upload' => 'Feltöltés', - 'Value' => '', - 'Variation' => 'Variáció', 'Zip' => 'Postai irányítószám', ]]; diff --git a/Theme/Backend/Lang/it.lang.php b/Theme/Backend/Lang/it.lang.php index a4f4b6b..072baed 100755 --- a/Theme/Backend/Lang/it.lang.php +++ b/Theme/Backend/Lang/it.lang.php @@ -15,33 +15,22 @@ declare(strict_types=1); return ['Billing' => [ 'Address' => 'Indirizzo', 'Addresses' => 'Indirizzi', - 'Africa' => '', 'AlreadyPaid' => 'Già pagato', - 'America' => '', 'Amount' => '', - 'Analyse' => '', 'Archive' => '', - 'Articles' => '', - 'Asia' => '', - 'Attribute' => '', - 'BaseTime' => '', 'Billing' => '', 'Bills' => 'Fatture', 'Bonus' => 'Bonus.', - 'CIS' => '', 'Cashback' => 'Rimborso', 'City' => 'Città', 'Client' => 'Cliente', 'ClientID' => 'Identificativo cliente', - 'ComparisonTime' => '', 'Confirmation' => 'Conferma', 'Country' => 'Nazione', - 'CreateBill' => '', 'Created' => 'Creato', 'CreditCard' => 'Carta di credito', 'CreditNote' => 'Nota di credito', 'Currency' => '', - 'Customers' => '', 'Date' => 'Data', 'Delivery' => 'Consegna', 'DeliveryNote' => 'Bolla d\'accompagnamento', @@ -50,13 +39,9 @@ return ['Billing' => [ 'DiscountP' => 'Sconto%', 'Due' => 'Dovuto', 'Email' => '', - 'Europe' => '', - 'Filter' => '', 'Freightage' => 'Liberare', - 'General' => '', 'Gross' => 'Schifoso', 'Invoice' => 'Fattura', - 'Invoices' => 'Fatture', 'Item' => 'Elemento', 'Items' => 'Elementi', 'Language' => '', @@ -68,7 +53,6 @@ return ['Billing' => [ 'Net' => 'Rete', 'Offer' => 'Offerta', 'Original' => '', - 'Other' => '', 'Payment' => 'Pagamento', 'PaymentPlan' => '', 'Postal' => 'postale', @@ -78,9 +62,6 @@ return ['Billing' => [ 'Profit' => 'Profitto', 'Quantity' => 'Quantità', 'Recipient' => 'Destinatario', - 'Region' => '', - 'Rep' => '', - 'Sales' => '', 'Select' => 'Selezionare', 'Shipment' => 'Spedizione', 'Source' => 'Fonte', @@ -92,7 +73,5 @@ return ['Billing' => [ 'Type' => 'Tipo', 'Types' => '', 'Upload' => 'Caricamento', - 'Value' => '', - 'Variation' => 'Variazione', 'Zip' => 'Cerniera lampo', ]]; diff --git a/Theme/Backend/Lang/ja.lang.php b/Theme/Backend/Lang/ja.lang.php index 301905e..33b1d1f 100755 --- a/Theme/Backend/Lang/ja.lang.php +++ b/Theme/Backend/Lang/ja.lang.php @@ -15,33 +15,22 @@ declare(strict_types=1); return ['Billing' => [ 'Address' => '住所', 'Addresses' => 'アドレス', - 'Africa' => '', 'AlreadyPaid' => '既に支払いました', - 'America' => '', 'Amount' => '', - 'Analyse' => '', 'Archive' => '', - 'Articles' => '', - 'Asia' => '', - 'Attribute' => '', - 'BaseTime' => '', 'Billing' => '', 'Bills' => 'bill', 'Bonus' => 'ボーナス', - 'CIS' => '', 'Cashback' => 'キャッシュバック', 'City' => '市', 'Client' => 'クライアント', 'ClientID' => 'クライアントID', - 'ComparisonTime' => '', 'Confirmation' => '確認', 'Country' => '国', - 'CreateBill' => '', 'Created' => '作成した', 'CreditCard' => 'クレジットカード', 'CreditNote' => 'クレジットノート', 'Currency' => '', - 'Customers' => '', 'Date' => '日にち', 'Delivery' => '配達', 'DeliveryNote' => '配達メモ', @@ -50,13 +39,9 @@ return ['Billing' => [ 'DiscountP' => '割引 %', 'Due' => '期限', 'Email' => '', - 'Europe' => '', - 'Filter' => '', 'Freightage' => 'fre fre', - 'General' => '', 'Gross' => 'きもい', 'Invoice' => '請求書', - 'Invoices' => '請求書', 'Item' => 'アイテム', 'Items' => 'アイテム', 'Language' => '', @@ -68,7 +53,6 @@ return ['Billing' => [ 'Net' => 'ネット', 'Offer' => 'オファー', 'Original' => '', - 'Other' => '', 'Payment' => '支払い', 'PaymentPlan' => '', 'Postal' => '郵便', @@ -78,9 +62,6 @@ return ['Billing' => [ 'Profit' => '利益', 'Quantity' => '量', 'Recipient' => '受信者', - 'Region' => '', - 'Rep' => '', - 'Sales' => '', 'Select' => '選択する', 'Shipment' => '出荷', 'Source' => 'ソース', @@ -92,7 +73,5 @@ return ['Billing' => [ 'Type' => 'タイプ', 'Types' => '', 'Upload' => 'アップロード', - 'Value' => '', - 'Variation' => '変化', 'Zip' => 'ジップ', ]]; diff --git a/Theme/Backend/Lang/ko.lang.php b/Theme/Backend/Lang/ko.lang.php index f168827..afb184a 100755 --- a/Theme/Backend/Lang/ko.lang.php +++ b/Theme/Backend/Lang/ko.lang.php @@ -15,33 +15,22 @@ declare(strict_types=1); return ['Billing' => [ 'Address' => '주소', 'Addresses' => '구애', - 'Africa' => '', 'AlreadyPaid' => '이미 지불', - 'America' => '', 'Amount' => '', - 'Analyse' => '', 'Archive' => '', - 'Articles' => '', - 'Asia' => '', - 'Attribute' => '', - 'BaseTime' => '', 'Billing' => '', 'Bills' => '청구서', 'Bonus' => '보너스', - 'CIS' => '', 'Cashback' => '현금', 'City' => '도시', 'Client' => '고객', 'ClientID' => '클라이언트 ID.', - 'ComparisonTime' => '', 'Confirmation' => '확인', 'Country' => '국가', - 'CreateBill' => '', 'Created' => '만들어진', 'CreditCard' => '신용 카드', 'CreditNote' => '신용 노트', 'Currency' => '', - 'Customers' => '', 'Date' => '날짜', 'Delivery' => '배달', 'DeliveryNote' => '배달 메모', @@ -50,13 +39,9 @@ return ['Billing' => [ 'DiscountP' => '할인 %', 'Due' => '로 인한', 'Email' => '', - 'Europe' => '', - 'Filter' => '', 'Freightage' => '화물', - 'General' => '', 'Gross' => '역겨운', 'Invoice' => '송장', - 'Invoices' => '송장', 'Item' => '안건', 'Items' => '항목', 'Language' => '', @@ -68,7 +53,6 @@ return ['Billing' => [ 'Net' => '그물', 'Offer' => '권하다', 'Original' => '', - 'Other' => '', 'Payment' => '지불', 'PaymentPlan' => '', 'Postal' => '우편 엽서', @@ -78,9 +62,6 @@ return ['Billing' => [ 'Profit' => '이익', 'Quantity' => '수량', 'Recipient' => '받는 사람', - 'Region' => '', - 'Rep' => '', - 'Sales' => '', 'Select' => '선택하다', 'Shipment' => '선적', 'Source' => '원천', @@ -92,7 +73,5 @@ return ['Billing' => [ 'Type' => '유형', 'Types' => '', 'Upload' => '업로드', - 'Value' => '', - 'Variation' => '변화', 'Zip' => '지퍼', ]]; diff --git a/Theme/Backend/Lang/no.lang.php b/Theme/Backend/Lang/no.lang.php index 5211e39..e337c9d 100755 --- a/Theme/Backend/Lang/no.lang.php +++ b/Theme/Backend/Lang/no.lang.php @@ -15,33 +15,22 @@ declare(strict_types=1); return ['Billing' => [ 'Address' => 'Adresse', 'Addresses' => 'Adresser', - 'Africa' => '', 'AlreadyPaid' => 'Allerede betalt', - 'America' => '', 'Amount' => '', - 'Analyse' => '', 'Archive' => '', - 'Articles' => '', - 'Asia' => '', - 'Attribute' => '', - 'BaseTime' => '', 'Billing' => '', 'Bills' => 'Regninger', 'Bonus' => 'Bonus', - 'CIS' => '', 'Cashback' => 'Penger tilbake', 'City' => 'By', 'Client' => 'Klient', 'ClientID' => 'klient-ID', - 'ComparisonTime' => '', 'Confirmation' => 'Bekreftelse', 'Country' => 'Land', - 'CreateBill' => '', 'Created' => 'Opprettet', 'CreditCard' => 'Kredittkort', 'CreditNote' => 'Kredittnota', 'Currency' => '', - 'Customers' => '', 'Date' => 'Dato', 'Delivery' => 'Leveranse', 'DeliveryNote' => 'Leveringsnotat', @@ -50,13 +39,9 @@ return ['Billing' => [ 'DiscountP' => 'Rabatt%', 'Due' => 'På grunn av det', 'Email' => '', - 'Europe' => '', - 'Filter' => '', 'Freightage' => 'Frakt', - 'General' => '', 'Gross' => 'Ekkelt', 'Invoice' => 'Faktura', - 'Invoices' => 'Fakturaer', 'Item' => 'Punkt', 'Items' => 'Elementer', 'Language' => '', @@ -68,7 +53,6 @@ return ['Billing' => [ 'Net' => 'Nett', 'Offer' => 'By på', 'Original' => '', - 'Other' => '', 'Payment' => 'innbetaling', 'PaymentPlan' => '', 'Postal' => 'Postal.', @@ -78,9 +62,6 @@ return ['Billing' => [ 'Profit' => 'Profitt', 'Quantity' => 'Mengde', 'Recipient' => 'Mottaker', - 'Region' => '', - 'Rep' => '', - 'Sales' => '', 'Select' => 'Plukke ut', 'Shipment' => 'Forsendelse', 'Source' => 'Kilde', @@ -92,7 +73,5 @@ return ['Billing' => [ 'Type' => 'Type', 'Types' => '', 'Upload' => 'Laste opp', - 'Value' => '', - 'Variation' => 'Variasjon', 'Zip' => 'Glidelås', ]]; diff --git a/Theme/Backend/Lang/pl.lang.php b/Theme/Backend/Lang/pl.lang.php index 90fcfa8..8fa2223 100755 --- a/Theme/Backend/Lang/pl.lang.php +++ b/Theme/Backend/Lang/pl.lang.php @@ -15,33 +15,22 @@ declare(strict_types=1); return ['Billing' => [ 'Address' => 'Adres', 'Addresses' => 'Adresy', - 'Africa' => '', 'AlreadyPaid' => 'Już zapłacone', - 'America' => '', 'Amount' => '', - 'Analyse' => '', 'Archive' => '', - 'Articles' => '', - 'Asia' => '', - 'Attribute' => '', - 'BaseTime' => '', 'Billing' => '', 'Bills' => 'Rachunki', 'Bonus' => 'Premia', - 'CIS' => '', 'Cashback' => 'Zwrot gotówki', 'City' => 'Miasto', 'Client' => 'Klient', 'ClientID' => 'Identyfikator klienta', - 'ComparisonTime' => '', 'Confirmation' => 'Potwierdzenie', 'Country' => 'Kraj', - 'CreateBill' => '', 'Created' => 'Utworzony', 'CreditCard' => 'Karta kredytowa', 'CreditNote' => 'Uwaga kredytowa', 'Currency' => '', - 'Customers' => '', 'Date' => 'Data', 'Delivery' => 'Dostawa', 'DeliveryNote' => 'Dowód dostawy', @@ -50,13 +39,9 @@ return ['Billing' => [ 'DiscountP' => 'Zniżka %', 'Due' => 'Z powodu', 'Email' => '', - 'Europe' => '', - 'Filter' => '', 'Freightage' => 'Frachtowanie', - 'General' => '', 'Gross' => 'Brutto', 'Invoice' => 'Faktura', - 'Invoices' => 'Faktury', 'Item' => 'Przedmiot', 'Items' => 'Przedmiotów', 'Language' => '', @@ -68,7 +53,6 @@ return ['Billing' => [ 'Net' => 'Internet', 'Offer' => 'Oferta', 'Original' => '', - 'Other' => '', 'Payment' => 'Zapłata', 'PaymentPlan' => '', 'Postal' => 'Pocztowy', @@ -78,9 +62,6 @@ return ['Billing' => [ 'Profit' => 'Zysk', 'Quantity' => 'Ilość', 'Recipient' => 'Odbiorca', - 'Region' => '', - 'Rep' => '', - 'Sales' => '', 'Select' => 'Wybierać', 'Shipment' => 'Wysyłka', 'Source' => 'Źródło', @@ -92,7 +73,5 @@ return ['Billing' => [ 'Type' => 'Rodzaj', 'Types' => '', 'Upload' => 'Wgrywać', - 'Value' => '', - 'Variation' => 'Zmiana', 'Zip' => 'Zamek błyskawiczny', ]]; diff --git a/Theme/Backend/Lang/pt.lang.php b/Theme/Backend/Lang/pt.lang.php index b411655..7fd6f35 100755 --- a/Theme/Backend/Lang/pt.lang.php +++ b/Theme/Backend/Lang/pt.lang.php @@ -15,33 +15,22 @@ declare(strict_types=1); return ['Billing' => [ 'Address' => 'Endereço', 'Addresses' => 'Endereços', - 'Africa' => '', 'AlreadyPaid' => 'Já pago', - 'America' => '', 'Amount' => '', - 'Analyse' => '', 'Archive' => '', - 'Articles' => '', - 'Asia' => '', - 'Attribute' => '', - 'BaseTime' => '', 'Billing' => '', 'Bills' => 'Notas', 'Bonus' => 'Bônus', - 'CIS' => '', 'Cashback' => 'Dinheiro de volta', 'City' => 'Cidade', 'Client' => 'Cliente', 'ClientID' => 'ID do Cliente', - 'ComparisonTime' => '', 'Confirmation' => 'Confirmação', 'Country' => 'País', - 'CreateBill' => '', 'Created' => 'Criado', 'CreditCard' => 'Cartão de crédito', 'CreditNote' => 'Nota de crédito', 'Currency' => '', - 'Customers' => '', 'Date' => 'Encontro', 'Delivery' => 'Entrega', 'DeliveryNote' => 'Nota de entrega', @@ -50,13 +39,9 @@ return ['Billing' => [ 'DiscountP' => 'Desconto%', 'Due' => 'Vencimento', 'Email' => '', - 'Europe' => '', - 'Filter' => '', 'Freightage' => 'Freightage', - 'General' => '', 'Gross' => 'Bruto', 'Invoice' => 'Fatura', - 'Invoices' => 'Faturas', 'Item' => 'Item', 'Items' => 'Itens', 'Language' => '', @@ -68,7 +53,6 @@ return ['Billing' => [ 'Net' => 'Internet', 'Offer' => 'Oferta', 'Original' => '', - 'Other' => '', 'Payment' => 'Pagamento', 'PaymentPlan' => '', 'Postal' => 'Postal', @@ -78,9 +62,6 @@ return ['Billing' => [ 'Profit' => 'Lucro', 'Quantity' => 'Quantidade', 'Recipient' => 'Destinatário', - 'Region' => '', - 'Rep' => '', - 'Sales' => '', 'Select' => 'Selecione.', 'Shipment' => 'envio', 'Source' => 'Fonte', @@ -92,7 +73,5 @@ return ['Billing' => [ 'Type' => 'Modelo', 'Types' => '', 'Upload' => 'Envio', - 'Value' => '', - 'Variation' => 'Variação', 'Zip' => 'Fecho eclair', ]]; diff --git a/Theme/Backend/Lang/ru.lang.php b/Theme/Backend/Lang/ru.lang.php index 385b0a8..a71b5b5 100755 --- a/Theme/Backend/Lang/ru.lang.php +++ b/Theme/Backend/Lang/ru.lang.php @@ -15,33 +15,22 @@ declare(strict_types=1); return ['Billing' => [ 'Address' => 'Адрес', 'Addresses' => 'Адреса', - 'Africa' => '', 'AlreadyPaid' => 'Уже оплачено', - 'America' => '', 'Amount' => '', - 'Analyse' => '', 'Archive' => '', - 'Articles' => '', - 'Asia' => '', - 'Attribute' => '', - 'BaseTime' => '', 'Billing' => '', 'Bills' => 'Счета', 'Bonus' => 'Бонус', - 'CIS' => '', 'Cashback' => 'Возврат наличных', 'City' => 'Город', 'Client' => 'Клиент', 'ClientID' => 'ID клиента', - 'ComparisonTime' => '', 'Confirmation' => 'Подтверждение', 'Country' => 'Страна', - 'CreateBill' => '', 'Created' => 'Созданный', 'CreditCard' => 'Кредитная карта', 'CreditNote' => 'Кредитная нота', 'Currency' => '', - 'Customers' => '', 'Date' => 'Дата', 'Delivery' => 'Доставка', 'DeliveryNote' => 'Накладная', @@ -50,13 +39,9 @@ return ['Billing' => [ 'DiscountP' => 'Скидка %', 'Due' => 'Должное', 'Email' => '', - 'Europe' => '', - 'Filter' => '', 'Freightage' => 'Фрахты', - 'General' => '', 'Gross' => 'Валовой', 'Invoice' => 'Счет', - 'Invoices' => 'Счета', 'Item' => 'Пункт', 'Items' => 'Предметы', 'Language' => '', @@ -68,7 +53,6 @@ return ['Billing' => [ 'Net' => 'Сеть', 'Offer' => 'Предложение', 'Original' => '', - 'Other' => '', 'Payment' => 'Оплата', 'PaymentPlan' => '', 'Postal' => 'Почтовый', @@ -78,9 +62,6 @@ return ['Billing' => [ 'Profit' => 'Выгода', 'Quantity' => 'Количество', 'Recipient' => 'Получатель', - 'Region' => '', - 'Rep' => '', - 'Sales' => '', 'Select' => 'Выбирать', 'Shipment' => 'Отгрузка', 'Source' => 'Источник', @@ -92,7 +73,5 @@ return ['Billing' => [ 'Type' => 'Тип', 'Types' => '', 'Upload' => 'Загрузить', - 'Value' => '', - 'Variation' => 'Вариация', 'Zip' => 'Zip.', ]]; diff --git a/Theme/Backend/Lang/sv.lang.php b/Theme/Backend/Lang/sv.lang.php index 22c7ea2..1a0d370 100755 --- a/Theme/Backend/Lang/sv.lang.php +++ b/Theme/Backend/Lang/sv.lang.php @@ -15,33 +15,22 @@ declare(strict_types=1); return ['Billing' => [ 'Address' => 'Adress', 'Addresses' => 'Adresser', - 'Africa' => '', 'AlreadyPaid' => 'Redan betalat', - 'America' => '', 'Amount' => '', - 'Analyse' => '', 'Archive' => '', - 'Articles' => '', - 'Asia' => '', - 'Attribute' => '', - 'BaseTime' => '', 'Billing' => '', 'Bills' => 'Räkningar', 'Bonus' => 'Bonus', - 'CIS' => '', 'Cashback' => 'Pengar tillbaka', 'City' => 'Stad', 'Client' => 'Klient', 'ClientID' => 'Klient ID', - 'ComparisonTime' => '', 'Confirmation' => 'Bekräftelse', 'Country' => 'Land', - 'CreateBill' => '', 'Created' => 'Skapad', 'CreditCard' => 'Kreditkort', 'CreditNote' => 'Kreditanteckning', 'Currency' => '', - 'Customers' => '', 'Date' => 'Datum', 'Delivery' => 'Leverans', 'DeliveryNote' => 'Leveransanteckning', @@ -50,13 +39,9 @@ return ['Billing' => [ 'DiscountP' => 'Rabatt%', 'Due' => 'På grund av', 'Email' => '', - 'Europe' => '', - 'Filter' => '', 'Freightage' => 'Frakt', - 'General' => '', 'Gross' => 'Äckligt', 'Invoice' => 'Faktura', - 'Invoices' => 'Fakturor', 'Item' => 'Artikel', 'Items' => 'Objekt', 'Language' => '', @@ -68,7 +53,6 @@ return ['Billing' => [ 'Net' => 'Netto', 'Offer' => 'Erbjudande', 'Original' => '', - 'Other' => '', 'Payment' => 'Betalning', 'PaymentPlan' => '', 'Postal' => 'Post', @@ -78,9 +62,6 @@ return ['Billing' => [ 'Profit' => 'Vinst', 'Quantity' => 'Kvantitet', 'Recipient' => 'Mottagare', - 'Region' => '', - 'Rep' => '', - 'Sales' => '', 'Select' => 'Välj', 'Shipment' => 'Sändning', 'Source' => 'Källa', @@ -92,7 +73,5 @@ return ['Billing' => [ 'Type' => 'Typ', 'Types' => '', 'Upload' => 'Ladda upp', - 'Value' => '', - 'Variation' => 'Variation', 'Zip' => 'Blixtlås', ]]; diff --git a/Theme/Backend/Lang/th.lang.php b/Theme/Backend/Lang/th.lang.php index 82955dc..076704f 100755 --- a/Theme/Backend/Lang/th.lang.php +++ b/Theme/Backend/Lang/th.lang.php @@ -15,33 +15,22 @@ declare(strict_types=1); return ['Billing' => [ 'Address' => 'ที่อยู่', 'Addresses' => 'ที่อยู่', - 'Africa' => '', 'AlreadyPaid' => 'จ่ายแล้ว', - 'America' => '', 'Amount' => '', - 'Analyse' => '', 'Archive' => '', - 'Articles' => '', - 'Asia' => '', - 'Attribute' => '', - 'BaseTime' => '', 'Billing' => '', 'Bills' => 'การเรียกเก็บเงิน', 'Bonus' => 'โบนัส', - 'CIS' => '', 'Cashback' => 'เงินคืน', 'City' => 'เมือง', 'Client' => 'ลูกค้า', 'ClientID' => 'รหัสลูกค้า', - 'ComparisonTime' => '', 'Confirmation' => 'การยืนยัน', 'Country' => 'ประเทศ', - 'CreateBill' => '', 'Created' => 'สร้าง', 'CreditCard' => 'บัตรเครดิต', 'CreditNote' => 'ใบลดหนี้', 'Currency' => '', - 'Customers' => '', 'Date' => 'วันที่', 'Delivery' => 'จัดส่ง', 'DeliveryNote' => 'บันทึกการส่งมอบ', @@ -50,13 +39,9 @@ return ['Billing' => [ 'DiscountP' => 'การลดราคา %', 'Due' => 'เนื่องจาก', 'Email' => '', - 'Europe' => '', - 'Filter' => '', 'Freightage' => 'การขนส่งสินค้า', - 'General' => '', 'Gross' => 'ทั้งหมด', 'Invoice' => 'ใบแจ้งหนี้', - 'Invoices' => 'ใบแจ้งหนี้', 'Item' => 'สิ่งของ', 'Items' => 'รายการ', 'Language' => '', @@ -68,7 +53,6 @@ return ['Billing' => [ 'Net' => 'สุทธิ', 'Offer' => 'เสนอ', 'Original' => '', - 'Other' => '', 'Payment' => 'การชำระเงิน', 'PaymentPlan' => '', 'Postal' => 'เกี่ยวกับการไปรษณีย์', @@ -78,9 +62,6 @@ return ['Billing' => [ 'Profit' => 'กำไร', 'Quantity' => 'ปริมาณ', 'Recipient' => 'ผู้รับ', - 'Region' => '', - 'Rep' => '', - 'Sales' => '', 'Select' => 'เลือก', 'Shipment' => 'การจัดส่ง', 'Source' => 'แหล่งที่มา', @@ -92,7 +73,5 @@ return ['Billing' => [ 'Type' => 'พิมพ์', 'Types' => '', 'Upload' => 'ที่อัพโหลด', - 'Value' => '', - 'Variation' => 'การเปลี่ยนแปลง', 'Zip' => 'ซิป', ]]; diff --git a/Theme/Backend/Lang/tr.lang.php b/Theme/Backend/Lang/tr.lang.php index 7d4a468..7782978 100755 --- a/Theme/Backend/Lang/tr.lang.php +++ b/Theme/Backend/Lang/tr.lang.php @@ -15,33 +15,22 @@ declare(strict_types=1); return ['Billing' => [ 'Address' => 'Adres', 'Addresses' => 'Adresler', - 'Africa' => '', 'AlreadyPaid' => 'Zaten ödendi', - 'America' => '', 'Amount' => '', - 'Analyse' => '', 'Archive' => '', - 'Articles' => '', - 'Asia' => '', - 'Attribute' => '', - 'BaseTime' => '', 'Billing' => '', 'Bills' => 'Faturalar', 'Bonus' => 'Bonus', - 'CIS' => '', 'Cashback' => 'Nakit para', 'City' => 'Şehir', 'Client' => 'Müşteri', 'ClientID' => 'Müşteri Kimliği', - 'ComparisonTime' => '', 'Confirmation' => 'Onayla', 'Country' => 'Ülke', - 'CreateBill' => '', 'Created' => 'Yaratılmış', 'CreditCard' => 'Kredi kartı', 'CreditNote' => 'Kredi notu', 'Currency' => '', - 'Customers' => '', 'Date' => 'Tarih', 'Delivery' => 'Teslimat', 'DeliveryNote' => 'Teslimat notu', @@ -50,13 +39,9 @@ return ['Billing' => [ 'DiscountP' => 'İndirim %', 'Due' => 'Vadesi dolmuş', 'Email' => '', - 'Europe' => '', - 'Filter' => '', 'Freightage' => 'Yük', - 'General' => '', 'Gross' => 'Brüt', 'Invoice' => 'Fatura', - 'Invoices' => 'Faturalar', 'Item' => 'Kalem', 'Items' => 'Öğeler', 'Language' => '', @@ -68,7 +53,6 @@ return ['Billing' => [ 'Net' => 'Ağ', 'Offer' => 'Teklif', 'Original' => '', - 'Other' => '', 'Payment' => 'Ödeme', 'PaymentPlan' => '', 'Postal' => 'Posta', @@ -78,9 +62,6 @@ return ['Billing' => [ 'Profit' => 'Kâr', 'Quantity' => 'Miktar', 'Recipient' => 'Alıcı', - 'Region' => '', - 'Rep' => '', - 'Sales' => '', 'Select' => 'Seçme', 'Shipment' => 'gönderi', 'Source' => 'Kaynak', @@ -92,7 +73,5 @@ return ['Billing' => [ 'Type' => 'Tip', 'Types' => '', 'Upload' => 'Yüklemek', - 'Value' => '', - 'Variation' => 'varyasyon', 'Zip' => 'Zip', ]]; diff --git a/Theme/Backend/Lang/uk.lang.php b/Theme/Backend/Lang/uk.lang.php index bd1dafd..f543a17 100755 --- a/Theme/Backend/Lang/uk.lang.php +++ b/Theme/Backend/Lang/uk.lang.php @@ -15,33 +15,22 @@ declare(strict_types=1); return ['Billing' => [ 'Address' => 'Адреса', 'Addresses' => 'Адреси', - 'Africa' => '', 'AlreadyPaid' => 'Вже оплачено', - 'America' => '', 'Amount' => '', - 'Analyse' => '', 'Archive' => '', - 'Articles' => '', - 'Asia' => '', - 'Attribute' => '', - 'BaseTime' => '', 'Billing' => '', 'Bills' => 'Векселі', 'Bonus' => 'Бонус', - 'CIS' => '', 'Cashback' => 'Готівка', 'City' => 'Місто', 'Client' => 'Клієнт', 'ClientID' => 'Ідентифікатор клієнта', - 'ComparisonTime' => '', 'Confirmation' => 'Підтвердження', 'Country' => 'Країна', - 'CreateBill' => '', 'Created' => 'Створений', 'CreditCard' => 'Кредитна карта', 'CreditNote' => 'Кредитове авізо', 'Currency' => '', - 'Customers' => '', 'Date' => 'Дата', 'Delivery' => 'Доставка', 'DeliveryNote' => 'Накладна', @@ -50,13 +39,9 @@ return ['Billing' => [ 'DiscountP' => 'Знижка%', 'Due' => 'Заборгованість', 'Email' => '', - 'Europe' => '', - 'Filter' => '', 'Freightage' => 'Фрахт', - 'General' => '', 'Gross' => 'Валовий', 'Invoice' => 'Рахунок-фактура', - 'Invoices' => 'Рахунки-фактури', 'Item' => 'Елемент', 'Items' => 'Предмети', 'Language' => '', @@ -68,7 +53,6 @@ return ['Billing' => [ 'Net' => 'Сітка', 'Offer' => 'Пропозиція', 'Original' => '', - 'Other' => '', 'Payment' => 'Платіж', 'PaymentPlan' => '', 'Postal' => 'Поштовий', @@ -78,9 +62,6 @@ return ['Billing' => [ 'Profit' => 'Прибуток', 'Quantity' => 'Кількість', 'Recipient' => 'Одержувач', - 'Region' => '', - 'Rep' => '', - 'Sales' => '', 'Select' => 'Вибирати', 'Shipment' => 'Відвантаження', 'Source' => 'Джерело', @@ -92,7 +73,5 @@ return ['Billing' => [ 'Type' => 'Тип', 'Types' => '', 'Upload' => 'Завантажувати', - 'Value' => '', - 'Variation' => 'Зміна', 'Zip' => 'Блиск', ]]; diff --git a/Theme/Backend/Lang/zh.lang.php b/Theme/Backend/Lang/zh.lang.php index 57a23db..96506e9 100755 --- a/Theme/Backend/Lang/zh.lang.php +++ b/Theme/Backend/Lang/zh.lang.php @@ -15,33 +15,22 @@ declare(strict_types=1); return ['Billing' => [ 'Address' => '地址', 'Addresses' => '地址', - 'Africa' => '', 'AlreadyPaid' => '已付款', - 'America' => '', 'Amount' => '', - 'Analyse' => '', 'Archive' => '', - 'Articles' => '', - 'Asia' => '', - 'Attribute' => '', - 'BaseTime' => '', 'Billing' => '', 'Bills' => '账单', 'Bonus' => '奖金', - 'CIS' => '', 'Cashback' => '返现金', 'City' => '城市', 'Client' => '客户', 'ClientID' => '客户ID', - 'ComparisonTime' => '', 'Confirmation' => '确认', 'Country' => '国家', - 'CreateBill' => '', 'Created' => '创造了', 'CreditCard' => '信用卡', 'CreditNote' => '信用票据', 'Currency' => '', - 'Customers' => '', 'Date' => '日期', 'Delivery' => '交货', 'DeliveryNote' => '送货单', @@ -50,13 +39,9 @@ return ['Billing' => [ 'DiscountP' => '折扣 %', 'Due' => '到期的', 'Email' => '', - 'Europe' => '', - 'Filter' => '', 'Freightage' => 'FRIGUTAGE.', - 'General' => '', 'Gross' => '总的', 'Invoice' => '发票', - 'Invoices' => '发票', 'Item' => '物品', 'Items' => '项目', 'Language' => '', @@ -68,7 +53,6 @@ return ['Billing' => [ 'Net' => '网', 'Offer' => '提供', 'Original' => '', - 'Other' => '', 'Payment' => '支付', 'PaymentPlan' => '', 'Postal' => '邮政', @@ -78,9 +62,6 @@ return ['Billing' => [ 'Profit' => '利润', 'Quantity' => '数量', 'Recipient' => '接受者', - 'Region' => '', - 'Rep' => '', - 'Sales' => '', 'Select' => '选择', 'Shipment' => '运输', 'Source' => '来源', @@ -92,7 +73,5 @@ return ['Billing' => [ 'Type' => '类型', 'Types' => '', 'Upload' => '上传', - 'Value' => '', - 'Variation' => '变化', 'Zip' => '压缩', ]]; diff --git a/Theme/Backend/bill-create.tpl.php b/Theme/Backend/bill-create.tpl.php index d8e30bc..b1c7d66 100755 --- a/Theme/Backend/bill-create.tpl.php +++ b/Theme/Backend/bill-create.tpl.php @@ -20,10 +20,10 @@ use phpOMS\Localization\ISO4217Enum; use phpOMS\Localization\ISO639Enum; use phpOMS\Uri\UriFactory; -$countryCodes = ISO3166TwoEnum::getConstants(); -$countries = ISO3166NameEnum::getConstants(); -$languages = ISO639Enum::getConstants(); -$currencies = ISO4217Enum::getConstants(); +$countryCodes = ISO3166TwoEnum::getConstants(); +$countries = ISO3166NameEnum::getConstants(); +$languages = ISO639Enum::getConstants(); +$currencies = ISO4217Enum::getConstants(); /** * @var \phpOMS\Views\View $this @@ -32,30 +32,59 @@ $media = $this->data['media'] ?? []; /** @var \Modules\Billing\Models\Bill $bill */ $bill = $this->getData('bill') ?? new NullBill(); -$elements = $bill->getElements(); +$elements = $bill->elements; $billTypes = $this->data['billtypes'] ?? []; -$archive = $bill->getFileByTypeName('original'); +$archive = $bill->getFileByTypeName('internal'); /** @var \Modules\Auditor\Models\Audit */ $logs = $this->data['logs'] ?? []; -$editable = $bill->id === 0 || \in_array($bill->getStatus(), [BillStatus::DRAFT, BillStatus::UNPARSED]); +$editable = $bill->id === 0 || \in_array($bill->status, [BillStatus::DRAFT, BillStatus::UNPARSED]); $disabled = $editable ? '' : ' disabled'; +$isNew = $archive->id === 0; + echo $this->data['nav']->render(); ?> +isValid()) : ?> +
+
+
+
+
    + areElementsValid()) : ?> +
  • getHtml('E_bill_items'); ?>
  • + + validateTaxAmountElements()) : ?> +
  • getHtml('E_bill_taxes'); ?>
  • + + validateNetElements()) : ?> +
  • getHtml('E_bill_net'); ?>
  • + + validateGrossElements()) : ?> +
  • getHtml('E_bill_gross'); ?>
  • + + validatePriceQuantityElements()) : ?> +
  • getHtml('E_bill_unit'); ?>
  • + +
+
+
+
+
+
@@ -80,7 +109,7 @@ echo $this->data['nav']->render(); ?>
@@ -89,7 +118,7 @@ echo $this->data['nav']->render(); ?> > @@ -99,7 +128,7 @@ echo $this->data['nav']->render(); ?>
@@ -110,14 +139,14 @@ echo $this->data['nav']->render(); ?>
>
client?->id ?? 0) > 0) : ?> @@ -266,44 +295,50 @@ echo $this->data['nav']->render(); ?>
-
getHtml('Invoice'); ?>
+
getHtml('Invoice'); ?>download
+
@@ -311,42 +346,60 @@ echo $this->data['nav']->render(); ?> - + +
- getHtml('Item'); ?> - getHtml('Name'); ?> - getHtml('Quantity'); ?> + getHtml('Item'); ?> + getHtml('Name'); ?> + getHtml('Quantity'); ?> getHtml('Discount'); ?> getHtml('DiscountP'); ?> - getHtml('Bonus'); ?> - getHtml('Tax'); ?> + getHtml('Bonus'); ?> getHtml('Price'); ?> + getHtml('TaxP'); ?> getHtml('Net'); ?> + getHtml('Margin'); ?>
- - - + expand_less + expand_more + close - > - - > - > - > - > - > - > - getCurrency($element->totalSalesPriceNet); ?> + + > + + > + > + > + > + > + > + getCurrency($element->totalSalesPriceNet, symbol: ''); ?> + totalSalesPriceNet->value === 0 ? 0 : (1 - $element->totalPurchasePriceNet->value / $element->totalSalesPriceNet->value) * 100, 2); ?>%
- - - - - - - - - - + expand_less + expand_more + close + + + + + + + + + + +
getHtml('Total'); ?> + + netDiscount->getAmount(2); ?> + netDiscount->value === 0 ? 0 : ($bill->netDiscount->value / ($bill->netSales->value + $bill->netDiscount->value)) * 100, 2); ?>% + + + taxP->getAmount(2); ?> + netSales->getAmount(2); ?> + netSales->value === 0 ? 0 : (1 - $bill->netCosts->value / $bill->netSales->value) * 100, 2); ?>%
- +
- +
@@ -356,9 +409,10 @@ echo $this->data['nav']->render(); ?>
- -
@@ -368,12 +422,14 @@ echo $this->data['nav']->render(); ?>
- +
+ +
@@ -386,6 +442,9 @@ echo $this->data['nav']->render(); ?>
+ + + + -
-
-
-
-
getHtml('Media'); ?>
-
-
-
- -
- -
-
-
- -
- - -
-
-
-
-
- -
- getData('medialist')?->render($media); ?> -
-
+
+ data['media-upload']->render('bill-file', 'files', '', $media); ?>
+ +
- l11nManager, $this->request, $this->response); - $footerView->setTemplate('/Web/Templates/Lists/Footer/PaginationBig'); - $footerView->setPages(20); - $footerView->setPage(1); - ?>
-
getHtml('Logs'); ?>
- +
getHtml('Logs'); ?>download
+
id); + $url = UriFactory::build('{/base}/admin/audit/view?id=' . $audit->id); ?>
getHtml('ID', '0', '0'); ?> - getHtml('Trigger', 'Auditor', 'Backend'); ?> getHtml('Action', 'Auditor', 'Backend'); ?> + getHtml('Trigger', 'Auditor', 'Backend'); ?> getHtml('CreatedBy', 'Auditor', 'Backend'); ?> getHtml('CreatedAt', 'Auditor', 'Backend'); ?>
id; ?> - trigger; ?> old === null) : echo $this->getHtml('CREATE', 'Auditor', 'Backend'); ?> old !== null && $audit->new !== null) : echo $this->getHtml('UPDATE', 'Auditor', 'Backend'); ?> new === null) : echo $this->getHtml('DELETE', 'Auditor', 'Backend'); ?> getHtml('UNKNOWN', 'Auditor', 'Backend'); ?> + trigger; ?> printHtml( $this->renderUserName('%3$s %2$s %1$s', [$audit->createdBy->name1, $audit->createdBy->name2, $audit->createdBy->name3, $audit->createdBy->login]) ); ?> - createdAt->format('Y-m-d'); ?> + createdAt->format('Y-m-d H:i'); ?>
+ +
diff --git a/Theme/Backend/payment-type-list.tpl.php b/Theme/Backend/payment-type-list.tpl.php new file mode 100644 index 0000000..d36f6d8 --- /dev/null +++ b/Theme/Backend/payment-type-list.tpl.php @@ -0,0 +1,71 @@ +data['types']; + +echo $this->data['nav']->render(); ?> + +
+
+
+
getHtml('PaymentTerms'); ?>download
+
+ + + + + $value) : ++$count; + $url = UriFactory::build('{/base}/bill/payment/view?{?}&id=' . $value->id); + ?> + +
getHtml('ID', '0', '0'); ?> + + + + getHtml('Name'); ?> + + + +
id; ?> + printHtml($value->getL11n()); ?> + + +
getHtml('Empty', '0', '0'); ?> + +
+
+
+
+
diff --git a/Theme/Backend/payment-view.tpl.php b/Theme/Backend/payment-view.tpl.php new file mode 100644 index 0000000..40f1511 --- /dev/null +++ b/Theme/Backend/payment-view.tpl.php @@ -0,0 +1,53 @@ +data['type']; + +/** @var \phpOMS\Views\View $this */ +echo $this->data['nav']->render(); ?> +
+
+
+
+
getHtml('PaymentTerm'); ?>
+
+
+ + +
+
+ +
+ + +
+
+
+
+
+ +
+ data['l11nView']->render( + $this->data['l11nValues'], + [], + '{/api}bill/payment/l11n' + ); + ?> +
\ No newline at end of file diff --git a/Theme/Backend/purchase-bill-list.tpl.php b/Theme/Backend/purchase-bill-list.tpl.php index a516aeb..ad3f775 100755 --- a/Theme/Backend/purchase-bill-list.tpl.php +++ b/Theme/Backend/purchase-bill-list.tpl.php @@ -21,7 +21,7 @@ echo $this->data['nav']->render(); ?>
-
getHtml('Bills'); ?>
+
getHtml('Bills'); ?>download
@@ -33,122 +33,134 @@ echo $this->data['nav']->render(); ?> data['nav']->render(); ?>
getHtml('ID', '0', '0'); ?> + getHtml('External'); ?> + + + getHtml('Type'); ?> getHtml('SupplierID'); ?> getHtml('Supplier'); ?> getHtml('Address'); ?> getHtml('Postal'); ?> getHtml('City'); ?> getHtml('Country'); ?> getHtml('Net'); ?> getHtml('Created'); ?>
getNumber(); ?> + external; ?> type->getL11n(); ?> - supplier->number; ?> + supplier->number; ?> printHtml($value->billTo); ?> billAddress; ?> billZip; ?> billCity; ?> billCountry; ?> - getCurrency($value->netSales); ?> + netSales->getAmount(); ?> createdAt->format('Y-m-d'); ?> diff --git a/Theme/Backend/purchase-bill.tpl.php b/Theme/Backend/purchase-bill.tpl.php index f84badb..eae5982 100755 --- a/Theme/Backend/purchase-bill.tpl.php +++ b/Theme/Backend/purchase-bill.tpl.php @@ -12,11 +12,17 @@ */ declare(strict_types=1); -use phpOMS\System\File\FileUtils; +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 @@ -24,26 +30,59 @@ include __DIR__ . '/../../../Media/Theme/Backend/template-functions.php'; /** @var \Modules\Billing\Models\Bill $bill */ $bill = $this->data['bill']; -$elements = $bill->getElements(); +$elements = $bill->elements; -$previewType = $this->data['previewType']; -$originalType = $this->data['originalType']; -$billPdf = $bill->getFileByType($previewType); -$original = $bill->getFileByType($originalType); -$media = $bill->files; +$billTypes = $this->data['billtypes'] ?? []; + +$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(); ?> +isValid()) : ?> +
+
+
+
+
    + areElementsValid()) : ?> +
  • getHtml('E_bill_items'); ?>
  • + + validateTaxAmountElements()) : ?> +
  • getHtml('E_bill_taxes'); ?>
  • + + validateNetElements()) : ?> +
  • getHtml('E_bill_net'); ?>
  • + + validateGrossElements()) : ?> +
  • getHtml('E_bill_gross'); ?>
  • + + validatePriceQuantityElements()) : ?> +
  • getHtml('E_bill_unit'); ?>
  • + +
+
+
+
+
+
@@ -55,61 +94,148 @@ echo $this->data['nav']->render(); ?>
getHtml('Invoice'); ?>
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ + +
+ +
+ + +
+ +
+ + + + > + +
+ +
+ + +
+ +
+ +
+
+ + + > + +
+ client?->id ?? 0) > 0) : ?> + + +
+
+ +
+ + > +
+ +
+ + > +
+ +
+ + > +
+ +
+ + +
+ +
+ + +
-
+ +
+ +
+
-
getHtml('Invoice'); ?>
+
getHtml('Billing'); ?>
- -
-
-
-
-
-
-
-
-
-
-
-
-
+
+ + +
+ +
+ + > +
+ +
+ + > +
+ +
+ + > +
+ +
+ + > +
+ +
+ + +
+ +
+ + > +
@@ -120,22 +246,42 @@ echo $this->data['nav']->render(); ?>
getHtml('Delivery'); ?>
- -
-
-
-
-
-
-
-
-
-
-
-
-
+
+ + +
+ +
+ + > +
+ +
+ + > +
+ +
+ + > +
+ +
+ + > +
+ +
+ + +
@@ -146,208 +292,322 @@ echo $this->data['nav']->render(); ?>
-
-
getHtml('Invoice'); ?>
+
+
getHtml('Invoice'); ?>download
- +
- - - - + + + + + + + + +
- getHtml('Item'); ?> - getHtml('Name'); ?> - getHtml('Quantity'); ?> - getHtml('Price'); ?> - getHtml('Discount'); ?> - getHtml('DiscountP'); ?> - getHtml('Bonus'); ?> - getHtml('Tax'); ?> + getHtml('Item'); ?> + getHtml('Name'); ?> + getHtml('Quantity'); ?> + getHtml('Discount'); ?> + getHtml('DiscountP'); ?> + getHtml('Bonus'); ?> + getHtml('Price'); ?> + getHtml('TaxP'); ?> getHtml('Net'); ?> -
- - - - - - - - - getCurrency($element->totalSalesPriceNet); ?> - -
- - - - - - - - - + getHtml('Margin'); ?> +
+ expand_less + expand_more + close + + + > + + > + > + > + > + > + > + getCurrency($element->totalSalesPriceNet, symbol: ''); ?> + totalSalesPriceNet->value === 0 ? 0 : (1 - $element->totalPurchasePriceNet->value / $element->totalSalesPriceNet->value) * 100, 2); ?>% + + +
expand_less + expand_more + close + + + + + + + + + + + + +
getHtml('Total'); ?> + + netDiscount->getAmount(2); ?> + netDiscount->value === 0 ? 0 : ($bill->netDiscount->value / ($bill->netSales->value + $bill->netDiscount->value)) * 100, 2); ?>% + + + taxP->getAmount(2); ?> + netSales->getAmount(2); ?> + netSales->value === 0 ? 0 : (1 - $bill->netCosts->value / $bill->netSales->value) * 100, 2); ?>%
-
- getHtml('Freightage'); ?>: 0.00 - - getHtml('Net'); ?>: getCurrency($bill->netSales); ?> - - getHtml('Tax'); ?>: 0.00 - - getHtml('Total'); ?>: getCurrency($bill->grossSales); ?> -
+
+ + +
+
+
+
-
+
+
+ +
+
+ +
- id > 0) : ?> - - +
+ +
-
+ status === BillStatus::DRAFT + || $bill->status === BillStatus::UNPARSED + || $bill->status === BillStatus::ACTIVE + ) : ?> +
+
+
+ +
+
+
+ + +
- id > 0) : ?> - - +
+ + + + + +
+ data['media-upload']->render('bill-file', 'files', '', $bill->files); ?> +
+ + + +
+
+
+
+
getHtml('Logs'); ?>download
+ + + + + id); ?> -
getHtml('ID', '0', '0'); ?> + getHtml('Action', 'Auditor', 'Backend'); ?> + getHtml('Trigger', 'Auditor', 'Backend'); ?> + getHtml('CreatedBy', 'Auditor', 'Backend'); ?> + getHtml('CreatedAt', 'Auditor', 'Backend'); ?> +
- - name; ?> - extension; ?> + id; ?> + old === null) : echo $this->getHtml('CREATE', 'Auditor', 'Backend'); ?> + old !== null && $audit->new !== null) : echo $this->getHtml('UPDATE', 'Auditor', 'Backend'); ?> + new === null) : echo $this->getHtml('DELETE', 'Auditor', 'Backend'); ?> + getHtml('UNKNOWN', 'Auditor', 'Backend'); ?> + + trigger; ?> + printHtml( + $this->renderUserName('%3$s %2$s %1$s', [$audit->createdBy->name1, $audit->createdBy->name2, $audit->createdBy->name3, $audit->createdBy->login]) + ); ?> + createdAt->format('Y-m-d H:i'); ?>
- -
-
-
-
- - - - - - -
getHtml('Logs'); ?>
IP - getHtml('ID', '0', '0'); ?> - getHtml('Name'); ?> - getHtml('Log'); ?> - getHtml('Date'); ?> -
printHtml($this->request->getOrigin()); ?> - printHtml((string) $this->request->header->account); ?> - printHtml((string) $this->request->header->account); ?> - Create Invoice - printHtml((new \DateTime('now'))->format('Y-m-d H:i:s')); ?> -
-
-
-
-
+ +
- diff --git a/Theme/Backend/sales-bill-list.tpl.php b/Theme/Backend/sales-bill-list.tpl.php index a71e9e0..61c72da 100755 --- a/Theme/Backend/sales-bill-list.tpl.php +++ b/Theme/Backend/sales-bill-list.tpl.php @@ -21,7 +21,7 @@ echo $this->data['nav']->render(); ?>
-
getHtml('Bills'); ?>
+
getHtml('Bills'); ?>download
@@ -33,134 +33,134 @@ echo $this->data['nav']->render(); ?> $value) : @@ -174,7 +174,7 @@ echo $this->data['nav']->render(); ?>
getHtml('ID', '0', '0'); ?> getHtml('Type'); ?> getHtml('ClientID'); ?> getHtml('Client'); ?> getHtml('Address'); ?> getHtml('Postal'); ?> getHtml('City'); ?> getHtml('Country'); ?> getHtml('Net'); ?> getHtml('Profit'); ?> getHtml('Created'); ?>
getNumber(); ?> type->getL11n(); ?> - client->number; ?> + client->number; ?> printHtml($value->billTo); ?> billAddress; ?> diff --git a/Theme/Backend/shipping-type-list.tpl.php b/Theme/Backend/shipping-type-list.tpl.php new file mode 100644 index 0000000..7db9527 --- /dev/null +++ b/Theme/Backend/shipping-type-list.tpl.php @@ -0,0 +1,71 @@ +data['types']; + +echo $this->data['nav']->render(); ?> + +
+
+
+
getHtml('ShippingTerms'); ?>download
+
+ + + + + $value) : ++$count; + $url = UriFactory::build('{/base}/bill/shipping/view?{?}&id=' . $value->id); + ?> + +
getHtml('ID', '0', '0'); ?> + + + + getHtml('Name'); ?> + + + +
id; ?> + printHtml($value->getL11n()); ?> + + +
getHtml('Empty', '0', '0'); ?> + +
+
+
+
+
diff --git a/Theme/Backend/shipping-view.tpl.php b/Theme/Backend/shipping-view.tpl.php new file mode 100644 index 0000000..545385f --- /dev/null +++ b/Theme/Backend/shipping-view.tpl.php @@ -0,0 +1,53 @@ +data['type']; + +/** @var \phpOMS\Views\View $this */ +echo $this->data['nav']->render(); ?> +
+
+
+
+
getHtml('ShippingTerm'); ?>
+
+
+ + +
+
+ +
+ + +
+
+
+
+
+ +
+ data['l11nView']->render( + $this->data['l11nValues'], + [], + '{/api}bill/shipping/l11n' + ); + ?> +
\ No newline at end of file diff --git a/Theme/Backend/user-purchase-bill-dashboard.tpl.php b/Theme/Backend/user-purchase-bill-dashboard.tpl.php index c66623b..176c8b3 100755 --- a/Theme/Backend/user-purchase-bill-dashboard.tpl.php +++ b/Theme/Backend/user-purchase-bill-dashboard.tpl.php @@ -23,7 +23,7 @@ echo $this->data['nav']->render(); ?>
-
getHtml('Bills'); ?>
+
getHtml('Bills'); ?>download
@@ -35,122 +35,134 @@ echo $this->data['nav']->render(); ?> data['nav']->render(); ?>
getHtml('ID', '0', '0'); ?> + getHtml('External'); ?> + + + getHtml('Type'); ?> getHtml('SupplierID'); ?> getHtml('Supplier'); ?> getHtml('Address'); ?> getHtml('Postal'); ?> getHtml('City'); ?> getHtml('Country'); ?> getHtml('Gross'); ?> getHtml('Date'); ?>
getNumber(); ?> + external; ?> type->getL11n(); ?> - supplier->number; ?> + supplier->number; ?> printHtml($value->billTo); ?> billAddress; ?> @@ -176,7 +189,7 @@ echo $this->data['nav']->render(); ?> : ISO3166NameEnum::getByName( ISO3166TwoEnum::getName($value->billCountry) ); ?> - grossCosts->getAmount(); ?> + grossSales->getAmount(); ?> billDate?->format('Y-m-d'); ?> diff --git a/Theme/Backend/user-purchase-bill.tpl.php b/Theme/Backend/user-purchase-bill.tpl.php deleted file mode 100755 index 8a8e827..0000000 --- a/Theme/Backend/user-purchase-bill.tpl.php +++ /dev/null @@ -1,350 +0,0 @@ -data['bill']; -$elements = $bill->getElements(); - -$previewType = $this->data['previewType']; -$originalType = $this->data['originalType']; -$billPdf = $bill->getFileByType($previewType); -$original = $bill->getFileByType($originalType); -$media = $bill->getMedia(); - -echo $this->data['nav']->render(); ?> - -
-
- -
-
- -
-
-
-
-
-
getHtml('Invoice'); ?>
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
getHtml('Invoice'); ?>
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
getHtml('Delivery'); ?>
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
getHtml('Invoice'); ?>
- - - - - - - -
- getHtml('Item'); ?> - getHtml('Name'); ?> - getHtml('Quantity'); ?> - getHtml('Price'); ?> - getHtml('Discount'); ?> - getHtml('DiscountP'); ?> - getHtml('Bonus'); ?> - getHtml('Tax'); ?> - getHtml('Net'); ?> -
- - - - - - - - - getCurrency($element->totalSalesPriceNet); ?> - -
- - - - - - - - - -
-
- getHtml('Freightage'); ?>: 0.00 - - getHtml('Net'); ?>: getCurrency($bill->netSales); ?> - - getHtml('Tax'); ?>: 0.00 - - getHtml('Total'); ?>: getCurrency($bill->grossSales); ?> -
-
-
-
-
- -
-
-
-
-
- id > 0) : ?> - - -
-
-
-
-
- -
-
-
-
-
- id > 0) : ?> - - -
-
-
-
-
- -
-
-
-
-

getHtml('Payment'); ?>

-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-

getHtml('Media'); ?>

- -
-
- - -
-
-
-
-
-
-
-
-
- -
-
-
getHtml('Media'); ?>
- - - - - extension === 'collection' - ? UriFactory::build('{/base}/media/list?path=' . \rtrim($file->getVirtualPath(), '/') . '/' . $file->name) - : UriFactory::build('{/base}/media/single?id=' . $file->id - . '&path={?path}' . ( - $file->id === 0 - ? '/' . $file->name - : '' - ) - ); - - $icon = $fileIconFunction(FileUtils::getExtensionType($file->extension)); - ?> - -
- - getHtml('Name'); ?> - getHtml('Type'); ?> -
- - name; ?> - extension; ?> - -
-
-
-
-
- -
-
-
-
- - - - - - -
getHtml('Logs'); ?>
IP - getHtml('ID', '0', '0'); ?> - getHtml('Name'); ?> - getHtml('Log'); ?> - getHtml('Date'); ?> -
printHtml($this->request->getOrigin()); ?> - printHtml((string) $this->request->header->account); ?> - printHtml((string) $this->request->header->account); ?> - Create Invoice - printHtml((new \DateTime('now'))->format('Y-m-d H:i:s')); ?> -
-
-
-
-
-
-
- diff --git a/info.json b/info.json index 3f570d3..ea95bcf 100755 --- a/info.json +++ b/info.json @@ -14,7 +14,6 @@ "name": "Jingga", "website": "jingga.app" }, - "description": "Accounting module.", "directory": "Billing", "dependencies": { "Admin": "1.0.0", diff --git a/tests/Autoloader.php b/tests/Autoloader.php index 96d8c02..28b8056 100755 --- a/tests/Autoloader.php +++ b/tests/Autoloader.php @@ -36,6 +36,7 @@ final class Autoloader __DIR__ . '/../', __DIR__ . '/../MainRepository/', __DIR__ . '/../../', + __DIR__ . '/../../../', ]; /** @@ -75,8 +76,8 @@ final class Autoloader */ public static function defaultAutoloader(string $class) : void { - $class = \ltrim($class, '\\'); - $class = \strtr($class, '_\\', '//'); + $class = \ltrim($class, '\\'); + $class = \strtr($class, '_\\', '//'); if (\stripos($class, 'Web/Backend') !== false || \stripos($class, 'Web/Api') !== false) { $class = \is_dir(__DIR__ . '/Web') ? $class : \str_replace('Web/', 'MainRepository/Web/', $class); diff --git a/tests/Bootstrap.php b/tests/Bootstrap.php index ddc049d..4bf4f19 100755 --- a/tests/Bootstrap.php +++ b/tests/Bootstrap.php @@ -1,4 +1,15 @@ [ + 'db' => [ 'core' => [ 'masters' => [ - 'admin' => [ + 'admin' => [ 'db' => 'mysql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '3306', /* db host port */ @@ -80,7 +91,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'insert' => [ + 'insert' => [ 'db' => 'mysql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '3306', /* db host port */ @@ -90,7 +101,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'select' => [ + 'select' => [ 'db' => 'mysql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '3306', /* db host port */ @@ -100,7 +111,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'update' => [ + 'update' => [ 'db' => 'mysql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '3306', /* db host port */ @@ -110,7 +121,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'delete' => [ + 'delete' => [ 'db' => 'mysql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '3306', /* db host port */ @@ -120,7 +131,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'schema' => [ + 'schema' => [ 'db' => 'mysql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '3306', /* db host port */ @@ -132,7 +143,7 @@ $CONFIG = [ ], ], 'postgresql' => [ - 'admin' => [ + 'admin' => [ 'db' => 'pgsql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '5432', /* db host port */ @@ -142,7 +153,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'insert' => [ + 'insert' => [ 'db' => 'pgsql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '5432', /* db host port */ @@ -152,7 +163,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'select' => [ + 'select' => [ 'db' => 'pgsql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '5432', /* db host port */ @@ -162,7 +173,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'update' => [ + 'update' => [ 'db' => 'pgsql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '5432', /* db host port */ @@ -172,7 +183,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'delete' => [ + 'delete' => [ 'db' => 'pgsql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '5432', /* db host port */ @@ -182,7 +193,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'schema' => [ + 'schema' => [ 'db' => 'pgsql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '5432', /* db host port */ @@ -194,37 +205,37 @@ $CONFIG = [ ], ], 'sqlite' => [ - 'admin' => [ + 'admin' => [ 'db' => 'sqlite', /* db type */ 'database' => __DIR__ . '/../Karaka/phpOMS/Localization/Defaults/localization.sqlite', /* db name */ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'insert' => [ + 'insert' => [ 'db' => 'sqlite', /* db type */ 'database' => __DIR__ . '/../Karaka/phpOMS/Localization/Defaults/localization.sqlite', /* db name */ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'select' => [ + 'select' => [ 'db' => 'sqlite', /* db type */ 'database' => __DIR__ . '/../Karaka/phpOMS/Localization/Defaults/localization.sqlite', /* db name */ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'update' => [ + 'update' => [ 'db' => 'sqlite', /* db type */ 'database' => __DIR__ . '/../Karaka/phpOMS/Localization/Defaults/localization.sqlite', /* db name */ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'delete' => [ + 'delete' => [ 'db' => 'sqlite', /* db type */ 'database' => __DIR__ . '/../Karaka/phpOMS/Localization/Defaults/localization.sqlite', /* db name */ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'schema' => [ + 'schema' => [ 'db' => 'sqlite', /* db type */ 'database' => __DIR__ . '/../Karaka/phpOMS/Localization/Defaults/localization.sqlite', /* db name */ 'weight' => 1000, /* db table prefix */ @@ -232,7 +243,7 @@ $CONFIG = [ ], ], 'mssql' => [ - 'admin' => [ + 'admin' => [ 'db' => 'mssql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '1433', /* db host port */ @@ -242,7 +253,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'insert' => [ + 'insert' => [ 'db' => 'mssql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '1433', /* db host port */ @@ -252,7 +263,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'select' => [ + 'select' => [ 'db' => 'mssql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '1433', /* db host port */ @@ -262,7 +273,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'update' => [ + 'update' => [ 'db' => 'mssql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '1433', /* db host port */ @@ -272,7 +283,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'delete' => [ + 'delete' => [ 'db' => 'mssql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '1433', /* db host port */ @@ -282,7 +293,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'schema' => [ + 'schema' => [ 'db' => 'mssql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '1433', /* db host port */ @@ -322,16 +333,16 @@ $CONFIG = [ 'password' => '123456', ], ], - 'log' => [ + 'log' => [ 'file' => [ 'path' => __DIR__ . '/Logs', ], ], - 'page' => [ + 'page' => [ 'root' => '/', 'https' => false, ], - 'app' => [ + 'app' => [ 'path' => __DIR__, 'default' => [ 'app' => 'Backend', @@ -350,7 +361,7 @@ $CONFIG = [ ], ], ], - 'socket' => [ + 'socket' => [ 'master' => [ 'host' => '127.0.0.1', 'limit' => 300, @@ -360,7 +371,7 @@ $CONFIG = [ 'language' => [ 'en', ], - 'apis' => [ + 'apis' => [ ], ]; diff --git a/tests/Controller/Api/ApiBillControllerTrait.php b/tests/Controller/Api/ApiBillControllerTrait.php new file mode 100644 index 0000000..a170765 --- /dev/null +++ b/tests/Controller/Api/ApiBillControllerTrait.php @@ -0,0 +1,133 @@ +header->account = 1; + + $request->setData('client', 1); + $request->setData('address', null); + $request->setData('type', 1); + $request->setData('status', null); // null = system settings, value = individual input + $request->setData('performancedate', DateTime::generateDateTime(new \DateTime('2020-01-01'), new \DateTime('now'))->format('Y-m-d H:i:s')); + $request->setData('sales_referral', null); // who these sales belong to + $request->setData('shipping_terms', 1); // e.g. incoterms + $request->setData('shipping_type', 1); + $request->setData('shipping_cost', null); + $request->setData('insurance_type', 1); + $request->setData('insurance_cost', null); // null = system settings, value = individual input + $request->setData('info', null); // null = system settings, value = individual input + $request->setData('currency', null); // null = system settings, value = individual input + $request->setData('payment', null); // null = system settings, value = individual input + $request->setData('payment_terms', null); // null = system settings, value = individual input + + $this->module->apiBillCreate($request, $response); + self::assertEquals('ok', $response->getData('')['status']); + self::assertGreaterThan(0, $response->getDataArray('')['response']->id); + } + + public function testBillElementCreate() : void + { + $response = new HttpResponse(); + $request = new HttpRequest(); + + $request->header->account = 1; + + $request->setData('bill', 1); + $request->setData('item', 1); + $request->setData('text', null); + $request->setData('quantity', 5); + $request->setData('tax', null); + + //$request->setData('discount_percentage', \mt_rand(5, 30)); + + $this->module->apiBillElementCreate($request, $response); + self::assertEquals('ok', $response->getData('')['status']); + self::assertGreaterThan(0, $response->getDataArray('')['response']->id); + } + + public function testBillArchiveCreate() : void + { + $response = new HttpResponse(); + $request = new HttpRequest(); + + $request->header->account = 1; + + $request->setData('bill', 1); + + $this->module->apiBillPdfArchiveCreate($request, $response); + self::assertEquals('ok', $response->getData('')['status']); + self::assertGreaterThan(0, $response->getDataArray('')['response']->id); + } + + public function testBillNoteCreate() : void + { + $response = new HttpResponse(); + $request = new HttpRequest(); + + $request->header->account = \mt_rand(2, 5); + + $request->setData('id', 1); + $request->setData('title', 'Test note title'); + $request->setData('plain', 'Test content text'); + + $this->module->apiNoteCreate($request, $response); + self::assertEquals('ok', $response->getData('')['status']); + self::assertGreaterThan(0, $response->getDataArray('')['response']->id); + } + + /** + * @covers Modules\Billing\Controller\ApiController + * @group module + */ + public function testBillCreateInvalidData() : void + { + $response = new HttpResponse(); + $request = new HttpRequest(); + + $request->header->account = 1; + $request->setData('invalid', '1'); + + $this->module->apiBillCreate($request, $response); + self::assertEquals(RequestStatusCode::R_400, $response->header->status); + } + + /** + * @covers Modules\Billing\Controller\ApiController + * @group module + */ + public function testBillElementCreateInvalidData() : void + { + $response = new HttpResponse(); + $request = new HttpRequest(); + + $request->header->account = 1; + $request->setData('invalid', '1'); + + $this->module->apiBillElementCreate($request, $response); + self::assertEquals(RequestStatusCode::R_400, $response->header->status); + } +} diff --git a/tests/Controller/Api/ApiPurchaseControllerTrait.php b/tests/Controller/Api/ApiPurchaseControllerTrait.php new file mode 100644 index 0000000..f1aef72 --- /dev/null +++ b/tests/Controller/Api/ApiPurchaseControllerTrait.php @@ -0,0 +1,70 @@ +header->account = 1; + + \copy(__DIR__ . '/billing/' . $file, __DIR__ . '/temp/' . $file); + + $toUpload['file0'] = [ + 'name' => $file, + 'type' => \explode('.', $file)[1], + 'tmp_name' => __DIR__ . '/temp/' . $file, + 'error' => \UPLOAD_ERR_OK, + 'size' => \filesize(__DIR__ . '/temp/' . $file), + ]; + + TestUtils::setMember($request, 'files', $toUpload); + + $this->modulePurchase->apiSupplierBillUpload($request, $response); + self::assertEquals(RequestStatusCode::R_200, $response->header->status); + } + + if (\is_dir(__DIR__ . '/temp')) { + Directory::delete(__DIR__ . '/temp'); + } + } +} diff --git a/tests/Controller/Api/billing/Invoice # INV0041-1-text.pdf b/tests/Controller/Api/billing/Invoice # INV0041-1-text.pdf new file mode 100644 index 0000000..99506f7 Binary files /dev/null and b/tests/Controller/Api/billing/Invoice # INV0041-1-text.pdf differ diff --git a/tests/Controller/Api/billing/Invoice - INV0041-1-image.pdf b/tests/Controller/Api/billing/Invoice - INV0041-1-image.pdf new file mode 100644 index 0000000..10a3a94 Binary files /dev/null and b/tests/Controller/Api/billing/Invoice - INV0041-1-image.pdf differ diff --git a/tests/Controller/ApiControllerTest.php b/tests/Controller/ApiControllerTest.php index 341509c..710f6de 100755 --- a/tests/Controller/ApiControllerTest.php +++ b/tests/Controller/ApiControllerTest.php @@ -16,6 +16,8 @@ namespace Modules\Billing\tests\Controller; use Model\CoreSettings; use Modules\Admin\Models\AccountPermission; +use Modules\Billing\tests\Controller\Api\ApiBillControllerTrait; +use Modules\Billing\tests\Controller\Api\ApiPurchaseControllerTrait; use phpOMS\Account\Account; use phpOMS\Account\AccountManager; use phpOMS\Account\PermissionType; @@ -23,14 +25,9 @@ use phpOMS\Application\ApplicationAbstract; use phpOMS\Dispatcher\Dispatcher; use phpOMS\Event\EventManager; use phpOMS\Localization\L11nManager; -use phpOMS\Message\Http\HttpRequest; -use phpOMS\Message\Http\HttpResponse; -use phpOMS\Message\Http\RequestStatusCode; use phpOMS\Module\ModuleAbstract; use phpOMS\Module\ModuleManager; use phpOMS\Router\WebRouter; -use phpOMS\Uri\HttpUri; -use phpOMS\Utils\RnG\DateTime; use phpOMS\Utils\TestUtils; /** @@ -47,6 +44,11 @@ final class ApiControllerTest extends \PHPUnit\Framework\TestCase */ protected ModuleAbstract $module; + /** + * @var \Modules\Billing\Controller\ApiController + */ + protected ModuleAbstract $modulePurchase; + /** * {@inheritdoc} */ @@ -57,14 +59,14 @@ final class ApiControllerTest extends \PHPUnit\Framework\TestCase protected string $appName = 'Api'; }; - $this->app->dbPool = $GLOBALS['dbpool']; - $this->app->unitId = 1; - $this->app->accountManager = new AccountManager($GLOBALS['session']); - $this->app->appSettings = new CoreSettings(); - $this->app->moduleManager = new ModuleManager($this->app, __DIR__ . '/../../../../Modules/'); - $this->app->dispatcher = new Dispatcher($this->app); - $this->app->eventManager = new EventManager($this->app->dispatcher); - $this->app->l11nManager = new L11nManager(); + $this->app->dbPool = $GLOBALS['dbpool']; + $this->app->unitId = 1; + $this->app->accountManager = new AccountManager($GLOBALS['session']); + $this->app->appSettings = new CoreSettings(); + $this->app->moduleManager = new ModuleManager($this->app, __DIR__ . '/../../../../Modules/'); + $this->app->dispatcher = new Dispatcher($this->app); + $this->app->eventManager = new EventManager($this->app->dispatcher); + $this->app->l11nManager = new L11nManager(); $this->app->eventManager->importFromFile(__DIR__ . '/../../../../Web/Api/Hooks.php'); $account = new Account(); @@ -86,192 +88,13 @@ final class ApiControllerTest extends \PHPUnit\Framework\TestCase $this->app->accountManager->add($account); $this->app->router = new WebRouter(); - $this->module = $this->app->moduleManager->get('Billing'); + $this->module = $this->app->moduleManager->get('Billing', 'ApiBill'); + $this->modulePurchase = $this->app->moduleManager->get('Billing', 'ApiPurchase'); TestUtils::setMember($this->module, 'app', $this->app); + TestUtils::setMember($this->modulePurchase, 'app', $this->app); } - /** - * Tests bill, bill element and bill pdf archive create - * - * @covers Modules\Billing\Controller\ApiController - * @group module - */ - /* - public function testBillClientCreate() : void - { - $response = new HttpResponse(); - $request = new HttpRequest(new HttpUri('')); - - $request->header->account = 1; - - $request->setData('client', 1); - $request->setData('address', null); - $request->setData('type', 1); - $request->setData('status', null); // null = system settings, value = individual input - $request->setData('performancedate', DateTime::generateDateTime(new \DateTime('2015-01-01'), new \DateTime('now'))->format('Y-m-d H:i:s')); - $request->setData('sales_referral', null); // who these sales belong to - $request->setData('shipping_terms', 1); // e.g. incoterms - $request->setData('shipping_type', 1); - $request->setData('shipping_cost', null); - $request->setData('insurance_type', 1); - $request->setData('insurance_cost', null); // null = system settings, value = individual input - $request->setData('info', null); // null = system settings, value = individual input - $request->setData('currency', null); // null = system settings, value = individual input - $request->setData('payment', null); // null = system settings, value = individual input - $request->setData('payment_terms', null); // null = system settings, value = individual input - - $this->module->apiBillCreate($request, $response); - - $bId = $response->getDataArray('')['response']->id; - self::assertGreaterThan(0, $bId); - - for ($k = 0; $k < 10; ++$k) { - $response = new HttpResponse(); - $request = new HttpRequest(new HttpUri('')); - - $request->header->account = 1; - - $iId = \mt_rand(0, 10); - - $request->setData('bill', $bId); - $request->setData('item', $iId === 0 ? null : $iId); - - if ($iId === 0) { - // @todo: add text - } - - $request->setData('quantity', \mt_rand(1, 11)); - $request->setData('tax', null); - $request->setData('text', $iId === 0 ? 'Some test text' : null); - - // discounts - if (\mt_rand(1, 100) < 31) { - $request->setData('discount_percentage', \mt_rand(5, 30)); - } - - $this->module->apiBillElementCreate($request, $response); - self::assertGreaterThan(0, $response->getDataArray('')['response']->id); - } - - $response = new HttpResponse(); - $request = new HttpRequest(new HttpUri('')); - - $request->header->account = 1; - $request->setData('bill', $bId); - - $this->module->apiBillPdfArchiveCreate($request, $response); - - $result = $response->getData(''); - self::assertGreaterThan(0, $result === null ? -1 : $result['response']?->id); - } - */ - - /** - * Tests bill, bill element and bill pdf archive create - * - * @covers Modules\Billing\Controller\ApiController - * @group module - */ - /* - public function testBillSupplierCreate() : void - { - $response = new HttpResponse(); - $request = new HttpRequest(new HttpUri('')); - - $request->header->account = 1; - - $request->setData('supplier', 1); - $request->setData('address', null); - $request->setData('type', 1); - $request->setData('status', null); // null = system settings, value = individual input - $request->setData('performancedate', DateTime::generateDateTime(new \DateTime('2015-01-01'), new \DateTime('now'))->format('Y-m-d H:i:s')); - $request->setData('sales_referral', null); // who these sales belong to - $request->setData('shipping_terms', 1); // e.g. incoterms - $request->setData('shipping_type', 1); - $request->setData('shipping_cost', null); - $request->setData('insurance_type', 1); - $request->setData('insurance_cost', null); // null = system settings, value = individual input - $request->setData('info', null); // null = system settings, value = individual input - $request->setData('currency', null); // null = system settings, value = individual input - $request->setData('payment', null); // null = system settings, value = individual input - $request->setData('payment_terms', null); // null = system settings, value = individual input - - $this->module->apiBillCreate($request, $response); - - $bId = $response->getDataArray('')['response']->id; - self::assertGreaterThan(0, $bId); - - for ($k = 0; $k < 10; ++$k) { - $response = new HttpResponse(); - $request = new HttpRequest(new HttpUri('')); - - $request->header->account = 1; - - $iId = \mt_rand(0, 10); - - $request->setData('bill', $bId); - $request->setData('item', $iId === 0 ? null : $iId); - - if ($iId === 0) { - // @todo: add text - } - - $request->setData('quantity', \mt_rand(1, 11)); - $request->setData('tax', null); - $request->setData('text', $iId === 0 ? 'Some test text' : null); - - // discounts - if (\mt_rand(1, 100) < 31) { - $request->setData('discount_percentage', \mt_rand(5, 30)); - } - - $this->module->apiBillElementCreate($request, $response); - self::assertGreaterThan(0, $response->getDataArray('')['response']->id); - } - - $response = new HttpResponse(); - $request = new HttpRequest(new HttpUri('')); - - $request->header->account = 1; - $request->setData('bill', $bId); - - $this->module->apiBillPdfArchiveCreate($request, $response); - - $result = $response->getData(''); - self::assertGreaterThan(0, $result === null ? -1 : $result['response']?->id); - } - */ - - /** - * @covers Modules\Billing\Controller\ApiController - * @group module - */ - public function testBillCreateInvalidData() : void - { - $response = new HttpResponse(); - $request = new HttpRequest(new HttpUri('')); - - $request->header->account = 1; - $request->setData('invalid', '1'); - - $this->module->apiBillCreate($request, $response); - self::assertEquals(RequestStatusCode::R_400, $response->header->status); - } - - /** - * @covers Modules\Billing\Controller\ApiController - * @group module - */ - public function testBillElementCreateInvalidData() : void - { - $response = new HttpResponse(); - $request = new HttpRequest(new HttpUri('')); - - $request->header->account = 1; - $request->setData('invalid', '1'); - - $this->module->apiBillElementCreate($request, $response); - self::assertEquals(RequestStatusCode::R_400, $response->header->status); - } + use ApiBillControllerTrait; + use ApiPurchaseControllerTrait; } diff --git a/tests/Models/BillTest.php b/tests/Models/BillTest.php index 374a70f..1851f5b 100755 --- a/tests/Models/BillTest.php +++ b/tests/Models/BillTest.php @@ -15,10 +15,7 @@ declare(strict_types=1); namespace Modules\Billing\tests\Models; use Modules\Billing\Models\Bill; -use Modules\Billing\Models\BillElement; -use Modules\Billing\Models\BillStatus; use Modules\Billing\Models\NullBillType; -use phpOMS\Localization\ISO4217CharEnum; /** * @internal @@ -51,8 +48,6 @@ final class BillTest extends \PHPUnit\Framework\TestCase self::assertNull($this->bill->send); self::assertNull($this->bill->client); self::assertNull($this->bill->supplier); - self::assertEquals([], $this->bill->getVouchers()); - self::assertEquals([], $this->bill->getTrackings()); self::assertInstanceOf('\Modules\Media\Models\NullMedia', $this->bill->getFileByType(0)); self::assertEquals('', $this->bill->shipTo); @@ -82,7 +77,7 @@ final class BillTest extends \PHPUnit\Framework\TestCase self::assertEquals('', $this->bill->paymentText); self::assertEquals(0, $this->bill->terms); self::assertEquals('', $this->bill->termsText); - self::assertEquals(0, $this->bill->shipping); + self::assertEquals(0, $this->bill->shippingTerms); self::assertEquals('', $this->bill->shippingText); } @@ -96,94 +91,44 @@ final class BillTest extends \PHPUnit\Framework\TestCase self::assertEquals(\date('Y') . \date('m') . \date('d') . '-0', $this->bill->getNumber()); } - /** - * @covers Modules\Billing\Models\Bill - * @group module - */ - public function testStatusInputOutput() : void - { - $this->bill->setStatus(BillStatus::ACTIVE); - self::assertEquals(BillStatus::ACTIVE, $this->bill->getStatus()); - } - - /** - * @covers Modules\Billing\Models\Bill - * @group module - */ - public function testCurrencyInputOutput() : void - { - $this->bill->setCurrency(ISO4217CharEnum::_USD); - self::assertEquals(ISO4217CharEnum::_USD, $this->bill->getCurrency()); - } - - /** - * @covers Modules\Billing\Models\Bill - * @group module - */ - public function testVoucherInputOutput() : void - { - $this->bill->addVoucher('TEST'); - self::assertEquals(['TEST'], $this->bill->getVouchers()); - } - - /** - * @covers Modules\Billing\Models\Bill - * @group module - */ - public function testTrackingInputOutput() : void - { - $this->bill->addTracking('TEST'); - self::assertEquals(['TEST'], $this->bill->getTrackings()); - } - - /** - * @covers Modules\Billing\Models\Bill - * @group module - */ - public function testElementInputOutput() : void - { - $this->bill->addElement(new BillElement()); - self::assertCount(1, $this->bill->getElements()); - } - /** * @covers Modules\Billing\Models\Bill * @group module */ public function testSerialize() : void { - $this->bill->number = '123456'; - $this->bill->type = new NullBillType(2); - $this->bill->shipTo = 'To'; - $this->bill->shipFAO = 'FAO'; - $this->bill->shipAddress = 'Address'; - $this->bill->shipCity = 'City'; - $this->bill->shipZip = 'Zip'; - $this->bill->shipCountry = 'Country'; - $this->bill->billTo = 'To'; - $this->bill->billFAO = 'FAO'; - $this->bill->billAddress = 'Address'; - $this->bill->billCity = 'City'; - $this->bill->billZip = 'Zip'; - $this->bill->billCountry = 'Country'; + $this->bill->number = '123456'; + $this->bill->type = new NullBillType(2); + $this->bill->shipTo = 'To'; + $this->bill->shipFAO = 'FAO'; + $this->bill->shipAddress = 'Address'; + $this->bill->shipCity = 'City'; + $this->bill->shipZip = 'Zip'; + $this->bill->shipCountry = 'Country'; + $this->bill->billTo = 'To'; + $this->bill->billFAO = 'FAO'; + $this->bill->billAddress = 'Address'; + $this->bill->billCity = 'City'; + $this->bill->billZip = 'Zip'; + $this->bill->billCountry = 'Country'; self::assertEquals( [ - 'id' => 0, - 'number' => '123456', - 'type' => $this->bill->type, - 'shipTo' => 'To', - 'shipFAO' => 'FAO', - 'shipAddress' => 'Address', - 'shipCity' => 'City', - 'shipZip' => 'Zip', - 'shipCountry' => 'Country', - 'billTo' => 'To', - 'billFAO' => 'FAO', - 'billAddress' => 'Address', - 'billCity' => 'City', - 'billZip' => 'Zip', - 'billCountry' => 'Country', + 'id' => 0, + 'number' => '123456', + 'type' => $this->bill->type, + 'shipTo' => 'To', + 'shipFAO' => 'FAO', + 'shipAddress' => 'Address', + 'shipCity' => 'City', + 'shipZip' => 'Zip', + 'shipCountry' => 'Country', + 'billTo' => 'To', + 'billFAO' => 'FAO', + 'billAddress' => 'Address', + 'billCity' => 'City', + 'billZip' => 'Zip', + 'billCountry' => 'Country', ], $this->bill->jsonSerialize() ); diff --git a/tests/Models/InvoiceRecognitionTest.php b/tests/Models/InvoiceRecognitionTest.php new file mode 100644 index 0000000..9559fb0 --- /dev/null +++ b/tests/Models/InvoiceRecognitionTest.php @@ -0,0 +1,352 @@ +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 = []; + } +} diff --git a/tests/Models/NullBillElementTest.php b/tests/Models/NullBillElementTest.php index 9db5122..3f919ce 100755 --- a/tests/Models/NullBillElementTest.php +++ b/tests/Models/NullBillElementTest.php @@ -23,7 +23,7 @@ final class NullBillElementTest extends \PHPUnit\Framework\TestCase { /** * @covers Modules\Billing\Models\NullBillElement - * @group framework + * @group module */ public function testNull() : void { @@ -32,11 +32,21 @@ final class NullBillElementTest extends \PHPUnit\Framework\TestCase /** * @covers Modules\Billing\Models\NullBillElement - * @group framework + * @group module */ public function testId() : void { $null = new NullBillElement(2); self::assertEquals(2, $null->id); } + + /** + * @covers Modules\Billing\Models\NullBillElement + * @group module + */ + public function testJsonSerialize() : void + { + $null = new NullBillElement(2); + self::assertEquals(['id' => 2], $null->jsonSerialize()); + } } diff --git a/tests/Models/NullBillTest.php b/tests/Models/NullBillTest.php index 7829489..c7031e9 100755 --- a/tests/Models/NullBillTest.php +++ b/tests/Models/NullBillTest.php @@ -23,7 +23,7 @@ final class NullBillTest extends \PHPUnit\Framework\TestCase { /** * @covers Modules\Billing\Models\NullBill - * @group framework + * @group module */ public function testNull() : void { @@ -32,11 +32,21 @@ final class NullBillTest extends \PHPUnit\Framework\TestCase /** * @covers Modules\Billing\Models\NullBill - * @group framework + * @group module */ public function testId() : void { $null = new NullBill(2); self::assertEquals(2, $null->id); } + + /** + * @covers Modules\Billing\Models\NullBill + * @group module + */ + public function testJsonSerialize() : void + { + $null = new NullBill(2); + self::assertEquals(['id' => 2], $null->jsonSerialize()); + } } diff --git a/tests/Models/NullBillTypeTest.php b/tests/Models/NullBillTypeTest.php index 4e04f20..9a76209 100755 --- a/tests/Models/NullBillTypeTest.php +++ b/tests/Models/NullBillTypeTest.php @@ -23,7 +23,7 @@ final class NullBillTypeTest extends \PHPUnit\Framework\TestCase { /** * @covers Modules\Billing\Models\NullBillType - * @group framework + * @group module */ public function testNull() : void { @@ -32,11 +32,21 @@ final class NullBillTypeTest extends \PHPUnit\Framework\TestCase /** * @covers Modules\Billing\Models\NullBillType - * @group framework + * @group module */ public function testId() : void { $null = new NullBillType(2); self::assertEquals(2, $null->id); } + + /** + * @covers Modules\Billing\Models\NullBillType + * @group module + */ + public function testJsonSerialize() : void + { + $null = new NullBillType(2); + self::assertEquals(['id' => 2], $null->jsonSerialize()); + } } diff --git a/tests/Models/NullSubscriptionTest.php b/tests/Models/NullSubscriptionTest.php new file mode 100644 index 0000000..c5efdf4 --- /dev/null +++ b/tests/Models/NullSubscriptionTest.php @@ -0,0 +1,52 @@ +id); + } + + /** + * @covers Modules\Billing\Models\NullSubscription + * @group module + */ + public function testJsonSerialize() : void + { + $null = new NullSubscription(2); + self::assertEquals(['id' => 2], $null->jsonSerialize()); + } +} diff --git a/tests/Models/Price/NullPriceTest.php b/tests/Models/Price/NullPriceTest.php new file mode 100644 index 0000000..e667445 --- /dev/null +++ b/tests/Models/Price/NullPriceTest.php @@ -0,0 +1,52 @@ +id); + } + + /** + * @covers Modules\Billing\Models\Price\NullPrice + * @group module + */ + public function testJsonSerialize() : void + { + $null = new NullPrice(2); + self::assertEquals(['id' => 2], $null->jsonSerialize()); + } +} diff --git a/tests/Models/SalesBillMapperTest.php b/tests/Models/SalesBillMapperTest.php index 9798b77..fa0f343 100755 --- a/tests/Models/SalesBillMapperTest.php +++ b/tests/Models/SalesBillMapperTest.php @@ -59,15 +59,6 @@ final class SalesBillMapperTest extends \PHPUnit\Framework\TestCase self::assertEquals(0, SalesBillMapper::getSalesByClientId(99999, new \DateTime('now'), new \DateTime('now'))->getInt()); } - /** - * @covers Modules\Billing\Models\SalesBillMapper - * @group module - */ - public function testGetAvgSalesPriceByItemIdInvalid() : void - { - self::assertEquals(0, SalesBillMapper::getAvgSalesPriceByItemId(99999, new \DateTime('now'), new \DateTime('now'))->getInt()); - } - /** * @covers Modules\Billing\Models\SalesBillMapper * @group module @@ -146,7 +137,7 @@ final class SalesBillMapperTest extends \PHPUnit\Framework\TestCase */ public function testGetItemMonthlySalesCostsInvalid() : void { - self::assertEquals([], SalesBillMapper::getItemMonthlySalesCosts(99999, new \DateTime('now'), new \DateTime('now'))); + self::assertEquals([], SalesBillMapper::getItemMonthlySalesCosts([99999], new \DateTime('now'), new \DateTime('now'))); } /** diff --git a/tests/Models/Tax/NullTaxCombinationTest.php b/tests/Models/Tax/NullTaxCombinationTest.php new file mode 100644 index 0000000..bb033a2 --- /dev/null +++ b/tests/Models/Tax/NullTaxCombinationTest.php @@ -0,0 +1,52 @@ +id); + } + + /** + * @covers Modules\Billing\Models\Tax\NullTaxCombination + * @group module + */ + public function testJsonSerialize() : void + { + $null = new NullTaxCombination(2); + self::assertEquals(['id' => 2], $null->jsonSerialize()); + } +} diff --git a/tests/Models/bills/1.json b/tests/Models/bills/1.json new file mode 100644 index 0000000..9ef8af5 --- /dev/null +++ b/tests/Models/bills/1.json @@ -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" +} \ No newline at end of file diff --git a/tests/Models/bills/1.png b/tests/Models/bills/1.png new file mode 100644 index 0000000..954323b Binary files /dev/null and b/tests/Models/bills/1.png differ diff --git a/tests/Models/bills/10.json b/tests/Models/bills/10.json new file mode 100644 index 0000000..7791aff --- /dev/null +++ b/tests/Models/bills/10.json @@ -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" +} \ No newline at end of file diff --git a/tests/Models/bills/10.png b/tests/Models/bills/10.png new file mode 100644 index 0000000..ae903d6 Binary files /dev/null and b/tests/Models/bills/10.png differ diff --git a/tests/Models/bills/11.jpg b/tests/Models/bills/11.jpg new file mode 100644 index 0000000..79ab0a9 Binary files /dev/null and b/tests/Models/bills/11.jpg differ diff --git a/tests/Models/bills/11.json b/tests/Models/bills/11.json new file mode 100644 index 0000000..b99ea4a --- /dev/null +++ b/tests/Models/bills/11.json @@ -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": "" +} \ No newline at end of file diff --git a/tests/Models/bills/12.json b/tests/Models/bills/12.json new file mode 100644 index 0000000..7525ebd --- /dev/null +++ b/tests/Models/bills/12.json @@ -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" +} \ No newline at end of file diff --git a/tests/Models/bills/12.png b/tests/Models/bills/12.png new file mode 100644 index 0000000..e231802 Binary files /dev/null and b/tests/Models/bills/12.png differ diff --git a/tests/Models/bills/14.json b/tests/Models/bills/14.json new file mode 100644 index 0000000..91d1784 --- /dev/null +++ b/tests/Models/bills/14.json @@ -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" +} \ No newline at end of file diff --git a/tests/Models/bills/14.png b/tests/Models/bills/14.png new file mode 100644 index 0000000..571dcc6 Binary files /dev/null and b/tests/Models/bills/14.png differ diff --git a/tests/Models/bills/15.json b/tests/Models/bills/15.json new file mode 100644 index 0000000..f504363 --- /dev/null +++ b/tests/Models/bills/15.json @@ -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" +} \ No newline at end of file diff --git a/tests/Models/bills/15.png b/tests/Models/bills/15.png new file mode 100644 index 0000000..f07c49d Binary files /dev/null and b/tests/Models/bills/15.png differ diff --git a/tests/Models/bills/16.json b/tests/Models/bills/16.json new file mode 100644 index 0000000..9bfc568 --- /dev/null +++ b/tests/Models/bills/16.json @@ -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" +} \ No newline at end of file diff --git a/tests/Models/bills/16.png b/tests/Models/bills/16.png new file mode 100644 index 0000000..c234834 Binary files /dev/null and b/tests/Models/bills/16.png differ diff --git a/tests/Models/bills/17.json b/tests/Models/bills/17.json new file mode 100644 index 0000000..3cc29b2 --- /dev/null +++ b/tests/Models/bills/17.json @@ -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": "" +} \ No newline at end of file diff --git a/tests/Models/bills/17.png b/tests/Models/bills/17.png new file mode 100644 index 0000000..dd8bd25 Binary files /dev/null and b/tests/Models/bills/17.png differ diff --git a/tests/Models/bills/18.jpg b/tests/Models/bills/18.jpg new file mode 100644 index 0000000..6419779 Binary files /dev/null and b/tests/Models/bills/18.jpg differ diff --git a/tests/Models/bills/18.json b/tests/Models/bills/18.json new file mode 100644 index 0000000..2fadccc --- /dev/null +++ b/tests/Models/bills/18.json @@ -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" +} \ No newline at end of file diff --git a/tests/Models/bills/19.json b/tests/Models/bills/19.json new file mode 100644 index 0000000..a841dbf --- /dev/null +++ b/tests/Models/bills/19.json @@ -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" +} \ No newline at end of file diff --git a/tests/Models/bills/19.png b/tests/Models/bills/19.png new file mode 100644 index 0000000..a620d69 Binary files /dev/null and b/tests/Models/bills/19.png differ diff --git a/tests/Models/bills/2.json b/tests/Models/bills/2.json new file mode 100644 index 0000000..93d22cd --- /dev/null +++ b/tests/Models/bills/2.json @@ -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": "" +} \ No newline at end of file diff --git a/tests/Models/bills/2.png b/tests/Models/bills/2.png new file mode 100644 index 0000000..d68b7ff Binary files /dev/null and b/tests/Models/bills/2.png differ diff --git a/tests/Models/bills/3.json b/tests/Models/bills/3.json new file mode 100644 index 0000000..70eb5b1 --- /dev/null +++ b/tests/Models/bills/3.json @@ -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" +} \ No newline at end of file diff --git a/tests/Models/bills/3.png b/tests/Models/bills/3.png new file mode 100644 index 0000000..5a5abd0 Binary files /dev/null and b/tests/Models/bills/3.png differ diff --git a/tests/Models/bills/4.json b/tests/Models/bills/4.json new file mode 100644 index 0000000..fe80f85 --- /dev/null +++ b/tests/Models/bills/4.json @@ -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" +} \ No newline at end of file diff --git a/tests/Models/bills/4.png b/tests/Models/bills/4.png new file mode 100644 index 0000000..44abfa9 Binary files /dev/null and b/tests/Models/bills/4.png differ diff --git a/tests/Models/bills/5.json b/tests/Models/bills/5.json new file mode 100644 index 0000000..67205eb --- /dev/null +++ b/tests/Models/bills/5.json @@ -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" +} \ No newline at end of file diff --git a/tests/Models/bills/5.png b/tests/Models/bills/5.png new file mode 100644 index 0000000..e45d2b6 Binary files /dev/null and b/tests/Models/bills/5.png differ diff --git a/tests/Models/bills/6.json b/tests/Models/bills/6.json new file mode 100644 index 0000000..e5974c6 --- /dev/null +++ b/tests/Models/bills/6.json @@ -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" +} \ No newline at end of file diff --git a/tests/Models/bills/6.png b/tests/Models/bills/6.png new file mode 100644 index 0000000..880b739 Binary files /dev/null and b/tests/Models/bills/6.png differ diff --git a/tests/Models/bills/7.json b/tests/Models/bills/7.json new file mode 100644 index 0000000..80f9d0c --- /dev/null +++ b/tests/Models/bills/7.json @@ -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" +} \ No newline at end of file diff --git a/tests/Models/bills/7.png b/tests/Models/bills/7.png new file mode 100644 index 0000000..41ae689 Binary files /dev/null and b/tests/Models/bills/7.png differ diff --git a/tests/Models/bills/8.json b/tests/Models/bills/8.json new file mode 100644 index 0000000..98be9ba --- /dev/null +++ b/tests/Models/bills/8.json @@ -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": "" +} \ No newline at end of file diff --git a/tests/Models/bills/8.pdf b/tests/Models/bills/8.pdf new file mode 100644 index 0000000..10a3a94 Binary files /dev/null and b/tests/Models/bills/8.pdf differ diff --git a/tests/Models/bills/9.json b/tests/Models/bills/9.json new file mode 100644 index 0000000..98be9ba --- /dev/null +++ b/tests/Models/bills/9.json @@ -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": "" +} \ No newline at end of file diff --git a/tests/Models/bills/9.pdf b/tests/Models/bills/9.pdf new file mode 100644 index 0000000..99506f7 Binary files /dev/null and b/tests/Models/bills/9.pdf differ