Commit 53acf176 authored by Karamel's avatar Karamel
Browse files

Fix CashSession fields and use it to include ZTicket. Update Z FiscalTicket.

parent 5139e2da
......@@ -40,12 +40,14 @@ class CashsessionAPI extends APIHelper implements API
* @return The requested session, created if there aren't any yet. */
public function get($id) {
if (!is_array($id)) { $id = array('cashRegister' => $id, 'sequence' =>null); }
// Look for the cash register
$cashRegister = $this->dao->read(CashRegister::class, $id['cashRegister']);
if ($cashRegister == null) {
return null;
}
// Look for session
if ($id['sequence'] === null) {
// Look for the latest session for the requested cash register
$cashRegister = $this->dao->read(CashRegister::class, $id['cashRegister']);
if ($cashRegister == null) {
return null;
}
$sessions = $this->dao->search(static::MODEL_NAME,
[new DAOCondition('cashRegister', '=', $cashRegister)],
1, 0, '-sequence');
......@@ -61,7 +63,14 @@ class CashsessionAPI extends APIHelper implements API
return $session;
}
} else {
return $this->dao->read(static::MODEL_NAME, $id);
$search = $this->dao->search(static::MODEL_NAME,
[new DAOCondition('cashRegister', '=', $cashRegister),
new DAOCondition('sequence', '=', $id['sequence'])],
1);
if (count($search) > 0) {
return $search[0];
}
return null;
}
}
......@@ -89,10 +98,6 @@ class CashsessionAPI extends APIHelper implements API
if ($currentData !== null && $currentData->isClosed()) {
throw new \BadMethodCallException('Closed sessions are read-only.');
}
// Closed session without printed content
if ($d->isClosed() && $d->getPrintedContent() === null) {
throw new \BadMethodCallException('Closed session must have some printed content.');
}
if ($currentData === null && $d->isClosed()) {
// Trying to register everything at once, this will prevent
// registering the tickets. Throw an error.
......@@ -104,7 +109,7 @@ class CashsessionAPI extends APIHelper implements API
$fiscalTicket->setType(FiscalTicket::TYPE_ZTICKET);
$fiscalTicket->setSequence(FiscalTicket::getZTicketSequence($d));
$fiscalTicket->setNumber($d->getSequence());
$fiscalTicket->setContent($d->getPrintedContent());
$fiscalTicket->setContent(json_encode($d->toStone()));
// Check continuity and integrity
if ($d->getSequence() == 1 && $previousFTicket !== null) {
// No.1 with a previous ticket
......@@ -186,6 +191,7 @@ class CashsessionAPI extends APIHelper implements API
$paymentCount = 0;
$cs = 0.0;
$payments = [];
$custBalances = [];
$taxes = [];
$catSales = [];
// Load tickets and add them to the summary
......@@ -195,6 +201,7 @@ class CashsessionAPI extends APIHelper implements API
$summaryPmts = [];
$summaryTaxes = [];
$summaryCats = []; // Meow?
$summaryCusts = [];
foreach ($tickets as $tkt) {
$ticketCount++;
if (!empty($tkt->getCustCount())) {
......@@ -220,6 +227,15 @@ class CashsessionAPI extends APIHelper implements API
$summaryPmts[$pmtRef]['amount'] += $pmt->getAmount();
$summaryPmts[$pmtRef]['currencyAmount'] += $pmt->getCurrencyAmount();
}
// Cust balance sums
if ($tkt->getCustomer() != null && abs($tkt->getCustBalance() > 0.005)) {
$custId = $tkt->getCustomer()->getId();
if (!isset($summaryCusts[$custId]) && $tkt->getCustBalance()) {
$summaryCusts[$custId] = ['customer' => $custId,
'balance' => 0.0];
}
$summaryCusts[$custId]['balance'] += $tkt->getCustBalance();
}
// Line sums
$tktLines = $tkt->getLines();
foreach ($tktLines as $line) {
......@@ -251,6 +267,13 @@ class CashsessionAPI extends APIHelper implements API
}
$payments[] = $pmtSum;
}
foreach ($summaryCusts as $custId => $sum) {
$custSum = new GenericModel();
foreach ($sum as $key => $value) {
$custSum->set($key, $value);
}
$custBalances[] = $custSum;
}
foreach ($summaryTaxes as $taxId => $sum) {
$taxSum = new GenericModel();
foreach ($sum as $key => $value) {
......@@ -270,6 +293,7 @@ class CashsessionAPI extends APIHelper implements API
$ret->set('paymentCount', $paymentCount);
$ret->set('cs', $cs);
$ret->set('payments', $payments);
$ret->set('custBalances', $custBalances);
$ret->set('taxes', $taxes);
$ret->set('catSales', $catSales);
return $ret;
......
......@@ -83,9 +83,13 @@ class TicketAPI extends APIHelper implements API
throw new \BadMethodCallException('Tickets are read-only.');
}
// Cash session must be opened
$session = $this->dao->read(CashSession::class,
['cashRegister' => $ticket->getCashRegister(),
'sequence' => $ticket->getSequence()]);
$sessSearch = $this->dao->search(CashSession::class,
[new DAOCondition('cashRegister', '=', $ticket->getCashRegister()),
new DAOCondition('sequence', '=', $ticket->getSequence())], 1);
$session = null;
if (count($sessSearch) > 0) {
$session = $sessSearch[0];
}
if ($session === null || $session->isClosed()
|| !$session->isOpened()){
throw new \BadMethodCallException('Tickets must be assigned to an opened cash session.');
......
......@@ -22,7 +22,9 @@
namespace Pasteque\Server\Model;
use \Pasteque\Server\API\VersionAPI;
use \Pasteque\Server\System\DateUtils;
use \Pasteque\Server\System\DAO\DAOCondition;
use \Pasteque\Server\System\DAO\DoctrineModel;
/**
......@@ -32,33 +34,70 @@ use \Pasteque\Server\System\DAO\DoctrineModel;
* @package Pasteque
* @SWG\Definition(type="object")
* @Entity
* @Table(name="sessions")
* @Table(name="sessions", uniqueConstraints={@UniqueConstraint(name="session_index", columns={"cashregister_id", "sequence"})})
*/
class CashSession extends DoctrineModel
{
protected function getDirectFieldNames() {
return ['sequence', 'continuous', 'openDate', 'closeDate',
'openCash', 'closeCash', 'expectedCash'];
'openCash', 'closeCash', 'expectedCash',
'ticketCount', 'custCount',
'cs'];
}
protected function getAssociationFields() {
return [
[
'name' => 'cashRegister',
'class' => '\Pasteque\Server\Model\CashRegister'
],
[
'name' => 'payments',
'class' => '\Pasteque\Server\Model\CashSessionPayment',
'array' => true,
'embedded' => true
],
[
'name' => 'taxes',
'class' => '\Pasteque\Server\Model\CashSessionTax',
'array' => true,
'embedded' => true
],
[
'name' => 'catSales',
'class' => '\Pasteque\Server\Model\CashSessionCat',
'array' => true,
'embedded' => true
],
[
'name' => 'custBalances',
'class' => '\Pasteque\Server\Model\CashSessionCustBalance',
'array' => true,
'embedded' => true
]
];
}
public function getId() {
return ['cashRegister' => $this->getCashRegister()->getId(),
'sequence' => $this->getSequence()];
public function __construct() {
$this->taxes = new \Doctrine\Common\Collections\ArrayCollection();
$this->payments = new \Doctrine\Common\Collections\ArrayCollection();
$this->catSales = new \Doctrine\Common\Collections\ArrayCollection();
$this->custBalances = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Internal Id of the session. Required to link taxes and payments.
* @var integer
* @SWG\Property()
* @Id @Column(type="integer")
* @GeneratedValue
*/
protected $id;
public function getId() { return $this->id; }
/**
* Id of a cash register
* @var int
* @SWG\Property(format="int32")
* @Id
* @ManyToOne(targetEntity="\Pasteque\Server\Model\CashRegister", inversedBy="sessions")
* @JoinColumn(name="cashregister_id", referencedColumnName="id", nullable=false)
*/
......@@ -72,7 +111,6 @@ class CashSession extends DoctrineModel
* Number of the session's cash register
* @var int
* @SWG\Property(format="int32")
* @Id
* @Column(type="integer")
*/
protected $sequence;
......@@ -102,7 +140,7 @@ class CashSession extends DoctrineModel
* @SWG\Property(format="date-time")
* @Column(type="datetime", nullable=true)
*/
protected $openDate;
protected $openDate = null;
public function getOpenDate() { return $this->openDate; }
/** @throws \UnexpectedValueException When trying to override it. */
public function setOpenDate($openDate) {
......@@ -120,7 +158,7 @@ class CashSession extends DoctrineModel
* @SWG\Property(format="date-time")
* @Column(type="datetime", nullable=true)
*/
protected $closeDate;
protected $closeDate = null;
public function getCloseDate() { return $this->closeDate; }
/** @throws \UnexpectedValueException When trying to override it. */
public function setCloseDate($closeDate) {
......@@ -136,13 +174,17 @@ class CashSession extends DoctrineModel
* Read-only. Will throw an exception if trying to override it.
* @var float
* @SWG\Property(format="double")
* @Column(type="float", nullable=true)
*/
protected $openCash;
public function getOpenCash() { return round($this->openCash, 5); }
protected $openCash = null;
public function getOpenCash() {
if ($this->openCash === null) { return null; }
else { return round($this->openCash, 5); }
}
/** @throws \UnexpectedValueException When trying to override it. */
public function setOpenCash($openCash) {
if ($this->openCash === null) {
$this->openCash = round($openCash, 5);
$this->openCash = ($openCash === null) ? null : round($openCash, 5);
} else if (round($this->openCash, 5) != round($openCash, 5)) {
throw new \UnexpectedValueException('Open cash is read only');
}
......@@ -153,15 +195,19 @@ class CashSession extends DoctrineModel
* Read-only. Will throw an exception if trying to override it.
* @var float
* @SWG\Property(format="double")
* @Column(type="float", nullable=true)
*/
protected $closeCash;
public function getCloseCash() { return round($this->closeCash, 5); }
protected $closeCash = null;
public function getCloseCash() {
if ($this->closeCash === null) { return null; }
else { return round($this->closeCash, 5); }
}
/** @throws \UnexpectedValueException When trying to override it. */
public function setCloseCash($closeCash) {
if ($this->closeCash === null) {
$this->closeCash = round($closeCash, 5);
$this->closeCash = ($closeCash === null) ? null : round($closeCash, 5);
} else if (round($this->closeCash, 5) != round($closeCash, 5)) {
throw new \UnexpectedValueException('Open cash is read only');
throw new \UnexpectedValueException('Close cash is read only');
}
}
......@@ -171,22 +217,161 @@ class CashSession extends DoctrineModel
* This field must computed automatically and stored on session close.
* @var float
* @SWG\Property(format="double")
* @Column(type="float", nullable=true)
*/
protected $expectedCash;
public function getExpectedCash() { return round($this->expectedCash, 5); }
protected $expectedCash = null;
public function getExpectedCash() {
if ($this->expectedCash === null) { return null; }
else { return round($this->expectedCash, 5); }
}
public function setExpectedCash($expectedCash) {
$this->expectedCash = round($expectedCash, 5);
$this->expectedCash = ($expectedCash === null) ? null : round($expectedCash, 5);
}
/**
* Number of the tickets in the session. Updated only for closed cashes.
* @var int
* @SWG\Property(format="int32")
* @Column(type="integer", nullable=true)
*/
protected $ticketCount = null;
public function getTicketCount() { return $this->ticketCount; }
public function setTicketCount($ticketCount) {
if ($this->ticketCount === null) { $this->ticketCount = $ticketCount; }
else if ($this->ticketCount != $ticketCount) {
throw new \UnexpectedValueException('Ticket count is read only');
}
}
/** Private method to remove ticketCount from structs if it is set when not closed. */
protected function resetTicketCount() { $this->ticketCount = null; }
/**
* Number of customers in the session. Updated only for closed cashes.
* @var int
* @SWG\Property(format="int32")
* @Column(type="integer", nullable=true)
*/
protected $custCount;
public function getCustCount() { return $this->custCount; }
/** @throws \UnexpectedValueException When trying to override it. */
public function setCustCount($custCount) {
if ($this->custCount === null) { $this->custCount = $custCount; }
else if ($this->custCount != $custCount) {
throw new \UnexpectedValueException('Customer count is read only');
}
}
/** Private method to remove custCount from structs if it is set when not closed. */
protected function resetCustCount() { $this->custCount = null; }
/**
* Consolidated sales. Read only and only set on close.
* Read-only. Will throw an exception if trying to override it.
* @var float
* @SWG\Property(format="double", nullable=true)
* @Column(type="float", nullable=true)
*/
protected $cs = null;
public function getCS() {
if ($this->cs === null) { return null; }
else { return round($this->cs, 5); }
}
/** @throws \UnexpectedValueException When trying to override it. */
public function setCS($cs) {
if ($this->cs === null) {
$this->cs = ($cs === null) ? null : round($cs, 5);
} else if (round($this->cs, 5) != round($cs, 5)) {
throw new \UnexpectedValueException('Consolidated sales is read only');
}
}
/** Private method to remove CS from structs if it is set when not closed. */
protected function resetCS() { $this->cs = null; }
/**
* Array of tax totals. It holds the final tax base/amount for each tax.
* @var \Pasteque\CashSessionTax[]
* @SWG\Property()
* @OneToMany(targetEntity="\Pasteque\Server\Model\CashSessionTax", mappedBy="cashSession", cascade={"persist"}, orphanRemoval=true)
*/
protected $taxes;
public function getTaxes() { return $this->taxes; }
public function setTaxes($taxes) {
$this->taxes->clear();
foreach ($taxes as $tax) {
$this->addTax($tax);
}
}
public function clearTaxes() {
$this->getTaxes()->clear();
}
public function addTax($tax) {
$this->taxes->add($tax);
$tax->setCashSession($this);
}
/**
* Array of total amount of payments by mode.
* @var \Pasteque\CashSessionPayment[]
* @SWG\Property()
* @OneToMany(targetEntity="\Pasteque\Server\Model\CashSessionPayment", mappedBy="cashSession", cascade={"persist"}, orphanRemoval=true)
*/
protected $payments;
public function getPayments() { return $this->payments; }
public function setPayments($payments) {
$this->payments->clear();
foreach ($payments as $payment) {
$this->addPayment($payment);
}
}
public function clearPayments() {
$this->getPayments()->clear();
}
public function addPayment($payment) {
$this->payments->add($payment);
$payment->setCashSession($this);
}
/**
* Array of total amount of cs by category.
* @var \Pasteque\CashSessionCat[]
* @SWG\Property()
* @OneToMany(targetEntity="\Pasteque\Server\Model\CashSessionCat", mappedBy="cashSession", cascade={"persist"}, orphanRemoval=true)
*/
protected $catSales;
public function getCatSales() { return $this->catSales; }
public function setCatSales($catSales) {
$this->catSales->clear();
foreach ($catSales as $cat) {
$this->addCatSales($cat);
}
}
public function clearCatSales() {
$this->getCatSales()->clear();
}
public function addCatSales($cat) {
$this->catSales->add($cat);
$cat->setCashSession($this);
}
/** XML content of the printed Z ticket.
* It is not stored in database but used when saving tickets
* to generate the associated FiscalTicket. It is build with
* fromStruct but not passed to toStruct at it is only meant
* for internal use. */
protected $printedContent;
public function getPrintedContent() { return $this->printedContent; }
public function setPrintedContent($content) {
$this->printedContent = $content;
/**
* Array of total balance change by customer.
* @var \Pasteque\CashSessionCat[]
* @SWG\Property()
* @OneToMany(targetEntity="\Pasteque\Server\Model\CashSessionCustBalance", mappedBy="cashSession", cascade={"persist"}, orphanRemoval=true)
*/
protected $custBalances;
public function getCustBalances() { return $this->custBalances; }
public function setCustBalances($custBalances) {
$this->custBalances->clear();
foreach ($custBalances as $custBalance) {
$this->addCustBalances($custBalance);
}
}
public function clearCustBalances() {
$this->getCustBalances()->clear();
}
public function addCustBalances($custBalance) {
$this->custBalances->add($custBalance);
$custBalance->setCashSession($this);
}
/**
......@@ -202,24 +387,29 @@ class CashSession extends DoctrineModel
public function isOpened() { return $this->openDate != null; }
protected static function instanceFromStruct($struct, $dao, $embedded = false) {
$model = null;
if (empty($struct['cashRegister'] || empty($struct['sequence']))) {
if (empty($struct['cashRegister']) || empty($struct['sequence'])) {
// Sessions are note allowed from scratch, they are always read
// from database.
// See CashsessionAPI to generate new CashSessions.
return null;
}
$model = $dao->read(static::class,
['cashRegister' => $struct['cashRegister'],
'sequence' => $struct['sequence']]);
$cashRegister = $dao->read(CashRegister::class, $struct['cashRegister']);
if ($cashRegister === null) { return null; }
$search = $dao->search(static::class,
[new DAOCondition('cashRegister', '=', $cashRegister),
new DAOCondition('sequence', '=', $struct['sequence'])]);
if (count($search) == 0) { return null; }
$model = $search[0];
if (!$model->isClosed()) {
$model->resetTicketCount();
$model->resetCustCount();
$model->resetCS();
}
return $model;
}
public static function fromStruct($struct, $dao, $embedded = false) {
$model = parent::fromStruct($struct, $dao, $embedded);
if (isset($struct['printedContent'])) {
$model->setPrintedContent($struct['printedContent']);
}
return $model;
}
......@@ -229,4 +419,60 @@ class CashSession extends DoctrineModel
$struct['closeDate'] = DateUtils::toTimestamp($this->getCloseDate());
return $struct;
}
/** Create struct of the full data to be written in stone
* (that is, FiscalTicket). The cash session must be closed. */
public function toStone() {
$struct = $this->toStruct();
// Include source version in case it has to be parsed.
$struct['version'] = VersionAPI::VERSION;
// Format date in human-readable format
if ($struct['openDate'] !== null) {
$struct['openDate'] = $this->getOpenDate()->format('Y-m-d H:i:s');
}
if ($struct['closeDate'] !== null) {
$struct['closeDate'] = $this->getCloseDate()->format('Y-m-d H:i:s');
}
// Fetch associative fields and include the data.
unset($struct['id']);
$struct['cashRegister'] = ['reference' => $this->getCashRegister()->getReference(),
'label' => $this->getCashRegister()->getLabel()];
for ($i = 0; $i < count($struct['payments']); $i++) {
$payment = $struct['payments'][$i];
unset($payment['id']);
unset($payment['cashSession']);
$paymentMode = $this->getPayments()->get($i)->getPaymentMode();
$currency = $this->getPayments()->get($i)->getCurrency();
$payment['paymentMode'] = ['reference' =>$paymentMode->getReference(),
'label' => $paymentMode->getLabel()];
$payment['currency'] = ['reference' => $currency->getReference(),
'label' => $currency->getLabel()];
$struct['payments'][$i] = $payment;
}
for ($i = 0; $i < count($struct['taxes']); $i++) {
$tax = $struct['taxes'][$i];
unset($tax['id']);
unset($tax['cashSession']);
unset($tax['sequence']);
unset($tax['tax']);
$struct['taxes'][$i] = $tax;
}
for ($i = 0; $i < count($struct['custBalances']); $i++) {
$bal = $struct['custBalances'][$i];
unset($bal['id']);
unset($bal['cashSession']);
unset($bal['sequence']);
$c = $this->getCustBalances()->get($i)->getCustomer();
$bal['customer'] = ['dispName' => $c->getDispName(),
'firstName' => $c->getFirstName(),
'lastName' => $c->getLastName()];
$struct['custBalances'][$i] = $bal;
}
for ($i = 0; $i < count($struct['catSales']); $i++) {
$cat = $struct['catSales'][$i];
unset($cat['id']);
unset($cat['cashSession']);
}
return $struct;
}
}
<?php
// Pastèque API
//
// Copyright (C)
// 2012 Scil (http://scil.coop)
// 2017 Karamel, Association Pastèque (karamel@creativekara.fr, https://pasteque.org)
//
// This file is part of Pastèque.
//
// Pastèque 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 3 of the License, or
// (at your option) any later version.
//
// Pastèque 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 Pastèque. If not, see <http://www.gnu.org/licenses/>.
namespace Pasteque\Server\Model;
use \Pasteque\Server\System\DAO\DAO;
use \Pasteque\Server\System\DAO\DoctrineModel;
/**
* Class CashSessionTax. Sum of the cs amount by category.
* This class is for fast data analysis only.
* For declarations see FiscalTicket.
* @package Pasteque
* @SWG\Definition(type="object")
* @Entity
* @Table(name="sessioncats")
*/
class CashSessionCat extends DoctrineModel // Embedded class
{
public function getDirectFieldNames() {
// Not associative to be able to delete empty categories.
return ['reference', 'label', 'amount'];
}
public function getAssociationFields() {
return [
[
'name' => 'cashSession',
'class' => '\Pasteque\Server\Model\CashSession',
'null' => true // because embedded
]
];
}
public function getId() {
return array('cashSession' => $this->getCashSession()->getId(),
'reference' => $this->getReference());
}
/**
* @var integer