diff --git a/Admin/Install/Media/bill.pdf.php b/Admin/Install/Media/bill.pdf.php index ddc4054..afc8579 100755 --- a/Admin/Install/Media/bill.pdf.php +++ b/Admin/Install/Media/bill.pdf.php @@ -2,83 +2,11 @@ declare(strict_types=1); -class MYPDF extends TCPDF -{ - public string $fontName = ''; - public int $fontSize = 8; - public int $sideMargin = 15; +require_once $this->getData('defaultTemplates') + ->findFile('.pdf.php') + ->getAbsolutePath(); - //Page header - public function Header() { - if ($this->header_xobjid === false) { - $this->header_xobjid = $this->startTemplate($this->w, 0); - - // Set Logo - $image_file = __DIR__ . '/logo.png'; - $this->Image($image_file, 15, 15, 15, 15, 'PNG', '', 'T', false, 300, '', false, false, 0, false, false, false); - - // Set Title - $this->SetFont('helvetica', 'B', 20); - $this->setX(15 + 15 + 3); - $this->Cell(0, 14, $this->header_title, 0, false, 'L', 0, '', 0, false, 'T', 'M'); - - $this->SetFont('helvetica', '', 10); - $this->setX(15 + 15 + 3); - $this->Cell(0, 26, $this->header_string, 0, false, 'L', 0, '', 0, false, 'T', 'M'); - - $this->endTemplate(); - } - - $x = 0; - $dx = 0; - - if (!$this->header_xobj_autoreset AND $this->booklet AND (($this->page % 2) == 0)) { - // adjust margins for booklet mode - $dx = ($this->original_lMargin - $this->original_rMargin); - } - - if ($this->rtl) { - $x = $this->w + $dx; - } else { - $x = 0 + $dx; - } - - $this->printTemplate($this->header_xobjid, $x, 0, 0, 0, '', '', false); - if ($this->header_xobj_autoreset) { - // reset header xobject template at each page - $this->header_xobjid = false; - } - } - - // Page footer - public function Footer() { - $this->SetY(-25); - - $this->SetFont('helvetica', 'I', 7); - $this->Cell($this->getPageWidth() - 22, 0, 'Page '.$this->getAliasNumPage().'/'.$this->getAliasNbPages(), 0, false, 'R', 0, '', 0, false, 'T', 'M'); - $this->Ln(); - $this->Ln(); - - $this->SetFillColor(245, 245, 245); - $this->SetX(0); - $this->Cell($this->getPageWidth(), 25, '', 0, 0, 'L', true, '', 0, false, 'T', 'T'); - - $this->SetFont('helvetica', '', 7); - $this->SetXY(15 + 10, -15, true); - $this->MultiCell(30, 0, "Jingga e.K.\nGartenstr. 26\n61206 Woellstadt", 0, 'L', false, 1, null, null, true, 0, false, true, 0, 'B'); - - $this->SetXY(25 + 15 + 20, -15, true); - $this->MultiCell(40, 0, "Geschäftsführer: Dennis Eichhorn\nFinanzamt: HRB ???\nUSt Id: DE ??????????", 0, 'L', false, 1, null, null, true, 0, false, true, 0, 'B'); - - $this->SetXY(25 + 45 + 15 + 30, -15, true); - $this->MultiCell(35, 0, "Volksbank Mittelhessen\nBIC: ??????????\nIBAN: ???????????", 0, 'L', false, 1, null, null, true, 0, false, true, 0, 'B'); - - $this->SetXY(25 + 45 + 35 + 15 + 40, -15, true); - $this->MultiCell(35, 0, "www.jingga.app\ninfo@jingga.app\n+49 0152 ???????", 0, 'L', false, 1, null, null, true, 0, false, true, 0, 'B'); - } -} - -$pdf = new MYPDF('P', 'mm', 'A4', true, 'UTF-8', false); +$pdf = new DefaultPdf('P', 'mm', 'A4', true, 'UTF-8', false); $creator = $this->getData('bill_creator') ?? 'Jingga'; $author = 'Jingga'; @@ -120,7 +48,6 @@ $terms = $this->getData('bill_terms') ?? 'https://jingga.app/terms'; $taxes = $this->getData('bill_taxes') ?? ['19%' => '0.00']; $currency = $this->getData('bill_currency') ?? 'EUR'; - // set document information $pdf->SetCreator($creator); $pdf->SetAuthor($author); @@ -128,28 +55,9 @@ $pdf->SetTitle($title); $pdf->SetSubject($subtitle); $pdf->SetKeywords(\implode(', ', $keywords)); -// set default header data -$pdf->SetHeaderData(PDF_HEADER_LOGO, PDF_HEADER_LOGO_WIDTH, $logoName, $slogan); - -// set header and footer fonts -$pdf->SetHeaderFont([PDF_FONT_NAME_MAIN, '', PDF_FONT_SIZE_MAIN]); -$pdf->SetFooterFont([PDF_FONT_NAME_DATA, '', PDF_FONT_SIZE_DATA]); - -// set default monospaced font -$pdf->SetDefaultMonospacedFont(PDF_FONT_MONOSPACED); - -// set margins -$pdf->SetMargins(15, 30, 15); - -// set auto page breaks -$pdf->SetAutoPageBreak(true, 25); - // set image scale factor $pdf->SetImageScale(PDF_IMAGE_SCALE_RATIO); -// add a page -$pdf->AddPage(); - $topPos = $pdf->getY(); // Address diff --git a/Admin/Install/attributes.json b/Admin/Install/attributes.json new file mode 100644 index 0000000..71d8ba4 --- /dev/null +++ b/Admin/Install/attributes.json @@ -0,0 +1,15 @@ +[ + { + "name": "external_payment_id", + "l11n": { + "en": "External payment Id", + "de": "Externe Zahlungsid" + }, + "value_type": 2, + "is_custom_allowed": true, + "validation_pattern": "", + "is_required": false, + "default_value": "", + "values": [] + } +] \ No newline at end of file diff --git a/Admin/Install/db.json b/Admin/Install/db.json index 59de8d2..57ff707 100755 --- a/Admin/Install/db.json +++ b/Admin/Install/db.json @@ -1,179 +1,271 @@ { - "billing_prices": { - "name": "billing_prices", + "billing_price": { + "name": "billing_price", "fields": { - "billing_prices_id": { - "name": "billing_prices_id", + "billing_price_id": { + "name": "billing_price_id", "type": "INT", "null": false, "primary": true, "autoincrement": true }, - "billing_prices_name": { - "name": "billing_prices_name", + "billing_price_name": { + "name": "billing_price_name", "type": "VARCHAR(255)", "null": false }, - "billing_prices_promocode": { - "name": "billing_prices_promocode", - "type": "INT", + "billing_price_promocode": { + "name": "billing_price_promocode", + "type": "VARCHAR(50)", "null": false }, - "billing_prices_item": { - "name": "billing_prices_item", + "billing_price_item": { + "name": "billing_price_item", "type": "INT", "null": true, "default": null, "foreignTable": "itemmgmt_item", "foreignKey": "itemmgmt_item_id" }, - "billing_prices_itemgroup": { - "name": "billing_prices_itemgroup", + "billing_price_itemgroup": { + "name": "billing_price_itemgroup", "type": "INT", "null": true, "default": null, "foreignTable": "itemmgmt_attr_value", "foreignKey": "itemmgmt_attr_value_id" }, - "billing_prices_itemsegment": { - "name": "billing_prices_itemsegment", + "billing_price_itemsegment": { + "name": "billing_price_itemsegment", "type": "INT", "null": true, "default": null, "foreignTable": "itemmgmt_attr_value", "foreignKey": "itemmgmt_attr_value_id" }, - "billing_prices_itemsection": { - "name": "billing_prices_itemsection", + "billing_price_itemsection": { + "name": "billing_price_itemsection", "type": "INT", "null": true, "default": null, "foreignTable": "itemmgmt_attr_value", "foreignKey": "itemmgmt_attr_value_id" }, - "billing_prices_itemtype": { - "name": "billing_prices_itemtype", + "billing_price_itemtype": { + "name": "billing_price_itemtype", "type": "INT", "null": true, "default": null, "foreignTable": "itemmgmt_attr_value", "foreignKey": "itemmgmt_attr_value_id" }, - "billing_prices_client": { - "name": "billing_prices_client", + "billing_price_client": { + "name": "billing_price_client", "type": "INT", "null": true, "default": null, "foreignTable": "clientmgmt_client", "foreignKey": "clientmgmt_client_id" }, - "billing_prices_clientgroup": { - "name": "billing_prices_clientgroup", + "billing_price_clientgroup": { + "name": "billing_price_clientgroup", "type": "INT", "null": true, "default": null, "foreignTable": "clientmgmt_attr_value", "foreignKey": "clientmgmt_attr_value_id" }, - "billing_prices_clientsegment": { - "name": "billing_prices_clientsegment", + "billing_price_clientsegment": { + "name": "billing_price_clientsegment", "type": "INT", "null": true, "default": null, "foreignTable": "clientmgmt_attr_value", "foreignKey": "clientmgmt_attr_value_id" }, - "billing_prices_clientsection": { - "name": "billing_prices_clientsection", + "billing_price_clientsection": { + "name": "billing_price_clientsection", "type": "INT", "null": true, "default": null, "foreignTable": "clientmgmt_attr_value", "foreignKey": "clientmgmt_attr_value_id" }, - "billing_prices_clienttype": { - "name": "billing_prices_clienttype", + "billing_price_clienttype": { + "name": "billing_price_clienttype", "type": "INT", "null": true, "default": null, "foreignTable": "clientmgmt_attr_value", "foreignKey": "clientmgmt_attr_value_id" }, - "billing_prices_clientcountry": { - "name": "billing_prices_clientcountry", + "billing_price_clientcountry": { + "name": "billing_price_clientcountry", "type": "VARCHAR(2)", "null": true, "default": null, "foreignTable": "country", "foreignKey": "country_code2" }, - "billing_prices_supplier": { - "name": "billing_prices_supplier", + "billing_price_supplier": { + "name": "billing_price_supplier", "type": "INT", "null": true, "default": null, "foreignTable": "suppliermgmt_supplier", "foreignKey": "suppliermgmt_supplier_id" }, - "billing_prices_unit": { - "name": "billing_prices_unit", + "billing_price_unit": { + "name": "billing_price_unit", "type": "INT", "null": true, - "default": null, - "foreignTable": "unit", - "foreignKey": "unit_id" + "default": null }, - "billing_prices_type": { - "name": "billing_prices_type", + "billing_price_type": { + "name": "billing_price_type", "type": "TINYINT(1)", "null": false }, - "billing_prices_quantity": { - "name": "billing_prices_quantity", + "billing_price_quantity": { + "name": "billing_price_quantity", "type": "BIGINT", "null": false }, - "billing_prices_price": { - "name": "billing_prices_price", + "billing_price_price": { + "name": "billing_price_price", "type": "BIGINT", "null": false }, - "billing_prices_discount": { - "name": "billing_prices_discount", + "billing_price_price_new": { + "name": "billing_price_price_new", "type": "BIGINT", "null": false }, - "billing_prices_discountp": { - "name": "billing_prices_discountp", + "billing_price_discount": { + "name": "billing_price_discount", "type": "BIGINT", "null": false }, - "billing_prices_bonus": { - "name": "billing_prices_bonus", + "billing_price_discountp": { + "name": "billing_price_discountp", "type": "BIGINT", "null": false }, - "billing_prices_multiply": { - "name": "billing_prices_multiply", + "billing_price_bonus": { + "name": "billing_price_bonus", + "type": "BIGINT", + "null": false + }, + "billing_price_multiply": { + "name": "billing_price_multiply", "type": "TINYINT(1)", "null": false }, - "billing_prices_currency": { - "name": "billing_prices_currency", + "billing_price_currency": { + "name": "billing_price_currency", "type": "VARCHAR(3)", "null": true, "default": null, "foreignTable": "currency", "foreignKey": "currency_code" }, - "billing_prices_start": { - "name": "billing_prices_start", + "billing_price_start": { + "name": "billing_price_start", "type": "DATETIME", "null": true, "default": null }, - "billing_prices_end": { - "name": "billing_prices_end", + "billing_price_end": { + "name": "billing_price_end", + "type": "DATETIME", + "null": true, + "default": null + } + } + }, + "billing_tax": { + "name": "billing_tax", + "fields": { + "billing_tax_id": { + "name": "billing_tax_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "billing_tax_client_code": { + "name": "billing_tax_client_code", + "type": "INT", + "null": true, + "default": null, + "foreignTable": "clientmgmt_attr_value", + "foreignKey": "clientmgmt_attr_value_id" + }, + "billing_tax_supplier_code": { + "name": "billing_tax_supplier_code", + "type": "INT", + "null": true, + "default": null, + "foreignTable": "suppliermgmt_attr_value", + "foreignKey": "suppliermgmt_attr_value_id" + }, + "billing_tax_item_code": { + "name": "billing_tax_item_code", + "type": "INT", + "null": false, + "foreignTable": "itemmgmt_attr_value", + "foreignKey": "itemmgmt_attr_value_id" + }, + "billing_tax_code": { + "description": "tax abbr. code", + "name": "billing_tax_code", + "type": "VARCHAR(10)", + "null": false + }, + "billing_tax_type": { + "description": "sales/purchase", + "name": "billing_tax_type", + "type": "TINYINT", + "null": false + }, + "billing_tax_account": { + "name": "billing_tax_account", + "type": "VARCHAR(10)", + "null": false, + "default": null + }, + "billing_tax_refund_account": { + "name": "billing_tax_refund_account", + "type": "VARCHAR(10)", + "null": false, + "default": null + }, + "billing_tax_discount_account": { + "name": "billing_tax_discount_account", + "type": "VARCHAR(10)", + "null": false, + "default": null + }, + "billing_tax_min_price": { + "name": "billing_tax_min_price", + "type": "BIGINT", + "null": true, + "default": null + }, + "billing_tax_max_price": { + "name": "billing_tax_max_price", + "type": "BIGINT", + "null": true, + "default": null + }, + "billing_tax_start": { + "name": "billing_tax_start", + "type": "DATETIME", + "null": true, + "default": null + }, + "billing_tax_end": { + "name": "billing_tax_end", "type": "DATETIME", "null": true, "default": null @@ -444,10 +536,17 @@ "type": "DATETIME", "null": false }, + "billing_bill_date": { + "name": "billing_bill_date", + "type": "DATETIME", + "default": null, + "null": true + }, "billing_bill_performance_date": { "name": "billing_bill_performance_date", "type": "DATETIME", - "null": false + "default": null, + "null": true }, "billing_bill_created_by": { "name": "billing_bill_created_by", @@ -799,6 +898,235 @@ } } }, + "billing_attr_type": { + "name": "billing_attr_type", + "fields": { + "billing_attr_type_id": { + "name": "billing_attr_type_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "billing_attr_type_name": { + "name": "billing_attr_type_name", + "type": "VARCHAR(255)", + "null": false, + "unique": true + }, + "billing_attr_type_datatype": { + "name": "billing_attr_type_datatype", + "type": "INT(11)", + "null": false + }, + "billing_attr_type_fields": { + "name": "billing_attr_type_fields", + "type": "INT(11)", + "null": false + }, + "billing_attr_type_custom": { + "name": "billing_attr_type_custom", + "type": "TINYINT(1)", + "null": false + }, + "billing_attr_type_required": { + "description": "Every item must have this attribute type if set to true.", + "name": "billing_attr_type_required", + "type": "TINYINT(1)", + "null": false + }, + "billing_attr_type_pattern": { + "description": "This is a regex validation pattern.", + "name": "billing_attr_type_pattern", + "type": "VARCHAR(255)", + "null": false + } + } + }, + "billing_attr_type_l11n": { + "name": "billing_attr_type_l11n", + "fields": { + "billing_attr_type_l11n_id": { + "name": "billing_attr_type_l11n_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "billing_attr_type_l11n_title": { + "name": "billing_attr_type_l11n_title", + "type": "VARCHAR(255)", + "null": false + }, + "billing_attr_type_l11n_type": { + "name": "billing_attr_type_l11n_type", + "type": "INT(11)", + "null": false, + "foreignTable": "billing_attr_type", + "foreignKey": "billing_attr_type_id" + }, + "billing_attr_type_l11n_lang": { + "name": "billing_attr_type_l11n_lang", + "type": "VARCHAR(2)", + "null": false, + "foreignTable": "language", + "foreignKey": "language_639_1" + } + } + }, + "billing_attr_value": { + "name": "billing_attr_value", + "fields": { + "billing_attr_value_id": { + "name": "billing_attr_value_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "billing_attr_value_default": { + "name": "billing_attr_value_default", + "type": "TINYINT(1)", + "null": false + }, + "billing_attr_value_valueStr": { + "name": "billing_attr_value_valueStr", + "type": "VARCHAR(255)", + "null": true, + "default": null + }, + "billing_attr_value_valueInt": { + "name": "billing_attr_value_valueInt", + "type": "INT(11)", + "null": true, + "default": null + }, + "billing_attr_value_valueDec": { + "name": "billing_attr_value_valueDec", + "type": "DECIMAL(19,5)", + "null": true, + "default": null + }, + "billing_attr_value_valueDat": { + "name": "billing_attr_value_valueDat", + "type": "DATETIME", + "null": true, + "default": null + }, + "billing_attr_value_unit": { + "name": "billing_attr_value_unit", + "type": "VARCHAR(255)", + "null": false + }, + "billing_attr_value_deptype": { + "name": "billing_attr_value_deptype", + "type": "INT(11)", + "null": true, + "default": null, + "foreignTable": "billing_attr_type", + "foreignKey": "billing_attr_type_id" + }, + "billing_attr_value_depvalue": { + "name": "billing_attr_value_depvalue", + "type": "INT(11)", + "null": true, + "default": null, + "foreignTable": "billing_attr_value", + "foreignKey": "billing_attr_value_id" + } + } + }, + "billing_attr_value_l11n": { + "name": "billing_attr_value_l11n", + "fields": { + "billing_attr_value_l11n_id": { + "name": "billing_attr_value_l11n_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "billing_attr_value_l11n_title": { + "name": "billing_attr_value_l11n_title", + "type": "VARCHAR(255)", + "null": false + }, + "billing_attr_value_l11n_value": { + "name": "billing_attr_value_l11n_value", + "type": "INT(11)", + "null": false, + "foreignTable": "billing_attr_value", + "foreignKey": "billing_attr_value_id" + }, + "billing_attr_value_l11n_lang": { + "name": "billing_attr_value_l11n_lang", + "type": "VARCHAR(2)", + "null": false, + "foreignTable": "language", + "foreignKey": "language_639_1" + } + } + }, + "billing_bill_attr_default": { + "name": "billing_bill_attr_default", + "fields": { + "billing_bill_attr_default_id": { + "name": "billing_bill_attr_default_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "billing_bill_attr_default_type": { + "name": "billing_bill_attr_default_type", + "type": "INT(11)", + "null": false, + "foreignTable": "billing_attr_type", + "foreignKey": "billing_attr_type_id" + }, + "billing_bill_attr_default_value": { + "name": "billing_bill_attr_default_value", + "type": "INT(11)", + "null": false, + "foreignTable": "billing_attr_value", + "foreignKey": "billing_attr_value_id" + } + } + }, + "billing_bill_attr": { + "name": "billing_bill_attr", + "fields": { + "billing_bill_attr_id": { + "name": "billing_bill_attr_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "billing_bill_attr_bill": { + "name": "billing_bill_attr_bill", + "type": "INT(11)", + "null": false, + "foreignTable": "billing_bill", + "foreignKey": "billing_bill_id" + }, + "billing_bill_attr_type": { + "name": "billing_bill_attr_type", + "type": "INT(11)", + "null": false, + "foreignTable": "billing_attr_type", + "foreignKey": "billing_attr_type_id" + }, + "billing_bill_attr_value": { + "name": "billing_bill_attr_value", + "type": "INT(11)", + "null": true, + "default": null, + "foreignTable": "billing_attr_value", + "foreignKey": "billing_attr_value_id" + } + } + }, "billing_bill_media": { "name": "billing_bill_media", "fields": { diff --git a/Admin/Install/taxes.json b/Admin/Install/taxes.json new file mode 100644 index 0000000..8e057f1 --- /dev/null +++ b/Admin/Install/taxes.json @@ -0,0 +1,362 @@ +[ + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "EU", + "tax_code": "EU_S0" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "AT", + "tax_code": "AT_S20" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "BE", + "tax_code": "BE_S21" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "BG", + "tax_code": "BG_S20" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "HR", + "tax_code": "HR_S25" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "CY", + "tax_code": "CY_S19" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "CZ", + "tax_code": "CZ_S21" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "DK", + "tax_code": "DK_S25" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "EE", + "tax_code": "EE_S20" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "FI", + "tax_code": "FI_S24" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "FR", + "tax_code": "FR_S20" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "DE", + "tax_code": "DE_S19" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "GR", + "tax_code": "GR_S24" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "HU", + "tax_code": "HU_S27" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "IE", + "tax_code": "IE_S23" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "IT", + "tax_code": "IT_S22" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "LV", + "tax_code": "LV_S21" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "LT", + "tax_code": "LT_S21" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "LU", + "tax_code": "LU_S17" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "MT", + "tax_code": "MT_S18" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "NL", + "tax_code": "NL_S21" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "PL", + "tax_code": "PL_S23" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "PT", + "tax_code": "PT_S23" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "RO", + "tax_code": "RO_S19" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "SK", + "tax_code": "SK_S20" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "SI", + "tax_code": "SI_S22" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "ES", + "tax_code": "ES_S21" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "SE", + "tax_code": "SE_S25" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "GB", + "tax_code": "GB_S20" + }, + { + "type": 1, + "item_code": "SOFTWARE", + "account_code": "INT", + "tax_code": "S0" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "EU", + "tax_code": "EU_S0" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "AT", + "tax_code": "AT_S20" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "BE", + "tax_code": "BE_S21" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "BG", + "tax_code": "BG_S20" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "HR", + "tax_code": "HR_S25" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "CY", + "tax_code": "CY_S19" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "CZ", + "tax_code": "CZ_S21" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "DK", + "tax_code": "DK_S25" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "EE", + "tax_code": "EE_S20" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "FI", + "tax_code": "FI_S24" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "FR", + "tax_code": "FR_S20" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "DE", + "tax_code": "DE_S19" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "GR", + "tax_code": "GR_S24" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "HU", + "tax_code": "HU_S27" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "IE", + "tax_code": "IE_S23" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "IT", + "tax_code": "IT_S22" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "LV", + "tax_code": "LV_S21" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "LT", + "tax_code": "LT_S21" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "LU", + "tax_code": "LU_S17" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "MT", + "tax_code": "MT_S18" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "NL", + "tax_code": "NL_S21" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "PL", + "tax_code": "PL_S23" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "PT", + "tax_code": "PT_S23" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "RO", + "tax_code": "RO_S19" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "SK", + "tax_code": "SK_S20" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "SI", + "tax_code": "SI_S22" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "ES", + "tax_code": "ES_S21" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "SE", + "tax_code": "SE_S25" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "GB", + "tax_code": "GB_S20" + }, + { + "type": 1, + "item_code": "SERVICE", + "account_code": "INT", + "tax_code": "S0" + } +] \ No newline at end of file diff --git a/Admin/Install/types.json b/Admin/Install/types.json index 8850d52..a12fa9e 100644 --- a/Admin/Install/types.json +++ b/Admin/Install/types.json @@ -200,7 +200,7 @@ { "name": "purchase_subscritpion", "numberFormat": "{y}-{id}", - "transferType": 1, + "transferType": 2, "transferStock": false, "isTemplate": false, "l11n": { @@ -211,7 +211,7 @@ { "name": "purchase_template", "numberFormat": "{y}-{id}", - "transferType": 1, + "transferType": 2, "transferStock": false, "isTemplate": true, "l11n": { diff --git a/Admin/Installer.php b/Admin/Installer.php index 87e01da..7f496c9 100755 --- a/Admin/Installer.php +++ b/Admin/Installer.php @@ -15,6 +15,9 @@ declare(strict_types=1); namespace Modules\Billing\Admin; use Modules\Billing\Models\BillTransferType; +use Modules\ClientManagement\Models\ClientAttributeTypeMapper; +use Modules\ItemManagement\Models\ItemAttributeTypeMapper; +use Modules\SupplierManagement\Models\SupplierAttributeTypeMapper; use phpOMS\Application\ApplicationAbstract; use phpOMS\Config\SettingsInterface; use phpOMS\Message\Http\HttpRequest; @@ -62,6 +65,208 @@ final class Installer extends InstallerAbstract $types = \json_decode($fileContent, true); self::createBillTypes($app, $types, $defaultTemplate); + + /* Tax types */ + $fileContent = \file_get_contents(__DIR__ . '/Install/taxes.json'); + if ($fileContent === false) { + return; + } + + $taxes = \json_decode($fileContent, true); + self::createTaxCombination($app, $taxes); + + /* Attributes */ + $fileContent = \file_get_contents(__DIR__ . '/Install/attributes.json'); + if ($fileContent === false) { + return; + } + + $attributes = \json_decode($fileContent, true); + $attrTypes = self::createBillAttributeTypes($app, $attributes); + $attrValues = self::createBillAttributeValues($app, $attrTypes, $attributes); + } + + /** + * Install default attribute types + * + * @param ApplicationAbstract $app Application + * @param array, is_required?:bool, is_custom_allowed?:bool, validation_pattern?:string, value_type?:string, values?:array}> $attributes Attribute definition + * + * @return array + * + * @since 1.0.0 + */ + private static function createBillAttributeTypes(ApplicationAbstract $app, array $attributes) : array + { + /** @var array $billAttrType */ + $billAttrType = []; + + /** @var \Modules\Billing\Controller\ApiController $module */ + $module = $app->moduleManager->getModuleInstance('Billing'); + + /** @var array $attribute */ + foreach ($attributes as $attribute) { + $response = new HttpResponse(); + $request = new HttpRequest(new HttpUri('')); + + $request->header->account = 1; + $request->setData('name', $attribute['name'] ?? ''); + $request->setData('title', \reset($attribute['l11n'])); + $request->setData('language', \array_keys($attribute['l11n'])[0] ?? 'en'); + $request->setData('is_required', $attribute['is_required'] ?? false); + $request->setData('is_custom_allowed', $attribute['is_custom_allowed'] ?? false); + $request->setData('validation_pattern', $attribute['validation_pattern'] ?? ''); + $request->setData('datatype', (int) $attribute['value_type']); + + $module->apiBillAttributeTypeCreate($request, $response); + + $responseData = $response->get(''); + + if (!\is_array($responseData)) { + continue; + } + + $billAttrType[$attribute['name']] = !\is_array($responseData['response']) + ? $responseData['response']->toArray() + : $responseData['response']; + + $isFirst = true; + foreach ($attribute['l11n'] as $language => $l11n) { + if ($isFirst) { + $isFirst = false; + continue; + } + + $response = new HttpResponse(); + $request = new HttpRequest(new HttpUri('')); + + $request->header->account = 1; + $request->setData('title', $l11n); + $request->setData('language', $language); + $request->setData('type', $billAttrType[$attribute['name']]['id']); + + $module->apiBillAttributeTypeL11nCreate($request, $response); + } + } + + return $billAttrType; + } + + /** + * Create default attribute values for types + * + * @param ApplicationAbstract $app Application + * @param array $billAttrType Attribute types + * @param array, is_required?:bool, is_custom_allowed?:bool, validation_pattern?:string, value_type?:string, values?:array}> $attributes Attribute definition + * + * @return array + * + * @since 1.0.0 + */ + private static function createBillAttributeValues(ApplicationAbstract $app, array $billAttrType, array $attributes) : array + { + /** @var array $billAttrValue */ + $billAttrValue = []; + + /** @var \Modules\Billing\Controller\ApiController $module */ + $module = $app->moduleManager->getModuleInstance('Billing'); + + foreach ($attributes as $attribute) { + $billAttrValue[$attribute['name']] = []; + + /** @var array $value */ + foreach ($attribute['values'] as $value) { + $response = new HttpResponse(); + $request = new HttpRequest(new HttpUri('')); + + $request->header->account = 1; + $request->setData('value', $value['value'] ?? ''); + $request->setData('unit', $value['unit'] ?? ''); + $request->setData('default', true); // always true since all defined values are possible default values + $request->setData('type', $billAttrType[$attribute['name']]['id']); + + if (isset($value['l11n']) && !empty($value['l11n'])) { + $request->setData('title', \reset($value['l11n'])); + $request->setData('language', \array_keys($value['l11n'])[0] ?? 'en'); + } + + $module->apiBillAttributeValueCreate($request, $response); + + $responseData = $response->get(''); + if (!\is_array($responseData)) { + continue; + } + + $attrValue = !\is_array($responseData['response']) + ? $responseData['response']->toArray() + : $responseData['response']; + + $billAttrValue[$attribute['name']][] = $attrValue; + + $isFirst = true; + foreach (($value['l11n'] ?? []) as $language => $l11n) { + if ($isFirst) { + $isFirst = false; + continue; + } + + $response = new HttpResponse(); + $request = new HttpRequest(new HttpUri('')); + + $request->header->account = 1; + $request->setData('title', $l11n); + $request->setData('language', $language); + $request->setData('value', $attrValue['id']); + + $module->apiBillAttributeValueL11nCreate($request, $response); + } + } + } + + return $billAttrValue; + } + + private static function createTaxCombination(ApplicationAbstract $app, array $taxes) : array + { + $result = []; + + /** @var \Modules\Billing\Controller\ApiController $module */ + $module = $app->moduleManager->getModuleInstance('Billing'); + + $itemAttributeSales = ItemAttributeTypeMapper::get()->with('defaults')->where('name', 'sales_tax_code')->execute(); + $clientAttributeSales = ClientAttributeTypeMapper::get()->with('defaults')->where('name', 'sales_tax_code')->execute(); + $supplierAttributeSales = SupplierAttributeTypeMapper::get()->with('defaults')->where('name', 'purchase_tax_code')->execute(); + + foreach ($taxes as $tax) { + $itemValue = $itemAttributeSales->getDefaultByValue($tax['item_code']); + $accountValue = $tax['type'] === 1 + ? $clientAttributeSales->getDefaultByValue($tax['account_code']) + : $supplierAttributeSales->getDefaultByValue($tax['account_code']); + + $response = new HttpResponse(); + $request = new HttpRequest(new HttpUri('')); + + $request->header->account = 1; + $request->setData('tax_type', $tax['type']); + $request->setData('tax_code', $tax['tax_code']); + $request->setData('item_code', $itemValue->getId()); + $request->setData('account_code', $accountValue->getId()); + + $module->apiTaxCombinationCreate($request, $response); + + $responseData = $response->get(''); + if (!\is_array($responseData)) { + continue; + } + + $result = !\is_array($responseData['response']) + ? $responseData['response']->toArray() + : $responseData['response']; + + $results[] = $result; + } + + return $result; } /** diff --git a/Admin/Routes/Cli.php b/Admin/Routes/Cli.php new file mode 100644 index 0000000..278c7cc --- /dev/null +++ b/Admin/Routes/Cli.php @@ -0,0 +1,13 @@ + [ + [ + 'dest' => '\Modules\Billing\Controller\CliController:cliParseSupplierBill', + 'verb' => RouteVerb::ANY, + ], + ], +]; diff --git a/Controller/ApiAttributeController.php b/Controller/ApiAttributeController.php new file mode 100644 index 0000000..9588b87 --- /dev/null +++ b/Controller/ApiAttributeController.php @@ -0,0 +1,419 @@ +validateBillAttributeCreate($request))) { + $response->set('attribute_create', new FormValidation($val)); + $response->header->status = RequestStatusCode::R_400; + + return; + } + + $attribute = $this->createBillAttributeFromRequest($request); + $this->createModel($request->header->account, $attribute, BillAttributeMapper::class, 'attribute', $request->getOrigin()); + + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Attribute', 'Attribute successfully created', $attribute); + } + + /** + * Method to create item attribute from request. + * + * @param RequestAbstract $request Request + * + * @return BillAttribute + * + * @since 1.0.0 + */ + private function createBillAttributeFromRequest(RequestAbstract $request) : BillAttribute + { + $attribute = new BillAttribute(); + $attribute->bill = (int) $request->getData('bill'); + $attribute->type = new NullBillAttributeType((int) $request->getData('type')); + + if ($request->getData('value') !== null) { + $attribute->value = new NullBillAttributeValue((int) $request->getData('value')); + } else { + $newRequest = clone $request; + $newRequest->setData('value', $request->getData('custom'), true); + + $value = $this->createBillAttributeValueFromRequest($newRequest); + + $attribute->value = $value; + } + + return $attribute; + } + + /** + * Validate bill attribute create request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateBillAttributeCreate(RequestAbstract $request) : array + { + $val = []; + if (($val['type'] = empty($request->getData('type'))) + || ($val['value'] = (empty($request->getData('value')) && empty($request->getData('custom')))) + || ($val['bill'] = empty($request->getData('bill'))) + ) { + return $val; + } + + return []; + } + + /** + * Api method to create bill attribute l11n + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiBillAttributeTypeL11nCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void + { + if (!empty($val = $this->validateBillAttributeTypeL11nCreate($request))) { + $response->set('attr_type_l11n_create', new FormValidation($val)); + $response->header->status = RequestStatusCode::R_400; + + return; + } + + $attrL11n = $this->createBillAttributeTypeL11nFromRequest($request); + $this->createModel($request->header->account, $attrL11n, BillAttributeTypeL11nMapper::class, 'attr_type_l11n', $request->getOrigin()); + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Localization', 'Localization successfully created', $attrL11n); + } + + /** + * Method to create bill attribute l11n from request. + * + * @param RequestAbstract $request Request + * + * @return BaseStringL11n + * + * @since 1.0.0 + */ + private function createBillAttributeTypeL11nFromRequest(RequestAbstract $request) : BaseStringL11n + { + $attrL11n = new BaseStringL11n(); + $attrL11n->ref = (int) ($request->getData('type') ?? 0); + $attrL11n->setLanguage((string) ( + $request->getData('language') ?? $request->getLanguage() + )); + $attrL11n->content = (string) ($request->getData('title') ?? ''); + + return $attrL11n; + } + + /** + * Validate bill attribute l11n create request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateBillAttributeTypeL11nCreate(RequestAbstract $request) : array + { + $val = []; + if (($val['title'] = empty($request->getData('title'))) + || ($val['type'] = empty($request->getData('type'))) + ) { + return $val; + } + + return []; + } + + /** + * Api method to create bill attribute type + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiBillAttributeTypeCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void + { + if (!empty($val = $this->validateBillAttributeTypeCreate($request))) { + $response->set('attr_type_create', new FormValidation($val)); + $response->header->status = RequestStatusCode::R_400; + + return; + } + + $attrType = $this->createBillAttributeTypeFromRequest($request); + $this->createModel($request->header->account, $attrType, BillAttributeTypeMapper::class, 'attr_type', $request->getOrigin()); + + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Attribute type', 'Attribute type successfully created', $attrType); + } + + /** + * Method to create bill attribute from request. + * + * @param RequestAbstract $request Request + * + * @return BillAttributeType + * + * @since 1.0.0 + */ + private function createBillAttributeTypeFromRequest(RequestAbstract $request) : BillAttributeType + { + $attrType = new BillAttributeType($request->getData('name') ?? ''); + $attrType->datatype = (int) ($request->getData('datatype') ?? 0); + $attrType->custom = (bool) ($request->getData('custom') ?? false); + $attrType->isRequired = (bool) ($request->getData('is_required') ?? false); + $attrType->validationPattern = (string) ($request->getData('validation_pattern') ?? ''); + $attrType->setL11n((string) ($request->getData('title') ?? ''), $request->getData('language') ?? ISO639x1Enum::_EN); + $attrType->setFields((int) ($request->getData('fields') ?? 0)); + + return $attrType; + } + + /** + * Validate bill attribute create request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateBillAttributeTypeCreate(RequestAbstract $request) : array + { + $val = []; + if (($val['title'] = empty($request->getData('title'))) + || ($val['name'] = empty($request->getData('name'))) + ) { + return $val; + } + + return []; + } + + /** + * Api method to create bill attribute value + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiBillAttributeValueCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void + { + if (!empty($val = $this->validateBillAttributeValueCreate($request))) { + $response->set('attr_value_create', new FormValidation($val)); + $response->header->status = RequestStatusCode::R_400; + + return; + } + + $attrValue = $this->createBillAttributeValueFromRequest($request); + $this->createModel($request->header->account, $attrValue, BillAttributeValueMapper::class, 'attr_value', $request->getOrigin()); + + if ($attrValue->isDefault) { + $this->createModelRelation( + $request->header->account, + (int) $request->getData('type'), + $attrValue->getId(), + BillAttributeTypeMapper::class, 'defaults', '', $request->getOrigin() + ); + } + + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Attribute value', 'Attribute value successfully created', $attrValue); + } + + /** + * Method to create bill attribute value from request. + * + * @param RequestAbstract $request Request + * + * @return BillAttributeValue + * + * @since 1.0.0 + */ + private function createBillAttributeValueFromRequest(RequestAbstract $request) : BillAttributeValue + { + /** @var BillAttributeType $type */ + $type = BillAttributeTypeMapper::get() + ->where('id', (int) ($request->getData('type') ?? 0)) + ->execute(); + + $attrValue = new BillAttributeValue(); + $attrValue->isDefault = (bool) ($request->getData('default') ?? false); + $attrValue->setValue($request->getData('value'), $type->datatype); + + if ($request->getData('title') !== null) { + $attrValue->setL11n($request->getData('title'), $request->getData('language') ?? ISO639x1Enum::_EN); + } + + return $attrValue; + } + + /** + * Validate bill attribute value create request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateBillAttributeValueCreate(RequestAbstract $request) : array + { + $val = []; + if (($val['type'] = empty($request->getData('type'))) + || ($val['value'] = empty($request->getData('value'))) + ) { + return $val; + } + + return []; + } + + /** + * Api method to create bill attribute l11n + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiBillAttributeValueL11nCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void + { + if (!empty($val = $this->validateBillAttributeValueL11nCreate($request))) { + $response->set('attr_value_l11n_create', new FormValidation($val)); + $response->header->status = RequestStatusCode::R_400; + + return; + } + + $attrL11n = $this->createBillAttributeValueL11nFromRequest($request); + $this->createModel($request->header->account, $attrL11n, BillAttributeValueL11nMapper::class, 'attr_value_l11n', $request->getOrigin()); + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Localization', 'Localization successfully created', $attrL11n); + } + + /** + * Method to create bill attribute l11n from request. + * + * @param RequestAbstract $request Request + * + * @return BaseStringL11n + * + * @since 1.0.0 + */ + private function createBillAttributeValueL11nFromRequest(RequestAbstract $request) : BaseStringL11n + { + $attrL11n = new BaseStringL11n(); + $attrL11n->ref = (int) ($request->getData('value') ?? 0); + $attrL11n->setLanguage((string) ( + $request->getData('language') ?? $request->getLanguage() + )); + $attrL11n->content = (string) ($request->getData('title') ?? ''); + + return $attrL11n; + } + + /** + * Validate bill attribute l11n create request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateBillAttributeValueL11nCreate(RequestAbstract $request) : array + { + $val = []; + if (($val['title'] = empty($request->getData('title'))) + || ($val['value'] = empty($request->getData('value'))) + ) { + return $val; + } + + return []; + } +} diff --git a/Controller/ApiBillController.php b/Controller/ApiBillController.php new file mode 100644 index 0000000..def2d61 --- /dev/null +++ b/Controller/ApiBillController.php @@ -0,0 +1,826 @@ +validateBillUpdate($request))) { + $response->set($request->uri->__toString(), new FormValidation($val)); + $response->header->status = RequestStatusCode::R_400; + + return; + } + + /** @var \Modules\Billing\Models\Bill $old */ + $old = BillMapper::get()->where('id', (int) $request->getData('bill')); + $new = $this->updateBillFromRequest($request, $response, $data); + $this->updateModel($request->header->account, $old, $new, BillMapper::class, 'bill', $request->getOrigin()); + + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Bill', 'Bill successfully created.', $new); + } + + /** + * Method to validate bill creation from request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateBillUpdate(RequestAbstract $request) : array + { + $val = []; + if (($val['bill'] = empty($request->getData('bill')))) { + return $val; + } + + return []; + } + + /** + * Method to create a bill from request. + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return Bill + * + * @since 1.0.0 + */ + public function updateBillFromRequest(RequestAbstract $request, ResponseAbstract $response, $data = null) : Bill + { + /** @var Bill $bill */ + $bill = BillMapper::get()->where('id', (int) $request->getData('bill'))->execute(); + + return $bill; + } + + /** + * Api method to create a bill + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiBillCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void + { + if (!empty($val = $this->validateBillCreate($request))) { + $response->set($request->uri->__toString(), new FormValidation($val)); + $response->header->status = RequestStatusCode::R_400; + + return; + } + + $bill = $this->createBillFromRequest($request, $response, $data); + $this->createModel($request->header->account, $bill, BillMapper::class, 'bill', $request->getOrigin()); + + $new = clone $bill; + $new->buildNumber(); // The bill id is part of the number + $this->updateModel($request->header->account, $bill, $new, BillMapper::class, 'bill', $request->getOrigin()); + + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Bill', 'Bill successfully created.', $bill); + } + + /** + * Method to create a bill from request. + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return Bill + * + * @since 1.0.0 + */ + public function createBillFromRequest(RequestAbstract $request, ResponseAbstract $response, $data = null) : Bill + { + /** @var \Modules\ClientManagement\Models\Client|\Modules\SupplierManagement\Models\Supplier $account */ + $account = null; + if ($request->getData('client') !== null) { + /** @var \Modules\ClientManagement\Models\Client $account */ + $account = ClientMapper::get() + ->with('account') + ->with('mainAddress') + ->where('id', (int) $request->getData('client')) + ->execute(); + } elseif (((int) ($request->getData('supplier') ?? -1)) === 0) { + /** @var \Modules\SupplierManagement\Models\Supplier $account */ + $account = new NullSupplier(); + } elseif ($request->getData('supplier') !== null) { + /** @var \Modules\SupplierManagement\Models\Supplier $account */ + $account = SupplierMapper::get() + ->with('account') + ->with('mainAddress') + ->where('id', (int) $request->getData('supplier')) + ->execute(); + } + + /** @var \Modules\Billing\Models\BillType $billType */ + $billType = BillTypeMapper::get() + ->where('id', (int) ($request->getData('type') ?? 1)) + ->execute(); + + /* @var \Modules\Account\Models\Account $account */ + $bill = new Bill(); + $bill->createdBy = new NullAccount($request->header->account); + $bill->type = $billType; + $bill->numberFormat = $billType->numberFormat; + // @todo: use defaultInvoiceAddress or mainAddress. also consider to use billto1, billto2, billto3 (for multiple lines e.g. name2, fao etc.) + $bill->billTo = (string) ($request->getData('billto') + ?? ($account->account->name1 . (!empty($account->account->name2) + ? ', ' . $account->account->name2 + : '' + ))); + $bill->billAddress = (string) ($request->getData('billaddress') ?? $account->mainAddress->address); + $bill->billZip = (string) ($request->getData('billtopostal') ?? $account->mainAddress->postal); + $bill->billCity = (string) ($request->getData('billtocity') ?? $account->mainAddress->city); + $bill->billCountry = (string) ( + $request->getData('billtocountry') ?? ( + ($country = $account->mainAddress->getCountry()) === ISO3166TwoEnum::_XXX ? '' : $country) + ); + $bill->client = !$request->hasData('client') ? null : $account; + $bill->supplier = !$request->hasData('supplier') ? null : $account; + $bill->performanceDate = new \DateTime($request->getData('performancedate') ?? 'now'); + $bill->setStatus((int) ($request->getData('status') ?? BillStatus::ACTIVE)); + + return $bill; + } + + /** + * Method to validate bill creation from request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateBillCreate(RequestAbstract $request) : array + { + $val = []; + if (($val['client/supplier'] = (empty($request->getData('client')) + && (empty($request->getData('supplier')) + && ((int) ($request->getData('supplier') ?? -1) !== 0) + ))) + || ($val['type'] = (empty($request->getData('type')))) + ) { + return $val; + } + + return []; + } + + /** + * Api method to create a bill + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiMediaAddToBill(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void + { + if (!empty($val = $this->validateMediaAddToBill($request))) { + $response->set($request->uri->__toString(), new FormValidation($val)); + $response->header->status = RequestStatusCode::R_400; + + return; + } + + /** @var \Modules\Billing\Models\Bill $bill */ + $bill = BillMapper::get()->where('id', (int) $request->getData('bill'))->execute(); + $path = $this->createBillDir($bill); + + $uploaded = []; + if (!empty($uploadedFiles = $request->getFiles())) { + $uploaded = $this->app->moduleManager->get('Media')->uploadFiles( + names: [], + fileNames: [], + files: $uploadedFiles, + account: $request->header->account, + basePath: __DIR__ . '/../../../Modules/Media/Files' . $path, + virtualPath: $path, + pathSettings: PathSettings::FILE_PATH, + hasAccountRelation: false, + readContent: (bool) ($request->getData('parse_content') ?? false) + ); + + $collection = null; + foreach ($uploaded as $media) { + $this->createModelRelation( + $request->header->account, + $bill->getId(), + $media->getId(), + BillMapper::class, + 'media', + '', + $request->getOrigin() + ); + + if ($request->hasData('type')) { + $this->createModelRelation( + $request->header->account, + $media->getId(), + $request->getData('type', 'int'), + MediaMapper::class, + 'types', + '', + $request->getOrigin() + ); + } + + if ($collection === null) { + $collection = MediaMapper::getParentCollection($path)->limit(1)->execute(); + + if ($collection instanceof NullCollection) { + $collection = $this->app->moduleManager->get('Media')->createRecursiveMediaCollection( + $path, + $request->header->account, + __DIR__ . '/../../../Modules/Media/Files' . $path, + ); + } + } + + $this->createModelRelation( + $request->header->account, + $collection->getId(), + $media->getId(), + CollectionMapper::class, + 'sources', + '', + $request->getOrigin() + ); + } + } + + if (!empty($mediaFiles = $request->getDataJson('media'))) { + foreach ($mediaFiles as $media) { + $this->createModelRelation( + $request->header->account, + $bill->getId(), + (int) $media, + BillMapper::class, + 'media', + '', + $request->getOrigin() + ); + } + } + + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Media', 'Media added to bill.', [ + 'upload' => $uploaded, + 'media' => $mediaFiles, + ]); + } + + /** + * Create media directory path + * + * @param Bill $bill Bill + * + * @return string + * + * @since 1.0.0 + */ + private function createBillDir(Bill $bill) : string + { + return '/Modules/Billing/Bills/' + . $this->app->unitId . '/' + . $bill->createdAt->format('Y') . '/' + . $bill->createdAt->format('m') . '/' + . $bill->createdAt->format('d') . '/' + . $bill->getId(); + } + + /** + * Method to validate bill creation from request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateMediaAddToBill(RequestAbstract $request) : array + { + $val = []; + if (($val['media'] = (empty($request->getData('media')) && empty($request->getFiles()))) + || ($val['bill'] = empty($request->getData('bill'))) + ) { + return $val; + } + + return []; + } + + /** + * Api method to create a bill element + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiBillElementCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void + { + if (!empty($val = $this->validateBillElementCreate($request))) { + $response->set($request->uri->__toString(), new FormValidation($val)); + $response->header->status = RequestStatusCode::R_400; + + return; + } + + $element = $this->createBillElementFromRequest($request, $response, $data); + $this->createModel($request->header->account, $element, BillElementMapper::class, 'bill_element', $request->getOrigin()); + + /** @var \Modules\Billing\Models\Bill $old */ + $old = BillMapper::get()->where('id', $element->bill)->execute(); + $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); + } + + /** + * Method to create a bill element from request. + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return BillElement + * + * @since 1.0.0 + * @todo in the database the customer localized version should be stored because this is the version which went out + */ + public function createBillElementFromRequest(RequestAbstract $request, ResponseAbstract $response, $data = null) : BillElement + { + $element = new BillElement(); + $element->bill = (int) $request->getData('bill'); + $element->item = (int) ($request->getData('item') ?? 0); + + if ($element->item === null) { + return $element; + } + + /** @var \Modules\ItemManagement\Models\Item $item */ + $item = ItemMapper::get() + ->with('l11n') + ->with('l11n/type') + ->where('id', $element->item) + ->where('l11n/type/title', ['name1', 'name2', 'name3'], 'IN') + ->where('l11n/language', $response->getLanguage()) + ->execute(); + + $element->itemNumber = $item->number; + $element->itemName = $item->getL11n('name1')->description; + $element->quantity = (int) ($request->getData('quantity') ?? 0); + + $element->singleSalesPriceNet = new Money($request->getData('singlesalespricenet', 'int') ?? $item->salesPrice->getInt()); + $element->totalSalesPriceNet = clone $element->singleSalesPriceNet; + $element->totalSalesPriceNet->mult($element->quantity); + + // discounts + if ($request->getData('discount_percentage') !== null) { + $discount = (int) $request->getData('discount_percentage'); + + $element->singleSalesPriceNet + ->sub((int) ($element->singleSalesPriceNet->getInt() / 100 * $discount)); + + $element->totalSalesPriceNet + ->sub((int) ($element->totalSalesPriceNet->getInt() / 100 * $discount)); + } + + $element->singlePurchasePriceNet = new Money($item->purchasePrice->getInt()); + $element->totalPurchasePriceNet = clone $element->singlePurchasePriceNet; + $element->totalPurchasePriceNet->mult($element->quantity); + + return $element; + } + + /** + * Method to update a bill because of a changed bill element (add, remove, change) from request. + * + * @param Bill $bill Bill + * @param BillElement $element Bill element + * @param int $type Change type (0 = update, -1 = remove, +1 = add) + * + * @return Bill + * + * @since 1.0.0 + */ + public function updateBillWithBillElement(Bill $bill, BillElement $element, int $type = 1) : Bill + { + if ($type === 1) { + $bill->netSales->add($element->totalSalesPriceNet); + $bill->netCosts->add($element->totalPurchasePriceNet); + } + + return $bill; + } + + /** + * Method to validate bill element creation from request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateBillElementCreate(RequestAbstract $request) : array + { + $val = []; + if (($val['bill'] = empty($request->getData('bill')))) { + return $val; + } + + return []; + } + + public function apiPreviewRender(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void + { + Autoloader::addPath(__DIR__ . '/../../../Resources/'); + + $templateId = $request->getData('bill_template', 'int'); + if ($templateId === null) { + $billTypeId = $request->getData('bill_type', 'int'); + $billType = BillTypeMapper::get() + ->where('id', $billTypeId) + ->execute(); + + $templateId = $billType->defaultTemplate->getId(); + } + + $template = CollectionMapper::get() + ->with('sources') + ->where('id', $templateId) + ->execute(); + + require_once __DIR__ . '/../../../Resources/tcpdf/tcpdf.php'; + + $response->header->set('Content-Type', MimeType::M_PDF, true); + + $view = new View($this->app->l11nManager, $request, $response); + $view->setTemplate('/' . \substr($template->getSourceByName('bill.pdf.php')->getPath(), 0, -8), 'pdf.php'); + + $settings = $this->app->appSettings->get(null, + [ + AdminSettingsEnum::DEFAULT_TEMPLATES, + AdminSettingsEnum::DEFAULT_ASSETS, + ], + unit: $this->app->unitId, + module: 'Admin' + ); + + $postKey = '::' . $this->app->unitId . ':Admin'; + + if ($settings === false) { + $settings = $this->app->appSettings->get(null, + [ + AdminSettingsEnum::DEFAULT_TEMPLATES, + AdminSettingsEnum::DEFAULT_ASSETS, + ], + unit: null, + module: 'Admin' + ); + + $postKey = ':::Admin'; + } + + $defaultTemplates = CollectionMapper::get() + ->with('sources') + ->where('id', (int) $settings[AdminSettingsEnum::DEFAULT_TEMPLATES . $postKey]->content) + ->execute(); + + $defaultAssets = CollectionMapper::get() + ->with('sources') + ->where('id', (int) $settings[AdminSettingsEnum::DEFAULT_ASSETS . $postKey]->content) + ->execute(); + + $view->setData('defaultTemplates', $defaultTemplates); + $view->setData('defaultAssets', $defaultAssets); + + $view->setData('bill', $bill); + $view->setData('path', $pdfDir . '/' . $request->getData('bill') . '.pdf'); + + $view->setData('bill_creator', $request->getData('bill_creator')); + $view->setData('bill_title', $request->getData('bill_title')); + $view->setData('bill_subtitle', $request->getData('bill_subtitle')); + $view->setData('keywords', $request->getData('keywords')); + $view->setData('bill_logo_name', $request->getData('bill_logo_name')); + $view->setData('bill_slogan', $request->getData('bill_slogan')); + + $view->setData('legal_company_name', $request->getData('legal_company_name')); + $view->setData('bill_company_address', $request->getData('bill_company_address')); + $view->setData('bill_company_city', $request->getData('bill_company_city')); + $view->setData('bill_company_ceo', $request->getData('bill_company_ceo')); + $view->setData('bill_company_website', $request->getData('bill_company_website')); + $view->setData('bill_company_email', $request->getData('bill_company_email')); + $view->setData('bill_company_phone', $request->getData('bill_company_phone')); + + $view->setData('bill_company_tax_office', $request->getData('bill_company_tax_office')); + $view->setData('bill_company_tax_id', $request->getData('bill_company_tax_id')); + $view->setData('bill_company_vat_id', $request->getData('bill_company_vat_id')); + + $view->setData('bill_company_bank_name', $request->getData('bill_company_bank_name')); + $view->setData('bill_company_bic', $request->getData('bill_company_bic')); + $view->setData('bill_company_iban', $request->getData('bill_company_iban')); + + $view->setData('bill_type_name', $request->getData('bill_type_name')); + + $view->setData('bill_invoice_no', $request->getData('bill_invoice_no')); + $view->setData('bill_invoice_date', $request->getData('bill_invoice_date')); + $view->setData('bill_service_date', $request->getData('bill_service_date')); + $view->setData('bill_customer_no', $request->getData('bill_customer_no')); + $view->setData('bill_po', $request->getData('bill_po')); + $view->setData('bill_due_date', $request->getData('bill_due_date')); + + $view->setData('bill_start_text', $request->getData('bill_start_text')); + $view->setData('bill_lines', $request->getData('bill_lines')); + $view->setData('bill_end_text', $request->getData('bill_end_text')); + + $view->setData('bill_payment_terms', $request->getData('bill_payment_terms')); + $view->setData('bill_terms', $request->getData('bill_terms')); + $view->setData('bill_taxes', $request->getData('bill_taxes')); + $view->setData('bill_currency', $request->getData('bill_currency')); + + $pdf = $view->render(); + + $response->set('', $pdf); + } + + /** + * Api method to create and archive a bill + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiBillPdfArchiveCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void + { + Autoloader::addPath(__DIR__ . '/../../../Resources/'); + + /** @var \Modules\Billing\Models\Bill $bill */ + $bill = BillMapper::get() + ->with('elements') + ->where('id', $request->getData('bill') ?? 0) + ->execute(); + + $templateId = $request->getData('bill_template', 'int'); + if ($templateId === null) { + $billTypeId = $bill->type->getId(); + $billType = BillTypeMapper::get() + ->where('id', $billTypeId) + ->execute(); + + $templateId = $billType->defaultTemplate->getId(); + } + + $template = CollectionMapper::get() + ->with('sources') + ->where('id', $templateId) + ->execute(); + + require_once __DIR__ . '/../../../Resources/tcpdf/tcpdf.php'; + + $view = new View($this->app->l11nManager, $request, $response); + $view->setTemplate('/' . \substr($template->getSourceByName('bill.pdf.php')->getPath(), 0, -8), 'pdf.php'); + + $settings = $this->app->appSettings->get(null, + [ + AdminSettingsEnum::DEFAULT_TEMPLATES, + AdminSettingsEnum::DEFAULT_ASSETS, + ], + unit: $this->app->unitId, + module: 'Admin' + ); + + $postKey = '::' . $this->app->unitId . ':Admin'; + + if ($settings === false) { + $settings = $this->app->appSettings->get(null, + [ + AdminSettingsEnum::DEFAULT_TEMPLATES, + AdminSettingsEnum::DEFAULT_ASSETS, + ], + unit: null, + module: 'Admin' + ); + + $postKey = ':::Admin'; + } + + $defaultTemplates = CollectionMapper::get() + ->with('sources') + ->where('id', (int) $settings[AdminSettingsEnum::DEFAULT_TEMPLATES . $postKey]->content) + ->execute(); + + $defaultAssets = CollectionMapper::get() + ->with('sources') + ->where('id', (int) $settings[AdminSettingsEnum::DEFAULT_ASSETS . $postKey]->content) + ->execute(); + + $view->setData('defaultTemplates', $defaultTemplates); + $view->setData('defaultAssets', $defaultAssets); + + /** + @todo: pass data to bill + */ + + $pdf = $view->render(); + + $path = $this->createBillDir($bill); + $pdfDir = __DIR__ . '/../../../Modules/Media/Files' . $path; + + $status = !\is_dir($pdfDir) ? \mkdir($pdfDir, 0755, true) : true; + if ($status === false) { + // @codeCoverageIgnoreStart + $response->set($request->uri->__toString(), new FormValidation(['status' => $status])); + $response->header->status = RequestStatusCode::R_400; + + return; + // @codeCoverageIgnoreEnd + } + + \file_put_contents($pdfDir . '/' . $request->getData('bill') . '.pdf', $pdf); + if (!\is_file($pdfDir . '/' . $request->getData('bill') . '.pdf')) { + $response->header->status = RequestStatusCode::R_400; + + return; + } + + $media = $this->app->moduleManager->get('Media')->createDbEntry( + status: [ + 'status' => UploadStatus::OK, + 'name' => $request->getData('bill') . '.pdf', + 'path' => $pdfDir, + 'filename' => $request->getData('bill') . '.pdf', + 'size' => \filesize($pdfDir . '/' . $request->getData('bill') . '.pdf'), + 'extension' => 'pdf', + ], + account: $request->header->account, + virtualPath: $path, + ip: $request->getOrigin(), + app: $this->app, + readContent: true, + unit: $this->app->unitId + ); + + $this->createModelRelation( + $request->header->account, + $bill->getId(), + $media->getId(), + BillMapper::class, + 'media', + '', + $request->getOrigin() + ); + + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'PDF', 'Bill Pdf successfully created.', $media); + } + + /** + * Api method to create bill files + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiNoteCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void + { + if (!empty($val = $this->validateNoteCreate($request))) { + $response->set('bill_note_create', new FormValidation($val)); + $response->header->status = RequestStatusCode::R_400; + + return; + } + + /** @var \Modules\Billing\Models\Bill $bill */ + $bill = BillMapper::get()->where('id', (int) $request->getData('id'))->execute(); + + $request->setData('virtualpath', $this->createBillDir($bill), true); + $this->app->moduleManager->get('Editor')->apiEditorCreate($request, $response, $data); + + if ($response->header->status !== RequestStatusCode::R_200) { + return; + } + + $model = $response->get($request->uri->__toString())['response']; + $this->createModelRelation($request->header->account, $request->getData('id'), $model->getId(), BillMapper::class, 'bill_note', '', $request->getOrigin()); + } + + /** + * Validate bill note create request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateNoteCreate(RequestAbstract $request) : array + { + $val = []; + if (($val['id'] = empty($request->getData('id')))) { + return $val; + } + + return []; + } +} diff --git a/Controller/ApiBillTypeController.php b/Controller/ApiBillTypeController.php new file mode 100644 index 0000000..b57d5b9 --- /dev/null +++ b/Controller/ApiBillTypeController.php @@ -0,0 +1,187 @@ +validateBillTypeCreate($request))) { + $response->set('bill_type_create', new FormValidation($val)); + $response->header->status = RequestStatusCode::R_400; + + return; + } + + $billType = $this->createBillTypeFromRequest($request); + $this->createModel($request->header->account, $billType, BillTypeMapper::class, 'bill_type', $request->getOrigin()); + + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Bill type', 'Bill type successfully created', $billType); + } + + /** + * Method to create item attribute from request. + * + * @param RequestAbstract $request Request + * + * @return BillType + * + * @since 1.0.0 + */ + private function createBillTypeFromRequest(RequestAbstract $request) : BillType + { + $billType = new BillType($request->getData('name') ?? ''); + $billType->setL11n((string) ($request->getData('title') ?? ''), $request->getData('language') ?? ISO639x1Enum::_EN); + $billType->numberFormat = (string) ($request->getData('number_format') ?? '{id}'); + $billType->transferStock = (bool) ($request->getData('transfer_stock') ?? false); + $billType->isTemplate = (bool) ($request->getData('is_template') ?? false); + $billType->transferType = (int) ($request->getData('transfer_type') ?? BillTransferType::SALES); + $billType->defaultTemplate = $request->hasData('template') + ? new NullCollection((int) $request->getData('template')) + : null; + + if ($request->hasData('template')) { + $billType->addTemplate(new NullCollection((int) $request->getData('template'))); + } + + return $billType; + } + + /** + * Validate item attribute create request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateBillTypeCreate(RequestAbstract $request) : array + { + $val = []; + if (($val['title'] = empty($request->getData('title'))) + || ($val['name'] = empty($request->getData('name'))) + ) { + return $val; + } + + return []; + } + + /** + * Api method to create item attribute l11n + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiBillTypeL11nCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void + { + if (!empty($val = $this->validateBillTypeL11nCreate($request))) { + $response->set('bill_type_l11n_create', new FormValidation($val)); + $response->header->status = RequestStatusCode::R_400; + + return; + } + + $billTypeL11n = $this->createBillTypeL11nFromRequest($request); + $this->createModel($request->header->account, $billTypeL11n, BillTypeL11nMapper::class, 'bill_type_l11n', $request->getOrigin()); + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Localization', 'Localization successfully created', $billTypeL11n); + } + + /** + * Method to create item attribute l11n from request. + * + * @param RequestAbstract $request Request + * + * @return BaseStringL11n + * + * @since 1.0.0 + */ + private function createBillTypeL11nFromRequest(RequestAbstract $request) : BaseStringL11n + { + $billTypeL11n = new BaseStringL11n(); + $billTypeL11n->ref = (int) ($request->getData('type') ?? 0); + $billTypeL11n->setLanguage((string) ( + $request->getData('language') ?? $request->getLanguage() + )); + $billTypeL11n->content = (string) ($request->getData('title') ?? ''); + + return $billTypeL11n; + } + + /** + * Validate item attribute l11n create request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateBillTypeL11nCreate(RequestAbstract $request) : array + { + $val = []; + if (($val['title'] = empty($request->getData('title'))) + || ($val['type'] = empty($request->getData('type'))) + ) { + return $val; + } + + return []; + } +} diff --git a/Controller/ApiController.php b/Controller/ApiController.php index e20c070..003a28a 100755 --- a/Controller/ApiController.php +++ b/Controller/ApiController.php @@ -16,42 +16,8 @@ declare(strict_types=1); namespace Modules\Billing\Controller; -use Modules\Admin\Models\NullAccount; -use Modules\Billing\Models\Bill; -use Modules\Billing\Models\BillElement; -use Modules\Billing\Models\BillElementMapper; -use Modules\Billing\Models\BillMapper; -use Modules\Billing\Models\BillStatus; -use Modules\Billing\Models\BillTransferType; -use Modules\Billing\Models\BillType; -use Modules\Billing\Models\BillTypeL11nMapper; -use Modules\Billing\Models\BillTypeMapper; -use Modules\Billing\Models\PricingMapper; -use Modules\Billing\Models\PricingType; -use Modules\Billing\Models\SettingsEnum; -use Modules\ClientManagement\Models\ClientMapper; -use Modules\ItemManagement\Models\ItemMapper; -use Modules\Media\Models\CollectionMapper; -use Modules\Media\Models\MediaMapper; -use Modules\Media\Models\NullCollection; -use Modules\Media\Models\PathSettings; -use Modules\Media\Models\UploadStatus; -use Modules\SupplierManagement\Models\NullSupplier; -use Modules\SupplierManagement\Models\SupplierMapper; -use phpOMS\Autoloader; -use phpOMS\Localization\BaseStringL11n; -use phpOMS\Localization\ISO639x1Enum; -use phpOMS\Localization\Money; -use phpOMS\Message\Http\HttpRequest; -use phpOMS\Message\Http\HttpResponse; -use phpOMS\Message\Http\RequestStatusCode; -use phpOMS\Message\NotificationLevel; use phpOMS\Message\RequestAbstract; use phpOMS\Message\ResponseAbstract; -use phpOMS\Model\Message\FormValidation; -use phpOMS\System\MimeType; -use phpOMS\Uri\HttpUri; -use phpOMS\Views\View; /** * Billing class. @@ -63,73 +29,6 @@ use phpOMS\Views\View; */ final class ApiController extends Controller { - /** - * Api method to find items - * - * @param RequestAbstract $request Request - * @param ResponseAbstract $response Response - * @param mixed $data Generic data - * - * @return void - * - * @api - * - * @since 1.0.0 - */ - public function apiPricingFind(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void - { - // tax based on customer location + item tax definition = matrix - // price based on - // item quantity - // customer price for item / item group - // customer location - - $queryMapper = PricingMapper::getAll(); - - if ($request->hasData('price_name')) { - $queryMapper->where('name', $request->getData('price_name')); - } - - $queryMapper->where('promocode', \array_unique([$request->getData('price_promocode'), null]), 'IN'); - $queryMapper->where('item', \array_unique([$request->getData('price_item'), null]), 'IN'); - $queryMapper->where('itemgroup', \array_unique([$request->getData('price_itemgroup'), null]), 'IN'); - $queryMapper->where('itemsegment', \array_unique([$request->getData('price_itemsegment'), null]), 'IN'); - $queryMapper->where('itemsection', \array_unique([$request->getData('price_itemsection'), null]), 'IN'); - $queryMapper->where('itemtype', \array_unique([$request->getData('price_itemtype'), null]), 'IN'); - $queryMapper->where('client', \array_unique([$request->getData('price_client'), null]), 'IN'); - $queryMapper->where('clientgroup', \array_unique([$request->getData('price_clientgroup'), null]), 'IN'); - $queryMapper->where('clientsegment', \array_unique([$request->getData('price_clientsegment'), null]), 'IN'); - $queryMapper->where('clientsection', \array_unique([$request->getData('price_clientsection'), null]), 'IN'); - $queryMapper->where('clienttype', \array_unique([$request->getData('price_clienttype'), null]), 'IN'); - $queryMapper->where('clientcountry', \array_unique([$request->getData('price_clientcountry'), null]), 'IN'); - $queryMapper->where('supplier', \array_unique([$request->getData('price_supplier'), null]), 'IN'); - $queryMapper->where('unit', \array_unique([$request->getData('price_unit'), null]), 'IN'); - $queryMapper->where('type', $request->getData('price_type', 'int') ?? PricingType::SALES); - $queryMapper->where('currency', array_unique([$request->getData('price_currency'), null]), 'IN'); - - /* - if ($request->hasData('price_quantity')) { - $whereQuery = new Where(); - $whereQuery->where('quantity', (int) $request->getData('price_quantity'), '<=') - ->where('quantity', null, '=', 'OR') - - $queryMapper->where('quantity', $whereQuery); - } - */ - - $result = $queryMapper->execute(); - - $response->header->set('Content-Type', MimeType::M_JSON, true); - $response->set( - $request->uri->__toString(), - \array_values( - ItemMapper::getAll() - ->where('name', '%' . ($request->getData('search') ?? '') . '%', 'LIKE') - ->execute() - ) - ); - } - /** * Api method to update a bill * @@ -145,57 +44,7 @@ final class ApiController extends Controller */ public function apiBillUpdate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void { - if (!empty($val = $this->validateBillUpdate($request))) { - $response->set($request->uri->__toString(), new FormValidation($val)); - $response->header->status = RequestStatusCode::R_400; - - return; - } - - /** @var \Modules\Billing\Models\Bill $old */ - $old = BillMapper::get()->where('id', (int) $request->getData('bill')); - $new = $this->updateBillFromRequest($request, $response, $data); - $this->updateModel($request->header->account, $old, $new, BillMapper::class, 'bill', $request->getOrigin()); - - $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Bill', 'Bill successfully created.', $new); - } - - /** - * Method to validate bill creation from request - * - * @param RequestAbstract $request Request - * - * @return array - * - * @since 1.0.0 - */ - private function validateBillUpdate(RequestAbstract $request) : array - { - $val = []; - if (($val['bill'] = empty($request->getData('bill')))) { - return $val; - } - - return []; - } - - /** - * Method to create a bill from request. - * - * @param RequestAbstract $request Request - * @param ResponseAbstract $response Response - * @param mixed $data Generic data - * - * @return Bill - * - * @since 1.0.0 - */ - public function updateBillFromRequest(RequestAbstract $request, ResponseAbstract $response, $data = null) : Bill - { - /** @var Bill $bill */ - $bill = BillMapper::get()->where('id', (int) $request->getData('bill'))->execute(); - - return $bill; + $this->app->moduleManager->get('Billing', 'ApiBill')->apiBillUpdate($request, $response, $data); } /** @@ -213,107 +62,7 @@ final class ApiController extends Controller */ public function apiBillCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void { - if (!empty($val = $this->validateBillCreate($request))) { - $response->set($request->uri->__toString(), new FormValidation($val)); - $response->header->status = RequestStatusCode::R_400; - - return; - } - - $bill = $this->createBillFromRequest($request, $response, $data); - $this->createModel($request->header->account, $bill, BillMapper::class, 'bill', $request->getOrigin()); - - $new = clone $bill; - $new->buildNumber(); // The bill id is part of the number - $this->updateModel($request->header->account, $bill, $new, BillMapper::class, 'bill', $request->getOrigin()); - - $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Bill', 'Bill successfully created.', $bill); - } - - /** - * Method to create a bill from request. - * - * @param RequestAbstract $request Request - * @param ResponseAbstract $response Response - * @param mixed $data Generic data - * - * @return Bill - * - * @since 1.0.0 - */ - public function createBillFromRequest(RequestAbstract $request, ResponseAbstract $response, $data = null) : Bill - { - /** @var \Modules\ClientManagement\Models\Client|\Modules\SupplierManagement\Models\Supplier $account */ - $account = null; - if ($request->getData('client') !== null) { - /** @var \Modules\ClientManagement\Models\Client $account */ - $account = ClientMapper::get() - ->with('profile') - ->with('profile/account') - ->with('mainAddress') - ->where('id', (int) $request->getData('client')) - ->execute(); - } elseif (((int) ($request->getData('supplier') ?? -1)) === 0) { - /** @var \Modules\SupplierManagement\Models\Supplier $account */ - $account = new NullSupplier(); - } elseif ($request->getData('supplier') !== null) { - /** @var \Modules\SupplierManagement\Models\Supplier $account */ - $account = SupplierMapper::get() - ->with('profile') - ->with('profile/account') - ->with('mainAddress') - ->where('id', (int) $request->getData('supplier')) - ->execute(); - } - - /** @var \Modules\Billing\Models\BillType $billType */ - $billType = BillTypeMapper::get()->where('id', (int) ($request->getData('type') ?? 1))->execute(); - - /* @var \Modules\Account\Models\Account $account */ - $bill = new Bill(); - $bill->createdBy = new NullAccount($request->header->account); - $bill->type = $billType; - $bill->numberFormat = $billType->numberFormat; - // @todo: use defaultInvoiceAddress or mainAddress. also consider to use billto1, billto2, billto3 (for multiple lines e.g. name2, fao etc.) - $bill->billTo = (string) ($request->getData('billto') - ?? ($account->profile->account->name1 . (!empty($account->profile->account->name2) - ? ', ' . $account->profile->account->name2 - : '' - ))); - $bill->billAddress = (string) ($request->getData('billaddress') ?? $account->mainAddress->address); - $bill->billZip = (string) ($request->getData('billtopostal') ?? $account->mainAddress->postal); - $bill->billCity = (string) ($request->getData('billtocity') ?? $account->mainAddress->city); - $bill->billCountry = (string) ($request->getData('billtocountry') ?? $account->mainAddress->getCountry()); - $bill->client = !$request->hasData('client') ? null : $account; - $bill->supplier = !$request->hasData('supplier') ? null : $account; - $bill->performanceDate = new \DateTime($request->getData('performancedate') ?? 'now'); - $bill->setStatus((int) ($request->getData('status') ?? BillStatus::ACTIVE)); - - return $bill; - } - - /** - * Method to validate bill creation from request - * - * @param RequestAbstract $request Request - * - * @return array - * - * @since 1.0.0 - */ - private function validateBillCreate(RequestAbstract $request) : array - { - $val = []; - if (($val['client/supplier'] = (empty($request->getData('client')) - && (empty($request->getData('supplier')) - && ((int) ($request->getData('supplier') ?? -1) !== 0) - ))) - || ($val['type'] = (empty($request->getData('type')))) - ) { - return $val; - } - - return []; + $this->app->moduleManager->get('Billing', 'ApiBill')->apiBillCreate($request, $response, $data); } /** @@ -331,136 +80,7 @@ final class ApiController extends Controller */ public function apiMediaAddToBill(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void { - if (!empty($val = $this->validateMediaAddToBill($request))) { - $response->set($request->uri->__toString(), new FormValidation($val)); - $response->header->status = RequestStatusCode::R_400; - - return; - } - - /** @var \Modules\Billing\Models\Bill $bill */ - $bill = BillMapper::get()->where('id', (int) $request->getData('bill'))->execute(); - $path = $this->createBillDir($bill); - - $uploaded = []; - if (!empty($uploadedFiles = $request->getFiles())) { - $uploaded = $this->app->moduleManager->get('Media')->uploadFiles( - [], - [], - $uploadedFiles, - $request->header->account, - __DIR__ . '/../../../Modules/Media/Files' . $path, - $path, - pathSettings: PathSettings::FILE_PATH, - hasAccountRelation: false, - readContent: (bool) ($request->getData('parse_content') ?? false) - ); - - $collection = null; - foreach ($uploaded as $media) { - $this->createModelRelation( - $request->header->account, - $bill->getId(), - $media->getId(), - BillMapper::class, - 'media', - '', - $request->getOrigin() - ); - - if ($request->hasData('type')) { - $this->createModelRelation( - $request->header->account, - $media->getId(), - $request->getData('type', 'int'), - MediaMapper::class, - 'types', - '', - $request->getOrigin() - ); - } - - if ($collection === null) { - $collection = MediaMapper::getParentCollection($path)->limit(1)->execute(); - - if ($collection instanceof NullCollection) { - $collection = $this->app->moduleManager->get('Media')->createRecursiveMediaCollection( - $path, - $request->header->account, - __DIR__ . '/../../../Modules/Media/Files' . $path, - ); - } - } - - $this->createModelRelation( - $request->header->account, - $collection->getId(), - $media->getId(), - CollectionMapper::class, - 'sources', - '', - $request->getOrigin() - ); - } - } - - if (!empty($mediaFiles = $request->getDataJson('media'))) { - foreach ($mediaFiles as $media) { - $this->createModelRelation( - $request->header->account, - $bill->getId(), - (int) $media, - BillMapper::class, - 'media', - '', - $request->getOrigin() - ); - } - } - - $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Media', 'Media added to bill.', [ - 'upload' => $uploaded, - 'media' => $mediaFiles, - ]); - } - - /** - * Create media directory path - * - * @param Bill $bill Bill - * - * @return string - * - * @since 1.0.0 - */ - private function createBillDir(Bill $bill) : string - { - return '/Modules/Billing/Bills/' - . $bill->createdAt->format('Y') . '/' - . $bill->createdAt->format('m') . '/' - . $bill->createdAt->format('d') . '/' - . $bill->getId(); - } - - /** - * Method to validate bill creation from request - * - * @param RequestAbstract $request Request - * - * @return array - * - * @since 1.0.0 - */ - private function validateMediaAddToBill(RequestAbstract $request) : array - { - $val = []; - if (($val['media'] = (empty($request->getData('media')) && empty($request->getFiles()))) - || ($val['bill'] = empty($request->getData('bill'))) - ) { - return $val; - } - - return []; + $this->app->moduleManager->get('Billing', 'ApiBill')->apiMediaAddToBill($request, $response, $data); } /** @@ -478,193 +98,12 @@ final class ApiController extends Controller */ public function apiBillElementCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void { - if (!empty($val = $this->validateBillElementCreate($request))) { - $response->set($request->uri->__toString(), new FormValidation($val)); - $response->header->status = RequestStatusCode::R_400; - - return; - } - - $element = $this->createBillElementFromRequest($request, $response, $data); - $this->createModel($request->header->account, $element, BillElementMapper::class, 'bill_element', $request->getOrigin()); - - /** @var \Modules\Billing\Models\Bill $old */ - $old = BillMapper::get()->where('id', $element->bill)->execute(); - $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); - } - - /** - * Method to create a bill element from request. - * - * @param RequestAbstract $request Request - * @param ResponseAbstract $response Response - * @param mixed $data Generic data - * - * @return BillElement - * - * @since 1.0.0 - * @todo in the database the customer localized version should be stored because this is the version which went out - */ - public function createBillElementFromRequest(RequestAbstract $request, ResponseAbstract $response, $data = null) : BillElement - { - $element = new BillElement(); - $element->bill = (int) $request->getData('bill'); - $element->item = (int) ($request->getData('item') ?? 0); - - if ($element->item === null) { - return $element; - } - - /** @var \Modules\ItemManagement\Models\Item $item */ - $item = ItemMapper::get() - ->with('l11n') - ->with('l11n/type') - ->where('id', $element->item) - ->where('l11n/type/title', ['name1', 'name2', 'name3'], 'IN') - ->where('l11n/language', $response->getLanguage()) - ->execute(); - - $element->itemNumber = $item->number; - $element->itemName = $item->getL11n('name1')->description; - $element->quantity = (int) ($request->getData('quantity') ?? 0); - - $element->singleSalesPriceNet = new Money($request->getData('singlesalespricenet', 'int') ?? $item->salesPrice->getInt()); - $element->totalSalesPriceNet = clone $element->singleSalesPriceNet; - $element->totalSalesPriceNet->mult($element->quantity); - - // discounts - if ($request->getData('discount_percentage') !== null) { - $discount = (int) $request->getData('discount_percentage'); - - $element->singleSalesPriceNet - ->sub((int) ($element->singleSalesPriceNet->getInt() / 100 * $discount)); - - $element->totalSalesPriceNet - ->sub((int) ($element->totalSalesPriceNet->getInt() / 100 * $discount)); - } - - $element->singlePurchasePriceNet = new Money($item->purchasePrice->getInt()); - $element->totalPurchasePriceNet = clone $element->singlePurchasePriceNet; - $element->totalPurchasePriceNet->mult($element->quantity); - - return $element; - } - - /** - * Method to update a bill because of a changed bill element (add, remove, change) from request. - * - * @param Bill $bill Bill - * @param BillElement $element Bill element - * @param int $type Change type (0 = update, -1 = remove, +1 = add) - * - * @return Bill - * - * @since 1.0.0 - */ - public function updateBillWithBillElement(Bill $bill, BillElement $element, int $type = 1) : Bill - { - if ($type === 1) { - $bill->netSales->add($element->totalSalesPriceNet); - $bill->netCosts->add($element->totalPurchasePriceNet); - } - - return $bill; - } - - /** - * Method to validate bill element creation from request - * - * @param RequestAbstract $request Request - * - * @return array - * - * @since 1.0.0 - */ - private function validateBillElementCreate(RequestAbstract $request) : array - { - $val = []; - if (($val['bill'] = empty($request->getData('bill')))) { - return $val; - } - - return []; + $this->app->moduleManager->get('Billing', 'ApiBill')->apiBillElementCreate($request, $response, $data); } public function apiPreviewRender(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void { - Autoloader::addPath(__DIR__ . '/../../../Resources/'); - - $templateId = $request->getData('bill_template', 'int'); - if ($templateId === null) { - $billTypeId = $request->getData('bill_type', 'int'); - $billType = BillTypeMapper::get() - ->where('id', $billTypeId) - ->execute(); - - $templateId = $billType->defaultTemplate->getId(); - } - - $template = CollectionMapper::get() - ->with('sources') - ->where('id', $templateId) - ->execute(); - - require_once __DIR__ . '/../../../Resources/tcpdf/tcpdf.php'; - - $response->header->set('Content-Type', MimeType::M_PDF, true); - - $view = new View($this->app->l11nManager, $request, $response); - $view->setTemplate('/' . \substr($template->getSourceByName('bill.pdf.php')->getPath(), 0, -8), 'pdf.php'); - $view->setData('bill', $bill); - $view->setData('path', $pdfDir . '/' . $request->getData('bill') . '.pdf'); - - $view->setData('bill_creator', $request->getData('bill_creator')); - $view->setData('bill_title', $request->getData('bill_title')); - $view->setData('bill_subtitle', $request->getData('bill_subtitle')); - $view->setData('keywords', $request->getData('keywords')); - $view->setData('bill_logo_name', $request->getData('bill_logo_name')); - $view->setData('bill_slogan', $request->getData('bill_slogan')); - - $view->setData('legal_company_name', $request->getData('legal_company_name')); - $view->setData('bill_company_address', $request->getData('bill_company_address')); - $view->setData('bill_company_city', $request->getData('bill_company_city')); - $view->setData('bill_company_ceo', $request->getData('bill_company_ceo')); - $view->setData('bill_company_website', $request->getData('bill_company_website')); - $view->setData('bill_company_email', $request->getData('bill_company_email')); - $view->setData('bill_company_phone', $request->getData('bill_company_phone')); - - $view->setData('bill_company_tax_office', $request->getData('bill_company_tax_office')); - $view->setData('bill_company_tax_id', $request->getData('bill_company_tax_id')); - $view->setData('bill_company_vat_id', $request->getData('bill_company_vat_id')); - - $view->setData('bill_company_bank_name', $request->getData('bill_company_bank_name')); - $view->setData('bill_company_bic', $request->getData('bill_company_bic')); - $view->setData('bill_company_iban', $request->getData('bill_company_iban')); - - $view->setData('bill_type_name', $request->getData('bill_type_name')); - - $view->setData('bill_invoice_no', $request->getData('bill_invoice_no')); - $view->setData('bill_invoice_date', $request->getData('bill_invoice_date')); - $view->setData('bill_service_date', $request->getData('bill_service_date')); - $view->setData('bill_customer_no', $request->getData('bill_customer_no')); - $view->setData('bill_po', $request->getData('bill_po')); - $view->setData('bill_due_date', $request->getData('bill_due_date')); - - $view->setData('bill_start_text', $request->getData('bill_start_text')); - $view->setData('bill_lines', $request->getData('bill_lines')); - $view->setData('bill_end_text', $request->getData('bill_end_text')); - - $view->setData('bill_payment_terms', $request->getData('bill_payment_terms')); - $view->setData('bill_terms', $request->getData('bill_terms')); - $view->setData('bill_taxes', $request->getData('bill_taxes')); - $view->setData('bill_currency', $request->getData('bill_currency')); - - $pdf = $view->render(); - - $response->set('', $pdf); + $this->app->moduleManager->get('Billing', 'ApiBill')->apiPreviewRender($request, $response, $data); } /** @@ -682,88 +121,7 @@ final class ApiController extends Controller */ public function apiBillPdfArchiveCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void { - /** @var \Modules\Billing\Models\Bill $bill */ - $bill = BillMapper::get() - ->with('elements') - ->where('id', $request->getData('bill') ?? 0) - ->execute(); - - Autoloader::addPath(__DIR__ . '/../../../Resources/'); - - $templateId = $request->getData('bill_template', 'int'); - if ($templateId === null) { - $billTypeId = $bill->type->getId(); - $billType = BillTypeMapper::get() - ->where('id', $billTypeId) - ->execute(); - - $templateId = $billType->defaultTemplate->getId(); - } - - $template = CollectionMapper::get() - ->with('sources') - ->where('id', $templateId) - ->execute(); - - require_once __DIR__ . '/../../../Resources/tcpdf/tcpdf.php'; - - $view = new View($this->app->l11nManager, $request, $response); - $view->setTemplate('/' . \substr($template->getSourceByName('bill.pdf.php')->getPath(), 0, -8), 'pdf.php'); - - /** - @todo: pass data to bill - */ - - $pdf = $view->render(); - - $path = $this->createBillDir($bill); - $pdfDir = __DIR__ . '/../../../Modules/Media/Files' . $path; - - $status = !\is_dir($pdfDir) ? \mkdir($pdfDir, 0755, true) : true; - if ($status === false) { - // @codeCoverageIgnoreStart - $response->set($request->uri->__toString(), new FormValidation(['status' => $status])); - $response->header->status = RequestStatusCode::R_400; - - return; - // @codeCoverageIgnoreEnd - } - - \file_put_contents($pdfDir . '/' . $request->getData('bill') . '.pdf', $pdf); - if (!\is_file($pdfDir . '/' . $request->getData('bill') . '.pdf')) { - $response->header->status = RequestStatusCode::R_400; - - return; - } - - $media = $this->app->moduleManager->get('Media')->createDbEntry( - status: [ - 'status' => UploadStatus::OK, - 'name' => $request->getData('bill') . '.pdf', - 'path' => $pdfDir, - 'filename' => $request->getData('bill') . '.pdf', - 'size' => \filesize($pdfDir . '/' . $request->getData('bill') . '.pdf'), - 'extension' => 'pdf', - ], - account: $request->header->account, - virtualPath: $path, - ip: $request->getOrigin(), - app: $this->app, - readContent: true, - unit: $this->app->unitId - ); - - $this->createModelRelation( - $request->header->account, - $bill->getId(), - $media->getId(), - BillMapper::class, - 'media', - '', - $request->getOrigin() - ); - - $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'PDF', 'Bill Pdf successfully created.', $media); + $this->app->moduleManager->get('Billing', 'ApiBill')->apiBillPdfArchiveCreate($request, $response, $data); } /** @@ -781,6 +139,7 @@ final class ApiController extends Controller */ public function apiBillPdfCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void { + $this->app->moduleManager->get('Billing', 'ApiBill')->apiBillPdfCreate($request, $response, $data); } /** @@ -798,44 +157,7 @@ final class ApiController extends Controller */ public function apiNoteCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void { - if (!empty($val = $this->validateNoteCreate($request))) { - $response->set('bill_note_create', new FormValidation($val)); - $response->header->status = RequestStatusCode::R_400; - - return; - } - - /** @var \Modules\Billing\Models\Bill $bill */ - $bill = BillMapper::get()->where('id', (int) $request->getData('id'))->execute(); - - $request->setData('virtualpath', $this->createBillDir($bill), true); - $this->app->moduleManager->get('Editor')->apiEditorCreate($request, $response, $data); - - if ($response->header->status !== RequestStatusCode::R_200) { - return; - } - - $model = $response->get($request->uri->__toString())['response']; - $this->createModelRelation($request->header->account, $request->getData('id'), $model->getId(), BillMapper::class, 'bill_note', '', $request->getOrigin()); - } - - /** - * Validate bill note create request - * - * @param RequestAbstract $request Request - * - * @return array - * - * @since 1.0.0 - */ - private function validateNoteCreate(RequestAbstract $request) : array - { - $val = []; - if (($val['id'] = empty($request->getData('id')))) { - return $val; - } - - return []; + $this->app->moduleManager->get('Billing', 'ApiBill')->apiNoteCreate($request, $response, $data); } /** @@ -853,68 +175,7 @@ final class ApiController extends Controller */ public function apiSupplierBillUpload(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void { - $originalType = (int) ($request->getData('type') ?? $this->app->appSettings->get( - names: SettingsEnum::ORIGINAL_MEDIA_TYPE, - module: self::NAME - )->content); - - /** @var \Modules\Billing\Models\BillType $purchaseTransferType */ - $purchaseTransferType = BillTypeMapper::get() - ->where('transferType', BillTransferType::PURCHASE) - ->limit(1) - ->execute(); - - $files = $request->getFiles(); - foreach ($files as $file) { - // Create default bill - $billRequest = new HttpRequest(new HttpUri('')); - $billRequest->header->account = $request->header->account; - $billRequest->header->l11n = $request->header->l11n; - $billRequest->setData('supplier', 0); - $billRequest->setData('status', BillStatus::UNPARSED); - $billRequest->setData('type', $purchaseTransferType->getId()); - - $billResponse = new HttpResponse(); - $billResponse->header->l11n = $response->header->l11n; - - $this->apiBillCreate($billRequest, $billResponse, $data); - - $billId = $billResponse->get('')['response']->getId(); - - // Upload and assign document to bill - $mediaRequest = new HttpRequest(); - $mediaRequest->header->account = $request->header->account; - $mediaRequest->header->l11n = $request->header->l11n; - $mediaRequest->addFile($file); - - $mediaResponse = new HttpResponse(); - $mediaResponse->header->l11n = $response->header->l11n; - - $mediaRequest->setData('bill', $billId); - $mediaRequest->setData('type', $originalType); - $mediaRequest->setData('parse_content', true, true); - $this->apiMediaAddToBill($mediaRequest, $mediaResponse, $data); - - $uploaded = $mediaResponse->get('')['response']['upload']; - $in = \reset($uploaded)->getAbsolutePath(); // pdf is parsed in $in->content - - if (!\is_file($in)) { - throw new \Exception(); - } - - // @todo: Parse text and analyze text structure - - // Create internal document - $billResponse = new HttpResponse(); - $billRequest = new HttpRequest(new HttpUri('')); - - $billRequest->header->account = $request->header->account; - $billRequest->setData('bill', $billId); - - $this->apiBillPdfArchiveCreate($billRequest, $billResponse); - - // @todo: Start workflow for bill, if a workflow is defined for bill uploading - } + $this->app->moduleManager->get('Billing', 'ApiPurchase')->apiSupplierBillUpload($request, $response, $data); } /** @@ -932,66 +193,7 @@ final class ApiController extends Controller */ public function apiBillTypeCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void { - if (!empty($val = $this->validateBillTypeCreate($request))) { - $response->set('bill_type_create', new FormValidation($val)); - $response->header->status = RequestStatusCode::R_400; - - return; - } - - $billType = $this->createBillTypeFromRequest($request); - $this->createModel($request->header->account, $billType, BillTypeMapper::class, 'bill_type', $request->getOrigin()); - - $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Bill type', 'Bill type successfully created', $billType); - } - - /** - * Method to create item attribute from request. - * - * @param RequestAbstract $request Request - * - * @return BillType - * - * @since 1.0.0 - */ - private function createBillTypeFromRequest(RequestAbstract $request) : BillType - { - $billType = new BillType($request->getData('name') ?? ''); - $billType->setL11n((string) ($request->getData('title') ?? ''), $request->getData('language') ?? ISO639x1Enum::_EN); - $billType->numberFormat = (string) ($request->getData('number_format') ?? '{id}'); - $billType->transferStock = (bool) ($request->getData('transfer_stock') ?? false); - $billType->isTemplate = (bool) ($request->getData('is_template') ?? false); - $billType->transferType = (int) ($request->getData('transfer_type') ?? BillTransferType::SALES); - $billType->defaultTemplate = $request->hasData('template') - ? new NullCollection((int) $request->getData('template')) - : null; - - if ($request->hasData('template')) { - $billType->addTemplate(new NullCollection((int) $request->getData('template'))); - } - - return $billType; - } - - /** - * Validate item attribute create request - * - * @param RequestAbstract $request Request - * - * @return array - * - * @since 1.0.0 - */ - private function validateBillTypeCreate(RequestAbstract $request) : array - { - $val = []; - if (($val['title'] = empty($request->getData('title'))) - || ($val['name'] = empty($request->getData('name'))) - ) { - return $val; - } - - return []; + $this->app->moduleManager->get('Billing', 'ApiBillType')->apiBillTypeCreate($request, $response, $data); } /** @@ -1009,57 +211,132 @@ final class ApiController extends Controller */ public function apiBillTypeL11nCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void { - if (!empty($val = $this->validateBillTypeL11nCreate($request))) { - $response->set('bill_type_l11n_create', new FormValidation($val)); - $response->header->status = RequestStatusCode::R_400; - - return; - } - - $billTypeL11n = $this->createBillTypeL11nFromRequest($request); - $this->createModel($request->header->account, $billTypeL11n, BillTypeL11nMapper::class, 'bill_type_l11n', $request->getOrigin()); - $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Bill type localization', 'Bill type localization successfully created', $billTypeL11n); + $this->app->moduleManager->get('Billing', 'ApiBillType')->apiBillTypeL11nCreate($request, $response, $data); } /** - * Method to create item attribute l11n from request. + * Api method to create item bill type * - * @param RequestAbstract $request Request + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data * - * @return BaseStringL11n + * @return void + * + * @api * * @since 1.0.0 */ - private function createBillTypeL11nFromRequest(RequestAbstract $request) : BaseStringL11n + public function apiTaxCombinationCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void { - $billTypeL11n = new BaseStringL11n(); - $billTypeL11n->ref = (int) ($request->getData('type') ?? 0); - $billTypeL11n->setLanguage((string) ( - $request->getData('language') ?? $request->getLanguage() - )); - $billTypeL11n->content = (string) ($request->getData('title') ?? ''); - - return $billTypeL11n; + $this->app->moduleManager->get('Billing', 'ApiTax')->apiTaxCombinationCreate($request, $response, $data); } /** - * Validate item attribute l11n create request + * Api method to create item bill type * - * @param RequestAbstract $request Request + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data * - * @return array + * @return void + * + * @api * * @since 1.0.0 */ - private function validateBillTypeL11nCreate(RequestAbstract $request) : array + public function apiPriceCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void { - $val = []; - if (($val['title'] = empty($request->getData('title'))) - || ($val['type'] = empty($request->getData('type'))) - ) { - return $val; - } + $this->app->moduleManager->get('Billing', 'ApiPrice')->apiPriceCreate($request, $response, $data); + } - return []; + /** + * Api method to create item attribute + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiBillAttributeCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void + { + $this->app->moduleManager->get('Billing', 'ApiAttribute')->apiBillAttributeCreate($request, $response, $data); + } + + /** + * Api method to create bill attribute l11n + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiBillAttributeTypeL11nCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void + { + $this->app->moduleManager->get('Billing', 'ApiAttribute')->apiBillAttributeTypeL11nCreate($request, $response, $data); + } + + /** + * Api method to create bill attribute type + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiBillAttributeTypeCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void + { + $this->app->moduleManager->get('Billing', 'ApiAttribute')->apiBillAttributeTypeCreate($request, $response, $data); + } + + /** + * Api method to create bill attribute value + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiBillAttributeValueCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void + { + $this->app->moduleManager->get('Billing', 'ApiAttribute')->apiBillAttributeValueCreate($request, $response, $data); + } + + /** + * Api method to create bill attribute l11n + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiBillAttributeValueL11nCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void + { + $this->app->moduleManager->get('Billing', 'ApiAttribute')->apiBillAttributeValueL11nCreate($request, $response, $data); } } diff --git a/Controller/ApiPriceController.php b/Controller/ApiPriceController.php new file mode 100644 index 0000000..35d5861 --- /dev/null +++ b/Controller/ApiPriceController.php @@ -0,0 +1,305 @@ +hasData('price_item')) { + $item = ItemMapper::get() + ->with('attributes') + ->with('attributes/type') + ->with('attributes/value') + ->where('id', (int) $request->getData('price_item')) + ->execute(); + } + + // Get account + $account = null; + + /** @var null|\Modules\ClientManagement\Models\Client $client */ + $client = null; + + /** @var null|\Modules\SupplierManagement\Models\Supplier $supplier */ + $supplier = null; + + if ($request->hasData('price_client')) { + $client = ClientMapper::get() + ->with('attributes/type') + ->with('attributes/value') + ->where('id', (int) $request->getData('price_client')) + ->execute(); + + $account = $client; + } else { + $supplier = SupplierMapper::get() + ->with('attributes/type') + ->with('attributes/value') + ->where('id', (int) $request->getData('price_supplier')) + ->execute(); + + $account = $supplier; + } + + // Get all relevant prices + // @todo: allow to define NOT IN somehow (e.g. not in France -> simple solution to define export prices etc.) + $queryMapper = PriceMapper::getAll(); + + if ($request->hasData('price_name')) { + $queryMapper->where('name', $request->getData('price_name')); + } + + $queryMapper->where('promocode', \array_unique([$request->getData('price_promocode'), null]), 'IN'); + + $queryMapper->where('item', \array_unique([$request->getData('price_item', 'int'), null]), 'IN'); + $queryMapper->where('itemgroup', \array_unique([$request->getData('price_itemgroup', 'int'), $item?->getAttribute('itemgroup')?->getId(), null]), 'IN'); + $queryMapper->where('itemsegment', \array_unique([$request->getData('price_itemsegment', 'int'), $item?->getAttribute('itemsegment')?->getId(), null]), 'IN'); + $queryMapper->where('itemsection', \array_unique([$request->getData('price_itemsection', 'int'), $item?->getAttribute('itemsection')?->getId(), null]), 'IN'); + $queryMapper->where('itemtype', \array_unique([$request->getData('price_itemtype', 'int'), $item?->getAttribute('itemtype')?->getId(), null]), 'IN'); + + $queryMapper->where('client', \array_unique([$request->getData('price_client', 'int'), null]), 'IN'); + $queryMapper->where('clientgroup', \array_unique([$request->getData('price_clientgroup', 'int'), $client?->getAttribute('clientgroup')?->getId(), null]), 'IN'); + $queryMapper->where('clientsegment', \array_unique([$request->getData('price_clientsegment', 'int'), $client?->getAttribute('clientsegment')?->getId(), null]), 'IN'); + $queryMapper->where('clientsection', \array_unique([$request->getData('price_clientsection', 'int'), $client?->getAttribute('clientsection')?->getId(), null]), 'IN'); + $queryMapper->where('clienttype', \array_unique([$request->getData('price_clienttype', 'int'), $client?->getAttribute('clienttype')?->getId(), null]), 'IN'); + $queryMapper->where('clientcountry', \array_unique([$request->getData('price_clientcountry'), $client?->mainAddress->getCountry(), null]), 'IN'); + + $queryMapper->where('supplier', \array_unique([$request->getData('price_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('price_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); + } + */ + + $prices = $queryMapper->execute(); + + // Find base price (@todo: probably not a good solution) + $bestBasePrice = null; + foreach ($prices as $price) { + if ($price->price !== 0 && $price->priceNew === 0 + && $price->item->getId() !== 0 + && $price->itemgroup->getId() === 0 + && $price->itemsegment->getId() === 0 + && $price->itemsection->getId() === 0 + && $price->itemtype->getId() === 0 + && $price->client->getId() === 0 + && $price->clientgroup->getId() === 0 + && $price->clientsegment->getId() === 0 + && $price->clientsection->getId() === 0 + && $price->clienttype->getId() === 0 + && $price->promocode === '' + ) { + if ($price->price < $bestBasePrice?->price ?? \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; + + if ($price->price < $newPrice) { + $newPrice = $price->price; + } + + if ($price->priceNew < $newPrice) { + $newPrice = $price->priceNew; + } + + $newPrice -= $price->discount; + $newPrice = (int) ((10000 / $price->discountPercentage) * $newPrice); + $newPrice = (int) (($price->quantity === 0 ? 10000 : $price->quantity) / (10000 + $price->bonus) * $newPrice); + + // @todo: the calculation above regarding discount and bonus don't consider the purchased quantity. + // 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. + + if ($newPrice < $bestPriceValue) { + $bestPriceValue = $newPrice; + $bestPrice = $price; + } + } + + // Get tax definition + $tax = ($request->getData('price_type', 'int') ?? PriceType::SALES) + ? TaxCombinationMapper::get() + ->where('itemCode', $request->getData('price_item')) + ->where('clientCode', $account->getAttribute('client_code')->getId()) + ->execute() + : TaxCombinationMapper::get() + ->where('itemCode', $request->getData('price_item')) + ->where('supplierCode', $account->getAttribute('supplier_code')->getId()) + ->execute(); + + $response->header->set('Content-Type', MimeType::M_JSON, true); + $response->set( + $request->uri->__toString(), + \array_values($prices) + ); + } + + /** + * Api method to create item bill type + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiPriceCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void + { + if (!empty($val = $this->validatePriceCreate($request))) { + $response->set('price_create', new FormValidation($val)); + $response->header->status = RequestStatusCode::R_400; + + return; + } + + $tax = $this->createPriceFromRequest($request); + $this->createModel($request->header->account, $tax, PriceMapper::class, 'price', $request->getOrigin()); + + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Price', 'Price successfully created', $tax); + } + + /** + * Method to create item attribute from request. + * + * @param RequestAbstract $request Request + * + * @return Price + * + * @since 1.0.0 + */ + private function createPriceFromRequest(RequestAbstract $request) : Price + { + $price = new Price(); + $price->name = $request->getData('name') ?? ''; + $price->promocode = $request->getData('promocode') ?? ''; + + $price->item = new NullItem((int) $request->getData('item')); + $price->itemgroup = new NullItemAttributeValue((int) $request->getData('itemgroup')); + $price->itemsegment = new NullItemAttributeValue((int) $request->getData('itemsegment')); + $price->itemsection = new NullItemAttributeValue((int) $request->getData('itemsection')); + $price->itemtype = new NullItemAttributeValue((int) $request->getData('itemtype')); + + $price->client = new NullClient((int) $request->getData('client')); + $price->clientgroup = new NullClientAttributeValue((int) $request->getData('clientgroup')); + $price->clientsegment = new NullClientAttributeValue((int) $request->getData('clientsegment')); + $price->clientsection = new NullClientAttributeValue((int) $request->getData('clientsection')); + $price->clienttype = new NullClientAttributeValue((int) $request->getData('clienttype')); + + $price->supplier = new NullSupplier((int) $request->getData('supplier')); + $price->unit = (int) $request->getData('unit'); + $price->type = (int) ($request->getData('type') ?? PriceType::SALES); + $price->quantity = (int) $request->getData('quantity'); + $price->price = (int) $request->getData('price'); + $price->priceNew = (int) $request->getData('price_name'); + $price->discount = (int) $request->getData('discount'); + $price->discountPercentage = (int) $request->getData('discountPercentage'); + $price->bonus = (int) $request->getData('bonus'); + $price->multiply = (bool) ($request->getData('multiply') ?? false); + $price->currency = $request->getData('currency') ?? ISO4217CharEnum::_EUR; + $price->start = $request->hasData('start') ? new \DateTime($request->getData('start')) : null; + $price->end = $request->hasData('end') ? new \DateTime($request->getData('end')) : null; + + return $price; + } + + /** + * Validate item attribute create request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validatePriceCreate(RequestAbstract $request) : array + { + $val = []; + if (false) { + return $val; + } + + return []; + } +} diff --git a/Controller/ApiPurchaseController.php b/Controller/ApiPurchaseController.php new file mode 100644 index 0000000..d2592db --- /dev/null +++ b/Controller/ApiPurchaseController.php @@ -0,0 +1,132 @@ +getData('type') ?? $this->app->appSettings->get( + names: SettingsEnum::ORIGINAL_MEDIA_TYPE, + module: self::NAME + )->content); + + /** @var \Modules\Billing\Models\BillType $purchaseTransferType */ + $purchaseTransferType = BillTypeMapper::get() + ->where('transferType', BillTransferType::PURCHASE) + ->limit(1) + ->execute(); + + $files = $request->getFiles(); + foreach ($files as $file) { + // Create default bill + $billRequest = new HttpRequest(new HttpUri('')); + $billRequest->header->account = $request->header->account; + $billRequest->header->l11n = $request->header->l11n; + $billRequest->setData('supplier', 0); + $billRequest->setData('status', BillStatus::UNPARSED); + $billRequest->setData('type', $purchaseTransferType->getId()); + + $billResponse = new HttpResponse(); + $billResponse->header->l11n = $response->header->l11n; + + $this->app->moduleManager->get('Billing', 'Api')->apiBillCreate($billRequest, $billResponse, $data); + + $billId = $billResponse->get('')['response']->getId(); + + // Upload and assign document to bill + $mediaRequest = new HttpRequest(); + $mediaRequest->header->account = $request->header->account; + $mediaRequest->header->l11n = $request->header->l11n; + $mediaRequest->addFile($file); + + $mediaResponse = new HttpResponse(); + $mediaResponse->header->l11n = $response->header->l11n; + + $mediaRequest->setData('bill', $billId); + $mediaRequest->setData('type', $originalType); + $mediaRequest->setData('parse_content', true, true); + $this->app->moduleManager->get('Billing', 'Api')->apiMediaAddToBill($mediaRequest, $mediaResponse, $data); + + $uploaded = $mediaResponse->get('')['response']['upload']; + $in = \reset($uploaded)->getAbsolutePath(); // pdf is parsed in $in->content + + if (!\is_file($in)) { + throw new \Exception(); + } + + // @todo: Parse text and analyze text structure + + // Create internal document + $billResponse = new HttpResponse(); + $billRequest = new HttpRequest(new HttpUri('')); + + $billRequest->header->account = $request->header->account; + $billRequest->setData('bill', $billId); + + $this->app->moduleManager->get('Billing', 'Api')->apiBillPdfArchiveCreate($billRequest, $billResponse); + + // Offload bill parsing to cli + $cliPath = \realpath(__DIR__ . '/../../../Cli/cli.php'); + if ($cliPath === false) { + return; + } + + SystemUtils::runProc( + OperatingSystem::getSystem() === SystemType::WIN ? 'php.exe' : 'php', + \escapeshellarg($cliPath) + . ' /billing/bill/purchase/parse ' + . '-i ' . \escapeshellarg((string) $billId), + true + ); + } + } +} diff --git a/Controller/ApiTaxController.php b/Controller/ApiTaxController.php new file mode 100644 index 0000000..1ed423a --- /dev/null +++ b/Controller/ApiTaxController.php @@ -0,0 +1,139 @@ +validateTaxCombinationCreate($request))) { + $response->set('tax_combination_create', new FormValidation($val)); + $response->header->status = RequestStatusCode::R_400; + + return; + } + + $tax = $this->createTaxCombinationFromRequest($request); + $this->createModel($request->header->account, $tax, TaxCombinationMapper::class, 'tax_combination', $request->getOrigin()); + + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Tax combination', 'Tax combination successfully created', $tax); + } + + /** + * Method to create item attribute from request. + * + * @param RequestAbstract $request Request + * + * @return TaxCombination + * + * @since 1.0.0 + */ + private function createTaxCombinationFromRequest(RequestAbstract $request) : TaxCombination + { + $tax = new TaxCombination(); + $tax->taxType = (int) $request->getData('tax_type') ?? 1; + $tax->taxCode = (string) $request->getData('tax_code'); + $tax->itemCode = new NullItemAttributeValue((int) $request->getData('item_code')); + + if ($tax->taxType === 1) { + $tax->clientCode = new NullClientAttributeValue((int) $request->getData('account_code')); + } else { + $tax->supplierCode = new NullSupplierAttributeValue((int) $request->getData('account_code')); + } + + return $tax; + } + + /** + * Validate item attribute create request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateTaxCombinationCreate(RequestAbstract $request) : array + { + $val = []; + if (($val['tax_type'] = empty($request->getData('tax_type'))) + || ($val['tax_code'] = empty($request->getData('tax_code'))) + || ($val['item_code'] = empty($request->getData('item_code'))) + || ($val['account_code'] = empty($request->getData('account_code'))) + ) { + return $val; + } + + return []; + } + + public function getClientTaxCode(Client $client, Address $taxOfficeAddress) : ClientAttributeValue + { + $codes = ClientAttributeTypeMapper::get() + ->with('defaults') + ->where('name', 'sales_tax_code') + ->execute(); + + $taxCode = new NullClientAttributeValue(); + + if ($taxOfficeAddress->getCountry() === $client->mainAddress->getCountry()) { + $taxCode = $codes->getDefaultByValue($client->mainAddress->getCountry()); + } elseif (\in_array($taxOfficeAddress->getCountry(), ISO3166CharEnum::getRegion('eu')) + && \in_array($client->mainAddress->getCountry(), ISO3166CharEnum::getRegion('eu')) + ) { + if (!empty($client->getAttribute('vat_id')?->value->getValue())) { + // Is EU company + $taxCode = $codes->getDefaultByValue('EU'); + } else { + // Is EU private customer + $taxCode = $codes->getDefaultByValue($client->mainAddress->getCountry()); + } + } elseif (\in_array($taxOfficeAddress->getCountry(), ISO3166CharEnum::getRegion('eu'))) { + // None EU company but we are EU company + $taxCode = $codes->getDefaultByValue('INT'); + } else { + // None EU company and we are also none EU company + $taxCode = $codes->getDefaultByValue('INT'); + } + + return $taxCode; + } +} \ No newline at end of file diff --git a/Controller/BackendController.php b/Controller/BackendController.php index 9461100..7dbd9d4 100755 --- a/Controller/BackendController.php +++ b/Controller/BackendController.php @@ -296,6 +296,7 @@ final class BackendController extends Controller $bill = PurchaseBillMapper::get() ->with('elements') ->with('media') + ->with('media/types') ->with('notes') ->where('id', (int) $request->getData('id')) ->execute(); @@ -803,6 +804,7 @@ final class BackendController extends Controller $bill = PurchaseBillMapper::get() ->with('elements') ->with('media') + ->with('media/types') ->with('notes') ->where('id', (int) $request->getData('id')) ->execute(); diff --git a/Controller/CliController.php b/Controller/CliController.php new file mode 100644 index 0000000..90ff7a7 --- /dev/null +++ b/Controller/CliController.php @@ -0,0 +1,306 @@ +getData('type') ?? $this->app->appSettings->get( + names: SettingsEnum::ORIGINAL_MEDIA_TYPE, + module: self::NAME + )->content); + + /** @var \Modules\Billing\Models\Bill $bill */ + $bill = BillMapper::get() + ->with('media') + ->with('media/types') + ->with('media/content') + ->where('id', (int) $request->getData('i')) + ->where('media/types/id', $originalType) + ->execute(); + + $old = clone $bill; + + $content = \strtolower($bill->getFileByType($originalType)->content->content ?? ''); + $lines = \explode("\n", $content); + + $language = $this->detectLanguage($content); + $bill->language = $language; + + $identifiers = \json_decode(\file_get_contents(__DIR__ . '/../Models/billIdentifier.json'), true); + + /* Supplier */ + $suppliers = SupplierMapper::getAll() + ->with('account') + ->with('mainAddress') + ->with('attributes/type') + ->where('attributes/type/name', ['bill_match_pattern', 'bill_date_format'], 'IN') + ->execute(); + + $supplierId = $this->matchSupplier($content, $suppliers); + $bill->supplier = new NullSupplier($supplierId); + $supplier = $suppliers[$supplierId] ?? new NullSupplier(); + + $bill->billTo = $supplier->account->name1; + $bill->billAddress = $supplier->mainAddress->address; + $bill->billCity = $supplier->mainAddress->city; + $bill->billZip = $supplier->mainAddress->postal; + $bill->billCountry = $supplier->mainAddress->getCountry(); + + /* Type */ + $type = $this->findSupplierInvoiceType($content, $identifiers['type'], $language); + $billType = BillTypeMapper::get() + ->where('name', $type) + ->execute(); + $bill->type = new NullBillType($billType->getId()); + + /* Number */ + $billNumber = $this->findBillNumber($lines, $identifiers['bill_no'][$language]); + $bill->number = $billNumber; + + /* Date */ + $billDateTemp = $this->findBillDate($lines, $identifiers['bill_date'][$language]); + $billDate = $this->parseDate($billDateTemp, $supplier, $identifiers['date_format']); + + $bill->billDate = $billDate; + + /* Total Gross */ + $totalGross = $this->findBillGross($lines, $identifiers['total_gross'][$language]); + $bill->grossCosts = new Money($totalGross); + + $this->updateModel($request->header->account, $old, $bill, BillMapper::class, 'bill_parsing', $request->getOrigin()); + + $view = new View($this->app->l11nManager, $request, $response); + $view->setTemplate('/Modules/Billing/Theme/Cli/bill-parsed'); + $view->setData('bill', $bill); + + return $view; + } + + private function detectLanguage(string $content) : string + { + $detector = new Language(); + $language = $detector->detect($content)->bestResults()->close(); + + if (!\is_array($language) || \count($language) < 1) { + return 'en'; + } + + return \substr(\array_keys($language)[0], 0, 2); + } + + private function findSupplierInvoiceType(string $content, array $types, string $language) : string + { + $bestPos = \strlen($content); + $bestMatch = ''; + + foreach ($types as $name => $type) { + foreach ($type[$language] as $l11n) { + $found = \stripos($content, \strtolower($l11n)); + + if ($found !== false && $found < $bestPos) { + $bestPos = $found; + $bestMatch = $name; + } + } + } + + return empty($bestMatch) ? 'purchase_invoice' : $bestMatch; + } + + private function findBillNumber(array $lines, array $matches) : string + { + $bestPos = \count($lines); + $bestMatch = ''; + + $found = []; + + foreach ($matches as $match) { + foreach ($lines as $row => $line) { + if (\preg_match($match, $line, $found) === 1) { + if ($row < $bestPos) { + $bestPos = $row; + $bestMatch = \trim($found['bill_no']); + } + + break; + } + } + } + + return $bestMatch; + } + + private function findBillDue(array $lines, array $matches) : string + { + $bestPos = \count($lines); + $bestMatch = ''; + + $found = []; + + foreach ($matches as $match) { + foreach ($lines as $row => $line) { + if (\preg_match($match, $line, $found) === 1) { + if ($row < $bestPos) { + // @question: don't many invoices have the due date at the bottom? bestPos doesn't make sense?! + $bestPos = $row; + $bestMatch = \trim($found['bill_due']); + } + + break; + } + } + } + + return $bestMatch; + } + + private function findBillDate(array $lines, array $matches) : string + { + $bestPos = \count($lines); + $bestMatch = ''; + + $found = []; + + foreach ($matches as $match) { + foreach ($lines as $row => $line) { + if (\preg_match($match, $line, $found) === 1) { + if ($row < $bestPos) { + $bestPos = $row; + $bestMatch = \trim($found['bill_date']); + } + + break; + } + } + } + + return $bestMatch; + } + + private function findBillGross(array $lines, array $matches) : int + { + $bestMatch = 0; + + $found = []; + + foreach ($matches as $match) { + foreach ($lines as $line) { + if (\preg_match($match, $line, $found) === 1) { + $temp = \trim($found['total_gross']); + + $posD = \stripos($temp, '.'); + $posK = \stripos($temp, ','); + + $hasDecimal = ($posD !== false || $posK !== false) + && \max((int) $posD, (int) $posK) + 3 >= \strlen($temp); + + $gross = ((int) \str_replace(['.', ','], ['', ''], $temp)) * ($hasDecimal + ? 100 + : 10000); + + if ($gross > $bestMatch) { + $bestMatch = $gross; + } + } + } + } + + return $bestMatch; + } + + private function matchSupplier(string $content, array $suppliers) : int + { + $bestMatch = 0; + + foreach ($suppliers as $supplier) { + if ((!empty($supplier->getAttributeByTypeName('iban')->value->valueStr) + && \stripos($content, $supplier->getAttributeByTypeName('iban')->value->valueStr) !== false) + || (!empty($supplier->getAttributeByTypeName('bill_match_pattern')->value->valueStr) + && \stripos($content, $supplier->getAttributeByTypeName('bill_match_pattern')->value->valueStr) !== false) + ) { + return $supplier->getId(); + } + + if (\stripos($content, $supplier->account->name1) !== false) { + if ((!empty($supplier->mainAddress->city) + && \stripos($content, $supplier->mainAddress->city) !== false) + || (!empty( $supplier->mainAddress->address) + && \stripos($content, $supplier->mainAddress->address) !== false) + ) { + return $supplier->getId(); + } + + $bestMatch = $supplier->getId(); + } + } + + return $bestMatch; + } + + private function parseDate(string $date, Supplier $supplier, array $formats) : ?\DateTime + { + if ((!empty($supplier->getAttributeByTypeName('bill_date_format')->value->valueStr))) { + return \DateTime::createFromFormat( + $supplier->getAttributeByTypeName('bill_date_format')->value->valueStr, + $date + ); + } + + foreach ($formats as $format) { + if (($obj = \DateTime::createFromFormat($format, $date)) !== false) { + return $obj; + } + } + + return null; + } +} diff --git a/Models/Attribute/AttributeValueType.php b/Models/Attribute/AttributeValueType.php new file mode 100755 index 0000000..fc933d7 --- /dev/null +++ b/Models/Attribute/AttributeValueType.php @@ -0,0 +1,40 @@ +type = new NullBillAttributeType(); + $this->value = new NullBillAttributeValue(); + } + + /** + * Get id + * + * @return int + * + * @since 1.0.0 + */ + public function getId() : int + { + return $this->id; + } + + /** + * {@inheritdoc} + */ + public function toArray() : array + { + return [ + 'id' => $this->id, + 'bill' => $this->bill, + 'type' => $this->type, + 'value' => $this->value, + ]; + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() : mixed + { + return $this->toArray(); + } +} diff --git a/Models/Attribute/BillAttributeMapper.php b/Models/Attribute/BillAttributeMapper.php new file mode 100755 index 0000000..1f1d998 --- /dev/null +++ b/Models/Attribute/BillAttributeMapper.php @@ -0,0 +1,74 @@ + + * @since 1.0.0 + */ + public const COLUMNS = [ + 'billing_bill_attr_id' => ['name' => 'billing_bill_attr_id', 'type' => 'int', 'internal' => 'id'], + 'billing_bill_attr_bill' => ['name' => 'billing_bill_attr_bill', 'type' => 'int', 'internal' => 'bill'], + 'billing_bill_attr_type' => ['name' => 'billing_bill_attr_type', 'type' => 'int', 'internal' => 'type'], + 'billing_bill_attr_value' => ['name' => 'billing_bill_attr_value', 'type' => 'int', 'internal' => 'value'], + ]; + + /** + * Has one relation. + * + * @var array + * @since 1.0.0 + */ + public const OWNS_ONE = [ + 'type' => [ + 'mapper' => BillAttributeTypeMapper::class, + 'external' => 'billing_bill_attr_type', + ], + 'value' => [ + 'mapper' => BillAttributeValueMapper::class, + 'external' => 'billing_bill_attr_value', + ], + ]; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'billing_bill_attr'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'billing_bill_attr_id'; +} diff --git a/Models/Attribute/BillAttributeType.php b/Models/Attribute/BillAttributeType.php new file mode 100755 index 0000000..6aa4a1d --- /dev/null +++ b/Models/Attribute/BillAttributeType.php @@ -0,0 +1,216 @@ +name = $name; + } + + /** + * Get id + * + * @return int + * + * @since 1.0.0 + */ + public function getId() : int + { + return $this->id; + } + + public function getDefaultByValue(mixed $value) : BillAttributeValue + { + foreach ($this->defaults as $default) { + if ($default->getValue() === $value) { + return $default; + } + } + + return new NullBillAttributeValue(); + } + + /** + * Set l11n + * + * @param string|BaseStringL11n $l11n Tag article l11n + * @param string $lang Language + * + * @return void + * + * @since 1.0.0 + */ + public function setL11n(string | BaseStringL11n $l11n, string $lang = ISO639x1Enum::_EN) : void + { + if ($l11n instanceof BaseStringL11n) { + $this->l11n = $l11n; + } elseif (isset($this->l11n) && $this->l11n instanceof BaseStringL11n) { + $this->l11n->content = $l11n; + $this->l11n->setLanguage($lang); + } else { + $this->l11n = new BaseStringL11n(); + $this->l11n->content = $l11n; + $this->l11n->setLanguage($lang); + } + } + + /** + * @return string + * + * @since 1.0.0 + */ + public function getL11n() : string + { + if (!isset($this->l11n)) { + return ''; + } + + return $this->l11n instanceof BaseStringL11n ? $this->l11n->content : $this->l11n; + } + + /** + * Set fields + * + * @param int $fields Fields + * + * @return void + * + * @since 1.0.0 + */ + public function setFields(int $fields) : void + { + $this->fields = $fields; + } + + /** + * Get default values + * + * @return array + * + * @sicne 1.0.0 + */ + public function getDefaults() : array + { + return $this->defaults; + } + + /** + * {@inheritdoc} + */ + public function toArray() : array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'validationPattern' => $this->validationPattern, + 'custom' => $this->custom, + 'isRequired' => $this->isRequired, + ]; + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() : mixed + { + return $this->toArray(); + } +} diff --git a/Models/Attribute/BillAttributeTypeL11nMapper.php b/Models/Attribute/BillAttributeTypeL11nMapper.php new file mode 100755 index 0000000..6b79741 --- /dev/null +++ b/Models/Attribute/BillAttributeTypeL11nMapper.php @@ -0,0 +1,66 @@ + + * @since 1.0.0 + */ + public const COLUMNS = [ + 'billing_attr_type_l11n_id' => ['name' => 'billing_attr_type_l11n_id', 'type' => 'int', 'internal' => 'id'], + 'billing_attr_type_l11n_title' => ['name' => 'billing_attr_type_l11n_title', 'type' => 'string', 'internal' => 'content', 'autocomplete' => true], + 'billing_attr_type_l11n_type' => ['name' => 'billing_attr_type_l11n_type', 'type' => 'int', 'internal' => 'ref'], + 'billing_attr_type_l11n_lang' => ['name' => 'billing_attr_type_l11n_lang', 'type' => 'string', 'internal' => 'language'], + ]; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'billing_attr_type_l11n'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'billing_attr_type_l11n_id'; + + /** + * Model to use by the mapper. + * + * @var class-string + * @since 1.0.0 + */ + public const MODEL = BaseStringL11n::class; +} diff --git a/Models/Attribute/BillAttributeTypeMapper.php b/Models/Attribute/BillAttributeTypeMapper.php new file mode 100755 index 0000000..b48b289 --- /dev/null +++ b/Models/Attribute/BillAttributeTypeMapper.php @@ -0,0 +1,82 @@ + + * @since 1.0.0 + */ + public const COLUMNS = [ + 'billing_attr_type_id' => ['name' => 'billing_attr_type_id', 'type' => 'int', 'internal' => 'id'], + 'billing_attr_type_name' => ['name' => 'billing_attr_type_name', 'type' => 'string', 'internal' => 'name', 'autocomplete' => true], + 'billing_attr_type_datatype' => ['name' => 'billing_attr_type_datatype', 'type' => 'int', 'internal' => 'datatype'], + 'billing_attr_type_fields' => ['name' => 'billing_attr_type_fields', 'type' => 'int', 'internal' => 'fields'], + 'billing_attr_type_custom' => ['name' => 'billing_attr_type_custom', 'type' => 'bool', 'internal' => 'custom'], + 'billing_attr_type_pattern' => ['name' => 'billing_attr_type_pattern', 'type' => 'string', 'internal' => 'validationPattern'], + 'billing_attr_type_required' => ['name' => 'billing_attr_type_required', 'type' => 'bool', 'internal' => 'isRequired'], + ]; + + /** + * Has many relation. + * + * @var array + * @since 1.0.0 + */ + public const HAS_MANY = [ + 'l11n' => [ + 'mapper' => BillAttributeTypeL11nMapper::class, + 'table' => 'billing_attr_type_l11n', + 'self' => 'billing_attr_type_l11n_type', + 'column' => 'content', + 'external' => null, + ], + 'defaults' => [ + 'mapper' => BillAttributeValueMapper::class, + 'table' => 'billing_bill_attr_default', + 'self' => 'billing_bill_attr_default_type', + 'external' => 'billing_bill_attr_default_value', + ], + ]; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'billing_attr_type'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'billing_attr_type_id'; +} diff --git a/Models/Attribute/BillAttributeValue.php b/Models/Attribute/BillAttributeValue.php new file mode 100755 index 0000000..9e567fe --- /dev/null +++ b/Models/Attribute/BillAttributeValue.php @@ -0,0 +1,230 @@ +id; + } + + /** + * Set l11n + * + * @param string|BaseStringL11n $l11n Tag article l11n + * @param string $lang Language + * + * @return void + * + * @since 1.0.0 + */ + public function setL11n(string | BaseStringL11n $l11n, string $lang = ISO639x1Enum::_EN) : void + { + if ($l11n instanceof BaseStringL11n) { + $this->l11n = $l11n; + } elseif (isset($this->l11n) && $this->l11n instanceof BaseStringL11n) { + $this->l11n->content = $l11n; + $this->l11n->setLanguage($lang); + } else { + $this->l11n = new BaseStringL11n(); + $this->l11n->content = $l11n; + $this->l11n->ref = $this->id; + $this->l11n->setLanguage($lang); + } + } + + /** + * Get localization + * + * @return null|string + * + * @since 1.0.0 + */ + public function getL11n() : ?string + { + return $this->l11n instanceof BaseStringL11n ? $this->l11n->content : $this->l11n; + } + + /** + * Set value + * + * @param int|string|float $value Value + * @param int $datatype Datatype + * + * @return void + * + * @since 1.0.0 + */ + public function setValue(mixed $value, int $datatype) : void + { + if ($datatype === AttributeValueType::_STRING) { + $this->valueStr = (string) $value; + } elseif ($datatype === AttributeValueType::_INT + || $datatype === AttributeValueType::_FLOAT_INT + || $datatype === AttributeValueType::_BOOL + ) { + $this->valueInt = (int) $value; + } elseif ($datatype === AttributeValueType::_FLOAT) { + $this->valueDec = (float) $value; + } elseif ($datatype === AttributeValueType::_DATETIME) { + $this->valueDat = new \DateTime((string) $value); + } + } + + /** + * Get value + * + * @return null|int|string|float|\DateTimeInterface + * + * @since 1.0.0 + */ + public function getValue() : mixed + { + if (!empty($this->valueStr)) { + return $this->valueStr; + } elseif (!empty($this->valueInt)) { + return $this->valueInt; + } elseif (!empty($this->valueDec)) { + return $this->valueDec; + } elseif ($this->valueDat instanceof \DateTimeInterface) { + return $this->valueDat; + } + + return null; + } + + /** + * {@inheritdoc} + */ + public function toArray() : array + { + return [ + 'id' => $this->id, + 'valueInt' => $this->valueInt, + 'valueStr' => $this->valueStr, + 'valueDec' => $this->valueDec, + 'valueDat' => $this->valueDat, + 'isDefault' => $this->isDefault, + ]; + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() : mixed + { + return $this->toArray(); + } +} diff --git a/Models/Attribute/BillAttributeValueL11nMapper.php b/Models/Attribute/BillAttributeValueL11nMapper.php new file mode 100755 index 0000000..e25a87c --- /dev/null +++ b/Models/Attribute/BillAttributeValueL11nMapper.php @@ -0,0 +1,66 @@ + + * @since 1.0.0 + */ + public const COLUMNS = [ + 'billing_attr_value_l11n_id' => ['name' => 'billing_attr_value_l11n_id', 'type' => 'int', 'internal' => 'id'], + 'billing_attr_value_l11n_title' => ['name' => 'billing_attr_value_l11n_title', 'type' => 'string', 'internal' => 'content', 'autocomplete' => true], + 'billing_attr_value_l11n_value' => ['name' => 'billing_attr_value_l11n_value', 'type' => 'int', 'internal' => 'ref'], + 'billing_attr_value_l11n_lang' => ['name' => 'billing_attr_value_l11n_lang', 'type' => 'string', 'internal' => 'language'], + ]; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'billing_attr_value_l11n'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'billing_attr_value_l11n_id'; + + /** + * Model to use by the mapper. + * + * @var class-string + * @since 1.0.0 + */ + public const MODEL = BaseStringL11n::class; +} diff --git a/Models/Attribute/BillAttributeValueMapper.php b/Models/Attribute/BillAttributeValueMapper.php new file mode 100755 index 0000000..66a773a --- /dev/null +++ b/Models/Attribute/BillAttributeValueMapper.php @@ -0,0 +1,77 @@ + + * @since 1.0.0 + */ + public const COLUMNS = [ + 'billing_attr_value_id' => ['name' => 'billing_attr_value_id', 'type' => 'int', 'internal' => 'id'], + 'billing_attr_value_default' => ['name' => 'billing_attr_value_default', 'type' => 'bool', 'internal' => 'isDefault'], + 'billing_attr_value_valueStr' => ['name' => 'billing_attr_value_valueStr', 'type' => 'string', 'internal' => 'valueStr'], + 'billing_attr_value_valueInt' => ['name' => 'billing_attr_value_valueInt', 'type' => 'int', 'internal' => 'valueInt'], + 'billing_attr_value_valueDec' => ['name' => 'billing_attr_value_valueDec', 'type' => 'float', 'internal' => 'valueDec'], + 'billing_attr_value_valueDat' => ['name' => 'billing_attr_value_valueDat', 'type' => 'DateTime', 'internal' => 'valueDat'], + 'billing_attr_value_unit' => ['name' => 'billing_attr_value_unit', 'type' => 'string', 'internal' => 'unit'], + 'billing_attr_value_deptype' => ['name' => 'billing_attr_value_deptype', 'type' => 'int', 'internal' => 'dependingAttributeType'], + 'billing_attr_value_depvalue' => ['name' => 'billing_attr_value_depvalue', 'type' => 'int', 'internal' => 'dependingAttributeValue'], + ]; + + /** + * Has many relation. + * + * @var array + * @since 1.0.0 + */ + public const HAS_MANY = [ + 'l11n' => [ + 'mapper' => BillAttributeValueL11nMapper::class, + 'table' => 'billing_attr_value_l11n', + 'self' => 'billing_attr_value_l11n_value', + 'external' => null, + ], + ]; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'billing_attr_value'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'billing_attr_value_id'; +} diff --git a/Models/Attribute/NullBillAttribute.php b/Models/Attribute/NullBillAttribute.php new file mode 100755 index 0000000..38fb1fd --- /dev/null +++ b/Models/Attribute/NullBillAttribute.php @@ -0,0 +1,47 @@ +id = $id; + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() : mixed + { + return ['id' => $this->id]; + } +} diff --git a/Models/Attribute/NullBillAttributeType.php b/Models/Attribute/NullBillAttributeType.php new file mode 100755 index 0000000..80ac220 --- /dev/null +++ b/Models/Attribute/NullBillAttributeType.php @@ -0,0 +1,46 @@ +id = $id; + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() : mixed + { + return ['id' => $this->id]; + } +} diff --git a/Models/Attribute/NullBillAttributeValue.php b/Models/Attribute/NullBillAttributeValue.php new file mode 100755 index 0000000..1908b5e --- /dev/null +++ b/Models/Attribute/NullBillAttributeValue.php @@ -0,0 +1,46 @@ +id = $id; + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() : mixed + { + return ['id' => $this->id]; + } +} diff --git a/Models/Bill.php b/Models/Bill.php index 67a0e94..0d8c0b3 100755 --- a/Models/Bill.php +++ b/Models/Bill.php @@ -16,13 +16,13 @@ namespace Modules\Billing\Models; use Modules\Admin\Models\Account; use Modules\Admin\Models\NullAccount; +use Modules\Billing\Models\Attribute\BillAttribute; use Modules\ClientManagement\Models\Client; use Modules\Editor\Models\EditorDoc; use Modules\Media\Models\Collection; use Modules\Media\Models\Media; use Modules\Media\Models\NullMedia; use Modules\SupplierManagement\Models\Supplier; -use phpOMS\Localization\ISO3166TwoEnum; use phpOMS\Localization\ISO4217CharEnum; use phpOMS\Localization\ISO639x1Enum; use phpOMS\Localization\Money; @@ -89,13 +89,21 @@ class Bill implements \JsonSerializable */ public \DateTimeImmutable $createdAt; + /** + * Bill created at. + * + * @var null|\DateTime + * @since 1.0.0 + */ + public ?\DateTime $billDate = null; + /** * Bill created at. * * @var \DateTime * @since 1.0.0 */ - public \DateTime $performanceDate; + public ?\DateTime $performanceDate = null; /** * Bill send at. @@ -435,6 +443,14 @@ class Bill implements \JsonSerializable */ protected array $media = []; + /** + * Attributes. + * + * @var BillAttribute[] + * @since 1.0.0 + */ + private array $attributes = []; + /** * Constructor. * @@ -452,7 +468,6 @@ class Bill implements \JsonSerializable $this->grossDiscount = new Money(0); $this->createdAt = new \DateTimeImmutable(); - $this->performanceDate = new \DateTime(); $this->createdBy = new NullAccount(); $this->referral = new NullAccount(); $this->type = new NullBillType(); @@ -514,6 +529,52 @@ class Bill implements \JsonSerializable return $this->number; } + /** + * Add attribute to client + * + * @param BillAttribute $attribute Attribute + * + * @return void + * + * @since 1.0.0 + */ + public function addAttribute(BillAttribute $attribute) : void + { + $this->attributes[] = $attribute; + } + + /** + * Get attributes + * + * @return BillAttribute[] + * + * @since 1.0.0 + */ + public function getAttributes() : array + { + return $this->attributes; + } + + /** + * Get attribute + * + * @param string $attrName Attribute name + * + * @return null|BillAttribute + * + * @since 1.0.0 + */ + public function getAttribute(string $attrName) : ?BillAttribute + { + foreach ($this->attributes as $attribute) { + if ($attribute->type->name === $attrName) { + return $attribute->value; + } + } + + return null; + } + /** * Get status * @@ -759,7 +820,7 @@ class Bill implements \JsonSerializable public function getFileByType(int $type) : Media { foreach ($this->media as $file) { - if ($file->type->getId() === $type) { + if ($file->hasMediaTypeId($type)) { return $file; } } diff --git a/Models/BillMapper.php b/Models/BillMapper.php index 96e136f..2db1da6 100755 --- a/Models/BillMapper.php +++ b/Models/BillMapper.php @@ -15,6 +15,7 @@ declare(strict_types=1); namespace Modules\Billing\Models; use Modules\Admin\Models\AccountMapper; +use Modules\Billing\Models\Attribute\BillAttributeMapper; use Modules\ClientManagement\Models\ClientMapper; use Modules\Editor\Models\EditorDocMapper; use Modules\Media\Models\CollectionMapper; @@ -82,6 +83,7 @@ class BillMapper extends DataMapperFactory 'billing_bill_client' => ['name' => 'billing_bill_client', 'type' => 'int', 'internal' => 'client'], 'billing_bill_supplier' => ['name' => 'billing_bill_supplier', 'type' => 'int', 'internal' => 'supplier'], 'billing_bill_created_by' => ['name' => 'billing_bill_created_by', 'type' => 'int', 'internal' => 'createdBy', 'readonly' => true], + 'billing_bill_date' => ['name' => 'billing_bill_date', 'type' => 'DateTime', 'internal' => 'billDate'], 'billing_bill_performance_date' => ['name' => 'billing_bill_performance_date', 'type' => 'DateTime', 'internal' => 'performanceDate', 'readonly' => true], 'billing_bill_created_at' => ['name' => 'billing_bill_created_at', 'type' => 'DateTimeImmutable', 'internal' => 'createdAt', 'readonly' => true], ]; @@ -153,6 +155,13 @@ class BillMapper extends DataMapperFactory 'mapper' => SupplierMapper::class, 'external' => 'billing_bill_supplier', ], + 'attributes' => [ + 'mapper' => BillAttributeMapper::class, + 'table' => 'billing_bill_attr', + 'self' => 'billing_bill_attr_bill', + 'conditional' => true, + 'external' => null, + ], ]; /** @@ -161,7 +170,7 @@ class BillMapper extends DataMapperFactory * @var string * @since 1.0.0 */ - public const PRIMARYFIELD ='billing_bill_id'; + public const PRIMARYFIELD = 'billing_bill_id'; /** * Primary table. diff --git a/Models/BillTypeL11nMapper.php b/Models/BillTypeL11nMapper.php index be7f67b..64ab458 100755 --- a/Models/BillTypeL11nMapper.php +++ b/Models/BillTypeL11nMapper.php @@ -54,7 +54,7 @@ final class BillTypeL11nMapper extends DataMapperFactory * @var string * @since 1.0.0 */ - public const PRIMARYFIELD ='billing_type_l11n_id'; + public const PRIMARYFIELD = 'billing_type_l11n_id'; /** * Model to use by the mapper. diff --git a/Models/BillTypeMapper.php b/Models/BillTypeMapper.php index 3c8cd9d..dcda8e3 100755 --- a/Models/BillTypeMapper.php +++ b/Models/BillTypeMapper.php @@ -116,5 +116,5 @@ final class BillTypeMapper extends DataMapperFactory * @var string * @since 1.0.0 */ - public const PRIMARYFIELD ='billing_type_id'; + public const PRIMARYFIELD = 'billing_type_id'; } diff --git a/Models/NullBill.php b/Models/NullBill.php old mode 100755 new mode 100644 diff --git a/Models/Price/NullPrice.php b/Models/Price/NullPrice.php new file mode 100644 index 0000000..dfaf774 --- /dev/null +++ b/Models/Price/NullPrice.php @@ -0,0 +1,47 @@ +id = $id; + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() : mixed + { + return ['id' => $this->id]; + } +} diff --git a/Models/Price/Price.php b/Models/Price/Price.php new file mode 100644 index 0000000..f2fe414 --- /dev/null +++ b/Models/Price/Price.php @@ -0,0 +1,144 @@ +item = new NullItem(); + $this->itemgroup = new NullItemAttributeValue(); + $this->itemsegment = new NullItemAttributeValue(); + $this->itemsection = new NullItemAttributeValue(); + $this->itemtype = new NullItemAttributeValue(); + + $this->client = new NullClient(); + $this->clientgroup = new NullClientAttributeValue(); + $this->clientsegment = new NullClientAttributeValue(); + $this->clientsection = new NullClientAttributeValue(); + $this->clienttype = new NullClientAttributeValue(); + + $this->supplier = new NullSupplier(); + } + + /** + * Get id. + * + * @return int Model id + * + * @since 1.0.0 + */ + public function getId() : int + { + return $this->id; + } + + /** + * {@inheritdoc} + */ + public function toArray() : array + { + return [ + 'id' => $this->id, + ]; + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() : mixed + { + return $this->toArray(); + } +} diff --git a/Models/Price/PriceMapper.php b/Models/Price/PriceMapper.php new file mode 100644 index 0000000..a2aa306 --- /dev/null +++ b/Models/Price/PriceMapper.php @@ -0,0 +1,179 @@ + + * @since 1.0.0 + */ + public const COLUMNS = [ + 'billing_price_id' => ['name' => 'billing_price_id', 'type' => 'int', 'internal' => 'id'], + 'billing_price_name' => ['name' => 'billing_price_name', 'type' => 'string', 'internal' => 'name'], + 'billing_price_promocode' => ['name' => 'billing_price_promocode', 'type' => 'string', 'internal' => 'promocode'], + 'billing_price_item' => ['name' => 'billing_price_item', 'type' => 'int', 'internal' => 'item'], + 'billing_price_itemgroup' => ['name' => 'billing_price_itemgroup', 'type' => 'int', 'internal' => 'itemgroup'], + 'billing_price_itemsegment' => ['name' => 'billing_price_itemsegment', 'type' => 'int', 'internal' => 'itemsegment'], + 'billing_price_itemsection' => ['name' => 'billing_price_itemsection', 'type' => 'int', 'internal' => 'itemsection'], + 'billing_price_itemtype' => ['name' => 'billing_price_itemtype', 'type' => 'int', 'internal' => 'itemtype'], + 'billing_price_client' => ['name' => 'billing_price_client', 'type' => 'int', 'internal' => 'client'], + 'billing_price_clientgroup' => ['name' => 'billing_price_clientgroup', 'type' => 'int', 'internal' => 'clientgroup'], + 'billing_price_clientsegment' => ['name' => 'billing_price_clientsegment', 'type' => 'int', 'internal' => 'clientsegment'], + 'billing_price_clientsection' => ['name' => 'billing_price_clientsection', 'type' => 'int', 'internal' => 'clientsection'], + 'billing_price_clienttype' => ['name' => 'billing_price_clienttype', 'type' => 'int', 'internal' => 'clienttype'], + 'billing_price_clientcountry' => ['name' => 'billing_price_clientcountry', 'type' => 'string', 'internal' => 'clientcountry'], + 'billing_price_supplier' => ['name' => 'billing_price_supplier', 'type' => 'int', 'internal' => 'supplier'], + 'billing_price_unit' => ['name' => 'billing_price_unit', 'type' => 'int', 'internal' => 'unit'], + 'billing_price_type' => ['name' => 'billing_price_type', 'type' => 'int', 'internal' => 'type'], + 'billing_price_quantity' => ['name' => 'billing_price_quantity', 'type' => 'int', 'internal' => 'quantity'], + 'billing_price_price' => ['name' => 'billing_price_price', 'type' => 'int', 'internal' => 'price'], + 'billing_price_price_new' => ['name' => 'billing_price_price_new', 'type' => 'int', 'internal' => 'priceNew'], + 'billing_price_discount' => ['name' => 'billing_price_discount', 'type' => 'int', 'internal' => 'discount'], + 'billing_price_discountp' => ['name' => 'billing_price_discountp', 'type' => 'int', 'internal' => 'discountPercentage'], + 'billing_price_bonus' => ['name' => 'billing_price_bonus', 'type' => 'int', 'internal' => 'bonus'], + 'billing_price_multiply' => ['name' => 'billing_price_multiply', 'type' => 'bool', 'internal' => 'multiply'], + 'billing_price_currency' => ['name' => 'billing_price_currency', 'type' => 'string', 'internal' => 'currency'], + 'billing_price_start' => ['name' => 'billing_price_start', 'type' => 'DateTime', 'internal' => 'start'], + 'billing_price_end' => ['name' => 'billing_price_end', 'type' => 'DateTime', 'internal' => 'end'], + ]; + + /** + * Has one relation. + * + * @var array + * @since 1.0.0 + */ + public const OWNS_ONE = [ + 'item' => [ + 'mapper' => ItemMapper::class, + 'external' => 'billing_price_item', + ], + 'itemgroup' => [ + 'mapper' => ItemAttributeValueMapper::class, + 'external' => 'billing_price_itemgroup', + ], + 'itemsegment' => [ + 'mapper' => ItemAttributeValueMapper::class, + 'external' => 'billing_price_itemsegment', + ], + 'itemsection' => [ + 'mapper' => ItemAttributeValueMapper::class, + 'external' => 'billing_price_itemsection', + ], + 'itemtype' => [ + 'mapper' => ItemAttributeValueMapper::class, + 'external' => 'billing_price_itemtype', + ], + 'client' => [ + 'mapper' => ClientMapper::class, + 'external' => 'billing_price_client', + ], + 'clientgroup' => [ + 'mapper' => ClientAttributeValueMapper::class, + 'external' => 'billing_price_clientgroup', + ], + 'clientsegment' => [ + 'mapper' => ClientAttributeValueMapper::class, + 'external' => 'billing_price_clientsegment', + ], + 'clientsection' => [ + 'mapper' => ClientAttributeValueMapper::class, + 'external' => 'billing_price_clientsection', + ], + 'clienttype' => [ + 'mapper' => ClientAttributeValueMapper::class, + 'external' => 'billing_price_clienttype', + ], + 'clientcountry' => [ + 'mapper' => CountryMapper::class, + 'external' => 'billing_price_clientcountry', + 'by' => 'code2', + 'column' => 'code2', + 'conditional' => true, + ], + 'supplier' => [ + 'mapper' => SupplierMapper::class, + 'external' => 'billing_price_supplier', + ], + ]; + + /** + * Model to use by the mapper. + * + * @var class-string + * @since 1.0.0 + */ + public const MODEL = Price::class; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'billing_price'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'billing_price_id'; + + public static function findClientPrice() : array + { + /* + select * from prices + where + (promoID = ? OR promoID = null) + AND (itemID = ? OR itemID = null) + AND (itemGroup = IN (?) OR itemGroup = null) + AND (itemSegment = ? OR itemSegment = null) + AND (itemSection = ? OR itemSection = null) + AND (productType = ? OR productType = null) + AND (customerID = ? OR customerID = null) + AND (customerGroup IN (?) OR customerGroup = null) + AND (customerCountry = IN (?) OR customerCountry = null) + AND (quantity < ? OR quantity = null) + AND (start <= ? OR start = null) + AND (end >= ? OR start = null) + AND (unit = ? OR unit = null) + */ + + // @todo: allow nested where clause (already possible with the query builder, but not with the mappers) + + return []; + } +} diff --git a/Models/PricingType.php b/Models/Price/PriceType.php similarity index 70% rename from Models/PricingType.php rename to Models/Price/PriceType.php index 69853b0..c3c8c42 100644 --- a/Models/PricingType.php +++ b/Models/Price/PriceType.php @@ -4,7 +4,7 @@ * * PHP Version 8.1 * - * @package Modules\Billing\Models + * @package Modules\Billing\Models\Price * @copyright Dennis Eichhorn * @license OMS License 1.0 * @version 1.0.0 @@ -12,19 +12,19 @@ */ declare(strict_types=1); -namespace Modules\Billing\Models; +namespace Modules\Billing\Models\Price; use phpOMS\Stdlib\Base\Enum; /** * Module settings enum. * - * @package Modules\Billing\Models + * @package Modules\Billing\Models\Price * @license OMS License 1.0 * @link https://jingga.app * @since 1.0.0 */ -abstract class PricingType extends Enum +abstract class PriceType extends Enum { public const SALES = 1; diff --git a/Models/PricingMapper.php b/Models/PricingMapper.php deleted file mode 100644 index 985220d..0000000 --- a/Models/PricingMapper.php +++ /dev/null @@ -1,94 +0,0 @@ - - * @since 1.0.0 - */ - public const COLUMNS = [ - 'billing_type_id' => ['name' => 'billing_type_id', 'type' => 'int', 'internal' => 'id'], - 'billing_type_name' => ['name' => 'billing_type_name', 'type' => 'string', 'internal' => 'name'], - 'billing_type_number_format' => ['name' => 'billing_type_number_format', 'type' => 'string', 'internal' => 'numberFormat'], - 'billing_type_transfer_type' => ['name' => 'billing_type_transfer_type', 'type' => 'int', 'internal' => 'transferType'], - 'billing_type_default_template' => ['name' => 'billing_type_default_template', 'type' => 'int', 'internal' => 'defaultTemplate'], - 'billing_type_transfer_stock' => ['name' => 'billing_type_transfer_stock', 'type' => 'bool', 'internal' => 'transferStock'], - 'billing_type_is_template' => ['name' => 'billing_type_is_template', 'type' => 'bool', 'internal' => 'isTemplate'], - ]; - - /** - * Model to use by the mapper. - * - * @var class-string - * @since 1.0.0 - */ - public const MODEL = Pricing::class; - - /** - * Primary table. - * - * @var string - * @since 1.0.0 - */ - public const TABLE = 'billing_price'; - - /** - * Primary field name. - * - * @var string - * @since 1.0.0 - */ - public const PRIMARYFIELD ='billing_price_id'; - - public static function findClientPrice() : array - { - /* - select * from prices - where - (promoID = ? OR promoID = null) - AND (itemID = ? OR itemID = null) - AND (itemGroup = IN (?) OR itemGroup = null) - AND (itemSegment = ? OR itemSegment = null) - AND (itemSection = ? OR itemSection = null) - AND (productType = ? OR productType = null) - AND (customerID = ? OR customerID = null) - AND (customerGroup IN (?) OR customerGroup = null) - AND (customerCountry = IN (?) OR customerCountry = null) - AND (quantity < ? OR quantity = null) - AND (start <= ? OR start = null) - AND (end >= ? OR start = null) - AND (unit = ? OR unit = null) - */ - - // @todo: allow nested where clause (already possible with the query builder, but not with the mappers) - - return []; - } -} diff --git a/Models/Tax/BillTaxType.php b/Models/Tax/BillTaxType.php new file mode 100644 index 0000000..5bbeccf --- /dev/null +++ b/Models/Tax/BillTaxType.php @@ -0,0 +1,32 @@ +id = $id; + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() : mixed + { + return ['id' => $this->id]; + } +} diff --git a/Models/Tax/TaxCombination.php b/Models/Tax/TaxCombination.php new file mode 100644 index 0000000..bbf4efd --- /dev/null +++ b/Models/Tax/TaxCombination.php @@ -0,0 +1,100 @@ +itemCode = new NullItemAttributeValue(); + } + + /** + * Get id + * + * @return int + * + * @since 1.0.0 + */ + public function getId() : int + { + return $this->id; + } + + /** + * {@inheritdoc} + */ + public function toArray() : array + { + return [ + 'id' => $this->id, + ]; + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() : mixed + { + return $this->toArray(); + } +} \ No newline at end of file diff --git a/Models/Tax/TaxCombinationMapper.php b/Models/Tax/TaxCombinationMapper.php new file mode 100644 index 0000000..c10eff7 --- /dev/null +++ b/Models/Tax/TaxCombinationMapper.php @@ -0,0 +1,98 @@ + + * @since 1.0.0 + */ + public const COLUMNS = [ + 'billing_tax_id' => ['name' => 'billing_tax_id', 'type' => 'int', 'internal' => 'id'], + 'billing_tax_client_code' => ['name' => 'billing_tax_client_code', 'type' => 'int', 'internal' => 'clientCode'], + 'billing_tax_supplier_code' => ['name' => 'billing_tax_supplier_code', 'type' => 'int', 'internal' => 'supplierCode'], + 'billing_tax_item_code' => ['name' => 'billing_tax_item_code', 'type' => 'int', 'internal' => 'itemCode'], + 'billing_tax_code' => ['name' => 'billing_tax_code', 'type' => 'string', 'internal' => 'taxCode'], + 'billing_tax_type' => ['name' => 'billing_tax_type', 'type' => 'int', 'internal' => 'taxType'], + 'billing_tax_account' => ['name' => 'billing_tax_account', 'type' => 'string', 'internal' => 'account'], + 'billing_tax_refund_account' => ['name' => 'billing_tax_refund_account', 'type' => 'string', 'internal' => 'refundAccount'], + 'billing_tax_discount_account' => ['name' => 'billing_tax_discount_account', 'type' => 'string', 'internal' => 'discountAccount'], + 'billing_tax_min_price' => ['name' => 'billing_tax_min_price', 'type' => 'int', 'internal' => 'minPrice'], + 'billing_tax_max_price' => ['name' => 'billing_tax_max_price', 'type' => 'int', 'internal' => 'maxPrice'], + 'billing_tax_start' => ['name' => 'billing_tax_start', 'type' => 'DateTime', 'internal' => 'start'], + 'billing_tax_end' => ['name' => 'billing_tax_end', 'type' => 'DateTime', 'internal' => 'end'], + ]; + + /** + * Has one relation. + * + * @var array + * @since 1.0.0 + */ + public const OWNS_ONE = [ + 'clientCode' => [ + 'mapper' => ClientAttributeValueMapper::class, + 'external' => 'billing_tax_client_code', + ], + 'supplierCode' => [ + 'mapper' => SupplierAttributeValueMapper::class, + 'external' => 'billing_tax_supplier_code', + ], + 'itemCode' => [ + 'mapper' => ItemAttributeValueMapper::class, + 'external' => 'billing_tax_item_code', + ], + ]; + + /** + * Model to use by the mapper. + * + * @var class-string + * @since 1.0.0 + */ + public const MODEL = TaxCombination::class; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'billing_tax'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'billing_tax_id'; +} diff --git a/Models/billIdentifier.json b/Models/billIdentifier.json new file mode 100644 index 0000000..dd133c9 --- /dev/null +++ b/Models/billIdentifier.json @@ -0,0 +1,106 @@ +{ + "type": { + "purchase_invoice": { + "en": [ + "Invoice" + ], + "de": [ + "Rechnung" + ] + }, + "purchase_credit_note": { + "en": [ + "Credit Note" + ], + "de": [ + "Rechnungskorrektur", + "Gutschrift" + ] + }, + "purchase_delivery_note": { + "en": [ + "Delivery Note" + ], + "de": [ + "Lieferschein" + ] + }, + "purchase_order_confirmation": { + "en": [ + "Order Confirmation" + ], + "de": [ + "Auftragsbestätigung" + ] + }, + "purchase_reverse_invoice": { + "en": [ + "Credit Note" + ], + "de": [ + "Gutschrift" + ] + } + }, + "date_format": [ + "Y-m-d", + "Y.m.d", + "Y/m/d", + "d-m-Y", + "d.m.Y", + "d/m/Y", + "m-d-Y", + "m.d.Y", + "m/d/Y", + "M. d.Y", + "M. d. Y", + "M. d Y", + "M. d,Y", + "M. d, Y", + "M d.Y", + "M d. Y", + "M d Y", + "M d,Y", + "M d, Y", + "M, d.Y", + "M, d. Y", + "M, d Y", + "M, d,Y", + "M, d, Y" + ], + "bill_no": { + "en": [ + "/(inv.*?)(no|\\s|,|:|\\.|#)+(?.*?)( |$)/i", + "/(#)(?.*?)( |$)/i" + ], + "de": [ + "/(rechnungsn.*?|beleg.*?)(?.*?)( |$)/i" + ] + }, + "bill_date": { + "en": [ + "/(inv.*?)(date.*?)(\\s|,|:|\\.)+(?.*?)( |$)/i", + "/(date.*?)(\\s|,|:|\\.)+(?.*?)( |$)/i" + ], + "de": [ + "/(rechnungsdat.*?|belegdat.*?)(\\s|,|:|\\.)+(?.*?)( |$)/i" + ] + }, + "bill_due": { + "en": [ + "/(due date.*?)(\\s|,|:|\\.)+(?.*?)( |$)/i", + "/(due.*?)(\\s|,|:|\\.)+(?.*?)( |$)/i" + ], + "de": [ + "/(fällig.*?)(\\s|,|:|\\.)+(?.*?)( |$)/i" + ] + }, + "total_gross": { + "en": [ + "/(total.*?|gross.*?)(?([0-9]+,*\\.*)+)/i" + ], + "de": [ + "/(betrag.*?|gesamt.*?|brutto.*?)(?([0-9]+,*\\.*)+)/i" + ] + } +} \ No newline at end of file diff --git a/Theme/Backend/Lang/Navigation.en.lang.php b/Theme/Backend/Lang/Navigation.en.lang.php index f0593b9..e32a6f5 100755 --- a/Theme/Backend/Lang/Navigation.en.lang.php +++ b/Theme/Backend/Lang/Navigation.en.lang.php @@ -17,4 +17,5 @@ return ['Navigation' => [ 'Archive' => 'Archive', 'Bill' => 'Bill', 'Billing' => 'Billing', + 'Upload' => 'Upload', ]]; diff --git a/Theme/Backend/Lang/en.lang.php b/Theme/Backend/Lang/en.lang.php index e459cd7..cd5e7b0 100755 --- a/Theme/Backend/Lang/en.lang.php +++ b/Theme/Backend/Lang/en.lang.php @@ -89,6 +89,7 @@ return ['Billing' => [ 'Type' => 'Type', 'Types' => 'Types', 'Upload' => 'Upload', + 'Original' => 'Original', 'Value' => 'Value', 'Variation' => 'Variation', 'Zip' => 'Zip', diff --git a/Theme/Backend/user-purchase-bill-dashboard.tpl.php b/Theme/Backend/user-purchase-bill-dashboard.tpl.php index 47d0ce6..e982f68 100755 --- a/Theme/Backend/user-purchase-bill-dashboard.tpl.php +++ b/Theme/Backend/user-purchase-bill-dashboard.tpl.php @@ -12,6 +12,8 @@ */ declare(strict_types=1); +use phpOMS\Localization\ISO3166NameEnum; +use phpOMS\Localization\ISO3166TwoEnum; use phpOMS\Uri\UriFactory; $bills = $this->getData('bills') ?? []; @@ -126,7 +128,7 @@ echo $this->getData('nav')->render(); ?> - getHtml('Net'); ?> + getHtml('Gross'); ?>