<?php /* part-db version 0.1 Copyright (C) 2005 Christoph Lechner http://www.cl-projects.de/ part-db version 0.2+ Copyright (C) 2009 K. Jacobs and others (see authors.php) http://code.google.com/p/part-db/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA $Id: class.Part.php 675 2013-07-12 11:25:23Z kami89@bluewin.ch $ Changelog (sorted by date): [DATE] [NICKNAME] [CHANGES] 2012-08-?? kami89 - created 2012-09-27 kami89 - added doxygen comments 2012-12-22 kami89 - added "get_manual_order()" + "set_manual_order()" - added "get_order_quantitiy()" + "set_order_quantity()" - added "get_order_supplier()" + "set_order_supplier_id()" 2013-01-29 kami89 - added support for transactions in "delete()" - moved attrubute "obsolete" from Parts to Orderdetails 2013-02-16 kami89 - changes "order_supplier" to "order_orderdetails" */ /** * @file class.Part.php * @brief class Part * * @class Part * @brief All elements of this class are stored in the database table "parts". * * A Part can contain: * - 1 Category * - 0..1 Footprint * - 0..1 Storelocation * - 0..1 Manufacturer * - 0..* Orderdetails * * @author kami89 * * @todo The attribute "visible" is no longer required if there is a user management. */ class Part extends AttachementsContainingDBElement { /******************************************************************************** * * Calculated Attributes * * Calculated attributes will be NULL until they are requested for first time (to save CPU time)! * After changing an element attribute, all calculated data will be NULLed again. * So: the calculated data will be cached. * *********************************************************************************/ /** @brief (Category) the category of this part */ private $category; /** @brief (Footprint|NULL) the footprint of this part (if there is one) */ private $footprint = NULL; /** @brief (Storelocation|NULL) the storelocation where this part is located (if there is one) */ private $storelocation = NULL; /** @brief (Manufacturer|NULL) the manufacturer of this part (if there is one) */ private $manufacturer = NULL; /** @brief (Attachement|NULL) the master picture Attachement of this part (if there is one) */ private $master_picture_attachement = NULL; /** @brief (array) all orderdetails-objects as a one-dimensional array of Orderdetails-objects (empty array if there are no orderdetails) */ private $orderdetails = NULL; /** @brief (Orderdetails|NULL) the order orderdetails of this part (for "parts to order") */ private $order_orderdetails; /** @brief (array) all devices in which this part is used (as a one-dimensional array of Device objects) */ private $devices = NULL; /******************************************************************************** * * Constructor / Destructor / reset_attributes() * *********************************************************************************/ /** * @brief Constructor * * @param Database &$database: reference to the Database-object * @param User &$current_user reference to the current user which is logged in * @param Log &$log: reference to the Log-object * @param integer $id: ID of the part we want to get * * @throws Exception if there is no such part in the database * @throws Exception if there was an error */ public function __construct(&$database, &$current_user, &$log, $id) { parent::__construct($database, $current_user, $log, 'parts', $id); } /** * @copydoc DBElement::reset_attributes() */ public function reset_attributes($all = false) { $this->category = NULL; $this->footprint = NULL; $this->storelocation = NULL; $this->manufacturer = NULL; $this->orderdetails = NULL; $this->devices = NULL; $this->master_picture_attachement = NULL; $this->order_orderdetails = NULL; parent::reset_attributes($all); } /******************************************************************************** * * Basic Methods * *********************************************************************************/ /** * @brief Delete this element * * @note This function overrides the same-named function from the parent class. * @note The associated orderdetails and attachements will be deleted too. * * @param boolean $delete_files_from_hdd if true, the attached files of this part will be deleted * from harddisc drive (!) * @param boolean $delete_device_parts @li if true, all DeviceParts with this part will be deleted * @li if false, there will be thrown an exception * if there are DeviceParts with this part * * @throws Exception if there are device parts and $delete_device_parts == false * @throws Exception if there was an error */ public function delete($delete_files_from_hdd = false, $delete_device_parts = false) { try { $transaction_id = $this->database->begin_transaction(); // start transaction $devices = $this->get_devices(); $orderdetails = $this->get_orderdetails(); $this->reset_attributes(); // set $this->devices ans $this->orderdetails to NULL // Check if there are no Devices with this Part (and delete them if neccessary) if (count($devices) > 0) { if ($delete_device_parts) { foreach ($devices as $device) { foreach ($device->get_parts() as $device_part) { if ($device_part->get_part()->get_id() == $this->get_id()) $device_part->delete(); } } } else throw new Exception('Das Bauteil "'.$this->get_name().'" wird noch in '.count($devices). ' Baugruppen verwendet und kann daher nicht gelöscht werden!'); } // Delete all Orderdetails foreach ($orderdetails as $details) $details->delete(); // now we can delete this element + all attachements of it parent::delete($delete_files_from_hdd); $this->database->commit($transaction_id); // commit transaction } catch (Exception $e) { $this->database->rollback(); // rollback transaction // restore the settings from BEFORE the transaction $this->reset_attributes(); throw new Exception("Das Bauteil \"".$this->get_name()."\" konnte nicht gelöscht werden!\nGrund: ".$e->getMessage()); } } /******************************************************************************** * * Getters * *********************************************************************************/ /** * @brief Get the description * * @retval string the description */ public function get_description() { return $this->db_data['description']; } public function get_modescription() { return $this->db_data['last_modified']; } /** * @brief Get the count of parts which are in stock * * @retval integer count of parts which are in stock */ public function get_instock() { return $this->db_data['instock']; } /** * @brief Get the count of parts which must be in stock at least * * @retval integer count of parts which must be in stock at least */ public function get_mininstock() { return $this->db_data['mininstock']; } /** * @brief Get the comment * * @retval string the comment */ public function get_comment() { return $this->db_data['comment']; } /** * @brief 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. * * @retval boolean @li true if this part is obsolete * @li false if this part isn't obsolete */ public function get_obsolete() { $all_orderdetails = $this->get_orderdetails(); if (count($all_orderdetails) == 0) return false; foreach ($all_orderdetails as $orderdetails) { if ( ! $orderdetails->get_obsolete()) return false; } return true; } /** * @brief Get if this part is visible * * @retval boolean @li true if this part is visible * @li false if this part isn't visible */ public function get_visible() { return $this->db_data['visible']; } /** * @brief Get the selected order orderdetails of this part * * @retval Orderdetails the selected order orderdetails * @retval NULL if there is no order supplier selected */ public function get_order_orderdetails() { if (( ! is_object($this->order_orderdetails)) && ($this->db_data['order_orderdetails_id'] != NULL)) { $this->order_orderdetails = new Orderdetails($this->database, $this->current_user, $this->log, $this->db_data['order_orderdetails_id']); if ($this->order_orderdetails->get_obsolete()) { $this->set_order_orderdetails_id(NULL); $this->order_orderdetails = NULL; } } return $this->order_orderdetails; } /** * @brief Get the order quantity of this part * * @retval integer the order quantity */ public function get_order_quantity() { return $this->db_data['order_quantity']; } /** * @brief 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 * * @retval integer the minimum order quantity */ public function get_min_order_quantity($with_devices = true) { 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::get_order_device_parts($this->database, $this->current_user, $this->log, $this->get_id()); foreach ($deviceparts as $devicepart) { $device = $devicepart->get_device(); if ($device->get_order_only_missing_parts()) $count_should_order += $device->get_order_quantity() * $devicepart->get_mount_quantity(); else $count_must_order += $device->get_order_quantity() * $devicepart->get_mount_quantity(); } return $count_must_order + max(0, $this->get_mininstock() - $this->get_instock() + $count_should_order); } else return max(0, $this->get_mininstock() - $this->get_instock()); } /** * @brief Get the "manual_order" attribute * * @retval boolean the "manual_order" attribute */ public function get_manual_order() { return $this->db_data['manual_order']; } /** * @brief Get the category of this part * * There is always a category, for each part! * * @retval Category the category of this part * * @throws Exception if there was an error */ public function get_category() { if ( ! is_object($this->category)) { $this->category = new Category($this->database, $this->current_user, $this->log, $this->db_data['id_category']); } return $this->category; } /** * @brief Get the footprint of this part (if there is one) * * @retval Footprint the footprint of this part (if there is one) * @retval NULL if this part has no footprint * * @throws Exception if there was an error */ public function get_footprint() { if (( ! is_object($this->footprint)) && ($this->db_data['id_footprint'] != NULL)) { $this->footprint = new Footprint($this->database, $this->current_user, $this->log, $this->db_data['id_footprint']); } return $this->footprint; } /** * @brief Get the storelocation of this part (if there is one) * * @retval Storelocation the storelocation of this part (if there is one) * @retval NULL if this part has no storelocation * * @throws Exception if there was an error */ public function get_storelocation() { if (( ! is_object($this->storelocation)) && ($this->db_data['id_storelocation'] != NULL)) { $this->storelocation = new Storelocation($this->database, $this->current_user, $this->log, $this->db_data['id_storelocation']); } return $this->storelocation; } /** * @brief Get the manufacturer of this part (if there is one) * * @retval Manufacturer the manufacturer of this part (if there is one) * @retval NULL if this part has no manufacturer * * @throws Exception if there was an error */ public function get_manufacturer() { if (( ! is_object($this->manufacturer)) && ($this->db_data['id_manufacturer'] != NULL)) { $this->manufacturer = new Manufacturer($this->database, $this->current_user, $this->log, $this->db_data['id_manufacturer']); } return $this->manufacturer; } /** * @brief Get the master picture "Attachement"-object of this part (if there is one) * * @retval Attachement the master picture Attachement of this part (if there is one) * @retval NULL if this part has no master picture * * @throws Exception if there was an error */ public function get_master_picture_attachement() { if (( ! is_object($this->master_picture_attachement)) && ($this->db_data['id_master_picture_attachement'] != NULL)) { $this->master_picture_attachement = new Attachement($this->database, $this->current_user, $this->log, $this->db_data['id_master_picture_attachement']); } return $this->master_picture_attachement; } /** * @brief Get all orderdetails of this part * * @param boolean $hide_obsolete If true, obsolete orderdetails will NOT be returned * * @retval array @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 get_orderdetails($hide_obsolete = false) { if ( ! is_array($this->orderdetails)) { $this->orderdetails = array(); $query = 'SELECT orderdetails.id FROM orderdetails '. 'LEFT JOIN suppliers ON suppliers.id = orderdetails.id_supplier '. 'WHERE part_id=? '. 'ORDER BY suppliers.name ASC'; $query_data = $this->database->query($query, array($this->get_id())); foreach ($query_data as $row) $this->orderdetails[] = new Orderdetails($this->database, $this->current_user, $this->log, $row['id']); } if ($hide_obsolete) { $orderdetails = $this->orderdetails; foreach ($orderdetails as $key => $details) { if ($details->get_obsolete()) unset($orderdetails[$key]); } return $orderdetails; } else return $this->orderdetails; } /** * @brief Get all devices which uses this part * * @retval array @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 get_devices() { if ( ! is_array($this->devices)) { $this->devices = array(); $query = 'SELECT id_device FROM device_parts '. 'LEFT JOIN devices ON device_parts.id_device=devices.id '. 'WHERE id_part=? '. 'GROUP BY id_device '. 'ORDER BY devices.name ASC'; $query_data = $this->database->query($query, array($this->get_id())); foreach ($query_data as $row) $this->devices[] = new Device($this->database, $this->current_user, $this->log, $row['id_device']); } return $this->devices; } /** * @brief 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 * * @retval array all suppliers as a one-dimensional array of Supplier objects * (if "$object_array == true") * @retval array all supplier-names as a one-dimensional array of strings * ("if $object_array == false" and "$delimeter == NULL") * @retval 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 get_suppliers($object_array = true, $delimeter = NULL, $full_paths = false, $hide_obsolete = false) { $suppliers = array(); $orderdetails = $this->get_orderdetails($hide_obsolete); foreach ($orderdetails as $details) $suppliers[] = $details->get_supplier(); if ($object_array) { return $suppliers; } else { $supplier_names = array(); foreach ($suppliers as $supplier) { if ($full_paths) $supplier_names[] = $supplier->get_full_path(); else $supplier_names[] = $supplier->get_name(); } if (is_string($delimeter)) return implode($delimeter, $supplier_names); else return $supplier_names; } } /** * @brief 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 * * @retval array all supplierpartnrs as an array of strings (if "$delimeter == NULL") * @retval string all supplierpartnrs as a string, delimeted ba $delimeter (if $delimeter is a string) * * @throws Exception if there was an error */ public function get_supplierpartnrs($delimeter = NULL, $hide_obsolete = false) { $supplierpartnrs = array(); foreach ($this->get_orderdetails($hide_obsolete) as $details) $supplierpartnrs[] = $details->get_supplierpartnr(); if (is_string($delimeter)) return implode($delimeter, $supplierpartnrs); else return $supplierpartnrs; } /** * @brief 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 * * @retval array all prices as an array of floats (if "$delimeter == NULL" & "$float_array == true") * @retval array all prices as an array of strings (if "$delimeter == NULL" & "$float_array == false") * @retval 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 get_prices($float_array = false, $delimeter = NULL, $quantity = 1, $multiplier = NULL, $hide_obsolete = false) { $prices = array(); foreach ($this->get_orderdetails($hide_obsolete) as $details) $prices[] = $details->get_price(( ! $float_array), $quantity, $multiplier); if (is_string($delimeter)) return implode($delimeter, $prices); else return $prices; } /** * @brief 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 * * @retval float price (if "$as_money_string == false") * @retval NULL if there are no prices for this part and "$as_money_string == false" * @retval string price with currency (if "$as_money_string == true") * * @throws Exception if there was an error */ public function get_average_price($as_money_string = false, $quantity = 1, $multiplier = NULL) { $prices = $this->get_prices(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 float_to_money_string($average_price); else return $average_price; } /** * @brief 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 * * @retval string the whole path + filename from filesystem root as a UNIX path (with slashes) * @retval NULL if there is no picture * * @throws Exception if there was an error */ public function get_master_picture_filename($use_footprint_filename = false) { $master_picture = $this->get_master_picture_attachement(); // returns an Attachement-object if (is_object($master_picture)) return $master_picture->get_filename(); if ($use_footprint_filename) { $footprint = $this->get_footprint(); if (is_object($footprint)) return $footprint->get_filename(); } return NULL; } /******************************************************************************** * * Setters * *********************************************************************************/ /** * @brief Set the description * * @param string $new_description the new description * * @throws Exception if there was an error */ public function set_description($new_description) { $this->set_attributes(array('description' => $new_description)); } /** * @brief 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 set_instock($new_instock) { $this->set_attributes(array('instock' => $new_instock)); } /** * @brief Set the count of parts which should be in stock at least * * @param integer $new_instock 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 set_mininstock($new_mininstock) { $this->set_attributes(array('mininstock' => $new_mininstock)); } /** * @brief Set the comment * * @param string $new_comment the new comment * * @throws Exception if there was an error */ public function set_comment($new_comment) { $this->set_attributes(array('comment' => $new_comment)); } /** * @brief 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 set_manual_order($new_manual_order, $new_order_quantity = 1, $new_order_orderdetails_id = NULL) { $this->set_attributes(array('manual_order' => $new_manual_order, 'order_orderdetails_id' => $new_order_orderdetails_id, 'order_quantity' => $new_order_quantity)); } /** * @brief 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 set_order_orderdetails_id($new_order_orderdetails_id) { $this->set_attributes(array('order_orderdetails_id' => $new_order_orderdetails_id)); } /** * @brief Set the order quantity * * @param integer $new_order_quantity the new order quantity * * @throws Exception if the order quantity is not valid * @throws Exception if there was an error */ public function set_order_quantity($new_order_quantity) { $this->set_attributes(array('order_quantity' => $new_order_quantity)); } /** * @brief 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 set_category_id($new_category_id) { $this->set_attributes(array('id_category' => $new_category_id)); } /** * @brief 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 set_footprint_id($new_footprint_id) { $this->set_attributes(array('id_footprint' => $new_footprint_id)); } /** * @brief 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 set_storelocation_id($new_storelocation_id) { $this->set_attributes(array('id_storelocation' => $new_storelocation_id)); } /** * @brief 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 set_manufacturer_id($new_manufacturer_id) { $this->set_attributes(array('id_manufacturer' => $new_manufacturer_id)); } /** * @brief 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 set_master_picture_attachement_id($new_master_picture_attachement_id) { $this->set_attributes(array('id_master_picture_attachement' => $new_master_picture_attachement_id)); } /******************************************************************************** * * Table Builder Methods * *********************************************************************************/ /** * @brief Build the array for the template table row of this part * * @param string $table_type @li the type of the table which will be builded * @li see Part::build_template_table_array() * @param boolen $row_index The index of this table row * @param array $additional_values Here you can pass more values than only the part attributes. * This is used in DevicePart::build_template_table_row_array(). * * @retval array The array for the template output (element of the loop "table") * * @throws Exception if there was an error */ public function build_template_table_row_array($table_type, $row_index, $additional_values = array()) { global $config; $table_row = array(); $table_row['row_odd'] = is_odd($row_index); $table_row['row_index'] = $row_index; $table_row['id'] = $this->get_id(); $table_row['row_fields'] = array(); foreach(explode(';', $config['table'][$table_type]['columns']) as $caption) { $row_field = array(); $row_field['row_index'] = $row_index; $row_field['caption'] = $caption; $row_field['id'] = $this->get_id(); $row_field['name'] = $this->get_name(); switch($caption) { case 'hover_picture': $picture_filename = str_replace(BASE, BASE_RELATIVE, $this->get_master_picture_filename(true)); $row_field['picture_name'] = strlen($picture_filename) ? basename($picture_filename) : ''; $row_field['small_picture'] = strlen($picture_filename) ? $picture_filename : ''; $row_field['hover_picture'] = strlen($picture_filename) ? $picture_filename : ''; break; case 'name': case 'description': case 'comment': case 'name_description': $row_field['obsolete'] = $this->get_obsolete(); $row_field['comment'] = $this->get_comment(); $row_field['description'] = $this->get_description(); break; case 'modescription': $row_field['modescription'] = $this->get_modescription(); break; case 'instock': case 'mininstock': case 'instock_mininstock': case 'instock_edit_buttons': $row_field['instock'] = $this->get_instock(); $row_field['mininstock'] = $this->get_mininstock(); $row_field['not_enought_instock'] = ($this->get_instock() < $this->get_mininstock()); break; case 'category': $category = $this->get_category(); $row_field['category_name'] = $category->get_name(); $row_field['category_path'] = $category->get_full_path(); break; case 'footprint': $footprint = $this->get_footprint(); if (is_object($footprint)) { $row_field['footprint_name'] = $footprint->get_name(); $row_field['footprint_path'] = $footprint->get_full_path(); } break; case 'manufacturer': $manufacturer = $this->get_manufacturer(); if (is_object($manufacturer)) { $row_field['manufacturer_name'] = $manufacturer->get_name(); $row_field['manufacturer_path'] = $manufacturer->get_full_path(); } break; case 'storelocation': $storelocation = $this->get_storelocation(); if (is_object($storelocation)) { $row_field['storelocation_name'] = $storelocation->get_name(); $row_field['storelocation_path'] = $storelocation->get_full_path(); } break; case 'suppliers': $suppliers_loop = array(); foreach ($this->get_suppliers(false, NULL, false, true) as $supplier_name) // suppliers from obsolete orderdetails will not be shown { $suppliers_loop[] = array( 'row_index' => $row_index, 'supplier_name' => $supplier_name); } $row_field['suppliers'] = $suppliers_loop; break; case 'suppliers_radiobuttons': if ($table_type == 'order_parts') { if (is_object($this->get_order_orderdetails())) $order_orderdetails_id = $this->get_order_orderdetails()->get_id(); else $order_orderdetails_id = 0; $suppliers_loop = array(); foreach ($this->get_orderdetails(true) as $orderdetails) // obsolete orderdetails will not be shown { $suppliers_loop[] = array( 'row_index' => $row_index, 'orderdetails_id' => $orderdetails->get_id(), 'supplier_name' => $orderdetails->get_supplier()->get_full_path(), 'selected' => ($order_orderdetails_id == $orderdetails->get_id())); } $suppliers_loop[] = array( 'row_index' => $row_index, 'orderdetails_id' => 0, 'supplier_name' => 'Noch nicht bestellen', 'selected' => ($order_orderdetails_id == 0)); $row_field['suppliers_radiobuttons'] = $suppliers_loop; } break; case 'supplier_partnrs': $partnrs_loop = array(); foreach ($this->get_supplierpartnrs(NULL, true) as $partnr) // partnrs from obsolete orderdetails will not be shown { $partnrs_loop[] = array( 'row_index' => $row_index, 'supplier_partnr' => $partnr); } $row_field['supplier_partnrs'] = $partnrs_loop; break; case 'datasheets': $datasheet_loop = $config['auto_datasheets']['entries']; foreach ($datasheet_loop as $key => $entry) $datasheet_loop[$key]['url'] = str_replace('%%PARTNAME%%', urlencode($this->get_name()), $entry['url']); $row_field['datasheets'] = $datasheet_loop; break; case 'average_single_price': $row_field['average_single_price'] = $this->get_average_price(true, 1); break; case 'single_prices': if ($table_type == 'order_parts') $min_discount_quantity = $this->get_order_quantity(); else $min_discount_quantity = 1; $prices_loop = array(); foreach ($this->get_prices(false, NULL, $min_discount_quantity, 1, true) as $price) // prices from obsolete orderdetails will not be shown { $prices_loop[] = array( 'row_index' => $row_index, 'single_price' => $price); } $row_field['single_prices'] = $prices_loop; break; case 'total_prices': switch ($table_type) { case 'order_parts': $min_discount_quantity = $this->get_order_quantity(); break; default: //throw new Exception('Keine Totalpreise verfügbar für den Tabellentyp "'.$table_type.'"!'); $min_discount_quantity = 0; } $prices_loop = array(); foreach ($this->get_prices(false, NULL, $min_discount_quantity, NULL, true) as $price) // prices from obsolete orderdetails will not be shown { $prices_loop[] = array( 'row_index' => $row_index, 'total_price' => $price); } $row_field['total_prices'] = $prices_loop; break; case 'order_quantity_edit': if ($table_type == 'order_parts') { $row_field['order_quantity'] = $this->get_order_quantity(); $row_field['min_order_quantity'] = $this->get_min_order_quantity(); } break; case 'order_options': if ($table_type == 'order_parts') { $suppliers_loop = array(); $row_field['enable_remove'] = (($this->get_instock() >= $this->get_mininstock()) && ($this->get_manual_order())); } break; case 'button_decrement': $row_field['decrement_disabled'] = ($this->get_instock() < 1); break; case 'attachements': $attachements = array(); foreach ($this->get_attachements(NULL, true) as $attachement) { $attachements[] = array( 'name' => $attachement->get_name(), 'filename' => str_replace(BASE, BASE_RELATIVE, $attachement->get_filename()), 'type' => $attachement->get_type()->get_full_path()); } $row_field['attachements'] = $attachements; break; case 'id': case 'button_increment': case 'quantity_edit': // for DevicePart Objects case 'mountnames_edit': // for DevicePart Objects // nothing to do, only to avoid the Exception in the default-case break; default: throw new Exception('Unbekannte Tabellenspalte: "'.$caption.'". Überprüfen Sie die Einstellungen '. 'für den Tabellentyp "'.$table_type.'" in Ihrer "config.php"'); } // maybe there are any additional values to add... if (array_key_exists($caption, $additional_values)) { foreach($additional_values[$caption] as $key => $value) $row_field[$key] = $additional_values[$caption][$key]; } $table_row['row_fields'][] = $row_field; } return $table_row; } /** * @brief Build the template table array of an array of parts * * @param array $parts array of all parts (Part or DevicePart objects) which will be printed * @param string $table_type the type of the table which will be builded * * @par Possible Table Types: * - "category_parts" * - "device_parts" * - "order_parts" * - "noprice_parts" * - "obsolete_parts" * * * @retval array the template loop array for the table * * @throws Exception if there was an error */ public static function build_template_table_array($parts, $table_type) { global $config; if ( ! isset($config['table'][$table_type])) { debug('error', '$table_type = "'.$table_type.'"', __FILE__, __LINE__, __METHOD__); throw new Exception('"$table_type" ist ungültig!'); } // table columns $columns = array(); foreach(explode(';', $config['table'][$table_type]['columns']) as $caption) $columns[] = array('caption' => $caption); $table_loop = array(); $table_loop[] = array('print_header' => true, 'columns' => $columns); // print the table header $row_index = 0; foreach ($parts as $part) { $table_loop[] = $part->build_template_table_row_array($table_type, $row_index); $row_index++; } return $table_loop; } /******************************************************************************** * * Static Methods * *********************************************************************************/ /** * @copydoc DBElement::check_values_validity() */ public static function check_values_validity(&$database, &$current_user, &$log, &$values, $is_new, &$element = NULL) { // first, we let all parent classes to check the values parent::check_values_validity($database, $current_user, $log, $values, $is_new, $element); // set "last_modified" to current datetime $values['last_modified'] = date('Y-m-d H:i:s'); // set the datetype of the boolean attributes settype($values['visible'], 'boolean'); settype($values['manual_order'], 'boolean'); // check "instock" if (( ! is_int($values['instock'])) && ( ! ctype_digit($values['instock']))) { debug('warning','"instock" ist keine gültige Zahl: "'.$values['instock'].'"!', __FILE__, __LINE__, __METHOD__); throw new Exception('Der neue Lagerbestand ist ungültig!'); } elseif ($values['instock'] < 0) throw new Exception('Der neue Lagerbestand von "'.$values['name'].'" wäre negativ und kann deshalb nicht gespeichert werden!'); // check "order_orderdetails_id" try { if ($values['order_orderdetails_id'] == 0) $values['order_orderdetails_id'] = NULL; if (( ! $is_new) && ($values['order_orderdetails_id'] == NULL) && (($values['instock'] < $values['mininstock']) || ($values['manual_order'])) && (($element->get_instock() >= $element->get_mininstock()) && ( ! $element->get_manual_order()))) { // if this part will be added now to the list of parts to order (instock is now less than mininstock, or manual_order is now true), // and this part has only one orderdetails, we will set that orderdetails as orderdetails to order from (attribute "order_orderdetails_id"). // Note: If that part was already in the list of parts to order, wo mustn't change the orderdetails to order!! $orderdetails = $element->get_orderdetails(); $order_orderdetails_id = ((count($orderdetails) == 1) ? $orderdetails[0]->get_id() : NULL); $values['order_orderdetails_id'] = $order_orderdetails_id; } if ($values['order_orderdetails_id'] != NULL) $order_orderdetails = new Orderdetails($database, $current_user, $log, $values['order_orderdetails_id']); } catch (Exception $e) { debug('error', 'Ungültige "order_orderdetails_id": "'.$values['order_orderdetails_id'].'"'. "\n\nUrsprüngliche Fehlermeldung: ".$e->getMessage(), __FILE__, __LINE__, __METHOD__); throw new Exception('Die gewählte Einkaufsinformation existiert nicht!'); } // check "order_quantity" if ((( ! is_int($values['order_quantity'])) && ( ! ctype_digit($values['order_quantity']))) || ($values['order_quantity'] < 1)) { debug('error', 'order_quantity = "'.$values['order_quantity'].'"', __FILE__, __LINE__, __METHOD__); throw new Exception('Die Bestellmenge ist ungültig!'); } // check if we have to reset the order attributes ("instock" is now less than "mininstock") if (($values['instock'] < $values['mininstock']) && (($is_new) || ($element->get_instock() >= $element->get_mininstock()))) { if ( ! $values['manual_order']) $values['order_quantity'] = $values['mininstock'] - $values['instock']; $values['manual_order'] = false; } // check "mininstock" if ((( ! is_int($values['mininstock'])) && ( ! ctype_digit($values['mininstock']))) || ($values['mininstock'] < 0)) { debug('warning', '"mininstock" ist keine gültige Zahl: "'.$values['mininstock'].'"!', __FILE__, __LINE__, __METHOD__); throw new Exception('Der neue Mindestlagerbestand ist ungültig!'); } // check "id_category" try { // id_category == NULL means "no category", and this is not allowed! if ($values['id_category'] == NULL) throw new Exception('"id_category" ist Null!'); $category = new Category($database, $current_user, $log, $values['id_category']); } catch (Exception $e) { debug('warning', 'Ungültige "id_category": "'.$values['id_category'].'"'. "\n\nUrsprüngliche Fehlermeldung: ".$e->getMessage(), __FILE__, __LINE__, __METHOD__); throw new Exception('Die gewählte Kategorie existiert nicht!'); } // check "id_footprint" try { if (($values['id_footprint'] == 0) && ($values['id_footprint'] !== NULL)) $values['id_footprint'] = NULL; $footprint = new Footprint($database, $current_user, $log, $values['id_footprint']); } catch (Exception $e) { debug('warning', 'Ungültige "id_footprint": "'.$values['id_footprint'].'"'. "\n\nUrsprüngliche Fehlermeldung: ".$e->getMessage(), __FILE__, __LINE__, __METHOD__); throw new Exception('Der gewählte Footprint existiert nicht!'); } // check "id_storelocation" try { if (($values['id_storelocation'] == 0) && ($values['id_storelocation'] !== NULL)) $values['id_storelocation'] = NULL; $storelocation = new Storelocation($database, $current_user, $log, $values['id_storelocation']); } catch (Exception $e) { debug('warning', 'Ungültige "id_storelocation": "'.$values['id_storelocation'].'"'. "\n\nUrsprüngliche Fehlermeldung: ".$e->getMessage(), __FILE__, __LINE__, __METHOD__); throw new Exception('Der gewählte Lagerort existiert nicht!'); } // check "id_manufacturer" try { if (($values['id_manufacturer'] == 0) && ($values['id_manufacturer'] !== NULL)) $values['id_manufacturer'] = NULL; $manufacturer = new Manufacturer($database, $current_user, $log, $values['id_manufacturer']); } catch (Exception $e) { debug('warning', 'Ungültige "id_manufacturer": "'.$values['id_manufacturer'].'"'. "\n\nUrsprüngliche Fehlermeldung: ".$e->getMessage(), __FILE__, __LINE__, __METHOD__); throw new Exception('Der gewählte Hersteller existiert nicht!'); } // check "id_master_picture_attachement" try { if ($values['id_master_picture_attachement']) $master_picture_attachement = new Attachement($database, $current_user, $log, $values['id_master_picture_attachement']); else $values['id_master_picture_attachement'] = NULL; // this will replace the integer "0" with NULL } catch (Exception $e) { debug('warning', 'Ungültige "id_master_picture_attachement": "'.$values['id_master_picture_attachement'].'"'. "\n\nUrsprüngliche Fehlermeldung: ".$e->getMessage(), __FILE__, __LINE__, __METHOD__); throw new Exception('Die gewählte Datei existiert nicht!'); } } /** * @brief Get count of parts * * @param Database &$database reference to the Database-object * * @retval integer count of parts * * @throws Exception if there was an error */ public static function get_count(&$database) { if (get_class($database) != 'Database') throw new Exception('$database ist kein Database-Objekt!'); return $database->get_count_of_records('parts'); } /** * @brief Get the sum of all "instock" attributes of all parts * * All values in the table row "instock" will be summed up. * * This method is used in statistics.php. * * @param Database &$database reference to the database object * * @retval integer the sum of all "instock" attributes of all parts * * @throws Exception if there was an error */ public static function get_sum_count_instock(&$database) { if (get_class($database) != 'Database') throw new Exception('$database ist kein Database-Objekt!'); $query_data = $database->query('SELECT sum(instock) as sum FROM parts'); return intval($query_data[0]['sum']); } /** * @brief Get the sum price of all parts in stock * * This method is used in statistics.php. * * @param Database &$database reference to the database object * @param User &$current_user reference to the user which is logged in * @param Log &$log reference to the Log-object * @param boolean $as_money_string @li if true, the price will be returned as a money string * (with currency) * @li if false, the price will be returned as a float * * @retval string sum price as a money string with currency (if "$as_money_string == true") * @retval float sum price as a float (if "$as_money_string == false") * * @throws Exception if there was an error */ public static function get_sum_price_instock(&$database, &$current_user, &$log, $as_money_string = true) { if (get_class($database) != 'Database') throw new Exception('$database ist kein Database-Objekt!'); $query = 'SELECT SUM(part_price) AS price_sum '. 'FROM (SELECT parts.id, AVG(pricedetails.price * parts.instock / pricedetails.price_related_quantity) AS part_price '. 'FROM pricedetails '. 'LEFT JOIN orderdetails ON pricedetails.orderdetails_id=orderdetails.id '. 'LEFT JOIN parts ON orderdetails.part_id=parts.id '. 'WHERE pricedetails.min_discount_quantity=1 '. 'GROUP BY parts.id) part_price'; $query_data = $database->query($query); $price_sum = $query_data[0]['price_sum']; if ($as_money_string) return float_to_money_string($price_sum); else return $price_sum; } /** * @brief Get all parts which should be ordered * * "parts which should be ordered" means: * ((("instock" is less than "mininstock") AND (Part isn't already ordered)) * OR (Part was manually marked as "should be ordered")) * * @param Database &$database reference to the database object * @param User &$current_user reference to the user which is logged in * @param Log &$log reference to the Log-object * @param array $supplier_ids @li array of all supplier IDs which will be listed * @li an empty array means, the parts from ALL suppliers will be listed * @param boolean $with_devices if true, parts which are in devices, marked as "to order", will be listed too * * @retval array all parts as a one-dimensional array of Part objects, sorted by their names * * @throws Exception if there was an error */ public static function get_order_parts(&$database, &$current_user, &$log, $supplier_ids = array(), $with_devices = true) { if (get_class($database) != 'Database') throw new Exception('$database ist kein Database-Objekt!'); $parts = array(); $query = 'SELECT parts.id FROM parts '. 'LEFT JOIN orderdetails ON orderdetails.id = parts.order_orderdetails_id '. 'WHERE (parts.instock < parts.mininstock '. 'OR parts.manual_order = true '. 'OR parts.id IN '. '(SELECT device_parts.id_part FROM device_parts '. 'LEFT JOIN devices ON devices.id = device_parts.id_device '. 'WHERE devices.order_quantity > 0)) '; if (count($supplier_ids) > 0) { $query .= 'AND ((false) OR '; foreach ($supplier_ids as $id) $query .= '(orderdetails.id_supplier <=> ?) '; $query .= ') '; } $query .= 'ORDER BY parts.name ASC'; $query_data = $database->query($query, $supplier_ids); foreach ($query_data as $row) { $part = new Part($database, $current_user, $log, $row['id']); if (($part->get_manual_order()) || ($part->get_min_order_quantity() > 0)) $parts[] = $part; } return $parts; } /** * @brief Get all parts which have no price * * @param Database &$database reference to the database object * @param User &$current_user reference to the user which is logged in * @param Log &$log reference to the Log-object * * @retval array all parts as a one-dimensional array of Part objects, sorted by their names * * @throws Exception if there was an error */ public static function get_noprice_parts(&$database, &$current_user, &$log) { if (get_class($database) != 'Database') throw new Exception('$database ist kein Database-Objekt!'); $parts = array(); $query = 'SELECT id from parts '. 'WHERE id NOT IN (SELECT DISTINCT part_id FROM orderdetails '. 'LEFT JOIN pricedetails ON orderdetails.id=pricedetails.orderdetails_id '. 'WHERE pricedetails.id IS NOT NULL) '. 'ORDER BY parts.name ASC'; $query_data = $database->query($query); foreach ($query_data as $row) $parts[] = new Part($database, $current_user, $log, $row['id']); return $parts; } /** * @brief Get all obsolete parts * * @param Database &$database reference to the database object * @param User &$current_user reference to the user which is logged in * @param Log &$log reference to the Log-object * @param boolean $no_orderdetails_parts if true, parts without any orderdetails will be returned too * * @retval array all parts as a one-dimensional array of Part objects, sorted by their names * * @throws Exception if there was an error */ public static function get_obsolete_parts(&$database, &$current_user, &$log, $no_orderdetails_parts = false) { if (get_class($database) != 'Database') throw new Exception('$database ist kein Database-Objekt!'); $parts = array(); if ($no_orderdetails_parts) { // show also parts which have no orderdetails $query = 'SELECT parts.id from parts '. 'LEFT JOIN orderdetails ON orderdetails.part_id = parts.id '. 'WHERE parts.id IN (SELECT part_id FROM `orderdetails` '. 'WHERE part_id IN (SELECT part_id FROM `orderdetails` '. 'WHERE obsolete = true GROUP BY part_id) '. 'AND part_id NOT IN (SELECT part_id FROM `orderdetails` '. 'WHERE obsolete = false GROUP BY part_id)) '. 'OR orderdetails.id IS NULL '. 'ORDER BY parts.name ASC'; } else { // don't show parts which have no orderdetails $query = 'SELECT parts.id from parts '. 'WHERE parts.id IN (SELECT part_id FROM `orderdetails` '. 'WHERE part_id IN (SELECT part_id FROM `orderdetails` '. 'WHERE obsolete = true GROUP BY part_id) '. 'AND part_id NOT IN (SELECT part_id FROM `orderdetails` '. 'WHERE obsolete = false GROUP BY part_id)) '. 'ORDER BY parts.name ASC'; } $query_data = $database->query($query); foreach ($query_data as $row) $parts[] = new Part($database, $current_user, $log, $row['id']); return $parts; } /** * @brief Search parts * * @param Database &$database reference to the database object * @param User &$current_user reference to the user which is logged in * @param Log &$log reference to the Log-object * @param string $keyword the search string * @param string $group_by @li if this is a non-empty string, the returned array is a * two-dimensional array with the group names as top level. * @li supported groups are: '' (none), 'categories', * 'footprints', 'storelocations', 'manufacturers' * @param boolean $part_name if ture, the search will include this attribute * @param boolean $part_description if ture, the search will include this attribute * @param boolean $part_comment if ture, the search will include this attribute * @param boolean $footprint_name if ture, the search will include this attribute * @param boolean $category_name if ture, the search will include this attribute * @param boolean $storelocation_name if ture, the search will include this attribute * @param boolean $supplier_name if ture, the search will include this attribute * @param boolean $supplierpartnr if ture, the search will include this attribute * @param boolean $manufacturer_name if ture, the search will include this attribute * * @retval array all found parts as a one-dimensional array of Part objects, * sorted by their names (if "$group_by == ''") * @retval array @li all parts as a two-dimensional array, grouped by $group_by, * sorted by name (if "$group_by != ''") * @li example: array('category1' => array(part1, part2, ...), * 'category2' => array(part123, part124, ...), ...) * @li for the group names (in the example 'category1', 'category2', ...) * are the full paths used * * @throws Exception if there was an error */ public static function search_parts(&$database, &$current_user, &$log, $keyword, $group_by = '', $part_name = true, $part_description = true, $part_comment = false, $footprint_name = false, $category_name = false, $storelocation_name = false, $supplier_name = false, $supplierpartnr = false, $manufacturer_name = false) { $keyword = trim($keyword); if (strlen($keyword) == 0) return array(); $keyword = str_replace('*', '%', $keyword); $keyword = '%'.$keyword.'%'; $groups = array(); $parts = array(); $values = array(); $query = 'SELECT parts.id FROM parts'. ' LEFT JOIN footprints ON parts.id_footprint=footprints.id'. ' LEFT JOIN storelocations ON parts.id_storelocation=storelocations.id'. ' LEFT JOIN manufacturers ON parts.id_manufacturer=manufacturers.id'. ' LEFT JOIN categories ON parts.id_category=categories.id'. ' LEFT JOIN orderdetails ON parts.id=orderdetails.part_id'. ' LEFT JOIN suppliers ON orderdetails.id_supplier=suppliers.id'. ' WHERE FALSE'; if ($part_name) { $query .= ' OR (parts.name LIKE ?)'; $values[] = $keyword; } if ($part_description) { $query .= ' OR (parts.description LIKE ?)'; $values[] = $keyword; } if ($part_comment) { $query .= ' OR (parts.comment LIKE ?)'; $values[] = $keyword; } if ($footprint_name) { $query .= ' OR (footprints.name LIKE ?)'; $values[] = $keyword; } if ($category_name) { $query .= ' OR (categories.name LIKE ?)'; $values[] = $keyword; } if ($storelocation_name) { $query .= ' OR (storelocations.name LIKE ?)'; $values[] = $keyword; } if ($supplier_name) { $query .= ' OR (suppliers.name LIKE ?)'; $values[] = $keyword; } if ($supplierpartnr) { $query .= ' OR (orderdetails.supplierpartnr LIKE ?)'; $values[] = $keyword; } if ($manufacturer_name) { $query .= ' OR (manufacturers.name LIKE ?)'; $values[] = $keyword; } switch($group_by) { case '': $query .= ' GROUP BY parts.id ORDER BY parts.name ASC LIMIT 200'; break; case 'categories': $query .= ' GROUP BY parts.id ORDER BY categories.id, parts.name ASC LIMIT 200'; break; default: throw new Exception('$group_by="'.$group_by.'" is not supported!'); } $query_data = $database->query($query, $values); foreach ($query_data as $row) { $part = new Part($database, $current_user, $log, $row['id']); switch($group_by) { case '': $parts[] = $part; break; case 'categories': $groups[$part->get_category()->get_full_path()][] = $part; break; } } if ($group_by != '') { ksort($groups); return $groups; } else return $parts; } /** * @brief Create a new part * * @param Database &$database reference to the database object * @param User &$current_user reference to the user which is logged in * @param Log &$log reference to the Log-object * @param string $name the name of the new part (see Part::set_name()) * @param integer $category_id the category ID of the new part (see Part::set_category_id()) * @param string $description the description of the new part (see Part::set_description()) * @param integer $instock the instock of the new part (see Part::set_instock()) * @param integer $mininstock the mininstock of the new part (see Part::set_mininstock()) * @param integer $storelocation_id the storelocation ID of the new part (see Part::set_storelocation_id()) * @param integer $manufacturer_id the manufacturer ID of the new part (see Part::set_manufacturer_id()) * @param integer $footprint_id the footprint ID of the new part (see Part::set_footprint_id()) * @param string $comment the comment of the new part (see Part::set_comment()) * @param boolean $visible the visible attribute of the new part (see Part::set_visible()) * * @retval Part the new part * * @throws Exception if (this combination of) values is not valid * @throws Exception if there was an error * * @see DBElement::add() */ public static function add(&$database, &$current_user, &$log, $name, $category_id, $description = '', $instock = 0, $mininstock = 0, $storelocation_id = NULL, $manufacturer_id = NULL, $footprint_id = NULL, $comment = '', $visible = false) { return parent::add($database, $current_user, $log, 'parts', array( 'name' => $name, 'id_category' => $category_id, 'description' => $description, 'instock' => $instock, 'mininstock' => $mininstock, 'id_storelocation' => $storelocation_id, 'id_manufacturer' => $manufacturer_id, 'id_footprint' => $footprint_id, 'visible' => $visible, 'comment' => $comment, 'id_master_picture_attachement' => NULL, 'manual_order' => false, 'order_orderdetails_id' => NULL, 'order_quantity' => 1)); // the column "datetime_added" will be automatically filled by MySQL // the column "last_modified" will be filled in the function check_values_validity() } } ?>