Compare commits

...

208 Commits

Author SHA1 Message Date
43756952fb Merge pull request 'Creada vista para listar los cuadres de tarro #90' (#91) from view_for_jar_reconciliation_#90 into main
Reviewed-on: #91
2025-02-02 23:08:00 -05:00
6a346bdf8a #90 fix(Frontend): set default tab in Reconciliation summary. 2025-02-02 23:04:54 -05:00
2a983c8056 #90 fix(Frontend): fix datetime in Reconciliation summary. 2025-02-02 22:49:38 -05:00
2c6449c727 #90 feat(Frontend): add Purchases in cuadres_de_tarro. 2025-02-02 22:48:21 -05:00
77a0c4ac0d #90 feat(Frontend): add Purchases in cuadres_de_tarro. 2025-02-02 22:40:31 -05:00
2fcf884cce #90 feat(api): add payment method to sales. 2025-01-28 09:17:48 -05:00
75d39c6ca7 #90 feat(frontend): add modal to ReconciliationJarIndex. 2025-01-18 12:53:53 -05:00
a5d4c1977a #90 feat(frontend): minor fix. 2025-01-18 12:43:50 -05:00
d85ad7cc38 #90 feat(frontend): getReconciliation api method. 2025-01-18 12:43:01 -05:00
aa45ea44ac #90 feat(frontend): getReconciliation api method. 2025-01-18 12:42:33 -05:00
1b06818583 #90 feat(frontend): add ReconciliationJarView.vue. 2025-01-18 12:22:45 -05:00
baa0677e7a #90 feat(frontend): add cuadres_de_tarro page. 2025-01-12 01:23:17 -05:00
d9d3239662 #90 api(frontend): add getListReconcliations. 2025-01-12 01:20:33 -05:00
8bc2d02572 #90 test: minor fix. 2025-01-12 01:18:53 -05:00
c9cfc7f873 #90 feat(ReconcilaitionJar): create api view with pagination. 2025-01-12 00:24:30 -05:00
2a937653df Merge pull request 'asegurnado paginas administrativas #88' (#89) from secure_admin_pages_#88 into main
Reviewed-on: #89
2025-01-11 19:24:43 -05:00
e5ae1bb142 feat(frontend): using api for admin valid code. 2025-01-11 19:22:48 -05:00
9ff1deb687 dev(rakeFile): add terminal django. 2025-01-11 19:21:00 -05:00
0caa6fbb56 #88 feat(AdminCode): create model and api view. 2025-01-11 19:08:05 -05:00
a7e3b9aaa8 #88 refactor(frontend): remove method. 2025-01-11 18:10:44 -05:00
0308da7370 #88 refactor(frontend): extract to Component. 2025-01-11 18:02:28 -05:00
0dec800637 #88 feat(frontend): initial secure pages. 2025-01-11 17:05:39 -05:00
0a88641d34 Merge pull request 'Generado repositorio para consultar la api #86' (#87) from generate_repository_code_#86 into main
Reviewed-on: #87
2025-01-11 16:05:31 -05:00
871a82eee5 #84 refactor(front): moved to api createCustomer. 2025-01-11 15:44:07 -05:00
9ad8ff8706 #84 refactor(front): moved to api create reconciliation jar. 2025-01-11 14:05:01 -05:00
d9f6be8b54 #84 refactor(front): moved to api purchases_for_reconciliation to. 2025-01-11 12:57:25 -05:00
34259921d9 #84 refactor(frontend): extact to method django-api. 2025-01-11 12:46:29 -05:00
8f9917c3a4 #84 refactor(frontend): summaryPurchase moved to repository. 2025-01-11 12:14:48 -05:00
fcb83d05fb #84 refactor(frontend): send purchase moved to repository. 2025-01-11 11:34:37 -05:00
6d6322b0cd #84 refactor(frontend): extract methods to repository. 2025-01-11 10:37:17 -05:00
a4048473ae Merge pull request 'refactor_endpoints_to_api_views #84' (#85) from refactor_endpoints_to_api_views_#84 into main
Reviewed-on: #85
2024-12-31 14:51:05 -05:00
9a6a931481 fix(Frontend): remove non existing component. 2024-12-31 14:35:44 -05:00
432b7dad74 #84 refactor(SaleSummary): rename saleline_set to lines at serializer. 2024-12-31 14:30:20 -05:00
69d8b1d2ad #84 refactor(SaleSummary): move to apiview. 2024-12-31 13:51:11 -05:00
8b7c2efcb3 #84 refactor(SalesForReconciliation): move to apiview. 2024-12-31 13:26:47 -05:00
5dff7565f4 #84 refactor(paymentMethods): move to apiview. 2024-12-31 13:09:16 -05:00
023beaa0ee Merge pull request 'Generando cuadre del tarro en vuetify' (#83) from streamline_reconciliation_jar_process_#69 into main
Reviewed-on: #83
2024-12-28 17:13:35 -05:00
b5fdd7fefd #69 fix(migrations): conflict. 2024-12-28 17:13:55 -05:00
eaf1afdcb4 Merge branch 'main' into streamline_reconciliation_jar_process_#69 2024-12-28 17:07:18 -05:00
5cfefdf91a #69 feat(ReconciliationJar): link purchases with other payment methods. 2024-12-28 17:01:51 -05:00
9c0eebd07d #69 refactor(ReconciliationJar): extract to method. 2024-12-14 22:34:58 -05:00
8ab7903a0a #69 test(ReconciliationJar): fix tests. 2024-12-14 21:37:24 -05:00
1b425542b3 #69 feat(ReconciliationJar): send reconciliation jar from frontend 2024-12-14 19:42:59 -05:00
f6620db6e2 ci(Rakefile): add start command. 2024-12-14 10:00:11 -05:00
1f2f484e95 #69 test(ReconciliationJar): add failed case. 2024-12-14 09:59:26 -05:00
ef721a6b53 #69 feat(ReconciliationJar): add purchases to list. 2024-12-13 17:21:29 -05:00
f0201a86b2 #69 feat(ReconciliationJar): accept post creation. 2024-12-02 22:52:04 -05:00
bea08da17d #69 feat(ReconciliationJar): Add total_cash_purchases field. 2024-12-02 22:00:05 -05:00
a3d5fb1b45 ci(Rake): add dev migrations tasks. 2024-12-02 21:56:23 -05:00
4679170ab9 #69 refactor(ReconciliationJar): remove old form. 2024-12-02 21:44:21 -05:00
0d61e457c7 #69 refactor(ReconciliationJar): remove old view. 2024-12-02 21:43:48 -05:00
3294b8e814 refactor(test): rename method. 2024-12-02 21:28:20 -05:00
3189363ba9 style: minor fix. 2024-12-02 21:22:36 -05:00
0a64373037 ci(Test): add task tests to Rake. 2024-12-02 21:22:06 -05:00
9a20212b27 #69 feat(Reconciliation):get purchases from backend. 2024-11-17 23:55:54 -05:00
a6b4c1c5b6 #69 feat(Reconciliation):get purchases for reconciliation endpoint. 2024-11-17 23:15:21 -05:00
b7984f7556 #69 style(Reconciliation): format total on sales. 2024-11-17 00:07:18 -05:00
ef1a520838 #69 feat(Reconciliation): show summary on modal. 2024-11-17 00:02:08 -05:00
bd6d4221b2 #69 feat(Reconciliation): improve datetime on form. 2024-11-16 23:10:43 -05:00
9aa543662b #69 feat(Reconciliation): improve form. 2024-11-16 22:58:28 -05:00
746686afcc #69 style(Reconciliation): cards to tabs. 2024-11-16 20:36:38 -05:00
c709dad36e #69 base ReconciliationJar. 2024-11-16 16:48:07 -05:00
Rodia
543e927fb0 Merge pull request 'Fix: closed #79' (#81) from PurchaseDateTimeField into main
Reviewed-on: #81
2024-11-16 16:00:27 -05:00
fe1e6e8336 Fix: closed #79 2024-11-16 15:59:56 -05:00
Rodia
00c6bac1d2 Merge pull request 'Feat: closed #74' (#80) from LineaCompraCantidadDiferenteCero into main
Reviewed-on: #80
2024-11-16 15:59:44 -05:00
4ed6bb9024 Feat: closed #74 2024-11-16 13:09:01 -05:00
Rodia
b4467e0292 Merge pull request 'Fix: #75' (#78) from BotonIrAComprarResumenDeCompra into main
Reviewed-on: #78
2024-11-16 12:30:08 -05:00
7c2a7fb1a1 Fix: #75 2024-11-16 12:30:17 -05:00
Rodia
8f48accb4f Merge pull request 'Fix: #76' (#77) from AumentarCampoProductoLineaDeCompra into main
Reviewed-on: #77
2024-11-16 11:43:15 -05:00
d0edba9c28 Fix: #76 2024-11-16 11:43:13 -05:00
6aca2007e0 #69 feat(View): add reconciliation jar components. 2024-11-15 17:44:30 -05:00
b2756ac7ce Merge pull request 'Se ajusta validaciones de formulario y advierte al usuario cuando abandona la página por iteraciones en el navegador.' (#73) from warn_before_leave_purchase_#67 into main
Reviewed-on: #73
2024-11-11 23:08:53 -05:00
159bd737c4 feat(Purchase): warn user whit browser actions #67. 2024-11-11 22:45:47 -05:00
201333ab4b feat(Purchase): warn user invalid form #71 2024-11-11 21:47:46 -05:00
a5ac06704b feat(Purchase): warn user when try eliminate the last line 2024-11-11 21:42:13 -05:00
f1b8cbdce1 fix(Purchase): validation of form. 2024-11-11 21:17:12 -05:00
2d86aba3e5 Merge pull request 'Habilitando la selección de método de pago en la compra' (#72) from chose_pay_method_on_purchase_#47 into main
Reviewed-on: #72
2024-11-11 16:27:21 -05:00
edea82e77b refactor(Purchase): remove unused methods. 2024-11-11 16:23:21 -05:00
d047edaa3f machete(Purchase): translate Cash to Efectivo. 2024-11-11 16:16:33 -05:00
5c24266e7b fix(Purchase): remove fixed payment Methods. 2024-11-11 16:15:24 -05:00
9d602c8ddc feat(PaymentMethods): create endpoint. 2024-11-11 16:09:16 -05:00
99f2f77b78 fix(test): fix create sale api test. 2024-11-11 16:08:20 -05:00
4f0f899c70 fix(Purchase): create Purchase with other payments methods. 2024-11-11 15:36:21 -05:00
c85f0554fb refactor(Purchase): move change cash to componet. 2024-11-11 15:28:08 -05:00
acd9bf53c6 feat(Purchase): add change cash. 2024-11-11 14:33:51 -05:00
8f62dfb9ec feat (frontend): add payment method on summary purchase. 2024-11-11 13:47:11 -05:00
ea77124ee4 feat (frontend): add payment method. 2024-11-09 14:38:38 -05:00
66495e25ff style(Purchase) 2024-11-09 13:55:59 -05:00
b7c4cf5d44 refactoring(Purchase): simplify payment_method #47 2024-11-09 13:49:24 -05:00
d5b7c99b79 Merge pull request 'Arreglar enlaces del menú' (#66) from fix_menu_links_#65 into main
Reviewed-on: #66
2024-11-04 23:20:36 -05:00
6bef38e457 fix(Menu): #65 2024-11-04 23:18:10 -05:00
b80e415393 rename(frontend): index to compra 2024-11-04 19:27:55 -05:00
8ba20acaea Merge pull request 'Redirigiendo al resumen cuando se finaliza una compra' (#64) from redirect_to_summary_when_end_purchase_#55 into main
Reviewed-on: #64
2024-11-02 16:55:01 -05:00
65e99b7ce2 view: fix total on summary purchase. 2024-11-02 16:54:07 -05:00
f6146c177b view: fix subtotal on summary purchase. 2024-11-02 16:53:01 -05:00
7c36524763 Redirect to summary after purchase #55 2024-11-02 16:46:45 -05:00
ac5c962ca7 Merge pull request 'Habilitando Resumen de compra' (#63) from enable_summary_purchase_#55 into main
Reviewed-on: #63
2024-11-02 15:31:23 -05:00
4472d8b6b8 view(Frontend): make dinamic summary_purchase. 2024-11-02 15:27:26 -05:00
8b15d9dd9d view (Frontend): purchase summary. 2024-11-02 14:52:44 -05:00
Rodia
02a010b50d Merge pull request 'Feat: issue #26' (#61) from MeasurementUnitField into main
Reviewed-on: #61
2024-11-02 13:59:12 -05:00
50534ef5b1 Feat: issue #26 2024-11-02 13:58:40 -05:00
8c00b89fb8 frontend: rename summary_purchase page. 2024-11-02 13:58:10 -05:00
b134b88791 summary purchase: generate draft. 2024-11-02 13:54:09 -05:00
1519b3c8bb django(View): add purchase summary json endpoint. 2024-11-02 12:51:26 -05:00
Rodia
589b7c0bfb Merge pull request 'Feat: Datos de contacto customer #46' (#59) from DatosDeClienteAPI into main
Reviewed-on: #59
2024-11-02 12:19:22 -05:00
2ed8b5b3a5 Feat: Datos de contacto customer #46 2024-11-02 12:18:39 -05:00
Rodia
ffa1622870 Merge pull request 'ValidarQueNoSePuedaCrearVentaSinLineas' (#58) from ValidarQueNoSePuedaCrearVentaSinLineas into main
Reviewed-on: #58
2024-11-02 11:46:12 -05:00
a90fb4d937 Fix: Linea de Cero debe ser mayor a cero issue RedEcovida#57 2024-11-02 11:40:51 -05:00
2a908d4e05 Fix: No permitir eliminar linea de venta issue #4 2024-11-02 11:28:10 -05:00
4cbeaa2560 refactor(test): sentence to method. 2024-11-02 11:05:39 -05:00
eeb9821675 Feat: Cargar ultimo cliente creado 2024-11-02 10:54:14 -05:00
2ea8cc7fd8 Feat: Actualizar listado de Clientes al crear uno nuevo 2024-11-02 10:28:15 -05:00
95fab71898 Merge pull request 'Importación de clientes por CSV' (#56) from import_customers into main
Reviewed-on: #56
2024-10-26 17:24:21 -05:00
cosmos
83f3bbdc85 Add migrate 2024-10-26 17:22:58 -05:00
cosmos
5910c0c227 Import Customers 2024-10-26 17:16:27 -05:00
8a2a568739 Fix: Vite not found 2024-10-26 17:08:10 -05:00
49ac668c14 begining purchase summar. 2024-10-26 16:06:47 -05:00
c2e91328fb Feat: Create Customer Post API 2024-10-26 14:38:58 -05:00
18df5742d5 Feat: Create Customer Modal 2024-10-26 12:57:14 -05:00
91f3d897e5 Fix: issue # 53 2024-10-12 09:00:33 -05:00
a97c424abc Feat: issue #53 2024-10-12 08:58:34 -05:00
0477e00f75 Merge pull request 'view(Purchase): enable autocomplete products #44' (#51) from enable_autocomplete_products_in_purchase into main
Reviewed-on: #51
2024-10-05 15:32:00 -05:00
63f6b3da10 view(Purchase): enable autocomplete products #44 2024-10-05 15:31:31 -05:00
d47afabade Merge pull request 'view(Purchase): add autocomplete customer on purchase #43.' (#50) from activate_customer_search_in_purchase into main
Reviewed-on: #50
2024-10-05 13:28:44 -05:00
8506f7c1bd view(Purchase): add autocomplete customer on purchase #43. 2024-10-05 13:28:10 -05:00
Rodia
5df97c588e Merge pull request 'BarraDeNavegacion' (#49) from BarraDeNavegacion into main
Reviewed-on: #49
2024-10-05 12:35:24 -05:00
f110c750a0 Fix: Unused variables 2024-10-05 12:36:43 -05:00
7a89eee9fb Merge branch 'main' into BarraDeNavegacion 2024-10-05 12:34:32 -05:00
80ef5c4d9d Fix: Barra de Navegacion 2024-10-05 12:07:41 -05:00
506a280f6e Feat: Barra de Navegacion en Formulario de Compras 2024-10-05 11:48:59 -05:00
0b8f9e54e9 Merge pull request 'refactor(Vue): avoid manual div and style class.' (#42) from #41_try_to_avoid_manual_div_and_style_class into main
Reviewed-on: #42
2024-10-05 11:31:55 -05:00
ee37bfe2cf refactor(Vue): avoid manual div and style class. 2024-10-05 11:28:24 -05:00
dd62baa3f0 Fix: Window NavBar 2024-10-05 11:10:42 -05:00
46904d0825 Merge pull request 'Changa purchese' (#40) from cosmos into main
Reviewed-on: #40
2024-10-05 10:44:39 -05:00
172915d1c1 Merge branch 'main' into cosmos 2024-10-05 10:44:13 -05:00
d00f3e60fd Fix: Archivo Faltante NavBar 2024-10-05 10:38:25 -05:00
2d519f7ca9 Feat: Se agrega barra de Navegacion 2024-10-04 20:39:53 -05:00
cosmos
b2de6d3e9d Changa purchese 2024-10-02 20:50:40 -05:00
982b2d0c32 Merge pull request 'Quitar el tema oscuro' (#39) from change_dark_theme into main
Reviewed-on: #39
2024-09-28 19:51:09 -05:00
7dc843d8f5 view(theme): change dark theme. 2024-09-28 19:50:33 -05:00
78c81549a2 Merge pull request 'vuetify_set_unit_price_automatically_#37' (#38) from vuetify_set_unit_price_automatically_#37 into main
Reviewed-on: #38
2024-09-28 19:37:50 -05:00
1519453591 view(Purchase): minor fixes. 2024-09-28 19:21:58 -05:00
6199a93061 view(Purchase): readonly unit_price. 2024-09-28 18:54:17 -05:00
71e3808c78 view(Purchase): Set price automatically based on product. 2024-09-28 18:50:57 -05:00
cf913884f0 Merge pull request 'Se realiza compra desde vuetify' (#36) from achive_purchase_from_vuetify_issue_#35 into main
Reviewed-on: #36
2024-09-28 17:46:57 -05:00
413c77b1fe view(Purchase): redirect after succesfull purchase. 2024-09-28 17:43:55 -05:00
0c065beb30 view(Purchase): generate purchase from vuetify. 2024-09-28 17:23:50 -05:00
f2befda953 view(Purchase): add products to purchase. 2024-09-28 16:20:47 -05:00
98d173bf00 fix(Vuetify): minor fix. 2024-09-28 16:10:59 -05:00
a517c2323f Merge pull request 'Adicionando viuetify y dockers para correr el proyecto completo' (#34) from experiment_sale_with_vuetify_#33 into main
Reviewed-on: #34
2024-09-28 16:03:56 -05:00
9857b90cd4 ci(dev): fix port npm to run on docker. 2024-09-28 16:03:03 -05:00
f14d27e83c ci(Docker): try to run npm on docker. 2024-09-28 15:49:30 -05:00
2c8911fb78 ci(Dev): Using docker to serve django and vue on same host. 2024-09-28 14:04:13 -05:00
a7147d1850 view(Purchase): add client list from requesta as example. 2024-09-28 14:03:46 -05:00
8329f3231d view(Front): seek date on purchase form. 2024-09-21 14:16:22 -05:00
ffbb2e53d8 feat(Front): add purchase vue form. 2024-09-21 13:03:22 -05:00
cf93fecf9f ci(Frontend): create vuetify proyect. 2024-09-12 23:02:35 -05:00
a43891942b Merge pull request 'Implementando api' (#32) from implement_api into main
Reviewed-on: #32
2024-09-08 22:39:15 -05:00
86b231a828 Merge branch 'main' into implement_api 2024-09-08 22:36:36 -05:00
19a618a671 fix(api): remove SaleWithLinesViewSet. 2024-09-08 22:33:41 -05:00
5adf9a9ce7 feat(api): implement endpoints to products, customer and sales. 2024-09-08 21:42:35 -05:00
eaa856e9da feat(api): implemented create sale as api endpoint. 2024-09-07 21:33:49 -05:00
Rodia
c0329c789a Merge pull request 'ExportTrytonSaleLine' (#31) from ExportTrytonSaleLine into main
Reviewed-on: #31
2024-09-07 15:25:45 -05:00
a83cd970e6 Feat: Export Ventas Para Tryton 2024-09-07 14:58:11 -05:00
b81d95a9ba B
C
iMerge branch 'main' of ssh://gitea.onecluster.org:6666/OneTeam/don_confiao into ExportTrytonSaleLine
2024-09-07 08:10:57 -05:00
603230beda Fix: Export Sale Line 2024-09-07 08:07:35 -05:00
02fbf51659 Merge pull request 'Agregando precio del producto al seleccionar el producto #24' (#30) from automatic_product_price_#24 into main
Reviewed-on: #30
2024-08-31 13:01:54 -05:00
74bc963096 Merge branch 'main' into automatic_product_price_#24 2024-08-31 13:01:14 -05:00
99ae098c1d fix(Purchase): set price based on product in all lines. 2024-08-31 12:38:17 -05:00
1b1a504bf5 feat(view): set price based on product select on first line from purchase. 2024-08-31 12:04:01 -05:00
f1d96467d6 feat(buy): send products list to buy view. 2024-08-31 09:46:03 -05:00
Rodia
0b5163c680 Merge pull request 'Fix: Se corrijen etiquetas de form' (#25) from BugBotonComprarNoFunciona into main
Reviewed-on: #25
2024-08-24 10:46:13 -05:00
9d06a588c8 Fix: Se corrijen etiquetas de form 2024-08-24 10:46:42 -05:00
23ec2bc298 refactor: move Products list logic to model. 2024-08-24 10:30:58 -05:00
77361c13db Merge pull request 'feature/estilos_don_confiao_comprar' (#23) from feature/estilos_don_confiao_comprar into main
Reviewed-on: #23
Reviewed-by: mono <mono@noreply.onecluster.org>
Reviewed-by: Rodia <rodia@noreply.onecluster.org>
2024-08-21 16:33:34 -05:00
e9e4e0e38d Fix: Se cambia tono de verde y shadow en botones 2024-08-19 21:30:09 -05:00
384fd3c7f8 Feat: Estilo botones Resumen de Venta 2024-08-19 19:44:58 -05:00
d9f54ce12b Feat: Se agrega logo y titulo a sidebar 2024-08-19 19:25:40 -05:00
157095dede Se agregan estilos a resumen de compra 2024-08-19 18:52:06 -05:00
027f7d75b6 fix: Scrollear lineas oculta botón de agregar 2024-08-19 18:34:28 -05:00
b0251b882e Se agregan estilos de lineas de compra 2024-08-19 18:16:42 -05:00
edaf8f4c77 Fix: Se agrega scroll para desbordamiento de lineas 2024-08-19 17:39:51 -05:00
939745cd54 Se agregan estilos de sidebar 2024-08-19 17:05:30 -05:00
e84c45b238 Merge pull request 'Creando plantilla base con menú #21' (#22) from make_menu_and_base_template_#21 into main
Reviewed-on: #22
2024-08-17 19:35:24 -05:00
f250cdada2 view(css): add style to main menu. 2024-08-17 19:07:06 -05:00
0e15192f37 view: using base template. 2024-08-17 18:25:41 -05:00
980deb61f9 feat(View): add base template and menu. 2024-08-17 18:25:05 -05:00
844372d585 Merge pull request 'Registrar pagos completos con compras en efectivo #13' (#20) from make_payment_with_efective_purchase_#13 into main
Reviewed-on: #20
2024-08-17 16:47:02 -05:00
9260230d38 Merge branch 'main' into make_payment_with_efective_purchase_#13 2024-08-17 16:46:05 -05:00
f847a0e16a migation. 2024-08-17 16:40:13 -05:00
0c95c21666 feat(Payments): Generate cash payment with purchase. 2024-08-17 16:39:29 -05:00
1f37e57e00 feat(Buy): add transaction on buy process. 2024-08-17 15:07:43 -05:00
7eb8f40d7a style(views): minor fix. 2024-08-17 14:57:46 -05:00
a401029082 fix view(Buy): rewrite save process. 2024-08-17 14:50:49 -05:00
2a9a73c430 refactor(Forms): rename varible. 2024-08-17 14:34:10 -05:00
4c0c4737ac migration(Customer): default None. 2024-08-17 14:29:20 -05:00
204bdbcb33 limit to CASH methods type in purchases. 2024-08-17 12:51:38 -05:00
1a54426af6 style(models): minor fix. 2024-08-17 12:49:43 -05:00
45199030a0 Feat: Calculo de subtotal para muchas lineas 2024-08-17 11:31:02 -05:00
c10aa4b9ca Feat: Se agrega calculo de subtotal para 1 sola linea 2024-08-17 11:31:02 -05:00
efad80970b Merge pull request 'make customer name required field #17' (#19) from make_customer_name_required_#17 into main
Reviewed-on: #19
2024-08-17 10:30:30 -05:00
8f611a523b make customer name required field. 2024-08-17 09:48:36 -05:00
398f7f2c36 Merge pull request 'fix: purchase summary test.' (#18) from fix_master_customer_test into main
Reviewed-on: #18
2024-08-17 08:45:44 -05:00
de4bb52346 fix: purchase summary test. 2024-08-17 08:44:06 -05:00
521d3b63fa Merge pull request 'Añadida vista resumen de compra' (#16) from summary_view_purchase into main
Reviewed-on: #16
2024-08-10 13:03:11 -05:00
cosmos
6decc963f6 Fix(views): Add sub total to purchase summary 2024-08-10 13:01:04 -05:00
cosmos
91bb2bce93 Add basic information 2024-08-10 10:55:03 -05:00
cosmos
108d983d74 Add Summary View Purchase 2024-08-03 12:37:21 -05:00
110 changed files with 9525 additions and 314 deletions

6
.gitignore vendored
View File

@@ -327,3 +327,9 @@ pip-selfcheck.json
# End of https://www.toptal.com/developers/gitignore/api/emacs,python,django,venv
/tienda_ilusion/don_confiao/static/frontend/
/tienda_ilusion/don_confiao/frontend/don-confiao/.vite/
/tienda_ilusion/don_confiao/frontend/don-confiao/.eslintrc.js
/tienda_ilusion/don_confiao/frontend/don-confiao/.eslintrc-auto-import.json
/tienda_ilusion/don_confiao/frontend/don-confiao/.editorconfig
/tienda_ilusion/don_confiao/frontend/don-confiao/.browserslistrc

87
Rakefile Normal file
View File

@@ -0,0 +1,87 @@
require 'bundler/setup'
require 'yaml'
require 'digest'
DOCKER_COMPOSE='docker-compose.yml'
desc 'entorno vivo'
namespace :live do
task :up do
compose('up', '--build', '-d', compose: DOCKER_COMPOSE)
end
desc 'monitorear salida'
task :tail do
compose('logs', '-f', 'django', compose: DOCKER_COMPOSE)
end
desc 'monitorear salida'
task :tail_end do
compose('logs', '-f', '-n 50', 'django', compose: DOCKER_COMPOSE)
end
desc 'iniciar entorno'
task :start do
compose('start', compose: DOCKER_COMPOSE)
end
desc 'bajar entorno'
task :down do
compose('down', compose: DOCKER_COMPOSE)
end
desc 'detener entorno'
task :stop do
compose('stop', compose: DOCKER_COMPOSE)
end
desc 'eliminar entorno'
task :del do
compose('down', '-v', '--rmi', 'all', compose: DOCKER_COMPOSE)
end
desc 'reiniciar entorno'
task :restart do
compose('restart', compose: DOCKER_COMPOSE)
end
desc 'detener entorno'
task :stop do
compose('stop', compose: DOCKER_COMPOSE)
end
desc 'terminal'
task :sh do
compose('exec', 'django', 'bash')
end
end
desc 'Desarrollo'
namespace :dev do
desc 'correr test de django'
task :test do
compose('exec', 'django', 'python', '/app/manage.py', 'test', '/app/don_confiao')
end
desc 'terminal django'
task :djangoShell do
compose('exec', 'django', 'python', '/app/manage.py', 'shell')
end
desc 'crear migraciones'
task :makemigrations do
compose('exec', 'django', 'python', '/app/manage.py', 'makemigrations')
end
desc 'aplicar migraciones'
task :migrate do
compose('exec', 'django', 'python', '/app/manage.py', 'migrate')
end
end
def compose(*arg, compose: DOCKER_COMPOSE)
sh "docker compose -f #{compose} #{arg.join(' ')}"
end

8
django.Dockerfile Normal file
View File

@@ -0,0 +1,8 @@
from python:3.12-slim
WORKDIR /app/
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
CMD ["python", "manage.py", "runserver", "0.0.0.0:9090"]

27
docker-compose.yml Normal file
View File

@@ -0,0 +1,27 @@
services:
nginx:
build:
context: ./
dockerfile: nginx.Dockerfile
ports:
- "7000:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- ./tienda_ilusion/don_confiao/static/frontend:/var/www/frontend/
django:
build:
context: ./
dockerfile: django.Dockerfile
volumes:
- ./tienda_ilusion:/app/
ports:
- "7001:9090"
frontend:
build:
context: ./
dockerfile: vuetify.Dockerfile
volumes:
- ./tienda_ilusion/don_confiao/frontend/don-confiao:/app/
ports:
- "7003:3000"

10
nginx.Dockerfile Normal file
View File

@@ -0,0 +1,10 @@
FROM nginx:latest
# Copiamos el archivo de configuración NGINX
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Establecemos la variable de entorno para el proxy inverso
ENV DJANGO_PROXY_URL http://django:8000
# Creamos un directorio estático
RUN mkdir -p /var/www/frontend

25
nginx.conf Normal file
View File

@@ -0,0 +1,25 @@
server {
listen 80;
server_name donconfiao.org;
# location /frontend {
# alias /var/www/frontend/;
# autoindex on;
# }
location /frontend {
proxy_pass http://frontend:3000/frontend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://django:9090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -1 +1,2 @@
Django==5.0.6
djangorestframework

View File

@@ -0,0 +1,146 @@
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework.status import HTTP_400_BAD_REQUEST
from rest_framework.views import APIView
from rest_framework.pagination import PageNumberPagination
from .models import Sale, SaleLine, Customer, Product, ReconciliationJar, PaymentMethods, AdminCode
from .serializers import SaleSerializer, ProductSerializer, CustomerSerializer, ReconciliationJarSerializer, PaymentMethodSerializer, SaleForRenconciliationSerializer, SaleSummarySerializer
from decimal import Decimal
import json
class Pagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
class SaleView(viewsets.ModelViewSet):
queryset = Sale.objects.all()
serializer_class = SaleSerializer
def create(self, request):
data = request.data
customer = Customer.objects.get(pk=data['customer'])
date = data['date']
lines = data['saleline_set']
payment_method = data['payment_method']
sale = Sale.objects.create(
customer=customer,
date=date,
payment_method=payment_method
)
for line in lines:
product = Product.objects.get(pk=line['product'])
quantity = line['quantity']
unit_price = line['unit_price']
SaleLine.objects.create(
sale=sale,
product=product,
quantity=quantity,
unit_price=unit_price
)
return Response(
{'id': sale.id, 'message': 'Venta creada con exito'},
status=201
)
class ProductView(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
class CustomerView(viewsets.ModelViewSet):
queryset = Customer.objects.all()
serializer_class = CustomerSerializer
class ReconciliateJarView(APIView):
def post(self, request):
data = request.data
cash_purchases_id = data.get('cash_purchases')
serializer = ReconciliationJarSerializer(data=data)
if serializer.is_valid():
cash_purchases = Sale.objects.filter(pk__in=cash_purchases_id)
if not self._is_valid_total(cash_purchases, data.get('total_cash_purchases')):
return Response(
{'error': 'total_cash_purchases not equal to sum of all purchases.'},
status=HTTP_400_BAD_REQUEST
)
reconciliation = serializer.save()
other_purchases = self._get_other_purchases(data.get('other_totals'))
self._link_purchases(reconciliation, cash_purchases, other_purchases)
return Response({'id': reconciliation.id})
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
def get(self, request):
reconciliations = ReconciliationJar.objects.all()
serializer = ReconciliationJarSerializer(reconciliations, many=True)
return Response(serializer.data)
def _is_valid_total(self, purchases, total):
calculated_total = sum(p.get_total() for p in purchases)
return calculated_total == Decimal(total)
def _get_other_purchases(self, other_totals):
if not other_totals:
return []
purchases = []
for method in other_totals:
purchases.extend(other_totals[method]['purchases'])
if purchases:
return Sale.objects.filter(pk__in=purchases)
return []
def _link_purchases(self, reconciliation, cash_purchases, other_purchases):
for purchase in cash_purchases:
purchase.reconciliation = reconciliation
purchase.clean()
purchase.save()
for purchase in other_purchases:
purchase.reconciliation = reconciliation
purchase.clean()
purchase.save()
class PaymentMethodView(APIView):
def get(self, request):
serializer = PaymentMethodSerializer(PaymentMethods.choices, many=True)
return Response(serializer.data)
class SalesForReconciliationView(APIView):
def get(self, request):
sales = Sale.objects.filter(reconciliation=None)
grouped_sales = {}
for sale in sales:
if sale.payment_method not in grouped_sales.keys():
grouped_sales[sale.payment_method] = []
serializer = SaleForRenconciliationSerializer(sale)
grouped_sales[sale.payment_method].append(serializer.data)
return Response(grouped_sales)
class SaleSummary(APIView):
def get(self, request, id):
sale = Sale.objects.get(pk=id)
serializer = SaleSummarySerializer(sale)
return Response(serializer.data)
class AdminCodeValidateView(APIView):
def get(self, request, code):
codes = AdminCode.objects.filter(value=code)
return Response({'validCode': bool(codes)})
class ReconciliateJarModelView(viewsets.ModelViewSet):
queryset = ReconciliationJar.objects.all().order_by('-date_time')
pagination_class = Pagination
serializer_class = ReconciliationJarSerializer

View File

@@ -0,0 +1,4 @@
nombre,correo,telefono
Alejandro Ayala,mono@disroot.org,3232321
Mono Francisco,pablo@onecluster.org,321312312
Pablo Bolivar,alejo@onecluster.org,3243242
1 nombre correo telefono
2 Alejandro Ayala mono@disroot.org 3232321
3 Mono Francisco pablo@onecluster.org 321312312
4 Pablo Bolivar alejo@onecluster.org 3243242

View File

@@ -0,0 +1 @@
#!/usr/bin/env python3

View File

@@ -3,7 +3,7 @@ from django.forms.models import inlineformset_factory
from django.forms.widgets import DateInput, DateTimeInput
from .models import Sale, SaleLine, ReconciliationJar, PaymentMethods
from .models import Sale, SaleLine, PaymentMethods
readonly_number_widget = forms.NumberInput(attrs={'readonly': 'readonly'})
@@ -12,6 +12,10 @@ class ImportProductsForm(forms.Form):
csv_file = forms.FileField()
class ImportCustomersForm(forms.Form):
csv_file = forms.FileField()
class PurchaseForm(forms.ModelForm):
class Meta:
model = Sale
@@ -50,29 +54,13 @@ class PurchaseSummaryForm(forms.Form):
widget=readonly_number_widget
)
payment_method = forms.ChoiceField(
choices=PaymentMethods.choices,
widget=forms.Select(attrs={'disabled': 'disabled'})
choices=[(PaymentMethods.CASH, PaymentMethods.CASH)],
)
LineaFormSet = inlineformset_factory(
SaleLineFormSet = inlineformset_factory(
Sale,
SaleLine,
extra=1,
fields='__all__'
)
class ReconciliationJarForm(forms.ModelForm):
class Meta:
model = ReconciliationJar
fields = [
'date_time',
'description',
'reconcilier',
'cash_taken',
'cash_discrepancy',
]
widgets = {
'date_time': DateTimeInput(attrs={'type': 'datetime-local'})
}

View File

@@ -0,0 +1,22 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,79 @@
# Vuetify (Default)
This is the official scaffolding tool for Vuetify, designed to give you a head start in building your new Vuetify application. It sets up a base template with all the necessary configurations and standard directory structure, enabling you to begin development without the hassle of setting up the project from scratch.
## ❗️ Important Links
- 📄 [Docs](https://vuetifyjs.com/)
- 🚨 [Issues](https://issues.vuetifyjs.com/)
- 🏬 [Store](https://store.vuetifyjs.com/)
- 🎮 [Playground](https://play.vuetifyjs.com/)
- 💬 [Discord](https://community.vuetifyjs.com)
## 💿 Install
Set up your project using your preferred package manager. Use the corresponding command to install the dependencies:
| Package Manager | Command |
|---------------------------------------------------------------|----------------|
| [yarn](https://yarnpkg.com/getting-started) | `yarn install` |
| [npm](https://docs.npmjs.com/cli/v7/commands/npm-install) | `npm install` |
| [pnpm](https://pnpm.io/installation) | `pnpm install` |
| [bun](https://bun.sh/#getting-started) | `bun install` |
After completing the installation, your environment is ready for Vuetify development.
## ✨ Features
- 🖼️ **Optimized Front-End Stack**: Leverage the latest Vue 3 and Vuetify 3 for a modern, reactive UI development experience. [Vue 3](https://v3.vuejs.org/) | [Vuetify 3](https://vuetifyjs.com/en/)
- 🗃️ **State Management**: Integrated with [Pinia](https://pinia.vuejs.org/), the intuitive, modular state management solution for Vue.
- 🚦 **Routing and Layouts**: Utilizes Vue Router for SPA navigation and vite-plugin-vue-layouts for organizing Vue file layouts. [Vue Router](https://router.vuejs.org/) | [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts)
-**Next-Gen Tooling**: Powered by Vite, experience fast cold starts and instant HMR (Hot Module Replacement). [Vite](https://vitejs.dev/)
- 🧩 **Automated Component Importing**: Streamline your workflow with unplugin-vue-components, automatically importing components as you use them. [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components)
These features are curated to provide a seamless development experience from setup to deployment, ensuring that your Vuetify application is both powerful and maintainable.
## 💡 Usage
This section covers how to start the development server and build your project for production.
### Starting the Development Server
To start the development server with hot-reload, run the following command. The server will be accessible at [http://localhost:3000](http://localhost:3000):
```bash
yarn dev
```
(Repeat for npm, pnpm, and bun with respective commands.)
> Add NODE_OPTIONS='--no-warnings' to suppress the JSON import warnings that happen as part of the Vuetify import mapping. If you are on Node [v21.3.0](https://nodejs.org/en/blog/release/v21.3.0) or higher, you can change this to NODE_OPTIONS='--disable-warning=5401'. If you don't mind the warning, you can remove this from your package.json dev script.
### Building for Production
To build your project for production, use:
```bash
yarn build
```
(Repeat for npm, pnpm, and bun with respective commands.)
Once the build process is completed, your application will be ready for deployment in a production environment.
## 💪 Support Vuetify Development
This project is built with [Vuetify](https://vuetifyjs.com/en/), a UI Library with a comprehensive collection of Vue components. Vuetify is an MIT licensed Open Source project that has been made possible due to the generous contributions by our [sponsors and backers](https://vuetifyjs.com/introduction/sponsors-and-backers/). If you are interested in supporting this project, please consider:
- [Requesting Enterprise Support](https://support.vuetifyjs.com/)
- [Sponsoring John on Github](https://github.com/users/johnleider/sponsorship)
- [Sponsoring Kael on Github](https://github.com/users/kaelwd/sponsorship)
- [Supporting the team on Open Collective](https://opencollective.com/vuetify)
- [Becoming a sponsor on Patreon](https://www.patreon.com/vuetify)
- [Becoming a subscriber on Tidelift](https://tidelift.com/subscription/npm/vuetify)
- [Making a one-time donation with Paypal](https://paypal.me/vuetify)
## 📑 License
[MIT](http://opensource.org/licenses/MIT)
Copyright (c) 2016-present Vuetify, LLC

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Don Confiao</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"allowJs": true,
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "bundler",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
{
"name": "don-confiao",
"version": "0.0.0",
"scripts": {
"dev": "vite --host 0.0.0.0",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --fix --ignore-path .gitignore"
},
"dependencies": {
"@mdi/font": "7.4.47",
"core-js": "^3.37.1",
"roboto-fontface": "*",
"vee-validate": "^4.14.6",
"vue": "^3.4.31",
"vuetify": "^3.6.11"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.5",
"eslint": "^8.57.0",
"eslint-config-standard": "^17.1.0",
"eslint-config-vuetify": "^1.0.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-n": "^16.6.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.4.0",
"eslint-plugin-vue": "^9.27.0",
"pinia": "^2.1.7",
"sass": "1.77.6",
"unplugin-auto-import": "^0.17.6",
"unplugin-fonts": "^1.1.1",
"unplugin-vue-components": "^0.27.2",
"unplugin-vue-router": "^0.10.0",
"vite": "^5.3.3",
"vite-plugin-vue-layouts": "^0.11.0",
"vite-plugin-vuetify": "^2.0.3",
"vue-router": "^4.4.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -0,0 +1,19 @@
<template>
<v-app>
<NavBar />
<v-main>
<router-view />
</v-main>
</v-app>
</template>
<script>
import NavBar from './components/NavBar.vue';
export default {
name: 'App',
components: {
NavBar,
},
}
</script>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32zM175 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z"/></svg>

After

Width:  |  Height:  |  Size: 576 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -0,0 +1,6 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M261.126 140.65L164.624 307.732L256.001 466L377.028 256.5L498.001 47H315.192L261.126 140.65Z" fill="#1697F6"/>
<path d="M135.027 256.5L141.365 267.518L231.64 111.178L268.731 47H256H14L135.027 256.5Z" fill="#AEDDFF"/>
<path d="M315.191 47C360.935 197.446 256 466 256 466L164.624 307.732L315.191 47Z" fill="#1867C0"/>
<path d="M268.731 47C76.0026 47 141.366 267.518 141.366 267.518L268.731 47Z" fill="#7BC6FF"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

View File

@@ -0,0 +1,79 @@
<template>
<v-footer height="40" app>
<a
v-for="item in items"
:key="item.title"
:href="item.href"
:title="item.title"
class="d-inline-block mx-2 social-link"
rel="noopener noreferrer"
target="_blank"
>
<v-icon
:icon="item.icon"
:size="item.icon === '$vuetify' ? 24 : 16"
/>
</a>
<div
class="text-caption text-disabled"
style="position: absolute; right: 16px;"
>
&copy; 2016-{{ (new Date()).getFullYear() }} <span class="d-none d-sm-inline-block">Vuetify, LLC</span>
<a
class="text-decoration-none on-surface"
href="https://vuetifyjs.com/about/licensing/"
rel="noopener noreferrer"
target="_blank"
>
MIT License
</a>
</div>
</v-footer>
</template>
<script setup>
const items = [
{
title: 'Vuetify Documentation',
icon: `$vuetify`,
href: 'https://vuetifyjs.com/',
},
{
title: 'Vuetify Support',
icon: 'mdi-shield-star-outline',
href: 'https://support.vuetifyjs.com/',
},
{
title: 'Vuetify X',
icon: ['M2.04875 3.00002L9.77052 13.3248L1.99998 21.7192H3.74882L10.5519 14.3697L16.0486 21.7192H22L13.8437 10.8137L21.0765 3.00002H19.3277L13.0624 9.76874L8.0001 3.00002H2.04875ZM4.62054 4.28821H7.35461L19.4278 20.4308H16.6937L4.62054 4.28821Z'],
href: 'https://x.com/vuetifyjs',
},
{
title: 'Vuetify GitHub',
icon: `mdi-github`,
href: 'https://github.com/vuetifyjs/vuetify',
},
{
title: 'Vuetify Discord',
icon: ['M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z'],
href: 'https://community.vuetifyjs.com/',
},
{
title: 'Vuetify Reddit',
icon: `mdi-reddit`,
href: 'https://reddit.com/r/vuetifyjs',
},
]
</script>
<style scoped lang="sass">
.social-link :deep(.v-icon)
color: rgba(var(--v-theme-on-background), var(--v-disabled-opacity))
text-decoration: none
transition: .2s ease-in-out
&:hover
color: rgba(25, 118, 210, 1)
</style>

View File

@@ -0,0 +1,58 @@
<template>
<v-dialog v-model="dialog" max-width="400">
<v-card>
<v-card-title>Calcular Devuelta</v-card-title>
<v-card-text>
<v-text-field
v-model.number="purchase"
label="Total de la compra"
type="number"
prefix="$"
readonly
></v-text-field>
<v-text-field
v-model.number="money"
label="Dinero"
type="number"
prefix="$"
></v-text-field>
<v-text-field
v-model.number="change_cash"
label="Devuelta"
type="number"
prefix="$"
readonly
></v-text-field>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="dialog = false">Cerrar</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
props: {
total_purchase: {
type: Number,
required: true
}
},
data() {
return {
dialog: false,
money: null,
}
},
computed: {
purchase() {
return this.total_purchase
},
change_cash() {
return (this.money || 0) - this.total_purchase
},
},
}
</script>

View File

@@ -0,0 +1,50 @@
<template>
<v-dialog v-model="dialog" persistent>
<v-card>
<v-card-title>
Ingrese el código
</v-card-title>
<v-card-text>
<v-form id="code-form" @submit.prevent="verifyCode">
<v-text-field v-model="code" label="Código" type="password" autocomplete="off" />
</v-form>
</v-card-text>
<v-card-actions>
<v-btn type="submit" form="code-form">Aceptar</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import { inject } from 'vue';
export default {
data() {
return {
api: inject('api'),
dialog: true,
code: '',
};
},
methods: {
verifyCode() {
this.api.isValidAdminCode(this.code)
.then(data => {
if (data['validCode']) {
this.$emit('code-verified', true);
this.dialog = false;
} else {
alert('Código incorrecto');
this.$emit('code-verified', false);
}
})
.catch(error => {
alert('Error al validar el código');
this.$emit('code-verified', false);
console.error(error);
});
}
},
}
</script>

View File

@@ -0,0 +1,99 @@
<template>
<v-dialog v-model="showModal" max-width="600px">
<v-card>
<v-card-title>
<span class="headline">Información del Cliente</span>
</v-card-title>
<v-card-text>
<v-form ref="form" v-model="valid">
<v-text-field
v-model="customer.name"
label="Nombre"
:rules="[rules.required]"
></v-text-field>
<v-text-field
v-model="customer.address"
label="Direccion"
:rules="[rules.required]"
required
></v-text-field>
<v-text-field
v-model="customer.email"
label="Correo Electrónico"
:rules="[rules.required, rules.email]"
required
></v-text-field>
<v-text-field
v-model="customer.phone"
label="Teléfono"
:rules="[rules.required]"
required
></v-text-field>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text @click="closeModal">Cancelar</v-btn>
<v-btn color="blue darken-1" text @click="submitForm">Guardar</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
data() {
return {
showModal: false,
api: inject('api'),
valid: false,
customer: {
name: '',
address: '',
email: '',
phone: ''
},
rules: {
required: value => !!value || 'Este campo es requerido.',
email: value => {
const pattern = /^[^ ]+@[^ ]+\.[a-z]{2,3}$/;
return pattern.test(value) || 'El correo no es válido.';
}
}
};
},
methods: {
openModal() {
this.showModal = true;
},
closeModal() {
this.showModal = false;
this.resetForm();
},
async submitForm() {
console.log(this.customer)
if (this.$refs.form.validate()) {
this.api.createCustomer(this.customer)
.then(data => {
console.log('Cliente Guardado:', data);
this.$emit('customerCreated', data);
this.closeModal();
})
.catch(error => console.error('Error:', error));
}
},
resetForm() {
this.customer = {
name: '',
address: '',
email: '',
phone: ''
};
this.$refs.form.reset();
},
}
};
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,26 @@
<template>
<span>{{ formattedValue }}</span>
</template>
<script>
export default {
props: {
value: {
type: Number,
required: true
},
locale: {
type: String,
default: 'es-CO',
},
currency: {
type: String,
default: 'COP',
},
},
computed: {
formattedValue() {
return new Intl.NumberFormat(this.locale, { style: 'currency', currency: this.currency }).format(this.value);
},
},
}
</script>

View File

@@ -0,0 +1,157 @@
<template>
<v-container class="fill-height">
<v-responsive
class="align-centerfill-height mx-auto"
max-width="900"
>
<v-img
class="mb-4"
height="150"
src="@/assets/logo.png"
/>
<div class="text-center">
<div class="text-body-2 font-weight-light mb-n1">Welcome to</div>
<h1 class="text-h2 font-weight-bold">Vuetify</h1>
</div>
<div class="py-4" />
<v-row>
<v-col cols="12">
<v-card
class="py-4"
color="surface-variant"
image="https://cdn.vuetifyjs.com/docs/images/one/create/feature.png"
prepend-icon="mdi-rocket-launch-outline"
rounded="lg"
variant="outlined"
>
<template #image>
<v-img position="top right" />
</template>
<template #title>
<h2 class="text-h5 font-weight-bold">Get started</h2>
</template>
<template #subtitle>
<div class="text-subtitle-1">
Replace this page by removing <v-kbd>{{ `<HelloWorld />` }}</v-kbd> in <v-kbd>pages/index.vue</v-kbd>.
</div>
</template>
<v-overlay
opacity=".12"
scrim="primary"
contained
model-value
persistent
/>
</v-card>
</v-col>
<v-col cols="6">
<v-card
append-icon="mdi-open-in-new"
class="py-4"
color="surface-variant"
href="https://vuetifyjs.com/"
prepend-icon="mdi-text-box-outline"
rel="noopener noreferrer"
rounded="lg"
subtitle="Learn about all things Vuetify in our documentation."
target="_blank"
title="Documentation"
variant="text"
>
<v-overlay
opacity=".06"
scrim="primary"
contained
model-value
persistent
/>
</v-card>
</v-col>
<v-col cols="6">
<v-card
append-icon="mdi-open-in-new"
class="py-4"
color="surface-variant"
href="https://vuetifyjs.com/introduction/why-vuetify/#feature-guides"
prepend-icon="mdi-star-circle-outline"
rel="noopener noreferrer"
rounded="lg"
subtitle="Explore available framework Features."
target="_blank"
title="Features"
variant="text"
>
<v-overlay
opacity=".06"
scrim="primary"
contained
model-value
persistent
/>
</v-card>
</v-col>
<v-col cols="6">
<v-card
append-icon="mdi-open-in-new"
class="py-4"
color="surface-variant"
href="https://vuetifyjs.com/components/all"
prepend-icon="mdi-widgets-outline"
rel="noopener noreferrer"
rounded="lg"
subtitle="Discover components in the API Explorer."
target="_blank"
title="Components"
variant="text"
>
<v-overlay
opacity=".06"
scrim="primary"
contained
model-value
persistent
/>
</v-card>
</v-col>
<v-col cols="6">
<v-card
append-icon="mdi-open-in-new"
class="py-4"
color="surface-variant"
href="https://discord.vuetifyjs.com"
prepend-icon="mdi-account-group-outline"
rel="noopener noreferrer"
rounded="lg"
subtitle="Connect with Vuetify developers."
target="_blank"
title="Community"
variant="text"
>
<v-overlay
opacity=".06"
scrim="primary"
contained
model-value
persistent
/>
</v-card>
</v-col>
</v-row>
</v-responsive>
</v-container>
</template>
<script setup>
//
</script>

View File

@@ -0,0 +1,19 @@
<template>
<div class="modal">
<div class="head">
<p>Nuevo Movimiento</p>
<img @click="close" src="@/assets/close-icon.svg" alt="cerrar"/>
</div>
<div class="body">
<slot></slot>
</div>
</div>
</template>
<script setup>
import { defineEmits } from "vue";
const emit = defineEmits(["close"]);
const close = () => emit("close");
</script>

View File

@@ -0,0 +1,47 @@
<template>
<v-app-bar color="primary" prominent>
<v-app-bar-nav-icon variant="text" @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
<v-toolbar-title>Menu</v-toolbar-title>
<v-spacer></v-spacer>
<template v-if="$vuetify.display.mdAndUp">
<v-btn icon="mdi-magnify" variant="text"></v-btn>
<v-btn icon="mdi-filter" variant="text"></v-btn>
</template>
<v-btn icon="mdi-dots-vertical" variant="text"></v-btn>
</v-app-bar>
<v-navigation-drawer v-model="drawer"
:location="$vuetify.display.mobile ? 'bottom' : undefined"
temporary>
<v-list>
<v-list-item v-for="item in menuItems" :key="item.title" @click="navigate(item.route)">
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item>
</v-list>
</v-navigation-drawer>
</template>
<script>
export default {
name: 'NavBar',
data: () => ({
drawer: false,
group: null,
menuItems: [
{ title: 'Inicio', route: '/'},
{ title: 'Comprar', route:'/comprar'},
{ title: 'Cuadrar tarro', route: '/cuadrar_tarro'},
{ title: 'Cuadres de tarro', route: '/cuadres_de_tarro'},
],
}),
watch: {
group () {
this.drawer = false
},
},
methods: {
navigate(route) {
this.$router.push(route);
},
}
}
</script>

View File

@@ -0,0 +1,342 @@
<template>
<v-container>
<v-form ref="purchase" v-model="valid" @change="onFormChange">
<v-row>
<v-col>
<v-autocomplete
v-model="purchase.customer"
:items="filteredClients"
:search="client_search"
no-data-text="No se hallaron clientes"
item-title="name"
item-value="id"
@update:model-value="onFormChange"
label="Cliente"
:rules="[rules.required]"
required
class="mr-4"
></v-autocomplete>
<v-btn color="primary" @click="openModal">Agregar Cliente</v-btn>
<CreateCustomerModal ref="customerModal" @customerCreated="handleNewCustomer"/>
</v-col>
<v-col lg="4">
<v-text-field
v-model="purchase.date"
label="Fecha"
type="datetime-local"
:rules="[rules.required]"
required
readonly
></v-text-field>
</v-col>
</v-row>
<v-textarea
v-model="purchase.notes"
label="Notas"
rows="2"
></v-textarea>
<v-divider></v-divider>
<v-container>
<v-toolbar>
<v-toolbar-title secondary>Productos</v-toolbar-title>
</v-toolbar>
<v-container v-for="(line, index) in purchase.saleline_set" :key="line.id">
<v-row>
<v-col
lg="9">
<v-autocomplete
v-model="line.product"
:items="filteredProducts"
:search="product_search"
@update:modelValue="onProductChange(index)"
no-data-text="No se hallaron productos"
item-title="name"
item-value="id"
item-subtitle="Price"
label="Producto"
:rules="[rules.required]"
required
>
<template v-slot:item="{ props, item }">
<v-list-item v-bind="props" :title="item.raw.name" :subtitle="formatPrice(item.raw.price)"></v-list-item>
</template>
</v-autocomplete>
</v-col>
<v-col
lg="2"
>
<v-text-field
v-model.number="line.quantity"
label="Cantidad"
type="number"
:rules="[rules.required,rules.positive]"
required
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
v-model.number="line.unit_price"
label="Precio"
type="number"
:rules="[rules.required]"
prefix="$"
required
readonly
></v-text-field>
</v-col>
<v-col>
<v-text-field
v-model="line.measuring_unit"
label="UdM"
persistent-placeholder="true"
readonly
></v-text-field>
</v-col>
<v-col>
<v-text-field
type="number"
:value="calculateSubtotal(line)"
label="Subtotal"
prefix="$"
readonly
disable
persistent-placeholder="true"
></v-text-field>
</v-col>
<v-col>
<v-btn @click="removeLine(index)" color="red">Eliminar</v-btn>
</v-col>
</v-row>
<v-alert type="warning" :duration="2000" closable v-model="show_alert_lines">
No se puede eliminar la única línea.
</v-alert>
</v-container>
<v-btn @click="addLine" color="blue">Agregar</v-btn>
</v-container>
<v-divider></v-divider>
<v-text-field
:value="calculateTotal"
label="Total"
prefix="$"
readonly
persistent-placeholder="true"
></v-text-field>
<v-container v-if="calculateTotal > 0">
<v-select
:items="payment_methods"
v-model="purchase.payment_method"
item-title="text"
item-value="value"
label="Pago en"
:rules="[rules.required]"
required
></v-select>
<v-btn @click="openCasherModal" v-if="purchase.payment_method === 'CASH'">Calcular Devuelta</v-btn>
<CasherModal :total_purchase="calculateTotal" ref="casherModal"</CasherModal>
</v-container>
<v-btn @click="submit" color="green">Comprar</v-btn>
<v-alert type="error" :duration="2000" closable v-model="show_alert_purchase">
Verifique los campos obligatorios.
</v-alert>
</v-form>
</v-container>
</template>
<script>
import CustomerForm from './CreateCustomerModal.vue';
import CasherModal from './CasherModal.vue';
import { inject } from 'vue';
export default {
name: 'DonConfiao',
components: {
CustomerForm,
CasherModal,
},
props: {
msg: String
},
data() {
return {
api: inject('api'),
valid: false,
form_changed: false,
show_alert_lines: false,
show_alert_purchase: false,
client_search: '',
product_search: '',
payment_methods: null,
purchase: {
date: this.getCurrentDate(),
customer: null,
notes: '',
payment_method: null,
saleline_set: [{product:'', unit_price: 0, quantity: 0, unit: ''}],
},
rules: {
required: value => !!value || 'Requerido.',
positive: value => value > 0 || 'La cantidad debe ser mayor que 0.',
},
menuItems: [
{ title: 'Inicio', route: '/'},
{ title: 'Compras', route:'/compras'},
],
clients: [],
products: [],
};
},
created() {
this.fetchClients();
this.fetchProducts();
this.fetchPaymentMethods();
},
watch: {
group () {
this.drawer = false
},
},
beforeMount() {
window.addEventListener('beforeunload', this.confirmLeave);
},
beforeDestroy() {
window.removeEventListener('beforeunload', this.confirmLeave);
},
computed: {
calculateTotal() {
return this.purchase.saleline_set.reduce((total, saleline) => {
return total + this.calculateSubtotal(saleline);
}, 0);
},
filteredClients() {
return this.clients.filter(client => {
if (this.client_search === '') {
return [];
} else {
return client.name.toLowerCase().includes(this.client_search.toLowerCase());
}
});
},
filteredProducts() {
return this.products.filter(product => {
if (this.product_search === '') {
return [];
} else {
return product.name.toLowerCase().includes(this.product_search.toLowerCase());
}
});
},
},
methods: {
openModal() {
this.$refs.customerModal.openModal();
},
onFormChange() {
this.form_changed = true;
},
openCasherModal() {
this.$refs.casherModal.dialog = true
},
confirmLeave(event) {
if (this.form_changed) {
const message = '¿seguro que quieres salir? Perderas la información diligenciada';
event.preventDefault();
event.returnValue = message;
return message;
}
},
getCurrentDate() {
const today = new Date();
const gmtOffSet = -5;
const localDate = new Date(today.getTime() + (gmtOffSet * 60 * 60 * 1000));
// Formatear la fecha y hora en el formato YYYY-MM-DDTHH:MM
const formattedDate = localDate.toISOString().slice(0,16);
return formattedDate;
},
onProductChange(index) {
const selectedProductId = this.purchase.saleline_set[index].product;
const selectedProduct = this.products.find(p => p.id == selectedProductId);
this.purchase.saleline_set[index].unit_price = selectedProduct.price;
this.purchase.saleline_set[index].measuring_unit = selectedProduct.measuring_unit;
},
fetchClients() {
this.api.getCustomers()
.then(data => {
this.clients = data;
})
.catch(error => {
console.error(error);
});
},
handleNewCustomer(newCustomer){
this.clients.push(newCustomer);
this.purchase.customer = newCustomer.id;
},
fetchProducts() {
this.api.getProducts()
.then(data => {
this.products = data;
})
.catch(error => {
console.error(error);
});
},
fetchPaymentMethods() {
this.api.getPaymentMethods()
.then(data => {
this.payment_methods = data;
})
.catch(error => {
console.error(error);
});
},
addLine() {
this.purchase.saleline_set.push({ product: '', unit_price: 0, quantity:0, measuring_unit: ''});
},
removeLine(index) {
if (this.purchase.saleline_set.length > 1) {
this.purchase.saleline_set.splice(index, 1);
} else {
this.show_alert_lines = true;
setTimeout(() => {
this.show_alert_lines = false;
}, 2000);
}
},
calculateSubtotal(line) {
return line.unit_price * line.quantity;
},
async submit() {
this.$refs.purchase.validate();
if (this.valid) {
this.api.createPurchase(this.purchase)
.then(data => {
console.log('Compra enviada:', data);
this.$router.push({
path: "/summary_purchase",
query : {id: parseInt(data.id)}
});
})
.catch(error => console.error('Error al enviarl la compra:', error));
} else {
this.show_alert_purchase = true;
setTimeout(() => {
this.show_alert_purchase = false;
}, 4000);
}
},
navigate(route) {
this.$router.push(route);
},
formatPrice(price) {
return new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'COP' }).format(price);
},
},
mounted() {
this.fetchClients();
}
};
</script>
<style>
</style>

View File

@@ -0,0 +1,35 @@
# Components
Vue template files in this folder are automatically imported.
## 🚀 Usage
Importing is handled by [unplugin-vue-components](https://github.com/unplugin/unplugin-vue-components). This plugin automatically imports `.vue` files created in the `src/components` directory, and registers them as global components. This means that you can use any component in your application without having to manually import it.
The following example assumes a component located at `src/components/MyComponent.vue`:
```vue
<template>
<div>
<MyComponent />
</div>
</template>
<script lang="ts" setup>
//
</script>
```
When your template is rendered, the component's import will automatically be inlined, which renders to this:
```vue
<template>
<div>
<MyComponent />
</div>
</template>
<script lang="ts" setup>
import MyComponent from '@/components/MyComponent.vue'
</script>
```

View File

@@ -0,0 +1,200 @@
<template>
<v-container>
<v-toolbar>
<v-toolbar-title> Cuadre del Tarro </v-toolbar-title>
</v-toolbar>
<v-card>
<v-card-text>
<v-form ref="taker" v-model="valid">
<v-text-field
v-model="reconciliation.date_time"
label="Fecha"
type="datetime-local"
:rules="[rules.required]"
required
readonly
></v-text-field>
<v-text-field
v-model="reconciliation.reconcilier"
label="Cajero"
:rules="[rules.required]"
required
></v-text-field>
<v-text-field
v-model="reconciliation.total_cash_purchases"
label="Total Ventas en efectivo"
:rules="[rules.required]"
prefix="$"
type="number"
readonly
></v-text-field>
<v-text-field
v-model="reconciliation.cash_taken"
label="Dinero Recogido"
:rules="[rules.required]"
prefix="$"
type="number"
></v-text-field>
<v-text-field
v-model="reconciliation.cash_discrepancy"
label="Descuadre"
:rules="[rules.integer]"
prefix="$"
type="number"
></v-text-field>
<v-btn @click="submit" color="green">Recoger Dinero</v-btn>
</v-form>
</v-card-text>
</v-card>
<v-tabs v-model="selectedTab">
<v-tab
v-for="(purchases, payment_method) in summary.purchases"
:key="payment_method"
:value="payment_method"
>
{{ payment_method }}&nbsp; <CurrencyText :value="totalByMethod(payment_method)"</CurrencyText>
</v-tab>
</v-tabs>
<v-tabs-window v-model="selectedTab">
<v-card>
<v-card-text>
<v-tabs-window-item
v-for="(purchases, payment_method) in summary.purchases"
:key="payment_method"
:value="payment_method"
>
<v-data-table-virtual
:headers="summary.headers"
:items="summary.purchases[payment_method]"
>
<template v-slot:item.id="{ item }">
<v-btn @click="openSummaryModal(item.id)">{{ item.id }}</v-btn>
</template>
<template v-slot:item.total="{ item }">
<CurrencyText :value="parseFloat(item.total)"></CurrencyText>
</template>
</v-data-table-virtual>
</v-tabs-window-item>
<SummaryPurchaseModal :id="selectedPurchaseId" ref="summaryModal" />
</v-card-text>
</v-card>
</v-tabs-window>
</v-container>
</template>
<script>
import { inject } from 'vue';
import CurrencyText from './CurrencyText.vue';
import SummaryPurchaseModal from './SummaryPurchaseModal.vue';
export default {
name: 'ReconciliationJar',
props: {
msg: String,
},
components: {
SummaryPurchaseModal,
},
data () {
return {
api: inject('api'),
valid: null,
selectedPurchaseId: null,
selectedTab: 'CASH',
reconciliation: {
date_time: '',
total_cash_purchases: 0,
cash_taken: 0,
cash_discrepancy: 0,
other_totals: {
},
cash_purchases: [],
},
summary: {
headers: [
{title: 'Id', value: 'id'},
{title: 'Fecha', value: 'date'},
{title: 'Cliente', value: 'customer.name'},
{title: 'Total', value: 'total'},
],
purchases: {},
},
rules: {
required: value => !!value || 'Requerido.',
integer: value => !!value || value === 0 || 'Requerido.',
},
};
},
mounted() {
this.fetchPurchases();
this.reconciliation.date_time = this.getCurrentDate();
},
watch: {
'reconciliation.cash_taken'() {
this.updateDiscrepancy();
},
},
methods: {
totalByMethod(method) {
if (method in this.summary.purchases) {
return this.summary.purchases[method].reduce((a, b) => a + parseFloat(b.total), 0);
}
return 0;
},
idsBymethod(method) {
if (method in this.summary.purchases) {
return this.summary.purchases[method].map(purchase => purchase.id)
}
return [];
},
processOtherMethods() {
for (const method of Object.keys(this.summary.purchases)) {
if (method !== 'CASH') {
this.reconciliation.other_totals[method] = {
total: this.totalByMethod(method),
purchases: this.idsBymethod(method),
}
}
}
},
updateDiscrepancy() {
this.reconciliation.cash_discrepancy = (this.reconciliation.total_cash_purchases || 0 ) - (this.reconciliation.cash_taken || 0);
},
getCurrentDate() {
const today = new Date();
const gmtOffSet = -5;
const localDate = new Date(today.getTime() + (gmtOffSet * 60 * 60 * 1000));
// Formatear la fecha y hora en el formato YYYY-MM-DDTHH:MM
const formattedDate = localDate.toISOString().slice(0,16);
return formattedDate;
},
openSummaryModal(id) {
this.selectedPurchaseId = id;
this.$refs.summaryModal.dialog = true;
},
fetchPurchases() {
this.api.getPurchasesForReconciliation()
.then(data => {
this.summary.purchases = data;
this.reconciliation.cash_purchases = this.idsBymethod('CASH');
this.reconciliation.total_cash_purchases = this.totalByMethod('CASH');
this.processOtherMethods();
})
.catch(error => {
console.error(error);
});
},
async submit() {
this.$refs.taker.validate();
if (this.valid) {
this.api.createReconciliationJar(this.reconciliation)
.then(data => {
console.log('Cuadre enviado:', data);
this.$router.push({path: "/"});
})
.catch(error => console.error('Error:', error));
}
}
},
}
</script>

View File

@@ -0,0 +1,64 @@
<template>
<v-container>
<v-toolbar>
<v-toolbar-title> Cuadres del Tarro </v-toolbar-title>
</v-toolbar>
<v-card>
<v-card-text>
<v-data-table-server
v-model:items-per-page="itemsPerPage"
:headers="headers"
:items="serverItems"
:items-length="totalItems"
:loading="loading"
:search="search"
@update:options="loadItems"
>
<template v-slot:item.id="{ item }">
<v-btn @click="openSummaryModal(item.id)">{{ item.id }}</v-btn>
</template>
</v-data-table-server>
<SummaryReconciliationModal :id="selectedReconciliationId" ref="summaryModal" />
</v-card-text>
</v-card>
</v-container>
</template>
<script>
export default {
data() {
return {
api: inject('api'),
selectedReconciliationId: null,
itemsPerPage: 10,
headers: [
{ title: 'Acciones', key: 'id'},
{ title: 'Fecha', key: 'date_time'},
{ title: 'Reconciliador', key: 'reconcilier'},
{ title: 'Total Compras Efectivo', key: 'total_cash_purchases'},
{ title: 'Recogido', key: 'cash_taken'},
{ title: 'Descuadre', key: 'cash_discrepancy'},
],
search: '',
serverItems: [],
loading: true,
totalItems: 0,
}
},
methods: {
loadItems ({page, itemsPerPage}) {
this.loading = true;
this.api.getListReconcliations(page, itemsPerPage)
.then(data => {
this.serverItems = data['results'];
this.totalItems = data['count'];
this.loading = false;
})
.catch(error => console.log('Error:', error));
},
openSummaryModal(id) {
this.selectedReconciliationId = id.toString();
this.$refs.summaryModal.dialog = true;
},
},
}
</script>

View File

@@ -0,0 +1,140 @@
<template>
<v-container>
<v-toolbar>
<v-toolbar-title> Cuadre de Tarro: {{ id }}</v-toolbar-title>
</v-toolbar>
<v-card>
<v-card-text>
<v-text-field
v-model="reconciliation.date_time"
label="Fecha"
required
readonly
></v-text-field>
<v-text-field
v-model="reconciliation.reconcilier"
label="Cajero"
required
readonly
></v-text-field>
<v-text-field
v-model="reconciliation.total_cash_purchases"
label="Total Ventas en efectivo"
prefix="$"
type="number"
readonly
></v-text-field>
<v-text-field
v-model="reconciliation.cash_taken"
label="Dinero Recogido"
prefix="$"
type="number"
></v-text-field>
<v-text-field
v-model="reconciliation.cash_discrepancy"
label="Descuadre"
prefix="$"
type="number"
></v-text-field>
<v-tabs v-model="tab">
<v-tab
v-for="(elements, paymentMethod) in purchases"
:key="paymentMethod"
>
{{ paymentMethod }}&nbsp; <CurrencyText :value="elements.total"</CurrencyText>
</v-tab>
</v-tabs>
<v-tabs-window v-model="tab">
<v-tabs-window-item
v-for="(elements, paymentMethod) in purchases"
:key="paymentMethod"
>
<v-table>
<thead>
<tr>
<th>Id</th>
<th>Fecha</th>
<th>Cliente</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<tr v-for="purchase in elements.purchases" :key="purchase.id">
<td><v-btn @click="openSummaryModal(purchase.id)">{{ purchase.id }}</v-btn></td>
<td>{{ purchase.date }}</td>
<td>{{ purchase.customer }}</td>
<td><CurrencyText :value="purchase.total"</CurrencyText></td>
</tr>
</tbody>
</v-table>
</v-tabs-window-item>
</v-tabs-window>
<SummaryPurchaseModal :id="selectedPurchaseId" ref="summaryModal" />
</v-card-text>
</v-card>
</v-container>
</template>
<script>
import { inject } from 'vue';
export default {
name: 'ReconciliationJar View',
props: {
msg: String,
id: {
type: String,
required: true
}
},
data () {
return {
tab: '0',
selectedPurchaseId: null,
api: inject('api'),
valid: null,
reconciliation: {
},
purchases: {},
};
},
created() {
if (this.id) {
this.fetchReconciliation(this.id);
} else {
console.error('No se proporcionó ID');
}
},
methods: {
fetchReconciliation(reconciliationId) {
this.api.getReconciliation(reconciliationId)
.then(data => {
this.reconciliation = data;
this.groupPurchases();
})
.catch(error => console.error(error));
},
groupPurchases() {
if (this.reconciliation.Sales) {
this.purchases = this.reconciliation.Sales.reduce((grouped, sale) => {
const paymentMethod = sale.payment_method;
if (!grouped[paymentMethod]) {
grouped[paymentMethod] = {
purchases: [],
total: 0,
};
}
grouped[paymentMethod].purchases.push(sale);
grouped[paymentMethod].total += sale.total;
return grouped;
}, {});
}
},
openSummaryModal(id) {
this.selectedPurchaseId = id;
this.$refs.summaryModal.dialog = true;
},
},
}
</script>

View File

@@ -0,0 +1,5 @@
<template>
<strong class="text-red-darken-4">
<slot></slot>
</strong>
</template>

View File

@@ -0,0 +1,107 @@
<template>
<v-container>
<v-container v-show="!id">
<v-toolbar>
<v-toolbar-title> No se indicó Id de la compra</v-toolbar-title>
</v-toolbar>
</v-container>
<v-container v-show="id">
<v-toolbar>
<v-toolbar-title> Resumen de la compra {{ id }}</v-toolbar-title>
</v-toolbar>
<v-list>
<v-list-item>
<v-list-item-title>Fecha:</v-list-item-title>
<v-list-item-subtitle>{{ purchase.date }}</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<v-list-item-title>Cliente:</v-list-item-title>
<v-list-item-subtitle v-if="purchase.customer">{{ purchase.customer.name }}</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<v-list-item-title>Pagado en:</v-list-item-title>
<v-list-item-subtitle v-if="purchase.payment_method">{{ purchase.payment_method }}</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<v-list-item-title>Total:</v-list-item-title>
<v-list-item-subtitle v-if="purchase.lines">{{ currencyFormat(calculateTotal(purchase.lines)) }}</v-list-item-subtitle>
</v-list-item>
</v-list>
<v-data-table-virtual
:headers="headers"
:items="purchase.lines"
>
<template v-slot:item.unit_price="{ item }">
{{ currencyFormat(item.unit_price) }}
</template>
<template v-slot:item.subtotal="{ item }">
{{ currencyFormat(calculateSubtotal(item.unit_price, item.quantity)) }}
</template>
</v-data-table-virtual>
<div class="text-center">
<v-btn :to="{ path: 'comprar' }" color="green">Ir a Comprar</v-btn>
</div>
</v-container>
</v-container>
</template>
<script>
import { inject } from 'vue';
export default {
name: 'SummaryPurchase',
props: {
msg: String,
id: Number
},
data () {
return {
api: inject('api'),
purchase: {},
headers: [
{ title: 'Producto', value: 'product.name' },
{ title: 'Precio', value: 'unit_price' },
{ title: 'Cantidad', value: 'quantity' },
{ title: 'Subtotal', value: 'subtotal' },
],
};
},
created() {
if (this.id) {
this.fetchPurchase(this.id);
} else {
console.error('No se proporcionó un ID de compra.');
}
},
methods: {
fetchPurchase(purchaseId) {
this.api.getSummaryPurchase(purchaseId)
.then(data => {
this.purchase = data;
})
.catch(error => {
console.error(error);
});
},
currencyFormat(value) {
return new Intl.NumberFormat('es-CO', { style: 'currency', currency: 'COP' }).format(value);
},
calculateSubtotal(price, quantity) {
price = parseFloat(price || 0);
quantity = parseFloat(quantity || 0);
return price * quantity;
},
calculateTotal(lines) {
let total = 0;
lines.forEach(line => {
total += this.calculateSubtotal(line.unit_price, line.quantity);
});
return total;
}
},
};
</script>
<style>
</style>

View File

@@ -0,0 +1,30 @@
<template>
<v-dialog v-model="dialog" max-width="400">
<v-card>
<v-card-text>
<SummaryPurchase :id="id"/>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="dialog = false">Cerrar</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
name: 'SummaryPurchase Modal',
props: {
id: {
type: Number,
required: true,
}
},
data() {
return {
dialog: false,
}
},
}
</script>

View File

@@ -0,0 +1,30 @@
<template>
<v-dialog v-model="dialog" max-width="400">
<v-card>
<v-card-text>
<SummaryPurchase :id="id"/>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="dialog = false">Cerrar</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
name: 'SummaryPurchase Modal',
props: {
id: {
type: Number,
required: true,
}
},
data() {
return {
dialog: false,
}
},
}
</script>

View File

@@ -0,0 +1,31 @@
<template>
<v-dialog v-model="dialog" max-width="400">
resumen
<v-card>
<v-card-text>
<ReconciliationJarView :id="id"/>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="dialog = false">Cerrar</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
name: 'Summary Reconciliation Modal',
props: {
id: {
type: String,
required: true,
}
},
data() {
return {
dialog: false,
}
},
}
</script>

View File

@@ -0,0 +1,34 @@
<template>
<v-container >
<v-responsive>
<v-toolbar>
<v-toolbar-title>Don Confiao te atiende</v-toolbar-title>
</v-toolbar>
<v-card>
<v-card-title>Hacer parte de la tienda la ilusión</v-card-title>
<v-card-text>
Recuerda que participando de esta tienda le apuestas a la economía solidaria, al mercado justo, a la alimentación sana, al campesinado colombiano y a un mundo mejor.
</v-card-text>
</v-card>
<v-card>
<v-card-title>En desarrollo</v-card-title>
<v-card-text>
Don confiao apenas esta entendiendo como funciona esta tienda y por ahora <ResaltedText>solo puede atender las compras de contado</ResaltedText>, ya sea en efectivo o consignación.
<v-alert type="warning">
Si no vas a pagar tu compra recuerda que debes hacerlo en la planilla manual</v-alert>
</v-card-text>
</v-card>
<v-card>
<v-card-title>A comprar</v-card-title>
<v-card-text>
El siguiente botón te permitirá registrar tu compra. Cuando finalices te pedimos que ingrese el número de la compra, la fecha y el valor en la planilla física.
<div class="text-center">
<v-btn :to="{ path: 'comprar' }" color="green">Ir a Comprar</v-btn>
</div>
</v-card-text>
</v-card>
</v-responsive>
</v-container>
</template>

View File

@@ -0,0 +1,5 @@
# Layouts
Layouts are reusable components that wrap around pages. They are used to provide a consistent look and feel across multiple pages.
Full documentation for this feature can be found in the Official [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) repository.

View File

@@ -0,0 +1,13 @@
<template>
<v-app>
<v-main>
<router-view />
</v-main>
<AppFooter />
</v-app>
</template>
<script setup>
//
</script>

View File

@@ -0,0 +1,26 @@
/**
* main.js
*
* Bootstraps Vuetify and other plugins then mounts the App`
*/
// Plugins
import { registerPlugins } from '@/plugins'
// Components
import App from './App.vue'
import ApiImplementation from './services/api-implementation';
// Composables
import { createApp } from 'vue'
process.env.API_IMPLEMENTATION = 'django';
let apiImplementation = new ApiImplementation();
const api = apiImplementation.getApi();
const app = createApp(App);
app.provide('api', api);
registerPlugins(app)
app.mount('#app')

View File

@@ -0,0 +1,5 @@
# Pages
Vue components created in this folder will automatically be converted to navigatable routes.
Full documentation for this feature can be found in the Official [unplugin-vue-router](https://github.com/posva/unplugin-vue-router) repository.

View File

@@ -0,0 +1,7 @@
<template>
<Purchase />
</template>
<script setup>
//
</script>

View File

@@ -0,0 +1,20 @@
<template>
<div>
<CodeDialog @code-verified="(verified) => showComponent = verified"/>
</div>
<ReconciliationJar v-if="showComponent" />
</template>
<script >
import CodeDialog from '../components/CodeDialog.vue'
export default {
data() {
return {
showComponent: false,
}
},
components: { CodeDialog },
methods: {},
}
</script>

View File

@@ -0,0 +1,20 @@
<template>
<div>
<CodeDialog @code-verified="(verified) => showComponent = verified" />
</div>
<ReconciliationJarIndex v-if="showComponent" />
</template>
<script>
import CodeDialog from '../components/CodeDialog.vue'
export default {
data() {
return {
showComponent: false,
}
},
components: { CodeDialog },
methods: {},
}
</script>

View File

@@ -0,0 +1,6 @@
<template>
<Wellcome />
</template>
<script setup>
</script>

View File

@@ -0,0 +1,7 @@
<template>
<SummaryPurchase :id="$route.query.id"/>
</template>
<script setup>
//
</script>

View File

@@ -0,0 +1,3 @@
# Plugins
Plugins are a way to extend the functionality of your Vue application. Use this folder for registering plugins that you want to use globally.

View File

@@ -0,0 +1,17 @@
/**
* plugins/index.js
*
* Automatically included in `./src/main.js`
*/
// Plugins
import vuetify from './vuetify'
import pinia from '@/stores'
import router from '@/router'
export function registerPlugins (app) {
app
.use(vuetify)
.use(router)
.use(pinia)
}

View File

@@ -0,0 +1,19 @@
/**
* plugins/vuetify.js
*
* Framework documentation: https://vuetifyjs.com`
*/
// Styles
import '@mdi/font/css/materialdesignicons.css'
import 'vuetify/styles'
// Composables
import { createVuetify } from 'vuetify'
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
export default createVuetify({
theme: {
defaultTheme: 'light',
},
})

View File

@@ -0,0 +1,37 @@
/**
* router/index.ts
*
* Automatic routes for `./src/pages/*.vue`
*/
// Composables
import { createRouter, createWebHistory } from 'vue-router/auto'
import { setupLayouts } from 'virtual:generated-layouts'
import { routes } from 'vue-router/auto-routes'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: setupLayouts(routes),
})
// Workaround for https://github.com/vitejs/vite/issues/11804
router.onError((err, to) => {
if (err?.message?.includes?.('Failed to fetch dynamically imported module')) {
if (!localStorage.getItem('vuetify:dynamic-reload')) {
console.log('Reloading page to fix dynamic import error')
localStorage.setItem('vuetify:dynamic-reload', 'true')
location.assign(to.fullPath)
} else {
console.error('Dynamic import error, reloading page did not fix it', err)
}
} else {
console.error(err)
}
})
router.isReady().then(() => {
localStorage.removeItem('vuetify:dynamic-reload')
})
export default router

View File

@@ -0,0 +1,21 @@
import DjangoApi from './django-api';
import Api from './api';
class ApiImplementation {
constructor() {
const implementation = process.env.API_IMPLEMENTATION;
let apiImplementation;
if (implementation === 'django') {
apiImplementation = new DjangoApi();
} else {
throw new Error("API implementation don't configured");
}
this.api = new Api(apiImplementation);
}
getApi() {
return this.api;
}
}
export default ApiImplementation;

View File

@@ -0,0 +1,51 @@
class Api {
constructor (apiImplementation) {
this.apiImplementation = apiImplementation;
}
getCustomers() {
return this.apiImplementation.getCustomers();
}
getProducts() {
return this.apiImplementation.getProducts();
}
getPaymentMethods() {
return this.apiImplementation.getPaymentMethods();
}
getSummaryPurchase(purchaseId) {
return this.apiImplementation.getSummaryPurchase(purchaseId);
}
getPurchasesForReconciliation() {
return this.apiImplementation.getPurchasesForReconciliation();
}
getListReconcliations(page=1, itemsPerPage=10) {
return this.apiImplementation.getListReconcliations(page, itemsPerPage);
}
getReconciliation(reconciliationId) {
return this.apiImplementation.getReconciliation(reconciliationId);
}
isValidAdminCode(code) {
return this.apiImplementation.isValidAdminCode(code);
}
createPurchase(purchase) {
return this.apiImplementation.createPurchase(purchase);
}
createReconciliationJar(reconciliation) {
return this.apiImplementation.createReconciliationJar(reconciliation);
}
createCustomer(customer) {
return this.apiImplementation.createCustomer(customer);
}
}
export default Api;

View File

@@ -0,0 +1,97 @@
class DjangoApi {
getCustomers() {
const url = '/don_confiao/api/customers/';
return this.getRequest(url);
}
getProducts() {
const url = '/don_confiao/api/products/';
return this.getRequest(url);
}
getPaymentMethods() {
const url = '/don_confiao/payment_methods/all/select_format';
return this.getRequest(url);
}
getSummaryPurchase(purchaseId) {
const url = `/don_confiao/resumen_compra_json/${purchaseId}`;
return this.getRequest(url);
}
getPurchasesForReconciliation() {
const url = '/don_confiao/purchases/for_reconciliation';
return this.getRequest(url);
}
getListReconcliations(page, itemsPerPage) {
const url = `/don_confiao/api/reconciliate_jar/?page=${page}&page_size=${itemsPerPage}`;
return this.getRequest(url);
}
getReconciliation(reconciliationId) {
const url = `/don_confiao/api/reconciliate_jar/${reconciliationId}/`;
return this.getRequest(url);
}
isValidAdminCode(code) {
const url = `/don_confiao/api/admin_code/validate/${code}`
return this.getRequest(url)
}
createPurchase(purchase) {
const url = '/don_confiao/api/sales/';
return this.postRequest(url, purchase);
}
createReconciliationJar(reconciliation) {
const url = '/don_confiao/reconciliate_jar';
return this.postRequest(url, reconciliation);
}
createCustomer(customer) {
const url = '/don_confiao/api/customers/';
return this.postRequest(url, customer);
}
getRequest(url) {
return new Promise ((resolve, reject) => {
fetch(url)
.then(response => response.json())
.then(data => {
resolve(data);
})
.catch(error => {
reject(error);
});
});
}
postRequest(url, content) {
return new Promise((resolve, reject) => {
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(content)
})
.then(response => {
if (!response.ok) {
reject(new Error(`Error ${response.status}: ${response.statusText}`));
} else {
response.json().then(data => {
if (!data) {
reject(new Error('La respuesta no es un JSON válido'));
} else {
resolve(data);
}
});
}
})
.catch(error => reject(error));
});
}
}
export default DjangoApi;

View File

@@ -0,0 +1,5 @@
# Store
Pinia stores are used to store reactive state and expose actions to mutate it.
Full documentation for this feature can be found in the Official [Pinia](https://pinia.esm.dev/) repository.

View File

@@ -0,0 +1,8 @@
// Utilities
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app', {
state: () => ({
//
}),
})

View File

@@ -0,0 +1,4 @@
// Utilities
import { createPinia } from 'pinia'
export default createPinia()

View File

@@ -0,0 +1,3 @@
# Styles
This directory is for configuring the styles of the application.

View File

@@ -0,0 +1,10 @@
/**
* src/styles/settings.scss
*
* Configures SASS variables and Vuetify overwrites
*/
// https://vuetifyjs.com/features/sass-variables/`
// @use 'vuetify/settings' with (
// $color-pack: false
// );

View File

@@ -0,0 +1,71 @@
// Plugins
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import Fonts from 'unplugin-fonts/vite'
import Layouts from 'vite-plugin-vue-layouts'
import Vue from '@vitejs/plugin-vue'
import VueRouter from 'unplugin-vue-router/vite'
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
// Utilities
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
VueRouter(),
Layouts(),
Vue({
template: { transformAssetUrls }
}),
// https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme
Vuetify({
autoImport: true,
styles: {
configFile: 'src/styles/settings.scss',
},
}),
Components(),
Fonts({
google: {
families: [{
name: 'Roboto',
styles: 'wght@100;300;400;500;700;900',
}],
},
}),
AutoImport({
imports: [
'vue',
'vue-router',
],
eslintrc: {
enabled: true,
},
vueTemplate: true,
}),
],
define: { 'process.env': {} },
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
extensions: [
'.js',
'.json',
'.jsx',
'.mjs',
'.ts',
'.tsx',
'.vue',
],
},
server: {
port: 3000,
},
build: {
outDir: '../../static/frontend/',
},
base: '/frontend/',
})

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2024-08-17 14:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('don_confiao', '0027_alter_product_name'),
]
operations = [
migrations.AlterField(
model_name='customer',
name='address',
field=models.CharField(blank=True, max_length=100, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2024-08-17 19:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('don_confiao', '0028_alter_customer_address'),
]
operations = [
migrations.AlterField(
model_name='customer',
name='name',
field=models.CharField(default=None, max_length=100),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.0.6 on 2024-08-17 21:00
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('don_confiao', '0029_alter_customer_name'),
]
operations = [
migrations.CreateModel(
name='PaymentSale',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('payment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='don_confiao.payment')),
('sale', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='don_confiao.sale')),
],
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.0.6 on 2024-10-26 22:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('don_confiao', '0030_paymentsale'),
]
operations = [
migrations.RenameField(
model_name='customer',
old_name='address',
new_name='email',
),
migrations.AddField(
model_name='customer',
name='phone',
field=models.CharField(blank=True, max_length=100, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2024-10-26 22:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('don_confiao', '0031_rename_address_customer_email_customer_phone'),
]
operations = [
migrations.AddField(
model_name='customer',
name='address',
field=models.CharField(blank=True, max_length=100, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2024-11-09 17:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('don_confiao', '0032_customer_address'),
]
operations = [
migrations.AddField(
model_name='sale',
name='payment_method',
field=models.CharField(choices=[('CASH', 'Cash'), ('CONFIAR', 'Confiar'), ('BANCOLOMBIA', 'Bancolombia')], default='CASH', max_length=30),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.0.6 on 2024-11-16 20:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('don_confiao', '0033_sale_payment_method'),
]
operations = [
migrations.AlterField(
model_name='payment',
name='type_payment',
field=models.CharField(choices=[('CASH', 'Efectivo'), ('CONFIAR', 'Confiar'), ('BANCOLOMBIA', 'Bancolombia')], default='CASH', max_length=30),
),
migrations.AlterField(
model_name='sale',
name='date',
field=models.DateTimeField(verbose_name='Date'),
),
migrations.AlterField(
model_name='sale',
name='payment_method',
field=models.CharField(choices=[('CASH', 'Efectivo'), ('CONFIAR', 'Confiar'), ('BANCOLOMBIA', 'Bancolombia')], default='CASH', max_length=30),
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 5.0.6 on 2024-11-18 03:16
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('don_confiao', '0033_sale_payment_method'),
]
operations = [
migrations.AddField(
model_name='sale',
name='reconciliation',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='Sales', to='don_confiao.reconciliationjar'),
),
migrations.AlterField(
model_name='payment',
name='type_payment',
field=models.CharField(choices=[('CASH', 'Efectivo'), ('CONFIAR', 'Confiar'), ('BANCOLOMBIA', 'Bancolombia')], default='CASH', max_length=30),
),
migrations.AlterField(
model_name='sale',
name='payment_method',
field=models.CharField(choices=[('CASH', 'Efectivo'), ('CONFIAR', 'Confiar'), ('BANCOLOMBIA', 'Bancolombia')], default='CASH', max_length=30),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.0.6 on 2024-12-03 02:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('don_confiao', '0034_sale_reconciliation_alter_payment_type_payment_and_more'),
]
operations = [
migrations.AddField(
model_name='reconciliationjar',
name='total_cash_purchases',
field=models.DecimalField(decimal_places=2, default=0, max_digits=9),
preserve_default=False,
),
]

View File

@@ -0,0 +1,14 @@
# Generated by Django 5.0.6 on 2024-12-28 22:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('don_confiao', '0034_alter_payment_type_payment_alter_sale_date_and_more'),
('don_confiao', '0035_reconciliationjar_total_cash_purchases'),
]
operations = [
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.0.6 on 2025-01-11 23:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('don_confiao', '0036_merge_20241228_2212'),
]
operations = [
migrations.CreateModel(
name='AdminCode',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.CharField(max_length=255)),
],
),
]

View File

@@ -3,11 +3,23 @@ from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from decimal import Decimal
from datetime import datetime
class PaymentMethods(models.TextChoices):
CASH = 'CASH', _('Efectivo')
CONFIAR = 'CONFIAR', _('Confiar')
BANCOLOMBIA = 'BANCOLOMBIA', _('Bancolombia')
class Customer(models.Model):
name = models.CharField(max_length=100)
address = models.CharField(max_length=100)
name = models.CharField(max_length=100, default=None, null=False, blank=False)
address = models.CharField(max_length=100, null=True, blank=True)
email = models.CharField(max_length=100, null=True, blank=True)
phone = models.CharField(max_length=100, null=True, blank=True)
def __str__(self):
return self.name
class MeasuringUnits(models.TextChoices):
@@ -34,21 +46,88 @@ class Product(models.Model):
def __str__(self):
return self.name
@classmethod
def to_list(cls):
products_list = []
all_products = cls.objects.all()
for product in all_products:
rproduct = {
"id": product.id,
"name": product.name,
"price_list": product.price,
"uom": product.measuring_unit,
"categories": [c.name for c in product.categories.all()]
}
products_list.append(rproduct)
return products_list
class ReconciliationJar(models.Model):
is_valid = models.BooleanField(default=False)
date_time = models.DateTimeField()
description = models.CharField(max_length=255, null=True, blank=True)
reconcilier = models.CharField(max_length=255, null=False, blank=False)
cash_taken = models.DecimalField(max_digits=9, decimal_places=2)
cash_discrepancy = models.DecimalField(max_digits=9, decimal_places=2)
total_cash_purchases = models.DecimalField(max_digits=9, decimal_places=2)
def clean(self):
self._validate_taken_ammount()
def add_payments(self, payments):
for payment in payments:
self.payment_set.add(payment)
self.is_valid = True
def _validate_taken_ammount(self):
ammount_cash = self.cash_taken + self.cash_discrepancy
if not self.total_cash_purchases == ammount_cash:
raise ValidationError(
{"cash_taken": _("The taken ammount has discrepancy.")}
)
class Sale(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.PROTECT)
date = models.DateField("Date")
date = models.DateTimeField("Date")
phone = models.CharField(max_length=13, null=True, blank=True)
description = models.CharField(max_length=255, null=True, blank=True)
payment_method = models.CharField(
max_length=30,
choices=PaymentMethods.choices,
default=PaymentMethods.CASH,
blank=False,
null=False
)
reconciliation = models.ForeignKey(
ReconciliationJar,
on_delete=models.RESTRICT,
related_name='Sales',
null=True
)
def __str__(self):
return f"{self.date} {self.customer}"
def get_total(self):
lines = self.saleline_set.all()
return sum([l.quantity * l.unit_price for l in lines])
def clean(self):
if self.payment_method not in PaymentMethods.values:
raise ValidationError({'payment_method': "Invalid payment method"})
@classmethod
def sale_header_csv(cls):
sale_header_csv = [field.name for field in cls._meta.fields]
return sale_header_csv
class SaleLine(models.Model):
sale = models.ForeignKey(Sale, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
product = models.ForeignKey(Product, null=False, blank=False, on_delete=models.CASCADE)
quantity = models.IntegerField(null=True)
unit_price = models.DecimalField(max_digits=9, decimal_places=2)
description = models.CharField(max_length=255, null=True, blank=True)
@@ -57,12 +136,6 @@ class SaleLine(models.Model):
return f"{self.sale} - {self.product}"
class PaymentMethods(models.TextChoices):
CASH = 'CASH', _('Cash')
CONFIAR = 'CONFIAR', _('Confiar')
BANCOLOMBIA = 'BANCOLOMBIA', _('Bancolombia')
class ReconciliationJarSummary():
def __init__(self, payments):
self._validate_payments(payments)
@@ -80,38 +153,6 @@ class ReconciliationJarSummary():
return self._payments
class ReconciliationJar(models.Model):
is_valid = models.BooleanField(default=False)
date_time = models.DateTimeField()
description = models.CharField(max_length=255, null=True, blank=True)
reconcilier = models.CharField(max_length=255, null=False, blank=False)
cash_taken = models.DecimalField(max_digits=9, decimal_places=2)
cash_discrepancy = models.DecimalField(max_digits=9, decimal_places=2)
def clean(self):
if not self.is_valid:
payments = Payment.get_reconciliation_jar_summary().payments
else:
payments = self.payment_set.all()
payments_amount = Decimal(sum([p.amount for p in payments]))
reconciliation_ammount = Decimal(sum([
self.cash_taken,
self.cash_discrepancy,
]))
equal_ammounts = reconciliation_ammount.compare(payments_amount) == Decimal('0')
if not equal_ammounts:
raise ValidationError(
{"cash_taken": _("The taken ammount has discrepancy.")}
)
def add_payments(self, payments):
for payment in payments:
self.payment_set.add(payment)
self.is_valid = True
class Payment(models.Model):
date_time = models.DateTimeField()
type_payment = models.CharField(
@@ -137,3 +178,27 @@ class Payment(models.Model):
reconciliation_jar=None
)
)
@classmethod
def total_payment_from_sale(cls, payment_method, sale):
payment = cls()
payment.date_time = datetime.today()
payment.type_payment = payment_method
payment.amount = sale.get_total()
payment.clean()
payment.save()
payment_sale = PaymentSale()
payment_sale.payment = payment
payment_sale.sale = sale
payment_sale.clean()
payment_sale.save()
class PaymentSale(models.Model):
payment = models.ForeignKey(Payment, on_delete=models.CASCADE)
sale = models.ForeignKey(Sale, on_delete=models.CASCADE)
class AdminCode(models.Model):
value = models.CharField(max_length=255, null=False, blank=False)

View File

@@ -0,0 +1,103 @@
from rest_framework import serializers
from .models import Sale, SaleLine, Product, Customer, ReconciliationJar
class SaleLineSerializer(serializers.ModelSerializer):
class Meta:
model = SaleLine
fields = ['id', 'sale', 'product', 'unit_price', 'quantity']
class SaleSerializer(serializers.ModelSerializer):
total = serializers.ReadOnlyField(source='get_total')
class Meta:
model = Sale
fields = ['id', 'customer', 'date', 'saleline_set',
'total', 'payment_method']
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ['id', 'name', 'price', 'measuring_unit', 'categories']
class CustomerSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = ['id', 'name', 'address', 'email', 'phone']
class ReconciliationJarSerializer(serializers.ModelSerializer):
Sales = SaleSerializer(many=True, read_only=True)
class Meta:
model = ReconciliationJar
fields = [
'id',
'date_time',
'reconcilier',
'cash_taken',
'cash_discrepancy',
'total_cash_purchases',
'Sales',
]
class PaymentMethodSerializer(serializers.Serializer):
text = serializers.CharField()
value = serializers.CharField()
def to_representation(self, instance):
return {
'text': instance[1],
'value': instance[0],
}
class SaleForRenconciliationSerializer(serializers.Serializer):
id = serializers.IntegerField()
date = serializers.DateTimeField()
payment_method = serializers.CharField()
customer = serializers.SerializerMethodField()
total = serializers.SerializerMethodField()
def get_customer(self, sale):
return {
'id': sale.customer.id,
'name': sale.customer.name,
}
def get_total(self, sale):
return sale.get_total()
class ListCustomerSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = ['id', 'name']
class ListProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ['id', 'name']
class SummarySaleLineSerializer(serializers.ModelSerializer):
product = ListProductSerializer()
class Meta:
model = SaleLine
fields = ['product', 'quantity', 'unit_price', 'description']
class SaleSummarySerializer(serializers.ModelSerializer):
customer = ListCustomerSerializer()
lines = SummarySaleLineSerializer(many=True, source='saleline_set')
class Meta:
model = Sale
fields = ['id', 'date', 'customer', 'payment_method', 'lines']

View File

@@ -0,0 +1,37 @@
nav#main_menu a {
text-decoration: none;
font-weight: bold;
color: white;
background-color: #178E79 ;
padding: 10px 20px;
min-width: 90%;
text-align: center;
border: solid #178E79 4px;
border-radius: 5px;
cursor: pointer;
}
nav#main_menu a:hover {
transition: ease-in-out 0.2s;
background-color: #7BDCB5;
color: #178E79;
}
nav#main_menu a:active {
background-color: #aaa;
color: #444;
}
nav#main_menu {
display: inline;
}
li{
width: 100%;
display: flex;
justify-content: center;
}
.page_title {
color: #04A1E4
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

View File

@@ -25,5 +25,6 @@ document.addEventListener('DOMContentLoaded', function(){
formContainer.appendChild(newForm);
totalForms.value = formCount + 1;
setPriceListeners();
});
});

View File

@@ -0,0 +1,22 @@
setPriceListeners();
function setPriceListeners() {
document.querySelectorAll('select[id^="id_saleline_set-"][id$="-product"]').forEach((input) => {
input.addEventListener('change', (e) => setLinePrice(e));
});
}
function setLinePrice(e) {
let input = e.target;
const idLine = input.id.split('-')[1];
const productId = input.value;
const priceInput = document.getElementById(`id_saleline_set-${idLine}-unit_price`);
const product = listProducts.find((product) => product.id == productId);
if (product) {
priceInput.value = product.price_list;
} else {
priceInput.value = '';
}
}

View File

@@ -0,0 +1,50 @@
const quantity_lineRegexSelector = `[id^="${idPrefix}"][id$="${quantitySuffix}"]`;
const price_lineRegexSelector = `[id^="${idPrefix}"][id$="${priceSuffix}"]`;
function insertSubtotalField() {
// Selecciona la fila de precio unitario para añadir la fila del subtotal después de ella
const unitPriceRow = document.querySelector('input[id="id_saleline_set-0-unit_price"]').closest('tr');
// Crear una nueva fila para el subtotal
const subtotalRow = document.createElement('tr');
subtotalRow.innerHTML = `
<th><label for="id_saleline_set-0-subtotal">Subtotal:</label></th>
<td><input type="number" name="saleline_set-0-subtotal" id="id_saleline_set-0-subtotal" readonly></td>
`;
// Insertar la fila del subtotal después de la fila del precio unitario
unitPriceRow.after(subtotalRow);
}
function calculateSubtotal(id) {
const quantityElement = document.getElementById(`id_saleline_set-${id}-quantity`);
const unitPriceElement = document.getElementById(`id_saleline_set-${id}-unit_price`);
const subtotalElement = document.getElementById(`id_saleline_set-${id}-subtotal`);
const quantity = parseFloat(quantityElement.value) || 0;
const unitPrice = parseFloat(unitPriceElement.value) || 0;
const subtotal = quantity * unitPrice;
subtotalElement.value = subtotal.toFixed(2);
}
// Inserta el campo subtotal al cargar la página
window.addEventListener('load', () => {
insertSubtotalField();
complete_form.addEventListener('change', function(event){
const quantityInputs = document.querySelectorAll(quantity_lineRegexSelector);
const ids = Array.prototype.map.call(quantityInputs, function(input) {
return input.id.match(/\d+/)[0];
});
ids.forEach(function(id) {
if (event.target.matches(quantity_lineRegexSelector)) {
calculateSubtotal(id);
}
if (event.target.matches(price_lineRegexSelector)) {
calculateSubtotal(id);
}
})
});
});

View File

@@ -0,0 +1,17 @@
{% load static %}
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<title>Don Confiao - Tienda la Ilusión</title>
</head>
<body class="flex h-full w-full">
<div id="menu" class="h-full w-2/12 border bg-green-400 max-h-screen overflow-auto">
{% include 'don_confiao/menu.html' %}
</div>
<div id="content" class="w-10/12 h-screen max-h-full overflow-auto">
{% block content %} {% endblock %}
</div>
</body>
<script src="https://cdn.tailwindcss.com/"></script>
</html>

View File

@@ -0,0 +1,13 @@
{% extends 'don_confiao/base.html' %}
{% block content %}
{% if form.is_multipart %}
<form enctype="multipart/form-data" method="post">
{% else %}
<form method="post">
{% endif %}
{% csrf_token %}
{{ form }}
<input type="submit" value="Importar">
</form>
{% endblock %}

View File

@@ -1,3 +1,5 @@
{% extends 'don_confiao/base.html' %}
{% block content %}
{% if form.is_multipart %}
<form enctype="multipart/form-data" method="post">
{% else %}
@@ -8,3 +10,4 @@
{{ form }}
<input type="submit" value="Importar">
</form>
{% endblock %}

View File

@@ -4,4 +4,5 @@
<li><a href='./comprar'>Comprar</a></li>
<li><a href='./productos'>Productos</a></li>
<li><a href='./importar_productos'>Importar Productos</a></li>
<li><a href='./importar_terceros'>Importar Terceros</a></li>
</ul>

View File

@@ -0,0 +1,17 @@
{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'css/main_menu.css' %}">
<div class="h-full flex flex-col justify-around shadow hover:shadow-lg">
<img class="w-full px-12" src="{% static 'img/recreo_logo.png' %}" alt="Recreo">
<nav id="main_menu">
<ul class="flex flex-col m-0 p-0 justify-center shadow hover:shadow-lg gap-y-12 items-center drop-shadow-lg">
<li><a href='/don_confiao/comprar' >Comprar</a></li>
<li><a href='/don_confiao/compras'>Compras</a></li>
<li><a href='/don_confiao/lista_productos'>Productos</a></li>
<li><a href='/don_confiao/importar_productos'>Importar Productos</a></li>
<li><a href='/don_confiao/importar_terceros'>Importar Terceros</a></li>
</ul>
</nav>
<p id="page_title" class="text-center decoration-solid font-mono font-bold text-lg page_title">Don Confiao - Tienda la Ilusión</p>
</div>
<script src="https://cdn.tailwindcss.com/"></script>

View File

@@ -1,9 +1,10 @@
{% extends 'don_confiao/base.html' %}
{% block content %}
<form action="" method="get">
<label>Filtro por nombre:</label>
<input type="text" name="name" value="{{ request.GET.name }}">
<button type="submit">Filtrar</button>
</form>
{% block content %}
<h1>Lista de productos</h1>
<ul>
{% for obj in object_list %}

View File

@@ -1,21 +1,39 @@
<!doctype html>
{% extends 'don_confiao/base.html' %}
{% block content %}
{% load static %}
<form id="complete_form_purchase" method="POST">
<script>
let listProducts = JSON.parse("{{ list_products|escapejs }}");
</script>
<div class="flex h-full">
<div class="h-full w-10/12 flex flex-col p-5">
<form id="complete_form_purchase" method="POST" class="h-10/12 w-full max-h-full overflow-auto">
{% csrf_token %}
{{ sale_form }}
{{ linea_formset.management_form }}
<div id="formset-container">
<div id="formset-container" class="w-full">
{% for form in linea_formset %}
<div class="form-container">
<table style="border: solid 1px blue; margin: 10px">
<div class="form-container flex justify-center ">
<table class="w-3/4 my-5 shadow-inner" style="border: solid 1px #178E79;">
{{ form.as_table }}
</table>
</div>
{% endfor %}
</div>
<button id="add_line" type="button" onclick="add_line">Añadir Linea</button>
<div class="h-2/12 flex justify-center">
<button id="add_line" type="button" class="bg-yellow-400 shadow hover:shadow-lg py-2 px-5 rounded-full font-bold hover:bg-violet-200 ease-in duration-150">Añadir Linea</button>
</div>
</div>
<div class="h-full w-3/12 bg-green-400 p-5 shadow hover:shadow-lg flex flex-col gap-y-3 font-semibold justify-around">
<p id="sale_resume_title" class="text-center decoration-solid font-mono font-bold text-xl page_title">Resumen de Venta</p>
{{ sale_form }}
{{ summary_form }}
<br/><button name="form" type="submit" >Comprar</button>
<script src="{% static 'js/add_line.js' %}"></script>
<script src="{% static 'js/sale_summary.js' %}"></script>
</form>
<button class="font-bold my-10 py-2 px-4 rounded-full bg-yellow-400 shadow hover:shadow-lg hover:bg-violet-200 ease-in duration-150" name="form" type="submit" >Comprar</button>
</div>
</form>
</div>
<script src="https://cdn.tailwindcss.com/"></script>
<script src="{% static 'js/buy_general.js' %}"></script>
<script src="{% static 'js/add_line.js' %}"></script>
<script src="{% static 'js/sale_summary.js' %}"></script>
<script src="{% static 'js/calculate_subtotal_line.js' %}"></script>
{% endblock %}

View File

@@ -0,0 +1,12 @@
{% extends 'don_confiao/base.html' %}
{% block content %}
<h1>Resumen de compra</h1>
<dl>
<dt>Date</dt> <dd>{{ purchase.date }}</dd>
<dt>ID</dt> <dd>{{ purchase.id }}</dd>
<dt>Customer</dt> <dd>{{ purchase.customer.name }}</dd>
<dt>Total</dt> <dd>{{ purchase.get_total }}</dd>
</dl>
{% endblock %}

View File

@@ -1,9 +1,14 @@
{% extends 'don_confiao/base.html' %}
{% block content %}
{% if purchases %}
<ul>
{% for purchase in purchases %}
<li><a href="/don_confiao/{{ purchase.id }}/">{{ purchase.date }}, {{ purchase.customer }}</a></li>
<li><a href="/don_confiao/resumen_compra/{{ purchase.id }}">{{ purchase.date }}, {{ purchase.customer }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No hay Compras</p>
{% endif %}
{% endblock %}

View File

@@ -1,30 +0,0 @@
<!doctype html>
{% if summary.total %}
<div class="reconciliate_jar summary" style="border: solid 1px brown; margin: 10px">
<h2>Pagos No reconciliados</h2>
<table style="border: solid 1px blue; margin: 10px">
<thead>
<tr><th>Fecha</th><th>Monto</th></tr>
</thead>
<tbody>
{% for payment in summary.payments %}
<tr><td>{{ payment.date_time }}</td><td>{{ payment.amount }}</td></tr>
{% endfor %}
</tbody>
<tfoot>
<tr><th>Total</th><td>{{ summary.total }}</td></tr>
</tfoot>
</table>
</div>
<form method="POST">
<table style="border: solid 1px blue; margin: 10px">
{% csrf_token %}
{{ form.as_table }}
</table>
<br/><button name="form" type="submit" >Recoger dinero</button>
</form>
{% else %}
<div class="reconciliate_jar information noform">
<h2>No hay pagos registrados.</h2>
</div>
{% endif %}

View File

@@ -0,0 +1,47 @@
[
{
"model": "don_confiao.customer",
"pk": 1,
"fields": {
"name": "Alejandro Fernandez",
"address": "Avenida Siempre Viva"
}
},
{
"model": "don_confiao.productcategory",
"pk": 1,
"fields": {
"name": "Unidad"
}
},
{
"model": "don_confiao.product",
"pk": 1,
"fields": {
"name": "Papaya",
"price": 2500,
"measuring_unit": "Unidad"
}
},
{
"model": "don_confiao.sale",
"pk": 1,
"fields": {
"customer": 1,
"date": "2024-08-31",
"phone": 312201103,
"description": "Primera Venta"
}
},
{
"model": "don_confiao.saleline",
"pk": 1,
"fields": {
"sale": 1,
"product": 1,
"quantity": 10,
"unit_price": 5000,
"description": "Primer Sale Line"
}
}
]

View File

@@ -0,0 +1,41 @@
from django.test import TestCase, Client
from ..models import AdminCode
import json
class TestAdminCode(TestCase):
def setUp(self):
self.valid_code = 'some valid code'
admin_code = AdminCode()
admin_code.value = self.valid_code
admin_code.clean()
admin_code.save()
self.client = Client()
def test_validate_code(self):
url = '/don_confiao/api/admin_code/validate/' + self.valid_code
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode('utf-8'))
self.assertTrue(content['validCode'])
def test_invalid_code(self):
invalid_code = 'some invalid code'
url = '/don_confiao/api/admin_code/validate/' + invalid_code
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode('utf-8'))
self.assertFalse(content['validCode'])
def test_empty_code(self):
empty_code = ''
url = '/don_confiao/api/admin_code/validate/' + empty_code
response = self.client.get(url)
self.assertEqual(response.status_code, 404)

View File

@@ -0,0 +1,59 @@
import json
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from ..models import Sale, Product, Customer
class TestAPI(APITestCase):
def setUp(self):
self.product = Product.objects.create(
name='Panela',
price=5000,
measuring_unit='UNIT'
)
self.customer = Customer.objects.create(
name='Camilo'
)
def test_create_sale(self):
response = self._create_sale()
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Sale.objects.count(), 1)
sale = Sale.objects.all()[0]
self.assertEqual(
sale.customer.name,
self.customer.name
)
self.assertEqual(
sale.id,
content['id']
)
def test_get_products(self):
url = '/don_confiao/api/products/'
response = self.client.get(url)
json_response = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, 200)
self.assertEqual(self.product.name, json_response[0]['name'])
def test_get_customers(self):
url = '/don_confiao/api/customers/'
response = self.client.get(url)
json_response = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, 200)
self.assertEqual(self.customer.name, json_response[0]['name'])
def _create_sale(self):
url = '/don_confiao/api/sales/'
data = {
'customer': self.customer.id,
'date': '2024-09-02',
'payment_method': 'CASH',
'saleline_set': [
{'product': self.product.id, 'quantity': 2, 'unit_price': 3000},
{'product': self.product.id, 'quantity': 3, 'unit_price': 5000}
],
}
return self.client.post(url, data, format='json')

View File

@@ -1,88 +0,0 @@
from django.test import TestCase
from django.core.exceptions import ValidationError
from ..models import Payment, ReconciliationJar
class TestBilling(TestCase):
def test_reconciliation_jar_summary(self):
cash_payment1, cash_payment2 = self._create_two_cash_payments()
jar_summary = Payment.get_reconciliation_jar_summary()
self.assertEqual(164000, jar_summary.total)
self.assertSetEqual(
{cash_payment1, cash_payment2},
set(jar_summary.payments)
)
def test_reconciliation_jar_summary_use_only_cash(self):
cash_payment1, cash_payment2 = self._create_two_cash_payments()
confiar_payment = Payment()
confiar_payment.date_time = '2024-07-07 16:00:00'
confiar_payment.type_payment = 'CONFIAR'
confiar_payment.amount = 85000
confiar_payment.save()
bancolombia_payment = Payment()
bancolombia_payment.date_time = '2024-07-07 12:30:00'
bancolombia_payment.type_payment = 'BANCOLOMBIA'
bancolombia_payment.amount = 12000
bancolombia_payment.save()
jar_summary = Payment.get_reconciliation_jar_summary()
self.assertEqual(164000, jar_summary.total)
self.assertSetEqual(
{cash_payment1, cash_payment2},
set(jar_summary.payments)
)
def test_fail_validate_reconciliation_jar_with_discrepancy_values(self):
cash_payment1, cash_payment2 = self._create_two_cash_payments()
jar_summary = Payment.get_reconciliation_jar_summary()
reconciliation_jar = ReconciliationJar()
reconciliation_jar.date_time = '2024-07-13 13:02:00'
reconciliation_jar.description = "test reconcialiation jar"
reconciliation_jar.reconcilier = 'Jorge'
reconciliation_jar.cash_float = 0
reconciliation_jar.cash_taken = 0
reconciliation_jar.cash_discrepancy = 0
reconciliation_jar.save()
reconciliation_jar.add_payments(jar_summary.payments)
with self.assertRaises(ValidationError):
reconciliation_jar.clean()
def test_validate_reconciliation_jar_with_cash_float(self):
cash_payment1, cash_payment2 = self._create_two_cash_payments()
jar_summary = Payment.get_reconciliation_jar_summary()
reconciliation_jar = ReconciliationJar()
reconciliation_jar.date_time = '2024-07-13 13:02:00'
reconciliation_jar.description = "test reconcialiation jar"
reconciliation_jar.reconcilier = 'Jorge'
reconciliation_jar.cash_taken = jar_summary.total
reconciliation_jar.cash_discrepancy = 0
reconciliation_jar.save()
reconciliation_jar.add_payments(jar_summary.payments)
reconciliation_jar.clean()
reconciliation_jar.save()
self.assertTrue(reconciliation_jar.is_valid)
def _create_two_cash_payments(self):
cash_payment1 = Payment()
cash_payment1.date_time = '2024-07-07 12:00:00'
cash_payment1.type_payment = 'CASH'
cash_payment1.amount = 132000
cash_payment1.description = 'Saldo en compra'
cash_payment1.save()
cash_payment2 = Payment()
cash_payment2.date_time = '2024-07-07 13:05:00'
cash_payment2.type_payment = 'CASH'
cash_payment2.amount = 32000
cash_payment2.save()
return [cash_payment1, cash_payment2]

View File

@@ -0,0 +1,22 @@
from django.test import Client, TestCase
from ..models import Product
class TestBuyForm(TestCase):
def setUp(self):
self.client = Client()
self.product = Product()
self.product.name = "Arroz"
self.product.price = 5000
self.product.save()
def test_buy_contains_products_list(self):
response = self.client.get('/don_confiao/comprar')
self.assertIn(
self.product.name,
response.context['list_products']
)
content = response.content.decode('utf-8')
self.assertIn('5000', content)
self.assertIn('Arroz', content)
self.assertIn(str(self.product.id), content)

View File

@@ -0,0 +1,50 @@
#!/usr/bin/env python3
from django.test import Client, TestCase
from io import StringIO
import csv
class TestExportSales(TestCase):
fixtures = ['sales_fixture']
def setUp(self):
self.client = Client()
def test_export_sales(self):
sales_response = self._export_sales_csv()
filename = sales_response.headers[
'Content-Disposition'].split('; ')[1].strip('filename=').strip("'")
content = sales_response.content
content_str = content.decode('utf-8')
csv_file = StringIO(content_str)
header = next(csv.reader(csv_file))
self.assertGreater(len(content), 0)
self.assertEqual(filename, 'sales.csv')
self.assertEqual(sales_response.headers['Content-Type'], 'text/csv')
self.assertEqual(header, self._tryton_sale_header())
def _export_sales_csv(self):
return self.client.get("/don_confiao/exportar_ventas_para_tryton")
def _tryton_sale_header(self):
return [
"Tercero",
"Dirección de facturación",
"Dirección de envío",
"Descripción",
"Referencia",
"Fecha venta",
"Plazo de pago",
"Almacén",
"Moneda",
"Líneas/Producto",
"Líneas/Cantidad",
"Líneas/Precio unitario",
"Líneas/Unidad",
"Empresa",
"Tienda",
"Terminal de venta",
"Autorecogida",
"Comentario"
]

View File

@@ -0,0 +1,264 @@
from django.test import TestCase, Client
from django.core.exceptions import ValidationError
from ..models import Sale, Product, SaleLine, Customer, ReconciliationJar
import json
class TestJarReconcliation(TestCase):
def setUp(self):
customer = Customer()
customer.name = 'Alejo Mono'
customer.save()
self.client = Client()
purchase = Sale()
purchase.customer = customer
purchase.date = "2024-07-30"
purchase.payment_method = 'CASH'
purchase.clean()
purchase.save()
product = Product()
product.name = "cafe"
product.price = "72500"
product.save()
line = SaleLine()
line.sale = purchase
line.product = product
line.quantity = "11"
line.unit_price = "72500"
line.save()
self.purchase = purchase
purchase2 = Sale()
purchase2.customer = customer
purchase2.date = "2024-07-30"
purchase.payment_method = 'CASH'
purchase2.clean()
purchase2.save()
line2 = SaleLine()
line2.sale = purchase2
line2.product = product
line2.quantity = "27"
line2.unit_price = "72500"
line2.save()
self.purchase2 = purchase2
purchase3 = Sale()
purchase3.customer = customer
purchase3.date = "2024-07-30"
purchase3.payment_method = 'CASH'
purchase3.clean()
purchase3.save()
line3 = SaleLine()
line3.sale = purchase3
line3.product = product
line3.quantity = "37"
line3.unit_price = "72500"
line3.save()
self.purchase3 = purchase3
purchase4 = Sale()
purchase4.customer = customer
purchase4.date = "2024-07-30"
purchase4.payment_method = 'CONFIAR'
purchase4.clean()
purchase4.save()
line4 = SaleLine()
line4.sale = purchase4
line4.product = product
line4.quantity = "47"
line4.unit_price = "72500"
line4.save()
self.purchase4 = purchase4
def test_create_reconciliation_jar(self):
reconciliation = self._create_simple_reconciliation()
self.assertTrue(isinstance(reconciliation, ReconciliationJar))
def test_get_purchases_for_reconciliation(self):
# link purchase to reconciliation to exclude from list
reconciliation = self._create_simple_reconciliation()
self.purchase3.reconciliation = reconciliation
self.purchase3.clean()
self.purchase3.save()
url = '/don_confiao/purchases/for_reconciliation'
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
rawContent = response.content.decode('utf-8')
content = json.loads(rawContent)
self.assertIn('CASH', content.keys())
self.assertIn('CONFIAR', content.keys())
self.assertEqual(2, len(content.get('CASH')))
self.assertEqual(1, len(content.get('CONFIAR')))
self.assertNotIn(str(37*72500), rawContent)
self.assertIn(str(47*72500), rawContent)
def test_don_create_reconcialiation_with_bad_numbers(self):
reconciliation = ReconciliationJar()
reconciliation.date_time = "2024-07-30"
reconciliation.total_cash_purchases = 145000
reconciliation.cash_taken = 143000
reconciliation.cash_discrepancy = 1000
with self.assertRaises(ValidationError):
reconciliation.clean()
reconciliation.save()
def test_fail_create_reconciliation_with_wrong_total_purchases_purchases(self):
url = '/don_confiao/reconciliate_jar'
total_purchases = (11 * 72500) + (27 * 72500)
bad_total_purchases = total_purchases + 2
data = {
'date_time': '2024-12-02T21:07',
'reconcilier': 'carlos',
'total_cash_purchases': bad_total_purchases,
'cash_taken': total_purchases,
'cash_discrepancy': 0,
'cash_purchases': [
self.purchase.id,
self.purchase2.id,
self.purchase.id,
],
}
response = self.client.post(url, data=json.dumps(data).encode('utf-8'),
content_type='application/json')
rawContent = response.content.decode('utf-8')
content = json.loads(rawContent)
self.assertEqual(response.status_code, 400)
self.assertIn('error', content)
self.assertIn('total_cash_purchases', content['error'])
def test_create_reconciliation_with_purchases(self):
response = self._create_reconciliation_with_purchase()
rawContent = response.content.decode('utf-8')
content = json.loads(rawContent)
self.assertEqual(response.status_code, 200)
self.assertIn('id', content)
purchases = Sale.objects.filter(reconciliation_id=content['id'])
self.assertEqual(len(purchases), 2)
def test_create_reconciliation_with_purchases_and_other_totals(self):
url = '/don_confiao/reconciliate_jar'
total_purchases = (11 * 72500) + (27 * 72500)
data = {
'date_time': '2024-12-02T21:07',
'reconcilier': 'carlos',
'total_cash_purchases': total_purchases,
'cash_taken': total_purchases,
'cash_discrepancy': 0,
'cash_purchases': [
self.purchase.id,
self.purchase2.id,
],
'other_totals': {
'Confiar': {
'total': (47 * 72500) + 1,
'purchases': [self.purchase4.id],
},
},
}
response = self.client.post(url, data=json.dumps(data).encode('utf-8'),
content_type='application/json')
rawContent = response.content.decode('utf-8')
content = json.loads(rawContent)
self.assertEqual(response.status_code, 200)
self.assertIn('id', content)
purchases = Sale.objects.filter(reconciliation_id=content['id'])
self.assertEqual(len(purchases), 3)
def test_list_reconciliations(self):
self._create_simple_reconciliation()
self._create_simple_reconciliation()
url = '/don_confiao/api/reconciliate_jar/'
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(2, content['count'])
self.assertEqual(2, len(content['results']))
self.assertEqual('2024-07-30T00:00:00Z',
content['results'][0]['date_time'])
def test_list_reconciliations_pagination(self):
self._create_simple_reconciliation()
self._create_simple_reconciliation()
url = '/don_confiao/api/reconciliate_jar/?page=2&page_size=1'
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(1, len(content['results']))
self.assertEqual('2024-07-30T00:00:00Z',
content['results'][0]['date_time'])
def test_get_single_reconciliation(self):
createResponse = self._create_reconciliation_with_purchase()
reconciliationId = json.loads(
createResponse.content.decode('utf-8')
)['id']
self.assertGreater(reconciliationId, 0)
url = f'/don_confiao/api/reconciliate_jar/{reconciliationId}/'
response = self.client.get(url, content_type='application/json')
content = json.loads(
response.content.decode('utf-8')
)
self.assertEqual(reconciliationId, content['id'])
self.assertGreater(len(content['Sales']), 0)
self.assertIn(
self.purchase.id,
[sale['id'] for sale in content['Sales']]
)
self.assertIn(
'CASH',
[sale['payment_method'] for sale in content['Sales']]
)
def _create_simple_reconciliation(self):
reconciliation = ReconciliationJar()
reconciliation.date_time = "2024-07-30"
reconciliation.total_cash_purchases = 0
reconciliation.cash_taken = 0
reconciliation.cash_discrepancy = 0
reconciliation.clean()
reconciliation.save()
return reconciliation
def _create_reconciliation_with_purchase(self):
url = '/don_confiao/reconciliate_jar'
total_purchases = (11 * 72500) + (27 * 72500)
data = {
'date_time': '2024-12-02T21:07',
'reconcilier': 'carlos',
'total_cash_purchases': total_purchases,
'cash_taken': total_purchases,
'cash_discrepancy': 0,
'cash_purchases': [
self.purchase.id,
self.purchase2.id,
self.purchase.id,
],
}
return self.client.post(url, data=json.dumps(data).encode('utf-8'),
content_type='application/json')

View File

@@ -1,5 +1,7 @@
#!/usr/bin/env python3
from django.test import TestCase
from django.db.utils import IntegrityError
from ..models import Customer
@@ -12,3 +14,8 @@ class TestCustomer(TestCase):
customer.save()
self.assertIsInstance(customer, Customer)
def test_don_create_customer_without_name(self):
customer = Customer()
with self.assertRaises(IntegrityError):
customer.save()

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