src/Controller/UTools/Store/SalesController.php line 198

Open in your IDE?
  1. <?php
  2. namespace FMS\eLibreria\CmsBundle\Controller\UTools\Store;
  3. use FMS\eLibreria\CmsBundle\Controller\AbstractCrudEntity;
  4. use FMS\eLibreria\CmsBundle\Utils\BreadcrumbItem;
  5. use FMS\eLibreria\CmsBundle\Verifactu\PosTicketView;
  6. use FMS\eLibreria\CoreBundle\Model\Entity\Employee;
  7. use FMS\eLibreria\CoreBundle\Model\Entity\Facility;
  8. use FMS\eLibreria\CoreBundle\Model\Entity\Item;
  9. use FMS\eLibreria\CoreBundle\Model\Entity\Location;
  10. use FMS\eLibreria\CoreBundle\Model\Entity\Provider;
  11. use FMS\eLibreria\CoreBundle\Model\Entity\Role;
  12. use FMS\eLibreria\CoreBundle\Model\Entity\Sale;
  13. use FMS\eLibreria\CoreBundle\Model\Entity\SaleItem;
  14. use FMS\eLibreria\CoreBundle\Model\Entity\SalePayment;
  15. use FMS\eLibreria\CoreBundle\Model\Entity\Bill;
  16. use FMS\eLibreria\CoreBundle\Model\Entity\SimpleBillMessage;
  17. use FMS\eLibreria\CoreBundle\Model\Entity\SimpleBillMessageType;
  18. use FMS\eLibreria\CoreBundle\Model\Entity\Tax;
  19. use FMS\eLibreria\CoreBundle\Model\Object\FilterElement;
  20. use FMS\eLibreria\CoreBundle\Model\Repository\FacilityRepository;
  21. use FMS\eLibreria\CoreBundle\Model\Repository\LocationRepository;
  22. use FMS\eLibreria\CoreBundle\Model\Repository\ProviderRepository;
  23. use FMS\eLibreria\CoreBundle\Model\Repository\SaleRepository;
  24. use FMS\eLibreria\CoreBundle\Model\Repository\SimpleBillMessageRepository;
  25. use FMS\eLibreria\CoreBundle\Model\Repository\TaxRepository;
  26. use FMS\eLibreria\CoreBundle\Verifactu\Breakdown\AdvancePaymentLedger;
  27. use FMS\eLibreria\CoreBundle\Verifactu\ClientAccount\ClientAccountLedger;
  28. use FMS\eLibreria\CoreBundle\Verifactu\Correction\TicketReturnService;
  29. use FMS\eLibreria\CoreBundle\Verifactu\Document\ValueObject\DocumentAction;
  30. use FMS\eLibreria\CoreBundle\Verifactu\Document\ValueObject\InvoiceType;
  31. use FMS\eLibreria\CoreBundle\Verifactu\Pos\ValueObject\PaymentMethod;
  32. use FMS\eLibreria\CoreBundle\Verifactu\Pos\ValueObject\SaleStatus;
  33. use FMS\eLibreria\CoreBundle\Verifactu\Status\RecordPanelReader;
  34. use FMS\eLibreria\CoreBundle\Verifactu\Status\ValueObject\DocumentRecordStatus;
  35. use Symfony\Component\HttpFoundation\Request;
  36. class SalesController extends AbstractCrudEntity {
  37. public static function getSubscribedServices(): array {
  38. return array_merge(parent::getSubscribedServices(), array(
  39. ClientAccountLedger::class => ClientAccountLedger::class,
  40. ));
  41. }
  42. /**
  43. * How a return or a cancelled reservation of this document may give the money back: cash, card and the
  44. * bearer voucher always, and the account of a client only when the document names that client (change
  45. * verifactu-14, task 4.4). The service refuses the account without a client all the same.
  46. *
  47. * @return list<PaymentMethod>
  48. */
  49. public static function refundMethodsOf(?Bill $document): array {
  50. $hasAClient = !is_null($document) && !is_null($document->getClient());
  51. return array_values(array_filter(
  52. PaymentMethod::cases(),
  53. static fn (PaymentMethod $method): bool => $method->isRefundable() && ($hasAClient || !$method->needsAClient())
  54. ));
  55. }
  56. protected function getEntityClass() {
  57. return Sale::class;
  58. }
  59. protected function getEntityName() {
  60. return "Sales";
  61. }
  62. protected function getRoutePrefix() {
  63. return "cms_utools_store_sales";
  64. }
  65. protected function getDefaultActions() {
  66. return array();
  67. }
  68. protected function getExtraActions() {
  69. return array();
  70. }
  71. protected function getSearchFilters() {
  72. // What a sale is, which is decided by the document issued when it was charged: a ticket, an invoice of
  73. // the till, one whose document was annulled, or a sale charged before this version (change
  74. // verifactu-05, design decisions 2 and 12; D-47).
  75. $statuses = array();
  76. foreach(SaleStatus::cases() as $status){
  77. $statuses[$status->value] = "sales.status." . $status->value;
  78. }
  79. // A sale shows the status of its ticket, because a sale is the document that was issued when it was
  80. // charged (change verifactu-10, design decision 1; change verifactu-05, design decision 2).
  81. $recordStatuses = array();
  82. foreach(DocumentRecordStatus::cases() as $recordStatus){
  83. $recordStatuses[$recordStatus->value] = $recordStatus->labelKey();
  84. }
  85. $filters = array(
  86. // The identifier of a sale is the identifier of its document since this change (decision 2).
  87. "idNum" => new FilterElement("idNum", "identificationNumber", "id_num"),
  88. "status" => new FilterElement("status", "status", "status", FilterElement::FILTER_TYPE_SELECT, $statuses),
  89. "recordStatus" => new FilterElement("recordStatus", "billLinked.recordStatus", "record_status", FilterElement::FILTER_TYPE_SELECT, $recordStatuses),
  90. "facility" => new FilterElement("facility", "facility.name", "facility"),
  91. "createdAt" => new FilterElement("createdAt", "createdAt", "created_at", FilterElement::FILTER_TYPE_DATE),
  92. "total" => new FilterElement("total", "total", "total"),
  93. "employee" => new FilterElement("employee", array("employee.firstname", "employee.lastname"), "created_by"),
  94. );
  95. // An installation that does not issue in VERI*FACTU has no status with the AEAT to filter by (change
  96. // verifactu-15, design decision 2; D-76).
  97. if (!$this->issuesInVerifactu()) {
  98. unset($filters["recordStatus"]);
  99. }
  100. return $filters;
  101. }
  102. protected function getHeaderList() {
  103. return array_values(array_filter(array(
  104. "idNum",
  105. "status",
  106. $this->issuesInVerifactu() ? "recordStatus" : null,
  107. "facility",
  108. "createdAt",
  109. "total",
  110. "employee",
  111. )));
  112. }
  113. protected function isExportEnabled() {
  114. return true;
  115. }
  116. /**
  117. * @param FilterElement $value
  118. */
  119. public function translateArray($value){
  120. $translator = $this->getTranslator();
  121. return $translator->trans(("sales.list." . $value->getFormLabel()), array(), "cms");
  122. }
  123. protected function exportTitleLine() {
  124. $headerList = $this->getHeaderList();
  125. $searchFilters = $this->getSearchFilters();
  126. $headers = array();
  127. foreach($headerList as $header){
  128. $headers[] = $searchFilters[$header];
  129. }
  130. return implode(";", array_map(array($this, "translateArray"), $headers)) . PHP_EOL;
  131. }
  132. /**
  133. * @param array $row
  134. */
  135. protected function exportRowToLine($row) {
  136. /** @var Sale $sale */
  137. $sale = $row[0];
  138. $translator = $this->getTranslator();
  139. // A sale charged before this version has no document of this version, so it has no AEAT status to
  140. // export either (change verifactu-10, design decision 1).
  141. $document = $sale->getBillLinked();
  142. $columns = array(
  143. sprintf("=\"%s\"", $sale->getIdentificationNumber()),
  144. $translator->trans("sales.status." . $sale->getStatus()->value, array(), "cms"),
  145. is_null($document) || !$document->getRecordStatus()->isApplicable()
  146. ? ""
  147. : $translator->trans($document->getRecordStatus()->labelKey(), array(), "cms"),
  148. $sale->getFacility(),
  149. $sale->getCreatedAt(true),
  150. str_replace(".",",",$sale->getTotal()),
  151. $sale->getEmployee(),
  152. );
  153. // The same columns as the header, which leaves the AEAT out while nothing is activated (change 15).
  154. if (!$this->issuesInVerifactu()) {
  155. unset($columns[2]);
  156. }
  157. return implode(";", $columns) . PHP_EOL;
  158. }
  159. protected function getEntityForms($entity, $editMode) {
  160. return array();
  161. }
  162. /**
  163. * The ticket view is the last argument because the base class fixes the two before it: Symfony injects
  164. * it as a service and nothing ever passes it by hand.
  165. */
  166. public function formEntity(
  167. Request $request,
  168. $hash = null,
  169. ?PosTicketView $tickets = null,
  170. ?TicketReturnService $returns = null,
  171. ?AdvancePaymentLedger $advances = null,
  172. ?RecordPanelReader $records = null
  173. ) {
  174. \assert($tickets instanceof PosTicketView);
  175. \assert($returns instanceof TicketReturnService);
  176. \assert($advances instanceof AdvancePaymentLedger);
  177. \assert($records instanceof RecordPanelReader);
  178. $options = $this->getBasicOptions();
  179. $editMode = !is_null($hash);
  180. $brRoot = $this->getBreadcrumbRootItem($options);
  181. $brRoot->addChildren(new BreadcrumbItem("sales.header." . ($editMode ? "edit" : "create")));
  182. /** @var Employee $user */
  183. $user = $this->getUser();
  184. $entity = null;
  185. $routes = $this->getEntityRoutes();
  186. /** @var SaleRepository $saleRepository */
  187. $saleRepository = $this->getEntityManager()->getRepository(Sale::class);
  188. /** @var TaxRepository $taxRepository */
  189. $taxRepository = $this->getEntityManager()->getRepository(Tax::class);
  190. /** @var FacilityRepository $facilityRepository */
  191. $facilityRepository = $this->getEntityManager()->getRepository(Facility::class);
  192. /** @var ProviderRepository $providerRepository */
  193. $providerRepository = $this->getEntityManager()->getRepository(Provider::class);
  194. /** @var SimpleBillMessageRepository $sBMRepository */
  195. $sBMRepository = $this->getEntityManager()->getRepository(SimpleBillMessage::class);
  196. if(!$editMode){
  197. $entity = new Sale($user);
  198. $maxIdYear = $saleRepository->getNumberSaleOnThisYear();
  199. $entity->setIdYear($maxIdYear + 1);
  200. } else {
  201. /** @var Sale $entity */
  202. $entity = $saleRepository->findByHash($hash);
  203. if(!$entity){
  204. $this->addFlash(self::FLASH_ERROR, self::ERROR_MSG_ID_NOT_FOUND);
  205. return $this->redirectToRoute($routes[self::ROUTE_KEY_INDEX]);
  206. }
  207. $promoCodeInSale = $entity->getPromoCodeApplied();
  208. if(!is_null($promoCodeInSale)){
  209. $promoCodeInSale->setLimit($entity->getTotalDiscounted());
  210. }
  211. }
  212. $options[self::TWIG_OPTIONS_ENTITY] = $entity;
  213. $options["simple_bill_messages"] = array(
  214. SimpleBillMessageType::HEADER => $sBMRepository->getByType(SimpleBillMessageType::HEADER),
  215. SimpleBillMessageType::BOTTOM => $sBMRepository->getByType(SimpleBillMessageType::BOTTOM),
  216. );
  217. // What the ticket prints: the copy the document was issued with, or the live messages when it has
  218. // none — a sale not billed yet, or a ticket issued before this version (change verifactu-02,
  219. // decision 22).
  220. $document = is_null($entity) ? null : $entity->getBillLinked();
  221. $keepsItsOwn = !is_null($document) && $document->hasPrintedMessages();
  222. $options["printed_header_messages"] = $keepsItsOwn
  223. ? Bill::printedMessageLines($document->getHeaderMessages())
  224. : self::messageLinesOf($options["simple_bill_messages"][SimpleBillMessageType::HEADER]);
  225. $options["printed_footer_messages"] = $keepsItsOwn
  226. ? Bill::printedMessageLines($document->getFooterMessages())
  227. : self::messageLinesOf($options["simple_bill_messages"][SimpleBillMessageType::BOTTOM]);
  228. // The paper itself, read off the document (change verifactu-05, design decision 7; R-27). A sale
  229. // being made has no document yet, and prints nothing.
  230. $options["ticket"] = is_null($document) ? null : $tickets->of($document);
  231. // Where the ticket of this sale stands with the AEAT, and every record behind that answer. It is
  232. // shown on the screen and never on the paper: what the customer was handed says what it said the day
  233. // it was handed over (change verifactu-10, design decisions 1 and 2).
  234. $options["record_status"] = is_null($document) ? null : $document->getRecordStatus();
  235. $options["record_panel"] = is_null($document)
  236. ? null
  237. : $records->of($document, $this->isGranted(Role::ROLE_ADMIN));
  238. // Whether this ticket can still be exchanged for an invoice with the customer's data, and which
  239. // exchange documents it when it cannot (design decision 9; D-11).
  240. /** @var \FMS\eLibreria\CoreBundle\Model\Repository\BillRepository $billRepository */
  241. $billRepository = $this->getEntityManager()->getRepository(Bill::class);
  242. $options["exchanged_in"] = is_null($document) ? null : $billRepository->exchangeInForceOf($document);
  243. $options["exchangeable"] = !is_null($document)
  244. && is_null($options["exchanged_in"])
  245. && \FMS\eLibreria\CoreBundle\Verifactu\Document\ValueObject\InvoiceType::F2 === $document->getInvoiceType()
  246. && !$document->isSimplifiedArt7273()
  247. && !$document->isPreVerifactu()
  248. && $document->isIssued();
  249. if (!is_null($document) && !$keepsItsOwn) {
  250. // A ticket issued before this version kept no copy of the messages, so it prints the live ones
  251. // exactly as it did before the upgrade (change 02, decision 22).
  252. $options["ticket"]["headerMessages"] = $options["printed_header_messages"];
  253. $options["ticket"]["footerMessages"] = $options["printed_footer_messages"];
  254. }
  255. // How the sale was paid: every payment with its amount, which is what the till wrote, never the one method
  256. // `sales.pay_method` keeps (change merch-09, task 0.9; M-94).
  257. $options["sale_payments"] = is_null($entity) || is_null($entity->getId())
  258. ? array()
  259. : $this->getEntityManager()->getRepository(SalePayment::class)->findBy(array("sale" => $entity), array("id" => "ASC"));
  260. // «Devolución»: what of this sale can still come back, what came back already, and whether this user may
  261. // take it back at all (change verifactu-06, design decision 7; D-18).
  262. $options["returnable_units"] = is_null($entity) || is_null($entity->getId())
  263. ? array()
  264. : $returns->returnableUnitsOf($entity);
  265. $options["sale_returns"] = is_null($entity) ? array() : $entity->getSaleReturns();
  266. $options["can_be_returned"] = !is_null($document)
  267. && $document->isIssued()
  268. && !$document->isAnnulled()
  269. && !$document->isAdvancePayment()
  270. && array_sum($options["returnable_units"]) > 0
  271. && $this->isGranted(DocumentAction::Return_->value, $document);
  272. $options["return_token"] = TicketReturnController::RETURN_TOKEN;
  273. $options["refund_methods"] = self::refundMethodsOf($document);
  274. // The account of the client the document names: what the shop owes them, which only they spend
  275. // (change verifactu-14, task 4.6).
  276. $client = is_null($document) ? null : $document->getClient();
  277. $options["client_account"] = is_null($client)
  278. ? null
  279. : array(
  280. "client" => $client,
  281. "balance" => $this->accounts()->balanceOf($client),
  282. );
  283. // «Cambiar por otro libro»: whatever may come back may be exchanged for other goods, and the difference is
  284. // paid with a method that needs nothing but its amount (change verifactu-14, design decisions 5 and 6).
  285. $options["can_exchange_goods"] = $options["can_be_returned"];
  286. $options["goods_exchange_token"] = GoodsExchangeController::EXCHANGE_TOKEN;
  287. $options["difference_methods"] = array_values(array_filter(
  288. PaymentMethod::cases(),
  289. static fn (PaymentMethod $method): bool => $method->isPaidWithAnAmountAlone()
  290. ));
  291. // «Anular reserva»: an advance document is the reservation, and what is left of it is what the ledger
  292. // says — never a column (change verifactu-06, design decision 9; change 03, decision 23; D-63).
  293. $options["advance_remaining"] = is_null($document) || !$document->isAdvancePayment() || !$document->isIssued()
  294. ? null
  295. : $advances->remainingOf($document)->total();
  296. $options["can_cancel_reservation"] = !is_null($options["advance_remaining"])
  297. && !is_null($document)
  298. && !$document->isAnnulled()
  299. && bccomp($options["advance_remaining"], "0.00", 2) > 0
  300. && $this->isGranted(
  301. (InvoiceType::F2 === $document->getInvoiceType()
  302. ? DocumentAction::Return_
  303. : DocumentAction::Correct)->value,
  304. $document
  305. );
  306. $options["cancel_reservation_token"] = TicketReturnController::CANCEL_TOKEN;
  307. $options["taxes"] = $taxRepository->findAllTaxesOrdered();
  308. // Never a system or a retired location (change merch-01, design decision 4).
  309. $options["locations"] = $this->getLocationChoices()->forFacility();
  310. $options["facilities"] = $facilityRepository->findAllFacilitiesOrdered();
  311. $options["providers"] = $providerRepository->findAll();
  312. $options["ajax_routes"] = array(
  313. "save" => $this->generateUrl("cms_utools_store_sales_ajax_save"),
  314. "autocomplete" => $this->generateUrl("cms_utools_store_sales_ajax_autocomplete"),
  315. "promo_code" => $this->generateUrl("cms_utools_store_sales_ajax_promo_code"),
  316. // What a line of the till holds while it is open (change merch-04, design decision 1; M-12).
  317. "till_hold" => $this->generateUrl("cms_utools_store_sales_ajax_till_hold"),
  318. "till_units" => $this->generateUrl("cms_utools_store_sales_ajax_till_units"),
  319. "till_renew" => $this->generateUrl("cms_utools_store_sales_ajax_till_renew"),
  320. "till_release" => $this->generateUrl("cms_utools_store_sales_ajax_till_release"),
  321. "till_release_all" => $this->generateUrl("cms_utools_store_sales_ajax_till_release_all"),
  322. // The till tells the server before it prints, so the delivery is written down and a document
  323. // that can still change is not handed over (change verifactu-02, decision 16).
  324. "print_notice" => $this->generateUrl("cms_utools_store_sales_ajax_print_notice"),
  325. // What the ticket would be worth if it were charged now. The till renders this and adds
  326. // nothing up of its own (change verifactu-03, design decision 15).
  327. "quote" => $this->generateUrl("cms_utools_store_sales_ajax_quote"),
  328. // Who the invoice is issued to, when the buyer asks for their data or the rules require it
  329. // (change verifactu-05, design decision 3). It is the search the bill editor already uses:
  330. // one place decides what a client is (§C5, change verifactu-04).
  331. "search_client" => $this->generateUrl("cms_utools_accounting_bills_ajax_search_fiscal_entity"),
  332. // The balance of the chosen client, which only that client spends (change verifactu-14, task 4.6).
  333. "client_balance" => $this->generateUrl("cms_utools_store_sales_ajax_client_balance"),
  334. // Money on account: charging an advance for what is on the screen, and reading what is left of
  335. // one the customer left earlier (change verifactu-05, design decision 15; D-63).
  336. "save_advance" => $this->generateUrl("cms_utools_store_sales_ajax_save_advance"),
  337. "advance" => $this->generateUrl("cms_utools_store_sales_ajax_advance"),
  338. );
  339. return $this->render("utools/store/sales/editor.html.twig", $options);
  340. }
  341. // The quick sale screen is gone with change verifactu-05 (design decision 13; D-48, X-21): it rendered
  342. // an editor that never created a sale, so nothing it showed was ever charged, printed or recorded.
  343. /**
  344. * The live messages of the installation as lines, in the order they are printed in.
  345. *
  346. * @param SimpleBillMessage[] $messages
  347. *
  348. * @return string[]
  349. */
  350. private static function messageLinesOf(array $messages) {
  351. $lines = array();
  352. foreach($messages as $message){
  353. $lines[] = (string) $message->getMessage();
  354. }
  355. return $lines;
  356. }
  357. /**
  358. * A sale is not deleted (change verifactu-05, design decision 6; D-09).
  359. *
  360. * The action that did it removed the sale, returned its units to stock and refused only when the sale
  361. * was linked to an invoice. Every sale is an invoice now, and one that was issued was declared to the
  362. * AEAT the moment it was: what is wrong with it is corrected by issuing another document (change 06),
  363. * never by taking the first one out of the database.
  364. */
  365. protected function isDeleteEnabled(){
  366. return false;
  367. }
  368. private function accounts(): ClientAccountLedger {
  369. /** @var ClientAccountLedger $accounts */
  370. $accounts = $this->container->get(ClientAccountLedger::class);
  371. return $accounts;
  372. }
  373. }