code fixes

This commit is contained in:
Dennis Eichhorn 2024-03-15 20:24:37 +00:00
parent 3cd6a79222
commit e70898dbd5
24 changed files with 967 additions and 281 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = <<<SQL
@ -462,11 +488,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 new FloatInt((int) ($result[0]['net_sales'] ?? 0));
}
/**
* Placeholder
* @todo Implement
*/
public static function getILVHistoric(int $item) : FloatInt
{
$sql = <<<SQL
@ -477,16 +507,24 @@ 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 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 = <<<SQL
@ -499,13 +537,17 @@ 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]['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 = <<<SQL
@ -518,11 +560,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 new FloatInt((int) ($result[0]['net_sales'] ?? 0));
}
/**
* Placeholder
* @todo Implement
*/
public static function getCLVHistoric(int $client) : FloatInt
{
$sql = <<<SQL
@ -532,16 +578,24 @@ 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 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 = <<<SQL
@ -553,7 +607,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]['billing_bill_created_at'])
? new \DateTime(($result[0]['billing_bill_created_at']))

View File

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

View File

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

View File

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

View File

@ -441,7 +441,7 @@ echo $this->data['nav']->render(); ?>
action="<?= UriFactory::build('{/api}bill/parse?id=' . $bill->id . '&async=0'); ?>"
method="post"
data-redirect="<?= UriFactory::build('{%}'); ?>">
<input type="submit" value="<?= $this->getHtml('Parse') ?>">
<input type="submit" value="<?= $this->getHtml('Parse'); ?>">
</form>
</div>
</div>

View File

@ -336,7 +336,7 @@ final class InvoiceRecognitionTest extends \PHPUnit\Framework\TestCase
$element = [
__DIR__ . '/bills/' . \implode('', $parts) . '.json',
$content
$content,
];
self::$billList[] = $element;