Compare commits

233 Commits

Author SHA1 Message Date
dfe0894305 feat: configure ALLOWED_HOSTS dynamically via environment variables in settings and docker-compose 2026-04-05 12:29:30 -03:00
df83dac830 feat: configure CSRF_TRUSTED_ORIGINS via environment variables for production domains 2026-04-05 12:20:40 -03:00
aec59990a7 fix: update STATIC_URL to use an absolute path for correct static file resolution 2026-04-05 12:07:06 -03:00
47821c4f71 refactor: update docker volume mapping, add collectstatic to startup command, and normalize settings.py formatting 2026-04-05 11:40:54 -03:00
405033d1bf chore: update container configuration, enable debug mode, and refine deployment command in docker-compose 2026-04-05 11:20:51 -03:00
19c6fe2ad8 chore: update project dependencies in requirements.txt 2026-04-05 10:26:03 -03:00
b73ba1ef10 refactor: clean up product models, remove deployment files, and add media static serving configuration 2026-04-05 10:09:37 -03:00
6c4e95e579 feat: add user management viewset and sync module to API routes 2026-04-04 17:44:05 -03:00
645a5b4093 deploy 2026-02-25 17:20:00 -03:00
2fc4fafed7 chore: Delete numerous application modules, templates, static assets, documentation, and build files. 2026-02-25 17:09:27 -03:00
7ddaa2d1f9 refactor: Update project settings, URL configurations, client views, and remove requirements.txt. 2026-02-23 18:44:06 -03:00
Welton Moura
2160998c23 Merge pull request #6 from welton89/dev_home
Dev home
2025-08-16 17:42:53 -03:00
4ccd77466e troca de db para pr 2025-08-09 16:01:59 -03:00
37428e8cc7 feat: criado funcções de movimentação de estoque 2025-07-22 18:22:41 -03:00
a9289cb28d feat: criado tabela no db (UnitOfMeasure, ProductComponent, StockMovementType, StockMovement) add UnitOfMeasure na tabela produtos 2025-07-22 15:39:37 -03:00
b74736f391 style: toast ao add product na comanda 2025-07-19 18:00:22 -03:00
f191b6a14b feats: ordenar tabela produtos clicando no cabeçalho | fundo red para quantidade menor de 20 na tabela produto 2025-07-19 13:29:55 -03:00
a20fa6d583 feat: gerenciamento quantidade product 2025-07-18 18:24:21 -03:00
Welton Moura
3ad690345a Merge pull request #5 from welton89/dev_home
Dev home #5
2025-07-07 14:35:34 -03:00
672af857a0 s 2025-07-07 14:34:17 -03:00
760d9db57c s 2025-07-07 14:32:47 -03:00
bb55dfdf77 s 2025-07-07 14:30:01 -03:00
ba93424398 s 2025-07-07 14:27:26 -03:00
92ebad35fe s 2025-07-07 14:26:29 -03:00
6310e91659 s 2025-07-07 14:25:42 -03:00
853b62a6cd remove db dev 2025-07-07 14:19:03 -03:00
ee75321b97 não lembro 2025-07-07 13:56:12 -03:00
7355d546cd criado metodo de mais vendidos no comandas.models | limitar lista de products a 15 itens 2025-07-07 10:35:29 -03:00
69a9e9e065 criado endpoint para gerar um json do cardapio 2025-07-05 21:08:38 -03:00
0b8614e684 ordernação dos produtos por ativo e categoria na page products 2025-07-05 20:13:36 -03:00
eb76e91066 image background card product 2025-07-05 19:52:15 -03:00
b969ea6663 add url image product 2025-07-05 16:21:56 -03:00
Welton Moura
5ccd1d84ab Atualizar o settings.py 2025-07-02 18:26:32 -03:00
Welton Moura
d74ed0a27c Atualizar o docker-compose.yml 2025-07-02 18:23:02 -03:00
Welton Moura
671fc5d34b Atualizar o docker-compose.yml 2025-07-02 17:45:40 -03:00
Welton Moura
a81916925f Atualizar o Dockerfile 2025-07-02 16:44:39 -03:00
Welton Moura
9bf2a69da1 Atualizar o Dockerfile 2025-07-02 15:05:48 -03:00
Welton Moura
86129037fb Atualizar o Dockerfile 2025-07-02 14:30:03 -03:00
Welton Moura
57c833fd66 Atualizar o Dockerfile 2025-07-02 14:24:40 -03:00
Welton Moura
b392a50774 Atualizar o Dockerfile 2025-07-02 14:22:57 -03:00
Welton Moura
c9def2fe2b Atualizar o Dockerfile 2025-07-02 14:13:57 -03:00
Welton Moura
45395ec1e2 Atualizar o Dockerfile 2025-07-02 14:02:45 -03:00
Welton Moura
f43029712b alterar dB para produção 2025-07-02 13:57:26 -03:00
Welton Moura
03ee9caaf6 Merge pull request #4 from welton89/dev_home
fixed: timezone de orders alterado para exibição utc -3
2025-07-02 13:28:09 -03:00
7c6586bbce fixed: timezone de orders alterado para exibição utc -3 2025-07-02 10:29:16 -03:00
09c5fd1e98 fixed: onclick apagar item da comanda removido por acidente 2025-07-01 12:36:36 -03:00
fe00c4da90 refactor: remoção do htmx do excluir item da comanda | feedback da exclusão 2025-07-01 12:28:47 -03:00
377fac4f1c refactor:alteração modal de add product na comanda 2025-06-30 18:08:37 -03:00
7dd76db5ba refactor:alteração modal de obs do pedido na tela viewcomanda 2025-06-28 18:54:44 -03:00
8cb63832e5 refactor: alteração na modal de confirmação para encerrar a comanda 2025-06-28 13:05:38 -03:00
c8c3653bf8 feat: abre modal de informações do status da order ao clicar sobre o nome do product na viewcomanda 2025-06-27 17:14:08 -03:00
1ef263d033 style: size navbar desktop 2025-06-27 10:40:27 -03:00
f232090577 style: ajustes menu mobile 2025-06-26 19:01:02 -03:00
0911483e62 style: remodelagem menu mobile 2025-06-26 13:19:27 -03:00
52ccb7801b framework css cdn 2025-06-26 11:52:19 -03:00
eaa7d42826 update: comandos.txt 2025-06-25 12:49:34 -03:00
a7cbf2da09 fix: api pagamento multipla comandas com dominio 2025-06-25 12:41:25 -03:00
d7013f0ab8 fix: focus input addProduct & feedback addObsOrder & openComanda default 'Sem Mesa' 2025-06-25 12:06:59 -03:00
b73e134d9e feedback pagamento de multiplas comandas 2025-06-25 11:18:39 -03:00
179342ff80 fix: conexão com api de pagamento de multiplas comandas ok 2025-06-22 17:19:18 -03:00
0e7c7d7c68 fix: add duplicati in docker-compose 2025-06-19 17:40:49 -03:00
a099952548 gggg 2025-06-17 18:36:33 -03:00
d6a3b58f89 sem paciencia 2025-06-17 18:35:22 -03:00
40951892fe ffff 2025-06-17 18:30:19 -03:00
782ea2bac5 remove duplicati 2025-06-17 18:29:01 -03:00
a476000c48 versao duplicati 2025-06-17 18:25:04 -03:00
b5a27884cd bug: ja sem idea 2025-06-17 18:21:25 -03:00
998c489dc3 bug:docker-compose duplicati 2025-06-17 18:15:24 -03:00
be907bfcd5 bub: docker-compose 2025-06-17 18:12:21 -03:00
132c8dcfc6 page orders | add duplicati container 2025-06-17 17:49:44 -03:00
de0f6912e2 doc: link test 2025-06-06 14:13:04 -03:00
99d17db5e6 doc: first docs 2025-06-06 13:59:52 -03:00
da59da79ae readme: imagens 2025-05-29 12:45:37 -03:00
7a6b69e5f6 readme: alteração links imagens 2025-05-29 12:25:16 -03:00
2cc2795b03 readme 2025-05-29 12:15:31 -03:00
bfa293e062 readme: test 2025-05-29 12:13:56 -03:00
d739f0c39b alter: alteração alleoewd_host 2025-04-29 15:51:47 -03:00
4c57a356b1 bug: envio msg para servidor wesocket 2025-04-27 14:36:18 -03:00
e33f9e2974 docker: add container websocket in docker-compose 2025-04-27 14:14:32 -03:00
d5d136fc5d test3 2025-04-26 20:08:15 -03:00
ec11ae7f7a test2 2025-04-26 20:06:01 -03:00
abe61440e5 test 2025-04-26 20:01:39 -03:00
3fe2ad1044 Test: update recreate image 2025-04-26 18:28:05 -03:00
0d97208489 Docker fix: environment duplicado 2025-04-26 18:10:29 -03:00
4d128290d6 Docker: variavel de ambiente do app 2025-04-26 18:08:02 -03:00
c3f0861255 Docker: pull file repository github 2025-04-26 17:24:25 -03:00
e13db8d469 docker: healthcheck 2025-04-19 20:29:40 -03:00
c1d88b586a Docker: Fix: conexão com db (volume estava sendo criado antes das credenciais) 2025-04-16 10:55:47 -03:00
81e6d3f8df docker: bug conexão com db 2025-04-15 11:04:05 -03:00
0a47948aea docker: implementanto postgresql no docker-compose 2025-04-14 17:02:47 -03:00
44cfa49455 docker: criado docker-compose com o django_app e o nginx_server como proxy reverso e servindo statics files em um volume compartilhado 2025-04-13 22:00:19 -03:00
3a3c804b3c Fix: erro ortofrafico 2025-04-09 14:06:19 -03:00
1f64d5ecf8 readMe: add mais iamagens 2025-04-09 13:58:48 -03:00
5617a68873 test(comandas): first test. views -> createComanda 2025-04-08 16:17:42 -03:00
2f03c0e175 new ambiente dev 2025-04-08 10:33:40 -03:00
5b167f0ed4 First Dockerfile 2025-03-29 19:35:07 -03:00
b35fc39a60 style e verificaçao: display flex para os botoes da viewComandas | verificação para apagar produto da comanda 2025-03-28 14:28:35 -03:00
ac2a0283d0 alter: alterado metodo http para fechar comanda | retirada do htmx 2025-03-28 14:07:30 -03:00
bb51147d99 alter: alteracao metodo POST osbOrder 2025-03-28 13:20:20 -03:00
8d31a83b13 fix: bug pagamento parcial. campo requerido(valor) 2025-03-26 20:16:22 -03:00
ef24e3f3bc fix: bug ao add product in comanda 2025-03-26 19:50:03 -03:00
a2c1c2bcf0 formatação 2025-03-25 20:11:13 -03:00
add06a2a67 fix: bug retorno do obs da order no tootip do viewcomanda 2025-03-25 19:08:00 -03:00
cac46a3e52 DELETE: env do python 2025-03-25 18:01:50 -03:00
0c4101d0c4 fix: comanda balcao repetindo 2025-03-21 17:07:16 -03:00
51a7743d47 Feat: instacia do balcão diferente para cada usuario 2025-03-21 17:01:27 -03:00
53d156b919 feat: obs from orders print 2025-03-21 16:34:14 -03:00
725e566d74 feat: botão imprimir pedido cozinha 2025-03-20 17:02:28 -03:00
6d9693bc26 ajustes 2025-03-06 11:00:28 -03:00
9599b31d69 new menubar mobile 2025-03-05 15:13:56 -03:00
d366d2db97 websocket part2 | notificação 2025-03-04 22:17:30 -03:00
3a7cbfc413 feat: websocket part1 (estudo) 2025-03-03 21:23:35 -03:00
76d2d4a879 style: toast 2025-02-22 18:41:49 -03:00
d15c58f4e7 bug: toast 2025-02-21 21:45:18 -03:00
8a58d0a0db feat: toast 2025-02-21 21:39:08 -03:00
baafa4cdec alterar modal dialog para popover em addProduct 2025-02-21 16:59:34 -03:00
fdb9d9e453 feat: taxa de serviço conticionado a configuração (que ainda não existe rs) 2025-02-21 10:33:03 -03:00
46f0900763 feat: valor total divida cliente 2025-02-19 17:08:11 -03:00
d2ab8212b9 feat: acossiar comandas fiados a conta client 2025-02-19 16:49:24 -03:00
456fba3d6a readme 2025-02-19 11:23:39 -03:00
f0c956d2ba redirect quando cria uma comanda 2025-02-18 22:32:41 -03:00
44a9c694dc feat: taxa de serviço part2 2025-02-18 16:11:27 -03:00
aac7f57fbe feat: taxa de serviso part1 2025-02-17 17:10:43 -03:00
e6beb6f4b5 feat: pagamento parcial comanda part2 2025-02-15 17:14:14 -03:00
a86bd2fb45 feat: pagamento parcial comanda part1 2025-02-15 15:38:43 -03:00
ecbeba02e8 remoove navbar prototipo 2025-02-11 11:09:15 -03:00
2da09a8a25 nem sei pq tantos arquivos 2025-02-11 11:07:58 -03:00
66fb4eb17b iniciado pwa. mas to sem tempo pra continuar agora 2025-01-30 11:00:49 -03:00
cc375d5d29 style 2025-01-29 18:31:26 -03:00
a0a898caee style: botton menubar mobile 2025-01-29 15:46:30 -03:00
0534b896ad feat: page mesas_map, popover com lista de comandas associadas com nome, total, e link para abrir a comanda. mesas livre cor diferente e não cria popover. 2025-01-28 20:57:53 -03:00
c8119609ea Style: nem lembro mais 2025-01-28 16:51:15 -03:00
7ccc505299 feat: speek funcionando no android atraves do firefox 2025-01-28 10:44:20 -03:00
7005796e7a style: buttons tab page orders 2025-01-27 17:01:57 -03:00
2b838cda4f feat: speek orders page comanda 2025-01-27 11:01:29 -03:00
1a83896449 update: comanda.js 2025-01-25 19:30:14 -03:00
56ca14e1cb feat: ssl pra debug 2025-01-25 13:39:50 -03:00
412eb763db feat: notificação garçom 2025-01-24 21:09:52 -03:00
1747305c05 feat: notificação de pedidos na cozinha 2025-01-24 17:00:55 -03:00
2d0cbae050 feat: estudo sobre notificação 2025-01-23 17:11:37 -03:00
de3f90f83b style: consistencia 2025-01-23 13:43:16 -03:00
eb424f90ff style: tooltip mesas_map 2025-01-23 08:30:41 -03:00
80aebd2030 bug: page orders resolvido 2025-01-22 19:08:34 -03:00
04e78a0f79 bug: criei um bug na page orders🙃 2025-01-22 16:59:26 -03:00
1d96e8791e style: page orders 2025-01-22 14:25:59 -03:00
155cf44fec style: padronixação modal | tooltips 2025-01-22 08:45:56 -03:00
891f23ec2e style: cores 2025-01-21 17:29:43 -03:00
88373b4570 readme 2025-01-20 21:29:19 -03:00
c06b8c5608 readme 2025-01-20 19:30:44 -03:00
6673fac076 style: label grafico vendas 2025-01-20 17:07:12 -03:00
3ef4aefaa0 bug: htmx page viewcomanda 2025-01-20 15:54:44 -03:00
4df3c6e8a8 style: paleta de cores 2025-01-20 15:32:05 -03:00
203c7345fb style: page home 2025-01-20 10:57:39 -03:00
dfc5072142 style: page home 2025-01-19 18:49:44 -03:00
6b7bd1bbb2 style: page home and page map 2025-01-18 21:17:13 -03:00
89a4d0e4be feat: grafico mais vendido dinamico com filtro de periodo 2025-01-17 22:14:51 -03:00
c7bc44aa54 feat: filter date in charts 2025-01-17 15:58:57 -03:00
1999701dcf bug: add product balcao 2025-01-16 17:00:05 -03:00
b403e5fc72 feat: add valor comanda a conta do cliente 2025-01-16 16:31:13 -03:00
cf73dba143 style: icons svg 2025-01-16 15:16:31 -03:00
137f5510f8 style: page login 2025-01-16 11:09:44 -03:00
966b5b2cf3 feat: editClient 2025-01-15 21:28:59 -03:00
15af99f765 feat: page clients and create client 2025-01-15 20:48:35 -03:00
dc21858b9a update: readme 2025-01-15 17:08:56 -03:00
c19b6e5fca feat: edit comanda 2025-01-15 16:49:41 -03:00
0d6a164872 bug: relacionamento id mesa com comanda resolvido 2025-01-15 14:04:04 -03:00
2ceae1c0e9 add permissoes as views 2025-01-15 13:58:45 -03:00
8a13a8806a bug: add product balcao resolved 2025-01-15 13:42:15 -03:00
43d9cd9c5b fiz um monte de coisa e criei um bug 2025-01-15 11:04:27 -03:00
a2fd5d12e3 feat: seta down do dropdown girar no hover 2025-01-14 21:34:08 -03:00
14d04eb280 url admin no menu mobile 2025-01-14 19:32:32 -03:00
aff5fe24d8 url admin na navbar 2025-01-14 19:31:16 -03:00
b06e1d22e3 feat: logout | testes permissoes | redirecionamento com base no gupo do usuario 2025-01-14 16:53:07 -03:00
9b4e1cfca4 feat: login 2025-01-14 15:44:50 -03:00
3b006e7a63 feat: page login 2025-01-14 11:02:56 -03:00
5040744c8a feat: decorator group_required 2025-01-13 22:18:28 -03:00
9961d080b6 testes com autenticação 2025-01-13 19:45:22 -03:00
3dae764fc8 feat: menu dropdown in navbar 2025-01-13 16:52:34 -03:00
956841ef2a editor product cuisine 2025-01-13 15:15:42 -03:00
c7687f7b9a grafico cozinha dinamico 2025-01-13 10:34:16 -03:00
7b01316834 bug: delay na manipulação das mesas no mapa 2025-01-11 19:21:26 -03:00
dd158ff7b6 style: layout print conta 2025-01-11 12:54:12 -03:00
052bfb7fb4 feat: add obs order 2025-01-10 22:40:57 -03:00
5c1188ecde feat: mudança de etapa do pedido da cozinha 2025-01-10 20:42:14 -03:00
bde7014717 style: orden itens navbar 2025-01-10 16:48:11 -03:00
b84fd3bb69 feat: create tabs page orders 2025-01-10 16:43:22 -03:00
0e096207a6 feat: create app orders and makemigrations 2025-01-10 14:01:14 -03:00
faaf0d3576 feat: chart in page home 2025-01-09 16:57:29 -03:00
1011624f03 page products mais responsivo 2025-01-09 15:48:56 -03:00
d874b94c16 style | clean 2025-01-08 19:43:53 -03:00
57f6f3a7fa feat: calcular troco com base no valor recebido 2025-01-08 16:50:50 -03:00
a41960dcd5 feat: first filter custom | valor total da comanda no card 2025-01-08 14:39:52 -03:00
1b5b02be0d update redme 2025-01-08 10:37:38 -03:00
6633110140 ordem add produtos na comanda 2025-01-07 20:00:06 -03:00
b2641691fe kkkk 2025-01-07 19:32:16 -03:00
d5cba23d4d home resposive mobile 2025-01-07 19:27:52 -03:00
c0e93980ac style: alteração icones e fixação barra nav 2025-01-07 19:16:29 -03:00
3a6d214356 feat: edit product 2025-01-07 17:03:38 -03:00
03192adf94 feat: buscar reativa em products 2025-01-07 14:51:18 -03:00
a761bd2db2 update db | settings ips 2025-01-06 16:55:20 -03:00
7ab0e99e6b feat: alert product add comanda 2025-01-05 18:15:18 -03:00
26af988846 page comanda e barra navegação resposivo 2025-01-05 14:23:44 -03:00
f11686dbdb bug: largura impressão fichas 2025-01-04 19:32:33 -03:00
3b4a871166 sempre retorna o focus para input search em page balcao 2025-01-04 19:28:06 -03:00
8de4f74aeb feat: filter products active 2025-01-04 18:32:42 -03:00
0ac4609ce6 bug: search products balcao 2025-01-04 18:06:05 -03:00
0460040af3 layout base | atualizar page se erro ao arrastar 2025-01-03 16:25:28 -03:00
a2d751e88f feat: arrastar mesas tambem para depósito 2025-01-03 15:58:44 -03:00
42f3e02b0d refactor: update method saveLocal for 'POST' 2025-01-03 15:25:23 -03:00
998fdaa516 db 2025-01-03 11:09:27 -03:00
4a29a85b55 imagem planta baixa inserido 2025-01-02 16:42:31 -03:00
f8d1373454 feat: save location mesa_map 2025-01-02 15:17:06 -03:00
79409843d0 matriz do mapa de mesas 2024-12-31 11:43:32 -03:00
7698cfae47 feat: mapa de mesas 2024-12-30 16:51:07 -03:00
eb55f60f7e feat: ticket medio na home 2024-12-30 15:33:38 -03:00
31c0fe5daa modal criar produto usado para editar produto tambem 2024-12-30 15:04:25 -03:00
c9f7b33fde feat: edit product 2024-12-30 10:46:16 -03:00
9cc0c72f10 feat: button on/off products 2024-12-29 20:02:51 -03:00
ac87c3a47d atualizar valor todal modal receber 2024-12-27 18:26:56 -03:00
356d45c3e5 removeProductBalcao trocado htmx por ajax 2024-12-27 17:17:17 -03:00
1beb2e00b2 trocado addProduct de htmx por ajax 2024-12-27 17:07:23 -03:00
a1866c842c trocando pesquisa reativo de htmx para ajax 2024-12-27 15:19:51 -03:00
e7212ab688 pge balcao produtos ordenados por mais vendidos | qtd funcionando 2024-12-26 19:38:32 -03:00
df0d652a36 nem sei o que fiz 2024-12-26 16:14:01 -03:00
a9896730eb contabilizar produtos vendidos no balcão 2024-12-26 10:37:05 -03:00
63c9859557 captura de dados pra dashbooad 2024-12-23 17:00:15 -03:00
a21b038868 ambiente dev 2024-12-23 12:20:41 -03:00
9d1d832045 ADD mensage imprimir ficha 2024-12-21 14:51:35 -03:00
8c41336029 feat: create home page 2024-12-21 13:58:35 -03:00
c5cd92a9c7 update: page balcao 2024-12-21 12:17:10 -03:00
0ef1c0a61b feat: create page balcao 2024-12-21 11:43:09 -03:00
59e4eb6039 commit depois do merge 2024-12-21 10:24:44 -03:00
Welton Moura
978decb3c0 Merge pull request #1 from Pindoba/dev_2
Dev 2
2024-12-21 10:11:37 -03:00
7312 changed files with 4851 additions and 882598 deletions

18
.gitignore vendored Normal file
View File

@@ -0,0 +1,18 @@
.venv/
__pycache__/
*.py[cod]
*$py.class
.env
db.sqlite3
media/
static/
.python-version
uv.lock
pyproject.toml
GUIA_POSTMAN_API.md
brain/
logs/
walkthrough.md
task.md
implementation_plan*.md
*.webp

111
DOCUMENTACAO_PROJETO.md Normal file
View File

@@ -0,0 +1,111 @@
# Documentação do Projeto RRBEC (Gestão Raul)
Esta documentação fornece uma visão geral técnica e funcional do sistema **Gestão Raul**, abrangendo sua estrutura, regras de negócio, modelagem de banco de dados, APIs e mecanismos de autenticação.
---
## 1. Estrutura do Projeto
O projeto é baseado no framework **Django** (Python) e segue o padrão de arquitetura de aplicações modulares.
### Diretórios Principais
- `gestaoRaul/`: Configurações centrais do Django (settings, urls, wsgi).
- `products/`: Gestão de produtos, categorias e unidades de medida.
- `clients/`: Cadastro de clientes e controle de débitos ("Fiados").
- `mesas/`: Gestão das mesas físicas do estabelecimento.
- `comandas/`: Núcleo do sistema. Gerencia o consumo, estoque e fluxo de vendas.
- `orders/`: Controle da fila de produção (cozinha/bar).
- `payments/`: Registros de pagamentos e cálculos de taxas.
- `typePay/`: Tipos de pagamentos aceitos (Crédito, Débito, Pix, Fiado, etc.).
- `login/`: Lógica de autenticação e customização de tokens JWT.
- `templates/`: Arquivos HTML para a interface web (utiliza Vanilla CSS e JS).
- `media/`: Armazenamento de imagens de produtos.
---
## 2. Regras de Negócio
O sistema foi projetado para gerenciar o fluxo operacional de um restaurante/bar de ponta a ponta.
### Fluxo de Comanda e Vendas
1. **Abertura**: Uma comanda é vinculada a uma mesa e, opcionalmente, a um cliente. O status inicial é `OPEN`.
2. **Lançamento de Itens**: Ao adicionar um produto a uma comanda, o sistema automaticamente:
- Deduz a quantidade do produto do estoque.
- Se o produto for composto (combo/receita), deduz os componentes individuais.
- Se o produto tiver a flag `cuisine=True`, gera um pedido (`Order`) na fila da cozinha.
3. **Gestão de Estoque**: Todas as entradas e saídas são registradas na tabela de `StockMovement` para auditoria.
4. **Fechamento**: Ao realizar o pagamento (total ou parcial), a comanda pode ser encerrada (`CLOSED`).
5. **Taxa de Serviço**: O sistema calcula automaticamente uma taxa de 10% sobre o consumo total.
### Fila de Produção (Cozinha)
Os pedidos passam por estados cronometrados:
- `Queue` (Em espera) -> `Preparing` (Preparando) -> `Finished` (Pronto) -> `Delivered` (Entregue).
- Também é possível cancelar um pedido, o que gera um estorno automático para o estoque.
### Sistema de "Fiado" (Débitos de Clientes)
- Clientes podem ter comandas marcadas com o tipo de pagamento "FIADO".
- O sistema mantém um registro do débito acumulado do cliente.
- Existem endpoints específicos para listar e quitar múltiplos débitos de uma vez.
---
## 3. Modelagem do Banco de Dados
O banco de dados utiliza a seguinte estrutura relacional (simplificada):
### Principais Entidades
- **Mesa**: `id`, `name`, `active`.
- **Client**: `id`, `name`, `debt`, `contact`, `active`.
- **Product**: `id`, `name`, `price`, `quantity`, `category_id`, `cuisine` (boolean), `active`.
- **ProductComponent**: (Tabela intermediária de composição) Liga um `Product` a outros `Products` com uma `quantity_required`.
- **Comanda**: `id`, `mesa_id`, `client_id`, `user_id`, `status` (OPEN/CLOSED/FIADO), `dt_open`, `dt_close`.
- **ProductComanda**: `id`, `comanda_id`, `product_id`, `data_time`, `applicant`.
- **Order (Pedido)**: `id`, `productComanda_id`, `obs`, `queue`, `preparing`, `finished`, `delivered`, `canceled`.
- **Payments**: `id`, `comanda_id`, `type_pay_id`, `value`, `client_id`, `datetime`.
---
## 4. API e Autenticação
A API é construída com **Django REST Framework (DRF)** e segue os princípios REST.
### Autenticação
O sistema utiliza **JWT (JSON Web Token)** para proteger os endpoints.
1. **Obter Token**: `POST /api/v1/token/`
- Body: `{"username": "...", "password": "..."}`
- Retorno: `access` e `refresh` tokens, além de dados do usuário (ID, nome, grupos).
2. **Atualizar Token**: `POST /api/v1/token/refresh/`
- Body: `{"refresh": "..."}`
3. **Uso**: Deve-se enviar o token no cabeçalho: `Authorization: Bearer <seu_token>`.
### Endpoints Principais (`/api/v1/`)
| Endpoint | Método | Descrição |
| :--- | :--- | :--- |
| `comandas/` | GET/POST | Lista ou cria novas comandas. |
| `comandas/{id}/pagar/` | POST | Registra pagamento e fecha a comanda. |
| `comandas/{id}/apagar/` | POST | Limpa todos os itens e fecha a comanda (estorna estoque). |
| `items-comanda/` | POST/DELETE | Adiciona ou remove itens de uma comanda individualmente. |
| `orders/` | GET/PATCH | Lista pedidos da cozinha ou edita observações. |
| `orders/{id}/preparing/` | POST | Inicia o preparo do pedido. |
| `orders/{id}/finish/` | POST | Marca como pronto para entrega. |
| `clients/{id}/fiados/` | GET | Lista todas as comandas pendentes (fiado) de um cliente. |
| `clients/pagar_fiados/`| POST | Quita uma lista de IDs de comandas fiadas. |
| `products/` | GET | Lista produtos ativos e estoque. |
### Lógica de Negócio Integrada na API
- **POST `items-comanda/`**: Ao postar um item, a API automaticamente executa `StockMovement.subTransactionStock` e cria um `Order` se necessário.
- **DELETE `items-comanda/{id}/`**: Ao deletar, a API executa `StockMovement.sumTransactionStock` para devolver o item ao estoque.
---
## 5. Tecnologias Utilizadas
- **Backend**: Django 5.1, Django REST Framework, SimpleJWT.
- **Frontend**: HTML5, Vanilla CSS, JavaScript.
- **Service Worker**: Configurado para suporte a PWA (Progressive Web App).
- **Banco de Dados**: SQLite (Desenvolvimento) / PostgreSQL ou MySQL (Produção).
- **Middleware**: WhiteNoise (arquivos estáticos), CORS Headers.
---
*Atualizado em: 24 de Março de 2026*

21
Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM python:3
RUN git clone https://github.com/welton89/RRBEC.git
RUN cd RRBEC
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN python gestaoRaul/manage.py collectstatic --noinput
# RUN python gestaoRaul/manage.py migrate --noinput
WORKDIR /app/gestaoRaul
CMD [ "gunicorn", "-w", "4", "--timeout", "15", "gestaoRaul.wsgi:application", "--bind", "0.0.0.0:8000" ]

View File

@@ -1,94 +0,0 @@
# RRBEC - Gestão de Bares e Restaurantes
## Sobre o Projeto
Este projeto é uma aplicação web desenvolvida em Django com o objetivo de explorar as funcionalidades e recursos desse framework. A aplicação visa simular um sistema de gestão para bares e restaurantes, abrangendo desde o cadastro de produtos e clientes até a geração de relatórios de vendas.
## Requisitos Funcionais
### Módulo de Produtos
* [x] Cadastrar novos produtos.
* [ ] Editar informações de produtos existentes.
* [ ] Ativar/Desativar produtos.
* [ ] Pesquisar produtos por nome.
* [ ] Gerenciar o estoque de cada produto.
### Módulo de Comandas
* [x] Abrir nova comanda(inserindo nome, associando ou não a mesa).
* [ ] Editar informações da comanda.
* [x] Adicionar produtos na comanda.
* [x] Remover produtos da comanda.
* [ ] Imprimir cupom de pagamento.
* [x] Imprimir fichas dos produtos.
* [x] Fechamento da comanda.
* [ ] Receber pagamento
### Módulo de Mesa
* [ ] Gerenciar mesas (ocupação, reserva).
* [x] Associar pedidos e comandas a mesas.
* [ ] Dividir contas.
### Módulo de Clientes
* [ ] Cadastrar novos clientes (nome, endereço, telefone, email).
* [ ] Editar informações de clientes existentes.
* [ ] Excluir clientes.
* [ ] Consultar o histórico de pedidos de um cliente.
### Módulo de Pedidos
* [ ] Realizar novos pedidos (produtos, quantidade, cliente).
* [ ] Editar pedidos (adicionar/remover itens, alterar quantidade).
* [ ] Cancelar pedidos.
* [ ] Consultar o status de um pedido (em aberto, em preparo, entregue).
* [ ] Gerar nota fiscal para pedidos finalizados.
### Módulo de Funcionários
* [ ] Cadastrar novos funcionários (nome, cargo, salário, data de admissão).
* [ ] Editar informações de funcionários existentes.
* [ ] Excluir funcionários.
* [ ] Gerenciar as permissões de cada funcionário (acesso a módulos, funções).
### Módulo de Relatórios
* [ ] Gerar relatório de vendas por período (diário, semanal, mensal).
* [ ] Gerar relatório de estoque (produtos em falta, produtos com alta rotatividade).
* [ ] Gerar relatório de clientes (mais ativos, menos ativos).
* [ ] Gerar relatório de funcionários (horas trabalhadas, faltas).
### Módulo de Pagamentos
* [ ] Integrar com gateways de pagamento (cartão de crédito, débito, dinheiro).
* [x] Gerenciar formas de pagamento.
* [ ] Emitir notas fiscais eletrônicas.
### Módulo de Delivery (opcional para restaurantes)
* [ ] Cadastrar entregadores.
* [ ] Gerenciar rotas de entrega.
* [ ] Acompanhar pedidos em tempo real.
### Módulo de Sistema
* [ ] Gerenciar usuários do sistema (login, senha, permissões).
* [ ] Realizar backups do sistema.
## Tecnologias Utilizadas
* **Django:** Framework Python para desenvolvimento web.
* **Python:** Linguagem de programação principal do projeto.
* **HTML:** Linguagem de marcação para disponibilizar os elementos na pagina.
* **HTMX:** Biblioteca para deixar a pagina mais dinâmica, reduzindo a necessidade do js.
* **CSS:** Linguagem para estilizar a interface do usuário.
* **JavaScript:** Linguagem que executa a lógica da pagina.
## Como Executar o Projeto
1. **Clonar o repositório:**
```bash
git clone https://github.com/Pindoba/RRBEC.git
2. **Criar um ambiente virtual:**
```bash
python -m venv [nome da sua preferencia]
source venv/bin/activate
3. **Instalar as dependências:**
```bash
pip install -r requirements.txt
4. **Executar as migrations:**
```bash
python manage.py migrate
5. **Iniciar o servidor de desenvolvimento:**
```bash
python manage.py runserver

View File

@@ -1,6 +1,6 @@
from django.apps import AppConfig from django.apps import AppConfig
class {{ camel_case_app_name }}Config(AppConfig): class ComandasConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = '{{ app_name }}' name = 'balcao'

125
balcao/htmx_views.py Normal file
View File

@@ -0,0 +1,125 @@
from django.shortcuts import render, redirect
from django.views.decorators.csrf import csrf_exempt
from django.db.models import Count, F
from django.contrib.auth.models import User
from comandas.models import Comanda, ProductComanda, StockMovementType, StockMovement
from mesas.models import Mesa
from products.models import Product
from payments.models import Payments
from typePay.models import TypePay
from gestaoRaul.decorators import group_required
def listProductBalcao(request, comanda_id, search_product):
comanda_id = request.GET.get("id-comanda-balcao")
if search_product == '*':
products_ordenados = ProductComanda.maisVendidos()
return render(request, "htmx_components/htmx_list_products_balcao.html", {"products": products_ordenados,'comanda_id':comanda_id})
else:
product = search_product
products = Product.objects.filter(name__icontains=product, active=True)
return render(request, "htmx_components/htmx_list_products_balcao.html", {"products": products[:15],'comanda_id':comanda_id})
@group_required(groupName='Garçom')
def addProductBalcao(request, product_id, comanda_id, qtd):
for i in range(qtd):
product_comanda = ProductComanda(comanda_id=comanda_id, product_id=product_id)
product_comanda.save()
product = Product.objects.get(id=product_id)
comanda = Comanda.objects.get(id=comanda_id)
user = User.objects.get(id=request.user.id)
typeMovement = StockMovementType.objects.get(name="Venda - Balcao")
StockMovement.subTransactionStock(
product=product,
movement_type=typeMovement,
comanda=comanda,
user=user,
qtd=1,
obs= "Vendido no balcão"
)
consumo = ProductComanda.objects.filter(comanda=comanda_id)
total = 0
for produto in consumo:
total += produto.product.price
return render(request, "htmx_components/htmx_list_products_in_balcao.html",{'consumo': consumo, 'total': total})
@group_required(groupName='Garçom')
@csrf_exempt
def addProductBalcaoTeclado(request, product_id, comanda_id, qtd):
for i in range(qtd):
product_comanda = ProductComanda(comanda_id=comanda_id, product_id=product_id)
product_comanda.save()
product = Product.objects.get(id=product_id)
comanda = Comanda.objects.get(id=comanda_id)
user = User.objects.get(id=request.user.id)
typeMovement = StockMovementType.objects.get(name="Venda - Balcao")
StockMovement.subTransactionStock(
product=product,
movement_type=typeMovement,
comanda=comanda,
user=user,
qtd=1,
obs= "Vendido no balcão"
)
consumo = ProductComanda.objects.filter(comanda=comanda_id)
total = 0
for produto in consumo:
total += produto.product.price
return render(request, "htmx_components/htmx_list_products_in_balcao.html",{'consumo': consumo, 'total': total})
@group_required(groupName='Garçom')
def removeProductBalcao(request, productComanda_id):
product_comanda = ProductComanda.objects.get(id=productComanda_id)
comanda = product_comanda.comanda
product = product_comanda.product
user = User.objects.get(id=request.user.id)
typeMovement = StockMovementType.objects.get(name="Estorno")
consumo = ProductComanda.objects.filter(comanda=product_comanda.comanda)
StockMovement.sumTransactionStock(
product=product,
movement_type=typeMovement,
comanda=comanda,
user=user,
qtd=1,
obs= "Excluido do balcão"
)
product_comanda.delete()
total = 0
for produto in consumo:
total += produto.product.price
return render(request, "htmx_components/htmx_list_products_in_balcao.html",{'consumo': consumo, 'total': total})
@group_required(groupName='Garçom')
def paymentBalcao(request, comanda_id):
typePayment = TypePay.objects.get(id=1)
consumo = ProductComanda.objects.filter(comanda=comanda_id)
user = User.objects.get(id=request.user.id)
try:
vendasBalcao = Comanda.objects.get(name='VENDAS BALCAO')
except:
mesa = Mesa.objects.get(id=1)
vendasBalcao = Comanda(name='VENDAS BALCAO', mesa=mesa, user=request.user, status='CLOSED')
vendasBalcao.save()
comanda = Comanda.objects.get(name=f'{user.id} - BALCÃO - {user.first_name}')
total = 0
for produto in consumo:
total += produto.product.price
produto.comanda = vendasBalcao
produto.save()
pagamento = Payments(value=total, comanda=comanda, type_pay=typePayment,description=f'{user.id} - BALCÃO - {user.first_name}')
pagamento.save()
return redirect('viewBalcao')

27
balcao/models.py Normal file
View File

@@ -0,0 +1,27 @@
# from django.db import models
# from clients.models import Client
# from products.models import Product
# from mesas.models import Mesa
# from typePay.models import TypePay
# class Comanda(models.Model):
# id = models.AutoField(primary_key=True)
# mesa = models.ForeignKey(Mesa, on_delete=models.CASCADE)
# type_pay = models.ForeignKey(TypePay, on_delete=models.SET_NULL, null=True)
# dt_open = models.DateTimeField(auto_now_add=True)
# dt_close = models.DateTimeField(null=True, blank=True)
# client = models.ForeignKey(Client, on_delete=models.SET_NULL, null=True, blank=True)
# name = models.CharField(max_length=255)
# status = models.CharField(max_length=255, default="OPEN")
# def __str__(self) -> str:
# return self.name
# class ProductComanda(models.Model):
# id = models.AutoField(primary_key=True)
# comanda = models.ForeignKey(Comanda, on_delete=models.CASCADE)
# data_time = models.DateTimeField(auto_now_add=True)
# product = models.ForeignKey(Product, on_delete=models.PROTECT)
# applicant = models.CharField(max_length=255, null=True, blank=True)
# def __str__(self) -> str:
# return self.comanda.name + " - " + self.product.name

View File

@@ -0,0 +1,142 @@
{% extends "base.html" %}
{% load static %}
{% block 'title' %}
{{comanda.name}}
{% endblock %}
{% block 'head' %}
<link rel="stylesheet" href="{% static 'comandas/css/viewbalcao.css' %}">
{% endblock %}
{% block 'body' %}
<body>
<div class="grid-container">
<div style="text-align: center;">
<h1>Venda Balcão</h1>
<input hidden type="text" name="id-comanda-balcao" id="id-comanda-balcao" value="{{comanda.id}}">
<div>
<button id="pagarComanda" class="btn-secondary" onclick="modal_payment_comanda()"
{% if total == 0 %}
disabled
{% endif %}>Receber</button>
<button class="btn-primary" id="imprimirFichas" onclick="imprimirFichas()"
{% if total == 0 %}
disabled
{% endif %}
>Imprimir Fichas</button>
</div>
<table id="list-products-balcao">
<tr>
<th style="text-align: left;"><b>Produto</b></th>
<th style="text-align: left;"><b>Preço</b></th>
</tr>
{% for item in consumo%}
<tr>
<td>{{item.product.name}}</td>
<td>R$ {{item.product.price}} </td>
<td><button class="btn-cancel" onclick="removeProductBalcao({{item.id}})">Excluir</button></td>
</tr>
{% endfor %}
<tfoot>
<tr>
<td colspan="2" style="text-align: center;">Total R$ {{total}}</td>
</tr>
<tr hidden>
<td hidden id="total">{{total}}</td>
</tr>
</tfoot>
</table>
</div>
<div id="add-produto">
<form id="productForm">
<h2 style="text-align: center;">Buscar Produto </h2>
<div class="grid-container">
<input id="search-product" name="search-product" type="text" oninput="searchProduct()" autofocus
placeholder="Buscar Produto">
<input type="number" id="qtd-product" name="qtd-product" value="1" required min="1"><br>
</div>
<div id="product-list" class="grid-list-products">
{% for product in products %}
{% if forloop.counter0 == 0 %}
<article
style="background-image: url('{{product.image}}');"
name="productBox"
id="productId-{{ product.id }}"
class="card-product"
onclick="addProductClick({{product.id}} )"
>
<p hidden id="{{forloop.counter0}}" >{{product.id}}</p>
<p hidden id="comanda{{forloop.counter0}}" >{{comanda.id}}</p>
<p class="card-product-p"> {{product.name}}</p>
<p id="{{product.id}}"></p>
<p class="card-product-p"> R$ {{product.price}}</p>
</article >
{% else %}
<article
style="background-image: url('{{product.image}}');"
name="productBox"
id="productId-{{ product.id }}"
class="card-product"
onclick="addProductClick({{product.id}})"
>
<p class="card-product-p"> {{product.name}}</p>
<p class="card-product-p"> R$ {{product.price}}</p>
</article >
{% endif %}
{% endfor %}
</div>
</form>
</div>
</div>
<dialog id="payment-comanda" style="display: none;">
<article>
<h2>Pagamento</h2>
<h1 id="first-total">R$ {{ total }}</h1>
<div>
<p>Recebido:</p> <input id="recebido" type="number">
<h4 id="troco">Troco: </h4>
</div>
<footer>
<button class="btn-secondary" hx-get="{% url 'paymentBalcao' comanda.id %} " hx-trigger="click" hx-swap="none"
onclick="reloadPage()">
Receber
</button>
<button class="btn-cancel" onclick="close_modal_payment_comanda()">Cancelar</button>
</footer>
</article>
</dialog>
<script src="{% static 'comandas/js/viewbalcao.js' %}"></script>
</body>
{% endblock %}

21
balcao/urls.py Normal file
View File

@@ -0,0 +1,21 @@
from django.urls import path
from balcao import htmx_views
from . import views
urlpatterns = [
path('', views.viewBalcao, name='viewBalcao'),
]
htmx_urlpatterns = [
path('listProductBalcao/<int:comanda_id>/<str:search_product>/', htmx_views.listProductBalcao, name='listProductBalcao'),
path('addProductBalcao<int:product_id>/<int:comanda_id>/<int:qtd>/', htmx_views.addProductBalcao, name='addProductBalcao'),
path('addProductBalcaoTeclado<int:product_id>/<int:comanda_id>/<int:qtd>/', htmx_views.addProductBalcaoTeclado, name='addProductBalcaoTeclado'),
path('removeProductBalcao<int:productComanda_id>/', htmx_views.removeProductBalcao, name='removeProductBalcao'),
path('paymentBalcao<int:comanda_id>/', htmx_views.paymentBalcao, name='paymentBalcao'),
]
urlpatterns += htmx_urlpatterns

30
balcao/views.py Normal file
View File

@@ -0,0 +1,30 @@
from django.shortcuts import render
from comandas.models import Comanda, ProductComanda
from products.models import Product
from mesas.models import Mesa
from django.db.models import Count, F
from django.contrib.auth.models import User
from gestaoRaul.decorators import group_required
@group_required(groupName='Garçom')
def viewBalcao(request):
user = User.objects.get(id=request.user.id)
try:
comanda = Comanda.objects.get(name=f'{user.id} - BALCÃO - {user.first_name}')
except:
mesa = Mesa.objects.get(id=1)
comanda = Comanda(name=f'{user.id} - BALCÃO - {user.first_name}', mesa=mesa, user=user,status='CLOSED')
comanda.save()
consumo = ProductComanda.objects.filter(comanda=comanda.id)
products_ordenados = ProductComanda.maisVendidos()
total = 0
for produto in consumo:
total += produto.product.price
return render(request, 'viewBalcao.html', {'comanda': comanda, 'consumo': consumo, 'total': total, 'products': products_ordenados})

6
categories/admin.py Normal file
View File

@@ -0,0 +1,6 @@
from django.contrib import admin
from categories.models import Categories
admin.site.register(Categories)

8
categories/api_views.py Normal file
View File

@@ -0,0 +1,8 @@
from rest_framework import viewsets, permissions
from .models import Categories
from .serializers import CategoriesSerializer
class CategoriesViewSet(viewsets.ModelViewSet):
queryset = Categories.objects.all()
serializer_class = CategoriesSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]

View File

@@ -0,0 +1,7 @@
from rest_framework import serializers
from .models import Categories
class CategoriesSerializer(serializers.ModelSerializer):
class Meta:
model = Categories
fields = '__all__'

6
clients/admin.py Normal file
View File

@@ -0,0 +1,6 @@
from django.contrib import admin
from clients.models import Client
admin.site.register(Client)

74
clients/api_views.py Normal file
View File

@@ -0,0 +1,74 @@
from rest_framework import viewsets, permissions, status
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Client
from .serializers import ClientSerializer
from comandas.models import Comanda, ProductComanda
from comandas.serializers import ComandaSerializer
from payments.models import Payments, somar
from typePay.models import TypePay
class ClientViewSet(viewsets.ModelViewSet):
queryset = Client.objects.all()
serializer_class = ClientSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Client.objects.all()
@action(detail=True, methods=['get'])
def fiados(self, request, pk=None):
client = self.get_object()
comandas = Comanda.objects.filter(client=client, status='FIADO')
serializer = ComandaSerializer(comandas, many=True)
return Response(serializer.data)
@action(detail=False, methods=['post'])
def pagar_fiados(self, request):
comanda_ids = request.data.get('ids', [])
if not comanda_ids:
return Response({'error': 'Nenhum ID de comanda fornecido.'}, status=status.HTTP_400_BAD_REQUEST)
results = []
for comanda_id in comanda_ids:
try:
comanda = Comanda.objects.get(id=comanda_id)
# Apenas processar se estiver como FIADO
if comanda.status != 'FIADO':
results.append({'id': comanda_id, 'status': 'erro', 'message': 'Status da comanda não é FIADO.'})
continue
# 1. Fechar a comanda
comanda.status = 'CLOSED'
comanda.save()
# 2. Gerar o pagamento (Inspirado no payDebt original)
try:
type_pay = TypePay.objects.get(id=1) # Assume ID 1 como padrão para recebimento de fiado
except TypePay.DoesNotExist:
type_pay, _ = TypePay.objects.get_or_create(id=1, defaults={'name': 'Dinheiro'})
consumo = ProductComanda.objects.filter(comanda=comanda)
valores = somar(consumo, comanda)
payment = Payments.objects.create(
value=valores['totalSemTaxa'], # Valor que falta pagar
type_pay=type_pay,
comanda=comanda,
client=comanda.client,
description='PAGAMENTO DE FIADO (via API)'
)
results.append({'id': comanda_id, 'status': 'sucesso', 'payment_id': payment.id})
except Comanda.DoesNotExist:
results.append({'id': comanda_id, 'status': 'erro', 'message': 'Comanda não encontrada.'})
except Exception as e:
results.append({'id': comanda_id, 'status': 'erro', 'message': str(e)})
return Response({
'success': True,
'message': f'{len(comanda_ids)} comandas processadas',
'results': results
}, status=status.HTTP_200_OK)

View File

@@ -0,0 +1,26 @@
# Generated by Django 5.1.4 on 2025-01-15 23:06
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('clients', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='client',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='client',
name='debt',
field=models.DecimalField(decimal_places=2, default=1, max_digits=10),
preserve_default=False,
),
]

View File

@@ -4,5 +4,10 @@ from django.db import models
class Client(models.Model): class Client(models.Model):
id = models.AutoField(primary_key=True) id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
debt = models.DecimalField(max_digits=10, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)
active = models.BooleanField(default=True) active = models.BooleanField(default=True)
contact = models.CharField(max_length=255, null=True, blank=True) contact = models.CharField(max_length=255, null=True, blank=True)
def __str__(self) -> str:
return self.name

23
clients/serializers.py Normal file
View File

@@ -0,0 +1,23 @@
from rest_framework import serializers
from .models import Client
from comandas.models import Comanda, ProductComanda
from payments.models import Payments, somar
from decimal import Decimal
class ClientSerializer(serializers.ModelSerializer):
debt = serializers.SerializerMethodField()
class Meta:
model = Client
fields = ['id', 'name', 'created_at', 'active', 'contact', 'debt']
def get_debt(self, obj):
comandas = Comanda.objects.filter(client=obj, status='FIADO')
total_debt = Decimal(0)
for comanda in comandas:
consumo = ProductComanda.objects.filter(comanda=comanda)
valores = somar(consumo, comanda)
total_debt += valores['totalSemTaxa']
return total_debt

View File

@@ -0,0 +1,96 @@
{% extends "base.html" %}
{% load static %}
{% load custom_filter_tag %}
{% block 'title' %}
Clientes
{% endblock %}
{% block 'head' %}
<link rel="stylesheet" href="{% static 'clients/css/clients.css' %}">
{% endblock %}
{% block 'body' %}
<div class="grid-container">
<div class="grid-top">
<button class="btn-primary"
onclick="openModal()" id="openModal">Novo Cliente</button>
</div>
<table id="client-list">
<tr>
<th style="text-align: left;">Cliente</th>
<th style="text-align: left;width: 20%;">Débito</th>
<th class="hide-on-mobile" style="text-align: left;">Contato</th>
<th class="hide-on-mobile" style="text-align: left;width: 20%;">Ações</th>
</tr>
{% for client in clients %}
<tr>
<td ><a id="name-{{client.id}}" href="{% url 'viewClient' client.id %}">{{client.name}}</a></td>
<td id="debt-{{client.id}}" >R$ {{client.id | totalFiado}}</td>
<td class="hide-on-mobile" id="contact-{{client.id}}" >{{client.contact}}</td>
<td hidden id="active-{{client.id}}" >{{client.active}}</td>
<td>
<div class="grid-buttons hide-on-mobile">
<img
src="{% static 'midia/icons/edit.svg' %}"
style=" width: 35px; height: 35px; cursor: pointer;"
onclick="editclient({{client.id}})" >
</img>
<input type="hidden" id="name-{{client.id}}" value="{{ client.name }}">
<input type="hidden" id="contact-{{client.id}}" value="{{ client.contact }}">
</div>
</td>
</tr>
{% endfor %}
</table>
</div>
<dialog id='Modal-create-client' >
<article >
<form action="{% url 'createClient' %}" id="clientForm" method="post" >
{% csrf_token %}
<h2 id="title-window">Cadastro Cliente</h2>
<input type="text" id="clientId" name="clientId" hidden >
<input type="text" id="clientName" name="name" required placeholder="Nome">
<input type="checkbox" id="active" name="active" placeholder="Ativo">Ativo
<input type="text" id="clientContact" name="contact" placeholder="Contato"></input>
<footer class="grid-buttons">
<button class="btn-primary" id="save" type="submit">Salvar</button>
<button class="btn-primary" onclick="closeModal()" type="button" id="edit" hx-post="{% url 'editClient' %}" hx-trigger="click" hx-swap="none" style="width: 100%;">Alterar</button>
<button class="btn-cancel" type="button" onclick="closeModal()" style="background-color:red;">Cancelar</button>
</footer>
</form>
</article>
</dialog>
<script src="{% static 'clients/js/clients.js' %}"></script>
{% endblock %}

View File

@@ -0,0 +1,70 @@
{% extends "base.html" %}
{% load static %}
{% load custom_filter_tag %}
{% block 'title' %}
Comandas
{% endblock %}
{% block 'head' %}
<link rel="stylesheet" href="{% static 'comandas/css/comandas.css' %}">
{% endblock %}
{% block 'body' %}
<body>
<div style="justify-self: center;">
<h4>{{client.name}}</h4>
<h4>R$ {{client.id | totalFiado}}</h4><br>
<!-- <h4 id="total-selecionado">R$</h4> -->
<button id="btn-fechar-comandas" class="btn-fechar" onclick="enviarComandasSelecionadas()">
Receber
</button>
</div>
<div class=" ">
<table>
<tr>
<th style="text-align: left;"><b>Nome</b></th>
<th style="text-align: left;"><b>Atendente</b></th>
<th style="text-align: left;"><b>Data abertura</b></th>
<th style="text-align: left;"><b>Data fechamento</b></th>
<th style="text-align: left;"><b><input id="selectAll" name="selectAll" type="checkbox"></b></th>
<th style="text-align: left;"><b>Detalhes</b></th>
<th style="text-align: left;"><b>Valor</b></th>
</tr>
{% for comanda in comandas %}
<tr>
<td>{{comanda.name}}</td>
<td>{{comanda.user.first_name}} {{comanda.user.last_name}}</td>
<td>{{comanda.dt_open}}</td>
<td>{{comanda.dt_close}}</td>
<td><input id="{{comanda.id}}" name="{{comanda.id}}" type="checkbox"></td>
<td>
<span data-tooltip="Visualizar Comanda" data-flow="top">
<a href="{% url 'viewcomanda' %}?parametro={{ comanda.id }}">
<img
src="{% static 'midia/icons/view.svg' %}"
style="height: 35px; cursor: pointer;">
</img>
</a>
</span>
</td>
<td>
{{ comanda.id | total }}
</td>
</tr>
{% endfor %}
</table>
</div>
</body>
<script src="{% static 'comandas/js/comandas.js' %}"></script>
<script src="{% static 'clients/js/clients.js' %}"></script>
{% endblock %}

14
clients/urls.py Normal file
View File

@@ -0,0 +1,14 @@
from django.urls import path, include
from . import views
urlpatterns = [
path('', views.clients, name='clients'),
path('createClient', views.createClient, name='createClient'),
path('editClient', views.editClient, name='editClient'),
path('payDebt', views.payDebt, name='payDebt'),
path('viewClient/<int:clientId>', views.viewClient, name='viewClient'),
]

109
clients/views.py Normal file
View File

@@ -0,0 +1,109 @@
from decimal import Decimal
from django.shortcuts import render, redirect
from django.contrib.auth.models import User
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
import json
from comandas.models import Comanda, ProductComanda
from gestaoRaul.decorators import group_required
from clients.models import Client
from payments.models import Payments, somar
from typePay.models import TypePay
# Create your views here.
@group_required(groupName='Gerente')
def clients(request):
clients = Client.objects.all()
return render(request, 'clients.html', {'clients': clients})
def viewClient(request,clientId):
# config = {
# 'taxa': False
# }
client = Client.objects.get(id=int(clientId))
comandas = Comanda.objects.filter(client = client).filter(status = 'FIADO')
total = Decimal(0)
# for comanda in comandas:
# totalConsumo = 0
# totalParcial = 0
# consumo = ProductComanda.objects.filter(comanda=comanda)
# parcial = Payments.objects.filter(comanda=comanda)
# for p in parcial:
# totalParcial += p.value
# for produto in consumo:
# totalConsumo += produto.product.price
# total+= (totalConsumo - totalParcial)
# total = total + round(total * Decimal(0.1), 2) if config['taxa'] else total
return render(request, 'viewclient.html', {'client': client, 'comandas': comandas})
@group_required(groupName='Gerente')
def createClient(request):
name = request.POST.get('name')
contact = request.POST.get('contact')
active = True if request.POST.get('active') else False
# debt = request.POST.get('debt')
client = Client(name=name, contact=contact,debt=0, active=active)
client.save()
return redirect('/clients')
@group_required(groupName='Gerente')
def editClient(request):
client_id = int(request.POST.get('clientId'))
client = Client.objects.get(id=client_id)
client.name = request.POST.get('name')
client.contact = request.POST.get('contact')
client.active = True if request.POST.get('active') else False
# client = Client(name=name, contact=contact,debt=0, active=active)
client.save()
return redirect('/clients')
@csrf_exempt
@require_POST
def payDebt(request):
try:
# Verifica se é uma requisição AJAX
if not request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({'error': 'Requisição inválida'}, status=400)
# Obter os IDs do corpo da requisição (não mais da URL)
try:
data = json.loads(request.body)
comanda_ids = data.get('ids', [])
except json.JSONDecodeError:
return JsonResponse({'error': 'JSON inválido'}, status=400)
for comanda_id in comanda_ids:
try:
comanda = Comanda.objects.get(id=comanda_id)
comanda.status = 'CLOSED'
comanda.save()
typePayment = TypePay.objects.get(id=1)
consumo = ProductComanda.objects.filter(comanda=comanda_id)
value = somar(consumo,comanda)
description = 'PAGAMENTO DE FIADO'
pagamento = Payments(value=value["totalSemTaxa"], comanda=comanda, type_pay=typePayment,description=description,client=comanda.client)
pagamento.save()
except Comanda.DoesNotExist:
return JsonResponse({'error': f'Comanda com ID {comanda_id} não encontrada'}, status=404)
return JsonResponse({
'success': True,
'message': f'{len(comanda_ids)} comandas processadas',
'ids': comanda_ids
}, status=200)
except Exception as e:
return JsonResponse({
'success': False,
'error': str(e)
}, status=500)

9
comandas/admin.py Normal file
View File

@@ -0,0 +1,9 @@
from django.contrib import admin
from comandas.models import Comanda, ProductComanda, StockMovement, StockMovementType
admin.site.register(Comanda)
admin.site.register(ProductComanda)
admin.site.register(StockMovementType)
admin.site.register(StockMovement)

168
comandas/api_views.py Normal file
View File

@@ -0,0 +1,168 @@
from rest_framework import viewsets, permissions, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django.utils import timezone
from .models import Comanda, ProductComanda, StockMovement, StockMovementType
from .serializers import ComandaSerializer, ProductComandaSerializer
from payments.models import Payments
from typePay.models import TypePay
from clients.models import Client
class ComandaViewSet(viewsets.ModelViewSet):
queryset = Comanda.objects.all()
serializer_class = ComandaSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Comanda.objects.all()
@action(detail=True, methods=['post'])
def apagar(self, request, pk=None):
comanda = self.get_object()
# 1. Recuperar os itens para devolver ao estoque
itens = ProductComanda.objects.filter(comanda=comanda)
# Tipo de movimentação: Estorno/Cancelamento (ajustar conforme os tipos existentes)
# Se não houver um tipo específico, podemos buscar um genérico ou usar o de Venda com sinal invertido via sumTransactionStock
try:
typeMovement = StockMovementType.objects.get(name="Estorno - Comanda Apagada")
except StockMovementType.DoesNotExist:
# Fallback para um tipo existente se o de estorno não existir
typeMovement, _ = StockMovementType.objects.get_or_create(name="Ajuste de Estoque (Cancelamento)")
for item in itens:
StockMovement.sumTransactionStock(
product=item.product,
movement_type=typeMovement,
comanda=comanda,
user=request.user,
qtd=1,
obs=f"Estorno: Comanda {comanda.name} apagada/limpa via API"
)
# 2. Excluir os itens da comanda
itens.delete()
# 3. Mudar o status para CLOSED
comanda.status = 'CLOSED'
comanda.save()
@action(detail=True, methods=['post'])
def pagar(self, request, pk=None):
comanda = self.get_object()
# Dados do pagamento vindos no request
value = request.data.get('value')
type_pay_id = request.data.get('type_pay')
client_id = request.data.get('client')
description = request.data.get('description', f"Pagamento da comanda {comanda.name}")
if not value or not type_pay_id:
return Response(
{'error': 'Campos "value" e "type_pay" são obrigatórios.'},
status=status.HTTP_400_BAD_REQUEST
)
try:
type_pay = TypePay.objects.get(id=type_pay_id)
except TypePay.DoesNotExist:
return Response({'error': 'Tipo de pagamento não encontrado.'}, status=status.HTTP_400_BAD_REQUEST)
client = None
if client_id:
try:
client = Client.objects.get(id=client_id)
except Client.DoesNotExist:
return Response({'error': 'Cliente não encontrado.'}, status=status.HTTP_400_BAD_REQUEST)
# 1. Criar o registro de pagamento
payment = Payments.objects.create(
value=value,
type_pay=type_pay,
comanda=comanda,
client=client,
description=description
)
# 2. Fechar a comanda
comanda.status = 'CLOSED'
comanda.dt_close = timezone.now()
comanda.save()
return Response({
'status': 'Pagamento registrado e comanda fechada com sucesso.',
'payment_id': payment.id
}, status=status.HTTP_200_OK)
class ProductComandaViewSet(viewsets.ModelViewSet):
queryset = ProductComanda.objects.all()
serializer_class = ProductComandaSerializer
permission_classes = [permissions.IsAuthenticated]
def perform_create(self, serializer):
# Salva o item na comanda
product_comanda = serializer.save()
# Recupera os dados para a movimentação de estoque
product = product_comanda.product
comanda = product_comanda.comanda
# Tipo de movimentação: Venda - Comanda (como na view original)
try:
typeMovement = StockMovementType.objects.get(name="Venda - Comanda")
except StockMovementType.DoesNotExist:
typeMovement, _ = StockMovementType.objects.get_or_create(name="Saída de Estoque (API)")
StockMovement.subTransactionStock(
product=product,
movement_type=typeMovement,
comanda=comanda,
user=self.request.user,
qtd=1,
obs="Adicionado na comanda via API"
)
# 3. Criar Pedido (Order) automaticamente se for item de cozinha
# (Lógica inspirada no addProduct original)
if product.cuisine:
from orders.models import Order
Order.objects.create(
id_comanda=comanda,
id_product=product,
productComanda=product_comanda,
obs=self.request.data.get('obs', '')
)
def perform_update(self, serializer):
instance = serializer.save()
obs = self.request.data.get('obs')
if obs is not None:
order = instance.order_set.first()
if order:
order.obs = obs
order.save()
def perform_destroy(self, instance):
# Recupera os dados antes de deletar
product = instance.product
comanda = instance.comanda
# Tipo de movimentação: Estorno/Cancelamento
try:
typeMovement = StockMovementType.objects.get(name="Estorno - Item Removido")
except StockMovementType.DoesNotExist:
typeMovement, _ = StockMovementType.objects.get_or_create(name="Ajuste de Estoque (Cancelamento)")
# Realiza a devolução ao estoque
StockMovement.sumTransactionStock(
product=product,
movement_type=typeMovement,
comanda=comanda,
user=self.request.user,
qtd=1,
obs=f"Estorno: Item {product.name} removido da comanda {comanda.name} via API"
)
# Deleta o registro definitivamente
instance.delete()

136
comandas/htmx_views.py Normal file
View File

@@ -0,0 +1,136 @@
from datetime import date
from decimal import Decimal
from django.http import JsonResponse
from django.shortcuts import render, redirect
from django.contrib.auth.models import User
from comandas.models import Comanda, ProductComanda, StockMovementType, StockMovement
from orders.models import Order
from products.models import Product
from payments.models import Payments, somar
from typePay.models import TypePay
from gestaoRaul.decorators import group_required
from websocket_client.websocketClient import enviar_mensagem
from asgiref.sync import async_to_sync
# import asyncio
# import websockets
# async def enviar_mensagem(msg):
# try:
# uri = "ws://websocket_server:8765"
# async with websockets.connect(uri) as websocket:
# await websocket.send(msg)
# # print(f"> Enviado: {msg}")
# # resposta = await websocket.recv()
# # print(f"< Recebido: {resposta}")
# except Exception as e:
# print(f"Erro ao enviar mensagem via websocket: {e}")
@group_required(groupName='Garçom')
def removeProductComanda(request, productComanda_id):
config = {
'taxa': False
}
product_comanda = ProductComanda.objects.get(id=productComanda_id)
comanda = product_comanda.comanda
product = product_comanda.product
user = User.objects.get(id=request.user.id)
typeMovement = StockMovementType.objects.get(name="Estorno")
if comanda.status == 'OPEN':
parcial = Payments.objects.filter(comanda=comanda)
consumo = ProductComanda.objects.filter(comanda=comanda)
valores = somar(consumo,comanda)
if product_comanda.product.cuisine == True:
order = Order.objects.get(productComanda=product_comanda)
StockMovement.sumTransactionStock(
product=product,
movement_type=typeMovement,
comanda=comanda,
user=user,
qtd=1,
obs= "Excluido da comanda"
)
product_comanda.delete()
msg = JsonResponse({
'type': 'broadcast',
'message': 'Atenção! Pedido cancelado',
'local':'cozinha',
'tipo':'delete',
'id':order.id,
'speak': f'Pedido cancelado! {order.id_product.name}.'
})
# asyncio.run(enviar_mensagem(msg))
# order.delete()
consumo = ProductComanda.objects.filter(comanda=comanda)
valores = somar(consumo,comanda)
else:
StockMovement.sumTransactionStock(
product=product,
movement_type=typeMovement,
comanda=comanda,
user=user,
qtd=1,
obs= "Excluido da comanda"
)
product_comanda.delete()
consumo = ProductComanda.objects.filter(comanda=comanda)
valores = somar(consumo,comanda)
return render(request,
"htmx_components/comandas/htmx_list_products_in_comanda.html",
{'config':config,
'valores': valores,
'parcials':parcial,
'consumo': consumo,
'comanda':comanda})
else:
pass
@group_required(groupName='Gerente')
def reopenComanda(request, comanda_id):
comanda = Comanda.objects.get(id=comanda_id)
if comanda.status == 'CLOSED':
pass
else:
comanda.status = "OPEN"
comanda.save()
@group_required(groupName='Gerente')
def paymentComanda(request, comanda_id):
taxa = request.POST.get('taxa',False)
typePayment = TypePay.objects.get(id=1)
consumo = ProductComanda.objects.filter(comanda=comanda_id)
comanda = Comanda.objects.get(id=comanda_id)
valores = somar(consumo,comanda)
if taxa == 'True':
pagamento = Payments(value=valores['totalComTaxa'], comanda=comanda, type_pay=typePayment,description='tipo de pagamento mokado')
pagamento.save()
else:
pagamento = Payments(value=valores['totalSemTaxa'], comanda=comanda, type_pay=typePayment,description='tipo de pagamento mokado')
pagamento.save()
comanda.status = 'CLOSED'
comanda.save()
return redirect('/comandas')
@group_required(groupName='Gerente')
def paymentParcial(request, comanda_id):
typePayment = TypePay.objects.get(id=1)
comanda = Comanda.objects.get(id=comanda_id)
value = Decimal(request.POST.get('value-parcial'))
description = request.POST.get('name-parcial')
pagamento = Payments(value=value, comanda=comanda, type_pay=typePayment,description=description)
pagamento.save()
return redirect('/comandas')

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.1.4 on 2025-01-15 12:43
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('comandas', '0003_comanda_status_alter_productcomanda_product'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='comanda',
name='user',
field=models.ForeignKey(blank=True, default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
]

View File

@@ -0,0 +1,42 @@
# Generated by Django 5.1.4 on 2025-07-22 18:36
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('comandas', '0004_comanda_user'),
('products', '0004_unitofmeasure_productcomponent_product_components_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='StockMovementType',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(help_text="Ex: 'Entrada por Compra', 'Saída por Venda', 'Ajuste de Estoque'", max_length=100, unique=True)),
('observation', models.TextField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='StockMovement',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('quantity', models.IntegerField(help_text='Quantidade movimentada. Use valores negativos para saídas.')),
('observation', models.TextField(blank=True, null=True)),
('movement_date', models.DateTimeField(auto_now_add=True)),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stock_movements', to='products.product')),
('related_comanda', models.ForeignKey(blank=True, help_text='Comanda relacionada à movimentação (opcional).', null=True, on_delete=django.db.models.deletion.SET_NULL, to='comandas.comanda')),
('user', models.ForeignKey(blank=True, help_text='Usuário que realizou a movimentação.', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('movement_type', models.ForeignKey(help_text='Tipo de movimentação (entrada, saída, ajuste).', on_delete=django.db.models.deletion.PROTECT, to='comandas.stockmovementtype')),
],
options={
'ordering': ['-movement_date'],
},
),
]

151
comandas/models.py Normal file
View File

@@ -0,0 +1,151 @@
from django.db import models
from django.contrib.auth.models import User
from django.db.models import Count, F
from clients.models import Client
from products.models import Product, ProductComponent
from mesas.models import Mesa
from typePay.models import TypePay
class Comanda(models.Model):
id = models.AutoField(primary_key=True)
mesa = models.ForeignKey(Mesa, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE, null=False, blank=True)
type_pay = models.ForeignKey(TypePay, on_delete=models.SET_NULL, null=True)
dt_open = models.DateTimeField(auto_now_add=True)
dt_close = models.DateTimeField(null=True, blank=True)
client = models.ForeignKey(Client, on_delete=models.SET_NULL, null=True, blank=True)
name = models.CharField(max_length=255)
status = models.CharField(max_length=255, default="OPEN")
def __str__(self) -> str:
return self.name
class ProductComanda(models.Model):
id = models.AutoField(primary_key=True)
comanda = models.ForeignKey(Comanda, on_delete=models.CASCADE)
data_time = models.DateTimeField(auto_now_add=True)
product = models.ForeignKey(Product, on_delete=models.PROTECT)
applicant = models.CharField(max_length=255, null=True, blank=True)
def __str__(self) -> str:
return self.comanda.name + " - " + self.product.name
def maisVendidos():
produtos_mais_vendidos = list(ProductComanda.objects.values('product').annotate(
quantidade=Count('product'),
nome=F('product__name') ).order_by('-quantidade'))
products = Product.objects.all()
products_ordenados = []
for produto in produtos_mais_vendidos:
for p in products:
if p.name == produto['nome'] and p.active == True:
products_ordenados.append(p)
return products_ordenados[:15]
class StockMovementType(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=100, unique=True, help_text="Ex: 'Entrada por Compra', 'Saída por Venda', 'Ajuste de Estoque'")
observation = models.TextField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
class StockMovement(models.Model):
id = models.AutoField(primary_key=True)
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='stock_movements')
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, help_text="Usuário que realizou a movimentação.")
related_comanda = models.ForeignKey(
Comanda,
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Comanda relacionada à movimentação (opcional)."
)
movement_type = models.ForeignKey(StockMovementType, on_delete=models.PROTECT, help_text="Tipo de movimentação (entrada, saída, ajuste).")
quantity = models.IntegerField(help_text="Quantidade movimentada. Use valores negativos para saídas.")
observation = models.TextField(null=True, blank=True)
movement_date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return (
f"Movimentação de {self.quantity} de {self.product.name} "
f"({self.movement_type.name}) em {self.movement_date.strftime('%d/%m/%Y %H:%M')}"
)
def subTransactionStock(product:Product,
movement_type:StockMovementType,
comanda:Comanda,
obs:str,
user:User=None,
qtd:int=1):
components = ProductComponent.objects.filter(composite_product=product)
if components.exists():
for component in components:
movi = StockMovement.objects.create(
product=component.component_product ,
related_comanda=comanda,
user=user,
movement_type=movement_type,
quantity=-component.quantity_required,
observation=obs
)
movi.save()
component.component_product.quantity -= component.quantity_required
component.component_product.save()
movi = StockMovement.objects.create(
product=product ,
related_comanda=comanda,
user=user,
movement_type=movement_type,
quantity=-qtd,
observation=obs
)
movi.save()
product.quantity -= qtd
product.save()
def sumTransactionStock(product:Product,
movement_type:StockMovementType,
comanda:Comanda,
obs:str,
user:User=None,
qtd:int=1):
components = ProductComponent.objects.filter(composite_product=product)
if components.exists():
for component in components:
movi = StockMovement.objects.create(
product=component.component_product ,
related_comanda=comanda,
user=user,
movement_type=movement_type,
quantity=component.quantity_required,
observation=obs
)
movi.save()
component.component_product.quantity += component.quantity_required
component.component_product.save()
movi = StockMovement.objects.create(
product=product ,
related_comanda=comanda,
user=user,
movement_type=movement_type,
quantity=qtd,
observation=obs
)
movi.save()
product.quantity += qtd
product.save()
class Meta:
ordering = ['-movement_date']

28
comandas/serializers.py Normal file
View File

@@ -0,0 +1,28 @@
from rest_framework import serializers
from .models import Comanda, ProductComanda
class ProductComandaSerializer(serializers.ModelSerializer):
product_name = serializers.ReadOnlyField(source='product.name')
obs = serializers.SerializerMethodField()
class Meta:
model = ProductComanda
fields = ['id', 'comanda', 'data_time', 'product', 'product_name', 'applicant', 'obs']
def get_obs(self, obj):
order = obj.order_set.first()
return order.obs if order else ""
class ComandaSerializer(serializers.ModelSerializer):
mesa_name = serializers.ReadOnlyField(source='mesa.name')
client_name = serializers.ReadOnlyField(source='client.name')
user_name = serializers.ReadOnlyField(source='user.username')
items = ProductComandaSerializer(many=True, read_only=True, source='productcomanda_set')
class Meta:
model = Comanda
fields = [
'id', 'mesa', 'mesa_name', 'user', 'user_name',
'type_pay', 'dt_open', 'dt_close', 'client',
'client_name', 'name', 'status', 'items'
]

View File

@@ -0,0 +1,78 @@
{% extends "base.html" %}
{% load static %}
{% load custom_filter_tag %}
{% block 'title' %}
Comandas
{% endblock %}
{% block 'head' %}
<link rel="stylesheet" href="{% static 'comandas/css/comandas.css' %}">
{% endblock %}
{% block 'body' %}
<body>
<div style="justify-self: center;">
<button class="btn-primary" id="openModal">Abrir Comanda</button>
</div>
<div class="grid-container ">
{% for comanda in comandas %}
<div class="card-comanda">
<a href="{% url 'viewcomanda' %}?parametro={{ comanda.id }}" style="text-decoration: none; ">
<header
{% if comanda.status == 'OPEN' %}
style="padding: 5px; min-height: 60px;border-radius: 5px 5px 0px 0px; background-color: #036800;"
{% elif comanda.status == 'PAYING' %}
style="padding: 5px; min-height: 60px;border-radius: 5px 5px 0px 0px; background-color: rgb(165, 86, 33);"
{% endif %}
>
<p>{{comanda.name}}</p>
<p>{{comanda.mesa}}</p>
</header>
<div style="padding: 5px;">
<p>
Hora: {{comanda.dt_open|date:"H:i"}}
</p>
<p>
{{ comanda.id | total }}
</p>
</div>
</a>
</div>
{% endfor %}
</div>
<dialog id="Modal-create-comanda">
<article >
<form id="form-comanda" method="post" action="{% url 'createComanda' %}">
{% csrf_token %}
<h2>Abrir Comanda</h2>
<input type="text" id="name-comanda" name="name-comanda" required placeholder="Nome"><br>
<select name="select-mesa" required>
<option value="1">Sem Mesa</option>
{% for mesa in mesas %}
<option value="{{mesa.id}}">{{mesa.name}}</option>
{% endfor %}
</select>
<div style="display: flex;gap: 10px;">
<button class="btn-primary" type="submit">Abrir</button>
<button class="btn-cancel" type="button" onclick="closeModal()">Cancelar</button>
</div>
</form>
</article>
</dialog >
</body>
<script src="{% static 'comandas/js/comandas.js' %}"></script>
{% endblock %}

View File

@@ -0,0 +1,338 @@
{% extends "base.html" %}
{% load static %}
{% load custom_filter_tag %}
{% block 'title' %}
Detalhes {{comanda.name}}
{% endblock %}
{% block 'head' %}
<link rel="stylesheet" href="{% static 'comandas/css/viewcomanda.css' %}">
<style>
.swal2-popup {
position: relative; /* Necessário para o posicionamento absoluto do botão */
}
.posi {
position: absolute !important;
background-color: rgba(72, 72, 72, 0.151);
color: rgba(255, 255, 255, 0.452);
border: 2px solid rgba(239, 239, 239, 0.107);
border-radius: 35px;
padding: 0px 15px 3px 15px ;
align-self: center;
font-size: 32px;
top: 0.1em;
right: 0.1em;
scale: 0.8;
}
.posi:hover{
background-color: rgba(183, 3, 3, 0.598);
color: rgb(255, 255, 255);
}
.custom-toast-container {
z-index: 99999 !important;
}
.swal2-container {
z-index: 9999 !important;
}
</style>
{% endblock %}
{% block 'body' %}
<body>
<input hidden id="id-temp" type="number">
<div class="grid-container" >
<div style="display: flex;padding: 5px;gap:8px">
<button class="btn-primary" id="openModal" onclick="modalAddProduct()"
{% if comanda.status != 'OPEN'%}
disabled
{% endif %}
>Add Produto</button>
<button class="btn-cancel" id="closeComanda" onclick="closeConta({{comanda.id}})"
{% if comanda.status != 'OPEN' %}
style="display: none;"
{% endif %}
>Fechar Conta</button>
{% if comanda.status == 'PAYING' or comanda.status == 'FIADO'%}
<button class="btn-secondary" id="pagarComanda"
onclick="modal_payment_comanda()"
>Receber</button>
<button class="btn-primary" id="printComanda"
style="display: flex;"
onclick="imprimirConta()"
>Imprimir Conta</button>
{% else %}
<button class="btn-secondary" id="pagarComanda"
onclick="modal_payment_comanda()"
style="display: none;"
>Receber</button>
<button class="btn-primary" id="printComanda"
style="display: none;"
onclick="imprimirConta()"
>Imprimir Conta</button>
{% endif %}
{% if user|groupUser:"Gerente" %}
<button class="btn-primary" id="reOpenComanda" hx-get="{% url 'reopenComanda' comanda.id %} " hx-trigger="click" hx-swap="none" onclick="reloadPage()"
{% if comanda.status == 'OPEN'%}
style="display: none;"
{% elif comanda.status == 'CLOSED' %}
style="display: none;"
{% endif %}
>Reabrir</button>
{% endif %}
</div>
<div>
<input hidden type="text" id="h-mesaId" value="{{comanda.mesa.id}}">
<input hidden type="text" id="id-comanda" value="{{comanda.id}}">
<span id="name-comanda">Nome: {{comanda.name}} | </span>
<span id="mesa-comanda">Local: {{comanda.mesa}}</span>
<img
onclick="openModalAlter()"
src="{% static 'midia/icons/pen.svg' %}"
style="width: 30px; height: 35px; cursor: pointer;">
</img>
</div>
<p id="open-comanda">Aberta em: {{comanda.dt_open|date:"D"}} {{comanda.dt_open|date:"d/m/Y - H:i"}}</p>
<img hidden src="{% static 'midia/logo.png' %}" style="width: 240px; height: 200px;">
<table id="list-products-comanda">
<tr>
<th style="text-align: left;"><b>Produto</b></th>
<th style="text-align: left;"><b>Preço</b></th>
</tr>
{% for item in consumo%}
<tr id="item-{{item.id}}">
<td id="id-for-print-{{item.id}}">
<spam style="cursor: pointer;" onclick="inforOrders({{item.id}})">
{{item.product.name}}</spam>
{% if item.product.cuisine == True %}
<input hidden id="{{item.id}}-obsOrder" type="order" value="{{item.id | obsOrder}}">
<img
onclick="openModalObs({{item.id}})"
src="{% static 'midia/icons/note.svg' %}"
style="width: 25px; height: 35px; cursor: pointer;">
</img>
<img
onclick="printOrder({{item.id}})"
src="{% static 'midia/icons/print.svg' %}"
style="width: 35px; height: 35px; cursor: pointer;">
</img>
{% endif %}
</td>
<td>R$ {{item.product.price}}</td>
{% if comanda.status != 'OPEN'%}
{% else %}
<td>
<img
onclick="removeProductComanda({{item.id}}, '{{item.product.name}}')"
src="{% static 'midia/icons/delete.svg' %}"
style="width: 35px; height: 35px; cursor: pointer;"
>
</img>
</td>
{% endif %}
</tr>
{% endfor %}
{% if config.taxa %}
<tr>
<td>
Taxa de serviço 10%
</td>
<td>
R$ {{valores.taxa}}
</td>
</tr>
{% endif %}
{% if parcials%}
<td colspan="2" style="text-align: center;"><b>Pagamentos parciais</b></td>
{% endif %}
{% for parcial in parcials %}
<tr>
<td>{{parcial.description}} ás {{parcial.datetime|date:"H:i"}}</td>
<td>R$ -{{parcial.value}}</td>
</tr>
{% endfor %}
<tfoot>
<tr>
{% if config.taxa %}
<td colspan="2" style="text-align: center;"><b>Total R$ {{valores.totalComTaxa}}</b></td>
{% else %}
<td colspan="2" style="text-align: center;"><b>Total R$ {{valores.totalSemTaxa}}</b></td>
{% endif %}
</tr>
</tfoot>
</table>
<button class="btn-secondary" onclick="modal_payment_parcial()">Pagamento Parcial</button>
</div>
<div hidden id="addProduct" >
{% csrf_token %}
<input type="text" oninput="searchProduct()" id="search-product" name="search-product" placeholder="Buscar Produto" ><br>
<div id="product-list" class="grid-list-products">
{% for product in products %}
<div
style="background-image: url('{{product.image}}');"
class="card-product"
onclick="addProductComanda({{product.id}}, {{comanda.id}}, '{{product.cuisine}}')" >
<p class="card-product-p"> {{product.name}}</p>
<p class="card-product-p"> R$ {{product.price}}</p>
</div >
{% endfor %}
</div>
</div>
<dialog id="payment-comanda" style="display: none;" >
<article>
<form action="{% url 'paymentComanda' comanda.id %}" method="post">
{% csrf_token %}
<h2>Pagamento</h2>
<div style="display: flex; align-content: space-around; align-items: center; justify-self: center;gap: 50px;">
{% if config.taxa %}
<h1 id="first-total">R$ {{ valores.totalComTaxa }}</h1>
<h1 hidden id="totalComTaxa">R$ {{ valores.totalComTaxa }}</h1>
<h1 hidden id="totalSemTaxa">R$ {{ valores.totalSemTaxa }}</h1>
<div>
<input id="taxa" name="taxa" type="checkbox" value="True" label="Taxa de serviço" checked >Taxa de serviço
{% else %}
<h1 id="first-total">R$ {{ valores.totalSemTaxa }}</h1>
{% endif %}
</div>
</div>
<div>
<p>Recebido:</p> <input id="recebido" oninput="troco()" name="recebido" type="number">
<h4 id="troco">Troco: </h4>
</div>
<footer>
<div style="display: flex;gap: 10px;">
<button type="submit" class="btn-secondary" onclick="backPage()">Receber</button>
{% if comanda.status != 'FIADO' %}
<button type="button" class="btn-primary" onclick=" modal_conta_client()">
Conta
</button>
{% endif %}
<button type="button" class="btn-cancel" onclick="close_modal_payment_comanda()">Cancelar</button>
</div>
</footer>
</form>
</article>
</dialog>
<dialog id="payment-parcial" style="display: none;" >
<article>
<h2>Pagamento Parcial</h2>
<form method="post" action="{% url 'paymentParcial' comanda.id %} ">
{% csrf_token %}
<input required id="value-parcial" name="value-parcial" type="number" step="0.01" max="{{total}}" placeholder="Valor">
<input id="name-parcial" name="name-parcial" type="text" placeholder="Nome" >
<footer>
<div style="display: flex;gap: 10px;">
<button type="submit" class="btn-secondary" >Receber</button>
<button type="button" class="btn-cancel" onclick="close_modal_payment_parcial()">Cancelar</button>
</div>
</footer>
</form>
</article>
</dialog>
<dialog id="Modal-alter-comanda">
<article >
<form id="form-comanda" method="post" action="{% url 'editComanda' %}">
{% csrf_token %}
<h2>Editar Comanda</h2>
<input hidden type="text" name="h-comandaId" id="h-comandaId" value="{{comanda.id}}">
<input type="text" id="nameComanda" name="nameComanda" required placeholder="Nome"><br>
<select name="select-mesa" id="select-mesa" required >
{% for mesa in mesas %}
<option value="{{mesa.id}}">{{mesa.name}}</option>
{% endfor %}
</select>
<div style="display: flex;gap: 10px;">
<button type="submit" class="btn-primary">Alterar</button>
<button type="button" class="btn-cancel" onclick="closeModalAlter()">Cancelar</button>
</div>
</form>
</article>
</dialog >
<dialog id="conta-cliente" style="display: none;" >
<article>
<form id="form-comanda" method="post" action="{% url 'addContaCliente' %}">
{% csrf_token %}
<h2>Adicionar na Conta</h2>
<h1>R$ {{ valores.totalSemTaxa }}</h1>
<div>
<input hidden type="text" name="valor-conta" value="{{ total }}">
<input hidden type="text" name="idComanda" value="{{ comanda.id }}">
<p>Selecione o Cliente:</p>
<select name="select-client" id="select-client" required>
<option value="">Selecione um cliente</option>
{% for client in clients %}
<option value="{{client.id}}">{{client.name}}</option>
{% endfor %}
</select>
</div>
<footer>
<div style="display: flex;gap: 10px;">
<button type="submit" class="btn-primary"> Adicionar a conta</button>
<button type="button" class="btn-cancel" onclick="close_modal_conta_client()">Cancelar</button>
</div>
</form>
</footer>
</article>
</dialog>
<script src="{% static 'comandas/js/viewcomanda.js' %}"></script>
</body>
{% endblock %}

View File

@@ -0,0 +1,69 @@
from decimal import Decimal
from django import template
from orders.models import Order
from comandas.models import Comanda, ProductComanda
from clients.models import Client
from payments.models import Payments
register = template.Library()
@register.filter(name='total')
def filter_total(value):
config = {
'taxa': False
}
id = value
comanda_id = int(id)
totalParcial = Decimal(0)
comanda = Comanda.objects.get(id=comanda_id)
consumo = ProductComanda.objects.filter(comanda=comanda_id)
parcial = Payments.objects.filter(comanda=comanda)
for p in parcial:
totalParcial += p.value
total = Decimal(0)
for produto in consumo:
total += produto.product.price
taxa = round(total * Decimal(0.1), 2)
total = (total + taxa) - totalParcial if config['taxa'] else total - totalParcial
return f'R$ {total}'
@register.filter(name='totalFiado')
def viewClient(clientId):
config = {
'taxa': False
}
client = Client.objects.get(id=int(clientId))
comandas = Comanda.objects.filter(client = client).filter(status = 'FIADO')
total = Decimal(0)
for comanda in comandas:
totalConsumo = 0
totalParcial = 0
consumo = ProductComanda.objects.filter(comanda=comanda)
parcial = Payments.objects.filter(comanda=comanda)
for p in parcial:
totalParcial += p.value
for produto in consumo:
totalConsumo += produto.product.price
total+= (totalConsumo - totalParcial)
total = total + round(total * Decimal(0.1), 2) if config['taxa'] else total
return total
@register.filter(name='groupUser')
def has_group(user, group_name):
return user.groups.filter(name=group_name).exists()
@register.filter(name='obsOrder')
def obsOrder(id):
try:
productComanda_obj = ProductComanda.objects.get(pk=id)
order = Order.objects.get(productComanda=productComanda_obj)
return order
except:
return ''

View File

@@ -0,0 +1 @@
default_app_config = 'comandas.apps.ComandasConfig' # Se você tiver um AppConfig

View File

@@ -0,0 +1,28 @@
from django.test import TestCase
class ComandaTestCase(TestCase):
def setUp(self):
# Setup code for creating test instances of Comanda and other related models
pass
def test_comanda_creation(self):
# Test the creation of a Comanda instance
pass
def test_comanda_str_method(self):
# Test the __str__ method of the Comanda model
pass
def test_comanda_total_value(self):
# Test the total value calculation of a Comanda instance
pass
def test_comanda_status_choices(self):
# Test the status choices for the Comanda model
pass
def tearDown(self):
# Cleanup code after tests
pass

View File

@@ -0,0 +1,119 @@
from django.test import TestCase, RequestFactory
from django.urls import reverse
from django.contrib.auth.models import User, Group
from django.contrib.sessions.middleware import SessionMiddleware
from django.contrib.messages.middleware import MessageMiddleware
from ..views import createComanda
from ..models import Mesa, Comanda
from mesas.models import Mesa
from gestaoRaul.decorators import group_required
class CreateComandaViewTest(TestCase):
def setUp(self):
self.factory = RequestFactory()
self.garcom_group = Group.objects.create(name='Garçom')
self.outro_group = Group.objects.create(name='OutroGrupo')
self.garcom_user = User.objects.create_user(username='garcom', password='password')
self.garcom_user.groups.add(self.garcom_group)
self.outro_user = User.objects.create_user(username='outro', password='password')
self.outro_user.groups.add(self.outro_group)
self.mesa = Mesa.objects.create(name='teste')
def add_middleware(self, request):
"""Adiciona middlewares necessários para request.user e mensagens."""
middleware = SessionMiddleware(lambda x: x)
middleware.process_request(request)
request.session.save()
message_middleware = MessageMiddleware(lambda x: x)
message_middleware.process_request(request)
return request
def test_garcom_can_access_and_create_comanda(self):
"""Testa se um usuário no grupo 'Garçom' pode acessar e criar uma comanda."""
request = self.factory.post(
reverse('createComanda'), # Assumindo que sua URL para createComanda se chama 'createcomanda'
{'name-comanda': 'Comanda Teste', 'select-mesa': self.mesa.id}
)
request.user = self.garcom_user
request = self.add_middleware(request)
response = createComanda(request)
self.assertEqual(response.status_code, 302) # Verifica se houve um redirect
self.assertIn(reverse('viewcomanda'), response.url) # Verifica se redirecionou para a view correta
self.assertTrue(Comanda.objects.filter(name='Comanda Teste', mesa=self.mesa, user=self.garcom_user).exists())
def test_non_garcom_cannot_access_create_comanda(self):
"""Testa se um usuário fora do grupo 'Garçom' não pode acessar a view."""
request = self.factory.post(
reverse('createComanda'),
{'name-comanda': 'Comanda Teste', 'select-mesa': self.mesa.id}
)
request.user = self.outro_user
request = self.add_middleware(request)
# Aplica o decorator diretamente para testar o acesso negado
wrapped_view = group_required(groupName='Garçom')(createComanda)
response = wrapped_view(request)
self.assertEqual(response.status_code, 403) # Verifica se o acesso foi negado (Forbidden)
self.assertFalse(Comanda.objects.filter(name='Comanda Teste').exists())
def test_anonymous_user_cannot_access_create_comanda(self):
"""Testa se um usuário anônimo não pode acessar a view."""
request = self.factory.post(
reverse('createComanda'),
{'name-comanda': 'Comanda Teste', 'select-mesa': self.mesa.id}
)
request.user = None # Simula um usuário não autenticado
request = self.add_middleware(request)
wrapped_view = group_required(groupName='Garçom')(createComanda)
response = wrapped_view(request)
self.assertEqual(response.status_code, 302) # Deve redirecionar para a página de login
self.assertTrue(response.url.startswith(reverse('login'))) # Verifica se redireciona para a página de login
self.assertFalse(Comanda.objects.filter(name='Comanda Teste').exists())
def test_create_comanda_invalid_mesa(self):
"""Testa o comportamento ao tentar criar uma comanda com uma mesa inválida."""
request = self.factory.post(
reverse('createComanda'),
{'name-comanda': 'Comanda Inválida', 'select-mesa': 999} # ID de mesa inexistente
)
request.user = self.garcom_user
request = self.add_middleware(request)
# response = createComanda(request)
with self.assertRaises(Mesa.DoesNotExist):
createComanda(request)
# self.assertEqual(response.status_code, 400)
self.assertFalse(Comanda.objects.filter(name='Comanda Inválida').exists())
def test_create_comanda_missing_data(self):
"""Testa o comportamento ao tentar criar uma comanda com dados faltando."""
request_sem_nome = self.factory.post(
reverse('createComanda'),
{'select-mesa': self.mesa.id}
)
request_sem_nome.user = self.garcom_user
request_sem_nome = self.add_middleware(request_sem_nome)
# with self.assertRaises(TypeError): # A view espera 'name' e 'mesa_id' no POST
# createComanda(request_sem_nome)
self.assertFalse(Comanda.objects.exists())
# Sem 'select-mesa'
request_sem_mesa = self.factory.post(
reverse('createComanda'),
{'name-comanda': 'Comanda Sem Mesa'}
)
request_sem_mesa.user = self.garcom_user
request_sem_mesa = self.add_middleware(request_sem_mesa)
# with self.assertRaises(TypeError): # A view espera 'name' e 'mesa_id' no POST
# createComanda(request_sem_mesa)
self.assertFalse(Comanda.objects.exists())

31
comandas/urls.py Normal file
View File

@@ -0,0 +1,31 @@
from django.urls import path
from comandas import htmx_views
from . import views
urlpatterns = [
path('', views.comandas, name='comandas'),
path('viewcomanda/', views.viewComanda, name='viewcomanda'),
path('createComanda/', views.createComanda, name='createComanda'),
path('editComanda/', views.editComanda, name='editComanda'),
path('addContaCliente/', views.addContaCliente, name='addContaCliente'),
path('notificacao/', views.notificacao, name='notificacao'),
path('editOrders/<int:productComanda_id>/<str:obs>', views.editOrders, name='editOrders'),
path('closeComanda/<int:comanda_id>/', views.closeComanda, name='closeComanda'),
path('listProduct/<int:comanda_id>/<str:product>/', views.listProduct, name='listProduct'),
path('product=<int:product_id>/comanda=<int:comanda_id>/', views.addProduct, name='addProduct'),
]
htmx_urlpatterns = [
path('removeProductComanda/<int:productComanda_id>/', htmx_views.removeProductComanda, name='removeProductComanda'),
path('reopenComanda<int:comanda_id>/', htmx_views.reopenComanda, name='reopenComanda'),
path('paymentComanda<int:comanda_id>/', htmx_views.paymentComanda, name='paymentComanda'),
path('paymentParcial<int:comanda_id>/', htmx_views.paymentParcial, name='paymentParcial'),
]
urlpatterns += htmx_urlpatterns

214
comandas/views.py Normal file
View File

@@ -0,0 +1,214 @@
from decimal import Decimal
from django.urls import reverse
from django.utils import timezone
from django.http import JsonResponse, HttpResponseRedirect
from django.contrib.auth.models import User
from django.shortcuts import render, redirect
from django.db.models import Count, F
from comandas.models import Comanda, ProductComanda, StockMovement, StockMovementType
from clients.models import Client
from payments.models import Payments, somar
from orders.models import Order
from products.models import Product, ProductComponent
from mesas.models import Mesa
from gestaoRaul.decorators import group_required
@group_required(groupName='Garçom')
def comandas(request):
comandas = Comanda.objects.filter(status__in=["OPEN", "PAYING"])
mesas = Mesa.objects.all()
return render(request, 'comandas.html', {'comandas': comandas, 'mesas': mesas})
@group_required(groupName='Garçom')
def viewComanda(request):
config = {
'taxa': False
}
id = request.GET.get('parametro')
comanda_id = int(id)
comanda = Comanda.objects.get(id=comanda_id)
consumo = ProductComanda.objects.filter(comanda=comanda_id)
parcial = Payments.objects.filter(comanda=comanda_id)
mesas = Mesa.objects.all()
clients = Client.objects.filter(active=True)
products_ordenados = ProductComanda.maisVendidos()
valores = somar(consumo,comanda)
return render(request, 'viewcomanda.html', {'config':config, 'valores':valores,'parcials':parcial,'clients':clients,'comanda': comanda, 'consumo': consumo, 'products': products_ordenados[:15],'mesas':mesas})
@group_required(groupName='Garçom')
def createComanda(request):
name = request.POST.get('name-comanda')
if name == '' or name == None or request.POST.get('select-mesa') == '' or request.POST.get('select-mesa') == None:
return HttpResponseRedirect(reverse('comandas'), status=400)
mesa_id = int(request.POST.get('select-mesa'))
mesa = Mesa.objects.get(id=mesa_id)
comanda = Comanda(name=name, mesa=mesa, user=request.user)
comanda.save()
return redirect(reverse('viewcomanda') + f'?parametro={comanda.id}')
@group_required(groupName='Garçom')
def editComanda(request):
name = request.POST.get('nameComanda')
comanda = Comanda.objects.get(id=int(request.POST.get('h-comandaId')))
mesa = Mesa.objects.get(id=int(request.POST.get('select-mesa')))
comanda.mesa = mesa
comanda.name = name
comanda.save()
return redirect('comandas')
@group_required(groupName='Gerente')
def addContaCliente(request):
comandaId = int(request.POST.get('idComanda'))
clientId = int(request.POST.get('select-client'))
comanda = Comanda.objects.get(id=comandaId)
client = Client.objects.get(id=clientId)
comanda.client = client
comanda.dt_close = timezone.now()
comanda.status = 'FIADO'
client.save()
comanda.save()
return redirect('comandas')
def notificacao(request):
fifteen_hours_ago = timezone.now() - timezone.timedelta(hours=12)
ordersPronto = Order.objects.filter(queue__gte=fifteen_hours_ago, finished__isnull=False)
grupoGarcom = request.user.groups.filter(name='Garçom').exists()
grupoGerente = request.user.groups.filter(name='Gerente').exists()
if grupoGarcom == True and grupoGerente == False:
if 'pronto' in request.COOKIES:
cookiesPronto = int(request.COOKIES['pronto'])
if len(ordersPronto) > cookiesPronto:
return JsonResponse({
'notificacao': 'true',
'pronto':len(ordersPronto),
'titulo': ordersPronto[len(ordersPronto)-1].id_comanda.name,
'corpo': ordersPronto[len(ordersPronto)-1].id_product.name,
})
else:
return JsonResponse({
'notificacao': 'false',
'pronto': len(ordersPronto),
})
else:
return JsonResponse({
'notificacao': 'true',
'pronto':len(ordersPronto),
'titulo': ordersPronto[len(ordersPronto)-1].id_comanda.name,
'corpo': ordersPronto[len(ordersPronto)-1].id_product.name,
})
else:
return JsonResponse({
'notificacao': 'false',
'pronto':len(ordersPronto),
})
@group_required(groupName='Garçom')
def editOrders(request, productComanda_id, obs):
order = Order.objects.get(productComanda=productComanda_id)
order.obs = obs
order.save()
msg = JsonResponse({
'type': 'broadcast',
'message': obs,
'local':'cozinha',
'tipo':'edit',
'id':order.id,
'speak': f'Pedido alterado! {order.id_product.name}, é {obs}.'
})
return JsonResponse({'status': 'ok', 'obs':order.obs})
@group_required(groupName='Garçom')
def closeComanda(request, comanda_id):
comanda = Comanda.objects.get(id=comanda_id)
comanda.status = "PAYING"
comanda.save()
return JsonResponse({'status': 'ok', 'obs':'order.obs'})
@group_required(groupName='Garçom')
def addProduct(request, product_id, comanda_id):
config = {
'taxa': False
}
obs = request.GET.get("obs")
product_comanda = ProductComanda(comanda_id=comanda_id, product_id=product_id)
product_comanda.save()
product = Product.objects.get(id=product_id)
comanda = Comanda.objects.get(id=comanda_id)
user = User.objects.get(id=request.user.id)
typeMovement = StockMovementType.objects.get(name="Venda - Comanda")
StockMovement.subTransactionStock(
product=product,
movement_type=typeMovement,
comanda=comanda,
user=user,
qtd=1,
obs= "Adicionado na comanda"
)
parcial = Payments.objects.filter(comanda=comanda)
if product.cuisine == True:
order = Order(id_comanda=comanda, id_product=product, productComanda=product_comanda, obs='')
order.save()
msg = JsonResponse({
'type': 'broadcast',
'message': f"""
<div class="m-card" id="m-card-{order.id}">
<h4>{product.name}</h4>
<h4 id="obs-{order.id}"> {order.obs}</h4>
<h4>{comanda.name} - {comanda.mesa.name}</h4>
<h4> {order.queue.strftime("%d/%m/%Y - %H:%M")}</h4>
<h4> Atendente: {comanda.user.first_name}</h4>
<form method="path" action="/pedidos/preparing/{order.id}/">
<button class="btn-primary" type="submit">Preparar</button>
</form>
</div>
""",
'local':'cozinha',
'tipo':'add',
'id':order.id,
'speak': f'Novo pedido! {product.name}, para {comanda.name}.'
})
try:
# Chama a função async dentro da view normal
async_to_sync(enviar_mensagem)(mensagem)
# return JsonResponse({"status": "Mensagem enviada com sucesso"})
except Exception as e:
print("Erro add product websocket: ",e)
# return JsonResponse({"status": "Erro", "erro": str(e)}, status=500)
# asyncio.run(enviar_mensagem(msg))
consumo = ProductComanda.objects.filter(comanda=comanda_id)
valores = somar(consumo,comanda)
return render(request, "htmx_components/comandas/htmx_list_products_in_comanda.html",{'config':config, 'valores':valores,'parcials':parcial,'consumo': consumo,'comanda':comanda})
def listProduct(request, comanda_id, product):
if product == '*':
allProducts = ProductComanda.maisVendidos()
else:
allProducts = Product.objects.filter(name__icontains=product)
products = []
for p in allProducts:
if p.active == True:
products.append(p)
return render(request, "htmx_components/comandas/htmx_list_products.html", {"products": products[:15],'comanda_id':comanda_id})

View File

@@ -1,9 +0,0 @@
mkdir djangotutorial
python -m venv meu_ambiente
django-admin startproject mysite djangotutorial
meu_ambiente\Scripts\Activate.ps1
python manage.py runserver
python manage.py startapp polls
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser

22
docker-compose.yml Normal file
View File

@@ -0,0 +1,22 @@
version: '3.8'
services:
django_app:
image: python:3.12-slim
container_name: rrbec_api_django
ports:
- "8069:8000"
restart: unless-stopped
environment:
- DEBUG=True
- ALLOWED_HOSTS=raulrockbar.com.br,api.raulrockbar.com.br
- CSRF_TRUSTED_ORIGINS=https://raulrockbar.com.br,https://api.raulrockbar.com.br
volumes:
- /DATA/AppData/rrbec-api-django:/app
- /DATA/AppData/rrbec-api-django/static:/app/static
- /DATA/AppData/rrbec-api-django/db:/app/db
working_dir: /app
command: ["/bin/sh", "-c", "apt-get update && apt-get install -y git && rm -rf app_clone && git clone --branch api --depth 1 https://github.com/welton89/RRBEC.git app_clone && cp -r app_clone/* . && rm -rf app_clone && pip install -r requirements.txt && python manage.py collectstatic --noinput && python manage.py migrate --noinput && gunicorn gestaoRaul.wsgi:application --bind 0.0.0.0:8000"]

34
gestaoRaul/api_urls.py Normal file
View File

@@ -0,0 +1,34 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from orders.api_views import OrderViewSet
from products.api_views import ProductViewSet
from clients.api_views import ClientViewSet
from mesas.api_views import MesaViewSet
from comandas.api_views import ComandaViewSet, ProductComandaViewSet
from categories.api_views import CategoriesViewSet
from typePay.api_views import TypePayViewSet
from payments.api_views import PaymentsViewSet
from rest_framework_simplejwt.views import (
TokenRefreshView,
)
from login.api_views import MyTokenObtainPairView, UserViewSet
from sync.api_views import ChangeLogViewSet
router = DefaultRouter()
router.register(r'orders', OrderViewSet, basename='order')
router.register(r'products', ProductViewSet, basename='product')
router.register(r'clients', ClientViewSet, basename='client')
router.register(r'mesas', MesaViewSet, basename='mesa')
router.register(r'comandas', ComandaViewSet, basename='comanda')
router.register(r'items-comanda', ProductComandaViewSet, basename='items-comanda')
router.register(r'categories', CategoriesViewSet, basename='category')
router.register(r'payment-types', TypePayViewSet, basename='payment-type')
router.register(r'payments', PaymentsViewSet, basename='payment')
router.register(r'users', UserViewSet, basename='user')
router.register(r'sync', ChangeLogViewSet, basename='sync')
urlpatterns = [
path('', include(router.urls)),
path('token/', MyTokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]

Some files were not shown because too many files have changed in this diff Show More