diff --git a/Admin/Install/db.json b/Admin/Install/db.json index a2bbe9e..10a833b 100644 --- a/Admin/Install/db.json +++ b/Admin/Install/db.json @@ -158,6 +158,11 @@ "type": "DATETIME", "null": false }, + "billing_out_performance_date": { + "name": "billing_out_performance_date", + "type": "DATETIME", + "null": false + }, "billing_out_created_by": { "name": "billing_out_created_by", "type": "INT", diff --git a/Controller/ApiController.php b/Controller/ApiController.php index e528d89..12eb138 100644 --- a/Controller/ApiController.php +++ b/Controller/ApiController.php @@ -31,6 +31,7 @@ use phpOMS\Model\Message\FormValidation; use phpOMS\Utils\Parser\Markdown\Markdown; use Modules\ItemManagement\Models\ItemMapper; use phpOMS\Localization\Money; +use Modules\ClientManagement\Models\ClientMapper; /** * Billing class. @@ -82,10 +83,16 @@ final class ApiController extends Controller */ public function createBillFromRequest(RequestAbstract $request, ResponseAbstract $response, $data = null) : Bill { + $client = ClientMapper::get((int) $request->getData('client')); + $bill = new Bill(); $bill->setCreatedBy(new NullAccount($request->header->account)); + $bill->number = '{y}-{id}'; // @todo: use admin defined format + $bill->billTo = $request->getData('billto') ?? $client->profile->account->name1; // @todo: use defaultInvoiceAddress or mainAddress + $bill->billCountry = $request->getData('billtocountry') ?? $client->mainAddress->getCountry(); $bill->type = new NullBillType((int) $request->getData('type')); $bill->client = new NullClient((int) $request->getData('client')); + $bill->performanceDate = new \DateTime($request->getData('performancedate') ?? 'now'); return $bill; } @@ -140,8 +147,9 @@ final class ApiController extends Controller $element = $this->createBillElementFromRequest($request, $response, $data); $this->createModel($request->header->account, $element, BillElementMapper::class, 'bill_element', $request->getOrigin()); - $bill = $this->updateBillWithBillElement($element, 1); - $this->updateModel($request->header->account, $element, BillMapper::class, 'bill_element', $request->getOrigin()); + $old = BillMapper::get($element->bill); + $new = $this->updateBillWithBillElement(clone $old, $element, 1); + $this->updateModel($request->header->account, $old, $new, BillMapper::class, 'bill_element', $request->getOrigin()); $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Bill element', 'Bill element successfully created.', $element); } @@ -185,6 +193,7 @@ final class ApiController extends Controller /** * Method to create a wiki entry from request. * + * @param Bill $bill Bill * @param BillElement $element Bill element * @param int $type Change type (0 = update, -1 = remove, +1 = add) * @@ -192,10 +201,8 @@ final class ApiController extends Controller * * @since 1.0.0 */ - public function updateBillWithBillElement(BillElement $element, int $type = 1) : Bill + public function updateBillWithBillElement(Bill $bill, BillElement $element, int $type = 1) : Bill { - $bill = BillMapper::get($element->bill); - if ($type === 1) { $bill->net->add($element->singleSalesPriceNet); } diff --git a/Models/Bill.php b/Models/Bill.php index c5d2cdd..c6bd247 100644 --- a/Models/Bill.php +++ b/Models/Bill.php @@ -43,7 +43,7 @@ class Bill implements \JsonSerializable * @var string * @since 1.0.0 */ - private string $number = ''; + public string $number = ''; /** * Bill type. @@ -69,6 +69,14 @@ class Bill implements \JsonSerializable */ public \DateTimeImmutable $createdAt; + /** + * Bill created at. + * + * @var \DateTime + * @since 1.0.0 + */ + public \DateTime $performanceDate; + /** * Bill send at. * @@ -93,7 +101,7 @@ class Bill implements \JsonSerializable * @var string * @since 1.0.0 */ - private $shipTo = ''; + public string $shipTo = ''; /** * For the attention of. @@ -141,7 +149,7 @@ class Bill implements \JsonSerializable * @var string * @since 1.0.0 */ - private $billTo = ''; + public string $billTo = ''; /** * Billing for the attention of. @@ -181,7 +189,7 @@ class Bill implements \JsonSerializable * @var string * @since 1.0.0 */ - private $billCountry = ''; + public string $billCountry = ''; /** * Person refering for this order. @@ -244,6 +252,7 @@ class Bill implements \JsonSerializable $this->profit = new Money(0); $this->createdAt = new \DateTimeImmutable(); + $this->performanceDate = new \DateTime(); $this->createdBy = new NullAccount(); $this->referer = new NullAccount(); } @@ -295,7 +304,25 @@ class Bill implements \JsonSerializable */ public function getNumber() : string { - return $this->number; + $number = $this->number; + + return \str_replace( + [ + '{y}', + '{m}', + '{d}', + '{id}', + '{type}', + ], + [ + $this->createdAt->format('Y'), + $this->createdAt->format('m'), + $this->createdAt->format('d'), + $this->id, + $this->type->getId(), + ], + $number + ); } /** diff --git a/Models/BillElementMapper.php b/Models/BillElementMapper.php index d5c557f..629b3a7 100644 --- a/Models/BillElementMapper.php +++ b/Models/BillElementMapper.php @@ -67,7 +67,7 @@ final class BillElementMapper extends DataMapperAbstract * @var string * @since 1.0.0 */ - protected static string $primaryField = 'billing_out_element_id'; + public static string $primaryField = 'billing_out_element_id'; /** * Primary table. @@ -75,5 +75,5 @@ final class BillElementMapper extends DataMapperAbstract * @var string * @since 1.0.0 */ - protected static string $table = 'billing_out_element'; + public static string $table = 'billing_out_element'; } diff --git a/Models/BillMapper.php b/Models/BillMapper.php index fb42f91..a899b68 100644 --- a/Models/BillMapper.php +++ b/Models/BillMapper.php @@ -17,6 +17,10 @@ namespace Modules\Billing\Models; use Modules\Admin\Models\AccountMapper; use Modules\ClientManagement\Models\ClientMapper; use phpOMS\DataStorage\Database\DataMapperAbstract; +use phpOMS\DataStorage\Database\Query\Builder; +use phpOMS\Localization\Money; +use phpOMS\DataStorage\Database\RelationType; +use phpOMS\Localization\Defaults\CountryMapper; /** * Mapper class. @@ -68,6 +72,7 @@ final class BillMapper extends DataMapperAbstract 'billing_out_ship_text' => ['name' => 'billing_out_ship_text', 'type' => 'string', 'internal' => 'shippingText'], 'billing_out_client' => ['name' => 'billing_out_client', 'type' => 'int', 'internal' => 'client'], 'billing_out_created_by' => ['name' => 'billing_out_created_by', 'type' => 'int', 'internal' => 'createdBy', 'readonly' => true], + 'billing_out_performance_date' => ['name' => 'billing_out_performance_date', 'type' => 'DateTime', 'internal' => 'performanceDate', 'readonly' => true], 'billing_out_created_at' => ['name' => 'billing_out_created_at', 'type' => 'DateTimeImmutable', 'internal' => 'createdAt', 'readonly' => true], ]; @@ -135,4 +140,170 @@ final class BillMapper extends DataMapperAbstract * @since 1.0.0 */ protected static string $table = 'billing_out'; + + public static function getSalesByItemId(int $id, \DateTime $start, \DateTime $end) : Money + { + $query = new Builder(self::$db); + $result = $query->select('SUM(billing_out_element_total_salesprice_net)') + ->from(self::$table) + ->leftJoin(BillElementMapper::getTable()) + ->on(self::$table . '.billing_out_id', '=', BillElementMapper::getTable() . '.billing_out_element_bill') + ->where(BillElementMapper::getTable() . '.billing_out_element_item', '=', $id) + ->andWhere(self::$table . '.billing_out_performance_date', '>=', $start) + ->andWhere(self::$table . '.billing_out_performance_date', '<=', $end) + ->execute() + ->fetch(); + + return new Money((int) $result[0]); + } + + public static function getAvgSalesPriceByItemId(int $id, \DateTime $start, \DateTime $end) : Money + { + $query = new Builder(self::$db); + $result = $query->select('SUM(billing_out_element_single_salesprice_net)', 'COUNT(billing_out_element_total_salesprice_net)') + ->from(self::$table) + ->leftJoin(BillElementMapper::getTable()) + ->on(self::$table . '.billing_out_id', '=', BillElementMapper::getTable() . '.billing_out_element_bill') + ->where(BillElementMapper::getTable() . '.billing_out_element_item', '=', $id) + ->andWhere(self::$table . '.billing_out_performance_date', '>=', $start) + ->andWhere(self::$table . '.billing_out_performance_date', '<=', $end) + ->execute() + ->fetch(); + + return new Money((int) (((int) $result[0]) / ((int) $result[1]))); + } + + public static function getLastOrderDateByItemId(int $id) : \DateTimeImmutable + { + // @todo: only delivers/invoice/production (no offers ...) + $query = new Builder(self::$db); + $result = $query->select('billing_out_performance_date') + ->from(self::$table) + ->leftJoin(BillElementMapper::getTable()) + ->on(self::$table . '.billing_out_id', '=', BillElementMapper::getTable() . '.billing_out_element_bill') + ->where(BillElementMapper::getTable() . '.billing_out_element_item', '=', $id) + ->orderBy('billing_out_id', 'DESC') + ->limit(1) + ->execute() + ->fetch(); + + return new \DateTimeImmutable($result[0]); + } + + public static function getItemRetentionRate(int $id, \DateTime $start, \DateTime $end) : float + { + + } + + public static function getItemLivetimeValue(int $id, \DateTime $start, \DateTime $end) : Money + { + + } + + public static function getNewestItemInvoices(int $id, int $limit = 10) : array + { + $depth = 3; + + // @todo: limit is not working correctly... only returns / 2 or something like that?. Maybe because bills arent unique? + + $query ??= self::getQuery(null, [], RelationType::ALL, $depth); + $query->leftJoin(BillElementMapper::getTable(), BillElementMapper::getTable() . '_' . $depth) + ->on(self::$table . '_' . $depth . '.billing_out_id', '=', BillElementMapper::getTable() . '_' . $depth . '.billing_out_element_bill') + ->where(BillElementMapper::getTable() . '_' . $depth . '.billing_out_element_item', '=', $id) + ->limit($limit); + + if (!empty(self::$createdAt)) { + $query->orderBy(self::$table . '_' . $depth . '.' . self::$columns[self::$createdAt]['name'], 'DESC'); + } else { + $query->orderBy(self::$table . '_' . $depth . '.' . self::$columns[self::$primaryField]['name'], 'DESC'); + } + + return self::getAllByQuery($query, RelationType::ALL, $depth); + } + + public static function getItemTopCustomers(int $id, \DateTime $start, \DateTime $end, int $limit = 10) : array + { + $depth = 3; + + $query ??= ClientMapper::getQuery(null, [], RelationType::ALL, $depth); + $query->selectAs('SUM(billing_out_element_total_salesprice_net)', 'net_sales') + ->leftJoin(self::$table, self::$table . '_' . $depth) + ->on(ClientMapper::getTable() . '_' . $depth . '.clientmgmt_client_id', '=', self::$table . '_' . $depth . '.billing_out_client') + ->leftJoin(BillElementMapper::getTable(), BillElementMapper::getTable() . '_' . $depth) + ->on(self::$table . '_' . $depth . '.billing_out_id', '=', BillElementMapper::getTable() . '_' . $depth . '.billing_out_element_bill') + ->where(BillElementMapper::getTable() . '_' . $depth . '.billing_out_element_item', '=', $id) + ->andWhere(self::$table . '_' . $depth . '.billing_out_performance_date', '>=', $start) + ->andWhere(self::$table . '_' . $depth . '.billing_out_performance_date', '<=', $end) + ->orderBy('net_sales', 'DESC') + ->limit($limit) + ->groupBy(ClientMapper::getTable() . '_' . $depth . '.clientmgmt_client_id'); + + $clients = ClientMapper::getAllByQuery($query, RelationType::ALL, $depth); + $data = ClientMapper::getDataLastQuery(); + + return [$clients, $data]; + } + + public static function getItemRegionSales(int $id, \DateTime $start, \DateTime $end) : array + { + $query = new Builder(self::$db); + $result = $query->select(CountryMapper::getTable() . '.country_region') + ->selectAs('SUM(billing_out_element_total_salesprice_net)', 'net_sales') + ->from(self::$table) + ->leftJoin(BillElementMapper::getTable()) + ->on(self::$table . '.billing_out_id', '=', BillElementMapper::getTable() . '.billing_out_element_bill') + ->leftJoin(CountryMapper::getTable()) + ->on(self::$table . '.billing_out_billCountry', '=', CountryMapper::getTable() . '.country_code2') + ->where(BillElementMapper::getTable() . '.billing_out_element_item', '=', $id) + ->andWhere(self::$table . '.billing_out_performance_date', '>=', $start) + ->andWhere(self::$table . '.billing_out_performance_date', '<=', $end) + ->groupBy(CountryMapper::getTable() . '.country_region') + ->execute() + ->fetchAll(\PDO::FETCH_KEY_PAIR); + + return $result; + } + + public static function getItemCountrySales(int $id, \DateTime $start, \DateTime $end, int $limit = 10) : array + { + $query = new Builder(self::$db); + $result = $query->select(CountryMapper::getTable() . '.country_code2') + ->selectAs('SUM(billing_out_element_total_salesprice_net)', 'net_sales') + ->from(self::$table) + ->leftJoin(BillElementMapper::getTable()) + ->on(self::$table . '.billing_out_id', '=', BillElementMapper::getTable() . '.billing_out_element_bill') + ->leftJoin(CountryMapper::getTable()) + ->on(self::$table . '.billing_out_billCountry', '=', CountryMapper::getTable() . '.country_code2') + ->where(BillElementMapper::getTable() . '.billing_out_element_item', '=', $id) + ->andWhere(self::$table . '.billing_out_performance_date', '>=', $start) + ->andWhere(self::$table . '.billing_out_performance_date', '<=', $end) + ->groupBy(CountryMapper::getTable() . '.country_code2') + ->orderBy('net_sales', 'DESC') + ->limit($limit) + ->execute() + ->fetchAll(\PDO::FETCH_KEY_PAIR); + + return $result; + } + + public static function getItemMonthlySalesCosts(int $id, \DateTime $start, \DateTime $end) : array + { + $query = new Builder(self::$db); + $result = $query->selectAs('SUM(billing_out_element_total_salesprice_net)', 'net_sales') + ->selectAs('SUM(billing_out_element_total_purchaseprice_net)', 'net_costs') + ->selectAs('YEAR(billing_out_performance_date)', 'year') + ->selectAs('MONTH(billing_out_performance_date)', 'month') + ->from(self::$table) + ->leftJoin(BillElementMapper::getTable()) + ->on(self::$table . '.billing_out_id', '=', BillElementMapper::getTable() . '.billing_out_element_bill') + ->where(BillElementMapper::getTable() . '.billing_out_element_item', '=', $id) + ->andWhere(self::$table . '.billing_out_performance_date', '>=', $start) + ->andWhere(self::$table . '.billing_out_performance_date', '<=', $end) + ->groupBy('year', 'month') + ->orderBy(['year', 'month'], ['ASC', 'ASC']) + ->execute() + ->fetchAll(); + + return $result; + } } diff --git a/Theme/Backend/sales-bill-list.tpl.php b/Theme/Backend/sales-bill-list.tpl.php index 52c131a..cd64693 100644 --- a/Theme/Backend/sales-bill-list.tpl.php +++ b/Theme/Backend/sales-bill-list.tpl.php @@ -43,7 +43,7 @@ echo $this->getData('nav')->render(); ?> $url = UriFactory::build('{/prefix}sales/invoice?{?}&id=' . $value->getId()); ?>