diff --git a/.env b/.env
index e84ed8e8..417b94b3 100644
--- a/.env
+++ b/.env
@@ -24,7 +24,7 @@ APP_SECRET=7915fd8481d1a52cf42010ebe2caa974
# Format described at http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db"
# Configure your db driver and server_version in config/packages/doctrine.yaml
-DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name
+DATABASE_URL=mysql://root:@127.0.0.1:3306/part-db
###< doctrine/doctrine-bundle ###
###> symfony/swiftmailer-bundle ###
diff --git a/src/Controller/HomepageController.php b/src/Controller/HomepageController.php
new file mode 100644
index 00000000..c0fb4f5f
--- /dev/null
+++ b/src/Controller/HomepageController.php
@@ -0,0 +1,30 @@
+getDoctrine()->getRepository(Category::class);
+
+ /** @var StructuralDBElement $attachment */
+ $attachment = $repo->find(1);
+
+ dump($attachment, $attachment->getSubelements(false)->toArray());
+ $response = "";
+ return $this->render('base.html.twig');
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Attachment.php b/src/Entity/Attachment.php
new file mode 100644
index 00000000..089311ea
--- /dev/null
+++ b/src/Entity/Attachment.php
@@ -0,0 +1,148 @@
+getFilename(), PATHINFO_EXTENSION);
+
+ // list all file extensions which are supported to display them by HTML code
+ $picture_extensions = array('gif', 'png', 'jpg', 'jpeg', 'bmp', 'svg', 'tif');
+
+ return in_array(strtolower($extension), $picture_extensions, true);
+ }
+
+ /********************************************************************************
+ *
+ * Getters
+ *
+ *********************************************************************************/
+
+ /**
+ * Get the element, associated with this Attachement (for example a "Part" object)
+ * @return DBElement The associated Element.
+ */
+ public function getElement() : AttachmentContainingDBElement
+ {
+ return $this->element;
+ }
+
+ /**
+ * Checks if the file in this attachement is existing. This works for files on the HDD, and for URLs
+ * (it's not checked if the ressource behind the URL is really existing).
+ *
+ * @return bool True if the file is existing.
+ */
+ public function isFileExisting() : bool
+ {
+ return file_exists($this->getFilename()) || isURL($this->getFilename());
+ }
+
+ /**
+ * Get the filename (absolute path from filesystem root, as a UNIX path [only slashes])
+ *
+ * @return string the filename as an absolute UNIX filepath from filesystem root
+ */
+ public function getFilename() : string
+ {
+ return str_replace('%BASE%', BASE, $this->filename);
+ }
+
+ /**
+ * Get the show_in_table attribute
+ *
+ * @return bool true means, this attachement will be listed in the "Attachements" column of the HTML tables
+ * false means, this attachement won't be listed in the "Attachements" column of the HTML tables
+ */
+ public function getShowInTable() : bool
+ {
+ return (bool) $this->show_in_table;
+ }
+
+ /**
+ * Get the type of this attachement
+ * @return AttachmentType the type of this attachement
+ * @throws Exception if there was an error
+ */
+ public function getType() : AttachmentType
+ {
+ //TODO
+ }
+
+ /**
+ * Returns the ID as an string, defined by the element class.
+ * This should have a form like P000014, for a part with ID 14.
+ * @return string The ID as a string;
+ */
+ public function getIDString(): string
+ {
+ return 'A' . sprintf('%09d', $this->getID());
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/AttachmentContainingDBElement.php b/src/Entity/AttachmentContainingDBElement.php
new file mode 100644
index 00000000..59faa071
--- /dev/null
+++ b/src/Entity/AttachmentContainingDBElement.php
@@ -0,0 +1,91 @@
+attachmentTypes;
+ }
+
+
+ /**
+ * Get all attachements of this element / Get the element's attachements with a specific type
+ *
+ * @param integer $type_id @li if NULL, all attachements of this element will be returned
+ * @li if this is a number > 0, only attachements with this type ID will be returned
+ * @param boolean $only_table_attachements if true, only attachements with "show_in_table == true"
+ *
+ * @return Attachment[] the attachements as a one-dimensional array of Attachement objects
+ *
+ * @throws Exception if there was an error
+ */
+ public function getAttachments($type_id = null, bool $only_table_attachements = false) : array
+ {
+ if ($only_table_attachements || $type_id) {
+ $attachements = $this->attachments;
+
+ foreach ($attachements as $key => $attachement) {
+ if (($only_table_attachements && (! $attachement->getShowInTable()))
+ || ($type_id && ($attachement->getType()->getID() != $type_id))) {
+ unset($attachements[$key]);
+ }
+ }
+
+ return $attachements;
+ } else {
+ return $this->attachments;
+ }
+ }
+}
diff --git a/src/Entity/AttachmentType.php b/src/Entity/AttachmentType.php
new file mode 100644
index 00000000..5f317e60
--- /dev/null
+++ b/src/Entity/AttachmentType.php
@@ -0,0 +1,82 @@
+attachements is used from class "AttachementsContainingDBELement"
+ if ($this->attachments == null) {
+ $this->attachments = new ArrayCollection();
+ }
+
+ return $this->attachments;
+ }
+
+ /**
+ * Returns the ID as an string, defined by the element class.
+ * This should have a form like P000014, for a part with ID 14.
+ * @return string The ID as a string;
+ */
+ public function getIDString(): string
+ {
+ return "";
+ //return 'AT' . sprintf('%09d', $this->getID());
+ }
+}
diff --git a/src/Entity/Category.php b/src/Entity/Category.php
new file mode 100644
index 00000000..1ae4935c
--- /dev/null
+++ b/src/Entity/Category.php
@@ -0,0 +1,110 @@
+getID());
+ }
+}
diff --git a/src/Entity/Company.php b/src/Entity/Company.php
new file mode 100644
index 00000000..5f8adf34
--- /dev/null
+++ b/src/Entity/Company.php
@@ -0,0 +1,211 @@
+address;
+ }
+
+ /**
+ * Get the phone number
+ *
+ * @return string the phone number of the company
+ */
+ public function getPhoneNumber() : string
+ {
+ return $this->phone_number;
+ }
+
+ /**
+ * Get the fax number
+ *
+ * @return string the fax number of the company
+ */
+ public function getFaxNumber() : string
+ {
+ return $this->fax_number;
+ }
+
+ /**
+ * Get the e-mail address
+ *
+ * @return string the e-mail address of the company
+ */
+ public function getEmailAddress() : string
+ {
+ return $this->email_address;
+ }
+
+ /**
+ * Get the website
+ *
+ * @return string the website of the company
+ */
+ public function getWebsite() : string
+ {
+ return $this->website;
+ }
+
+ /**
+ * Get the link to the website of an article
+ *
+ * @param string $partnr @li NULL for returning the URL with a placeholder for the part number
+ * @li or the part number for returning the direct URL to the article
+ *
+ * @return string the link to the article
+ */
+ public function getAutoProductUrl($partnr = null) : string
+ {
+ if (\is_string($partnr)) {
+ return str_replace('%PARTNUMBER%', $partnr, $this->auto_product_url);
+ }
+ return $this->auto_product_url;
+ }
+
+ /********************************************************************************
+ *
+ * Setters
+ *
+ *********************************************************************************/
+
+ /**
+ * Set the addres
+ * @param string $new_address the new address (with "\n" as line break)
+ */
+ public function setAddress(string $new_address)
+ {
+ $this->address = $new_address;
+ }
+
+ /**
+ * Set the phone number
+ *
+ * @param string $new_phone_number the new phone number
+ */
+ public function setPhoneNumber(string $new_phone_number)
+ {
+ $this->phone_number = $new_phone_number;
+ }
+
+ /**
+ * Set the fax number
+ *
+ * @param string $new_fax_number the new fax number
+ */
+ public function setFaxNumber(string $new_fax_number)
+ {
+ $this->fax_number = $new_fax_number;
+ }
+
+ /**
+ * Set the e-mail address
+ *
+ * @param string $new_email_address the new e-mail address
+
+ */
+ public function setEmailAddress(string $new_email_address)
+ {
+ $this->email_address = $new_email_address;
+ }
+
+ /**
+ * Set the website
+ *
+ * @param string $new_website the new website
+ */
+ public function setWebsite(string $new_website)
+ {
+ $this->website = $new_website;
+ }
+
+ /**
+ * Set the link to the website of an article
+ *
+ * @param string $new_url the new URL with the placeholder %PARTNUMBER% for the part number
+ *
+ */
+ public function setAutoProductUrl(string $new_url)
+ {
+ $this->auto_product_url = $new_url;
+ }
+
+}
\ No newline at end of file
diff --git a/src/Entity/DBElement.php b/src/Entity/DBElement.php
new file mode 100644
index 00000000..37815dd4
--- /dev/null
+++ b/src/Entity/DBElement.php
@@ -0,0 +1,66 @@
+id;
+ }
+
+ /**
+ * Returns the ID as an string, defined by the element class.
+ * This should have a form like P000014, for a part with ID 14.
+ * @return string The ID as a string;
+ */
+ abstract public function getIDString() : string;
+
+}
\ No newline at end of file
diff --git a/src/Entity/Device.php b/src/Entity/Device.php
new file mode 100644
index 00000000..ed96ff0e
--- /dev/null
+++ b/src/Entity/Device.php
@@ -0,0 +1,111 @@
+order_quantity;
+ }
+
+ /**
+ * Get the "order_only_missing_parts" attribute
+ *
+ * @return boolean the "order_only_missing_parts" attribute
+ */
+ public function getOrderOnlyMissingParts() : bool
+ {
+ return $this->order_only_missing_parts;
+ }
+
+ /********************************************************************************
+ *
+ * Setters
+ *
+ *********************************************************************************/
+
+ /**
+ * Set the order quantity
+ *
+ * @param integer $new_order_quantity the new order quantity
+ */
+ public function setOrderQuantity(int $new_order_quantity)
+ {
+ if($new_order_quantity < 0)
+ {
+ throw new \InvalidArgumentException("The new order quantity must not be negative!");
+ }
+ $this->order_quantity = $new_order_quantity;
+ }
+
+ /**
+ * Set the "order_only_missing_parts" attribute
+ *
+ * @param boolean $new_order_only_missing_parts the new "order_only_missing_parts" attribute
+ *
+ */
+ public function setOrderOnlyMissingParts(bool $new_order_only_missing_parts)
+ {
+ $this->order_only_missing_parts = $new_order_only_missing_parts;
+ }
+
+
+ /**
+ * Returns the ID as an string, defined by the element class.
+ * This should have a form like P000014, for a part with ID 14.
+ * @return string The ID as a string;
+ */
+ public function getIDString(): string
+ {
+ return 'D' . sprintf('%09d', $this->getID());
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/DevicePart.php b/src/Entity/DevicePart.php
new file mode 100644
index 00000000..e58180d2
--- /dev/null
+++ b/src/Entity/DevicePart.php
@@ -0,0 +1,42 @@
+getID());
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Footprint.php b/src/Entity/Footprint.php
new file mode 100644
index 00000000..892e5fa5
--- /dev/null
+++ b/src/Entity/Footprint.php
@@ -0,0 +1,192 @@
+getID());
+ }
+
+ /****************************************
+ * Getters
+ ****************************************/
+
+ /**
+ * Get the filename of the picture (absolute path from filesystem root)
+ *
+ * @param bool $absolute If set to true, then the absolute filename (from system root) is returned.
+ * If set to false, then the path relative to Part-DB folder is returned.
+ * @return string @li the absolute path to the picture (from filesystem root), as a UNIX path (with slashes)
+ * @li an empty string if there is no picture
+ */
+ public function getFilename(bool $absolute = true) : string
+ {
+ if ($absolute == true) {
+ //TODO
+ throw new \Exception("Not Implemented yet...");
+ //return str_replace('%BASE%', BASE, $this->db_data['filename']);
+ } else {
+ return $this->filename;
+ }
+ }
+
+ /**
+ * Get the filename of the 3d model (absolute path from filesystem root)
+ *
+ * @param bool $absolute If set to true, then the absolute filename (from system root) is returned.
+ * If set to false, then the path relative to Part-DB folder is returned.
+ *
+ * @return string @li the absolute path to the model (from filesystem root), as a UNIX path (with slashes)
+ * @li an empty string if there is no model
+ */
+ public function get3dFilename(bool $absolute = true) : string
+ {
+ if ($absolute == true) {
+ //TODO
+ throw new \Exception("Not Implemented yet...");
+ //return str_replace('%BASE%', BASE, $this->db_data['filename_3d']);
+ } else {
+ return $this->filename_3d;
+ }
+ }
+
+ /**
+ * Check if the filename of this footprint is valid (picture exists)
+ *
+ * This method is used to get all footprints with broken filename
+ * (Footprint::get_broken_filename_footprints()).
+ *
+ * @note An empty filename is a valid filename.
+ *
+ * @return boolean @li true if file exists or filename is empty
+ * @li false if there is no file with this filename
+ */
+ public function isFilenameValid() : bool
+ {
+ if (empty($this->getFilename())) {
+ return true;
+ }
+
+ return file_exists($this->getFilename());
+ }
+
+ /**
+ * Check if the filename of this 3d footprint is valid (model exists and have )
+ *
+ * This method is used to get all footprints with broken 3d filename
+ * (Footprint::get_broken_3d_filename_footprints()).
+ *
+ * @note An empty filename is a valid filename.
+ *
+ * @return boolean @li true if file exists or filename is empty
+ * @li false if there is no file with this filename
+ */
+ public function is3dFilenameValid() : bool
+ {
+ if (empty($this->get3dFilename())) {
+ return true;
+ }
+
+ //Check if file is X3D-Model (these has .x3d extension)
+ if (strpos($this->get3dFilename(), '.x3d') == false) {
+ return false;
+ }
+
+ return file_exists($this->get3dFilename());
+ }
+
+ /*****************************************************************************
+ * Setters
+ ****************************************************************************/
+
+ /********************************************************************************
+ *
+ * Setters
+ *
+ *********************************************************************************/
+
+ /**
+ * Change the filename of this footprint
+ *
+ * @note The filename won't be checked if it is valid.
+ * It's not really a Problem if there is no such file...
+ * (For this purpose we have the method Footprint::get_broken_filename_footprints())
+ *
+ * @param string $new_filename @li the new filename (absolute path from filesystem root, as a UNIX path [only slashes!] !! )
+ * @li see also lib.functions.php::to_unix_path()
+ *
+ * @warning It's really important that you pass the whole (UNIX) path from filesystem root!
+ * If the file is located in the base directory of Part-DB, the base path
+ * will be automatically replaced with a placeholder before write it in the database.
+ * This way, the filenames are still correct if the installation directory
+ * of Part-DB is moved.
+ *
+ * @note The path-replacing will be done in Footprint::check_values_validity(), not here.
+ *
+ * @throws Exception if there was an error
+ */
+ public function setFilename(string $new_filename)
+ {
+ $this->filename = $new_filename;
+ }
+
+ /**
+ * Change the 3d model filename of this footprint
+ * @throws Exception if there was an error
+ */
+ public function set3dFilename(string $new_filename)
+ {
+ $this->filename = $new_filename;
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Manufacturer.php b/src/Entity/Manufacturer.php
new file mode 100644
index 00000000..9867355a
--- /dev/null
+++ b/src/Entity/Manufacturer.php
@@ -0,0 +1,47 @@
+getID());
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/NamedDBElement.php b/src/Entity/NamedDBElement.php
new file mode 100644
index 00000000..30aaca39
--- /dev/null
+++ b/src/Entity/NamedDBElement.php
@@ -0,0 +1,114 @@
+name);
+ }
+
+ /**
+ * Returns the last time when the element was modified.
+ * @param $formatted bool When true, the date gets formatted with the locale and timezone settings.
+ * When false, the raw value from the DB is returned.
+ * @return string The time of the last edit.
+ */
+ public function getLastModified(bool $formatted = true) : string
+ {
+ //TODO
+ return "TODO";
+ }
+
+ /**
+ * Returns the date/time when the element was created.
+ * @param $formatted bool When true, the date gets formatted with the locale and timezone settings.
+ * When false, the raw value from the DB is returned.
+ * @return string The creation time of the part.
+ */
+ public function getDatetimeAdded(bool $formatted = true) : string
+ {
+ //TODO
+ return "TODO";
+ }
+
+ /********************************************************************************
+ *
+ * Setters
+ *
+ *********************************************************************************/
+
+ /**
+ * Change the name of this element
+ *
+ * @note Spaces at the begin and at the end of the string will be removed
+ * automatically in NamedDBElement::check_values_validity().
+ * So you don't have to do this yourself.
+ *
+ * @param string $new_name the new name
+ */
+ public function setName(string $new_name)
+ {
+ $this->name = $new_name;
+ }
+
+}
\ No newline at end of file
diff --git a/src/Entity/Orderdetail.php b/src/Entity/Orderdetail.php
new file mode 100644
index 00000000..a84f326f
--- /dev/null
+++ b/src/Entity/Orderdetail.php
@@ -0,0 +1,266 @@
+getID());
+ }
+
+ /********************************************************************************
+ *
+ * Getters
+ *
+ *********************************************************************************/
+
+ /**
+ * Get the part
+ *
+ * @return Part the part of this orderdetails
+ */
+ public function getPart() : Part
+ {
+ return $this->part;
+ }
+
+ /**
+ * Get the supplier
+ *
+ * @return Supplier the supplier of this orderdetails
+ *
+ * @throws DatabaseException if there was an error
+ */
+ public function getSupplier() : Supplier
+ {
+ return $this->supplier;
+ }
+
+ /**
+ * Get the supplier part-nr.
+ *
+ * @return string the part-nr.
+ */
+ public function getSupplierPartNr() : string
+ {
+ return $this->supplierpartnr;
+ }
+
+ /**
+ * Get if this orderdetails is obsolete
+ *
+ * "Orderdetails is obsolete" means that the part with that supplier-part-nr
+ * is no longer available from the supplier of that orderdetails.
+ *
+ * @return boolean @li true if this part is obsolete at that supplier
+ * @li false if this part isn't obsolete at that supplier
+ */
+ public function getObsolete() : bool
+ {
+ return (bool) $this->obsolete;
+ }
+
+ /**
+ * Get the link to the website of the article on the suppliers website.
+ *
+ * @param $no_automatic_url bool Set this to true, if you only want to get the local set product URL for this Orderdetail
+ * and not a automatic generated one, based from the Supplier
+ *
+ * @return string the link to the article
+ */
+ public function getSupplierProductUrl(bool $no_automatic_url = false) : string
+ {
+ if ($no_automatic_url || $this->supplierpartnr != '') {
+ return $this->supplierpartnr;
+ } else {
+ return $this->getSupplier()->getAutoProductUrl($this->supplierpartnr);
+ } // maybe an automatic url is available...
+ }
+
+ /**
+ * Get all pricedetails
+ *
+ * @return Pricedetails[] all pricedetails as a one-dimensional array of Pricedetails objects,
+ * sorted by minimum discount quantity
+ *
+ * @throws Exception if there was an error
+ */
+ public function getPricedetails() : array
+ {
+ return $this->pricedetails;
+ }
+
+ /**
+ * Get the price for a specific quantity
+ *
+ * @param boolean $as_money_string @li if true, this method returns a money string incl. currency
+ * @li if false, this method returns the price as float
+ * @param integer $quantity this is the quantity to choose the correct pricedetails
+ * @param integer|NULL $multiplier @li This is the multiplier which will be applied to every single price
+ * @li If you pass NULL, the number from $quantity will be used
+ *
+ * @return float|null|string float: the price as a float number (if "$as_money_string == false")
+ * * null: if there are no prices and "$as_money_string == false"
+ * * string: the price as a string incl. currency (if "$as_money_string == true")
+ *
+ * @throws Exception if there are no pricedetails for the choosed quantity
+ * (for example, there are only one pricedetails with the minimum discount quantity '10',
+ * but the choosed quantity is '5' --> the price for 5 parts is not defined!)
+ * @throws Exception if there was an error
+ *
+ * @see floatToMoneyString()
+ */
+ public function getPrice(bool $as_money_string = false, int $quantity = 1, $multiplier = null)
+ {
+ /**
+ if (($quantity == 0) && ($multiplier === null)) {
+ if ($as_money_string) {
+ return floatToMoneyString(0);
+ } else {
+ return 0;
+ }
+ }
+
+ $all_pricedetails = $this->getPricedetails();
+
+ if (count($all_pricedetails) == 0) {
+ if ($as_money_string) {
+ return floatToMoneyString(null);
+ } else {
+ return null;
+ }
+ }
+
+ foreach ($all_pricedetails as $pricedetails) {
+ // choose the correct pricedetails for the choosed quantity ($quantity)
+ if ($quantity < $pricedetails->getMinDiscountQuantity()) {
+ break;
+ }
+
+ $correct_pricedetails = $pricedetails;
+ }
+
+ if (! isset($correct_pricedetails) || (! \is_object($correct_pricedetails))) {
+ throw new Exception(_('Es sind keine Preisinformationen für die angegebene Bestellmenge vorhanden!'));
+ }
+
+ if ($multiplier === null) {
+ $multiplier = $quantity;
+ }
+
+ return $correct_pricedetails->getPrice($as_money_string, $multiplier);
+ * */
+ //TODO
+ throw new \Exception("Not implemented yet...");
+ }
+
+ /********************************************************************************
+ *
+ * Setters
+ *
+ *********************************************************************************/
+
+ /**
+ * Set the supplier ID
+ *
+ * @param integer $new_supplier_id the ID of the new supplier
+
+ */
+ public function setSupplierId(int $new_supplier_id)
+ {
+ throw new \Exception("Not implemented yet!");
+ //TODO;
+ }
+
+ /**
+ * Set the supplier part-nr.
+ *
+ * @param string $new_supplierpartnr the new supplier-part-nr
+ *
+ */
+ public function setSupplierpartnr(string $new_supplierpartnr)
+ {
+ $this->supplierpartnr = $new_supplierpartnr;
+ }
+
+ /**
+ * Set if the part is obsolete at the supplier of that orderdetails
+ *
+ * @param boolean $new_obsolete true means that this part is obsolete
+ */
+ public function setObsolete(bool $new_obsolete)
+ {
+ $this->obsolete = $new_obsolete;
+ }
+
+ /**
+ * Sets the custom product supplier URL for this order detail.
+ * Set this to "", if the function getSupplierProductURL should return the automatic generated URL.
+ * @param $new_url string The new URL for the supplier URL.
+ * @throws Exception if there was an error
+ */
+ public function setSupplierProductUrl(string $new_url)
+ {
+ $this->setAttributes(array('supplier_product_url' => $new_url));
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Part.php b/src/Entity/Part.php
new file mode 100644
index 00000000..809b008c
--- /dev/null
+++ b/src/Entity/Part.php
@@ -0,0 +1,1050 @@
+getID());
+ }
+
+
+ /*********************************************************************************
+ * Getters
+ ********************************************************************************/
+
+ /**
+ * Get the description
+ *
+ * @param boolean|int $bbcode_parse_level Should BBCode converted to HTML, before returning
+ * @param int $short_output If this is bigger than 0, than the description will be shortened to this length.
+ * @return string the description
+ */
+ public function getDescription($bbcode_parse_level = 0/*BBCodeParsingLevel::PARSE*/, int $short_output = 0) : string
+ {
+ $val = htmlspecialchars($this->description);
+
+ if ($short_output > 0 && \strlen($val) > $short_output) {
+ $val = substr($val, 0, $short_output);
+ $val .= '...';
+ $val = '' . $val . '';
+ }
+
+ //TODO
+ /**
+ if ($bbcode_parse_level === BBCodeParsingLevel::PARSE) {
+ $bbcode = new BBCodeParser();
+ $val = $bbcode->only('bold', 'italic', 'underline', 'linethrough')->parse($val);
+ } elseif ($bbcode_parse_level === BBCodeParsingLevel::STRIP) {
+ $bbcode = new BBCodeParser();
+ $val = $bbcode->stripBBCodeTags($val);
+ }*/
+
+ return $val;
+ }
+
+ /**
+ * Get the count of parts which are in stock
+ * @param $with_unknown bool Set this, to true, if the unknown state should be returned as string. Otherwise -2 is returned.
+ *
+ * @return integer|string count of parts which are in stock, "Unknown" if $with_unknown is set and instock is unknown.
+ */
+ public function getInstock(bool $with_unknown = false) : int
+ {
+
+ if ($with_unknown && $this->isInstockUnknown()) {
+ return _('[Unbekannt]');
+ }
+
+ return $this->instock;
+ }
+
+ /**
+ * Check if the value of the Instock is unknown.
+ * @return bool True, if the value of the instock is unknown.
+ */
+ public function isInstockUnknown() : bool
+ {
+ return $this->instock <= static::INSTOCK_UNKNOWN;
+ }/** @noinspection ReturnTypeCanBeDeclaredInspection */
+
+ /**
+ * Get the count of parts which must be in stock at least
+ *
+ * @return integer count of parts which must be in stock at least
+ */
+ public function getMinInstock() : int
+ {
+ return $this->mininstock;
+ }
+
+ /**
+ * Get the comment
+ *
+ * @param boolean|int $bbcode_parsing_level Should BBCode converted to HTML, before returning
+ * @return string the comment
+ */
+ public function getComment($bbcode_parsing_level = BBCodeParsingLevel::PARSE) : string
+ {
+
+ $val = htmlspecialchars($this->comment);
+
+ //TODO
+ /*if ($bbcode_parsing_level === BBCodeParsingLevel::PARSE) {
+ $bbcode = new BBCodeParser();
+ $bbcode->setParser('brLinebreak', "/\[br\]/s", ' ', '');
+ $bbcode->setParser('namedlink', '/\[url\=(.*?)\](.*?)\[\/url\]/s', '$2', '$2');
+ $bbcode->setParser('link', '/\[url\](.*?)\[\/url\]/s', '$1', '$1');
+ $val = $bbcode->parse($val);
+ } elseif ($bbcode_parsing_level === BBCodeParsingLevel::STRIP) {
+ $bbcode = new BBCodeParser();
+ $val = str_replace("\n", ' ', $val);
+ $val = $bbcode->stripBBCodeTags($val);
+ }*/
+
+ return $val;
+ }
+
+ /**
+ * Get if this part is obsolete
+ *
+ * @note A Part is marked as "obsolete" if all their orderdetails are marked as "obsolete".
+ * If a part has no orderdetails, the part isn't marked as obsolete.
+ *
+ * @return boolean @li true if this part is obsolete
+ * @li false if this part isn't obsolete
+ * @throws Exception
+ */
+ public function getObsolete() : bool
+ {
+ $all_orderdetails = $this->getOrderdetails();
+
+ if (count($all_orderdetails) == 0) {
+ return false;
+ }
+
+ foreach ($all_orderdetails as $orderdetails) {
+ if (! $orderdetails->getObsolete()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Get if this part is visible
+ *
+ * @return boolean @li true if this part is visible
+ * @li false if this part isn't visible
+ */
+ public function getVisible() : bool
+ {
+ return (bool) $this->visible;
+ }
+
+ /**
+ * Get if this part is a favorite.
+ *
+ * @return bool * true if this part is a favorite
+ * * false if this part is not a favorite.
+ */
+ public function getFavorite() : bool
+ {
+ return (bool) $this->favorite;
+ }
+
+ /**
+ * Get the selected order orderdetails of this part
+ *
+ * @return Orderdetail the selected order orderdetails
+ * @return NULL if there is no order supplier selected
+ * @throws Exception
+ */
+ public function getOrderOrderdetails() : ?Orderdetail
+ {
+ //TODO
+ /*
+ if ($this->order_orderdetails->getObsolete()) {
+ $this->setOrderOrderdetailsID(null);
+ $this->order_orderdetails = null;
+ }*/
+
+ return $this->order_orderdetail;
+ }
+
+ /**
+ * Get the order quantity of this part
+ *
+ * @return integer the order quantity
+ */
+ public function getOrderQuantity() : int
+ {
+ return (int) $this->order_quantity;
+ }
+
+ /**
+ * Get the minimum quantity which should be ordered
+ *
+ * @param boolean $with_devices @li if true, all parts from devices which are marked as "to order" will be included in the calculation
+ * @li if false, only max(mininstock - instock, 0) will be returned
+ *
+ * @return integer the minimum order quantity
+ * @throws Exception
+ */
+ public function getMinOrderQuantity(bool $with_devices = true) : int
+ {
+ //TODO
+ throw new \Exception("Not implemented yet...");
+
+ /**
+ if ($with_devices) {
+ $count_must_order = 0; // for devices with "order_only_missing_parts == false"
+ $count_should_order = 0; // for devices with "order_only_missing_parts == true"
+ $deviceparts = DevicePart::getOrderDeviceParts($this->database, $this->current_user, $this->log, $this->getID());
+ foreach ($deviceparts as $devicepart) {
+ /** @var $devicepart DevicePart */
+ /** @var $device Device */ /**
+ $device = $devicepart->getDevice();
+ if ($device->getOrderOnlyMissingParts()) {
+ $count_should_order += $device->getOrderQuantity() * $devicepart->getMountQuantity();
+ } else {
+ $count_must_order += $device->getOrderQuantity() * $devicepart->getMountQuantity();
+ }
+ }
+
+ return $count_must_order + max(0, $this->getMinInstock() - $this->getInstock() + $count_should_order);
+ } else {
+ return max(0, $this->getMinInstock() - $this->getInstock());
+ } **/
+ }
+
+ /**
+ * Get the "manual_order" attribute
+ *
+ * @return boolean the "manual_order" attribute
+ */
+ public function getManualOrder() : bool
+ {
+ return (bool) $this->manual_order;
+ }
+
+ /**
+ * Check if the part is automatically marked for Ordering, because the instock value is smaller than the min instock value.
+ * @return bool True, if the part should be ordered.
+ */
+ public function getAutoOrder() : bool
+ {
+ //Parts with negative instock never gets ordered.
+ if ($this->getInstock() < 0) {
+ return false;
+ }
+
+ return $this->getInstock() < $this->getMinInstock();
+ }
+
+ /**
+ * Get the link to the website of the article on the manufacturers website
+ *
+ * @param
+ *
+ * @return string the link to the article
+ * @throws Exception
+ */
+ public function getManufacturerProductUrl(bool $no_auto_url = false) : string
+ {
+ if ($no_auto_url || $this->manufacturer_product_url != '') {
+ return $this->manufacturer_product_url;
+ } elseif (\is_object($this->getManufacturer())) {
+ return $this->getManufacturer()->getAutoProductUrl($this->name);
+ } else {
+ return '';
+ } // no url is available
+ }
+
+ /**
+ * Get the category of this part
+ *
+ * There is always a category, for each part!
+ *
+ * @return Category the category of this part
+ */
+ public function getCategory() : Category
+ {
+ return $this->category;
+ }
+
+ /**
+ * Get the footprint of this part (if there is one)
+ *
+ * @return Footprint the footprint of this part (if there is one)
+ * @return NULL if this part has no footprint
+ */
+ public function getFootprint() : ?Footprint
+ {
+ return $this->footprint;
+ }
+
+ /**
+ * Get the storelocation of this part (if there is one)
+ *
+ * @return Storelocation the storelocation of this part (if there is one)
+ * @return NULL if this part has no storelocation
+ */
+ public function getStorelocation() : ?Storelocation
+ {
+ return $this->storelocation;
+ }
+
+ /**
+ * Get the manufacturer of this part (if there is one)
+ *
+ * @return Manufacturer the manufacturer of this part (if there is one)
+ * @return NULL if this part has no manufacturer
+ */
+ public function getManufacturer() : ?Manufacturer
+ {
+ return $this->manufacturer;
+ }
+
+ /**
+ * Get the master picture "Attachement"-object of this part (if there is one)
+ *
+ * @return Attachment the master picture Attachement of this part (if there is one)
+ * @return NULL if this part has no master picture
+ */
+ public function getMasterPictureAttachement() : ?Attachment
+ {
+ return $this->master_picture_attachement;
+ }
+
+ /**
+ * Get all orderdetails of this part
+ *
+ * @param boolean $hide_obsolete If true, obsolete orderdetails will NOT be returned
+ *
+ * @return Orderdetails[] @li all orderdetails as a one-dimensional array of Orderdetails objects
+ * (empty array if there are no ones)
+ * @li the array is sorted by the suppliers names / minimum order quantity
+ *
+ * @throws Exception if there was an error
+ */
+ public function getOrderdetails(bool $hide_obsolete = false)
+ {
+ if ($hide_obsolete) {
+ $orderdetails = $this->orderdetails;
+ foreach ($orderdetails as $key => $details) {
+ if ($details->getObsolete()) {
+ unset($orderdetails[$key]);
+ }
+ }
+ return $orderdetails;
+ } else {
+ return $this->orderdetails;
+ }
+ }
+
+ /**
+ * Get all devices which uses this part
+ *
+ * @return Device[] @li all devices which uses this part as a one-dimensional array of Device objects
+ * (empty array if there are no ones)
+ * @li the array is sorted by the devices names
+ *
+ * @throws Exception if there was an error
+ */
+ public function getDevices() : array
+ {
+ return $this->devices;
+ }
+
+ /**
+ * Get all suppliers of this part
+ *
+ * This method simply gets the suppliers of the orderdetails and prepare them.\n
+ * You can get the suppliers as an array or as a string with individual delimeter.
+ *
+ * @param boolean $object_array @li if true, this method returns an array of Supplier objects
+ * @li if false, this method returns an array of strings
+ * @param string|NULL $delimeter @li if this is a string and "$object_array == false",
+ * this method returns a string with all
+ * supplier names, delimeted by "$delimeter"
+ * @param boolean $full_paths @li if true and "$object_array = false", the returned
+ * suppliernames are full paths (path + name)
+ * @li if true and "$object_array = false", the returned
+ * suppliernames are only the names (without path)
+ * @param boolean $hide_obsolete If true, suppliers from obsolete orderdetails will NOT be returned
+ *
+ * @return array all suppliers as a one-dimensional array of Supplier objects
+ * (if "$object_array == true")
+ * @return array all supplier-names as a one-dimensional array of strings
+ * ("if $object_array == false" and "$delimeter == NULL")
+ * @return string a sting of all supplier names, delimeted by $delimeter
+ * ("if $object_array == false" and $delimeter is a string)
+ *
+ * @throws Exception if there was an error
+ */
+ public function getSuppliers(bool $object_array = true, $delimeter = null, bool $full_paths = false, bool $hide_obsolete = false)
+ {
+ $suppliers = array();
+ $orderdetails = $this->getOrderdetails($hide_obsolete);
+
+ foreach ($orderdetails as $details) {
+ $suppliers[] = $details->getSupplier();
+ }
+
+ if ($object_array) {
+ return $suppliers;
+ } else {
+ $supplier_names = array();
+ foreach ($suppliers as $supplier) {
+ /** @var Supplier $supplier */
+ if ($full_paths) {
+ $supplier_names[] = $supplier->getFullPath();
+ } else {
+ $supplier_names[] = $supplier->getName();
+ }
+ }
+
+ if (\is_string($delimeter)) {
+ return implode($delimeter, $supplier_names);
+ } else {
+ return $supplier_names;
+ }
+ }
+ }
+
+ /**
+ * Get all supplier-part-Nrs
+ *
+ * This method simply gets the suppliers-part-Nrs of the orderdetails and prepare them.\n
+ * You can get the numbers as an array or as a string with individual delimeter.
+ *
+ * @param string|NULL $delimeter @li if this is a string, this method returns a delimeted string
+ * @li otherwise, this method returns an array of strings
+ * @param boolean $hide_obsolete If true, supplierpartnrs from obsolete orderdetails will NOT be returned
+ *
+ * @return array all supplierpartnrs as an array of strings (if "$delimeter == NULL")
+ * @return string all supplierpartnrs as a string, delimeted ba $delimeter (if $delimeter is a string)
+ *
+ * @throws Exception if there was an error
+ */
+ public function getSupplierPartNrs($delimeter = null, bool $hide_obsolete = false)
+ {
+ $supplierpartnrs = array();
+
+ foreach ($this->getOrderdetails($hide_obsolete) as $details) {
+ $supplierpartnrs[] = $details->getSupplierPartNr();
+ }
+
+ if (\is_string($delimeter)) {
+ return implode($delimeter, $supplierpartnrs);
+ } else {
+ return $supplierpartnrs;
+ }
+ }
+
+ /**
+ * Get all prices of this part
+ *
+ * This method simply gets the prices of the orderdetails and prepare them.\n
+ * In the returned array/string there is a price for every supplier.
+ *
+ * @param boolean $float_array @li if true, the returned array is an array of floats
+ * @li if false, the returned array is an array of strings
+ * @param string|NULL $delimeter if this is a string, this method returns a delimeted string
+ * instead of an array.
+ * @param integer $quantity this is the quantity to choose the correct priceinformation
+ * @param integer|NULL $multiplier @li This is the multiplier which will be applied to every single price
+ * @li If you pass NULL, the number from $quantity will be used
+ * @param boolean $hide_obsolete If true, prices from obsolete orderdetails will NOT be returned
+ *
+ * @return array all prices as an array of floats (if "$delimeter == NULL" & "$float_array == true")
+ * @return array all prices as an array of strings (if "$delimeter == NULL" & "$float_array == false")
+ * @return string all prices as a string, delimeted by $delimeter (if $delimeter is a string)
+ *
+ * @warning If there are orderdetails without prices, for these orderdetails there
+ * will be a "NULL" in the returned float array (or a "-" in the string array)!!
+ * (This is needed for the HTML output, if there are all orderdetails and prices listed.)
+ *
+ * @throws Exception if there was an error
+ */
+ public function getPrices(bool $float_array = false, $delimeter = null, int $quantity = 1, $multiplier = null, bool $hide_obsolete = false)
+ {
+ $prices = array();
+
+ foreach ($this->getOrderdetails($hide_obsolete) as $details) {
+ $prices[] = $details->getPrice(! $float_array, $quantity, $multiplier);
+ }
+
+ if (\is_string($delimeter)) {
+ return implode($delimeter, $prices);
+ } else {
+ return $prices;
+ }
+ }
+
+ /**
+ * Get the average price of all orderdetails
+ *
+ * With the $multiplier you're able to multiply the price before it will be returned.
+ * This is useful if you want to have the price as a string with currency, but multiplied with a factor.
+ *
+ * @param boolean $as_money_string @li if true, the retruned value will be a string incl. currency,
+ * ready to print it out. See float_to_money_string().
+ * @li if false, the returned value is a float
+ * @param integer $quantity this is the quantity to choose the correct priceinformations
+ * @param integer|NULL $multiplier @li This is the multiplier which will be applied to every single price
+ * @li If you pass NULL, the number from $quantity will be used
+ *
+ * @return float price (if "$as_money_string == false")
+ * @return NULL if there are no prices for this part and "$as_money_string == false"
+ * @return string price with currency (if "$as_money_string == true")
+ *
+ * @throws Exception if there was an error
+ */
+ public function getAveragePrice(bool $as_money_string = false, int $quantity = 1, $multiplier = null)
+ {
+ $prices = $this->getPrices(true, null, $quantity, $multiplier, true);
+ $average_price = null;
+
+ $count = 0;
+ foreach ($prices as $price) {
+ if ($price !== null) {
+ $average_price += $price;
+ $count++;
+ }
+ }
+
+ if ($count > 0) {
+ $average_price /= $count;
+ }
+
+ if ($as_money_string) {
+ return floatToMoneyString($average_price);
+ } else {
+ return $average_price;
+ }
+ }
+
+ /**
+ * Get the filename of the master picture (absolute path from filesystem root)
+ *
+ * @param boolean $use_footprint_filename @li if true, and this part has no picture, this method
+ * will return the filename of its footprint (if available)
+ * @li if false, and this part has no picture,
+ * this method will return NULL
+ *
+ * @return string the whole path + filename from filesystem root as a UNIX path (with slashes)
+ * @return NULL if there is no picture
+ *
+ * @throws Exception if there was an error
+ */
+ public function getMasterPictureFilename(bool $use_footprint_filename = false)
+ {
+ $master_picture = $this->getMasterPictureAttachement(); // returns an Attachement-object
+
+ if (\is_object($master_picture)) {
+ return $master_picture->getFilename();
+ }
+
+ if ($use_footprint_filename) {
+ $footprint = $this->getFootprint();
+ if (\is_object($footprint)) {
+ return $footprint->getFilename();
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Parses the selected fields and extract Properties of the part.
+ * @param bool $use_description Use the description field for parsing
+ * @param bool $use_comment Use the comment field for parsing
+ * @param bool $use_name Use the name field for parsing
+ * @param bool $force_output Properties are parsed even if properties are disabled.
+ * @return array A array of PartProperty objects.
+ * @return array If Properties are disabled or nothing was detected, then an empty array is returned.
+ * @throws Exception
+ */
+ public function getProperties(bool $use_description = true, bool $use_comment = true, bool $use_name = true, bool $force_output = false) : array
+ {
+ //TODO
+ throw new \Exception("Not implemented yet!");
+ /*
+ global $config;
+
+ if ($config['properties']['active'] || $force_output) {
+ if ($this->getCategory()->getDisableProperties(true)) {
+ return array();
+ }
+
+ $name = array();
+ $desc = array();
+ $comm = array();
+
+ if ($use_name === true) {
+ $name = $this->getCategory()->getPartnameRegexObj()->getProperties($this->getName());
+ }
+ if ($use_description === true) {
+ $desc = PartProperty::parseDescription($this->getDescription());
+ }
+ if ($use_comment === true) {
+ $comm = PartProperty::parseDescription($this->getComment(false));
+ }
+
+ return array_merge($name, $desc, $comm);
+ } else {
+ return array();
+ }*/
+ }
+
+ /**
+ * Returns a loop (array) of the array representations of the properties of this part.
+ * @param bool $use_description Use the description field for parsing
+ * @param bool $use_comment Use the comment field for parsing
+ * @return array A array of arrays with the name and value of the properties.
+ */
+ public function getPropertiesLoop(bool $use_description = true, bool $use_comment = true, bool $use_name = true) : array
+ {
+ //TODO
+ throw new \Exception("Not implemented yet!");
+ $arr = array();
+ foreach ($this->getProperties($use_description, $use_comment, $use_name) as $property) {
+ /* @var PartProperty $property */
+ $arr[] = $property->getArray(true);
+ }
+ return $arr;
+ }
+
+ /*
+ public function hasValidName() : bool
+ {
+ return self::isValidName($this->getName(), $this->getCategory());
+ } */
+
+
+ public function getAttachmentTypes() : array
+ {
+ return parent::getAttachmentTypes();
+ }
+
+ public function getAttachments($type_id = null, bool $only_table_attachements = false) : array
+ {
+ return parent::getAttachments($type_id, $only_table_attachements);
+ }
+
+ /********************************************************************************
+ *
+ * Setters
+ *
+ *********************************************************************************/
+
+ /**
+ * Set the description
+ *
+ * @param string $new_description the new description
+ *
+ * @throws Exception if there was an error
+ */
+ public function setDescription(string $new_description)
+ {
+ $this->description = $new_description;
+ }
+
+ /**
+ * Set the count of parts which are in stock
+ *
+ * @param integer $new_instock the new count of parts which are in stock
+ *
+ * @throws Exception if the new instock is not valid
+ * @throws Exception if there was an error
+ */
+ public function setInstock(int $new_instock, $comment = null)
+ {
+ $old_instock = (int) $this->getInstock();
+ $this->instock = $new_instock;
+ //TODO
+ /*
+ InstockChangedEntry::add(
+ $this->database,
+ $this->current_user,
+ $this->log,
+ $this,
+ $old_instock,
+ $new_instock,
+ $comment
+ );*/
+ }
+
+ /**
+ * Withdrawal the given number of parts.
+ * @param $count int The number of parts which should be withdrawan.
+ * @param $comment string A comment that should be associated with the withdrawal.
+ * @throws Exception if there was an error
+ */
+ public function withdrawalParts(int $count, $comment = null)
+ {
+ if ($count <= 0) {
+ throw new \Exception('Zahl der entnommenen Bauteile muss größer 0 sein!');
+ }
+ if ($count > $this->getInstock()) {
+ throw new Exception('Es können nicht mehr Bauteile entnommen werden, als vorhanden sind!');
+ }
+
+ $old_instock = (int) $this->getInstock();
+ $new_instock = $old_instock - $count;
+
+ //TODO
+ /*
+ InstockChangedEntry::add(
+ $this->database,
+ $this->current_user,
+ $this->log,
+ $this,
+ $old_instock,
+ $new_instock,
+ $comment
+ );*/
+
+ $this->instock = $new_instock;
+ }
+
+ /**
+ * Add the given number of parts.
+ * @param $count int The number of parts which should be withdrawan.
+ * @param $comment string A comment that should be associated with the withdrawal.
+ * @throws Exception if there was an error
+ */
+ public function addParts(int $count, string $comment = null)
+ {
+
+ //TODO
+ if ($count <= 0) {
+ throw new Exception('Zahl der entnommenen Bauteile muss größer 0 sein!');
+ }
+
+ $old_instock = (int) $this->getInstock();
+ $new_instock = $old_instock + $count;
+
+ //TODO
+ /*
+ InstockChangedEntry::add(
+ $this->database,
+ $this->current_user,
+ $this->log,
+ $this,
+ $old_instock,
+ $new_instock,
+ $comment
+ );*/
+
+ $this->instock = $new_instock;
+ }
+
+ /**
+ * Set the count of parts which should be in stock at least
+ *
+ * @param integer $new_mininstock the new count of parts which should be in stock at least
+ *
+ * @throws Exception if the new mininstock is not valid
+ * @throws Exception if there was an error
+ */
+ public function setMinInstock(int $new_mininstock)
+ {
+ $this->mininstock = $new_mininstock;
+ }
+
+ /**
+ * Set the comment
+ *
+ * @param string $new_comment the new comment
+ *
+ * @throws Exception if there was an error
+ */
+ public function setComment(string $new_comment)
+ {
+ $this->comment = $new_comment;
+ }
+
+ /**
+ * Set the "manual_order" attribute
+ *
+ * @param boolean $new_manual_order the new "manual_order" attribute
+ * @param integer $new_order_quantity the new order quantity
+ * @param integer|NULL $new_order_orderdetails_id @li the ID of the new order orderdetails
+ * @li or Zero for "no order orderdetails"
+ * @li or NULL for automatic order orderdetails
+ * (if the part has exactly one orderdetails,
+ * set this orderdetails as order orderdetails.
+ * Otherwise, set "no order orderdetails")
+ *
+ * @throws Exception if there was an error
+ */
+ public function setManualOrder(bool $new_manual_order, int $new_order_quantity = 1, $new_order_orderdetails_id = null)
+ {
+ $this->manual_order = $new_manual_order;
+ //TODO;
+ /* $this->order_orderdetail = $new_order_orderdetails_id; */
+ $this->order_quantity = $new_order_quantity;
+ }
+
+ /**
+ * Set the ID of the order orderdetails
+ *
+ * @param integer|NULL $new_order_orderdetails_id @li the new order orderdetails ID
+ * @li Or, to remove the orderdetails, pass a NULL
+ *
+ * @throws Exception if there was an error
+ */
+ public function setOrderOrderdetailsID($new_order_orderdetails_id) : void
+ {
+ //TODO
+ throw new \Exception("Not implemented yet...");
+ }
+
+ /**
+ * Set the order quantity
+ *
+ * @param integer $new_order_quantity the new order quantity
+ */
+ public function setOrderQuantity(int $new_order_quantity) : void
+ {
+ $this->order_quantity = $new_order_quantity;
+ }
+
+ /**
+ * Set the ID of the category
+ *
+ * @note Every part must have a valid category (in contrast to the
+ * attributes "footprint", "storelocation", ...)!
+ *
+ * @param integer $new_category_id the ID of the category
+ *
+ * @throws Exception if the new category ID is not valid
+ * @throws Exception if there was an error
+ */
+ public function setCategoryID(int $new_category_id)
+ {
+ //TODO
+ throw new \Exception("Not implemented yet!");
+ }
+
+ /**
+ * Set the footprint ID
+ *
+ * @param integer|NULL $new_footprint_id @li the ID of the footprint
+ * @li NULL means "no footprint"
+ *
+ * @throws Exception if the new footprint ID is not valid
+ * @throws Exception if there was an error
+ */
+ public function setFootprintID($new_footprint_id)
+ {
+ //TODO
+ throw new \Exception("Not implemented yet!");
+ }
+
+ /**
+ * Set the storelocation ID
+ *
+ * @param integer|NULL $new_storelocation_id @li the ID of the storelocation
+ * @li NULL means "no storelocation"
+ *
+ * @throws Exception if the new storelocation ID is not valid
+ * @throws Exception if there was an error
+ */
+ public function setStorelocationID($new_storelocation_id)
+ {
+ //TODO
+ throw new \Exception("Not implemented yet!");
+ }
+
+ /**
+ * Set the manufacturer ID
+ *
+ * @param integer|NULL $new_manufacturer_id @li the ID of the manufacturer
+ * @li NULL means "no manufacturer"
+ *
+ * @throws Exception if the new manufacturer ID is not valid
+ * @throws Exception if there was an error
+ */
+ public function setManufacturerID($new_manufacturer_id)
+ {
+ //TODO
+ throw new \Exception("Not implemented yet!");
+ }
+
+ /**
+ * Set the favorite status for this part.
+ * @param $new_favorite_status bool The new favorite status, that should be applied on this part.
+ * Set this to true, when the part should be a favorite.
+ */
+ public function setFavorite(bool $new_favorite_status)
+ {
+ $this->favorite = $new_favorite_status;
+ }
+
+ /**
+ * Sets the URL to the manufacturer site about this Part. Set to "" if this part should use the automatically URL based on its manufacturer.
+ * @param string $new_url The new url
+ * @throws Exception when an error happens.
+ */
+ public function setManufacturerProductURL(string $new_url)
+ {
+ $this->manufacturer_product_url = $new_url;
+ }
+
+ /**
+ * Set the ID of the master picture Attachement
+ *
+ * @param integer|NULL $new_master_picture_attachement_id @li the ID of the Attachement object of the master picture
+ * @li NULL means "no master picture"
+ *
+ * @throws Exception if the new ID is not valid
+ * @throws Exception if there was an error
+ */
+ public function setMasterPictureAttachementID($new_master_picture_attachement_id)
+ {
+ //TODO
+ throw new \Exception("Not implemented yet!");
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/Entity/PartsContainingDBElement.php b/src/Entity/PartsContainingDBElement.php
new file mode 100644
index 00000000..14f74845
--- /dev/null
+++ b/src/Entity/PartsContainingDBElement.php
@@ -0,0 +1,39 @@
+orderdetail;
+ }
+
+ /**
+ * Get the price
+ *
+ * @param boolean $as_money_string @li if true, this method returns a money string incl. currency
+ * @li if false, this method returns the price as float
+ * @param integer $multiplier The returned price (float or string) will be multiplied
+ * with this multiplier.
+ *
+ * @note You will get the price for $multiplier parts. If you want the price which is stored
+ * in the database, you have to pass the "price_related_quantity" count as $multiplier.
+ *
+ * @return float the price as a float number (if "$as_money_string == false")
+ * @return string the price as a string incl. currency (if "$as_money_string == true")
+ *
+ * @see floatToMoneyString()
+ */
+ public function getPrice(bool $as_money_string = false, int $multiplier = 1)
+ {
+ $price = ($this->price * $multiplier) / $this->price_related_quantity;
+
+ if ($as_money_string) {
+ throw new \Exception("Not implemented yet...");
+ //return floatToMoneyString($price);
+ } else {
+ return $price;
+ }
+ }
+
+ /**
+ * Get the price related quantity
+ *
+ * This is the quantity, for which the price is valid.
+ *
+ * @return integer the price related quantity
+ *
+ * @see Pricedetails::setPriceRelatedQuantity()
+ */
+ public function getPriceRelatedQuantity() : int
+ {
+ return (int) $this->price_related_quantity;
+ }
+
+ /**
+ * Get the minimum discount quantity
+ *
+ * "Minimum discount quantity" means the minimum order quantity for which the price
+ * of this orderdetails is valid.
+ *
+ * @return integer the minimum discount quantity
+ *
+ * @see Pricedetails::setMinDiscountQuantity()
+ */
+ public function getMinDiscountQuantity() : int
+ {
+ return (int) $this->min_discount_quantity;
+ }
+
+ /********************************************************************************
+ *
+ * Setters
+ *
+ *********************************************************************************/
+
+ /**
+ * Set the price
+ *
+ * @param float $new_price the new price as a float number
+ *
+ * @warning @li This is the price for "price_related_quantity" parts!!
+ * @li Example: if "price_related_quantity" is '10',
+ * you have to set here the price for 10 parts!
+ */
+ public function setPrice(float $new_price) : void
+ {
+ if($new_price < 0)
+ {
+ throw new \InvalidArgumentException('$new_price must be positive!');
+ }
+ $this->price = $new_price;
+ }
+
+ /**
+ * Set the price related quantity
+ *
+ * This is the quantity, for which the price is valid.
+ *
+ * @par Example:
+ * If 100pcs costs 20$, you have to set the price to 20$ and the price related
+ * quantity to 100. The single price (20$/100 = 0.2$) will be calculated automatically.
+ *
+ * @param integer $new_price_related_quantity the price related quantity
+ */
+ public function setPriceRelatedQuantity(int $new_price_related_quantity) : void
+ {
+ if($new_price_related_quantity <= 0) {
+ throw new \InvalidArgumentException('$new_price_related_quantity must be greater 0!');
+ }
+ $this->price_related_quantity = $new_price_related_quantity;
+ }
+
+ /**
+ * Set the minimum discount quantity
+ *
+ * "Minimum discount quantity" means the minimum order quantity for which the price
+ * of this orderdetails is valid. This way, you're able to use different prices
+ * for different order quantities (quantity discount!).
+ *
+ * @par Example:
+ * - 1-9pcs costs 10$: set price to 10$/pcs and minimum discount quantity to 1
+ * - 10-99pcs costs 9$: set price to 9$/pcs and minimum discount quantity to 10
+ * - 100pcs or more costs 8$: set price/pcs to 8$ and minimum discount quantity to 100
+ *
+ * (Each of this examples would be an own Pricedetails-object.
+ * So the orderdetails would have three Pricedetails for one supplier.)
+ *
+ * @param integer $new_min_discount_quantity the minimum discount quantity
+ */
+ public function setMinDiscountQuantity(int $new_min_discount_quantity)
+ {
+ if($new_min_discount_quantity <= 0 ){
+ throw new \InvalidArgumentException('$new_min_discount_quantity must be positive!');
+ }
+ $this->min_discount_quantity = $new_min_discount_quantity;
+ }
+
+
+
+
+ /**
+ * Returns the ID as an string, defined by the element class.
+ * This should have a form like P000014, for a part with ID 14.
+ * @return string The ID as a string;
+ */
+ public function getIDString(): string
+ {
+ return 'PD' . sprintf('%06d', $this->getID());
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/Storelocation.php b/src/Entity/Storelocation.php
new file mode 100644
index 00000000..1ea67a54
--- /dev/null
+++ b/src/Entity/Storelocation.php
@@ -0,0 +1,89 @@
+is_full;
+ }
+
+ /********************************************************************************
+ *
+ * Setters
+ *
+ *********************************************************************************/
+
+ /**
+ * Change the "is full" attribute of this storelocation
+ *
+ * @note "is_full" = true means that there is no more space in this storelocation.
+ * @note This attribute is only for information, it has no effect.
+ *
+ * @param boolean $new_is_full @li true means that the storelocation is full
+ * @li false means that the storelocation isn't full
+ *
+ * @throws Exception if there was an error
+ */
+ public function setIsFull(bool $new_is_full) : void
+ {
+ $this->is_full = $new_is_full;
+ }
+
+ /**
+ * Returns the ID as an string, defined by the element class.
+ * This should have a form like P000014, for a part with ID 14.
+ * @return string The ID as a string;
+ */
+ public function getIDString(): string
+ {
+ return 'L' . sprintf('%06d', $this->getID());
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/StructuralDBElement.php b/src/Entity/StructuralDBElement.php
new file mode 100644
index 00000000..25c507a1
--- /dev/null
+++ b/src/Entity/StructuralDBElement.php
@@ -0,0 +1,416 @@
+getID() == null) { // this is the root node
+ return false;
+ } else {
+ //If this' parents element, is $another_element, then we are finished
+ return (($this->parent->getID() == $another_element->getID())
+ || $this->parent->isChildOf($another_element)); //Otherwise, check recursivley
+ }
+ }
+
+
+ /******************************************************************************
+ *
+ * Getters
+ *
+ ******************************************************************************/
+
+ /**
+ * @brief Get the parent-ID
+ *
+ * @retval integer @li the ID of the parent element
+ * @li NULL means, the parent is the root node
+ * @li the parent ID of the root node is -1
+ */
+ public function getParentID() : int
+ {
+ return $this->parent_id ?? self::ID_ROOT_ELEMENT; //Null means root element
+ }
+
+ /**
+ * Get the comment of the element.
+ *
+ * @param boolean $parse_bbcode Should BBCode converted to HTML, before returning
+ * @return string the comment
+ */
+ public function getComment(bool $parse_bbcode = true) : string
+ {
+ $val = htmlspecialchars($this->comment ?? '');
+ if ($parse_bbcode) {
+ //$bbcode = new BBCodeParser();
+ //$val = $bbcode->parse($val);
+ }
+
+ return $val;
+ }
+
+ /**
+ * Get the level
+ *
+ * @note The level of the root node is -1.
+ *
+ * @return integer the level of this element (zero means a most top element
+ * [a subelement of the root node])
+ *
+ */
+ public function getLevel() : int
+ {
+ if ($this->level === 0) {
+ $element = $this->parent;
+ $parent_id = $element->getParentID();
+ while ($parent_id > 0) {
+ /** @var StructuralDBElement $element */
+ $element = $element->parent;
+ $parent_id = $element->getParentID();
+ $this->level++;
+ }
+ }
+
+ return $this->level;
+ }
+
+ /**
+ * Get the full path
+ *
+ * @param string $delimeter the delimeter of the returned string
+ *
+ * @return string the full path (incl. the name of this element), delimeted by $delimeter
+ *
+ * @throws Exception if there was an error
+ */
+ public function getFullPath(string $delimeter = self::PATH_DELIMITER_ARROW) : string
+ {
+ if (! \is_array($this->full_path_strings)) {
+ $this->full_path_strings = array();
+ $this->full_path_strings[] = $this->getName();
+ $parent_id = $this->getParentID();
+ while ($parent_id > 0) {
+ /** @var StructuralDBElement $element */
+ $element = static::getInstance($this->database, $this->current_user, $this->log, $parent_id);
+ $parent_id = $element->getParentID();
+ $this->full_path_strings[] = $element->getName();
+ }
+ $this->full_path_strings = array_reverse($this->full_path_strings);
+ }
+
+ return implode($delimeter, $this->full_path_strings);
+ }
+
+ /**
+ * Get all subelements of this element
+ *
+ * @param boolean $recursive if true, the search is recursive
+ *
+ * @return static[] all subelements as an array of objects (sorted by their full path)
+ */
+ public function getSubelements(bool $recursive) : PersistentCollection
+ {
+ if ($this->children == null) {
+ $this->children = new \Doctrine\Common\Collections\ArrayCollection();
+ }
+
+ if (! $recursive) {
+ return $this->children;
+ } else {
+ $all_elements = array();
+ foreach ($this->children as $subelement) {
+ $all_elements[] = $subelement;
+ $all_elements = array_merge($all_elements, $subelement->getSubelements(true));
+ }
+
+ return $all_elements;
+ }
+ }
+
+ /******************************************************************************
+ *
+ * Setters
+ *
+ ******************************************************************************/
+
+ /**
+ * Change the parent ID of this element
+ *
+ * @param integer|null $new_parent_id @li the ID of the new parent element
+ * @li NULL if the parent should be the root node
+ */
+ public function setParentID($new_parent_id)
+ {
+ $this->parent_id = $new_parent_id;
+ }
+
+ /**
+ * Set the comment
+ *
+ * @param string $new_comment the new comment
+ * @throws Exception if there was an error
+ */
+ public function setComment(string $new_comment)
+ {
+ $this->comment = $new_comment;
+ }
+
+ /********************************************************************************
+ *
+ * Tree / Table Builders
+ *
+ *********************************************************************************/
+
+ /**
+ * Build a HTML tree with all subcategories of this element
+ *
+ * This method prints a ';
+ } else {
+ $root_level = $this->getLevel() + 1;
+ }
+
+ // get all subelements
+ $subelements = $this->getSubelements($recursive);
+
+ foreach ($subelements as $element) {
+ $level = $element->getLevel() - $root_level;
+ $selected = ($element->getID() == $selected_id) ? 'selected' : '';
+
+ $html[] = '';
+ }
+
+ return implode("\n", $html);
+ }
+
+
+ public function buildBootstrapTree(
+ $page,
+ $parameter,
+ $recursive = false,
+ $show_root = false,
+ $use_db_root_name = true,
+ $root_name = '$$'
+ ): array
+ {
+ if ($root_name == '$$') {
+ $root_name = _('Oberste Ebene');
+ }
+
+ $subelements = $this->getSubelements(false);
+ $nodes = array();
+
+ foreach ($subelements as $element) {
+ $nodes[] = $element->buildBootstrapTree($page, $parameter);
+ }
+
+ // if we are on root level?
+ if ($this->getParentID() == -1) {
+ if ($show_root) {
+ $tree = array(
+ array('text' => $use_db_root_name ? htmlspecialchars($this->getName()) : $root_name ,
+ 'href' => $page . '?' . $parameter . '=' . $this->getID(),
+ 'nodes' => $nodes)
+ );
+ } else { //Dont show root node
+ $tree = $nodes;
+ }
+ } elseif (!empty($nodes)) {
+ $tree = array('text' => htmlspecialchars($this->getName()),
+ 'href' => $page . '?' . $parameter . '=' . $this->getID(),
+ 'nodes' => $nodes
+ );
+ } else {
+ $tree = array('text' => htmlspecialchars($this->getName()),
+ 'href' => $page . '?' . $parameter . '=' . $this->getID()
+ );
+ }
+
+
+ return $tree;
+ }
+
+ /**
+ * Creates a template loop for a Breadcrumb bar, representing the structural DB element.
+ * @param $page string The base page, to which the breadcrumb links should be directing to.
+ * @param $parameter string The parameter, which selects the ID of the StructuralDBElement.
+ * @param bool $show_root Show the root as its own breadcrumb.
+ * @param string $root_name The label which should be used for the root breadcrumb.
+ * @return array An Loop containing multiple arrays, which contains href and caption for the breadcrumb.
+ */
+ public function buildBreadcrumbLoop(string $page, string $parameter, bool $show_root = false, $root_name = '$$', bool $element_is_link = false) : array
+ {
+ $breadcrumb = array();
+
+ if ($root_name == '$$') {
+ $root_name = _('Oberste Ebene');
+ }
+
+ if ($show_root) {
+ $breadcrumb[] = array('label' => $root_name,
+ 'disabled' => true);
+ }
+
+ if (!$this->current_user->canDo(static::getPermissionName(), StructuralPermission::READ)) {
+ return array('label' => '???',
+ 'disabled' => true);
+ }
+
+ $tmp = array();
+
+ if ($element_is_link) {
+ $tmp[] = array('label' => $this->getName(), 'href' => $page . '?' . $parameter . '=' .$this->getID(), 'selected' => true);
+ } else {
+ $tmp[] = array('label' => $this->getName(), 'selected' => true);
+ }
+
+ $parent_id = $this->getParentID();
+ while ($parent_id > 0) {
+ /** @var StructuralDBElement $element */
+ $element = static::getInstance($this->database, $this->current_user, $this->log, $parent_id);
+ $parent_id = $element->getParentID();
+ $tmp[] = array('label' => $element->getName(), 'href' => $page . '?' . $parameter . '=' . $element->getID());
+ }
+ $tmp = array_reverse($tmp);
+
+ $breadcrumb = array_merge($breadcrumb, $tmp);
+
+ return $breadcrumb;
+ }
+
+}
\ No newline at end of file
diff --git a/src/Entity/Supplier.php b/src/Entity/Supplier.php
new file mode 100644
index 00000000..bb023d00
--- /dev/null
+++ b/src/Entity/Supplier.php
@@ -0,0 +1,81 @@
+ 0) '.
+ 'AND ((devices.order_only_missing_parts = false) '.
+ 'OR (parts.instock - device_parts.quantity * devices.order_quantity < parts.mininstock)))) '.
+ 'AND (parts.order_orderdetails_id IS NOT NULL) '.
+ 'AND (orderdetails.id_supplier = ?)';
+
+ $query_data = $this->database->query($query, array($this->getID()));
+
+
+
+ return (int) $query_data[0]['count']; */
+
+
+ //TODO
+ throw new \Exception("Not implemented yet!");
+ }
+
+
+ /**
+ * Returns the ID as an string, defined by the element class.
+ * This should have a form like P000014, for a part with ID 14.
+ * @return string The ID as a string;
+ */
+ public function getIDString(): string
+ {
+ return 'L' . sprintf('%06d', $this->getID());
+ }
+
+}
\ No newline at end of file