mirror of
https://github.com/Karaka-Management/oms-Billing.git
synced 2026-01-27 23:08:41 +00:00
update
This commit is contained in:
parent
9f400de255
commit
47651186c8
|
|
@ -123,7 +123,7 @@ $pdf->MultiCell(
|
|||
. $lang[$pdf->language]['InvoiceDate'] . "\n"
|
||||
. $lang[$pdf->language]['ServiceDate'] . "\n"
|
||||
. $lang[$pdf->language]['CustomerNo'] . "\n"
|
||||
. $lang[$pdf->language]['PO'] . "\n"
|
||||
. $lang[$pdf->language]['REF'] . "\n"
|
||||
. $lang[$pdf->language]['DueDate'],
|
||||
0, 'L'
|
||||
);
|
||||
|
|
@ -138,7 +138,7 @@ $pdf->MultiCell(
|
|||
. ($bill->billDate?->format('Y-m-d') ?? '0') . "\n"
|
||||
. ($bill->performanceDate?->format('Y-m-d') ?? '0') . "\n"
|
||||
. $bill->accountNumber . "\n"
|
||||
. '' . "\n" /* @todo implement customer / supplier reference as string */
|
||||
. $bill->externalReferral . "\n"
|
||||
. ($bill->billDate?->format('Y-m-d') ?? '0'), /* Consider to add dueDate in addition */
|
||||
0, 'L'
|
||||
);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ return [
|
|||
'InvoiceDate' => 'Invoice Date',
|
||||
'ServiceDate' => 'Service Date',
|
||||
'CustomerNo' => 'Customer No.',
|
||||
'PO' => 'PO',
|
||||
'REF' => 'REF',
|
||||
'DueDate' => 'Due Date',
|
||||
'Item' => 'Item',
|
||||
'Currency' => 'Currency',
|
||||
|
|
@ -40,7 +40,7 @@ return [
|
|||
'InvoiceDate' => 'Belegdatum',
|
||||
'ServiceDate' => 'Leistungsdatum',
|
||||
'CustomerNo' => 'Kundennummer',
|
||||
'PO' => 'Kundenreferenz',
|
||||
'REF' => 'REF',
|
||||
'DueDate' => 'Fälligkeitsdatum',
|
||||
'Item' => 'Artikel',
|
||||
'Currency' => 'Währung',
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@
|
|||
"l11n": {
|
||||
"en": {
|
||||
"subject": "Billing",
|
||||
"body": "<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Billing</title></head><body style=\"font-family: Arial, sans-serif; font-size: 16px; line-height: 1.5;\"><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\"><tr><td><table align=\"center\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"600\" style=\"margin: 0 auto; background-color: #ffffff;\"><tr><td style=\"padding: 40px 0; text-align: center;\"><h1 style=\"margin-top: 0; color: #000000; font-size: 24px;\">Billing</h1><p style=\"margin-bottom: 20px;\">Dear {user_name},</p><p style=\"margin-bottom: 20px;\">Thank you for for doing business with us.</p><p style=\"margin-bottom: 20px;\">Attached kindly find your bill.</p><p style=\"margin-top: 40px;\">Jingga e.K. - www.jingga.app - CEO Dennis Eichhorn - Amtsgericht Friedberg HRA 5058</p></td></tr></table></td></tr></table></body></html>",
|
||||
"body": "<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Billing</title></head><body style=\"font-family: Arial, sans-serif; font-size: 14px; line-height: 1.5;\"><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\"><tr><td><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"600\" style=\"margin: 0 auto; background-color: #ffffff;\"><tr><td style=\"padding: 40px 0;\"><h1 style=\"margin-top: 0; color: #000000; font-size: 24px; text-align: center;\">Billing</h1><p style=\"margin-bottom: 20px;\">Dear {user_name},</p><p style=\"margin-bottom: 20px;\">Thank you for for doing business with us.</p><p style=\"margin-bottom: 20px;\">Attached kindly find your bill.</p><p style=\"margin-top: 40px;\">Jingga e.K. - www.jingga.app - CEO Dennis Eichhorn - Amtsgericht Friedberg HRA 5058</p></td></tr></table></td></tr></table></body></html>",
|
||||
"bodyalt": "Dear {user_name},\n\nThank you for doing business with us.\n\nAttached kindly find your bill.\n\n\nJingga e.K. - www.jingga.app - CEO Dennis Eichhorn - Amtsgericht Friedberg HRA 5058"
|
||||
},
|
||||
"de": {
|
||||
"subject": "Rechnungsstellung",
|
||||
"body": "<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Abrechnung</title></head><body style=\"font-family: Arial, sans-serif; font-size: 16px; line-height: 1.5;\"><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\"><tr><td><table align=\"center\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"600\" style=\"margin: 0 auto; background-color: #ffffff;\"><tr><td style=\"padding: 40px 0; text-align: center;\"><h1 style=\"margin-top: 0; color: #000000; font-size: 24px;\">Abrechnung</h1><p style=\"margin-bottom: 20px;\">Sehr geehrte/r {user_name},</p><p style=\"margin-bottom: 20px;\">Vielen Dank für Ihre Geschäftsbeziehung mit uns.</p><p style=\"margin-bottom: 20px;\">Im Anhang finden Sie Ihre Rechnung.</p><p style=\"margin-top: 40px;\">Jingga e.K. - www.jingga.app - CEO Dennis Eichhorn - Amtsgericht Friedberg HRA 5058</p></td></tr></table></td></tr></table></body></html>",
|
||||
"body": "<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Abrechnung</title></head><body style=\"font-family: Arial, sans-serif; font-size: 14px; line-height: 1.5;\"><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\"><tr><td><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"600\" style=\"margin: 0 auto; background-color: #ffffff;\"><tr><td style=\"padding: 40px 0;\"><h1 style=\"margin-top: 0; color: #000000; font-size: 24px; text-align: center;\">Abrechnung</h1><p style=\"margin-bottom: 20px;\">Sehr geehrte/r {user_name},</p><p style=\"margin-bottom: 20px;\">Vielen Dank für Ihre Geschäftsbeziehung mit uns.</p><p style=\"margin-bottom: 20px;\">Im Anhang finden Sie Ihre Rechnung.</p><p style=\"margin-top: 40px;\">Jingga e.K. - www.jingga.app - CEO Dennis Eichhorn - Amtsgericht Friedberg HRA 5058</p></td></tr></table></td></tr></table></body></html>",
|
||||
"bodyalt": "Sehr geehrte/r {user_name},\n\nvielen Dank für Ihre Geschäftsbeziehung mit uns.\n\nIm Anhang finden Sie freundlicherweise Ihre Rechnung.\n\n\nJingga e.K. - www.jingga.app - CEO Dennis Eichhorn - Amtsgericht Friedberg HRA 5058"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -347,9 +347,6 @@ final class Installer extends InstallerAbstract
|
|||
/** @var \Modules\Billing\Controller\ApiBillTypeController $module */
|
||||
$module = $app->moduleManager->getModuleInstance('Billing', 'ApiBillType');
|
||||
|
||||
// @todo allow multiple alternative bill templates
|
||||
// @todo implement ordering of templates
|
||||
|
||||
foreach ($types as $type) {
|
||||
$response = new HttpResponse();
|
||||
$request = new HttpRequest();
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ use Modules\ItemManagement\Models\ItemMapper;
|
|||
use Modules\ItemManagement\Models\NullContainer;
|
||||
use Modules\Media\Models\CollectionMapper;
|
||||
use Modules\Media\Models\Media;
|
||||
use Modules\Media\Models\MediaClass;
|
||||
use Modules\Media\Models\MediaMapper;
|
||||
use Modules\Media\Models\NullCollection;
|
||||
use Modules\Media\Models\PathSettings;
|
||||
|
|
@ -117,6 +118,16 @@ final class ApiBillController extends Controller
|
|||
|
||||
/** @var \Modules\Billing\Models\Bill $old */
|
||||
$old = BillMapper::get()->where('id', (int) $request->getData('bill'))->execute();
|
||||
|
||||
// @feature Allow to update internal statistical fields
|
||||
// Example: Referral account
|
||||
if ($old->status === BillStatus::ARCHIVED) {
|
||||
$response->header->status = RequestStatusCode::R_423;
|
||||
$this->createInvalidUpdateResponse($request, $response, $val);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$new = $this->updateBillFromRequest($request, clone $old);
|
||||
|
||||
$this->updateModel($request->header->account, $old, $new, BillMapper::class, 'bill', $request->getOrigin());
|
||||
|
|
@ -229,23 +240,27 @@ final class ApiBillController extends Controller
|
|||
*
|
||||
* @return Bill The new Bill object with default values
|
||||
*
|
||||
* @todo Validate VAT before creation (maybe need to add a status when last validated, we don't want to validate every time)
|
||||
* @todo Set the correct date of payment
|
||||
* @todo Validate VAT before creation
|
||||
* Maybe needs to add a status when last validated, we don't want to validate every time
|
||||
* https://github.com/Karaka-Management/oms-Billing/issues/44
|
||||
*
|
||||
* @todo Use bill and shipping address instead of main address if available
|
||||
*
|
||||
* @todo Implement allowed invoice languages and a default invoice language if none match
|
||||
*
|
||||
* @todo Implement client invoice language (allowing for different invoice languages than invoice address)
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function createBaseBill(Client | Supplier $account, RequestAbstract $request) : Bill
|
||||
{
|
||||
// @todo validate vat before creation for clients
|
||||
$bill = new Bill();
|
||||
$bill->createdBy = new NullAccount($request->header->account);
|
||||
$bill->unit = $account->unit ?? $this->app->unitId;
|
||||
$bill->billDate = new \DateTime('now'); // @todo Date of payment
|
||||
$bill->performanceDate = $request->getDataDateTime('performancedate') ?? new \DateTime('now'); // @todo Date of payment
|
||||
$bill->billDate = $request->getDataDateTime('bill_date') ?? new \DateTime('now');
|
||||
$bill->performanceDate = $request->getDataDateTime('performancedate') ?? new \DateTime('now');
|
||||
$bill->accountNumber = $account->number;
|
||||
$bill->externalReferral = $request->getDataString('externalreferral') ?? '';
|
||||
$bill->status = BillStatus::tryFromValue($request->getDataInt('status')) ?? BillStatus::DRAFT;
|
||||
|
||||
$bill->shippingTerms = null;
|
||||
|
|
@ -254,6 +269,10 @@ final class ApiBillController extends Controller
|
|||
$bill->paymentTerms = null;
|
||||
$bill->paymentText = '';
|
||||
|
||||
// @todo Handle payment due
|
||||
// Careful, there can be multiple due dates
|
||||
// Example: payment plan or discounted and none-discounted date
|
||||
|
||||
if ($account instanceof Client) {
|
||||
$bill->client = $account;
|
||||
$bill->accTaxCode = empty($temp = $bill->client->getAttribute('sales_tax_code')->value->id) ? null : $temp;
|
||||
|
|
@ -406,7 +425,7 @@ final class ApiBillController extends Controller
|
|||
$item,
|
||||
$taxCombination,
|
||||
FloatInt::toInt($request->getDataString('quantity') ?? 1),
|
||||
$bill->id,
|
||||
$bill,
|
||||
$container
|
||||
);
|
||||
|
||||
|
|
@ -540,7 +559,7 @@ final class ApiBillController extends Controller
|
|||
|
||||
$uploaded = [];
|
||||
if (!empty($uploadedFiles = $request->files)) {
|
||||
$uploaded = $this->app->moduleManager->get('Media')->uploadFiles(
|
||||
$uploaded = $this->app->moduleManager->get('Media', 'Api')->uploadFiles(
|
||||
names: [],
|
||||
fileNames: [],
|
||||
files: $uploadedFiles,
|
||||
|
|
@ -603,20 +622,20 @@ final class ApiBillController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
if (!empty($mediaFiles = $request->getDataJson('media'))) {
|
||||
foreach ($mediaFiles as $media) {
|
||||
$this->createModelRelation(
|
||||
$request->header->account,
|
||||
$bill->id,
|
||||
(int) $media,
|
||||
BillMapper::class,
|
||||
'files',
|
||||
'',
|
||||
$request->getOrigin()
|
||||
);
|
||||
}
|
||||
$mediaFiles = $request->getDataJson('media');
|
||||
foreach ($mediaFiles as $media) {
|
||||
$this->createModelRelation(
|
||||
$request->header->account,
|
||||
$bill->id,
|
||||
(int) $media,
|
||||
BillMapper::class,
|
||||
'files',
|
||||
'',
|
||||
$request->getOrigin()
|
||||
);
|
||||
}
|
||||
|
||||
// @todo media should be an array of NullMedia elements
|
||||
$this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Media', 'Media added to bill.', [
|
||||
'upload' => $uploaded,
|
||||
'media' => $mediaFiles,
|
||||
|
|
@ -638,7 +657,6 @@ final class ApiBillController extends Controller
|
|||
*/
|
||||
public function apiMediaRemoveFromBill(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
|
||||
{
|
||||
// @todo check that it is not system generated media!
|
||||
if (!empty($val = $this->validateMediaRemoveFromBill($request))) {
|
||||
$response->header->status = RequestStatusCode::R_400;
|
||||
$this->createInvalidDeleteResponse($request, $response, $val);
|
||||
|
|
@ -652,20 +670,35 @@ final class ApiBillController extends Controller
|
|||
/** @var \Modules\Billing\Models\Bill $bill */
|
||||
$bill = BillMapper::get()->where('id', (int) $request->getData('bill'))->execute();
|
||||
|
||||
$path = $this->createBillDir($bill);
|
||||
// Cannot delete system generated bill
|
||||
if (\stripos($media->name, $bill->number) !== false) {
|
||||
$response->header->status = RequestStatusCode::R_423;
|
||||
$this->createInvalidDeleteResponse($request, $response, $media);
|
||||
|
||||
/** @var \Modules\Media\Models\Collection[] */
|
||||
$billCollection = CollectionMapper::getAll()
|
||||
->where('virtual', $path)
|
||||
->execute();
|
||||
|
||||
if (\count($billCollection) !== 1) {
|
||||
// @todo For some reason there are multiple collections with the same virtual path?
|
||||
// @todo check if this is the correct way to handle it or if we need to make sure that it is a collection
|
||||
return;
|
||||
}
|
||||
|
||||
$collection = \reset($billCollection);
|
||||
$path = \dirname($this->createBillDir($bill));
|
||||
|
||||
/** @var \Modules\Media\Models\Collection $collection */
|
||||
$collection = CollectionMapper::get()
|
||||
->where('name', (string) $bill->id)
|
||||
->where('virtual', $path)
|
||||
->where('class', MediaClass::COLLECTION)
|
||||
->limit(1)
|
||||
->execute();
|
||||
|
||||
if ($collection->id !== 0) {
|
||||
$this->deleteModelRelation(
|
||||
$request->header->account,
|
||||
$collection->id,
|
||||
$media->id,
|
||||
CollectionMapper::class,
|
||||
'sources',
|
||||
'',
|
||||
$request->getOrigin()
|
||||
);
|
||||
}
|
||||
|
||||
$this->deleteModelRelation(
|
||||
$request->header->account,
|
||||
|
|
@ -677,23 +710,9 @@ final class ApiBillController extends Controller
|
|||
$request->getOrigin()
|
||||
);
|
||||
|
||||
$this->deleteModelRelation(
|
||||
$request->header->account,
|
||||
$collection->id,
|
||||
$media->id,
|
||||
CollectionMapper::class,
|
||||
'sources',
|
||||
'',
|
||||
$request->getOrigin()
|
||||
);
|
||||
|
||||
// Check if media referenced by other media except the parent collection
|
||||
$referenceCount = MediaMapper::countInternalReferences($media->id);
|
||||
|
||||
if ($referenceCount === 0) {
|
||||
// Is not used anywhere else -> remove from db and file system
|
||||
|
||||
// @todo remove media types from media
|
||||
|
||||
if ($referenceCount === 1) {
|
||||
$this->deleteModel($request->header->account, $media, MediaMapper::class, 'bill_media', $request->getOrigin());
|
||||
|
||||
if (\is_dir($media->getAbsolutePath())) {
|
||||
|
|
@ -797,11 +816,9 @@ final class ApiBillController extends Controller
|
|||
$element = $this->createBillElementFromRequest($request, $response, $old, $data);
|
||||
$this->createModel($request->header->account, $element, BillElementMapper::class, 'bill_element', $request->getOrigin());
|
||||
|
||||
// @todo handle stock transaction here
|
||||
// @todo if transaction fails don't update below and send warning to user
|
||||
// @todo however mark transaction as reserved and only update when bill is finalized!!!
|
||||
|
||||
// @todo in BillElementUpdate do the same
|
||||
// @todo Handle stock transaction here
|
||||
// If the transaction fails don't perform the update below
|
||||
// The same goes for BillElementUpdate
|
||||
|
||||
$new = clone $old;
|
||||
$new->addElement($element);
|
||||
|
|
@ -1076,6 +1093,9 @@ final class ApiBillController extends Controller
|
|||
// Handle PDF generation
|
||||
$templateId = $request->getDataInt('bill_template') ?? $bill->type->defaultTemplate?->id ?? 0;
|
||||
|
||||
// @todo It would be nice if we could somehow make the two settings calls below in one go.
|
||||
// Maybe always make with unit if defined AND with null (maybe also with app?)
|
||||
// Then return none-empty strictest match
|
||||
/** @var \Model\Setting[] $settings */
|
||||
$settings = $this->app->appSettings->get(null,
|
||||
[
|
||||
|
|
@ -1175,7 +1195,9 @@ final class ApiBillController extends Controller
|
|||
);
|
||||
|
||||
// Send bill via email
|
||||
// @todo maybe not all bill types, and bill status (e.g. deleted should not be sent)
|
||||
// @bug Not all bills should be sent as email
|
||||
// Depends on bill type and status (i.e. draft, deleted)
|
||||
// https://github.com/Karaka-Management/oms-Billing/issues/50
|
||||
$client = ClientMapper::get()
|
||||
->with('account')
|
||||
->with('attributes')
|
||||
|
|
@ -1186,9 +1208,10 @@ final class ApiBillController extends Controller
|
|||
->execute();
|
||||
|
||||
if ($client->getAttribute('bill_emails')->value->getValue() === 1) {
|
||||
// @todo should this really be a string or an ID for a contact element?
|
||||
$email = empty($tmp = $client->getAttribute('bill_email_address')->value->getValue())
|
||||
? (string) $tmp
|
||||
: $client->account->getEmail();
|
||||
? $client->account->email
|
||||
: (string) $tmp;
|
||||
|
||||
$this->sendBillEmail($media, $email, $response->header->l11n->language);
|
||||
}
|
||||
|
|
@ -1245,6 +1268,10 @@ final class ApiBillController extends Controller
|
|||
module: 'Admin'
|
||||
);
|
||||
|
||||
if (empty($emailFrom->content)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var \Model\Setting $billingTemplate */
|
||||
$billingTemplate = $this->app->appSettings->get(
|
||||
names: SettingsEnum::BILLING_CUSTOMER_EMAIL_TEMPLATE,
|
||||
|
|
@ -1263,8 +1290,6 @@ final class ApiBillController extends Controller
|
|||
$mail->addAttachment($media->getAbsolutePath(), $media->name);
|
||||
|
||||
$handler->send($mail);
|
||||
|
||||
$this->app->moduleManager->get('Billing', 'Api')->sendMail($mail);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1380,8 +1405,6 @@ final class ApiBillController extends Controller
|
|||
*
|
||||
* @return array<string, bool>
|
||||
*
|
||||
* @todo Implement API validation function
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function validateBillDelete(RequestAbstract $request) : array
|
||||
|
|
@ -1417,7 +1440,17 @@ final class ApiBillController extends Controller
|
|||
}
|
||||
|
||||
/** @var BillElement $old */
|
||||
$old = BillElementMapper::get()->where('id', (int) $request->getData('id'))->execute();
|
||||
$old = BillElementMapper::get()
|
||||
->with('bill')
|
||||
->where('id', (int) $request->getData('id'))
|
||||
->execute();
|
||||
|
||||
if ($old->bill->status === BillStatus::ARCHIVED) {
|
||||
$response->header->status = RequestStatusCode::R_423;
|
||||
$this->createInvalidUpdateResponse($request, $response, $old);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// @todo can be edited?
|
||||
// @todo adjust transfer protocols
|
||||
|
|
|
|||
|
|
@ -20,23 +20,19 @@ use Modules\Billing\Models\Price\NullPrice;
|
|||
use Modules\Billing\Models\Price\Price;
|
||||
use Modules\Billing\Models\Price\PriceMapper;
|
||||
use Modules\Billing\Models\Price\PriceType;
|
||||
use Modules\Billing\Models\Tax\TaxCombinationMapper;
|
||||
use Modules\ClientManagement\Models\Client;
|
||||
use Modules\ClientManagement\Models\ClientMapper;
|
||||
use Modules\ClientManagement\Models\NullClient;
|
||||
use Modules\Finance\Models\TaxCodeMapper;
|
||||
use Modules\ItemManagement\Models\Item;
|
||||
use Modules\ItemManagement\Models\ItemMapper;
|
||||
use Modules\ItemManagement\Models\NullItem;
|
||||
use Modules\SupplierManagement\Models\NullSupplier;
|
||||
use Modules\SupplierManagement\Models\Supplier;
|
||||
use Modules\SupplierManagement\Models\SupplierMapper;
|
||||
use phpOMS\Localization\ISO4217CharEnum;
|
||||
use phpOMS\Message\Http\RequestStatusCode;
|
||||
use phpOMS\Message\RequestAbstract;
|
||||
use phpOMS\Message\ResponseAbstract;
|
||||
use phpOMS\Stdlib\Base\FloatInt;
|
||||
use phpOMS\System\MimeType;
|
||||
|
||||
/**
|
||||
* Billing class.
|
||||
|
|
@ -74,6 +70,11 @@ final class ApiPriceController extends Controller
|
|||
->execute();
|
||||
}
|
||||
|
||||
// Get supplier
|
||||
if ($supplier === null && $request->hasData('supplier')) {
|
||||
$supplier = new NullSupplier($request->getDataInt('supplier'));
|
||||
}
|
||||
|
||||
$quantity = new FloatInt($request->getDataString('price_quantity') ?? 10000);
|
||||
|
||||
// Get all relevant prices
|
||||
|
|
@ -99,9 +100,9 @@ final class ApiPriceController extends Controller
|
|||
$queryMapper->where('clienttype', \array_unique([$request->getDataInt('client_type'), $client?->getAttribute('client_type')->value->getValue(), null]), 'IN');
|
||||
$queryMapper->where('clientcountry', \array_unique([$request->getData('client_region'), $client?->mainAddress->country, null]), 'IN');
|
||||
|
||||
$queryMapper->where('supplier', \array_unique([$request->getDataInt('supplier'), $supplier?->id, null]), 'IN');
|
||||
$queryMapper->where('supplier', \array_unique([$supplier?->id, null]), 'IN');
|
||||
$queryMapper->where('unit', \array_unique([$request->getDataInt('price_unit'), null]), 'IN');
|
||||
$queryMapper->where('type', $request->getDataInt('price_type') ?? PriceType::SALES);
|
||||
$queryMapper->where('type', $request->getDataInt('price_type') ?? (($supplier?->id ?? 0) === 0 ? PriceType::SALES : PriceType::PURCHASE));
|
||||
$queryMapper->where('currency', \array_unique([$request->getDataString('currency'), null]), 'IN');
|
||||
|
||||
// @todo implement start and end
|
||||
|
|
@ -120,7 +121,7 @@ final class ApiPriceController extends Controller
|
|||
/** @var \Modules\Billing\Models\Price\Price[] $prices */
|
||||
$prices = $queryMapper->execute();
|
||||
|
||||
// Find base price (@todo probably not a good solution)
|
||||
// Find base price
|
||||
$basePrice = null;
|
||||
foreach ($prices as $price) {
|
||||
if ($price->priceNew > 0
|
||||
|
|
@ -144,8 +145,11 @@ final class ApiPriceController extends Controller
|
|||
|
||||
$basePrice ??= new NullPrice();
|
||||
|
||||
// @todo implement prices which cannot be improved even if there are better prices available (i.e. some customer groups may not get better prices, Dentagen Beispiel)
|
||||
// alternatively set prices as 'improvable' => which whitelists a price as can be improved or 'alwaysimproces' which always overwrites other prices
|
||||
// @todo implement prices which cannot be improved even if there are better prices available
|
||||
// (i.e. some customer groups may not get better prices, Dentagen Beispiel)
|
||||
// alternatively set prices as 'improvable' => which whitelists a price as can be improved
|
||||
// or 'always_improves' which always overwrites other prices
|
||||
|
||||
// Find best price
|
||||
$bestPrice = $basePrice;
|
||||
$bestPriceValue = \PHP_INT_MAX;
|
||||
|
|
@ -219,197 +223,6 @@ final class ApiPriceController extends Controller
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Api method to find items
|
||||
*
|
||||
* @param RequestAbstract $request Request
|
||||
* @param ResponseAbstract $response Response
|
||||
* @param array $data Generic data
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function apiPricingFind(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
|
||||
{
|
||||
// Get item
|
||||
/** @var null|\Modules\ItemManagement\Models\Item $item */
|
||||
$item = null;
|
||||
if ($request->hasData('price_item')) {
|
||||
/** @var null|\Modules\ItemManagement\Models\Item $item */
|
||||
$item = ItemMapper::get()
|
||||
->with('attributes')
|
||||
->with('attributes/type')
|
||||
->with('attributes/value')
|
||||
->where('id', (int) $request->getData('price_item'))
|
||||
->execute();
|
||||
}
|
||||
|
||||
// Get account
|
||||
/** @var null|\Modules\ClientManagement\Models\Client|\Modules\SupplierManagement\Models\Supplier $account */
|
||||
$account = null;
|
||||
if ($request->hasData('client')) {
|
||||
/** @var \Modules\ClientManagement\Models\Client $client */
|
||||
$client = ClientMapper::get()
|
||||
->with('attributes')
|
||||
->with('attributes/type')
|
||||
->with('attributes/value')
|
||||
->where('id', (int) $request->getData('client'))
|
||||
->execute();
|
||||
|
||||
/** @var \Modules\ClientManagement\Models\Client */
|
||||
$account = $client;
|
||||
} else {
|
||||
/** @var \Modules\SupplierManagement\Models\Supplier $supplier */
|
||||
$supplier = SupplierMapper::get()
|
||||
->with('attributes')
|
||||
->with('attributes/type')
|
||||
->with('attributes/value')
|
||||
->where('id', (int) $request->getData('supplier'))
|
||||
->execute();
|
||||
|
||||
/** @var \Modules\SupplierManagement\Models\Supplier $account */
|
||||
$account = $supplier;
|
||||
}
|
||||
|
||||
$quantity = new FloatInt($request->getDataString('price_quantity') ?? 10000);
|
||||
|
||||
// Get all relevant prices
|
||||
$queryMapper = PriceMapper::getAll();
|
||||
|
||||
if ($request->hasData('price_name')) {
|
||||
$queryMapper->where('name', $request->getData('name'));
|
||||
}
|
||||
|
||||
$queryMapper->where('promocode', \array_unique([$request->getData('promocode'), null]), 'IN');
|
||||
|
||||
$queryMapper->where('item', \array_unique([$request->getData('item', 'int'), null]), 'IN');
|
||||
$queryMapper->where('itemsalesgroup', \array_unique([$request->getData('sales_group', 'int'), $item?->getAttribute('sales_group')->id, null]), 'IN');
|
||||
$queryMapper->where('itemproductgroup', \array_unique([$request->getData('product_group', 'int'), $item?->getAttribute('product_group')->id, null]), 'IN');
|
||||
$queryMapper->where('itemsegment', \array_unique([$request->getData('item_segment', 'int'), $item?->getAttribute('segment')->id, null]), 'IN');
|
||||
$queryMapper->where('itemsection', \array_unique([$request->getData('item_section', 'int'), $item?->getAttribute('section')->id, null]), 'IN');
|
||||
$queryMapper->where('itemtype', \array_unique([$request->getData('product_type', 'int'), $item?->getAttribute('product_type')->id, null]), 'IN');
|
||||
|
||||
$queryMapper->where('client', \array_unique([$request->getData('client', 'int'), null]), 'IN');
|
||||
$queryMapper->where('clientgroup', \array_unique([$request->getData('client_group', 'int'), $client?->getAttribute('client_group')->id, null]), 'IN');
|
||||
$queryMapper->where('clientsegment', \array_unique([$request->getData('client_segment', 'int'), $client?->getAttribute('segment')->id, null]), 'IN');
|
||||
$queryMapper->where('clientsection', \array_unique([$request->getData('client_section', 'int'), $client?->getAttribute('section')->id, null]), 'IN');
|
||||
$queryMapper->where('clienttype', \array_unique([$request->getData('client_type', 'int'), $client?->getAttribute('client_type')->id, null]), 'IN');
|
||||
$queryMapper->where('clientcountry', \array_unique([$request->getData('client_region'), $client?->mainAddress->country, null]), 'IN');
|
||||
|
||||
$queryMapper->where('supplier', \array_unique([$request->getData('supplier', 'int'), null]), 'IN');
|
||||
$queryMapper->where('unit', \array_unique([$request->getData('price_unit', 'int'), null]), 'IN');
|
||||
$queryMapper->where('type', $request->getData('price_type', 'int') ?? PriceType::SALES);
|
||||
$queryMapper->where('currency', \array_unique([$request->getData('currency', 'int'), null]), 'IN');
|
||||
|
||||
// @todo implement start and end
|
||||
|
||||
/*
|
||||
@todo implement quantity
|
||||
if ($request->hasData('price_quantity')) {
|
||||
$whereQuery = new Where();
|
||||
$whereQuery->where('quantity', (int) $request->getData('price_quantity'), '<=')
|
||||
->where('quantity', null, '=', 'OR')
|
||||
|
||||
$queryMapper->where('quantity', $whereQuery);
|
||||
}
|
||||
*/
|
||||
|
||||
/** @var \Modules\Billing\Models\Price\Price[] $prices */
|
||||
$prices = $queryMapper->execute();
|
||||
|
||||
// Find base price (@todo probably not a good solution)
|
||||
$bestBasePrice = null;
|
||||
foreach ($prices as $price) {
|
||||
if ($price->priceNew->value !== 0 && $price->priceNew === 0
|
||||
&& $price->item->id !== 0
|
||||
&& $price->itemsalesgroup->id === 0
|
||||
&& $price->itemproductgroup->id === 0
|
||||
&& $price->itemsegment->id === 0
|
||||
&& $price->itemsection->id === 0
|
||||
&& $price->itemtype->id === 0
|
||||
&& $price->client->id === 0
|
||||
&& $price->clientgroup->id === 0
|
||||
&& $price->clientsegment->id === 0
|
||||
&& $price->clientsection->id === 0
|
||||
&& $price->clienttype->id === 0
|
||||
&& $price->promocode === ''
|
||||
&& $price->priceNew->value < ($bestBasePrice?->price->value ?? \PHP_INT_MAX)
|
||||
) {
|
||||
$bestBasePrice = $price;
|
||||
}
|
||||
}
|
||||
|
||||
// @todo implement prices which cannot be improved even if there are better prices available (i.e. some customer groups may not get better prices, Dentagen Beispiel)
|
||||
// alternatively set prices as 'improvable' => which whitelists a price as can be improved or 'alwaysimproces' which always overwrites other prices
|
||||
// Find best price
|
||||
$bestPrice = null;
|
||||
$bestPriceValue = \PHP_INT_MAX;
|
||||
|
||||
foreach ($prices as $price) {
|
||||
$newPrice = $bestBasePrice?->price->value ?? \PHP_INT_MAX;
|
||||
|
||||
if ($price->priceNew->value < $newPrice) {
|
||||
$newPrice = $price->priceNew->value;
|
||||
}
|
||||
|
||||
// Calculate the price EFFECT (this is the theoretical unit price)
|
||||
// 1. subtract discount value
|
||||
// 2. subtract discount percentage
|
||||
// 3. subtract bonus effect
|
||||
|
||||
$newPrice -= $price->discount->value;
|
||||
$newPrice = (int) ((1000000 - $price->discountPercentage->value) / 1000000 * $newPrice);
|
||||
$newPrice = (int) ($newPrice - $price->bonus->value / 10000 * $price->priceNew->value / $quantity->value);
|
||||
|
||||
// @todo If a customer receives 1+1 but purchases 2, then he gets 2+2 (if multiply === true) which is better than 1+1 with multiply false.
|
||||
// Same goes for amount discounts?
|
||||
|
||||
if ($newPrice < $bestPriceValue) {
|
||||
$bestPriceValue = $newPrice;
|
||||
$bestPrice = $price;
|
||||
}
|
||||
}
|
||||
|
||||
// Actual price calculation
|
||||
$bestActualPrice = $bestBasePrice?->price->value ?? \PHP_INT_MAX;
|
||||
$bestActualPrice -= $bestPrice->discount->value;
|
||||
|
||||
// @todo now perform subtractive improvements (e.g. promocodes are often subtractive)
|
||||
|
||||
// Get tax definition
|
||||
/** @var \Modules\Billing\Models\Tax\TaxCombination $tax */
|
||||
$tax = ($request->getDataInt('price_type') ?? PriceType::SALES) === PriceType::SALES
|
||||
? TaxCombinationMapper::get()
|
||||
->where('itemCode', $request->getDataInt('price_item'))
|
||||
->where('clientCode', $account->getAttribute('client_code')->value->id)
|
||||
->execute()
|
||||
: TaxCombinationMapper::get()
|
||||
->where('itemCode', $request->getDataInt('price_item'))
|
||||
->where('supplierCode', $account->getAttribute('supplier_code')->value->id)
|
||||
->execute();
|
||||
|
||||
$taxCode = TaxCodeMapper::get()
|
||||
->where('abbr', $tax->taxCode)
|
||||
->execute();
|
||||
|
||||
$result = [
|
||||
'taxcode' => $taxCode->abbr,
|
||||
'grossPercentage' => $taxCode->percentageInvoice,
|
||||
'net' => $bestActualPrice,
|
||||
'taxes' => $bestActualPrice * $taxCode->percentageInvoice / 1000000,
|
||||
'gross' => $bestActualPrice + $bestActualPrice * $taxCode->percentageInvoice / 1000000,
|
||||
];
|
||||
|
||||
$response->header->set('Content-Type', MimeType::M_JSON, true);
|
||||
$response->set(
|
||||
$request->uri->__toString(),
|
||||
$result
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Api method to create item bill type
|
||||
*
|
||||
|
|
@ -530,6 +343,22 @@ final class ApiPriceController extends Controller
|
|||
$old = PriceMapper::get()->where('id', (int) $request->getData('id'))->execute();
|
||||
$new = $this->updatePriceFromRequest($request, clone $old);
|
||||
|
||||
if ($new->name === 'default'
|
||||
&& $old->priceNew->value !== $new->priceNew->value
|
||||
) {
|
||||
/** @var \Modules\ItemManagement\Models\Item $item */
|
||||
$item = ItemMapper::get()->where('id', $new->item)->execute();
|
||||
$itemNew = clone $item;
|
||||
|
||||
if ($new->type === PriceType::SALES) {
|
||||
$itemNew->salesPrice = $new->priceNew;
|
||||
} else {
|
||||
$itemNew->purchasePrice = $new->priceNew;
|
||||
}
|
||||
|
||||
$this->updateModel($request->header->account, $item, $itemNew, ItemMapper::class, 'price', $request->getOrigin());
|
||||
}
|
||||
|
||||
$this->updateModel($request->header->account, $old, $new, PriceMapper::class, 'price', $request->getOrigin());
|
||||
$this->createStandardUpdateResponse($request, $response, $new);
|
||||
}
|
||||
|
|
@ -552,7 +381,6 @@ final class ApiPriceController extends Controller
|
|||
|
||||
$new->promocode = $request->getDataString('promocode') ?? $new->promocode;
|
||||
|
||||
$new->item = $request->hasData('item') ? new NullItem((int) $request->getData('item')) : $new->item;
|
||||
$new->itemsalesgroup = $request->hasData('itemsalesgroup') ? new NullAttributeValue((int) $request->getData('itemsalesgroup')) : $new->itemsalesgroup;
|
||||
$new->itemproductgroup = $request->hasData('itemproductgroup') ? new NullAttributeValue((int) $request->getData('itemproductgroup')) : $new->itemproductgroup;
|
||||
$new->itemsegment = $request->hasData('itemsegment') ? new NullAttributeValue((int) $request->getData('itemsegment')) : $new->itemsegment;
|
||||
|
|
@ -567,10 +395,9 @@ final class ApiPriceController extends Controller
|
|||
|
||||
$new->supplier = $request->hasData('supplier') ? new NullSupplier((int) $request->getData('supplier')) : $new->supplier;
|
||||
$new->unit = $request->getDataInt('unit') ?? $new->unit;
|
||||
$new->type = PriceType::tryFromValue($request->getDataInt('type')) ?? $new->type;
|
||||
$new->quantity = $request->getDataInt('quantity') ?? $new->quantity;
|
||||
$new->price = $request->hasData('price') ? new FloatInt((int) $request->getData('price')) : $new->price;
|
||||
$new->priceNew = $request->getDataInt('price_new') ?? $new->priceNew;
|
||||
$new->price = $new->priceNew;
|
||||
$new->priceNew = $request->hasData('price_new') ? new FloatInt((int) $request->getData('price_new')) :
|
||||
$new->discount = $request->getDataInt('discount') ?? $new->discount;
|
||||
$new->discountPercentage = $request->getDataInt('discountPercentage') ?? $new->discountPercentage;
|
||||
$new->bonus = $request->getDataInt('bonus') ?? $new->bonus;
|
||||
|
|
@ -589,9 +416,6 @@ final class ApiPriceController extends Controller
|
|||
*
|
||||
* @return array<string, bool>
|
||||
*
|
||||
* @todo implement
|
||||
* @todo consider to block 'default' name
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function validatePriceUpdate(RequestAbstract $request) : array
|
||||
|
|
|
|||
|
|
@ -69,7 +69,9 @@ final class ApiPurchaseController extends Controller
|
|||
->limit(1)
|
||||
->execute();
|
||||
|
||||
$files = $request->files;
|
||||
$bills = [];
|
||||
|
||||
$files = \array_merge($request->files, $request->getDataJson('media'));
|
||||
foreach ($files as $file) {
|
||||
// Create default bill
|
||||
$billRequest = new HttpRequest();
|
||||
|
|
@ -85,32 +87,41 @@ final class ApiPurchaseController extends Controller
|
|||
$this->app->moduleManager->get('Billing', 'ApiBill')->apiBillCreate($billRequest, $billResponse, $data);
|
||||
|
||||
$billId = $billResponse->getDataArray('')['response']->id;
|
||||
$bills[] = $billId;
|
||||
|
||||
// Upload and assign document to bill
|
||||
$mediaRequest = new HttpRequest();
|
||||
$mediaResponse = new HttpResponse();
|
||||
$mediaRequest = new HttpRequest();
|
||||
|
||||
$mediaResponse->header->l11n = $response->header->l11n;
|
||||
|
||||
$mediaRequest->header->account = $request->header->account;
|
||||
$mediaRequest->header->l11n = $request->header->l11n;
|
||||
$mediaRequest->addFile($file);
|
||||
|
||||
$mediaResponse = new HttpResponse();
|
||||
$mediaResponse->header->l11n = $response->header->l11n;
|
||||
if (\is_array($file)) {
|
||||
$mediaRequest->addFile($file);
|
||||
} else {
|
||||
$mediaRequest->setData('media', \json_encode($file));
|
||||
}
|
||||
|
||||
$mediaRequest->setData('bill', $billId);
|
||||
$mediaRequest->setData('type', $originalType);
|
||||
$mediaRequest->setData('parse_content', true, true);
|
||||
$this->app->moduleManager->get('Billing', 'ApiBill')->apiMediaAddToBill($mediaRequest, $mediaResponse, $data);
|
||||
|
||||
/** @var \Modules\Media\Models\Media[] $uploaded */
|
||||
$uploaded = $mediaResponse->getDataArray('')['response']['upload'];
|
||||
if (empty($uploaded)) {
|
||||
$response->header->status = RequestStatusCode::R_400;
|
||||
throw new \Exception();
|
||||
}
|
||||
if (\is_array($file)) {
|
||||
/** @var \Modules\Media\Models\Media[] $uploaded */
|
||||
$uploaded = $mediaResponse->getDataArray('')['response']['upload'];
|
||||
if (empty($uploaded)) {
|
||||
$response->header->status = RequestStatusCode::R_400;
|
||||
throw new \Exception();
|
||||
}
|
||||
|
||||
$in = \reset($uploaded)->getAbsolutePath(); // pdf parsed content is available in $in->content
|
||||
if (!\is_file($in)) {
|
||||
$response->header->status = RequestStatusCode::R_400;
|
||||
throw new \Exception();
|
||||
$in = \reset($uploaded)->getAbsolutePath();
|
||||
if (!\is_file($in)) {
|
||||
$response->header->status = RequestStatusCode::R_400;
|
||||
throw new \Exception();
|
||||
}
|
||||
}
|
||||
|
||||
// Create internal document
|
||||
|
|
@ -134,12 +145,14 @@ final class ApiPurchaseController extends Controller
|
|||
\escapeshellarg($cliPath)
|
||||
. ' /billing/bill/purchase/parse '
|
||||
. '-i ' . \escapeshellarg((string) $billId),
|
||||
true
|
||||
$request->getDataBool('async') ?? true
|
||||
);
|
||||
} catch (\Throwable $t) {
|
||||
$response->header->status = RequestStatusCode::R_400;
|
||||
$this->app->logger->error($t->getMessage());
|
||||
}
|
||||
|
||||
$this->createStandardCreateResponse($request, $response, $bills);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -465,8 +465,6 @@ final class ApiTaxController extends Controller
|
|||
*
|
||||
* @return array<string, bool>
|
||||
*
|
||||
* @todo Implement API validation function
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function validateTaxCombinationDelete(RequestAbstract $request) : array
|
||||
|
|
|
|||
|
|
@ -315,7 +315,8 @@ final class CliController extends Controller
|
|||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @todo What about multiple tax lines?
|
||||
* @todo Handle multiple tax lines
|
||||
* Example: 19% and 7%
|
||||
*/
|
||||
private function findBillTaxAmount(array $lines, array $matches) : int
|
||||
{
|
||||
|
|
|
|||
|
|
@ -247,7 +247,7 @@ class Bill implements \JsonSerializable
|
|||
*/
|
||||
public Account $referral;
|
||||
|
||||
public string $referralName = '';
|
||||
public string $externalReferral = '';
|
||||
|
||||
/**
|
||||
* Net amount.
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@ class BillElement implements \JsonSerializable
|
|||
* @param Item $item Item
|
||||
* @param TaxCode $taxCode Tax code used for gross amount calculation
|
||||
* @param int $quantity Quantity
|
||||
* @param int $bill Bill
|
||||
* @param Bill $bill Bill
|
||||
*
|
||||
* @return self
|
||||
*
|
||||
|
|
@ -293,12 +293,12 @@ class BillElement implements \JsonSerializable
|
|||
Item $item,
|
||||
TaxCombination $taxCombination,
|
||||
int $quantity = 10000,
|
||||
int $bill = 0,
|
||||
Bill $bill = null,
|
||||
?Container $container = null
|
||||
) : self
|
||||
{
|
||||
$element = new self();
|
||||
$element->bill = new NullBill($bill);
|
||||
$element->bill = $bill;
|
||||
$element->item = empty($item->id) ? null : $item;
|
||||
$element->container = empty($container->id) ? null : $container;
|
||||
$element->itemNumber = $item->number;
|
||||
|
|
@ -324,7 +324,7 @@ class BillElement implements \JsonSerializable
|
|||
$element->subscription = new Subscription();
|
||||
$element->subscription->bill = $element->bill->id;
|
||||
$element->subscription->item = $element->item->id;
|
||||
$element->subscription->start = new \DateTime('now'); // @todo change to bill performanceDate
|
||||
$element->subscription->start = $bill?->performanceDate ?? new \DateTime('now');
|
||||
$element->subscription->end = (new SmartDateTime('now'))->smartModify(m: 1); // @todo depends on subscription type
|
||||
|
||||
$element->subscription->quantity = $element->quantity;
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ class BillMapper extends DataMapperFactory
|
|||
'billing_bill_currency' => ['name' => 'billing_bill_currency', 'type' => 'string', 'internal' => 'currency'],
|
||||
'billing_bill_language' => ['name' => 'billing_bill_language', 'type' => 'string', 'internal' => 'language'],
|
||||
'billing_bill_referral' => ['name' => 'billing_bill_referral', 'type' => 'int', 'internal' => 'referral'],
|
||||
'billing_bill_referral_name' => ['name' => 'billing_bill_referral_name', 'type' => 'string', 'internal' => 'referralName'],
|
||||
'billing_bill_referral_name' => ['name' => 'billing_bill_referral_name', 'type' => 'string', 'internal' => 'externalReferral'],
|
||||
'billing_bill_reference' => ['name' => 'billing_bill_reference', 'type' => 'int', 'internal' => 'reference'],
|
||||
'billing_bill_accsegment' => ['name' => 'billing_bill_accsegment', 'type' => 'int', 'internal' => 'accSegment'],
|
||||
'billing_bill_accsection' => ['name' => 'billing_bill_accsection', 'type' => 'int', 'internal' => 'accSection'],
|
||||
|
|
|
|||
|
|
@ -32,6 +32,12 @@ use phpOMS\Stdlib\Base\FloatInt;
|
|||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @todo Find a way to handle references to the total invoice amount and other items
|
||||
* Example: If total invoice > $X no shipping expenses
|
||||
* Maybe additional column referencing total value
|
||||
* Example: If item Y quantity > Z no costs for item A (e.g. service fee)
|
||||
* Maybe by referencing another price (i.e. if other price triggered than this is triggered as well)
|
||||
*/
|
||||
class Price implements \JsonSerializable
|
||||
{
|
||||
|
|
|
|||
|
|
@ -309,13 +309,13 @@ final class SalesBillMapper extends BillMapper
|
|||
/**
|
||||
* Placeholder
|
||||
*/
|
||||
public static function getClientBills(int $id, \DateTime $start, \DateTime $end) : array
|
||||
public static function getClientBills(int $id, string $language, \DateTime $start, \DateTime $end) : array
|
||||
{
|
||||
return self::getAll()
|
||||
->with('type')
|
||||
->with('type/l11n')
|
||||
->where('client', $id)
|
||||
->where('type/l11n/language', 'en') // @todo fix localization
|
||||
->where('type/l11n/language', $language)
|
||||
->where('billDate', $start, '>=')
|
||||
->where('billDate', $end, '<=')
|
||||
->execute();
|
||||
|
|
@ -362,10 +362,11 @@ final class SalesBillMapper extends BillMapper
|
|||
/**
|
||||
* Placeholder
|
||||
*/
|
||||
public static function getItemMonthlySalesCosts(int $item, \DateTime $start, \DateTime $end) : array
|
||||
public static function getItemMonthlySalesCosts(array $item, \DateTime $start, \DateTime $end) : array
|
||||
{
|
||||
$sql = <<<SQL
|
||||
SELECT
|
||||
billing_bill_element_item
|
||||
SUM(billing_bill_element_total_netsalesprice) as net_sales,
|
||||
SUM(billing_bill_element_total_netpurchaseprice) as net_costs,
|
||||
YEAR(billing_bill_performance_date) as year,
|
||||
|
|
@ -373,11 +374,35 @@ final class SalesBillMapper extends BillMapper
|
|||
FROM billing_bill_element
|
||||
JOIN billing_bill ON billing_bill_element.billing_bill_element_bill = billing_bill.billing_bill_id
|
||||
WHERE
|
||||
billing_bill_element_item = {$item}
|
||||
billing_bill_element_item IN ({$item})
|
||||
AND billing_bill_performance_date >= '{$start->format('Y-m-d H:i:s')}'
|
||||
AND billing_bill_performance_date <= '{$end->format('Y-m-d H:i:s')}'
|
||||
GROUP BY year, month
|
||||
ORDER BY year ASC, month ASC;
|
||||
GROUP BY billing_bill_element_item, year, month
|
||||
ORDER BY billing_bill_element_item, year ASC, month ASC;
|
||||
SQL;
|
||||
|
||||
$query = new Builder(self::$db);
|
||||
$result = $query->raw($sql)->execute()->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
return $result ?? [];
|
||||
}
|
||||
|
||||
public static function getItemMonthlySalesQuantity(array $item, \DateTime $start, \DateTime $end) : array
|
||||
{
|
||||
$sql = <<<SQL
|
||||
SELECT
|
||||
billing_bill_element_item
|
||||
SUM(billing_bill_element_quantity) as quantity,
|
||||
YEAR(billing_bill_performance_date) as year,
|
||||
MONTH(billing_bill_performance_date) as month
|
||||
FROM billing_bill_element
|
||||
JOIN billing_bill ON billing_bill_element.billing_bill_element_bill = billing_bill.billing_bill_id
|
||||
WHERE
|
||||
billing_bill_element_item IN ({$item})
|
||||
AND billing_bill_performance_date >= '{$start->format('Y-m-d H:i:s')}'
|
||||
AND billing_bill_performance_date <= '{$end->format('Y-m-d H:i:s')}'
|
||||
GROUP BY billing_bill_element_item, year, month
|
||||
ORDER BY billing_bill_element_item, year ASC, month ASC;
|
||||
SQL;
|
||||
|
||||
$query = new Builder(self::$db);
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@
|
|||
"type": {
|
||||
"purchase_invoice": {
|
||||
"en": [
|
||||
"Invoice"
|
||||
"Invoice",
|
||||
"Receipt"
|
||||
],
|
||||
"de": [
|
||||
"Rechnung"
|
||||
"Rechnung",
|
||||
"Quittung"
|
||||
]
|
||||
},
|
||||
"purchase_credit_note": {
|
||||
|
|
@ -34,9 +36,7 @@
|
|||
]
|
||||
},
|
||||
"purchase_reverse_invoice": {
|
||||
"en": [
|
||||
"Credit Note"
|
||||
],
|
||||
"en": [],
|
||||
"de": [
|
||||
"Gutschrift"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -59,15 +59,6 @@ final class SalesBillMapperTest extends \PHPUnit\Framework\TestCase
|
|||
self::assertEquals(0, SalesBillMapper::getSalesByClientId(99999, new \DateTime('now'), new \DateTime('now'))->getInt());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers Modules\Billing\Models\SalesBillMapper
|
||||
* @group module
|
||||
*/
|
||||
public function testGetAvgSalesPriceByItemIdInvalid() : void
|
||||
{
|
||||
self::assertEquals(0, SalesBillMapper::getAvgSalesPriceByItemId(99999, new \DateTime('now'), new \DateTime('now'))->getInt());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers Modules\Billing\Models\SalesBillMapper
|
||||
* @group module
|
||||
|
|
@ -146,7 +137,7 @@ final class SalesBillMapperTest extends \PHPUnit\Framework\TestCase
|
|||
*/
|
||||
public function testGetItemMonthlySalesCostsInvalid() : void
|
||||
{
|
||||
self::assertEquals([], SalesBillMapper::getItemMonthlySalesCosts(99999, new \DateTime('now'), new \DateTime('now')));
|
||||
self::assertEquals([], SalesBillMapper::getItemMonthlySalesCosts([99999], new \DateTime('now'), new \DateTime('now')));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user