add exante and ibkr

This commit is contained in:
2023-04-12 15:13:55 +02:00
parent 90952d883e
commit fc1ef6f3ec
17 changed files with 439 additions and 75 deletions
+10 -2
View File
@@ -3,6 +3,7 @@
namespace App\Controller; namespace App\Controller;
use App\Helper\Currency; use App\Helper\Currency;
use App\Helper\DataNeed\NeedFileName;
use App\Services\CryptoService; use App\Services\CryptoService;
use App\Services\DividendsService; use App\Services\DividendsService;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
@@ -67,9 +68,14 @@ class TaxesController extends AbstractController
$i=1; $i=1;
/** @var UploadedFile $file */ /** @var UploadedFile $file */
foreach ($files as $file) { foreach ($files as $file) {
$newName = $i;
$checkName = NeedFileName::check($file->getClientOriginalName());
if ($checkName !== null) {
$newName .= "-".$checkName;
}
$file->move( $file->move(
$this->getParameter("uploads_path")."/".$hash, $this->getParameter("uploads_path")."/".$hash,
$i.".".$file->getClientOriginalExtension() $newName.".".$file->getClientOriginalExtension()
); );
$i++; $i++;
} }
@@ -94,7 +100,9 @@ class TaxesController extends AbstractController
// dd($gen); // dd($gen);
return $this->render("taxes/show.html.twig", [ return $this->render("taxes/show.html.twig", [
"taxes" => $gen->getSummaryByCountry(), "taxes" => $gen->getSummaryByCountry(),
"error" => $gen->error "errorDividend" => $gen->getErrorDividend(),
"errorString" => $gen->getErrorString(),
"rawCount" => $gen->takeCountRawData()
]); ]);
} }
+14 -4
View File
@@ -7,7 +7,7 @@ use Doctrine\ORM\EntityManager;
class Currency class Currency
{ {
public static array $currencies = ["USD","GBP","PLN","EUR"]; public static array $currencies = ["USD","GBP","PLN","EUR","CAD"];
public static array $currenciesCrypto = ["USDT"]; public static array $currenciesCrypto = ["USDT"];
private static string $apiUrl = "https://api.nbp.pl/api/exchangerates/rates/a/:code/:date/?format=json"; private static string $apiUrl = "https://api.nbp.pl/api/exchangerates/rates/a/:code/:date/?format=json";
private EntityManager $manager; private EntityManager $manager;
@@ -19,7 +19,7 @@ class Currency
$this->takeRates(); $this->takeRates();
} }
public function getRate(string $currency, \DateTime $dateTime, &$err) : float public function getRate(string $currency, \DateTime $dateTime) : float
{ {
$dayOfWeek = $dateTime->format("N"); $dayOfWeek = $dateTime->format("N");
$minusDay = 1; $minusDay = 1;
@@ -35,12 +35,16 @@ class Currency
return $this->ratesPerDate[$currency][$dateStr]; return $this->ratesPerDate[$currency][$dateStr];
} }
$dateCopy = clone $dateTime; $dateCopy = clone $dateTime;
$limitLoop = 5;
do { do {
$rate = $this->takePLNOnDate($currency, $dateCopy->format("Y-m-d")); $rate = $this->takePLNOnDate($currency, $dateCopy->format("Y-m-d"));
if (!$rate) { if (!$rate) {
$dateCopy->modify("-1 day"); $dateCopy->modify("-1 day");
} }
if ($limitLoop < 1) {
break;
}
$limitLoop--;
}while($rate == 0); }while($rate == 0);
if ($rate) { if ($rate) {
@@ -61,7 +65,7 @@ class Currency
private function takePLNOnDate(string $currency, string $dateStr) : float private function takePLNOnDate(string $currency, string $dateStr) : float
{ {
if (!in_array($currency, self::$currencies)) { if (!self::isCurrencyExists($currency)) {
return 0; return 0;
} }
$url = strtr(self::$apiUrl,[ $url = strtr(self::$apiUrl,[
@@ -95,4 +99,10 @@ class Currency
} }
} }
} }
public static function isCurrencyExists(string $currency) : bool
{
return in_array($currency, self::$currencies);
}
} }
+2 -2
View File
@@ -12,8 +12,8 @@ final class CryptoValues
"date" => ["Data operacji","Date(UTC)"], "date" => ["Data operacji","Date(UTC)"],
"desc" => ["Rodzaj"], "desc" => ["Rodzaj"],
"price" => ["Price"], "price" => ["Price"],
"transactionType" => ["Type"], "transactionType" => ["Type","Side"],
"market" => ["Market"], "market" => ["Market","Pair"],
"realValue" => ["Total"] "realValue" => ["Total"]
]; ];
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Helper\DataNeed;
class DividendValues
{
private static array $samples = [
'count' => ["LICZBA POSIADANYCH SZT"],
'isin' => ["ISIN"],
'currency' => ["WALUTA","CurrencyPrimary"],
'tax' => ["PODATK"],
'value' => ["DYWIDENDA BRUTTO"],
'date' => ["DATA","Date"],
'description' => ["Desc"],
'amount' => ["Amount"],
'groupId' => ["ActionID"],
'type' => ["Type"]
];
public static function searchForKeyName(string $val) : ?string
{
$ret=null;
foreach (self::$samples as $key => $sample) {
foreach ($sample as $item) {
if (strpos($val, $item) !== false) {
$ret = $key;
break;
}
if ($ret) {
break;
}
}
}
return $ret;
}
}
@@ -0,0 +1,23 @@
<?php
namespace App\Helper\DataNeed;
class NeedFileName
{
const NAMES = [
"exante","ibkr"
];
public static function check(string $fileName) : ?string
{
$name = null;
$fileName = strtolower($fileName);
foreach (self::NAMES as $str) {
if (strpos($fileName, $str) !== false) {
$name = $str;
break;
}
}
return $name;
}
}
+3
View File
@@ -73,6 +73,9 @@ class Crypto extends Main
if (in_array($currency,Currency::$currenciesCrypto)) { if (in_array($currency,Currency::$currenciesCrypto)) {
$curr=null; $curr=null;
foreach ($i as $item) { foreach ($i as $item) {
if (!isset($realCurrency[$item->getRealCurrency()])) {
continue;
}
if (!($curr instanceof MainObject)) { if (!($curr instanceof MainObject)) {
$curr = $realCurrency[$item->getRealCurrency()][array_key_first($realCurrency[$item->getRealCurrency()])]; $curr = $realCurrency[$item->getRealCurrency()][array_key_first($realCurrency[$item->getRealCurrency()])];
} }
+16 -9
View File
@@ -17,15 +17,15 @@ class Main
public function addData(array $data) public function addData(array $data)
{ {
if (isset($data[0]) && $data[0][0]==='Date(UTC)') { if (isset($data[0]) && $data[0][0]==='Date(UTC)') {
$first = array_shift($data); // $first = array_shift($data);
array_unshift($data, $first, [ // array_unshift($data, $first, [
"2021-04-14 13:00:00", // "2021-04-14 13:00:00",
"EURPLN", // "EURPLN",
"BUY", // "BUY",
4.5755, // 4.5755,
100, // 100,
457.55 // 457.55
]); // ]);
} }
$this->data[] = $data; $this->data[] = $data;
} }
@@ -87,6 +87,13 @@ class Main
if ($name === "setRealValue") { if ($name === "setRealValue") {
$datum[$key] = intval($datum[$key]*100); $datum[$key] = intval($datum[$key]*100);
} }
if ($name === "setPrice" && is_string($datum[$key])) {
$val = $datum[$key];
if (strpos($val, ".") !== false && strpos($val, ",") !== false) {
$val = str_replace(",", "", $val);
$datum[$key] = intval(floatval($val)*100);
}
}
$obj->$name($datum[$key]); $obj->$name($datum[$key]);
} }
$obj->trySetCurrencies(); $obj->trySetCurrencies();
+7 -3
View File
@@ -21,17 +21,21 @@ class ReadSheet
return $this->arr; return $this->arr;
} }
public function takeFile() : bool public function takeFile(?string $delimiter = null) : bool
{ {
if (!$this->filePath) { if (!$this->filePath) {
return false; return false;
} }
$fileType = explode(".", $this->filePath); $fileType = explode(".", $this->filePath)[1];
$reader = IOFactory::createReader(ucfirst($fileType[1])); $reader = IOFactory::createReader(ucfirst($fileType));
$reader->setLoadAllSheets(); $reader->setLoadAllSheets();
$reader->setReadDataOnly(true); $reader->setReadDataOnly(true);
if ($delimiter) {
$reader->setDelimiter($delimiter);
}
$spreadsheet = $reader->load($this->filePath); $spreadsheet = $reader->load($this->filePath);
// $spreadsheet->getDefaultStyle()->getNumberFormat()->setFormatCode('@');
foreach ($spreadsheet->getAllSheets() as $sheetName) { foreach ($spreadsheet->getAllSheets() as $sheetName) {
$this->arr[] = $sheetName->toArray(); $this->arr[] = $sheetName->toArray();
} }
+1 -1
View File
@@ -209,6 +209,7 @@ class MainObject
public function trySetCurrencies() : void public function trySetCurrencies() : void
{ {
if ($this->marketName) { if ($this->marketName) {
$cryptoFiat = ["USDT","BUSD"];
if (strlen($this->marketName) === 6) { if (strlen($this->marketName) === 6) {
$cur1 = substr($this->marketName,0,3); $cur1 = substr($this->marketName,0,3);
$cur2 = substr($this->marketName,3); $cur2 = substr($this->marketName,3);
@@ -221,7 +222,6 @@ class MainObject
$this->setRealCurrency($cur1); $this->setRealCurrency($cur1);
} }
} else { } else {
$cryptoFiat = ["USDT"];
foreach ($cryptoFiat as $item) { foreach ($cryptoFiat as $item) {
$currReal = strpos($this->marketName, $item); $currReal = strpos($this->marketName, $item);
if ($currReal === false) { if ($currReal === false) {
@@ -7,14 +7,17 @@ use Doctrine\ORM\EntityManager;
class DividendsSummary class DividendsSummary
{ {
/** @var SheetData|array $sheetData */ /** @var SheetDataAll|array $sheetData */
private array $sheetData=[]; private array $sheetData=[];
/** @var XMLReader|array $sheetData */ /** @var XMLReader|array $sheetData */
private array $xmlData=[]; private array $xmlData=[];
private Currency $currencyObj; private Currency $currencyObj;
public array $error=[]; /** @var array|DividendsData $error */
private ?array $errorDividend=null;
private ?array $errorString=null;
private array $allData=[]; private array $allData=[];
private array $countryToCurrency=[]; private array $countryToCurrency=[];
private int $realCalculatedRows = 0;
public function __construct(EntityManager $manager) public function __construct(EntityManager $manager)
{ {
@@ -88,6 +91,7 @@ class DividendsSummary
if (!isset($countryToCurrency[$country])) { if (!isset($countryToCurrency[$country])) {
$countryToCurrency[$country] = $val->getCurrency(); $countryToCurrency[$country] = $val->getCurrency();
} }
$this->realCalculatedRows++;
} }
} }
} }
@@ -101,7 +105,11 @@ class DividendsSummary
private function getRate(DividendsData $data) : float private function getRate(DividendsData $data) : float
{ {
return $this->currencyObj->getRate($data->getCurrency(),$data->getDateTime(),$this->error); if (!Currency::isCurrencyExists($data->getCurrency())) {
$this->errorString[$data->getCurrency()] = "Currency: {$data->getCurrency()} is not supported! Please contact with administrator.";
return 0;
}
return $this->currencyObj->getRate($data->getCurrency(),$data->getDateTime());
} }
private function count(DividendsData $data, bool $fromDestiny = false) : float private function count(DividendsData $data, bool $fromDestiny = false) : float
@@ -120,7 +128,7 @@ class DividendsSummary
if ($rate > 0) { if ($rate > 0) {
return $valueTax * $rate; return $valueTax * $rate;
} }
$this->error[]=$data; $this->errorDividend[]=$data;
return 0; return 0;
} }
@@ -140,9 +148,9 @@ class DividendsSummary
} }
/** /**
* @param SheetData $sheetData * @param SheetDataAll $sheetData
*/ */
public function addSheetData(SheetData $sheetData): void public function addSheetData(SheetDataAll $sheetData): void
{ {
$this->sheetData[] = $sheetData; $this->sheetData[] = $sheetData;
} }
@@ -155,4 +163,27 @@ class DividendsSummary
$this->xmlData[] = $reader; $this->xmlData[] = $reader;
} }
public function takeCountRawData(): array
{
$rowCount = 0;
/** @var SheetData $sheetDatum */
foreach ($this->sheetData as $sheetDatum) {
$rowCount += $sheetDatum->getCount();
}
return [
'countSheet' => count($this->sheetData),
'countRows' => $rowCount,
'realCountRows' => $this->realCalculatedRows
];
}
public function getErrorDividend(): ?array
{
return $this->errorDividend;
}
public function getErrorString(): ?array
{
return $this->errorString;
}
} }
@@ -0,0 +1,54 @@
<?php
namespace App\Logic\Dividends;
class ExanteSheet extends SheetDataAll
{
protected function takeData(): array
{
$commentKey = -1;
$operationKey = -1;
$ret = [];
foreach ($this->getRawData() as $index => $rawDatum) {
if (!isset($rawDatum[1])) {
continue;
}
foreach ($rawDatum as $key => $value) {
if (strpos($value, "Comment") !== false) {
$commentKey = $key;
}
if (strpos($value, "Operation type") !== false) {
$operationKey = $key;
}
}
break;
}
foreach ($this->getRawData() as $index => $rawDatum) {
if ($index<2) {
continue;
}
if (strtolower($rawDatum[$operationKey]) === "tax") {
continue;
}
$comment = $rawDatum[$commentKey];
$separate = explode(" ",$comment);
$tax = intval(str_replace(["(","-","%",")"],"",$separate[16]));
$value = floatval(str_replace(["(",")"],"",$separate[10]));
$date = new \DateTime($separate[3]);
$dividendObj = new DividendsData();
$dividendObj->setCount($separate[0]);
$dividendObj->setCurrency($separate[9]);
$dividendObj->setTax($tax);
$dividendObj->setDateTime($date);
$dividendObj->setIsin($separate[18]);
$dividendObj->setValue($value);
$ret[] = $dividendObj;
}
if ($commentKey === -1) {
trigger_error(var_export("false",true));
return [];
}
return $ret;
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
namespace App\Logic\Dividends;
use App\Helper\DataNeed\DividendValues;
class IbkrSheet extends SheetDataAll
{
protected function takeData(): array
{
$needIndex = [];
foreach ($this->getRawData() as $index => $rawDatum) {
if ($index === 0) {
foreach ($rawDatum as $i => $item) {
if (!$item) {
continue;
}
$keyName = DividendValues::searchForKeyName($item);
if ($keyName) {
$needIndex[$i] = $keyName;
}
}
break;
}
}
$ret = [];
$dividendArr = [];
$taxArr = [];
$test = [];
foreach ($this->getRawData() as $index => $rawDatum) {
if ($index < 1) {
continue;
}
$row=[];
foreach ($rawDatum as $i => $d) {
if (isset($needIndex[$i])) {
$row[$needIndex[$i]] = $d;
}
}
$test[$row['groupId']][] = $row['amount'];
if (strpos($row['type'], "Dividend") !== false) {
$dividendArr[$row['groupId']] = $row;
} else {
if (!isset($taxArr[$row['groupId']])) {
$taxArr[$row['groupId']] = $row;
} else {
$taxArr[$row['groupId']]['amount'] += $row['amount'];
}
}
}
foreach ($dividendArr as $groupId => $dividend) {
$taxGet = $taxArr[$groupId]['amount'];
// $dividend['amount'] += $taxGet;
$taxPercentage = intval(round(abs($taxGet) / $dividend['amount'], 2) * 100);
$separate = explode(" ", $dividend['description']);
$perShare = floatval($separate[4]);
if (!$perShare) {
$perShare = floatval($separate[5]);
}
$date = explode(";", $dividend['date'])[0];
$date = substr($date, 0, 4) . "-" . substr($date, 4, 2) . "-" . substr($date, 6, 2);
$dividendObj = new DividendsData();
$dividendObj->setCount(intval($dividend['amount'] / $perShare));
$dividendObj->setIsin($dividend['isin']);
$dividendObj->setCurrency($dividend['currency']);
$dividendObj->setTax($taxPercentage);
$dividendObj->setValue($perShare);
$dividendObj->setDateTime(new \DateTime($date));
$ret[] = $dividendObj;
}
return $ret;
}
}
+45 -43
View File
@@ -3,43 +3,36 @@ declare(strict_types=1);
namespace App\Logic\Dividends; namespace App\Logic\Dividends;
use App\Helper\Currency; use App\Helper\Currency;
use App\Helper\DataNeed\DividendValues;
use PhpOffice\PhpSpreadsheet\Shared\Date;
class SheetData class SheetData extends SheetDataAll
{ {
private array $rawData;
// eg. 0 -> name, 1 -> ISIN // eg. 0 -> name, 1 -> ISIN
private array $dataIndex; private array $dataIndex;
private array $data;
public function __construct(array $data)
{
$this->rawData = $data;
}
public function get() : array public function takeData() : array
{ {
if (isset($this->data)) {
return $this->data;
}
$needIndex = []; $needIndex = [];
foreach ($this->rawData as $index => $rawDatum) { foreach ($this->getRawData() as $index => $rawDatum) {
if ($index === 0) { if ($index === 0) {
$this->dataIndex = $rawDatum; $this->dataIndex = $rawDatum;
continue; foreach ($rawDatum as $i => $item) {
} if (!$item) {
$setDatas=[]; continue;
foreach ($rawDatum as $i => $data) { }
$dataType = $this->getDataType($data,$setDatas); $keyName = DividendValues::searchForKeyName($item);
if ($dataType){ if ($keyName) {
$needIndex[$i] = $dataType; $needIndex[$i] = $keyName;
}
} }
break;
// continue;
} }
unset($setDatas); // trigger_error(var_export($rawDatum,true));
break;
} }
$ret = []; $ret = [];
foreach ($this->rawData as $index => $datum) { foreach ($this->getRawData() as $index => $datum) {
if ($index === 0) { if ($index === 0) {
continue; continue;
} }
@@ -49,35 +42,30 @@ class SheetData
$row[$needIndex[$i]] = $d; $row[$needIndex[$i]] = $d;
} }
} }
if (count($row) < 1) {
continue;
}
$date = &$row['date'];
if (is_int($date)) {
$date = Date::excelToDateTimeObject($date);
} else if (is_string($date)) {
$date = new \DateTime(str_replace(".", "-", $date));
}
if (is_float($row['tax'])) {
$row['tax'] = intval(round($row['tax']*100));
}
$dividendObj = new DividendsData(); $dividendObj = new DividendsData();
$dividendObj->setCount($row['count']); $dividendObj->setCount($row['count']);
$dividendObj->setIsin($row['isin']); $dividendObj->setIsin($row['isin']);
$dividendObj->setCurrency($row['currency']); $dividendObj->setCurrency($row['currency']);
$dividendObj->setTax($row['tax']); $dividendObj->setTax($row['tax']);
$dividendObj->setValue($row['value']); $dividendObj->setValue($row['value']);
$dividendObj->setDateTime(new \DateTime(str_replace(".","-",$row['date']))); $dividendObj->setDateTime($date);
$ret[] = $dividendObj; $ret[] = $dividendObj;
} }
$this->data = $ret;
return $ret; return $ret;
} }
public function getByCountry() : array
{
$data = $this->get();
if (!$data) {
return [];
}
$newArr = [];
/** @var DividendsData $datum */
foreach ($data as $datum) {
$key = mb_substr($datum->getIsin(), 0, 2);
$newArr[$key][] = $datum;
}
return $newArr;
}
public function takeColumnNames() : array public function takeColumnNames() : array
{ {
return $this->dataIndex; return $this->dataIndex;
@@ -118,10 +106,24 @@ class SheetData
if (in_array($data, Currency::$currencies)) { if (in_array($data, Currency::$currencies)) {
return "currency"; return "currency";
} }
if (count(explode(".", $data)) === 3) { if (count(explode(".", $data)) === 3 || count(explode("-", $data)) === 3) {
return "date"; return "date";
} }
return null; return null;
} }
private function searchForDateIndex() : int
{
$ret = -1;
foreach ($this->takeColumnNames() as $index => $takeColumnName) {
if (strpos($takeColumnName,"DATE") !== false || strpos($takeColumnName,"DATA") !== false) {
$ret = $index;
break;
}
}
return $ret;
}
} }
@@ -0,0 +1,83 @@
<?php
namespace App\Logic\Dividends;
abstract class SheetDataAll
{
protected int $countData;
/**
* @var array|DividendsData $data
*/
protected array $data = [];
private array $rawData;
/**
* Assign raw data to field, then create array of DividendsData
*
* @param array $rawData
*/
public function __construct(array $rawData)
{
$this->rawData = $rawData;
$this->countData = count($this->rawData) - 1;
$this->data = $this->takeData();
}
/**
* Get array grouped values by country
*
* @return array
*/
public function getByCountry() : array
{
$data = $this->getData();
if (!$data) {
return [];
}
$newArr = [];
/** @var DividendsData $datum */
foreach ($data as $datum) {
$key = mb_substr($datum->getIsin(), 0, 2);
$newArr[$key][] = $datum;
}
return $newArr;
}
/**
* Get count of rows
*
* @return int
*/
public function getCount(): int
{
return $this->countData;
}
/**
* Get array of DividendsData
*
* @return array
*/
protected function getData() : array
{
return $this->data;
}
/**
* Get array of raw data
*
* @return array
*/
protected function getRawData() : array
{
return $this->rawData;
}
/**
* Generate array of DividendsData from $rawData
*
* @return array
*/
abstract protected function takeData() : array;
}
+18 -2
View File
@@ -4,6 +4,8 @@ namespace App\Services;
use App\Helper\Sheets\ReadSheet; use App\Helper\Sheets\ReadSheet;
use App\Logic\Dividends\DividendsSummary; use App\Logic\Dividends\DividendsSummary;
use App\Logic\Dividends\ExanteSheet;
use App\Logic\Dividends\IbkrSheet;
use App\Logic\Dividends\SheetData; use App\Logic\Dividends\SheetData;
use App\Logic\Dividends\XMLReader; use App\Logic\Dividends\XMLReader;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
@@ -16,12 +18,26 @@ class DividendsService extends MasterServices
foreach ($this->allFiles as $file) { foreach ($this->allFiles as $file) {
$extension = pathinfo($file)['extension']; $extension = pathinfo($file)['extension'];
$isExante = strpos($file, "exante") !== false;
$isIbkr = strpos($file, "ibkr") !== false;
if (in_array($extension, ['xlsx','csv'])) { if (in_array($extension, ['xlsx','csv'])) {
if ($isExante) {
$delimiter = ";";
} else {
$delimiter = null;
}
$reader = new ReadSheet($file); $reader = new ReadSheet($file);
if ($reader->takeFile()) { if ($reader->takeFile($delimiter)) {
/** @var SheetData $item */ /** @var SheetData $item */
foreach ($reader->getAll() as $val) { foreach ($reader->getAll() as $val) {
$item = new SheetData($val); if ($isExante) {
$item = new ExanteSheet($val);
} elseif ($isIbkr) {
$item = new IbkrSheet($val);
}
else {
$item = new SheetData($val);
}
$summaryObj->addSheetData($item); $summaryObj->addSheetData($item);
} }
} }
+11 -3
View File
@@ -77,21 +77,29 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
Calculated <b>{{ rawCount.realCountRows }}</b> of <b>{{ rawCount.countRows }}</b> rows in <b>{{ rawCount.countSheet }}</b> sheets
{% endif %} {% endif %}
{% if error %} {% if errorDividend %}
<table class="table table-dark table-striped"> <table class="table table-dark table-striped">
<tr> <tr>
{% for curr,val in error %} {% for curr,val in errorDividend %}
<td>{{ curr }}</td> <td>{{ curr }}</td>
{% endfor %} {% endfor %}
</tr> </tr>
<tr> <tr>
{% for curr,val in error %} {% for curr,val in errorDividend %}
<td>{{ val }}</td> <td>{{ val }}</td>
{% endfor %} {% endfor %}
</tr> </tr>
</table> </table>
{% endif %} {% endif %}
{% if errorString %}
{% for error in errorString %}
<div class="alert alert-warning">
<span>{{ error }}</span>
</div>
{% endfor %}
{% endif %}
</div> </div>
</div> </div>
Regular → Executable
View File