diff --git a/Admin/Install/Media/bill.pdf.php b/Admin/Install/Media/bill.pdf.php index 0027d7d..4d39396 100755 --- a/Admin/Install/Media/bill.pdf.php +++ b/Admin/Install/Media/bill.pdf.php @@ -145,7 +145,7 @@ $pdf->MultiCell( ); $pdf->Ln(); -$tempY = $pdf->getY(); +$tempY = $pdf->getY(); $height = 0; $pdf->setY($tempY - 20); @@ -211,7 +211,7 @@ foreach($bill->elements as $line) { 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, 'L', $fill, 0, 15 + $w[0], $tempY, true, 0, false, true, 0, 'M', true); + $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); } 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 83336e7..66c1067 100755 --- a/Admin/Install/db.json +++ b/Admin/Install/db.json @@ -1152,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)", diff --git a/Admin/Installer.php b/Admin/Installer.php index 02239ec..e5afc82 100755 --- a/Admin/Installer.php +++ b/Admin/Installer.php @@ -311,6 +311,7 @@ final class Installer extends InstallerAbstract $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); diff --git a/Controller/ApiBillController.php b/Controller/ApiBillController.php index 8b1d978..2b84a91 100755 --- a/Controller/ApiBillController.php +++ b/Controller/ApiBillController.php @@ -93,7 +93,17 @@ final class ApiBillController extends Controller } } - public function apiBillEmail(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + /** + * 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') @@ -102,12 +112,12 @@ final class ApiBillController extends Controller ->where('id', $request->getDataInt('bill') ?? 0) ->execute(); - $media = $data['media'] ?? $bill->getFileByTypeName('internal');; + $media = $data['media'] ?? $bill->getFileByTypeName('internal'); if ($bill->status === BillStatus::ARCHIVED && $bill->type->email ) { - $email = $request->getDataString('email'); + $email = $request->getDataString('email'); $billingTemplate = null; if (!empty($email)) { @@ -134,7 +144,7 @@ final class ApiBillController extends Controller if ($client->getAttribute('bill_emails')->value->getValue() === 1) { // @todo should this really be a string or an ID for a contact element? - $email ??= empty($tmp = $client->getAttribute('bill_email_address')->value->getValue()) + $email ??= empty($tmp = $client->getAttribute('bill_email_address')->value->valueStr) ? $client->account->email : (string) $tmp; } @@ -156,18 +166,34 @@ final class ApiBillController extends Controller if ($supplier->getAttribute('bill_emails')->value->getValue() === 1) { // @todo should this really be a string or an ID for a contact element? - $email ??= empty($tmp = $supplier->getAttribute('bill_email_address')->value->getValue()) + $email ??= empty($tmp = $supplier->getAttribute('bill_email_address')->value->valueStr) ? $supplier->account->email : (string) $tmp; } } - if (!empty($email)) { + 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( @@ -192,13 +218,13 @@ final class ApiBillController extends Controller } // Archive bill - /** @var \Modules\Billing\Models\Bill $bill */ + /** @var \Modules\Billing\Models\Bill $old */ $old = BillMapper::get() ->with('type') ->where('id', $request->getDataInt('bill') ?? 0) ->execute(); - $new = clone $old; + $new = clone $old; $new->status = BillStatus::ARCHIVED; $this->updateModel($request->header->account, $old, $new, BillMapper::class, 'bill', $request->getOrigin()); @@ -218,7 +244,7 @@ final class ApiBillController extends Controller ]); // Send bill via email - $this->apiBillEmail($request, $response, ['bill' => $new, 'media' => $media]); + $this->apiBillEmail($request, ['bill' => $new, 'media' => $media]); $this->createStandardUpdateResponse($request, $response, $new); } @@ -440,6 +466,15 @@ final class ApiBillController extends Controller 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 */ @@ -516,10 +551,13 @@ final class ApiBillController extends Controller $attr->type = $attrType; $attr->value = $attrValue; - $container = $request->hasData('container') ? new NullContainer($request->getDataInt('container')) : null; - $attr = new NullAttribute(); + $container = $request->hasData('container') + ? new NullContainer((int) $request->getData('container')) + : null; - if ($bill->type->transferType === BillTransferType::PURCHASE) { + $attr = new NullAttribute(); + + if ($bill->type->transferType === BillTransferType::PURCHASE && $bill->supplier !== null) { $bill->supplier->attributes[] = $attr; if ($container === null) { @@ -534,7 +572,7 @@ final class ApiBillController extends Controller ->execute(); } } - } else { + } elseif ($bill->client !== null) { $bill->client->attributes[] = $attr; if ($container === null) { @@ -552,7 +590,7 @@ final class ApiBillController extends Controller } $container = $container === null && $attr->id !== 0 - ? new NullContainer($attr->value->getValue()) + ? new NullContainer($attr->value->valueInt ?? 0) : $container; $taxCombination = $this->app->moduleManager->get('Billing', 'ApiTax') @@ -561,8 +599,8 @@ final class ApiBillController extends Controller $element = BillElement::fromItem( $item, $taxCombination, - FloatInt::toInt($request->getDataString('quantity') ?? '1'), $bill, + FloatInt::toInt($request->getDataString('quantity') ?? '1'), $container ); @@ -1278,7 +1316,7 @@ final class ApiBillController extends Controller ->with('type') ->with('type/l11n') ->where('id', $request->getDataInt('bill') ?? 0) - ->where('type/l11n/language', new ColumnName(BillMapper::getColumnByMember('language'))) + ->where('type/l11n/language', new ColumnName(BillMapper::getColumnByMember('language') ?? '')) ->execute(); // Handle PDF generation @@ -1444,7 +1482,7 @@ final class ApiBillController extends Controller $media->setPath(\Modules\Media\Controller\ApiController::normalizeDbPath($pdfDir . '/' . $billFileName)); $media->setVirtualPath($path); - $media->size = \filesize($media->getAbsolutePath()); + $media->size = (int) \filesize($media->getAbsolutePath()); $this->updateModel($request->header->account, $oldFile, $media, MediaMapper::class, 'media', $request->getOrigin()); } @@ -1457,7 +1495,7 @@ final class ApiBillController extends Controller * * @param Media $media Media to send * @param string $email Email address - * @param string $template Email template + * @param int $template Email template * @param string $language Message language * * @return void diff --git a/Controller/ApiPriceController.php b/Controller/ApiPriceController.php index f27176b..eb42653 100755 --- a/Controller/ApiPriceController.php +++ b/Controller/ApiPriceController.php @@ -46,15 +46,31 @@ use phpOMS\Stdlib\Base\FloatInt; */ final class ApiPriceController extends Controller { - public function findBestPrice(RequestAbstract $request, ?Item $item = null, ?Client $client = null, ?Supplier $supplier = null) + /** + * Find the best price for a client/supplier and an item + * + * @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 array + * + * @since 1.0.0 + */ + public function findBestPrice( + RequestAbstract $request, + ?Item $item = null, + ?Client $client = null, ?Supplier $supplier = null + ) : array { - $item ??= new NullItem(); - $client ??= new NullClient(); + $item ??= new NullItem(); + $client ??= new NullClient(); $supplier ??= new NullSupplier(); // Get item if ($item->id === 0 && $request->hasData('price_item')) { - /** @var null|\Modules\ItemManagement\Models\Item $item */ + /** @var \Modules\ItemManagement\Models\Item $item */ $item = ItemMapper::get() ->with('attributes') ->with('attributes/type') @@ -84,7 +100,7 @@ final class ApiPriceController extends Controller ->execute(); } - $quantity = new FloatInt($request->getDataString('price_quantity') ?? FloatInt::DIVISOR); + $quantity = new FloatInt($request->getDataString('price_quantity') ?? FloatInt::DIVISOR); $quantity->value = $quantity->value === 0 ? FloatInt::DIVISOR : $quantity->value; // Get all relevant prices @@ -442,7 +458,7 @@ final class ApiPriceController extends Controller && $old->priceNew->value !== $new->priceNew->value ) { /** @var \Modules\ItemManagement\Models\Item $item */ - $item = ItemMapper::get()->where('id', $new->item)->execute(); + $item = ItemMapper::get()->where('id', $new->item)->execute(); $itemNew = clone $item; if ($new->type === PriceType::SALES) { @@ -492,14 +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->quantity = $request->getDataInt('quantity') ?? $new->quantity; - $new->price = $new->priceNew; - $new->priceNew = $request->hasData('price_new') ? new FloatInt((int) $request->getData('price_new')) : - $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; diff --git a/Controller/ApiPurchaseController.php b/Controller/ApiPurchaseController.php index 3ea7d2a..d15f382 100755 --- a/Controller/ApiPurchaseController.php +++ b/Controller/ApiPurchaseController.php @@ -84,7 +84,9 @@ final class ApiPurchaseController extends Controller /** * Method to create item attribute from request. * - * @param RequestAbstract $request Request + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data * * @return array * @@ -123,7 +125,7 @@ final class ApiPurchaseController extends Controller $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 diff --git a/Controller/ApiTaxController.php b/Controller/ApiTaxController.php index 215b249..cb22186 100755 --- a/Controller/ApiTaxController.php +++ b/Controller/ApiTaxController.php @@ -16,11 +16,12 @@ namespace Modules\Billing\Controller; 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; @@ -46,16 +47,26 @@ final class ApiTaxController extends Controller /** * Get tax code from client and item. * - * @param Item $item Item to get tax code from - * @param Client $client Client 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 + * @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 TaxCombination * * @since 1.0.0 */ - public function getTaxForPerson(Item $item, ?Client $client = null, ?Supplier $supplier = null, string $defaultCountry = '') : TaxCombination + public function getTaxForPerson( + Item $item, + ?Client $client = null, ?Supplier $supplier = null, + string $defaultCountry = '' + ) : TaxCombination { + if ($client === null && $supplier === null) { + return new NullTaxCombination(); + } + // @todo define default sales tax code if none available?! $itemCode = 0; $accountCode = 0; @@ -146,6 +157,7 @@ final class ApiTaxController extends Controller $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')); @@ -217,25 +229,23 @@ final class ApiTaxController extends Controller } elseif (\in_array($taxOfficeAddress->country, ISO3166TwoEnum::getRegion('eu'))) { // None EU company but we are EU company return $codes->getDefaultByValue('INT'); - } else { - // None EU company and we are also none EU company - return $codes->getDefaultByValue('INT'); } - return $taxCode; + // None EU company and we are also none EU company + return $codes->getDefaultByValue('INT'); } /** - * Get the client's tax code based on their country and tax office address + * Get the supplier's tax code based on their country and tax office address * - * @param Supplier $client The client to get the tax code for + * @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 client's tax code + * @return AttributeValue The supplier's tax code * * @since 1.0.0 */ - public function getSupplierTaxCode(Supplier $client, Address $taxOfficeAddress) : AttributeValue + public function getSupplierTaxCode(Supplier $supplier, Address $taxOfficeAddress) : AttributeValue { /** @var \Modules\Attribute\Models\AttributeType $codes */ $codes = SupplierAttributeTypeMapper::get() @@ -247,28 +257,26 @@ final class ApiTaxController extends Controller // @todo need to consider own tax id as well // @todo consider delivery & invoice location (Reihengeschaeft) - if ($taxOfficeAddress->country === $client->mainAddress->country) { + if ($taxOfficeAddress->country === $supplier->mainAddress->country) { // Same country as we (= local tax code) - return $codes->getDefaultByValue($client->mainAddress->country); + return $codes->getDefaultByValue($supplier->mainAddress->country); } elseif (\in_array($taxOfficeAddress->country, ISO3166TwoEnum::getRegion('eu')) - && \in_array($client->mainAddress->country, ISO3166TwoEnum::getRegion('eu')) + && \in_array($supplier->mainAddress->country, ISO3166TwoEnum::getRegion('eu')) ) { - if (!empty($client->getAttribute('vat_id')->value->getValue())) { + 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($client->mainAddress->country); + 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'); - } else { - // None EU company and we are also none EU company - return $codes->getDefaultByValue('INT'); } - return $taxCode; + // None EU company and we are also none EU company + return $codes->getDefaultByValue('INT'); } /** @@ -399,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) { diff --git a/Controller/BackendController.php b/Controller/BackendController.php index 8511c8d..2d53acc 100755 --- a/Controller/BackendController.php +++ b/Controller/BackendController.php @@ -191,7 +191,7 @@ final class BackendController extends Controller PermissionCategory::BILL_LOG, ) ) { - /** @var \Modules\Auditor\Models\Audit[] $logsBill */ + /** @var \Modules\Auditor\Models\Audit[] $logs */ $logs = AuditMapper::getAll() ->with('createdBy') ->where('module', 'Billing') @@ -212,8 +212,6 @@ final class BackendController extends Controller } } - - $view->data['logs'] = $logs; $view->data['media-upload'] = new \Modules\Media\Theme\Backend\Components\Upload\BaseView($this->app->l11nManager, $request, $response); diff --git a/Controller/CliController.php b/Controller/CliController.php index 9f1c050..0271494 100755 --- a/Controller/CliController.php +++ b/Controller/CliController.php @@ -138,14 +138,14 @@ final class CliController extends Controller $bill->billCountry = InvoiceRecognition::findCountry($lines, $identifiers, $language); } - $currency = InvoiceRecognition::findCurrency($lines); + $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 + ISO4217CharEnum::_CNH, ISO4217CharEnum::_CNY, ]) ) { $currency = $countryCurrency; @@ -156,7 +156,7 @@ final class CliController extends Controller $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() @@ -166,7 +166,7 @@ final class CliController extends Controller $bill->type = new NullBillType($billType->id); /* Number */ - $billNumber = InvoiceRecognition::findBillNumber($lines, $identifiers['bill_no'][$language]); + $billNumber = InvoiceRecognition::findBillNumber($lines, $identifiers['bill_no'][$language]); $bill->external = $billNumber; /* Reference / PO */ @@ -185,7 +185,7 @@ final class CliController extends Controller /* Total */ $totalGross = InvoiceRecognition::findBillGross($lines, $identifiers['total_gross'][$language]); - $totalNet = InvoiceRecognition::findBillNet($lines, $identifiers['total_net'][$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 @@ -194,11 +194,11 @@ final class CliController extends Controller if ($format !== null) { $l11n->thousands = $format['thousands']; - $l11n->decimal = $format['decimal']; + $l11n->decimal = $format['decimal']; } $bill->grossSales = new FloatInt($totalGross, $l11n->thousands, $l11n->decimal); - $bill->netSales = new FloatInt($totalNet, $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) @@ -238,7 +238,7 @@ final class CliController extends Controller foreach ($itemLines as $line => $itemLine) { $itemLineEnd = $line; - $billElement = new BillElement(); + $billElement = new BillElement(); $billElement->bill = $bill; $billElement->taxR->value = $taxRates; @@ -255,14 +255,14 @@ final class CliController extends Controller if (isset($itemLine['price'])) { $billElement->singleListPriceNet = new FloatInt($itemLine['price'], $l11n->thousands, $l11n->decimal); - $billElement->singleSalesPriceNet = $billElement->singleListPriceNet; + $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; + $billElement->singleSalesPriceGross = $billElement->singleListPriceGross; } else { - $billElement->singleListPriceGross = $billElement->singleListPriceNet; + $billElement->singleListPriceGross = $billElement->singleListPriceNet; $billElement->singleSalesPriceGross = $billElement->singleListPriceGross; } } @@ -271,14 +271,14 @@ final class CliController extends Controller if (isset($itemLine['total'])) { $billElement->totalListPriceNet = new FloatInt($itemLine['total'], $l11n->thousands, $l11n->decimal); - $billElement->totalSalesPriceNet = $billElement->totalListPriceNet; + $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; + $billElement->totalSalesPriceGross = $billElement->totalListPriceGross; } else { - $billElement->totalListPriceGross = $billElement->totalListPriceNet; + $billElement->totalListPriceGross = $billElement->totalListPriceNet; $billElement->totalSalesPriceGross = $billElement->totalListPriceGross; } } @@ -305,21 +305,21 @@ final class CliController extends Controller $key = \str_replace('total_', '', $key); - $billElement = new BillElement(); + $billElement = new BillElement(); $billElement->bill = $bill; $billElement->taxR->value = $taxRates; - $internalRequest = new HttpRequest(); + $internalRequest = new HttpRequest(); $internalResponse = new HttpResponse(); $internalRequest->header->account = $request->header->account; - $internalRequest->header->l11n = $request->header->l11n; + $internalRequest->header->l11n = $request->header->l11n; $internalRequest->setData('search', $key); $internalRequest->setData('limit', 1); - $internalResponse->header->l11n = clone $response->header->l11n; + $internalResponse->header->l11n = clone $response->header->l11n; $internalResponse->header->l11n->language = $bill->language; $this->app->moduleManager->get('ItemManagement', 'Api')->apiItemFind($internalRequest, $internalResponse); @@ -328,9 +328,9 @@ final class CliController extends Controller $billElement->itemName = $key; if ($item->id !== 0) { - $billElement->item = $item; + $billElement->item = $item; $billElement->itemNumber = $item->number; - $billElement->itemName = $item->getL11n('name1')->content; + $billElement->itemName = $item->getL11n('name1')->content; } $billElement->quantity->value = FloatInt::DIVISOR; @@ -338,23 +338,23 @@ final class CliController extends Controller // Unit $billElement->singleListPriceNet = new FloatInt($amount, $l11n->thousands, $l11n->decimal); - $billElement->singleSalesPriceNet = $billElement->singleListPriceNet; + $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; + $billElement->singleSalesPriceGross = $billElement->singleListPriceGross; } else { - $billElement->singleListPriceGross = $billElement->singleListPriceNet; + $billElement->singleListPriceGross = $billElement->singleListPriceNet; $billElement->singleSalesPriceGross = $billElement->singleListPriceGross; } // Total - $billElement->totalListPriceNet = $billElement->singleListPriceNet; - $billElement->totalSalesPriceNet = $billElement->singleSalesPriceNet; + $billElement->totalListPriceNet = $billElement->singleListPriceNet; + $billElement->totalSalesPriceNet = $billElement->singleSalesPriceNet; $billElement->totalPurchasePriceNet = $billElement->singlePurchasePriceNet; - $billElement->totalListPriceGross = $billElement->singleListPriceGross; - $billElement->totalSalesPriceGross = $billElement->singleSalesPriceGross; + $billElement->totalListPriceGross = $billElement->singleListPriceGross; + $billElement->totalSalesPriceGross = $billElement->singleSalesPriceGross; $billElement->taxP->value = $billElement->totalSalesPriceGross->value - $billElement->totalSalesPriceNet->value; @@ -367,40 +367,40 @@ final class CliController extends Controller if (!empty($bill->elements)) { // Calculate totals from elements - $totalNet = 0; + $totalNet = 0; $totalGross = 0; foreach ($bill->elements as $element) { - $totalNet += $element->totalSalesPriceNet->value; + $totalNet += $element->totalSalesPriceNet->value; $totalGross += $element->totalSalesPriceGross->value; } $bill->grossSales = new FloatInt($totalGross); - $bill->netCosts = new FloatInt($totalNet); - $bill->netSales = $bill->netCosts; + $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 = new BillElement(); $billElement->bill = $bill; // List price $billElement->singleListPriceNet->value = $bill->netSales->value; - $billElement->totalListPriceNet->value = $bill->netSales->value; + $billElement->totalListPriceNet->value = $bill->netSales->value; $billElement->singleListPriceGross->value = $bill->grossSales->value; - $billElement->totalListPriceGross->value = $bill->grossSales->value; + $billElement->totalListPriceGross->value = $bill->grossSales->value; // Unit price - $billElement->singleSalesPriceNet->value = $bill->netSales->value; + $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->totalSalesPriceNet->value = $bill->netSales->value; $billElement->totalPurchasePriceNet->value = $bill->netSales->value; $billElement->totalSalesPriceGross->value = $bill->grossSales->value; @@ -415,16 +415,16 @@ final class CliController extends Controller } // Re-calculate totals from elements due to change - $totalNet = 0; + $totalNet = 0; $totalGross = 0; foreach ($bill->elements as $element) { - $totalNet += $element->totalSalesPriceNet->value; + $totalNet += $element->totalSalesPriceNet->value; $totalGross += $element->totalSalesPriceGross->value; } $bill->grossSales = new FloatInt($totalGross); - $bill->netCosts = new FloatInt($totalNet); - $bill->netSales = $bill->netCosts; + $bill->netCosts = new FloatInt($totalNet); + $bill->netSales = $bill->netCosts; $bill->taxP->value = $bill->grossSales->value - $bill->netSales->value; @@ -442,37 +442,6 @@ final class CliController extends Controller return $view; } - - /** - * 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; - } - /** * Find possible supplier id * diff --git a/Models/Bill.php b/Models/Bill.php index 64197df..2a6784a 100755 --- a/Models/Bill.php +++ b/Models/Bill.php @@ -411,6 +411,8 @@ class Bill implements \JsonSerializable public ?string $fiAccount = null; + // @todo Implement reason for bill (especially useful for credit notes, warehouse bookings) + /** * Constructor. * @@ -418,12 +420,12 @@ class Bill implements \JsonSerializable */ public function __construct() { - $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->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(); @@ -530,7 +532,15 @@ class Bill implements \JsonSerializable $this->netDiscount->value += $element->totalDiscountP->value; } - // @todo also consider rounding similarly to recalculatePrices in elements + /** + * 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() @@ -542,6 +552,13 @@ class Bill implements \JsonSerializable && $this->areElementsValid(); } + /** + * Validate the correctness of the bill elements + * + * @return bool + * + * @since 1.0.0 + */ public function areElementsValid() : bool { foreach ($this->elements as $element) { @@ -553,21 +570,49 @@ class Bill implements \JsonSerializable 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; @@ -578,6 +623,13 @@ class Bill implements \JsonSerializable 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; @@ -588,6 +640,13 @@ class Bill implements \JsonSerializable 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; @@ -598,6 +657,13 @@ class Bill implements \JsonSerializable 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) { @@ -620,7 +686,7 @@ class Bill implements \JsonSerializable return [ 'id' => $this->id, 'number' => $this->number, - 'external' => $this->external, + 'external' => $this->external, 'type' => $this->type, 'shipTo' => $this->shipTo, 'shipFAO' => $this->shipFAO, diff --git a/Models/BillElement.php b/Models/BillElement.php index 6041f00..024c17a 100755 --- a/Models/BillElement.php +++ b/Models/BillElement.php @@ -123,6 +123,8 @@ class BillElement implements \JsonSerializable public ?string $costobject = null; + public ?TaxCombination $taxCombination = null; + /** * Tax amount * @@ -187,11 +189,11 @@ class BillElement implements \JsonSerializable $this->totalSalesPriceNet = new FloatInt(); $this->totalSalesPriceGross = new FloatInt(); - $this->singlePurchasePriceNet = new FloatInt(); - $this->totalPurchasePriceNet = new FloatInt(); + $this->singlePurchasePriceNet = new FloatInt(); + $this->totalPurchasePriceNet = new FloatInt(); - $this->singleProfitNet = new FloatInt(); - $this->totalProfitNet = new FloatInt(); + $this->singleProfitNet = new FloatInt(); + $this->totalProfitNet = new FloatInt(); $this->singleDiscountP = new FloatInt(); $this->totalDiscountP = new FloatInt(); @@ -213,15 +215,24 @@ class BillElement implements \JsonSerializable */ public function setQuantity(int $quantity) : void { - if ($this->quantity === $quantity) { + if ($this->quantity->value === $quantity) { return; } - $this->quantity = $quantity; + $this->quantity->value = $quantity; $this->recalculatePrices(); } + /** + * Re-calculate prices. + * + * This function is very important to call after changing any prices/quantities + * + * @return void + * + * @since 1.0.0 + */ public function recalculatePrices() : void { $rd = -FloatInt::MAX_DECIMALS + ISO4217DecimalEnum::getByName('_' . $this->bill->currency); @@ -249,7 +260,15 @@ class BillElement implements \JsonSerializable $this->effectiveSingleSalesPriceNet->value = (int) \round($this->totalSalesPriceNet->value / ($this->quantity->value / FloatInt::DIVISOR), $rd); } - // @todo also consider rounding similarly to recalculatePrices + /** + * 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() @@ -261,6 +280,13 @@ class BillElement implements \JsonSerializable && $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 @@ -269,21 +295,42 @@ class BillElement implements \JsonSerializable && $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->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) @@ -291,6 +338,13 @@ class BillElement implements \JsonSerializable && \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; @@ -301,11 +355,25 @@ class BillElement implements \JsonSerializable && ((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) @@ -332,10 +400,11 @@ class BillElement implements \JsonSerializable /** * Create element from item * - * @param Item $item Item - * @param TaxCode $taxCode Tax code used for gross amount calculation - * @param int $quantity Quantity - * @param Bill $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 * @@ -344,8 +413,8 @@ class BillElement implements \JsonSerializable public static function fromItem( Item $item, TaxCombination $taxCombination, + Bill $bill, int $quantity = FloatInt::DIVISOR, - Bill $bill = null, ?Container $container = null ) : self { @@ -358,9 +427,10 @@ class BillElement implements \JsonSerializable $element->itemDescription = $item->getL11n('description_short')->content; $element->quantity->value = $quantity; - $element->taxR = new FloatInt($taxCombination->taxCode->percentageInvoice); - $element->taxCode = $taxCombination->taxCode->abbr; - $element->fiAccount = $taxCombination->account; + $element->taxR = new FloatInt($taxCombination->taxCode->percentageInvoice); + $element->taxCode = $taxCombination->taxCode->abbr; + $element->fiAccount = $taxCombination->account; + $element->taxCombination = $taxCombination; // @todo the purchase price is based on lot/sn/avg prices if available $element->singlePurchasePriceNet->value = $item->purchasePrice->value; @@ -391,7 +461,7 @@ class BillElement implements \JsonSerializable return [ 'id' => $this->id, 'order' => $this->order, - 'item' => $this->item->id, + 'item' => $this->item?->id, 'itemNumber' => $this->itemNumber, 'itemName' => $this->itemName, 'itemDescription' => $this->itemDescription, diff --git a/Models/BillElementMapper.php b/Models/BillElementMapper.php index 8966399..d8771e3 100755 --- a/Models/BillElementMapper.php +++ b/Models/BillElementMapper.php @@ -14,6 +14,7 @@ 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; @@ -58,16 +59,17 @@ final class BillElementMapper extends DataMapperFactory '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_total_netprofit' => ['name' => 'billing_bill_element_total_netprofit', 'type' => 'Serializable', 'internal' => 'totalProfitNet'], + '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_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_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'], @@ -112,6 +114,10 @@ final class BillElementMapper extends DataMapperFactory '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 eac195c..2d94804 100755 --- a/Models/BillMapper.php +++ b/Models/BillMapper.php @@ -48,7 +48,7 @@ class BillMapper extends DataMapperFactory 'billing_bill_id' => ['name' => 'billing_bill_id', 'type' => 'int', 'internal' => 'id'], 'billing_bill_sequence' => ['name' => 'billing_bill_sequence', 'type' => 'int', 'internal' => 'sequence'], 'billing_bill_number' => ['name' => 'billing_bill_number', 'type' => 'string', 'internal' => 'number'], - 'billing_bill_external' => ['name' => 'billing_bill_external', 'type' => 'string', 'internal' => 'external'], + 'billing_bill_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'], diff --git a/Models/BillTypeMapper.php b/Models/BillTypeMapper.php index c5452a0..d3cccfe 100755 --- a/Models/BillTypeMapper.php +++ b/Models/BillTypeMapper.php @@ -46,7 +46,7 @@ final class BillTypeMapper extends DataMapperFactory 'billing_type_transfer_stock' => ['name' => 'billing_type_transfer_stock', 'type' => 'bool', 'internal' => 'transferStock'], 'billing_type_accounting' => ['name' => 'billing_type_accounting', 'type' => 'bool', 'internal' => 'isAccounting'], 'billing_type_transfer_sign' => ['name' => 'billing_type_transfer_sign', 'type' => 'int', 'internal' => 'sign'], - 'billing_type_email' => ['name' => 'billing_type_email', 'type' => 'bool', 'internal' => 'email'], + 'billing_type_email' => ['name' => 'billing_type_email', 'type' => 'bool', 'internal' => 'email'], 'billing_type_is_template' => ['name' => 'billing_type_is_template', 'type' => 'bool', 'internal' => 'isTemplate'], ]; diff --git a/Models/InvoiceRecognition.php b/Models/InvoiceRecognition.php index 18ed979..74c11be 100644 --- a/Models/InvoiceRecognition.php +++ b/Models/InvoiceRecognition.php @@ -35,9 +35,19 @@ use phpOMS\Validation\Finance\IbanEnum; */ class InvoiceRecognition { - public static function detect(Bill $bill, string $content) + /** + * Detect bill components + * + * @param Bill $bill Bill + * @param string $content Bill content + * + * @return void + * + * @since 1.0.0 + */ + public static function detect(Bill $bill, string $content) : void { - $content = \strtolower($content ?? ''); + $content = \strtolower($content); $lines = \explode("\n", $content); foreach ($lines as $line => $value) { if (empty(\trim($value))) { @@ -64,16 +74,16 @@ class InvoiceRecognition /** @var array $identifiers */ $identifiers = \json_decode($identifierContent, true); - $bill->billCountry = InvoiceRecognition::findCountry($lines, $identifiers, $language); + $bill->billCountry = self::findCountry($lines, $identifiers, $language); - $currency = self::findCurrency($lines); + $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 + ISO4217CharEnum::_CNH, ISO4217CharEnum::_CNY, ]) ) { $currency = $countryCurrency; @@ -86,8 +96,8 @@ class InvoiceRecognition /* Type */ $type = self::findSupplierInvoiceType($content, $identifiers['type'], $language); - /** @var \Modules\Billing\Models\BillType $billType */ /* + @var \Modules\Billing\Models\BillType $billType $billType = BillTypeMapper::get() ->where('name', $type) ->execute(); @@ -96,7 +106,7 @@ class InvoiceRecognition */ /* Number */ - $billNumber = self::findBillNumber($lines, $identifiers['bill_no'][$language]); + $billNumber = self::findBillNumber($lines, $identifiers['bill_no'][$language]); $bill->external = $billNumber; /* Reference / PO */ @@ -115,7 +125,7 @@ class InvoiceRecognition /* Total */ $totalGross = self::findBillGross($lines, $identifiers['total_gross'][$language]); - $totalNet = self::findBillNet($lines, $identifiers['total_net'][$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 @@ -124,11 +134,11 @@ class InvoiceRecognition if ($format !== null) { $l11n->thousands = $format['thousands']; - $l11n->decimal = $format['decimal']; + $l11n->decimal = $format['decimal']; } $bill->grossSales = new FloatInt($totalGross, $l11n->thousands, $l11n->decimal); - $bill->netSales = new FloatInt($totalNet, $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) @@ -166,7 +176,7 @@ class InvoiceRecognition foreach ($itemLines as $line => $itemLine) { $itemLineEnd = $line; - $billElement = new BillElement(); + $billElement = new BillElement(); $billElement->bill = $bill; $billElement->taxR->value = $taxRates; @@ -183,14 +193,14 @@ class InvoiceRecognition if (isset($itemLine['price'])) { $billElement->singleListPriceNet = new FloatInt($itemLine['price'], $l11n->thousands, $l11n->decimal); - $billElement->singleSalesPriceNet = $billElement->singleListPriceNet; + $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; + $billElement->singleSalesPriceGross = $billElement->singleListPriceGross; } else { - $billElement->singleListPriceGross = $billElement->singleListPriceNet; + $billElement->singleListPriceGross = $billElement->singleListPriceNet; $billElement->singleSalesPriceGross = $billElement->singleListPriceGross; } } @@ -199,14 +209,14 @@ class InvoiceRecognition if (isset($itemLine['total'])) { $billElement->totalListPriceNet = new FloatInt($itemLine['total'], $l11n->thousands, $l11n->decimal); - $billElement->totalSalesPriceNet = $billElement->totalListPriceNet; + $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; + $billElement->totalSalesPriceGross = $billElement->totalListPriceGross; } else { - $billElement->totalListPriceGross = $billElement->totalListPriceNet; + $billElement->totalListPriceGross = $billElement->totalListPriceNet; $billElement->totalSalesPriceGross = $billElement->totalListPriceGross; } } @@ -231,7 +241,7 @@ class InvoiceRecognition $key = \str_replace('total_', '', $key); - $billElement = new BillElement(); + $billElement = new BillElement(); $billElement->bill = $bill; $billElement->taxR->value = $taxRates; @@ -241,23 +251,23 @@ class InvoiceRecognition // Unit $billElement->singleListPriceNet = new FloatInt($amount, $l11n->thousands, $l11n->decimal); - $billElement->singleSalesPriceNet = $billElement->singleListPriceNet; + $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; + $billElement->singleSalesPriceGross = $billElement->singleListPriceGross; } else { - $billElement->singleListPriceGross = $billElement->singleListPriceNet; + $billElement->singleListPriceGross = $billElement->singleListPriceNet; $billElement->singleSalesPriceGross = $billElement->singleListPriceGross; } // Total - $billElement->totalListPriceNet = $billElement->singleListPriceNet; - $billElement->totalSalesPriceNet = $billElement->singleSalesPriceNet; + $billElement->totalListPriceNet = $billElement->singleListPriceNet; + $billElement->totalSalesPriceNet = $billElement->singleSalesPriceNet; $billElement->totalPurchasePriceNet = $billElement->singlePurchasePriceNet; - $billElement->totalListPriceGross = $billElement->singleListPriceGross; - $billElement->totalSalesPriceGross = $billElement->singleSalesPriceGross; + $billElement->totalListPriceGross = $billElement->singleListPriceGross; + $billElement->totalSalesPriceGross = $billElement->singleSalesPriceGross; $billElement->taxP->value = $billElement->totalSalesPriceGross->value - $billElement->totalSalesPriceNet->value; @@ -268,41 +278,40 @@ class InvoiceRecognition if (!empty($bill->elements)) { // Calculate totals from elements - $totalNet = 0; + $totalNet = 0; $totalGross = 0; foreach ($bill->elements as $element) { - $totalNet += $element->totalSalesPriceNet->value; + $totalNet += $element->totalSalesPriceNet->value; $totalGross += $element->totalSalesPriceGross->value; } - $bill->grossSales = new FloatInt($totalGross); - $bill->netCosts = new FloatInt($totalNet); - $bill->netSales = $bill->netCosts; + $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 = new BillElement(); $billElement->bill = $bill; // List price $billElement->singleListPriceNet->value = $bill->netSales->value; - $billElement->totalListPriceNet->value = $bill->netSales->value; + $billElement->totalListPriceNet->value = $bill->netSales->value; $billElement->singleListPriceGross->value = $bill->grossSales->value; - $billElement->totalListPriceGross->value = $bill->grossSales->value; + $billElement->totalListPriceGross->value = $bill->grossSales->value; // Unit price - $billElement->singleSalesPriceNet->value = $bill->netSales->value; + $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->totalSalesPriceNet->value = $bill->netSales->value; $billElement->totalPurchasePriceNet->value = $bill->netSales->value; $billElement->totalSalesPriceGross->value = $bill->grossSales->value; @@ -315,16 +324,16 @@ class InvoiceRecognition } // Re-calculate totals from elements due to change - $totalNet = 0; + $totalNet = 0; $totalGross = 0; foreach ($bill->elements as $element) { - $totalNet += $element->totalSalesPriceNet->value; + $totalNet += $element->totalSalesPriceNet->value; $totalGross += $element->totalSalesPriceGross->value; } $bill->grossSales = new FloatInt($totalGross); - $bill->netCosts = new FloatInt($totalNet); - $bill->netSales = $bill->netCosts; + $bill->netCosts = new FloatInt($totalNet); + $bill->netSales = $bill->netCosts; $bill->taxP->value = $bill->grossSales->value - $bill->netSales->value; } @@ -572,7 +581,7 @@ class InvoiceRecognition * @param string[] $lines Bill lines * @param array $matches Net match patterns * - * @return int + * @return string * * @bug Issue with net/discount/gross in one line * @@ -582,7 +591,7 @@ class InvoiceRecognition */ public static function findBillNet(array $lines, array $matches) : string { - $bestMatch = 0; + $bestMatch = 0; $bestMatchStr = ''; $found = []; @@ -605,7 +614,7 @@ class InvoiceRecognition : FloatInt::DIVISOR); if ($net > $bestMatch) { - $bestMatch = $net; + $bestMatch = $net; $bestMatchStr = $temp; } } @@ -629,7 +638,7 @@ class InvoiceRecognition */ public static function findBillGross(array $lines, array $matches) : string { - $bestMatch = 0; + $bestMatch = 0; $bestMatchStr = ''; $found = []; @@ -652,7 +661,7 @@ class InvoiceRecognition : FloatInt::DIVISOR); if ($gross > $bestMatch) { - $bestMatch = $gross; + $bestMatch = $gross; $bestMatchStr = $temp; } } @@ -678,9 +687,7 @@ class InvoiceRecognition { // Find discounts $bestDiscount = 0; - $found = []; - - $discountLine = 0; + $found = []; foreach ($matches['total_discount'][$language] as $match) { foreach ($lines as $idx => $line) { @@ -715,7 +722,7 @@ class InvoiceRecognition // Find shipping $bestShipping = 0; - $found = []; + $found = []; $shippingLine = 0; @@ -750,7 +757,7 @@ class InvoiceRecognition // Find customs $bestCustoms = 0; - $found = []; + $found = []; $customsLine = 0; @@ -785,7 +792,7 @@ class InvoiceRecognition // Find insurance $bestInsurance = 0; - $found = []; + $found = []; $insuranceLine = 0; @@ -820,9 +827,7 @@ class InvoiceRecognition // Find surcharge $bestSurcharge = 0; - $found = []; - - $surchargeLine = 0; + $found = []; foreach ($matches['total_surcharge'][$language] as $match) { foreach ($lines as $idx => $line) { @@ -849,7 +854,6 @@ class InvoiceRecognition if ($surcharge > $bestSurcharge) { $bestSurcharge = $surcharge; - $surchargeLine = $idx; break; } @@ -858,9 +862,9 @@ class InvoiceRecognition } return [ - 'total_discount' => -1 * $bestDiscount, - 'total_shipping' => $bestShipping, - 'total_customs' => $bestCustoms, + 'total_discount' => -1 * $bestDiscount, + 'total_shipping' => $bestShipping, + 'total_customs' => $bestCustoms, 'total_insurance' => $bestInsurance, 'total_surcharge' => $bestSurcharge, ]; @@ -926,9 +930,9 @@ class InvoiceRecognition $rows = []; // Get item list until end of item list/table is reached - $found = []; + $found = []; $structureCount = \count($headlineStructure); - $linesSkipped = 0; + $linesSkipped = 0; foreach ($lines as $l => $line) { // @todo find better way to identify end of item table @@ -950,7 +954,7 @@ class InvoiceRecognition $linesSkipped = 0; $temp = []; - $c = 0; + $c = 0; foreach ($headlineStructure as $idx => $_) { $subFound = []; @@ -970,8 +974,8 @@ class InvoiceRecognition /** * Create DateTime from date string * - * @param string $date Date string - * @param string[] $formats Date formats + * @param string $date Date string + * @param string[] $formats Date formats * * @return null|\DateTime * @@ -981,14 +985,14 @@ class InvoiceRecognition { if ((!empty($supplierFormat))) { $dt = \DateTime::createFromFormat( - $supplierFormat ?? '', + $supplierFormat, $date ); return $dt === false ? new \DateTime('1970-01-01') : $dt; } - $now = new \DateTime('now'); + $now = new \DateTime('now'); $bestMatch = null; foreach ($formats as $format) { @@ -1137,8 +1141,8 @@ class InvoiceRecognition if (\stripos($bestMatch, 'S') > 1 || \stripos($bestMatch, 'O') > 1 ) { - $subIban = \substr($bestMatch, 2); - $subIban = \str_replace(['S', 'O'], ['5', '0'], $subIban); + $subIban = \substr($bestMatch, 2); + $subIban = \str_replace(['S', 'O'], ['5', '0'], $subIban); $bestMatch = \substr($bestMatch, 0, 2) . $subIban; } @@ -1221,9 +1225,10 @@ class InvoiceRecognition if (\stripos($bestMatch, 'S') > 1 || \stripos($bestMatch, 'O') > 1 ) { - $format = IbanEnum::getByName('_' . \substr($bestMatch, 0, 2)); + /** @var string $format */ + $format = IbanEnum::getByName('_' . \substr($bestMatch, 0, 2)) ?? ''; - $len = \strlen($bestMatch); + $len = \strlen($bestMatch); $formatLen = \strlen($format); for ($i = 0; $i < $len; ++$i) { @@ -1245,12 +1250,18 @@ class InvoiceRecognition $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']); @@ -1267,7 +1278,7 @@ class InvoiceRecognition return \strtoupper(\substr($vatId, 0, 2)); } - $email = self::findEmail($lines, $matches['email']); + $email = self::findEmail($lines, $matches['email']); $country = \strtoupper(\substr($email, \strrpos($email, '.') + 1)); if (ISO3166TwoEnum::isValidValue($country)) { @@ -1286,9 +1297,18 @@ class InvoiceRecognition 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(); + $symbols = ISO4217SymbolEnum::getConstants(); $currency = ''; foreach ($lines as $line) { @@ -1299,8 +1319,11 @@ class InvoiceRecognition } if (\strpos($line, $match) !== false) { + /** @var string $currency */ $currency = ISO4217SymbolEnum::getName($symbol); - $currency = ISO4217CharEnum::getByName($currency); + + /** @var string $currency */ + $currency = ISO4217CharEnum::getByName($currency) ?? ''; break; } diff --git a/Models/Price/PriceMapper.php b/Models/Price/PriceMapper.php index 9fa9a26..6d15044 100755 --- a/Models/Price/PriceMapper.php +++ b/Models/Price/PriceMapper.php @@ -60,7 +60,7 @@ final class PriceMapper extends DataMapperFactory 'billing_price_supplier' => ['name' => 'billing_price_supplier', 'type' => 'int', 'internal' => 'supplier'], 'billing_price_unit' => ['name' => 'billing_price_unit', 'type' => 'int', 'internal' => 'unit'], 'billing_price_type' => ['name' => 'billing_price_type', 'type' => 'int', 'internal' => 'type'], - 'billing_price_status' => ['name' => 'billing_price_status', 'type' => 'int', 'internal' => 'status'], + 'billing_price_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'], diff --git a/Models/SalesBillMapper.php b/Models/SalesBillMapper.php index ae406bb6..60b1360 100755 --- a/Models/SalesBillMapper.php +++ b/Models/SalesBillMapper.php @@ -39,6 +39,7 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getSalesBeforePivot( mixed $pivot, @@ -58,6 +59,7 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getSalesAfterPivot( mixed $pivot, @@ -77,6 +79,7 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getSalesByItemId(int $id, \DateTime $start, \DateTime $end) : FloatInt { @@ -98,6 +101,7 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getSalesByClientId(int $id, \DateTime $start, \DateTime $end) : FloatInt { @@ -117,6 +121,7 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getItemAvgSalesPrice(int $item, \DateTime $start, \DateTime $end) : FloatInt { @@ -133,7 +138,7 @@ final class SalesBillMapper extends BillMapper SQL; $query = new Builder(self::$db); - $result = $query->raw($sql)->execute()->fetchAll(\PDO::FETCH_ASSOC); + $result = $query->raw($sql)->execute()?->fetchAll(\PDO::FETCH_ASSOC) ?? []; return isset($result[0]['net_count']) ? new FloatInt((int) ($result[0]['net_sales'] ?? 0) / ($result[0]['net_count'])) @@ -142,6 +147,7 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getLastOrderDateByItemId(int $id) : ?\DateTimeImmutable { @@ -164,6 +170,7 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getLastOrderDateByClientId(int $id) : ?\DateTimeImmutable { @@ -184,6 +191,7 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getItemRetentionRate(int $id, \DateTime $start, \DateTime $end) : float { @@ -192,6 +200,7 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getItemLivetimeValue(int $id, \DateTime $start, \DateTime $end) : FloatInt { @@ -200,6 +209,7 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getNewestItemInvoices(int $id, int $limit = 10) : array { @@ -221,6 +231,7 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getNewestClientInvoices(int $id, int $limit = 10) : array { @@ -240,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 { @@ -280,6 +292,7 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getItemBills(int $id, \DateTime $start, \DateTime $end) : array { @@ -308,6 +321,7 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getClientBills(int $id, string $language, \DateTime $start, \DateTime $end) : array { @@ -323,6 +337,7 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getClientItem(int $client, \DateTime $start, \DateTime $end) : array { @@ -336,6 +351,7 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getItemCountrySales(int $id, \DateTime $start, \DateTime $end, int $limit = 10) : array { @@ -361,6 +377,7 @@ final class SalesBillMapper extends BillMapper /** * Placeholder + * @todo Implement */ public static function getItemMonthlySalesCosts(array $items, \DateTime $start, \DateTime $end) : array { @@ -388,11 +405,15 @@ final class SalesBillMapper extends BillMapper SQL; $query = new Builder(self::$db); - $result = $query->raw($sql)->execute()->fetchAll(\PDO::FETCH_ASSOC); + $result = $query->raw($sql)->execute()?->fetchAll(\PDO::FETCH_ASSOC) ?? []; return $result ?? []; } + /** + * Placeholder + * @todo Implement + */ public static function getItemMonthlySalesQuantity(array $items, \DateTime $start, \DateTime $end) : array { if (empty($items)) { @@ -418,13 +439,14 @@ final class SalesBillMapper extends BillMapper SQL; $query = new Builder(self::$db); - $result = $query->raw($sql)->execute()->fetchAll(\PDO::FETCH_ASSOC); + $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 { @@ -444,11 +466,15 @@ final class SalesBillMapper extends BillMapper SQL; $query = new Builder(self::$db); - $result = $query->raw($sql)->execute()->fetchAll(\PDO::FETCH_ASSOC); + $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 = <<raw($sql)->execute()->fetchAll(\PDO::FETCH_ASSOC); + $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); + $result = $query->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); + $result = $query->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 = <<raw($sql)->execute()->fetchAll(\PDO::FETCH_ASSOC); + $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); + $result = $query->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); + $result = $query->raw($sql)->execute()?->fetchAll(\PDO::FETCH_ASSOC) ?? []; return isset($result[0]['billing_bill_created_at']) ? new \DateTime(($result[0]['billing_bill_created_at'])) diff --git a/Models/Tax/TaxCombinationMapper.php b/Models/Tax/TaxCombinationMapper.php index 5239c37..b4ad10f 100755 --- a/Models/Tax/TaxCombinationMapper.php +++ b/Models/Tax/TaxCombinationMapper.php @@ -40,24 +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_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_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'], + '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'], ]; /** @@ -83,7 +83,6 @@ final class TaxCombinationMapper extends DataMapperFactory 'mapper' => TaxCodeMapper::class, 'external' => 'billing_tax_code', 'by' => 'abbr', - 'column' => 'abbr', ], ]; diff --git a/Theme/Backend/Lang/de.lang.php b/Theme/Backend/Lang/de.lang.php index 3fd4ab0..d1b05de 100755 --- a/Theme/Backend/Lang/de.lang.php +++ b/Theme/Backend/Lang/de.lang.php @@ -18,10 +18,10 @@ return ['Billing' => [ 'AlreadyPaid' => 'Bereits bezahlt', 'Amount' => 'Betrag', 'Archive' => 'Archiev', - 'Internal' => 'Intern', + 'Internal' => 'Intern', 'Error' => 'Fehler', 'Billing' => 'Rechnungsstellung', - 'External' => 'Extern', + 'External' => 'Extern', 'Bills' => 'Rechnungen', 'Bonus' => 'Bonus', 'Cashback' => 'Kennzeichnen', @@ -54,7 +54,7 @@ return ['Billing' => [ 'MoneyTransfer' => 'Überweisung', 'Name' => 'Name', 'Net' => 'Netz', - 'Parse' => 'Rechnungserkennung', + 'Parse' => 'Rechnungserkennung', 'Offer' => 'Angebot', 'Original' => 'Original', 'Payment' => 'Zahlung', diff --git a/Theme/Backend/Lang/en.lang.php b/Theme/Backend/Lang/en.lang.php index cf8f496..a7bbb44 100755 --- a/Theme/Backend/Lang/en.lang.php +++ b/Theme/Backend/Lang/en.lang.php @@ -18,10 +18,10 @@ return ['Billing' => [ 'AlreadyPaid' => 'Already Paid', 'Amount' => 'Amount', 'Archive' => 'Archive', - 'Internal' => 'Internal', - 'Error' => 'Error', + 'Internal' => 'Internal', + 'Error' => 'Error', 'Billing' => 'Billing', - 'External' => 'External', + 'External' => 'External', 'Bills' => 'Bills', 'Bonus' => 'Bonus', 'Cashback' => 'Cash Back', @@ -85,10 +85,9 @@ return ['Billing' => [ 'ShippingTerms' => 'Shipping Terms', 'PaymentTerm' => 'Payment Term', 'ShippingTerm' => 'Shipping Term', - 'ShippingTerm' => 'Shipping Term', 'E_bill_items' => 'There is an issue with your bill items.', 'E_bill_taxes' => 'The total tax amount doesn\'t match the tax amount of the elements. Potential issue with tax rates or additional items.', - 'E_bill_net' => 'The total net amount doesn\'t match the net amount of the elements. Potential issue with tax rates or additional items.', + 'E_bill_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.', + 'E_bill_unit' => 'Unit price doesn\'t match total price. Potential issues with price, quantity or discounts.', ]]; diff --git a/Theme/Backend/purchase-bill.tpl.php b/Theme/Backend/purchase-bill.tpl.php index 0d0b001..eae5982 100755 --- a/Theme/Backend/purchase-bill.tpl.php +++ b/Theme/Backend/purchase-bill.tpl.php @@ -441,7 +441,7 @@ echo $this->data['nav']->render(); ?> action="id . '&async=0'); ?>" method="post" data-redirect=""> - + diff --git a/tests/Models/InvoiceRecognitionTest.php b/tests/Models/InvoiceRecognitionTest.php index c593c45..9559fb0 100644 --- a/tests/Models/InvoiceRecognitionTest.php +++ b/tests/Models/InvoiceRecognitionTest.php @@ -336,7 +336,7 @@ final class InvoiceRecognitionTest extends \PHPUnit\Framework\TestCase $element = [ __DIR__ . '/bills/' . \implode('', $parts) . '.json', - $content + $content, ]; self::$billList[] = $element;