Initial xentral_oss_20.3.c9ffacf

This commit is contained in:
Alex
2021-05-21 08:49:41 +02:00
parent 5406e4a551
commit 34e5ac43d9
18884 changed files with 2109867 additions and 0 deletions
@@ -0,0 +1,126 @@
<?php
namespace Xentral\Modules\CopperSurcharge;
use ApplicationCore;
use Xentral\Core\DependencyInjection\ContainerInterface;
use Xentral\Modules\CopperSurcharge\Service\DocumentGateway;
use Xentral\Modules\CopperSurcharge\Service\PurchasePriceGateway;
use Xentral\Modules\CopperSurcharge\Service\DocumentService;
use Xentral\Modules\CopperSurcharge\Service\RawMaterialGateway;
use Xentral\Modules\CopperSurcharge\Wrapper\CompanyDataWrapper;
use Xentral\Modules\CopperSurcharge\Wrapper\DocumentPositionWrapper;
final class Bootstrap
{
/**
* @return array
*/
public static function registerServices(): array
{
return [
'CopperSurchargeCalculatorFactory' => 'onInitCopperSurchargeCalculatorFactory',
'CopperSurchargeService' => 'onInitCopperSurchargeService',
];
}
/**
* @param ContainerInterface $container
*
* @return CopperSurchargeCalculatorFactory
*/
public static function onInitCopperSurchargeCalculatorFactory(ContainerInterface $container
): CopperSurchargeCalculatorFactory {
return new CopperSurchargeCalculatorFactory(
self::onInitPurchasePriceGateway($container),
self::onInitRawMaterialGateway($container),
self::onInitDocumentPositionWrapper($container),
self::onInitDocumentService($container),
self::onInitDocumentGateway($container)
);
}
/**
* @param ContainerInterface $container
*
* @return CopperSurchargeService
*/
public static function onInitCopperSurchargeService(ContainerInterface $container
): CopperSurchargeService {
return new CopperSurchargeService(
$container->get('SystemConfigModule'),
self::onInitCompanyDataWrapper($container)
);
}
/**
* @param ContainerInterface $container
*
* @return PurchasePriceGateway
*/
private static function onInitPurchasePriceGateway(ContainerInterface $container
): PurchasePriceGateway {
return new PurchasePriceGateway(
$container->get('Database')
);
}
/**
* @param ContainerInterface $container
*
* @return RawMaterialGateway
*/
private static function onInitRawMaterialGateway(ContainerInterface $container
): RawMaterialGateway {
return new RawMaterialGateway(
$container->get('Database')
);
}
/**
* @param ContainerInterface $container
*
* @return DocumentPositionWrapper
*/
private static function onInitDocumentPositionWrapper(ContainerInterface $container
): DocumentPositionWrapper {
/** @var ApplicationCore $app */
$app = $container->get('LegacyApplication');
return new DocumentPositionWrapper($app->erp, $container->get('Database'));
}
/**
* @param ContainerInterface $container
*
* @return CompanyDataWrapper
*/
private static function onInitCompanyDataWrapper(ContainerInterface $container
): CompanyDataWrapper {
/** @var ApplicationCore $app */
$app = $container->get('LegacyApplication');
return new CompanyDataWrapper($app->erp);
}
/**
* @param ContainerInterface $container
*
* @return DocumentService
*/
private static function onInitDocumentService(ContainerInterface $container): DocumentService
{
return new DocumentService($container->get('Database'));
}
/**
* @param ContainerInterface $container
*
* @return DocumentGateway
*/
private static function onInitDocumentGateway(ContainerInterface $container): DocumentGateway
{
return new DocumentGateway($container->get('Database'));
}
}
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Xentral\Modules\CopperSurcharge;
use Xentral\Modules\CopperSurcharge\Data\CopperSurchargeData;
use Xentral\Modules\CopperSurcharge\Service\CopperSurchargeCalculator;
use Xentral\Modules\CopperSurcharge\Service\DocumentGateway;
use Xentral\Modules\CopperSurcharge\Service\PurchasePriceGateway;
use Xentral\Modules\CopperSurcharge\Service\DocumentService;
use Xentral\Modules\CopperSurcharge\Service\RawMaterialGateway;
use Xentral\Modules\CopperSurcharge\Wrapper\DocumentPositionWrapper;
use Xentral\Modules\CopperSurcharge\Wrapper\DocumentPositionWrapperInterface;
final class CopperSurchargeCalculatorFactory
{
/** @var RawMaterialGateway $rawMaterialGateway */
private $rawMaterialGateway;
/** @var PurchasePriceGateway $purchasePriceGateway */
private $purchasePriceGateway;
/** @var DocumentPositionWrapper $documentPositionWrapper */
private $documentPositionWrapper;
/** @var DocumentService $documentPositionService */
private $documentService;
/** @var DocumentGateway $documentGateway */
private $documentGateway;
/**
* @param PurchasePriceGateway $purchasePriceGateway
* @param RawMaterialGateway $rawMaterialGateway
* @param DocumentPositionWrapperInterface $documentPositionWrapper
* @param DocumentService $documentService
* @param DocumentGateway $documentGateway
*/
public function __construct(
PurchasePriceGateway $purchasePriceGateway,
RawMaterialGateway $rawMaterialGateway,
DocumentPositionWrapperInterface $documentPositionWrapper,
DocumentService $documentService,
DocumentGateway $documentGateway
) {
$this->purchasePriceGateway = $purchasePriceGateway;
$this->rawMaterialGateway = $rawMaterialGateway;
$this->documentPositionWrapper = $documentPositionWrapper;
$this->documentService = $documentService;
$this->documentGateway = $documentGateway;
}
/**
* @param CopperSurchargeData $configData
*
* @return CopperSurchargeCalculator
*/
public function createCopperSurchargeCalculator(CopperSurchargeData $configData): CopperSurchargeCalculator
{
return new CopperSurchargeCalculator(
$this->purchasePriceGateway,
$this->rawMaterialGateway,
$this->documentPositionWrapper,
$this->documentService,
$this->documentGateway,
$configData
);
}
}
@@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
namespace Xentral\Modules\CopperSurcharge;
use Xentral\Modules\CopperSurcharge\Data\CopperSurchargeData;
use Xentral\Modules\CopperSurcharge\Wrapper\CompanyDataWrapper;
use Xentral\Modules\SystemConfig\Exception\ConfigurationKeyNotFoundException;
use Xentral\Modules\SystemConfig\Exception\InvalidArgumentException;
use Xentral\Modules\SystemConfig\Exception\ValueTooLargeException;
use Xentral\Modules\SystemConfig\SystemConfigModule;
final class CopperSurchargeService
{
/** @var string */
private const NAMESPACE = 'coppersurcharge';
/** @var SystemConfigModule $systemConfig */
private $systemConfig;
/** @var CompanyDataWrapper $companyDataWrapper */
private $companyDataWrapper;
/**
* @param SystemConfigModule $systemConfig
* @param CompanyDataWrapper $companyDataWrapper
*/
public function __construct(SystemConfigModule $systemConfig, CompanyDataWrapper $companyDataWrapper)
{
$this->systemConfig = $systemConfig;
$this->companyDataWrapper = $companyDataWrapper;
}
/**
* @return CopperSurchargeData|null
*/
public function findConfigurationData(): ?CopperSurchargeData
{
try {
$articleId = (int)$this->systemConfig->getValue(
self::NAMESPACE,
'articleid'
);
$surchargePositionType = (int)$this->systemConfig->getValue(
self::NAMESPACE,
'surchargepositiontype'
);
$surchargeDocumentConversion = (int)$this->systemConfig->getValue(
self::NAMESPACE,
'surchargedocumentconversion'
);
$surchargeInvoice = (int)$this->systemConfig->getValue(
self::NAMESPACE,
'surchargeinvoice'
);
$surchargeDeliveryCosts = (float)$this->systemConfig->getValue(
self::NAMESPACE,
'surchargedeliverycosts'
);
$surchargeCopperBase = (string)$this->systemConfig->getValue(
self::NAMESPACE,
'surchargecopperbase'
);
$surchargeCopperBaseStandard = (float)$this->systemConfig->getValue(
self::NAMESPACE,
'surchargecopperbasestandard'
);
$copperNumberOption = (string)$this->systemConfig->getValue(
self::NAMESPACE,
'coppernumberoption'
);
$surchargeMaintenanceType = (int)$this->systemConfig->getValue(
self::NAMESPACE,
'surchargemaintenancetype'
);
} catch (ConfigurationKeyNotFoundException $e) {
return null;
}
return new CopperSurchargeData(
$articleId,
$surchargePositionType,
$surchargeDocumentConversion,
$surchargeInvoice,
$surchargeDeliveryCosts,
$surchargeCopperBase,
$surchargeCopperBaseStandard,
$surchargeMaintenanceType,
$copperNumberOption
);
}
/**
* @param CopperSurchargeData $copperSurchargeData
*
* @throws InvalidArgumentException
* @throws ValueTooLargeException
*/
public function setConfigurationData(CopperSurchargeData $copperSurchargeData)
{
$this->systemConfig->setValue(
self::NAMESPACE,
'articleid',
(string)$copperSurchargeData->getCopperSurchargeArticleId()
);
$this->systemConfig->setValue(
self::NAMESPACE,
'surchargepositiontype',
(string)$copperSurchargeData->getSurchargePositionType()
);
$this->systemConfig->setValue(
self::NAMESPACE,
'surchargedocumentconversion',
(string)$copperSurchargeData->getSurchargeDocumentConversion()
);
$this->systemConfig->setValue(
self::NAMESPACE,
'surchargeinvoice',
(string)$copperSurchargeData->getSurchargeInvoice()
);
$this->systemConfig->setValue(
self::NAMESPACE,
'surchargedeliverycosts',
(string)$copperSurchargeData->getSurchargeDeliveryCosts()
);
$this->systemConfig->setValue(
self::NAMESPACE,
'surchargecopperbase',
(string)$copperSurchargeData->getSurchargeCopperBase()
);
$this->systemConfig->setValue(
self::NAMESPACE,
'surchargecopperbasestandard',
(string)$copperSurchargeData->getSurchargeCopperBaseStandard()
);
$this->systemConfig->setValue(
self::NAMESPACE,
'coppernumberoption',
$copperSurchargeData->getCopperNumberOption()
);
$this->systemConfig->setValue(
self::NAMESPACE,
'surchargemaintenancetype',
(string)$copperSurchargeData->getSurchargeMaintenanceType()
);
}
/**
* @param string $field
*
* @return string
*/
public function findCompanyData(string $field): string
{
return $this->companyDataWrapper->getCompanyData($field);
}
}
@@ -0,0 +1,260 @@
<?php
declare(strict_types=1);
namespace Xentral\Modules\CopperSurcharge\Data;
use Xentral\Modules\CopperSurcharge\Exception\ValidationFailedException;
final class CopperSurchargeData
{
/**
* a surcharge position gets added to every copper article
*/
public const POSITION_TYPE_ALWAYS = 0;
/**
* only one surcharge position gets added for all copper articles
*/
public const POSITION_TYPE_ONETIME = 1;
/**
* a surcharge position gets added to all position groups
*/
public const POSITION_TYPE_GROUP = 2;
/**
* calculation base is the date from the offer of the order
*/
public const DOCUMENT_CONVERSION_FROM_OFFER = 0;
//public const DOCUMENT_CONVERSION_CREATE_NEW = 1;
/**
* calculation base is the date from the order of the invoice
*/
public const INVOICE_CREATE_POS_BY_ORDER_DATE = 0;
/**
* calculation base is the delivery date of the invoice
*/
public const INVOICE_CREATE_POS_BY_DELIVERY_DATE = 1;
/**
* calculation base is the date from the invoice
*/
public const INVOICE_CREATE_POS_BY_INVOICE_DATE = 2;
/**
* calculation base is the date from the offer of the invoice
*/
public const INVOICE_CREATE_POS_BY_OFFER_DATE = 3;
/**
* data gets managed in the app raw materials
*/
public const SURCHARGE_MAINTENANCE_TYPE_APP = 0;
/**
* data gets managed in the additional fields of the copper article
*/
public const SURCHARGE_MAINTENANCE_TYPE_ARTICLE = 1;
/** @var int $copperSurchargeArticleId */
private $copperSurchargeArticleId;
/** @var int $surchargePositionType */
private $surchargePositionType;
/** @var int $surchargeDocumentConversion */
private $surchargeDocumentConversion;
/** @var int $surchargeInvoice */
private $surchargeInvoice;
/** @var float $surchargeDeliveryCosts */
private $surchargeDeliveryCosts;
/** @var string $surchargeCopperBase */
private $surchargeCopperBase;
/** @var float $surchargeCopperBaseStandard */
private $surchargeCopperBaseStandard;
/** @var int $copperNumberOption */
private $copperNumberOption;
/** @var int $surchargeMaintenaceType */
private $surchargeMaintenanceType;
/**
* @param int $articleId
* @param int $surchargePositionType
* @param int $surchargeDocumentConversion
* @param int $surchargeInvoice
* @param float $surchargeDeliveryCosts
* @param string $surchargeCopperBase
* @param float $surchargeCopperBaseStandard
* @param int $surchargeMaintenanceType
* @param string $copperNumberOption
*
*/
public function __construct(
int $articleId,
int $surchargePositionType,
int $surchargeDocumentConversion,
int $surchargeInvoice,
float $surchargeDeliveryCosts,
string $surchargeCopperBase,
float $surchargeCopperBaseStandard,
int $surchargeMaintenanceType,
string $copperNumberOption
) {
$this->validate(
$articleId,
$surchargeDeliveryCosts,
$surchargeCopperBaseStandard,
$surchargeMaintenanceType,
$copperNumberOption
);
$this->copperSurchargeArticleId = $articleId;
$this->surchargePositionType = $surchargePositionType;
$this->surchargeDocumentConversion = $surchargeDocumentConversion;
$this->surchargeInvoice = $surchargeInvoice;
$this->surchargeDeliveryCosts = $surchargeDeliveryCosts;
$this->surchargeCopperBase = $surchargeCopperBase;
$this->surchargeCopperBaseStandard = $surchargeCopperBaseStandard;
$this->copperNumberOption = $copperNumberOption;
$this->surchargeMaintenanceType = $surchargeMaintenanceType;
}
/**
* @param int $articleId
* @param float $surchargeDeliveryCosts
* @param float $surchargeCopperBaseStandard
*
* @param int $surchargeMaintenanceType
* @param string $copperNumberOption
*/
private function validate(
int $articleId,
float $surchargeDeliveryCosts,
float $surchargeCopperBaseStandard,
int $surchargeMaintenanceType,
string $copperNumberOption
) {
if (empty($articleId)) {
throw new ValidationFailedException('copper surcharge article is missing');
}
if (empty($surchargeDeliveryCosts)) {
throw new ValidationFailedException('surcharge delivery costs are missing');
}
if (empty($surchargeCopperBaseStandard)) {
throw new ValidationFailedException('surcharge copper base standard is missing');
}
if ($surchargeMaintenanceType === self::SURCHARGE_MAINTENANCE_TYPE_ARTICLE && empty($copperNumberOption)) {
throw new ValidationFailedException('copper number option is missing');
}
}
/**
* @return int article id of the surcharge article
*/
public function getCopperSurchargeArticleId(): int
{
return $this->copperSurchargeArticleId;
}
/**
* 0 = add always a surcharge position to every copper article,
* 1 = add only one surcharge position for all copper articles,
* 2 = add a surcharge position in every position group
*
* @return int
*/
public function getSurchargePositionType(): int
{
return $this->surchargePositionType;
}
/**
* decides which date is the base if a surcharge positions is added to an order
* 0 = date of offer (fallback current date)
* 1 = date of the order
*
* @return int
*/
public function getSurchargeDocumentConversion(): int
{
return $this->surchargeDocumentConversion;
}
/**
* decides which date is the base if a surcharge positions is added to an invoice
* 0 = from the order (fallback current date)
* 1 = delivery date (fallback current date)
* 2 = from the invoice
* 3 = from the offer (fallback current date)
*
* @return int
*/
public function getSurchargeInvoice(): int
{
return $this->surchargeInvoice;
}
/**
* part of the calculation, default 1, in percent
*
* @return float
*/
public function getSurchargeDeliveryCosts(): float
{
return $this->surchargeDeliveryCosts;
}
/**
* can be used instead of surchargeCopperBaseStandard for specific articles, EUR/100kg
*
* @return string
*/
public function getSurchargeCopperBase(): string
{
return $this->surchargeCopperBase;
}
/**
* part of the the calculation, default 150, EUR/100kg
*
* @return float
*/
public function getSurchargeCopperBaseStandard(): float
{
return $this->surchargeCopperBaseStandard;
}
/**
* describes which additional field in the article contains the weight,
* only useful with surchargeMaintenanceType = 1
*
* @return string
*/
public function getCopperNumberOption(): string
{
return $this->copperNumberOption;
}
/**
* choice how copper articles should hold their necessary infos
* 0 = with the app 'raw materials'
* 1 = with additional article fields
*
* @return int
*/
public function getSurchargeMaintenanceType(): int
{
return $this->surchargeMaintenanceType;
}
}
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Xentral\Modules\CopperSurcharge\Data;
final class DocumentPositionData
{
/** @var int $positionId */
private $positionId;
/** @var int $articleId */
private $articleId;
/** @var string $currency */
private $currency;
/**
* @param int $positionId
* @param int $articleId
* @param string $currency
*/
public function __construct(
int $positionId,
int $articleId,
string $currency
) {
$this->positionId = $positionId;
$this->articleId = $articleId;
$this->currency = $currency;
}
/**
* @return int
*/
public function getPositionId(): int
{
return $this->positionId;
}
/**
* @return int
*/
public function getArticleId(): int
{
return $this->articleId;
}
/**
* @return string
*/
public function getCurrency(): string
{
return $this->currency;
}
}
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Xentral\Modules\CopperSurcharge\Exception;
use Xentral\Core\Exception\ModuleExceptionInterface;
interface CopperSurchargeExceptionInterface extends ModuleExceptionInterface
{
}
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Xentral\Modules\CopperSurcharge\Exception;
use RuntimeException;
final class EmptyResultException extends RuntimeException implements CopperSurchargeExceptionInterface
{
}
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Xentral\Modules\CopperSurcharge\Exception;
use InvalidArgumentException;
final class InvalidDateFormatException extends InvalidArgumentException implements CopperSurchargeExceptionInterface
{
}
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Xentral\Modules\CopperSurcharge\Exception;
use InvalidArgumentException;
final class ValidationFailedException extends InvalidArgumentException implements CopperSurchargeExceptionInterface
{
}
@@ -0,0 +1,933 @@
<?php
declare(strict_types=1);
namespace Xentral\Modules\CopperSurcharge\Service;
use DateTimeImmutable;
use DateTimeInterface;
use Xentral\Modules\CopperSurcharge\Data\CopperSurchargeData;
use Xentral\Modules\CopperSurcharge\Data\DocumentPositionData;
use Xentral\Modules\CopperSurcharge\Exception\InvalidDateFormatException;
use Xentral\Modules\CopperSurcharge\Wrapper\DocumentPositionWrapper;
use Xentral\Modules\CopperSurcharge\Wrapper\DocumentPositionWrapperInterface;
final class CopperSurchargeCalculator
{
/** @var RawMaterialGateway $rawMaterialGateway */
private $rawMaterialGateway;
/** @var PurchasePriceGateway $purchasePriceGateway */
private $purchasePriceGateway;
/** @var DocumentPositionWrapper $documentPositionWrapper */
private $documentPositionWrapper;
/** @var CopperSurchargeData $config */
private $config;
/** @var DocumentService $documentService */
private $documentService;
/** @var DocumentGateway $documentGateway */
private $documentGateway;
/**
* @param PurchasePriceGateway $purchasePriceGateway
* @param RawMaterialGateway $rawMaterialGateway
* @param DocumentPositionWrapperInterface $documentPositionWrapper
* @param DocumentService $documentService
* @param DocumentGateway $documentGateway
* @param CopperSurchargeData $copperSurchargeConfig
*/
public function __construct(
PurchasePriceGateway $purchasePriceGateway,
RawMaterialGateway $rawMaterialGateway,
DocumentPositionWrapperInterface $documentPositionWrapper,
DocumentService $documentService,
DocumentGateway $documentGateway,
CopperSurchargeData $copperSurchargeConfig
) {
$this->purchasePriceGateway = $purchasePriceGateway;
$this->rawMaterialGateway = $rawMaterialGateway;
$this->documentPositionWrapper = $documentPositionWrapper;
$this->documentService = $documentService;
$this->config = $copperSurchargeConfig;
$this->documentGateway = $documentGateway;
}
/**
* @param string $docType
* @param int $docId
* @param array|DocumentPositionData[] $possibleCopperPositions
* @param array $copperPositionsInPartsList
*
* @return int
*/
public function handleCopperSurchargePositions(
string $docType,
int $docId,
array $possibleCopperPositions,
array $copperPositionsInPartsList
): int {
$calcDate = $this->evaluateCalcDate($docType, $docId);
if ($this->config->getSurchargePositionType() === CopperSurchargeData::POSITION_TYPE_ALWAYS) {
$this->createManyPositions(
$docType,
$docId,
$possibleCopperPositions,
$copperPositionsInPartsList,
$calcDate
);
return 0;
} elseif ($this->config->getSurchargePositionType() === CopperSurchargeData::POSITION_TYPE_ONETIME) {
$this->createSinglePosition(
$docType,
$docId,
$possibleCopperPositions,
$copperPositionsInPartsList,
$calcDate
);
return 1;
} else {
$this->createGroupPositions($docType, $docId, $copperPositionsInPartsList, $calcDate);
return 2;
}
}
/**
* Because there is no connection between positions,
* all surcharge positions get deleted and recreated in the next step
*
* @param string $docType
* @param int $docId
*/
public function resetDocument(string $docType, int $docId): void
{
$this->resetBetweenPositions($docId, $docType, $this->config->getCopperSurchargeArticleId());
$this->documentService->deleteCopperSurchargePositions(
$docType,
$docId,
$this->config->getCopperSurchargeArticleId()
);
$this->documentService->updatePositionSorts($docType, $docId);
}
/**
* @param string $docType
* @param int $docId
* @param array|DocumentPositionData[] $copperPositions
* @param array $copperPositionsInPartsList
* @param DateTimeInterface $calcDate
*
*/
private function createManyPositions(
string $docType,
int $docId,
array $copperPositions,
array $copperPositionsInPartsList,
DateTimeInterface $calcDate
): void {
foreach ($copperPositions as $position) {
$amount = $this->calcAmount($docType, $position->getPositionId(), $position->getArticleId());
$copperBase = $this->getCopperBase($position->getArticleId());
$price = $this->calculateCopperSurchargePrice(
$copperBase,
$amount,
$calcDate
);
$newPosId = $this->addCopperSurchargePosition(
$docType,
$docId,
$price,
$amount,
$copperBase,
$position->getCurrency(),
$calcDate
);
$this->documentService->updatePositionSort($docType, $docId, $position->getPositionId(), $newPosId);
}
if (empty($copperPositionsInPartsList)) {
return;
}
foreach ($copperPositionsInPartsList as $partListPosition) {
$partListData = $this->evaluateSurchargeDataForPartList(
$partListPosition['article_id'],
$partListPosition['pos_id'],
$calcDate,
$docType,
$partListPosition['amount']
);
$newPosId = $this->addCopperSurchargePosition(
$docType,
$docId,
$partListData['price'],
$partListData['amount'],
$partListData['copper_base'],
$partListPosition['currency'],
$calcDate
);
$this->documentService->updatePositionSort($docType, $docId, $partListData['position_id'], $newPosId);
}
}
/**
* @param int $partListHeadId
* @param int $positionId
* @param DateTimeInterface $calcDate
* @param string $docType
* @param float $partListPositionAmount
*
* @return array
*/
private function evaluateSurchargeDataForPartList(
int $partListHeadId,
int $positionId,
DateTimeInterface $calcDate,
string $docType,
float $partListPositionAmount
): array {
$copperArticles =
$this->getCopperArticlesFromPartList(
$partListHeadId,
$this->config->getSurchargeMaintenanceType(),
$this->config->getCopperNumberOption(),
$this->config->getCopperSurchargeArticleId()
);
$amount = 0;
$price = 0.0;
$copperBase = 0.0;
foreach ($copperArticles as $copperArticle) {
$amount += $copperArticle['amount'];
$copperBase = $this->getCopperBase($copperArticle['article_id']);
$price += $this->calculateCopperSurchargePrice(
$copperBase,
$amount,
$calcDate
);
}
return [
'amount' => $amount * $partListPositionAmount,
'price' => $price * $partListPositionAmount,
'copper_base' => $copperBase,
'position_id' => $this->documentGateway->evaluatePartListLastPositionId($docType, $positionId),
];
}
/**
* @param string $docType
* @param int $docId
* @param float $price
* @param float $amount
* @param float $copperBase
* @param $currency
* @param DateTimeInterface $calcDate
*
* @return int
*/
private function addCopperSurchargePosition(
string $docType,
int $docId,
float $price,
float $amount,
float $copperBase,
$currency,
DateTimeInterface $calcDate
): int {
$copperSurchargeArticleId = $this->config->getCopperSurchargeArticleId();
$articleData = $this->documentGateway->getArticleData($copperSurchargeArticleId);
$description = $this->findCopperSurchargeArticleDescription(
$amount,
$copperBase,
$price,
$articleData,
$calcDate
);
return $this->documentPositionWrapper->addPositionManuallyWithPrice(
$docType,
$docId,
$copperSurchargeArticleId,
$articleData,
1,
$price,
$currency,
$description
);
}
/**
* @param float $amount
* @param float $copperBase
* @param float $price
* @param array $articleData
* @param DateTimeInterface $calcDate
*
* @return string
*/
private function findCopperSurchargeArticleDescription(
float $amount,
float $copperBase,
float $price,
array $articleData,
DateTimeInterface $calcDate
): string {
$description = $articleData['description'];
$delPrice = $this->purchasePriceGateway->getDelCopperPriceByDate(
$calcDate,
$this->config->getCopperSurchargeArticleId()
);
$price = number_format($price, 2, ",", ".");
$delPrice = number_format($delPrice, 2, ",", ".");
$copperBase = number_format($copperBase, 2, ",", ".");
$amount = str_replace('.', ',', $amount);
$description = str_replace('{NETPRICE}', $price, $description);
$description = str_replace('{ARTIKELNUMMER}', $articleData['number'], $description);
$description = str_replace('{ARTIKELNAME}', $articleData['name_de'], $description);
$description = str_replace('{COPPERBASIS}', $copperBase, $description);
$description = str_replace('{COPPERNUMBER}', $amount, $description);
$description = str_replace('{DELVALUE}', $delPrice, $description);
return $description;
}
/**
* @param string $docType
* @param int $docId
* @param array|DocumentPositionData[] $copperPositions
* @param array $copperPositionsInPartsList
* @param DateTimeInterface $calcDate
*
* @return int
*/
private function createSinglePosition(
string $docType,
int $docId,
array $copperPositions,
array $copperPositionsInPartsList,
DateTimeInterface $calcDate
): int {
$price = 0.0;
$totalAmount = 0.0;
$currency = 'EUR';
$copperBase = 0.0;
foreach ($copperPositions as $position) {
$currency = $position->getCurrency();
$amount = $this->calcAmount($docType, $position->getPositionId(), $position->getArticleId());
$totalAmount += $amount;
$copperBase = $this->getCopperBase($position->getArticleId());
$price += $this->calculateCopperSurchargePrice(
$copperBase,
$amount,
$calcDate
);
}
if (!empty($copperPositionsInPartsList)) {
foreach ($copperPositionsInPartsList as $position) {
$partListData = $this->evaluateSurchargeDataForPartList(
$position['article_id'],
$position['pos_id'],
$calcDate,
$docType,
$position['amount']
);
$totalAmount += $partListData['amount'];
$price += $partListData['price'];
$copperBase = $partListData['copper_base'];
}
}
$newPosId = 0;
if ($price > 0) {
$newPosId = $this->addCopperSurchargePosition(
$docType,
$docId,
$price,
$totalAmount,
$copperBase,
$currency,
$calcDate
);
}
return $newPosId;
}
/**
* @param string $docType
* @param int $docId
* @param array $copperPositionsInPartsList
* @param DateTimeInterface $calcDate
*/
private function createGroupPositions(
string $docType,
int $docId,
array $copperPositionsInPartsList,
DateTimeInterface $calcDate
): void {
if ($this->config->getSurchargeMaintenanceType() === CopperSurchargeData::SURCHARGE_MAINTENANCE_TYPE_APP) {
$positions = $this->rawMaterialGateway->findAllPositionsForGrouped(
$docType,
$docId,
$this->config->getCopperSurchargeArticleId()
);
} else {
$positions = $this->documentGateway->findAllPositionsForGrouped(
$docType,
$docId,
$this->config->getCopperNumberOption()
);
}
$price = 0.0;
$totalAmount = 0.0;
$currency = 'EUR';
$prev = null;
$copperBase = 0.0;
$lastSort = 0;
foreach ($positions as $key => $position) {
if ((bool)$position['is_copper']) {
$amount = $this->calcAmount($docType, (int)$position['pos_id'], (int)$position['article_id']);
$copperBase = $this->getCopperBase($position['article_id']);
$price += $this->calculateCopperSurchargePrice(
$copperBase,
$amount,
$calcDate
);
$currency = $position['currency'];
$totalAmount += $amount;
}
if ($position['between_type'] === 'gruppe') {
if (!empty($copperPositionsInPartsList)) {
$partListElements = $this->getElementsFromPartListBetweenSorts(
$copperPositionsInPartsList,
$lastSort,
$position['sort']
);
foreach ($partListElements as $partListElement) {
$partListKey = $partListElement['part_list_key'];
$partListData = $this->evaluateSurchargeDataForPartList(
$partListElement['article_id'],
$position['pos_id'],
$calcDate,
$docType,
$partListElement['amount']
);
$totalAmount += $partListData['amount'];
$price += $partListData['price'];
$copperBase = $partListData['copper_base'];
unset($copperPositionsInPartsList[$partListKey]);
}
}
if ($price > 0.0) {
$newPosId = $this->addCopperSurchargePosition(
$docType,
$docId,
$price,
$totalAmount,
$copperBase,
$currency,
$calcDate
);
if (!empty($prev)) {
$this->documentService->updatePositionSort($docType, $docId, (int)$prev['pos_id'], $newPosId);
}
$price = 0.0;
$totalAmount = 0.0;
$lastSort = $position['sort'];
}
}
$prev = $position;
}
if (!empty($copperPositionsInPartsList)) {
foreach ($copperPositionsInPartsList as $partListElement) {
$partListData = $this->evaluateSurchargeDataForPartList(
$partListElement['article_id'],
$partListElement['pos_id'],
$calcDate,
$docType,
$partListElement['amount']
);
$totalAmount += $partListData['amount'];
$price += $partListData['price'];
$copperBase = $partListData['copper_base'];
}
}
if ($price > 0.0) {
$this->addCopperSurchargePosition(
$docType,
$docId,
$price,
$totalAmount,
$copperBase,
$currency,
$calcDate
);
}
}
/**
* @param float $copperBase
* @param float $amount
* @param DateTimeInterface $calcDate
*
* @return float
*/
private function calculateCopperSurchargePrice(
float $copperBase,
float $amount,
DateTimeInterface $calcDate
): float {
$delPrice = $this->purchasePriceGateway->getDelCopperPriceByDate(
$calcDate,
$this->config->getCopperSurchargeArticleId()
);
$perCent = $this->config->getSurchargeDeliveryCosts() / 100;
return (($delPrice + ($delPrice * $perCent)) - $copperBase) * $amount / 100;
}
/**
* @param string $docType
* @param int $positionId
* @param int $positionArticleId
*
* @return float
*/
private function calcAmount(string $docType, int $positionId, int $positionArticleId): float
{
if ($this->config->getSurchargeMaintenanceType() === CopperSurchargeData::SURCHARGE_MAINTENANCE_TYPE_APP) {
$amount = $this->rawMaterialGateway->getRawMaterialAmount(
$positionArticleId,
$this->config->getCopperSurchargeArticleId()
);
} else {
$articleId = $this->documentGateway->getArticleIdByPositionId($docType, $positionId);
$amount = $this->documentGateway->getArticleCopperNumber(
$articleId,
$this->config->getCopperNumberOption()
);
}
$documentAmount = $this->documentGateway->getPositionAmount($docType, $positionId);
$amount *= $documentAmount;
return (float)$amount;
}
/**
* @param int $positionArticleId
*
* @return float
*/
private function getCopperBase(int $positionArticleId): float
{
$copperBase = $this->config->getSurchargeCopperBaseStandard();
$articleCopperBaseField = $this->config->getSurchargeCopperBase();
if (!empty($articleCopperBaseField)) {
$copperBaseTemp = $this->documentGateway->getArticleCopperBase($positionArticleId, $articleCopperBaseField);
if (!empty($copperBaseTemp)) {
$copperBase = $copperBaseTemp;
}
}
return $copperBase;
}
/**
* @param string $doctype
* @param int $docId
*
* @throws InvalidDateFormatException
* @return DateTimeInterface
*/
private function evaluateCalcDate(string $doctype, int $docId): DateTimeInterface
{
$orderOfferId = 0;
if ($doctype === 'auftrag') {
$orderOfferId = $this->documentGateway->findOrderOfferId($docId);
}
if (
$doctype === 'rechnung' &&
$this->config->getSurchargeInvoice() === CopperSurchargeData::INVOICE_CREATE_POS_BY_DELIVERY_DATE
) {
$calcDate = $this->documentGateway->findDeliveryDate($docId);
if (empty($calcDate)) {
$calcDate = new DateTimeImmutable();
}
} elseif (
$doctype === 'rechnung' &&
$this->config->getSurchargeInvoice() === CopperSurchargeData::INVOICE_CREATE_POS_BY_ORDER_DATE
) {
$orderId = $this->documentGateway->findInvoiceOrderId($docId);
if (empty($orderId)) {
$calcDate = new DateTimeImmutable();
} else {
$calcDate = $this->documentGateway->getCalcDate('auftrag', $orderId);
}
} elseif ($doctype === 'rechnung' &&
$this->config->getSurchargeInvoice() === CopperSurchargeData::INVOICE_CREATE_POS_BY_INVOICE_DATE) {
$calcDate = $this->documentGateway->getCalcDate($doctype, $docId);
} elseif ($doctype === 'rechnung' &&
$this->config->getSurchargeInvoice() === CopperSurchargeData::INVOICE_CREATE_POS_BY_OFFER_DATE) {
$offerId = $this->documentGateway->findInvoiceOfferId($docId);
if (!empty($offerId)) {
$calcDate = $this->documentGateway->getCalcDate('angebot', $offerId);
} else {
$calcDate = new DateTimeImmutable();
}
} elseif (
$doctype === 'auftrag' &&
$orderOfferId !== 0 &&
$this->config->getSurchargeDocumentConversion() === CopperSurchargeData::DOCUMENT_CONVERSION_FROM_OFFER
) {
$calcDate = $this->documentGateway->getCalcDate('angebot', $orderOfferId);
} else {
$calcDate = new DateTimeImmutable();
}
return $calcDate;
}
/**
* @param int $docId
* @param string $docType
* @param int $copperSurchargeArticleId
*/
private function resetBetweenPositions(int $docId, string $docType, int $copperSurchargeArticleId): void
{
if ($this->config->getSurchargeMaintenanceType() === CopperSurchargeData::SURCHARGE_MAINTENANCE_TYPE_APP) {
$positions = $this->rawMaterialGateway->findAllPositionsForGrouped(
$docType,
$docId,
$copperSurchargeArticleId
);
} else {
$positions = $this->documentGateway->findAllPositionsForGrouped(
$docType,
$docId,
$this->config->getCopperNumberOption()
);
}
$offset = 0;
foreach ($positions as $position) {
if ((int)$position['article_id'] === $copperSurchargeArticleId) {
$offset--;
}
if ((int)$position['pos_type'] === 2 && $offset < 0) {
$this->documentService->updateBetweenSort(
(int)$position['between_id'],
(int)$position['sort'] + $offset
);
}
}
}
/**
* @param string $docType
* @param int $docId
*/
public function deleteRemainingCopperSurchargeArticles(string $docType, int $docId): void
{
if ($this->config->getSurchargeMaintenanceType() === CopperSurchargeData::SURCHARGE_MAINTENANCE_TYPE_ARTICLE) {
$hasCopperArticles =
$this->documentGateway->hasCopperArticles(
$docType,
$docId,
$this->config->getCopperNumberOption()
);
} else {
$hasCopperArticles =
$this->rawMaterialGateway->hasCopperArticles(
$docType,
$docId,
$this->config->getCopperSurchargeArticleId()
);
}
if (!$hasCopperArticles) {
$this->documentService->deleteCopperSurchargePositions(
$docType,
$docId,
$this->config->getCopperSurchargeArticleId()
);
}
}
/**
* @param string $docType
* @param int $posId
*/
public function updatePositionContributionMargin(string $docType, int $posId)
{
$this->documentService->updatePositionContributionMargin($docType, $posId, 0);
}
/**
* @param string $docType
* @param int $posId
*/
public function updatePositionPurchasePrice(string $docType, int $posId)
{
$this->documentService->updatePositionPurchasePrice($docType, $posId, 0.0);
}
/**
* @param string $docType
*
* @param int $docTypeId
*
* @return array|DocumentPositionData[]
*/
public function findPositionsForMaintenanceApp(string $docType, int $docTypeId): array
{
$data = $this->rawMaterialGateway->findPositions(
$docType,
$docTypeId,
$this->config->getCopperSurchargeArticleId()
);
return $this->transformToDocumentPositionData($data);
}
/**
* @param string $docType
*
* @param int $docTypeId
*
* @return array|DocumentPositionData[]
*/
public function findPositionsForMaintenanceArticle(string $docType, int $docTypeId): array
{
$data = $this->documentGateway->findPositions(
$docType,
$docTypeId,
$this->config->getCopperNumberOption()
);
return $this->transformToDocumentPositionData($data);
}
/**
* @param array $copperPositionsRaw
*
* @return array|DocumentPositionData[]
*/
private function transformToDocumentPositionData(array $copperPositionsRaw): array
{
$documentPositions = [];
foreach ($copperPositionsRaw as $position) {
$data = new DocumentPositionData(
(int)$position['pos_id'],
(int)$position['article_id'],
(string)$position['currency']
);
$documentPositions[] = $data;
}
return $documentPositions;
}
/**
* @param int $doctypeId
* @param string $doctype
*/
public function updateCopperSurchargeArticles(int $doctypeId, string $doctype)
{
$copperSurchargeArticleId = $this->config->getCopperSurchargeArticleId();
$surchargePositions = $this->documentGateway->findCopperSurchargeArticlePositionIds(
$doctype,
$doctypeId,
$copperSurchargeArticleId
);
if (!empty($surchargePositions)) {
foreach ($surchargePositions as $surchargePosition) {
$this->updatePositionContributionMargin($doctype, (int)$surchargePosition['pos_id']);
$this->updatePositionPurchasePrice($doctype, (int)$surchargePosition['pos_id']);
}
}
}
/**
* @param array $copperPositionsInPartsList
* @param int $lastSort
* @param int $nextSort
*
* @return array
*/
private function getElementsFromPartListBetweenSorts(
array $copperPositionsInPartsList,
int $lastSort,
int $nextSort
): array {
$result = [];
foreach ($copperPositionsInPartsList as $partListKey => $position) {
$currentSort = $position['sort'];
if ($currentSort > $lastSort && $currentSort <= $nextSort) {
$position['part_list_key'] = $partListKey;
$result[] = $position;
}
}
return $result;
}
/**
* @param int $partListHeadId
* @param int $surchargeMaintenanceType
* @param string $copperNumberOption
* @param int $copperArticleId
*
* @return array
*/
private function getCopperArticlesFromPartList(
int $partListHeadId,
int $surchargeMaintenanceType,
string $copperNumberOption,
int $copperArticleId
): array {
$result = [];
$childElements = $this->documentGateway->getAllPartListChildElements($partListHeadId);
foreach ($childElements as $childElement) {
if ($surchargeMaintenanceType === CopperSurchargeData::SURCHARGE_MAINTENANCE_TYPE_ARTICLE) {
$possibleArticle = $this->documentGateway->findPossibleCopperArticle(
$childElement['id'],
$copperNumberOption
);
} else {
$possibleArticle = $this->rawMaterialGateway->findPossibleCopperArticle(
$childElement['id'],
$copperArticleId
);
}
if (empty($possibleArticle)) {
continue;
}
$result[] = [
'article_id' => $possibleArticle['article_id'],
'amount' => $possibleArticle['amount'] * $childElement['amount'],
];
}
return $result;
}
/**
* @param string $docType
* @param int $docTypeId
*
* @return array
*/
public function findPositionsForMaintenanceAppInPartsList(
string $docType,
int $docTypeId
): array {
$result = [];
$copperArticleId = $this->config->getCopperSurchargeArticleId();
$headArticles = $this->documentGateway->findPartListHeadArticles($docType, $docTypeId);
foreach ($headArticles as $headArticle) {
$childElements = $this->documentGateway->getAllPartListChildElements($headArticle['id']);
$hasCopper = false;
foreach ($childElements as $childElement) {
if (!$hasCopper) {
$hasCopper = !empty(
$this->rawMaterialGateway->findPossibleCopperArticle(
$childElement['id'],
$copperArticleId
)
);
}
}
if ($hasCopper) {
$result[] = [
'article_id' => $headArticle['id'],
'sort' => $headArticle['sort'],
'pos_id' => $headArticle['pos_id'],
'currency' => $headArticle['currency'],
'amount' => (float)$headArticle['amount'],
];
}
}
return $result;
}
/**
* @param string $docType
* @param int $docTypeId
*
* @return array
*/
public function findPositionsForMaintenanceArticleInPartsList(
string $docType,
int $docTypeId
): array {
$result = [];
$copperNumberOption = $this->config->getCopperNumberOption();
$headArticles = $this->documentGateway->findPartListHeadArticles($docType, $docTypeId);
foreach ($headArticles as $headArticle) {
$childElements = $this->documentGateway->getAllPartListChildElements((int)$headArticle['id']);
$hasCopper = false;
foreach ($childElements as $childElement) {
if (!$hasCopper) {
$hasCopper = !empty(
$this->documentGateway->findPossibleCopperArticle(
$childElement['id'],
$copperNumberOption
)
);
}
}
if ($hasCopper) {
$result[] = [
'article_id' => $headArticle['id'],
'sort' => $headArticle['sort'],
'pos_id' => $headArticle['pos_id'],
'currency' => $headArticle['currency'],
'amount' => (float)$headArticle['amount'],
];
}
}
return $result;
}
}
@@ -0,0 +1,470 @@
<?php
declare(strict_types=1);
namespace Xentral\Modules\CopperSurcharge\Service;
use DateTimeImmutable;
use Exception;
use Xentral\Components\Database\Database;
use Xentral\Modules\CopperSurcharge\Exception\EmptyResultException;
use Xentral\Modules\CopperSurcharge\Exception\InvalidDateFormatException;
final class DocumentGateway
{
/** @var Database $db */
private $db;
/**
* @param Database $db
*/
public function __construct(Database $db)
{
$this->db = $db;
}
/**
* @param int $orderId
*
* @return int
*/
public function findOrderOfferId(int $orderId): int
{
$sql =
"SELECT a.angebotid
FROM `auftrag` AS `a`
WHERE id = :order_id";
return (int)$this->db->fetchValue($sql, ['order_id' => $orderId]);
}
/**
* @param string $doctype
* @param int $documentId
*
* @throws InvalidDateFormatException
* @return DateTimeImmutable
*/
public function getCalcDate(string $doctype, int $documentId): DateTimeImmutable
{
$sql =
"SELECT b.datum AS `date`
FROM `" . $doctype . "` AS `b`
WHERE b.id = :document_id";
$result = $this->db->fetchValue($sql, ['document_id' => $documentId]);
try {
return new DateTimeImmutable($result);
} catch (Exception $e) {
throw new InvalidDateFormatException('Could not convert date: ' . $result['date']);
}
}
/**
* @param int $documentId
*
* @throws InvalidDateFormatException
*
* @return DateTimeImmutable|null
*/
public function findDeliveryDate(int $documentId): ?DateTimeImmutable
{
$sql =
"SELECT b.lieferdatum AS `delivery_date`
FROM `rechnung` AS `b`
WHERE b.id = :document_id";
$result = $this->db->fetchValue($sql, ['document_id' => $documentId]);
if ($result === '0000-00-00') {
return null;
}
try {
return new DateTimeImmutable($result);
} catch (Exception $e) {
throw new InvalidDateFormatException('Could not convert date: ' . $result['date']);
}
}
/**
* @param int $articleId
* @param string $articleCopperBaseField
*
* @return float
*/
public function getArticleCopperBase(int $articleId, string $articleCopperBaseField): float
{
$copperBase = 0.0;
$sql =
"SELECT a.{$articleCopperBaseField} AS `copper_base`
FROM `artikel` AS `a`
WHERE a.id = :article_id";
$result = $this->db->fetchValue($sql, ['article_id' => $articleId]);
if (!empty($result)) {
$copperBase = $this->formatToFloat($result);
}
return $copperBase;
}
/**
* @param string $string
*
* @return float
*/
private function formatToFloat(string $string): float
{
$string = str_replace(',', '.', $string);
return (float)$string;
}
/**
* @param string $docType
* @param int $docId
* @param string $freeField
*
* @return array
*/
public function findAllPositionsForGrouped(
string $docType,
int $docId,
string $freeField
): array {
$sql =
"SELECT * FROM(
SELECT
pos.id AS `pos_id`,
pos.artikel AS `article_id`,
pos.waehrung AS `currency`,
pos.sort AS `sort`,
IF(a.{$freeField} = '', 0, 1) AS `is_copper`,
1 AS `pos_type`,
'' AS `between_type`,
0 AS `between_id`
FROM `" . $docType . "_position` AS `pos`
INNER JOIN `artikel` AS `a` ON a.id = pos.artikel
WHERE " . $docType . " = :doc_id
UNION
SELECT
0 AS `pos_id`,
0 AS `article_id`,
'' AS `currency`,
z.pos AS `sort`,
0 AS `is_copper`,
2 AS `pos_type`,
z.postype AS `between_type`,
z.id AS `between_id`
FROM `beleg_zwischenpositionen` AS `z`
WHERE z.doctype = :doc_type
AND z.doctypeid = :doc_id
) AS `data`
ORDER BY data.sort, data.pos_type";
return $this->db->fetchAll(
$sql,
[
'doc_id' => $docId,
'doc_type' => $docType,
]
);
}
/**
* @param string $docType
* @param int $positionArticleId
*
* @return int
*/
public function getPositionAmount(string $docType, int $positionArticleId): int
{
$sql =
"SELECT pos.menge
FROM `" . $docType . "_position` AS `pos`
WHERE pos.id = :pos_id";
return (int)$this->db->fetchValue($sql, ['pos_id' => $positionArticleId]);
}
/**
* @param int $docId
*
* @return int
*/
public function findInvoiceOrderId(int $docId): int
{
$sql =
"SELECT r.auftragid
FROM `rechnung` AS `r`
WHERE id = :doc_id";
return (int)$this->db->fetchValue($sql, ['doc_id' => $docId]);
}
/**
* @param string $docType
* @param int $docId
* @param string $copperNumberOption
*
* @return bool
*/
public function hasCopperArticles(
string $docType,
int $docId,
string $copperNumberOption
): bool {
$sql = "SELECT pos.id
FROM `" . $docType . "_position` AS `pos`
INNER JOIN `artikel` AS `a` ON a.id = pos.artikel
WHERE pos." . $docType . " = :doc_id
AND a.{$copperNumberOption} != ''";
return !empty($this->db->fetchAll($sql, ['doc_id' => $docId]));
}
/**
* @param $articleId
*
* @throws EmptyResultException
*
* @return array
*/
public function getArticleData($articleId): array
{
$sql =
"SELECT
a.name_de,
a.anabregs_text AS `description`,
a.umsatzsteuer AS `vat`,
a.rabatt AS `discount`,
a.projekt AS `project`,
a.nummer AS `number`
FROM `artikel` AS `a`
WHERE a.id = :article_id
";
$articleData = $this->db->fetchRow($sql, ['article_id' => $articleId]);
if (empty($articleData)) {
throw new EmptyResultException('No article found for id: ' . $articleId);
}
return $articleData;
}
/**
* @param string $docType
* @param int $positionId
*
* @return int
*/
public function getArticleIdByPositionId(string $docType, int $positionId): int
{
$sql =
"SELECT pos.artikel
FROM `{$docType}_position` AS `pos`
WHERE pos.id = :position_id";
return $this->db->fetchValue($sql, ['position_id' => $positionId]);
}
/**
* @param int $docId
*
* @return int
*/
public function findInvoiceOfferId(int $docId): int
{
$sql =
"SELECT a.angebotid
FROM `rechnung` AS `r`
INNER JOIN `auftrag` AS `a` ON a.id = r.auftragid
WHERE r.id = :doc_id";
return (int)$this->db->fetchValue($sql, ['doc_id' => $docId]);
}
/**
* @param string $docType
* @param int $docTypeId
* @param string $copperNumberOption
*
* @return array
*/
public function findPositions(
string $docType,
int $docTypeId,
string $copperNumberOption
): array {
$explodedColumnName = 'explodiert_parent';
if ($docType === 'rechnung') {
$explodedColumnName = 'explodiert_parent_artikel';
}
$sql = "SELECT
beleg_pos.id AS `pos_id`,
a.id AS `article_id`,
beleg_pos.waehrung AS `currency`
FROM `{$docType}_position` AS `beleg_pos`
INNER JOIN `{$docType}` AS `beleg` ON beleg.id = beleg_pos.{$docType}
INNER JOIN `artikel` AS `a` ON a.id = beleg_pos.artikel
WHERE beleg_pos.{$docType} = :doc_type_id
AND a.{$copperNumberOption} != ''
AND beleg.schreibschutz = 0
AND beleg_pos.{$explodedColumnName} = 0
ORDER BY beleg_pos.sort";
$result = $this->db->fetchAll($sql, ['doc_type_id' => $docTypeId]);
if (!empty($result)) {
return $result;
}
return [];
}
/**
* @param string $docType
* @param int $doctypeId
* @param int $copperSurchargeArticleId
*
* @return array
*/
public function findCopperSurchargeArticlePositionIds(
string $docType,
int $doctypeId,
int $copperSurchargeArticleId
): array {
$sql =
"SELECT
beleg_pos.id AS `pos_id`
FROM `{$docType}_position` AS `beleg_pos`
WHERE beleg_pos.{$docType} = :doc_type_id
AND beleg_pos.artikel = :copper_surcharge_article_id";
return $this->db->fetchAll(
$sql,
['doc_type_id' => $doctypeId, 'copper_surcharge_article_id' => $copperSurchargeArticleId]
);
}
/**
* @param int $copperArticleId
* @param string $copperNumberOption
*
* @return array
*/
public function findPossibleCopperArticle(int $copperArticleId, string $copperNumberOption): array
{
$sql =
"SELECT art.id AS `article_id`, art.{$copperNumberOption} AS `copper_number`
FROM `artikel` AS `art`
WHERE art.id = :copper_article_id
AND art.{$copperNumberOption} != ''";
$result = $this->db->fetchAll($sql, ['copper_article_id' => $copperArticleId]);
if (!empty($result)) {
return [
'article_id' => $result[0]['article_id'],
'amount' => $this->formatToFloat($result[0]['copper_number']),
];
}
return [];
}
/**
* @param string $docType
* @param int $docTypeId
*
* @return array
*/
public function findPartListHeadArticles(string $docType, int $docTypeId): array
{
$sql =
"SELECT art.id, pos.sort, pos.id AS `pos_id`, pos.waehrung AS `currency`, pos.menge AS `amount`
FROM `{$docType}_position` AS `pos`
INNER JOIN artikel AS `art` ON art.id = pos.artikel
WHERE pos.{$docType} = :doc_type_id
AND art.stueckliste = 1";
return $this->db->fetchAll($sql, ['doc_type_id' => $docTypeId]);
}
/**
* @param $headArticleId
* @param float $amount
*
* @return array
*/
public function getAllPartListChildElements($headArticleId, float $amount = 1.0): array
{
$result = [];
$sql =
"SELECT art.id, art.stueckliste, partlist.menge AS `amount`
FROM `artikel` AS `art`
INNER JOIN `stueckliste` AS `partlist` ON art.id = partlist.artikel
WHERE partlist.stuecklistevonartikel = :head_article_id";
$datas = $this->db->fetchAll($sql, ['head_article_id' => $headArticleId]);
foreach ($datas as $data) {
if (!empty($data['stueckliste'])) {
$result = array_merge(
$result,
$this->getAllPartListChildElements((int)$data['id'], (float)$data['amount'])
);
} else {
$result[] = [
'id' => $data['id'],
'amount' => $data['amount'] * $amount,
];
}
}
return $result;
}
/**
* @param int $articleId
* @param string $copperNumberOption
*
* @return float
*/
public function getArticleCopperNumber(int $articleId, string $copperNumberOption): float
{
$sql =
"SELECT a.{$copperNumberOption}
FROM `artikel` AS `a`
WHERE a.id = :article_id";
return (float)str_replace(',', '.', $this->db->fetchValue($sql, ['article_id' => $articleId]));
}
/**
* @param string $docType
* @param int $positionId
*
* @return int
*/
public function evaluatePartListLastPositionId(string $docType, int $positionId): int
{
$explodedColumnName = 'explodiert_parent';
if ($docType === 'rechnung') {
$explodedColumnName = 'explodiert_parent_artikel';
}
$sql =
"SELECT MAX(id) AS `pos_id`
FROM `{$docType}_position` AS `pos`
WHERE pos.{$explodedColumnName} = :pos_id";
$result = $this->db->fetchValue($sql, ['pos_id' => $positionId]);
if (!empty($result)) {
return (int)$result;
}
return $positionId;
}
}
@@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
namespace Xentral\Modules\CopperSurcharge\Service;
use Xentral\Components\Database\Database;
final class DocumentService
{
/** @var Database $db */
private $db;
/**
* @param Database $db
*/
public function __construct(Database $db)
{
$this->db = $db;
}
/**
* @param string $docType
* @param int $docId
* @param int $precedingPositionId
* @param int $followingPositionId
*/
public function updatePositionSort(
string $docType,
int $docId,
int $precedingPositionId,
int $followingPositionId
): void {
$sql =
"SELECT `sort`
FROM `" . $docType . "_position`
WHERE `id` = :preceding_position_id
LIMIT 1";
$precedingSort = $this->db->fetchValue($sql, ['preceding_position_id' => $precedingPositionId]);
$this->db->perform(
"UPDATE `" . $docType . "_position`
SET `sort` = `sort` + 1
WHERE `sort` > :preceding_sort
AND " . $docType . " = :doc_id",
['preceding_sort' => $precedingSort, 'doc_id' => $docId]
);
$this->db->perform(
"UPDATE `beleg_zwischenpositionen`
SET `pos` = `pos` + 1
WHERE `doctype` = :doc_type
AND `doctypeid` = :doc_id
AND `pos` >= :preceding_sort",
[
'doc_type' => $docType,
'doc_id' => $docId,
'preceding_sort' => $precedingSort,
]
);
$this->db->perform(
"UPDATE `" . $docType . "_position`
SET `sort` = :preceding_sort + 1
WHERE `id` = :following_position_id",
['following_position_id' => $followingPositionId, 'preceding_sort' => $precedingSort]
);
}
/**
* @param int $betweenId
* @param int $betweenSort
*/
public function updateBetweenSort(int $betweenId, int $betweenSort): void
{
$this->db->perform(
"UPDATE `beleg_zwischenpositionen`
SET `pos` = :between_pos
WHERE `id` = :between_id",
[
'between_pos' => $betweenSort,
'between_id' => $betweenId,
]
);
}
/**
* @param string $docType
* @param int $docId
* @param int $copperSurchargeArticleId
*/
public function deleteCopperSurchargePositions(string $docType, int $docId, int $copperSurchargeArticleId): void
{
$sql =
"DELETE
FROM `" . $docType . "_position`
WHERE `artikel` = :copper_surcharge_article_id
AND `" . $docType . "` = :doc_id";
$this->db->perform($sql, ['copper_surcharge_article_id' => $copperSurchargeArticleId, 'doc_id' => $docId]);
}
/**
* @param string $docType
* @param int $docId
*/
public function updatePositionSorts(string $docType, int $docId): void
{
$sql =
"SELECT pos.id, pos.sort
FROM `" . $docType . "_position` AS pos
WHERE " . $docType . " = :doc_id
ORDER BY pos.sort";
$positions = $this->db->fetchAll($sql, ['doc_id' => $docId]);
if (!empty($positions)) {
foreach ($positions as $key => $position) {
$sql =
"UPDATE `" . $docType . "_position` SET `sort` = :sort WHERE `id` = :pos_id";
$this->db->perform($sql, ['sort' => $key + 1, 'pos_id' => $position['id']]);
}
}
}
/**
* @param string $docType
* @param $positionId
* @param float $contributionMargin
*/
public function updatePositionContributionMargin(string $docType, $positionId, float $contributionMargin)
{
$sql =
"UPDATE `" . $docType . "_position`
SET `deckungsbeitrag` = :contribution_margin
WHERE `id` = :position_id";
$this->db->perform($sql, ['contribution_margin' => $contributionMargin, 'position_id' => $positionId]);
}
/**
* @param string $docType
* @param int $posId
* @param float $purchasePrice
*/
public function updatePositionPurchasePrice(string $docType, int $posId, float $purchasePrice): void
{
$sql =
"UPDATE `" . $docType . "_position`
SET `einkaufspreis` = :purchase_price
WHERE `id` = :position_id";
$this->db->perform($sql, ['purchase_price' => $purchasePrice, 'position_id' => $posId]);
}
}
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Xentral\Modules\CopperSurcharge\Service;
use DateTimeInterface;
use Xentral\Components\Database\Database;
use Xentral\Modules\CopperSurcharge\Exception\EmptyResultException;
final class PurchasePriceGateway
{
/** @var Database $db */
private $db;
/**
* @param Database $db
*/
public function __construct(Database $db)
{
$this->db = $db;
}
/**
* @param DateTimeInterface $calcDate
* @param int $copperArticleId
*
* @throws EmptyResultException
*
* @return float
*/
public function getDelCopperPriceByDate(DateTimeInterface $calcDate, int $copperArticleId): float
{
$sql =
"SELECT data.price, data.valid_to FROM(
SELECT
e.preis AS `price`,
IF(
e.gueltig_bis = '0000-00-00',
CURDATE(),
e.gueltig_bis
) AS `valid_to`
FROM `einkaufspreise` AS `e`
WHERE e.artikel = :copper_article_id
) AS `data`
WHERE data.valid_to <= :calc_date
ORDER BY data.valid_to DESC
LIMIT 1";
$result = $this->db->fetchAll(
$sql,
[
'copper_article_id' => $copperArticleId,
'calc_date' => $calcDate->format('Y-m-d'),
]
);
if (empty($result)) {
$sql =
"SELECT
e.preis AS `price`
FROM `einkaufspreise` AS `e`
WHERE id = (
SELECT
MIN(e2.id)
FROM `einkaufspreise` AS `e2`
WHERE e2.artikel = :copper_article_id
)";
$result = $this->db->fetchAll(
$sql,
[
'copper_article_id' => $copperArticleId,
]
);
if (empty($result)) {
throw new EmptyResultException('No prices found for article: ' . $copperArticleId);
}
}
return (float)$result[0]['price'];
}
}
@@ -0,0 +1,213 @@
<?php
declare(strict_types=1);
namespace Xentral\Modules\CopperSurcharge\Service;
use Xentral\Components\Database\Database;
use Xentral\Modules\CopperSurcharge\Exception\EmptyResultException;
final class RawMaterialGateway
{
/** @var Database $db */
private $db;
/**
* @param Database $db
*/
public function __construct(Database $db)
{
$this->db = $db;
}
/**
* @param int $articleId
* @param int $copperArticleId
*
* @throws EmptyResultException
*
* @return float
*/
public function getRawMaterialAmount(int $articleId, int $copperArticleId): float
{
$sql =
"SELECT r.menge
FROM `rohstoffe` AS `r`
WHERE r.rohstoffvonartikel = :article_id
AND r.artikel = :copper_article_id
LIMIT 1";
$result = $this->db->fetchValue($sql, ['article_id' => $articleId, 'copper_article_id' => $copperArticleId]);
if (empty($result)) {
throw new EmptyResultException('No raw material amount found for articleId: ' . $articleId);
}
return (float)$result;
}
/**
* @param int $copperArticleId
* @param int $copperSurchargeArticleId
*
* @return array
*/
public function findPossibleCopperArticle(int $copperArticleId, int $copperSurchargeArticleId): array
{
$sql =
"SELECT art.id AS `article_id`, r.menge AS `amount`
FROM `artikel` AS `art`
INNER JOIN `rohstoffe` AS `r` ON r.rohstoffvonartikel = art.id
WHERE r.rohstoffvonartikel = :copper_article_id
AND r.artikel = :copper_surcharge_article_id
AND art.rohstoffe = 1";
$result = $this->db->fetchAll(
$sql,
[
'copper_article_id' => $copperArticleId,
'copper_surcharge_article_id' => $copperSurchargeArticleId,
]
);
if (!empty($result)) {
return [
'article_id' => $result[0]['article_id'],
'amount' => (float)$result[0]['amount'],
];
}
return [];
}
/**
* @param string $docType
* @param int $docTypeId
* @param int $copperSurchargeArticleId
*
* @return array
*/
public function findPositions(
string $docType,
int $docTypeId,
int $copperSurchargeArticleId
): array {
$explodedColumnName = 'explodiert_parent';
if ($docType === 'rechnung') {
$explodedColumnName = 'explodiert_parent_artikel';
}
$sql = "SELECT DISTINCT
beleg_pos.id AS `pos_id`,
a.id AS `article_id`,
beleg_pos.waehrung AS `currency`
FROM `{$docType}_position` AS `beleg_pos`
INNER JOIN `{$docType}` AS `beleg` ON beleg.id = beleg_pos.{$docType}
INNER JOIN `artikel` AS `a` ON a.id = beleg_pos.artikel
LEFT JOIN `rohstoffe` AS `r`
ON r.rohstoffvonartikel = beleg_pos.artikel
AND r.artikel = :copper_surcharge_article_id
WHERE beleg_pos.{$docType} = :doc_type_id
AND a.rohstoffe = 1
AND beleg.schreibschutz = 0
AND beleg_pos.{$explodedColumnName} = 0
AND r.id IS NOT NULL
ORDER BY beleg_pos.sort";
$result = $this->db->fetchAll(
$sql,
[
'copper_surcharge_article_id' => $copperSurchargeArticleId,
'doc_type_id' => $docTypeId,
]
);
if (!empty($result)) {
return $result;
}
return [];
}
/**
* @param string $docType
* @param int $docId
* @param int $copperSurchargeArticleId
*
* @return bool
*/
public function hasCopperArticles(
string $docType,
int $docId,
int $copperSurchargeArticleId
): bool {
$sql = "SELECT pos.id
FROM `" . $docType . "_position` AS `pos`
INNER JOIN `rohstoffe` AS `raw`
ON raw.rohstoffvonartikel = pos.artikel
AND raw.artikel = :copper_surcharge_article_id
WHERE pos." . $docType . " = :doc_id";
return !empty(
$this->db->fetchAll(
$sql,
[
'doc_id' => $docId,
'copper_surcharge_article_id' => $copperSurchargeArticleId,
]
)
);
}
/**
* @param string $docType
* @param int $docId
* @param int $copperSurchargeArticleId
*
* @return array
*/
public function findAllPositionsForGrouped(
string $docType,
int $docId,
int $copperSurchargeArticleId
): array {
$sql =
"SELECT * FROM(
SELECT
pos.id AS `pos_id`,
pos.artikel AS `article_id`,
pos.waehrung AS `currency`,
pos.sort AS `sort`,
IF(raw.id IS NULL,0,1) AS `is_copper`,
1 AS `pos_type`,
'' AS `between_type`,
0 AS `between_id`
FROM `" . $docType . "_position` AS `pos`
LEFT JOIN `rohstoffe` AS `raw`
ON raw.rohstoffvonartikel = pos.artikel
AND raw.artikel = :copper_surcharge_article_id
WHERE " . $docType . " = :doc_id
UNION
SELECT
0 AS `pos_id`,
0 AS `article_id`,
'' AS `currency`,
z.pos AS `sort`,
0 AS `is_copper`,
2 AS `pos_type`,
z.postype AS `between_type`,
z.id AS `between_id`
FROM `beleg_zwischenpositionen` AS `z`
WHERE z.doctype = :doc_type
AND z.doctypeid = :doc_id
) AS `data`
ORDER BY data.sort, data.pos_type";
return $this->db->fetchAll(
$sql,
[
'doc_id' => $docId,
'doc_type' => $docType,
'copper_surcharge_article_id' => $copperSurchargeArticleId,
]
);
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Xentral\Modules\CopperSurcharge\Wrapper;
use erpAPI;
final class CompanyDataWrapper
{
/** @var erpAPI $erp */
private $erp;
/**
* @param erpAPI $erp
*/
public function __construct(erpAPI $erp)
{
$this->erp = $erp;
}
/**
* @param string $fieldName
*
* @return string
*/
public function getCompanyData(string $fieldName): string
{
return (string)$this->erp->Firmendaten($fieldName);
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Xentral\Modules\CopperSurcharge\Wrapper;
use erpAPI;
use Xentral\Components\Database\Database;
final class DocumentPositionWrapper implements DocumentPositionWrapperInterface
{
/** @var erpAPI $erp */
private $erp;
/** @var Database $db */
private $db;
/**
* @param erpAPI $erp
* @param Database $db
*/
public function __construct(erpAPI $erp, Database $db)
{
$this->erp = $erp;
$this->db = $db;
}
/**
* @param string $doctype
* @param int $docId
* @param int $articleId
* @param array $articleData
* @param float $amount
* @param float $price
* @param string $currency
* @param string $description
*
* @return int
*/
public function addPositionManuallyWithPrice(
string $doctype,
int $docId,
int $articleId,
array $articleData,
float $amount,
float $price,
string $currency = 'EUR',
string $description = ''
): int {
$posId = $this->erp->AddPositionManuellPreis(
$doctype,
$docId,
$articleId,
$amount,
$articleData['name_de'],
$price,
$articleData['vat'],
$articleData['discount'],
$currency,
$description
);
if (empty($posId)) {
return 0;
}
return (int)$posId;
}
}
@@ -0,0 +1,29 @@
<?php
namespace Xentral\Modules\CopperSurcharge\Wrapper;
interface DocumentPositionWrapperInterface
{
/**
* @param string $doctype
* @param int $docId
* @param int $articleId
* @param array $articleData
* @param float $amount
* @param float $price
* @param string $currency
* @param string $description
*
* @return int
*/
public function addPositionManuallyWithPrice(
string $doctype,
int $docId,
int $articleId,
array $articleData,
float $amount,
float $price,
string $currency = 'EUR',
string $description = ''
): int;
}
@@ -0,0 +1,94 @@
###To find under Apps -> Master data -> Kupferzuschlag
The modul adds extra positions to business documents which contain articles with copper.
The purchase prices of these surcharge articles can be edited quickly with the module 'Tagespreise' (index.php?module=tagespreise)
Theses purchase prices represent the DEL-values (DEL stands for Deutsches Elektrolytkupfer für Leitzwecke)
###Preparation:
The following values get mangaged in the module:
- copper surcharge article (Kupferzuschlagsartikel)
an article which gets addes as surcharge position in all business documents
- Add position (Position einfügen)
There are three posibilities how surcharge articles can be added as positions:
- a position gets added for every article which is a copper article
- only one position gets added for all copper articles
- for every group a surcharge position gets added
- Copper surcharge - offer to order (Kupferzuschlag - Angebot zu Auftrag)
two posibilities:
- DEL from offer date: if an offer exists the date from it will be used to evaluate the DEL. If there is no offer the present day is used
- DEL from order date: the date from the order is used
- coppersurcharge - create invoice (Kupferzuschlag Rechnung erstellen)
four posibilities:
- DEL from order date: date from order, if not found present day
- DEL from order delivery date: delivery date, if not found present day
- DEL from invoice date: date from invoice
- DEL from offer date: offer date, if not found present day
- Where should the values get specified (Wie sollen die Daten gespeichert werden?)
There are two posibilities to save the data needed to specify a copper article:
By the app raw materials (Rohstoffe) or with additional fields in articles (managed in (Grundeinstellungen -> Freifelder))
- article specific copper number (kg/km) (Artikelspezifische Kupferzahl (kg/km))
this field only appears when 'with additional article fields is seleted'. Only fields which are already set in the 'Grundeinstellungen' appear
- delivery costs (in percent) (Bezugskosten (in Prozent))
The current calculation includes always a delivery cost addition. Normally this is 1% for all articles
- standard copper base (Standard Kupferbasis (in EUR pro 100kg))
The current calculation also includes this value, default is 150
- article specific copper base (Artikelspezifische Kupferbasis (in EUR pro 100kg))
if the value from the field before differs in an article another additional field in the article can be assigned here
####Create copper surcharge article:
New surcharge positions need an article as base. Therefore an new one must be created.
This article must be marked as daily price article (the checkbox 'Daily prices'(Tagespreise))
In Artikelbeschreibung (DE) several placeholders can be used to get replaced in the position:
{ARTIKELNUMMER}, {ARTIKELNAME}, {NETPRICE} the net price for the surcharge, {COPPERBASIS} the copper base (see calculation), {COPPERNUMBER} the copper weight (kg/km) (see calculation), {DELVALUE} the DEL value
####Mark an Article as copper base:
Dependinfg on how the module should work (raw materials or additional article fields) the article must be marked as 'Raw material list' (Rohstoffliste) or the additional field(s) must be filled
If raw materials are used, switch to teh slider 'raw materials' (Rohstoffe) and create a new entry with the surcharge article as article and amount as copper weight (kg/km)
####Daily Prices:
in the module daily prices (Tagespreise - index.php?module=tagespreise ), in the slider configuration select the surcharge article in one of the seven possible rows and add a name
In the slider 'overview' (Übersicht) you now can add the newest DEL values every day. DEL values can be found here: http://www.del-notiz.org/ - neu ausgegeben
If a new daily price is added an an older one is existing the old will be changed to invalid and the new one gets added
####Price calculation:
Copper surcharge EUR/km = (copper weight (kg/km) * (DEL + 1% delivery costs)) - copper base / 100
Example:
copper weight/km: 13,00 kg
copper base: 150,00 EUR/100 kg
DEL: 550,00 EUR/100 kg
13 * ((550,00 + (550,00 * 0.01)) - 150,00) / 100 = 52,72 EUR/km
###How the code works:
The logic starts its work by calling the hooks 'ANABREGSNeuberechnen_1' (adding positions)
and 'ANABREGSNeuberechnenEnde' (updating some unneccesary values which are filled between the hooks).
Because there is no relation between different positions of on business document, I always delete all
surcharge positions and create new ones. Time will show if this is not too much and if it wouldn't be better
to add a relation table for positions and their dependencies.
After deleting these positions the code starts collecting all necessary copper positions.
It differentiates between regular articles and part list articles.
The main difference between these types of articles is, that part list elements with copper get always grouped.
After that the prices get calculated and depending on the settings the positions get added.
@@ -0,0 +1,52 @@
var CopperSurcharge = (function ($) {
'use strict';
var me = {
isInitialized: false,
selector: {
optional: '.surcharge-optional',
maintenanceType: 'surcharge-maintenance-type'
}
, init: function (){
me.checkOptional();
me.registerEvents();
}
, registerEvents: function () {
$('input[name=\''+me.selector.maintenanceType+'\']').on('click', me.selector.clickClass,
function (event) {
let maintenanceType = $(this).val();
me.showHideOptional(maintenanceType);
});
}
, checkOptional: function (){
let maintenanceType = $('input[name=\''+me.selector.maintenanceType+'\']:checked').val();
me.showHideOptional(maintenanceType);
}
,showHideOptional: function(maintenanceType){
if(maintenanceType === '1'){
$(me.selector.optional).show();
}
else {
$(me.selector.optional).hide();
}
}
};
return {
init: me.init
};
})(jQuery);
$(document).ready(function () {
CopperSurcharge.init();
});