. */ /** * An interface to the offer table in the database */ class Offer { /** * @var pdo $pdo The PDO class for database communication * @var int $id The id of the offer * @var int $contactId The id of the contact this offer is linked to * @var int $start_date A UNIX timestamp of the start date * @var int $end_date A UNIX timestamp of the end date * @var int $invoice_date A UNIX timestamp of the invoice date * @var bool $accepted Whether the offer is accepted or not * @var null|int $invoice_fileId If an invoice has been generated, an the id of the file */ protected $pdo, $id, $contactId, $start_date, $end_date, $invoice_date, $accepted, $invoice_fileId; const SUBTOTAL = 1; const VAT = 2; const TOTAL = 3; /** * Create a new instance * * Blah * * @param PDO $pdo The PDO class, to access the database * @param int $id The id of the offer to fetch * * @throws PDOException If something went wrong with the database * @throws Exception If the offer could not be found */ public function __construct($pdo, $id) { $this->pdo = $pdo; $stmt = $this->pdo->prepare("SELECT * FROM `".Constants::db_prefix."offer` WHERE `id`=?"); $stmt->execute(array($id)); if ($stmt->rowCount() == 0) { throw new Exception("The offer with id '$id' could not be found."); } $offer = $stmt->fetch(PDO::FETCH_ASSOC); $this->id = $offer['id']; $this->contactId = $offer['contactId']; $this->start_date = strtotime($offer['start_date']); $this->end_date = strtotime($offer['end_date']); $this->invoice_date = strtotime($offer['invoice_date']); $this->accepted = (bool) $offer['accepted']; $this->invoice_fileId = $offer['invoice_fileId']; } //------------------------------------------------------------------------------ // Getters and setters //------------------------------------------------------------------------------ /** * Get the ID of the offer * * @return int The ID */ public function getId() { return $this->id; } /** * Get the ID of the contact that this offer is linked to * * @see offer::getContact() This function returns the contact as an instance of the object class * * @return int The ID */ public function getContactId() { return $this->contactId; } /** * Get the contact that this offer is linked to * * @see offer::getContactId() This function returns just the id * * @return contact The contact */ public function getContact() { return new Contact($this->pdo, $this->contactId); } /** * Get all assignment ids for this offer * * @see offer::getAssignments() This funtion returns instances of the assignment class instead of just the ids * * @throws PDOException Is something went wrong with the database * * @return int[] The ids */ public function getAssignmentIds() { $ids = array(); $assignments = $this->pdo->query("SELECT `id` FROM `".Constants::db_prefix."assignment` WHERE `offerId`={$this->id}")->fetchAll(PDO::FETCH_ASSOC); foreach ($assignments as $assignment) { $ids[] = $assignment['id']; } return $ids; } /** * Get all assignments for this offer * * @see offer::getAssignmentIds() This function returns just the ids of the assignments, and not instances of the assignment class * * @throws PDOException If something went wrong with the database * * @return assignment[] An array indexed by id of instances of the assignment class */ public function getAssignments() { $ids = $this->getAssignmentIds(); $assignments = array(); foreach ($ids as $id) { $assignments[$id] = new Assignment($this->pdo, $id); } return $assignments; } /** * Get all discount ids for this offer * * @see offer::getDiscounts() This funtion returns instances of the discount class instead of just the ids * * @throws PDOException Is something went wrong with the database * * @return int[] The ids */ public function getDiscountIds() { $ids = array(); $discounts = $this->pdo->query("SELECT `id` FROM `".Constants::db_prefix."discount` WHERE `offerId`={$this->id}")->fetchAll(PDO::FETCH_ASSOC); foreach ($discounts as $discount) { $ids[] = $discount['id']; } return $ids; } /** * Get all discounts for this offer * * @see offer::getDiscountIds() This function returns just the ids of the discounts, and not instances of the discount class * * @throws PDOException If something went wrong with the database * * @return discount[] An array indexed by id of instances of the discount class */ public function getDiscounts() { $ids = $this->getDiscountIds(); $discounts = array(); foreach ($ids as $id) { $discounts[$id] = new Discount($this->pdo, $id); } return $discounts; } /** * Get the payment id for this offer * * @see offer::getPayment() This funtion returns an instance of the payment class instead of just the id * * @throws PDOException Is something went wrong with the database * * @return int|null The id, or null if no payment exists */ public function getPaymentId() { $ids = array(); $payments = $this->pdo->query("SELECT `id` FROM `".Constants::db_prefix."payment` WHERE `offerId`={$this->id}")->fetchAll(PDO::FETCH_ASSOC); foreach ($payments as $payment) { return $payment['id']; } return null; } /** * Get the payment for this offer * * @see offer::getPaymentId() This function returns just the id of the payment, and not an instance of the payment class * * @throws PDOException If something went wrong with the database * * @return payment|null The payment, or null if it does not exist */ public function getPayment() { $id = $this->getPaymentId(); if (is_null($id)) { return null; } else { return new Payment($this->pdo, $id); } } /** * Get the start date of the assignment * * @return int The start date as a UNIX timestamp */ public function getStartDate() { return $this->start_date; } /** * Set the start date of the assignment * * @param int $start_date The new start date for the assignment as a UNIX timestamp * * @throws PDOException If something went wrong with the database * * @return bool True on succes, false on failure */ public function setStartDate($start_date) { $stmt = $this->pdo->prepare("UPDATE `".Constants::db_prefix."offer` SET `start_date`=? WHERE `id`=?"); $stmt->execute(array(date('Y-m-d', $start_date), $this->id)); if ($stmt->rowCount() == 1) { $this->start_date = $start_date; return true; } else { return false; } } /** * Get the end date of the assignment * * @return int The end date as a UNIX timestamp */ public function getEndDate() { return $this->end_date; } /** * Set the end date of the assignment * * @param int $end_date The new end date for the assignment as a UNIX timestamp * * @throws PDOException If something went wrong with the database * * @return bool True on succes, false on failure */ public function setEndDate($end_date) { $stmt = $this->pdo->prepare("UPDATE `".Constants::db_prefix."offer` SET `end_date`=? WHERE `id`=?"); $stmt->execute(array(date('Y-m-d', $end_date), $this->id)); if ($stmt->rowCount() == 1) { $this->end_date = $end_date; return true; } else { return false; } } /** * Get the invoice date of the assignment * * @return int The invoice date as a UNIX timestamp */ public function getInvoiceDate() { return $this->invoice_date; } /** * Set the invoice date of the assignment * * @param int $invoice_date The new invoice date for the assignment as a UNIX timestamp * * @throws PDOException If something went wrong with the database * * @return bool True on succes, false on failure */ public function setInvoiceDate($invoice_date) { $stmt = $this->pdo->prepare("UPDATE `".Constants::db_prefix."offer` SET `invoice_date`=? WHERE `id`=?"); $stmt->execute(array(date('Y-m-d', $invoice_date), $this->id)); if ($stmt->rowCount() == 1) { $this->invoice_date = $invoice_date; return true; } else { return false; } } /** * Get the date the payment was received * * @return int|null The date as a UNIX timestamp, or null if it wasn't received yet */ public function getPaymentReceived() { $payment = $this->getPayment(); if (is_null($payment)) { return null; } else { return $payment->date; } } /** * Check if the offer is accepted or not * * @return bool True if the offer is accepted, false if not */ public function isAccepted() { return $this->accepted; } /** * Toggle the `accepted' status of the offer * * @throws PDOException If something went wrong with the database * * @return bool True on success, false on failure */ public function toggleAccepted() { $stmt = $this->pdo->prepare("UPDATE `".Constants::db_prefix."offer` SET `accepted`=? WHERE `id`=?"); $new_value = !$this->accepted; $stmt->execute(array($new_value, $this->id)); if ($stmt->rowCount() == 1) { $this->accepted = $new_value; return true; } else { return false; } } /** * Get the ID of the file that the invoice of this offer is linked to * * @see offer::getInvoiceFile() This function returns the file as an instance of the file class * * @return int The ID */ public function getInvoiceFileId() { return $this->invoice_fileId; } /** * Get the file that the invoice this offer is linked to * * @see offer::getInvoiceId() This function returns just the id * * @return file|null The file, or null if it doesn't exist */ public function getInvoiceFile() { if ($this->invoice_fileId != null) { return new File($this->pdo, $this->invoice_fileId); } else { return null; } } /** * Set the invoice file id of the assignment * * @param int $invoice_fileId The new invoice file id for the assignment * * @throws PDOException If something went wrong with the database * * @return bool True on succes, false on failure */ public function setInvoiceFileId($invoice_fileId) { $stmt = $this->pdo->prepare("UPDATE `".Constants::db_prefix."offer` SET `invoice_fileId`=? WHERE `id`=?"); $stmt->execute(array($invoice_fileId, $this->id)); if ($stmt->rowCount() == 1) { $this->invoice_fileId = $invoice_fileId; return true; } else { return false; } } //------------------------------------------------------------------------------ // Other functions //------------------------------------------------------------------------------ /** * Calculate a handy number about the invoice * * Subtotal: the sum of the prices of the assignments excl. VAT * * VAT: the sum of all the VAT from all the assignments * * Total: the sum of subtotal and total * * @param int $what Any of offer::SUBTOTAL, offer::VAT and offer::TOTAL * @param int $round How many decimals to round the result on * @param bool $format Whether to format the number nicely (for output) or not (for calculations) * * @throws PDOException If something went wrong with the database * * @return float|bool The calculated value rounded to $round decimals, or false on incorrect input */ public function calculate($what = self::TOTAL, $round = 2, $format = true) { $return = 0; switch ($what) { case self::SUBTOTAL: foreach ($this->getAssignments() as $assignment) { $return += $assignment->calculate(assignment::SUBTOTAL, $round + 1, false); } foreach ($this->getDiscounts() as $discount) { $return += $discount->calculate(discount::SUBTOTAL, $round + 1, false); } break; case self::VAT: $assignments = $this->getAssignments(); foreach ($assignments as $assignment) { $return += $assignment->calculate(assignment::VAT, $round + 1, false); } foreach ($this->getDiscounts() as $discount) { $return += $discount->calculate(discount::VAT, $round + 1, false); } break; case self::TOTAL: $return = $this->calculate(self::SUBTOTAL, $round + 1, false) + $this->calculate(self::VAT, $round + 1, false); break; default: return false; } if ($format) { return number_format($return, 2); } else { return round($return, 2); } } /** * Remove this offer from the database * * If this doesn't succeed (i.e. false is returned), that means the offer was removed manually or by another instance of this class * * @throws PDOException If something went wrong with the database * * @return bool True on success, false on failure */ public function delete() { $stmt = $this->pdo->prepare("DELETE FROM `".Constants::db_prefix."offer` WHERE `id`=?"); $stmt->execute(array($this->id)); if ($stmt->rowCount() != 1) { return false; } else { return true; } } /** * Make a new assignment linked to this order * * @param string $title The title for this assignment * @param string $description The description for this assignment * @param int $hours The amount of hours to work on this assignment * @param float $price_per_hour The price per hour on this assignment * @param float $vat The VAT percentage (so, 21 for 21%, not 0.21!) * * @throws PDOException If something went wrong with the database * @throws Exception If there was a problem with the input * * @return assignment A new instance of the assignment class containing the new assignment */ public function createAssignment($title, $description, $hours, $price_per_hour, $vat) { $stmt = $this->pdo->prepare("INSERT INTO `".Constants::db_prefix."assignment` (`offerId`,`title`,`description`,`hours`,`price_per_hour`,`VAT_percentage`) VALUES (?,?,?,?,?,?)"); $stmt->execute(array( $this->id, $title, $description, $hours, $price_per_hour, $vat )); if ($stmt->rowCount() == 1) { return new Assignment($this->pdo, $this->pdo->lastInsertId()); } else { $error = $stmt->errorInfo(); throw new Exception($error[2]); } } /** * Make a new discount linked to this order * * @param string $title The title for this discount * @param string $description The description for this discount * @param float $value The value for this discount * @param float $vat The VAT percentage (so, 21 for 21%, not 0.21!) * * @throws PDOException If something went wrong with the database * @throws Exception If there was a problem with the input * * @return discount A new instance of the discount class containing the new discount */ public function createDiscount($title, $description, $value, $vat) { $stmt = $this->pdo->prepare("INSERT INTO `".Constants::db_prefix."discount` (`offerId`,`title`,`description`,`value`,`VAT_percentage`) VALUES (?,?,?,?,?)"); $stmt->execute(array( $this->id, $title, $description, $value, $vat )); if ($stmt->rowCount() == 1) { return new Discount($this->pdo, $this->pdo->lastInsertId()); } else { $error = $stmt->errorInfo(); throw new Exception($error[2]); } } /** * Add a payment for this order * * @param string $date Optional: the date for the payment * * @throws PDOException If something went wrong with the database * @throws Exception If there was a problem with the input * * @return payment A new instance of the payment class containing the new payment */ public function createPayment($date=null) { $date = is_null($date) ? time() : $date; $stmt = $this->pdo->prepare("INSERT INTO `".Constants::db_prefix."payment` (`offerId`,`date`) VALUES (?,?)"); $stmt->execute([$this->id, date('Y-m-d H:i:s', $date)]); if ($stmt->rowCount() == 1) { return new Payment($this->pdo, $this->pdo->lastInsertId()); } else { $error = $stmt->errorInfo(); throw new Exception($error[2]); } } /** * Generate a PDF invoice * * @throws PDOException If something went wrong with the database * @throws Exception If the file could not be written or an other error occured * * @return file An instance of the file class with information on the invoice file generated */ public function generateInvoice() { // Check if we already have a file $file = $this->getInvoiceFile(); if (!($file instanceof file)) { // If not, create a new file $i = 1; do { $invoice_nr = date('Y',$this->invoice_date) . str_pad($i++, 2, '0', STR_PAD_LEFT); $filename = 'invoice-' . $invoice_nr . '.pdf'; } while (file_exists(Constants::files_folder . $filename)); $file = BusinessAdmin::createFile($this->pdo, $filename); $this->setInvoiceFileId($file->id); } else { $invoice_nr = str_replace(array('invoice-','.pdf'), array('',''), $file->filename); } $list = array(); foreach ($this->getAssignments() as $assignment) $list[] = array( $assignment->title, $assignment->price_per_hour * $assignment->hours, $assignment->VAT_percentage . "%", $assignment->price_per_hour * $assignment->hours * (1 + $assignment->VAT_percentage / 100) ); foreach ($this->getDiscounts() as $discount) $list[] = array( $discount->getTitle(), $discount->calculate(discount::SUBTOTAL), $discount->getVAT() . "%", $discount->calculate(discount::TOTAL) ); $pdf = new Correspondence(); $pdf->SetContact($this->getContact()); $pdf->SetTitle($pdf->_('invoice') . ' ' . $invoice_nr); $pdf->AddPage(); $pdf->correspondenceHeader(); $pdf->SetY(100); $pdf->SetFont('','B',14); $pdf->SetTextColor(correspondence::HEAD_RED, correspondence::HEAD_GREEN, correspondence::HEAD_BLUE); $pdf->Cell(60,6, $pdf->_('invoice'),'B'); $pdf->SetTextColor(0); $pdf->Ln(); $width = array(90,25,20,25); $subtotal = 0; $btw = array(); $total = 0; // Header $pdf->SetFont('','',9); $pdf->Cell(60,4); $pdf->Ln(); $pdf->SetFont('','B'); $pdf->Cell(30,4.5,$pdf->_('invoice-date')); $pdf->SetFont('',''); $pdf->Cell(50,4.5,date("d-m-Y", $this->invoice_date)); $pdf->Ln(); $pdf->SetFont('','B'); $pdf->Cell(30,4.5,$pdf->_('invoice-nr')); $pdf->SetFont('',''); $pdf->Cell(50,4.5,$invoice_nr); $pdf->Ln(); $pdf->SetFont('','B'); $pdf->Cell(30,4.5,$pdf->_('due-date')); $pdf->SetFont('',''); $pdf->Cell(50,4.5,date("d-m-Y",$this->invoice_date+3600*24*30)); $pdf->Ln(); $pdf->Cell(60,4.5,'','B'); $pdf->Ln(); $pdf->SetY(140); // Table $pdf->SetFont('','B',11); $pdf->SetTextColor(correspondence::HEAD_RED, correspondence::HEAD_GREEN, correspondence::HEAD_BLUE); $pdf->Cell($width[0],7,$pdf->_('description'),'B'); $pdf->Cell($width[1],7,$pdf->_('price-excl'),'B',0,'R'); $pdf->Cell($width[2],7,$pdf->_('vat'),'B',0,'R'); $pdf->Cell($width[3],7,$pdf->_('price-incl'),'B',0,'R'); $pdf->SetTextColor(0); $pdf->Ln(); $pdf->SetFont('',''); foreach ($list as $row) { $x = $pdf->getX(); $y = $pdf->getY(); $pdf->MultiCell($width[0],6,iconv('utf-8', 'iso-8859-1', $row[0]),0,'L'); $newy = $pdf->getY(); $pdf->SetXY($x + $width[0], $y); $pdf->Cell($width[1],6,correspondence::valuta().number_format($row[1],2),'',0,'R'); $pdf->Cell($width[2],6,round($row[2],0) . '%','',0,'R'); $pdf->Cell($width[3],6,correspondence::valuta().number_format($row[3],2),'',0,'R'); $pdf->Ln(); $pdf->SetY($newy); $pdf->addPageIfOnEnd(); $subtotal += $row[1]; if (!isset($btw[$row[2]])) $btw[$row[2]] = 0; $btw[$row[2]] += $row[3] - $row[1]; } $total = $subtotal; foreach ($btw as $m) { $total += $m; } $pdf->Cell(array_sum($width),5,'','T'); $pdf->Ln(); $pdf->Cell(array_sum($width),5); $pdf->Ln(); $pdf->Cell($width[0],7); $pdf->SetFont('','B'); $pdf->Cell($width[1] + $width[2],7,$pdf->_('amount')); $pdf->SetFont('',''); $pdf->Cell($width[3],7,correspondence::valuta() . $this->calculate(self::SUBTOTAL),'',0,'R'); $pdf->Ln(); foreach ($btw as $p => $m) { $pdf->Cell($width[0],7); $pdf->Cell($width[1] + $width[2],7,$pdf->_('vat') . ' '.round($p,0).'%'); $pdf->Cell($width[3],7,correspondence::valuta() . number_format($m,2),'',0,'R'); $pdf->Ln(); } $pdf->Cell(array_sum($width),5); $pdf->Ln(); $pdf->Cell($width[0],7); $pdf->SetFont('','B'); $pdf->Cell($width[1] + $width[2],7,$pdf->_('total')); $pdf->SetFont('',''); $pdf->Cell($width[3],7,correspondence::valuta() . $this->calculate(self::TOTAL),'T',0,'R'); $pdf->Ln(); // Footer $pdf->Ln(); $pdf->addPageIfOnEnd(); if ($pdf->GetY() < 230) { $pdf->SetY(230); } $oldY = $pdf->GetY(); $pdf->Cell(45,20,'',1); $pdf->Cell(12.5,20); $pdf->Cell(45,20,'',1); $pdf->Cell(12.5,20); $pdf->Cell(45,20,'',1); $pdf->SetFont('','B',10); $pdf->SetTextColor(correspondence::HEAD_RED, correspondence::HEAD_GREEN, correspondence::HEAD_BLUE); $pdf->SetY($oldY + 3); $pdf->Cell(5,5); $pdf->Cell(40,5,$pdf->_('iban')); $pdf->Cell(17.5,5); $pdf->Cell(40,5,$pdf->_('invoice-nr')); $pdf->Cell(17.5,5); $pdf->Cell(40,5,$pdf->_('amount-due')); $pdf->SetTextColor(0); $pdf->SetFont('','',8); $pdf->Ln(); $pdf->Ln(); $oldY = $pdf->GetY(); $pdf->Cell(5,5); $pdf->Cell(40,5, Constants::invoice_iban); $pdf->Cell(17.5,5); $pdf->Cell(40,5,$invoice_nr); $pdf->Cell(17.5,5); $pdf->Cell(40,5,correspondence::valuta() . $this->calculate(self::TOTAL)); $pdf->SetY($oldY + 14); $pdf->SetFontSize(7); $pdf->Cell(160,5,$pdf->_('request'),0,0,'C'); $pdf->Ln(); $pdf->Cell(160,5,str_replace('%%', Constants::invoice_bic, $pdf->_('biccode')),0,0,'C'); if (file_exists($file->getFilenamePath())) { unlink($file->getFilenamePath()); } $pdf->Output($file->getFilenamePath(),'F'); chmod($file->getFilenamePath(),0644); return $file; } }