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

@ -211,7 +211,7 @@ foreach($bill->elements as $line) {
if ($line->quantity->value === 0) { 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); $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 { } 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[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); $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, "type": 1,
"item_code": "SOFTWARE", "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, "type": 1,
"item_code": "SOFTWARE", "item_code": "SOFTWARE",
@ -69,7 +272,29 @@
"type": 1, "type": 1,
"item_code": "SOFTWARE", "item_code": "SOFTWARE",
"account_code": "DE", "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, "type": 1,
@ -249,7 +474,29 @@
"type": 1, "type": 1,
"item_code": "SERVICE", "item_code": "SERVICE",
"account_code": "DE", "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, "type": 1,

View File

@ -1152,6 +1152,14 @@
"null": true, "null": true,
"default": null "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": { "billing_bill_element_tax_type": {
"name": "billing_bill_element_tax_type", "name": "billing_bill_element_tax_type",
"type": "VARCHAR(10)", "type": "VARCHAR(10)",

View File

@ -311,6 +311,7 @@ final class Installer extends InstallerAbstract
$request->setData('tax_code', $tax['tax_code']); $request->setData('tax_code', $tax['tax_code']);
$request->setData('item_code', $itemValue->id); $request->setData('item_code', $itemValue->id);
$request->setData('account_code', $accountValue->id); $request->setData('account_code', $accountValue->id);
$request->setData('account', $tax['account'] ?? null);
$module->apiTaxCombinationCreate($request, $response); $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() $bill = $data['bill'] ?? BillMapper::get()
->with('type') ->with('type')
@ -102,7 +112,7 @@ final class ApiBillController extends Controller
->where('id', $request->getDataInt('bill') ?? 0) ->where('id', $request->getDataInt('bill') ?? 0)
->execute(); ->execute();
$media = $data['media'] ?? $bill->getFileByTypeName('internal');; $media = $data['media'] ?? $bill->getFileByTypeName('internal');
if ($bill->status === BillStatus::ARCHIVED if ($bill->status === BillStatus::ARCHIVED
&& $bill->type->email && $bill->type->email
@ -134,7 +144,7 @@ final class ApiBillController extends Controller
if ($client->getAttribute('bill_emails')->value->getValue() === 1) { if ($client->getAttribute('bill_emails')->value->getValue() === 1) {
// @todo should this really be a string or an ID for a contact element? // @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 ? $client->account->email
: (string) $tmp; : (string) $tmp;
} }
@ -156,18 +166,34 @@ final class ApiBillController extends Controller
if ($supplier->getAttribute('bill_emails')->value->getValue() === 1) { if ($supplier->getAttribute('bill_emails')->value->getValue() === 1) {
// @todo should this really be a string or an ID for a contact element? // @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 ? $supplier->account->email
: (string) $tmp; : (string) $tmp;
} }
} }
if (!empty($email)) { if (!empty($email) && $billingTemplate !== null) {
$this->sendBillEmail($media, $email, (int) $billingTemplate->content, $bill->language); $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 public function apiBillFinalize(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
{ {
if (!$this->app->accountManager->get($request->header->account)->hasPermission( if (!$this->app->accountManager->get($request->header->account)->hasPermission(
@ -192,7 +218,7 @@ final class ApiBillController extends Controller
} }
// Archive bill // Archive bill
/** @var \Modules\Billing\Models\Bill $bill */ /** @var \Modules\Billing\Models\Bill $old */
$old = BillMapper::get() $old = BillMapper::get()
->with('type') ->with('type')
->where('id', $request->getDataInt('bill') ?? 0) ->where('id', $request->getDataInt('bill') ?? 0)
@ -218,7 +244,7 @@ final class ApiBillController extends Controller
]); ]);
// Send bill via email // Send bill via email
$this->apiBillEmail($request, $response, ['bill' => $new, 'media' => $media]); $this->apiBillEmail($request, ['bill' => $new, 'media' => $media]);
$this->createStandardUpdateResponse($request, $response, $new); $this->createStandardUpdateResponse($request, $response, $new);
} }
@ -440,6 +466,15 @@ final class ApiBillController extends Controller
return $bill; 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 private function findBillLanguage(Client|Supplier $account) : string
{ {
/** @var \Model\Setting $settings */ /** @var \Model\Setting $settings */
@ -516,10 +551,13 @@ final class ApiBillController extends Controller
$attr->type = $attrType; $attr->type = $attrType;
$attr->value = $attrValue; $attr->value = $attrValue;
$container = $request->hasData('container') ? new NullContainer($request->getDataInt('container')) : null; $container = $request->hasData('container')
? new NullContainer((int) $request->getData('container'))
: null;
$attr = new NullAttribute(); $attr = new NullAttribute();
if ($bill->type->transferType === BillTransferType::PURCHASE) { if ($bill->type->transferType === BillTransferType::PURCHASE && $bill->supplier !== null) {
$bill->supplier->attributes[] = $attr; $bill->supplier->attributes[] = $attr;
if ($container === null) { if ($container === null) {
@ -534,7 +572,7 @@ final class ApiBillController extends Controller
->execute(); ->execute();
} }
} }
} else { } elseif ($bill->client !== null) {
$bill->client->attributes[] = $attr; $bill->client->attributes[] = $attr;
if ($container === null) { if ($container === null) {
@ -552,7 +590,7 @@ final class ApiBillController extends Controller
} }
$container = $container === null && $attr->id !== 0 $container = $container === null && $attr->id !== 0
? new NullContainer($attr->value->getValue()) ? new NullContainer($attr->value->valueInt ?? 0)
: $container; : $container;
$taxCombination = $this->app->moduleManager->get('Billing', 'ApiTax') $taxCombination = $this->app->moduleManager->get('Billing', 'ApiTax')
@ -561,8 +599,8 @@ final class ApiBillController extends Controller
$element = BillElement::fromItem( $element = BillElement::fromItem(
$item, $item,
$taxCombination, $taxCombination,
FloatInt::toInt($request->getDataString('quantity') ?? '1'),
$bill, $bill,
FloatInt::toInt($request->getDataString('quantity') ?? '1'),
$container $container
); );
@ -1278,7 +1316,7 @@ final class ApiBillController extends Controller
->with('type') ->with('type')
->with('type/l11n') ->with('type/l11n')
->where('id', $request->getDataInt('bill') ?? 0) ->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(); ->execute();
// Handle PDF generation // Handle PDF generation
@ -1444,7 +1482,7 @@ final class ApiBillController extends Controller
$media->setPath(\Modules\Media\Controller\ApiController::normalizeDbPath($pdfDir . '/' . $billFileName)); $media->setPath(\Modules\Media\Controller\ApiController::normalizeDbPath($pdfDir . '/' . $billFileName));
$media->setVirtualPath($path); $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()); $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 Media $media Media to send
* @param string $email Email address * @param string $email Email address
* @param string $template Email template * @param int $template Email template
* @param string $language Message language * @param string $language Message language
* *
* @return void * @return void

View File

@ -46,7 +46,23 @@ use phpOMS\Stdlib\Base\FloatInt;
*/ */
final class ApiPriceController extends Controller 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(); $item ??= new NullItem();
$client ??= new NullClient(); $client ??= new NullClient();
@ -54,7 +70,7 @@ final class ApiPriceController extends Controller
// Get item // Get item
if ($item->id === 0 && $request->hasData('price_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() $item = ItemMapper::get()
->with('attributes') ->with('attributes')
->with('attributes/type') ->with('attributes/type')
@ -492,14 +508,16 @@ final class ApiPriceController extends Controller
$new->supplier = $request->hasData('supplier') ? new NullSupplier((int) $request->getData('supplier')) : $new->supplier; $new->supplier = $request->hasData('supplier') ? new NullSupplier((int) $request->getData('supplier')) : $new->supplier;
$new->unit = $request->getDataInt('unit') ?? $new->unit; $new->unit = $request->getDataInt('unit') ?? $new->unit;
$new->quantity = $request->getDataInt('quantity') ?? $new->quantity;
$new->price = $new->priceNew; $new->quantity = new FloatInt($request->getDataString('quantity') ?? $new->quantity->value);
$new->priceNew = $request->hasData('price_new') ? new FloatInt((int) $request->getData('price_new')) : $new->price = new FloatInt($request->getDataString('price') ?? $new->price->value);
$new->discount = $request->getDataInt('discount') ?? $new->discount; $new->priceNew = new FloatInt($request->getDataString('price_new') ?? $new->priceNew->value);
$new->discountPercentage = $request->getDataInt('discountPercentage') ?? $new->discountPercentage; $new->discount = new FloatInt($request->getDataString('discount') ?? $new->discount->value);
$new->bonus = $request->getDataInt('bonus') ?? $new->bonus; $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->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->start = $request->getDataDateTime('start') ?? $new->start;
$new->end = $request->getDataDateTime('end') ?? $new->end; $new->end = $request->getDataDateTime('end') ?? $new->end;

View File

@ -85,6 +85,8 @@ final class ApiPurchaseController extends Controller
* Method to create item attribute from request. * 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 * @return array
* *

View File

@ -16,11 +16,12 @@ namespace Modules\Billing\Controller;
use Modules\Attribute\Models\AttributeValue; use Modules\Attribute\Models\AttributeValue;
use Modules\Attribute\Models\NullAttributeValue; use Modules\Attribute\Models\NullAttributeValue;
use Modules\Billing\Models\Tax\NullTaxCombination;
use Modules\Billing\Models\Tax\TaxCombination; use Modules\Billing\Models\Tax\TaxCombination;
use Modules\Billing\Models\Tax\TaxCombinationMapper; use Modules\Billing\Models\Tax\TaxCombinationMapper;
use Modules\ClientManagement\Models\Attribute\ClientAttributeTypeMapper; use Modules\ClientManagement\Models\Attribute\ClientAttributeTypeMapper;
use Modules\ClientManagement\Models\Client; use Modules\ClientManagement\Models\Client;
use Modules\Finance\Models\TaxCode; use Modules\Finance\Models\NullTaxCode;
use Modules\Finance\Models\TaxCodeMapper; use Modules\Finance\Models\TaxCodeMapper;
use Modules\ItemManagement\Models\Item; use Modules\ItemManagement\Models\Item;
use Modules\Organization\Models\UnitMapper; use Modules\Organization\Models\UnitMapper;
@ -47,15 +48,25 @@ final class ApiTaxController extends Controller
* Get tax code from client and item. * Get tax code from client and item.
* *
* @param Item $item Item to get tax code from * @param Item $item Item to get tax code from
* @param Client $client Client to get tax code from * @param null|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 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 * @return TaxCombination
* *
* @since 1.0.0 * @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?! // @todo define default sales tax code if none available?!
$itemCode = 0; $itemCode = 0;
$accountCode = 0; $accountCode = 0;
@ -146,6 +157,7 @@ final class ApiTaxController extends Controller
$tax->taxType = $request->getDataInt('tax_type') ?? 1; $tax->taxType = $request->getDataInt('tax_type') ?? 1;
$tax->taxCode->abbr = (string) $request->getData('tax_code'); $tax->taxCode->abbr = (string) $request->getData('tax_code');
$tax->itemCode = new NullAttributeValue((int) $request->getData('item_code')); $tax->itemCode = new NullAttributeValue((int) $request->getData('item_code'));
$tax->account = $request->getDataString('account') ?? '';
if ($tax->taxType === 1) { if ($tax->taxType === 1) {
$tax->clientCode = new NullAttributeValue((int) $request->getData('account_code')); $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'))) { } elseif (\in_array($taxOfficeAddress->country, ISO3166TwoEnum::getRegion('eu'))) {
// None EU company but we are EU company // None EU company but we are EU company
return $codes->getDefaultByValue('INT'); return $codes->getDefaultByValue('INT');
} else { }
// None EU company and we are also none EU company // None EU company and we are also none EU company
return $codes->getDefaultByValue('INT'); return $codes->getDefaultByValue('INT');
} }
return $taxCode;
}
/** /**
* 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 * @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 * @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 */ /** @var \Modules\Attribute\Models\AttributeType $codes */
$codes = SupplierAttributeTypeMapper::get() $codes = SupplierAttributeTypeMapper::get()
@ -247,28 +257,26 @@ final class ApiTaxController extends Controller
// @todo need to consider own tax id as well // @todo need to consider own tax id as well
// @todo consider delivery & invoice location (Reihengeschaeft) // @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) // 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')) } 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 // Is EU company and we are EU company
return $codes->getDefaultByValue('EU'); return $codes->getDefaultByValue('EU');
} else { } else {
// Is EU private customer and we are EU company // 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'))) { } elseif (\in_array($taxOfficeAddress->country, ISO3166TwoEnum::getRegion('eu'))) {
// None EU company but we are EU company // None EU company but we are EU company
return $codes->getDefaultByValue('INT'); 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 public function updateTaxCombinationFromRequest(RequestAbstract $request, TaxCombination $new) : TaxCombination
{ {
$new->taxType = $request->getDataInt('tax_type') ?? $new->taxType; $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; $new->itemCode = $request->hasData('item_code') ? new NullAttributeValue((int) $request->getData('item_code')) : $new->itemCode;
if ($new->taxType === 1) { if ($new->taxType === 1) {

View File

@ -191,7 +191,7 @@ final class BackendController extends Controller
PermissionCategory::BILL_LOG, PermissionCategory::BILL_LOG,
) )
) { ) {
/** @var \Modules\Auditor\Models\Audit[] $logsBill */ /** @var \Modules\Auditor\Models\Audit[] $logs */
$logs = AuditMapper::getAll() $logs = AuditMapper::getAll()
->with('createdBy') ->with('createdBy')
->where('module', 'Billing') ->where('module', 'Billing')
@ -212,8 +212,6 @@ final class BackendController extends Controller
} }
} }
$view->data['logs'] = $logs; $view->data['logs'] = $logs;
$view->data['media-upload'] = new \Modules\Media\Theme\Backend\Components\Upload\BaseView($this->app->l11nManager, $request, $response); $view->data['media-upload'] = new \Modules\Media\Theme\Backend\Components\Upload\BaseView($this->app->l11nManager, $request, $response);

View File

@ -145,7 +145,7 @@ final class CliController extends Controller
if ($currency !== \in_array($currency, [ if ($currency !== \in_array($currency, [
$countryCurrency, ISO4217CharEnum::_USD, ISO4217CharEnum::_EUR, ISO4217CharEnum::_JPY, $countryCurrency, ISO4217CharEnum::_USD, ISO4217CharEnum::_EUR, ISO4217CharEnum::_JPY,
ISO4217CharEnum::_GBP, ISO4217CharEnum::_AUD, ISO4217CharEnum::_CAD, ISO4217CharEnum::_CHF, ISO4217CharEnum::_GBP, ISO4217CharEnum::_AUD, ISO4217CharEnum::_CAD, ISO4217CharEnum::_CHF,
ISO4217CharEnum::_CNH, ISO4217CharEnum::_CNY ISO4217CharEnum::_CNH, ISO4217CharEnum::_CNY,
]) ])
) { ) {
$currency = $countryCurrency; $currency = $countryCurrency;
@ -156,7 +156,7 @@ final class CliController extends Controller
$rd = -FloatInt::MAX_DECIMALS + ISO4217DecimalEnum::getByName('_' . $bill->currency); $rd = -FloatInt::MAX_DECIMALS + ISO4217DecimalEnum::getByName('_' . $bill->currency);
/* Type */ /* Type */
$type = $this->findSupplierInvoiceType($content, $identifiers['type'], $language); $type = InvoiceRecognition::findSupplierInvoiceType($content, $identifiers['type'], $language);
/** @var \Modules\Billing\Models\BillType $billType */ /** @var \Modules\Billing\Models\BillType $billType */
$billType = BillTypeMapper::get() $billType = BillTypeMapper::get()
@ -442,37 +442,6 @@ final class CliController extends Controller
return $view; 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 * Find possible supplier id
* *

View File

@ -411,6 +411,8 @@ class Bill implements \JsonSerializable
public ?string $fiAccount = null; public ?string $fiAccount = null;
// @todo Implement reason for bill (especially useful for credit notes, warehouse bookings)
/** /**
* Constructor. * Constructor.
* *
@ -530,7 +532,15 @@ class Bill implements \JsonSerializable
$this->netDiscount->value += $element->totalDiscountP->value; $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 public function isValid() : bool
{ {
return $this->validateTaxAmountElements() return $this->validateTaxAmountElements()
@ -542,6 +552,13 @@ class Bill implements \JsonSerializable
&& $this->areElementsValid(); && $this->areElementsValid();
} }
/**
* Validate the correctness of the bill elements
*
* @return bool
*
* @since 1.0.0
*/
public function areElementsValid() : bool public function areElementsValid() : bool
{ {
foreach ($this->elements as $element) { foreach ($this->elements as $element) {
@ -553,21 +570,49 @@ class Bill implements \JsonSerializable
return true; return true;
} }
/**
* Validate the correctness of the net and gross values
*
* @return bool
*
* @since 1.0.0
*/
public function validateNetGross() : bool public function validateNetGross() : bool
{ {
return $this->netSales->value <= $this->grossSales->value; return $this->netSales->value <= $this->grossSales->value;
} }
/**
* Validate the correctness of the profit
*
* @return bool
*
* @since 1.0.0
*/
public function validateProfit() : bool public function validateProfit() : bool
{ {
return $this->netSales->value - $this->netCosts->value === $this->netProfit->value; 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 public function validateTax() : bool
{ {
return \abs($this->netSales->value + $this->taxP->value - $this->grossSales->value) === 0; 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 public function validateTaxAmountElements() : bool
{ {
$taxes = 0; $taxes = 0;
@ -578,6 +623,13 @@ class Bill implements \JsonSerializable
return $taxes === $this->taxP->value; return $taxes === $this->taxP->value;
} }
/**
* Validate the correctness of the net of the elements
*
* @return bool
*
* @since 1.0.0
*/
public function validateNetElements() : bool public function validateNetElements() : bool
{ {
$net = 0; $net = 0;
@ -588,6 +640,13 @@ class Bill implements \JsonSerializable
return $net === $this->netSales->value; return $net === $this->netSales->value;
} }
/**
* Validate the correctness of the gross of the elements
*
* @return bool
*
* @since 1.0.0
*/
public function validateGrossElements() public function validateGrossElements()
{ {
$gross = 0; $gross = 0;
@ -598,6 +657,13 @@ class Bill implements \JsonSerializable
return $gross === $this->grossSales->value; return $gross === $this->grossSales->value;
} }
/**
* Validate the correctness of the quantities and total price
*
* @return bool
*
* @since 1.0.0
*/
public function validatePriceQuantityElements() public function validatePriceQuantityElements()
{ {
foreach ($this->elements as $element) { foreach ($this->elements as $element) {

View File

@ -123,6 +123,8 @@ class BillElement implements \JsonSerializable
public ?string $costobject = null; public ?string $costobject = null;
public ?TaxCombination $taxCombination = null;
/** /**
* Tax amount * Tax amount
* *
@ -213,15 +215,24 @@ class BillElement implements \JsonSerializable
*/ */
public function setQuantity(int $quantity) : void public function setQuantity(int $quantity) : void
{ {
if ($this->quantity === $quantity) { if ($this->quantity->value === $quantity) {
return; return;
} }
$this->quantity = $quantity; $this->quantity->value = $quantity;
$this->recalculatePrices(); $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 public function recalculatePrices() : void
{ {
$rd = -FloatInt::MAX_DECIMALS + ISO4217DecimalEnum::getByName('_' . $this->bill->currency); $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); $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 public function isValid() : bool
{ {
return $this->validateNetGross() return $this->validateNetGross()
@ -261,6 +280,13 @@ class BillElement implements \JsonSerializable
&& $this->validateTotalPrice(); && $this->validateTotalPrice();
} }
/**
* Validate the correctness of the net and gross values
*
* @return bool
*
* @since 1.0.0
*/
public function validateNetGross() : bool public function validateNetGross() : bool
{ {
return $this->singleListPriceNet->value <= $this->singleListPriceGross->value return $this->singleListPriceNet->value <= $this->singleListPriceGross->value
@ -269,21 +295,42 @@ class BillElement implements \JsonSerializable
&& $this->totalSalesPriceNet->value <= $this->totalSalesPriceGross->value; && $this->totalSalesPriceNet->value <= $this->totalSalesPriceGross->value;
} }
/**
* Validate the correctness of the profit
*
* @return bool
*
* @since 1.0.0
*/
public function validateProfit() : bool public function validateProfit() : bool
{ {
return $this->totalSalesPriceNet->value - $this->totalPurchasePriceNet->value === $this->totalProfitNet->value; 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 public function validateTax() : bool
{ {
$paidQuantity = $this->quantity->value - $this->discountQ->value; $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 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->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; && \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 public function validateTaxRate() : bool
{ {
return (($this->taxP->value === 0 && $this->taxR->value === 0) 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); && \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 public function validateSingleTotal() : bool
{ {
$paidQuantity = $this->quantity->value - $this->discountQ->value; $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; && ((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 public function validateEffectiveSinglePrice() : bool
{ {
return $this->effectiveSingleSalesPriceNet->value === (int) \round($this->totalSalesPriceNet->value / ($this->quantity->value / FloatInt::DIVISOR)); 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 public function validateTotalPrice() : bool
{ {
return ((int) \round($this->singleListPriceNet->value * ($this->quantity->value / FloatInt::DIVISOR) return ((int) \round($this->singleListPriceNet->value * ($this->quantity->value / FloatInt::DIVISOR)
@ -333,9 +401,10 @@ class BillElement implements \JsonSerializable
* Create element from item * Create element from item
* *
* @param Item $item Item * @param Item $item Item
* @param TaxCode $taxCode Tax code used for gross amount calculation * @param TaxCombination $taxCombination Tax combination
* @param int $quantity Quantity
* @param Bill $bill Bill * @param Bill $bill Bill
* @param int $quantity Quantity (1.0 = 10000)
* @param null|Container $container Item container
* *
* @return self * @return self
* *
@ -344,8 +413,8 @@ class BillElement implements \JsonSerializable
public static function fromItem( public static function fromItem(
Item $item, Item $item,
TaxCombination $taxCombination, TaxCombination $taxCombination,
Bill $bill,
int $quantity = FloatInt::DIVISOR, int $quantity = FloatInt::DIVISOR,
Bill $bill = null,
?Container $container = null ?Container $container = null
) : self ) : self
{ {
@ -361,6 +430,7 @@ class BillElement implements \JsonSerializable
$element->taxR = new FloatInt($taxCombination->taxCode->percentageInvoice); $element->taxR = new FloatInt($taxCombination->taxCode->percentageInvoice);
$element->taxCode = $taxCombination->taxCode->abbr; $element->taxCode = $taxCombination->taxCode->abbr;
$element->fiAccount = $taxCombination->account; $element->fiAccount = $taxCombination->account;
$element->taxCombination = $taxCombination;
// @todo the purchase price is based on lot/sn/avg prices if available // @todo the purchase price is based on lot/sn/avg prices if available
$element->singlePurchasePriceNet->value = $item->purchasePrice->value; $element->singlePurchasePriceNet->value = $item->purchasePrice->value;
@ -391,7 +461,7 @@ class BillElement implements \JsonSerializable
return [ return [
'id' => $this->id, 'id' => $this->id,
'order' => $this->order, 'order' => $this->order,
'item' => $this->item->id, 'item' => $this->item?->id,
'itemNumber' => $this->itemNumber, 'itemNumber' => $this->itemNumber,
'itemName' => $this->itemName, 'itemName' => $this->itemName,
'itemDescription' => $this->itemDescription, 'itemDescription' => $this->itemDescription,

View File

@ -14,6 +14,7 @@ declare(strict_types=1);
namespace Modules\Billing\Models; namespace Modules\Billing\Models;
use Modules\Billing\Models\Tax\TaxCombinationMapper;
use Modules\ItemManagement\Models\ContainerMapper; use Modules\ItemManagement\Models\ContainerMapper;
use Modules\ItemManagement\Models\ItemMapper; use Modules\ItemManagement\Models\ItemMapper;
use phpOMS\DataStorage\Database\Mapper\DataMapperFactory; use phpOMS\DataStorage\Database\Mapper\DataMapperFactory;
@ -65,6 +66,7 @@ final class BillElementMapper extends DataMapperFactory
'billing_bill_element_total_netpurchaseprice' => ['name' => 'billing_bill_element_total_netpurchaseprice', 'type' => 'Serializable', 'internal' => 'totalPurchasePriceNet'], '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_bill' => ['name' => 'billing_bill_element_bill', 'type' => 'int', 'internal' => 'bill'],
'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_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_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_percentage' => ['name' => 'billing_bill_element_tax_percentage', 'type' => 'Serializable', 'internal' => 'taxR'],
@ -112,6 +114,10 @@ final class BillElementMapper extends DataMapperFactory
'mapper' => ContainerMapper::class, 'mapper' => ContainerMapper::class,
'external' => 'billing_bill_element_container', 'external' => 'billing_bill_element_container',
], ],
'taxCombination' => [
'mapper' => TaxCombinationMapper::class,
'external' => 'billing_bill_element_tax_combination',
],
]; ];
/** /**

View File

@ -35,9 +35,19 @@ use phpOMS\Validation\Finance\IbanEnum;
*/ */
class InvoiceRecognition 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); $lines = \explode("\n", $content);
foreach ($lines as $line => $value) { foreach ($lines as $line => $value) {
if (empty(\trim($value))) { if (empty(\trim($value))) {
@ -64,7 +74,7 @@ class InvoiceRecognition
/** @var array $identifiers */ /** @var array $identifiers */
$identifiers = \json_decode($identifierContent, true); $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); $countryCurrency = ISO4217CharEnum::currencyFromCountry($bill->billCountry);
@ -73,7 +83,7 @@ class InvoiceRecognition
if ($currency !== \in_array($currency, [ if ($currency !== \in_array($currency, [
$countryCurrency, ISO4217CharEnum::_USD, ISO4217CharEnum::_EUR, ISO4217CharEnum::_JPY, $countryCurrency, ISO4217CharEnum::_USD, ISO4217CharEnum::_EUR, ISO4217CharEnum::_JPY,
ISO4217CharEnum::_GBP, ISO4217CharEnum::_AUD, ISO4217CharEnum::_CAD, ISO4217CharEnum::_CHF, ISO4217CharEnum::_GBP, ISO4217CharEnum::_AUD, ISO4217CharEnum::_CAD, ISO4217CharEnum::_CHF,
ISO4217CharEnum::_CNH, ISO4217CharEnum::_CNY ISO4217CharEnum::_CNH, ISO4217CharEnum::_CNY,
]) ])
) { ) {
$currency = $countryCurrency; $currency = $countryCurrency;
@ -86,8 +96,8 @@ class InvoiceRecognition
/* Type */ /* Type */
$type = self::findSupplierInvoiceType($content, $identifiers['type'], $language); $type = self::findSupplierInvoiceType($content, $identifiers['type'], $language);
/** @var \Modules\Billing\Models\BillType $billType */
/* /*
@var \Modules\Billing\Models\BillType $billType
$billType = BillTypeMapper::get() $billType = BillTypeMapper::get()
->where('name', $type) ->where('name', $type)
->execute(); ->execute();
@ -275,7 +285,6 @@ class InvoiceRecognition
$totalGross += $element->totalSalesPriceGross->value; $totalGross += $element->totalSalesPriceGross->value;
} }
$bill->grossSales = new FloatInt($totalGross); $bill->grossSales = new FloatInt($totalGross);
$bill->netCosts = new FloatInt($totalNet); $bill->netCosts = new FloatInt($totalNet);
$bill->netSales = $bill->netCosts; $bill->netSales = $bill->netCosts;
@ -572,7 +581,7 @@ class InvoiceRecognition
* @param string[] $lines Bill lines * @param string[] $lines Bill lines
* @param array $matches Net match patterns * @param array $matches Net match patterns
* *
* @return int * @return string
* *
* @bug Issue with net/discount/gross in one line * @bug Issue with net/discount/gross in one line
* *
@ -680,8 +689,6 @@ class InvoiceRecognition
$bestDiscount = 0; $bestDiscount = 0;
$found = []; $found = [];
$discountLine = 0;
foreach ($matches['total_discount'][$language] as $match) { foreach ($matches['total_discount'][$language] as $match) {
foreach ($lines as $idx => $line) { foreach ($lines as $idx => $line) {
if ($idx < $lineStart) { if ($idx < $lineStart) {
@ -822,8 +829,6 @@ class InvoiceRecognition
$bestSurcharge = 0; $bestSurcharge = 0;
$found = []; $found = [];
$surchargeLine = 0;
foreach ($matches['total_surcharge'][$language] as $match) { foreach ($matches['total_surcharge'][$language] as $match) {
foreach ($lines as $idx => $line) { foreach ($lines as $idx => $line) {
if ($idx < $lineStart) { if ($idx < $lineStart) {
@ -849,7 +854,6 @@ class InvoiceRecognition
if ($surcharge > $bestSurcharge) { if ($surcharge > $bestSurcharge) {
$bestSurcharge = $surcharge; $bestSurcharge = $surcharge;
$surchargeLine = $idx;
break; break;
} }
@ -981,7 +985,7 @@ class InvoiceRecognition
{ {
if ((!empty($supplierFormat))) { if ((!empty($supplierFormat))) {
$dt = \DateTime::createFromFormat( $dt = \DateTime::createFromFormat(
$supplierFormat ?? '', $supplierFormat,
$date $date
); );
@ -1221,7 +1225,8 @@ class InvoiceRecognition
if (\stripos($bestMatch, 'S') > 1 if (\stripos($bestMatch, 'S') > 1
|| \stripos($bestMatch, 'O') > 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); $formatLen = \strlen($format);
@ -1245,12 +1250,18 @@ class InvoiceRecognition
$bestMatch[$i] = '5'; $bestMatch[$i] = '5';
} }
} }
} }
return \trim($bestMatch); 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 public static function findCountry(array $lines, array $matches, string $language) : string
{ {
$iban = self::findIban($lines, $matches['iban']); $iban = self::findIban($lines, $matches['iban']);
@ -1286,6 +1297,15 @@ class InvoiceRecognition
return empty($countries) ? 'US' : \reset($countries); 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 public static function findCurrency(array $lines) : string
{ {
$symbols = ISO4217SymbolEnum::getConstants(); $symbols = ISO4217SymbolEnum::getConstants();
@ -1299,8 +1319,11 @@ class InvoiceRecognition
} }
if (\strpos($line, $match) !== false) { if (\strpos($line, $match) !== false) {
/** @var string $currency */
$currency = ISO4217SymbolEnum::getName($symbol); $currency = ISO4217SymbolEnum::getName($symbol);
$currency = ISO4217CharEnum::getByName($currency);
/** @var string $currency */
$currency = ISO4217CharEnum::getByName($currency) ?? '';
break; break;
} }

View File

@ -39,6 +39,7 @@ final class SalesBillMapper extends BillMapper
/** /**
* Placeholder * Placeholder
* @todo Implement
*/ */
public static function getSalesBeforePivot( public static function getSalesBeforePivot(
mixed $pivot, mixed $pivot,
@ -58,6 +59,7 @@ final class SalesBillMapper extends BillMapper
/** /**
* Placeholder * Placeholder
* @todo Implement
*/ */
public static function getSalesAfterPivot( public static function getSalesAfterPivot(
mixed $pivot, mixed $pivot,
@ -77,6 +79,7 @@ final class SalesBillMapper extends BillMapper
/** /**
* Placeholder * Placeholder
* @todo Implement
*/ */
public static function getSalesByItemId(int $id, \DateTime $start, \DateTime $end) : FloatInt public static function getSalesByItemId(int $id, \DateTime $start, \DateTime $end) : FloatInt
{ {
@ -98,6 +101,7 @@ final class SalesBillMapper extends BillMapper
/** /**
* Placeholder * Placeholder
* @todo Implement
*/ */
public static function getSalesByClientId(int $id, \DateTime $start, \DateTime $end) : FloatInt public static function getSalesByClientId(int $id, \DateTime $start, \DateTime $end) : FloatInt
{ {
@ -117,6 +121,7 @@ final class SalesBillMapper extends BillMapper
/** /**
* Placeholder * Placeholder
* @todo Implement
*/ */
public static function getItemAvgSalesPrice(int $item, \DateTime $start, \DateTime $end) : FloatInt public static function getItemAvgSalesPrice(int $item, \DateTime $start, \DateTime $end) : FloatInt
{ {
@ -133,7 +138,7 @@ final class SalesBillMapper extends BillMapper
SQL; SQL;
$query = new Builder(self::$db); $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']) return isset($result[0]['net_count'])
? new FloatInt((int) ($result[0]['net_sales'] ?? 0) / ($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 * Placeholder
* @todo Implement
*/ */
public static function getLastOrderDateByItemId(int $id) : ?\DateTimeImmutable public static function getLastOrderDateByItemId(int $id) : ?\DateTimeImmutable
{ {
@ -164,6 +170,7 @@ final class SalesBillMapper extends BillMapper
/** /**
* Placeholder * Placeholder
* @todo Implement
*/ */
public static function getLastOrderDateByClientId(int $id) : ?\DateTimeImmutable public static function getLastOrderDateByClientId(int $id) : ?\DateTimeImmutable
{ {
@ -184,6 +191,7 @@ final class SalesBillMapper extends BillMapper
/** /**
* Placeholder * Placeholder
* @todo Implement
*/ */
public static function getItemRetentionRate(int $id, \DateTime $start, \DateTime $end) : float public static function getItemRetentionRate(int $id, \DateTime $start, \DateTime $end) : float
{ {
@ -192,6 +200,7 @@ final class SalesBillMapper extends BillMapper
/** /**
* Placeholder * Placeholder
* @todo Implement
*/ */
public static function getItemLivetimeValue(int $id, \DateTime $start, \DateTime $end) : FloatInt public static function getItemLivetimeValue(int $id, \DateTime $start, \DateTime $end) : FloatInt
{ {
@ -200,6 +209,7 @@ final class SalesBillMapper extends BillMapper
/** /**
* Placeholder * Placeholder
* @todo Implement
*/ */
public static function getNewestItemInvoices(int $id, int $limit = 10) : array public static function getNewestItemInvoices(int $id, int $limit = 10) : array
{ {
@ -221,6 +231,7 @@ final class SalesBillMapper extends BillMapper
/** /**
* Placeholder * Placeholder
* @todo Implement
*/ */
public static function getNewestClientInvoices(int $id, int $limit = 10) : array public static function getNewestClientInvoices(int $id, int $limit = 10) : array
{ {
@ -240,6 +251,7 @@ final class SalesBillMapper extends BillMapper
/** /**
* Placeholder * Placeholder
* @todo Implement
*/ */
public static function getItemTopClients(int $id, \DateTime $start, \DateTime $end, int $limit = 10) : array public static function getItemTopClients(int $id, \DateTime $start, \DateTime $end, int $limit = 10) : array
{ {
@ -280,6 +292,7 @@ final class SalesBillMapper extends BillMapper
/** /**
* Placeholder * Placeholder
* @todo Implement
*/ */
public static function getItemBills(int $id, \DateTime $start, \DateTime $end) : array public static function getItemBills(int $id, \DateTime $start, \DateTime $end) : array
{ {
@ -308,6 +321,7 @@ final class SalesBillMapper extends BillMapper
/** /**
* Placeholder * Placeholder
* @todo Implement
*/ */
public static function getClientBills(int $id, string $language, \DateTime $start, \DateTime $end) : array public static function getClientBills(int $id, string $language, \DateTime $start, \DateTime $end) : array
{ {
@ -323,6 +337,7 @@ final class SalesBillMapper extends BillMapper
/** /**
* Placeholder * Placeholder
* @todo Implement
*/ */
public static function getClientItem(int $client, \DateTime $start, \DateTime $end) : array public static function getClientItem(int $client, \DateTime $start, \DateTime $end) : array
{ {
@ -336,6 +351,7 @@ final class SalesBillMapper extends BillMapper
/** /**
* Placeholder * Placeholder
* @todo Implement
*/ */
public static function getItemCountrySales(int $id, \DateTime $start, \DateTime $end, int $limit = 10) : array public static function getItemCountrySales(int $id, \DateTime $start, \DateTime $end, int $limit = 10) : array
{ {
@ -361,6 +377,7 @@ final class SalesBillMapper extends BillMapper
/** /**
* Placeholder * Placeholder
* @todo Implement
*/ */
public static function getItemMonthlySalesCosts(array $items, \DateTime $start, \DateTime $end) : array public static function getItemMonthlySalesCosts(array $items, \DateTime $start, \DateTime $end) : array
{ {
@ -388,11 +405,15 @@ final class SalesBillMapper extends BillMapper
SQL; SQL;
$query = new Builder(self::$db); $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 ?? []; return $result ?? [];
} }
/**
* Placeholder
* @todo Implement
*/
public static function getItemMonthlySalesQuantity(array $items, \DateTime $start, \DateTime $end) : array public static function getItemMonthlySalesQuantity(array $items, \DateTime $start, \DateTime $end) : array
{ {
if (empty($items)) { if (empty($items)) {
@ -418,13 +439,14 @@ final class SalesBillMapper extends BillMapper
SQL; SQL;
$query = new Builder(self::$db); $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 ?? []; return $result ?? [];
} }
/** /**
* Placeholder * Placeholder
* @todo Implement
*/ */
public static function getClientMonthlySalesCosts(int $client, \DateTime $start, \DateTime $end) : array public static function getClientMonthlySalesCosts(int $client, \DateTime $start, \DateTime $end) : array
{ {
@ -444,11 +466,15 @@ final class SalesBillMapper extends BillMapper
SQL; SQL;
$query = new Builder(self::$db); $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 ?? []; return $result ?? [];
} }
/**
* Placeholder
* @todo Implement
*/
public static function getItemNetSales(int $item, \DateTime $start, \DateTime $end) : FloatInt public static function getItemNetSales(int $item, \DateTime $start, \DateTime $end) : FloatInt
{ {
$sql = <<<SQL $sql = <<<SQL
@ -462,11 +488,15 @@ final class SalesBillMapper extends BillMapper
SQL; SQL;
$query = new Builder(self::$db); $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)); return new FloatInt((int) ($result[0]['net_sales'] ?? 0));
} }
/**
* Placeholder
* @todo Implement
*/
public static function getILVHistoric(int $item) : FloatInt public static function getILVHistoric(int $item) : FloatInt
{ {
$sql = <<<SQL $sql = <<<SQL
@ -477,16 +507,24 @@ final class SalesBillMapper extends BillMapper
SQL; SQL;
$query = new Builder(self::$db); $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)); return new FloatInt((int) ($result[0]['net_sales'] ?? 0));
} }
/**
* Placeholder
* @todo Implement
*/
public static function getItemMRR() : FloatInt public static function getItemMRR() : FloatInt
{ {
return new FloatInt(0); return new FloatInt(0);
} }
/**
* Placeholder
* @todo Implement
*/
public static function getItemLastOrder(int $item) : ?\DateTime public static function getItemLastOrder(int $item) : ?\DateTime
{ {
$sql = <<<SQL $sql = <<<SQL
@ -499,13 +537,17 @@ final class SalesBillMapper extends BillMapper
SQL; SQL;
$query = new Builder(self::$db); $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']) return isset($result[0]['billing_bill_created_at'])
? new \DateTime(($result[0]['billing_bill_created_at'])) ? new \DateTime(($result[0]['billing_bill_created_at']))
: null; : null;
} }
/**
* Placeholder
* @todo Implement
*/
public static function getClientNetSales(int $client, \DateTime $start, \DateTime $end) : FloatInt public static function getClientNetSales(int $client, \DateTime $start, \DateTime $end) : FloatInt
{ {
$sql = <<<SQL $sql = <<<SQL
@ -518,11 +560,15 @@ final class SalesBillMapper extends BillMapper
SQL; SQL;
$query = new Builder(self::$db); $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)); return new FloatInt((int) ($result[0]['net_sales'] ?? 0));
} }
/**
* Placeholder
* @todo Implement
*/
public static function getCLVHistoric(int $client) : FloatInt public static function getCLVHistoric(int $client) : FloatInt
{ {
$sql = <<<SQL $sql = <<<SQL
@ -532,16 +578,24 @@ final class SalesBillMapper extends BillMapper
SQL; SQL;
$query = new Builder(self::$db); $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)); return new FloatInt((int) ($result[0]['net_sales'] ?? 0));
} }
/**
* Placeholder
* @todo Implement
*/
public static function getClientMRR() : FloatInt public static function getClientMRR() : FloatInt
{ {
return new FloatInt(0); return new FloatInt(0);
} }
/**
* Placeholder
* @todo Implement
*/
public static function getClientLastOrder(int $client) : ?\DateTime public static function getClientLastOrder(int $client) : ?\DateTime
{ {
$sql = <<<SQL $sql = <<<SQL
@ -553,7 +607,7 @@ final class SalesBillMapper extends BillMapper
SQL; SQL;
$query = new Builder(self::$db); $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']) return isset($result[0]['billing_bill_created_at'])
? new \DateTime(($result[0]['billing_bill_created_at'])) ? new \DateTime(($result[0]['billing_bill_created_at']))

View File

@ -83,7 +83,6 @@ final class TaxCombinationMapper extends DataMapperFactory
'mapper' => TaxCodeMapper::class, 'mapper' => TaxCodeMapper::class,
'external' => 'billing_tax_code', 'external' => 'billing_tax_code',
'by' => 'abbr', 'by' => 'abbr',
'column' => 'abbr',
], ],
]; ];

View File

@ -85,7 +85,6 @@ return ['Billing' => [
'ShippingTerms' => 'Shipping Terms', 'ShippingTerms' => 'Shipping Terms',
'PaymentTerm' => 'Payment Term', 'PaymentTerm' => 'Payment Term',
'ShippingTerm' => 'Shipping Term', 'ShippingTerm' => 'Shipping Term',
'ShippingTerm' => 'Shipping Term',
'E_bill_items' => 'There is an issue with your bill items.', '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_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.',

View File

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

View File

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