Compare commits
	
		
			196 Commits
		
	
	
		
			8d29202d3a
			...
			TrytonApiC
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 03d38f0b64 | |||
| a097bf7141 | |||
| eb75a13857 | |||
| 2a937653df | |||
| e5ae1bb142 | |||
| 9ff1deb687 | |||
| 0caa6fbb56 | |||
| a7e3b9aaa8 | |||
| 0308da7370 | |||
| 0dec800637 | |||
| 0a88641d34 | |||
| 871a82eee5 | |||
| 9ad8ff8706 | |||
| d9f6be8b54 | |||
| 34259921d9 | |||
| 8f9917c3a4 | |||
| fcb83d05fb | |||
| 6d6322b0cd | |||
| a4048473ae | |||
| 9a6a931481 | |||
| 432b7dad74 | |||
| 69d8b1d2ad | |||
| 8b7c2efcb3 | |||
| 5dff7565f4 | |||
| 023beaa0ee | |||
| b5fdd7fefd | |||
| eaf1afdcb4 | |||
| 5cfefdf91a | |||
| 9c0eebd07d | |||
| 8ab7903a0a | |||
| 1b425542b3 | |||
| f6620db6e2 | |||
| 1f2f484e95 | |||
| ef721a6b53 | |||
| f0201a86b2 | |||
| bea08da17d | |||
| a3d5fb1b45 | |||
| 4679170ab9 | |||
| 0d61e457c7 | |||
| 3294b8e814 | |||
| 3189363ba9 | |||
| 0a64373037 | |||
| 9a20212b27 | |||
| a6b4c1c5b6 | |||
| b7984f7556 | |||
| ef1a520838 | |||
| bd6d4221b2 | |||
| 9aa543662b | |||
| 746686afcc | |||
| c709dad36e | |||
|  | 543e927fb0 | ||
| fe1e6e8336 | |||
|  | 00c6bac1d2 | ||
| 4ed6bb9024 | |||
|  | b4467e0292 | ||
| 7c2a7fb1a1 | |||
|  | 8f48accb4f | ||
| d0edba9c28 | |||
| 6aca2007e0 | |||
| b2756ac7ce | |||
| 159bd737c4 | |||
| 201333ab4b | |||
| a5ac06704b | |||
| f1b8cbdce1 | |||
| 2d86aba3e5 | |||
| edea82e77b | |||
| d047edaa3f | |||
| 5c24266e7b | |||
| 9d602c8ddc | |||
| 99f2f77b78 | |||
| 4f0f899c70 | |||
| c85f0554fb | |||
| acd9bf53c6 | |||
| 8f62dfb9ec | |||
| ea77124ee4 | |||
| 66495e25ff | |||
| b7c4cf5d44 | |||
| d5b7c99b79 | |||
| 6bef38e457 | |||
| b80e415393 | |||
| 8ba20acaea | |||
| 65e99b7ce2 | |||
| f6146c177b | |||
| 7c36524763 | |||
| ac5c962ca7 | |||
| 4472d8b6b8 | |||
| 8b15d9dd9d | |||
|  | 02a010b50d | ||
| 50534ef5b1 | |||
| 8c00b89fb8 | |||
| b134b88791 | |||
| 1519b3c8bb | |||
|  | 589b7c0bfb | ||
| 2ed8b5b3a5 | |||
|  | ffa1622870 | ||
| a90fb4d937 | |||
| 2a908d4e05 | |||
| 4cbeaa2560 | |||
| eeb9821675 | |||
| 2ea8cc7fd8 | |||
| 95fab71898 | |||
|  | 83f3bbdc85 | ||
|  | 5910c0c227 | ||
| 8a2a568739 | |||
| 49ac668c14 | |||
| c2e91328fb | |||
| 18df5742d5 | |||
| 91f3d897e5 | |||
| a97c424abc | |||
| 0477e00f75 | |||
| 63f6b3da10 | |||
| d47afabade | |||
| 8506f7c1bd | |||
|  | 5df97c588e | ||
| f110c750a0 | |||
| 7a89eee9fb | |||
| 80ef5c4d9d | |||
| 506a280f6e | |||
| 0b8f9e54e9 | |||
| ee37bfe2cf | |||
| dd62baa3f0 | |||
| 46904d0825 | |||
| 172915d1c1 | |||
| d00f3e60fd | |||
| 2d519f7ca9 | |||
|  | b2de6d3e9d | ||
| 982b2d0c32 | |||
| 7dc843d8f5 | |||
| 78c81549a2 | |||
| 1519453591 | |||
| 6199a93061 | |||
| 71e3808c78 | |||
| cf913884f0 | |||
| 413c77b1fe | |||
| 0c065beb30 | |||
| f2befda953 | |||
| 98d173bf00 | |||
| a517c2323f | |||
| 9857b90cd4 | |||
| f14d27e83c | |||
| 2c8911fb78 | |||
| a7147d1850 | |||
| 8329f3231d | |||
| ffbb2e53d8 | |||
| cf93fecf9f | |||
| a43891942b | |||
| 86b231a828 | |||
| 19a618a671 | |||
| 5adf9a9ce7 | |||
| eaa856e9da | |||
|  | c0329c789a | ||
| a83cd970e6 | |||
| b81d95a9ba | |||
| 603230beda | |||
| 02fbf51659 | |||
| 74bc963096 | |||
| 99ae098c1d | |||
| 1b1a504bf5 | |||
| f1d96467d6 | |||
|  | 0b5163c680 | ||
| 9d06a588c8 | |||
| 23ec2bc298 | |||
| 77361c13db | |||
| e9e4e0e38d | |||
| 384fd3c7f8 | |||
| d9f54ce12b | |||
| 157095dede | |||
| 027f7d75b6 | |||
| b0251b882e | |||
| edaf8f4c77 | |||
| 939745cd54 | |||
| e84c45b238 | |||
| f250cdada2 | |||
| 0e15192f37 | |||
| 980deb61f9 | |||
| 844372d585 | |||
| 9260230d38 | |||
| f847a0e16a | |||
| 0c95c21666 | |||
| 1f37e57e00 | |||
| 7eb8f40d7a | |||
| a401029082 | |||
| 2a9a73c430 | |||
| 4c0c4737ac | |||
| 204bdbcb33 | |||
| 1a54426af6 | |||
| 45199030a0 | |||
| c10aa4b9ca | |||
| efad80970b | |||
| 8f611a523b | |||
| 398f7f2c36 | |||
| de4bb52346 | |||
| 521d3b63fa | |||
|  | 6decc963f6 | ||
|  | 91bb2bce93 | ||
|  | 108d983d74 | 
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -327,3 +327,9 @@ pip-selfcheck.json | |||||||
|  |  | ||||||
| # End of https://www.toptal.com/developers/gitignore/api/emacs,python,django,venv | # 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
									
								
							
							
						
						
									
										87
									
								
								Rakefile
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										8
									
								
								django.Dockerfile
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										27
									
								
								docker-compose.yml
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										10
									
								
								nginx.Dockerfile
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										25
									
								
								nginx.conf
									
									
									
									
									
										Normal 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; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1 +1,2 @@ | |||||||
| Django==5.0.6 | Django==5.0.6 | ||||||
|  | djangorestframework | ||||||
|   | |||||||
							
								
								
									
										133
									
								
								tienda_ilusion/don_confiao/api_views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								tienda_ilusion/don_confiao/api_views.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | |||||||
|  | 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 .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 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)}) | ||||||
							
								
								
									
										4
									
								
								tienda_ilusion/don_confiao/example_customer.csv
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								tienda_ilusion/don_confiao/example_customer.csv
									
									
									
									
									
										Normal 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
									
								
								tienda_ilusion/don_confiao/export_csv.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tienda_ilusion/don_confiao/export_csv.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
| @@ -3,7 +3,7 @@ from django.forms.models import inlineformset_factory | |||||||
|  |  | ||||||
| from django.forms.widgets import DateInput, DateTimeInput | 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'}) | readonly_number_widget = forms.NumberInput(attrs={'readonly': 'readonly'}) | ||||||
|  |  | ||||||
| @@ -12,6 +12,10 @@ class ImportProductsForm(forms.Form): | |||||||
|     csv_file = forms.FileField() |     csv_file = forms.FileField() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ImportCustomersForm(forms.Form): | ||||||
|  |     csv_file = forms.FileField() | ||||||
|  |  | ||||||
|  |  | ||||||
| class PurchaseForm(forms.ModelForm): | class PurchaseForm(forms.ModelForm): | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = Sale |         model = Sale | ||||||
| @@ -50,29 +54,13 @@ class PurchaseSummaryForm(forms.Form): | |||||||
|         widget=readonly_number_widget |         widget=readonly_number_widget | ||||||
|     ) |     ) | ||||||
|     payment_method = forms.ChoiceField( |     payment_method = forms.ChoiceField( | ||||||
|         choices=PaymentMethods.choices, |         choices=[(PaymentMethods.CASH, PaymentMethods.CASH)], | ||||||
|         widget=forms.Select(attrs={'disabled': 'disabled'}) |  | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| LineaFormSet = inlineformset_factory( | SaleLineFormSet = inlineformset_factory( | ||||||
|     Sale, |     Sale, | ||||||
|     SaleLine, |     SaleLine, | ||||||
|     extra=1, |     extra=1, | ||||||
|     fields='__all__' |     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'}) |  | ||||||
|         } |  | ||||||
|   | |||||||
							
								
								
									
										22
									
								
								tienda_ilusion/don_confiao/frontend/don-confiao/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								tienda_ilusion/don_confiao/frontend/don-confiao/.gitignore
									
									
									
									
										vendored
									
									
										Normal 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? | ||||||
							
								
								
									
										79
									
								
								tienda_ilusion/don_confiao/frontend/don-confiao/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								tienda_ilusion/don_confiao/frontend/don-confiao/README.md
									
									
									
									
									
										Normal 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 | ||||||
							
								
								
									
										16
									
								
								tienda_ilusion/don_confiao/frontend/don-confiao/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								tienda_ilusion/don_confiao/frontend/don-confiao/index.html
									
									
									
									
									
										Normal 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> | ||||||
| @@ -0,0 +1,20 @@ | |||||||
|  | { | ||||||
|  |   "compilerOptions": { | ||||||
|  |     "allowJs": true, | ||||||
|  |     "target": "es5", | ||||||
|  |     "module": "esnext", | ||||||
|  |     "baseUrl": "./", | ||||||
|  |     "moduleResolution": "bundler", | ||||||
|  |     "paths": { | ||||||
|  |       "@/*": [ | ||||||
|  |         "src/*" | ||||||
|  |       ] | ||||||
|  |     }, | ||||||
|  |     "lib": [ | ||||||
|  |       "esnext", | ||||||
|  |       "dom", | ||||||
|  |       "dom.iterable", | ||||||
|  |       "scripthost" | ||||||
|  |     ] | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										6018
									
								
								tienda_ilusion/don_confiao/frontend/don-confiao/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6018
									
								
								tienda_ilusion/don_confiao/frontend/don-confiao/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										41
									
								
								tienda_ilusion/don_confiao/frontend/don-confiao/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								tienda_ilusion/don_confiao/frontend/don-confiao/package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | { | ||||||
|  |   "name": "don-confiao", | ||||||
|  |   "type": "module", | ||||||
|  |   "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", | ||||||
|  |     "cors": "^2.8.5", | ||||||
|  |     "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" | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								tienda_ilusion/don_confiao/frontend/don-confiao/public/1.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								tienda_ilusion/don_confiao/frontend/don-confiao/public/1.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 15 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 55 KiB | 
							
								
								
									
										19
									
								
								tienda_ilusion/don_confiao/frontend/don-confiao/src/App.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								tienda_ilusion/don_confiao/frontend/don-confiao/src/App.vue
									
									
									
									
									
										Normal 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> | ||||||
| @@ -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 | 
| @@ -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 | 
| @@ -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;" | ||||||
|  |     > | ||||||
|  |       © 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> | ||||||
| @@ -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> | ||||||
| @@ -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> | ||||||
| @@ -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> | ||||||
| @@ -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> | ||||||
| @@ -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> | ||||||
| @@ -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> | ||||||
| @@ -0,0 +1,46 @@ | |||||||
|  | <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'} | ||||||
|  |          ], | ||||||
|  |      }), | ||||||
|  |      watch: { | ||||||
|  |          group () { | ||||||
|  |              this.drawer = false | ||||||
|  |          }, | ||||||
|  |      }, | ||||||
|  |      methods: { | ||||||
|  |          navigate(route) { | ||||||
|  |              this.$router.push(route); | ||||||
|  |          }, | ||||||
|  |      } | ||||||
|  |  } | ||||||
|  | </script> | ||||||
| @@ -0,0 +1,349 @@ | |||||||
|  | <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 => { | ||||||
|  |                console.log(data) | ||||||
|  |                const transformed_products = data.map(item => ({ | ||||||
|  |                    name: item.name, | ||||||
|  |                    price: item["template."]?.list_price?.decimal, | ||||||
|  |                    measuring_unit: item["default_uom"]?.name, | ||||||
|  |                    categories: [] | ||||||
|  |                })); | ||||||
|  |                this.products = transformed_products; | ||||||
|  |            }) | ||||||
|  |            .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> | ||||||
| @@ -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> | ||||||
|  | ``` | ||||||
| @@ -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 }}  <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> | ||||||
| @@ -0,0 +1,5 @@ | |||||||
|  | <template> | ||||||
|  |   <strong class="text-red-darken-4"> | ||||||
|  |     <slot></slot> | ||||||
|  |   </strong> | ||||||
|  | </template> | ||||||
| @@ -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: String | ||||||
|  |     }, | ||||||
|  |     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> | ||||||
| @@ -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> | ||||||
| @@ -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> | ||||||
| @@ -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. | ||||||
| @@ -0,0 +1,13 @@ | |||||||
|  | <template> | ||||||
|  |   <v-app> | ||||||
|  |     <v-main> | ||||||
|  |       <router-view /> | ||||||
|  |     </v-main> | ||||||
|  |  | ||||||
|  |     <AppFooter /> | ||||||
|  |   </v-app> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  |   // | ||||||
|  | </script> | ||||||
							
								
								
									
										34
									
								
								tienda_ilusion/don_confiao/frontend/don-confiao/src/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								tienda_ilusion/don_confiao/frontend/don-confiao/src/main.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | /** | ||||||
|  |  * 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' | ||||||
|  | import cors from 'cors'; | ||||||
|  |  | ||||||
|  | // const cors = require('cors'); | ||||||
|  |  | ||||||
|  | process.env.API_IMPLEMENTATION = 'tryton'; | ||||||
|  | // process.env.API_IMPLEMENTATION = 'django'; | ||||||
|  | let apiImplementation = new ApiImplementation(); | ||||||
|  | const api = apiImplementation.getApi(); | ||||||
|  |  | ||||||
|  | const app = createApp(App); | ||||||
|  | // app.use(cors({ | ||||||
|  | //   origin: '*', // Permitir todas las solicitudes de origen | ||||||
|  | //   exposedHeaders: ['X-Custom-Header', 'Content-Length'], // Exponer headers específicos | ||||||
|  | // })); | ||||||
|  | app.provide('api', api); | ||||||
|  |  | ||||||
|  | registerPlugins(app) | ||||||
|  |  | ||||||
|  | app.mount('#app') | ||||||
| @@ -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. | ||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | <template> | ||||||
|  |   <Purchase /> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | // | ||||||
|  | </script> | ||||||
| @@ -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> | ||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | <template> | ||||||
|  |   <Wellcome /> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | </script> | ||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | <template> | ||||||
|  |   <SummaryPurchase :id="$route.query.id"/> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  |   // | ||||||
|  | </script> | ||||||
| @@ -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. | ||||||
| @@ -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) | ||||||
|  | } | ||||||
| @@ -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', | ||||||
|  |   }, | ||||||
|  | }) | ||||||
| @@ -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 | ||||||
| @@ -0,0 +1,29 @@ | |||||||
|  | import DjangoApi from './django-api'; | ||||||
|  | import TrytonApiClient from './tryton-api'; | ||||||
|  | import Api from './api'; | ||||||
|  |  | ||||||
|  | class ApiImplementation { | ||||||
|  |   constructor() { | ||||||
|  |     const implementation = process.env.API_IMPLEMENTATION; | ||||||
|  |     let apiImplementation; | ||||||
|  |     if (implementation === 'django') { | ||||||
|  |       apiImplementation = new DjangoApi(); | ||||||
|  |     } else if (implementation === 'tryton'){ | ||||||
|  |       const url = 'http://192.168.0.114:18030'; | ||||||
|  |       const key = '9a9ffc430146447d81e6698240199a4be2b0e774cb18474999d0f60e33b5b1eb1cfff9d9141346a98844879b5a9e787489c891ddc8fb45cc903b7244cab64fb1'; | ||||||
|  |       const db = 'tryton'; | ||||||
|  |       const applicationName = 'sale_don_confiao'; | ||||||
|  |       apiImplementation = new TrytonApiClient( | ||||||
|  |         url, key, db, applicationName); | ||||||
|  |     } else { | ||||||
|  |       throw new Error("API implementation don't configured"); | ||||||
|  |     } | ||||||
|  |     this.api = new Api(apiImplementation); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getApi() { | ||||||
|  |     return this.api; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default ApiImplementation; | ||||||
| @@ -0,0 +1,44 @@ | |||||||
|  | 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(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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; | ||||||
| @@ -0,0 +1,87 @@ | |||||||
|  | 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); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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; | ||||||
| @@ -0,0 +1,98 @@ | |||||||
|  | class TrytonApiClient { | ||||||
|  |   constructor(url, key, db, applicationName) { | ||||||
|  |     this.baseUrl = `${url}/${db}/${applicationName}`; | ||||||
|  |     this.headers = { | ||||||
|  |       'Authorization': `Bearer ${key}`, | ||||||
|  |       'Content-Type': 'application/json', | ||||||
|  |       mode: 'cors' | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getCustomers() { | ||||||
|  |     const url = this.baseUrl + '/parties'; | ||||||
|  |     const customers = this.getRequest(url); | ||||||
|  |     return customers; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getProducts() { | ||||||
|  |     const url = this.baseUrl + '/products' | ||||||
|  |     const products = this.getRequest(url); | ||||||
|  |     return products; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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, { | ||||||
|  |         method: 'GET', | ||||||
|  |         headers: this.headers | ||||||
|  |       }).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: this.headers, | ||||||
|  |         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 TrytonApiClient; | ||||||
| @@ -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. | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | // Utilities | ||||||
|  | import { defineStore } from 'pinia' | ||||||
|  |  | ||||||
|  | export const useAppStore = defineStore('app', { | ||||||
|  |   state: () => ({ | ||||||
|  |     // | ||||||
|  |   }), | ||||||
|  | }) | ||||||
| @@ -0,0 +1,4 @@ | |||||||
|  | // Utilities | ||||||
|  | import { createPinia } from 'pinia' | ||||||
|  |  | ||||||
|  | export default createPinia() | ||||||
| @@ -0,0 +1,3 @@ | |||||||
|  | # Styles | ||||||
|  |  | ||||||
|  | This directory is for configuring the styles of the application. | ||||||
| @@ -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 | ||||||
|  | // ); | ||||||
| @@ -0,0 +1,86 @@ | |||||||
|  | // 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, | ||||||
|  |     cors: { | ||||||
|  |       origin: '*', | ||||||
|  |       methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], | ||||||
|  |       allowedHeaders: ['Content-Type', 'Authorization'], | ||||||
|  |     }, | ||||||
|  |     proxy: { | ||||||
|  |       '/sale_don_confiao': { | ||||||
|  |         target: "http://127.0.0.1:8000/tryton/sale_don_confiao", // Cambia esto a la URL de tu API | ||||||
|  |         changeOrigin: true, | ||||||
|  |         rewrite: (path) => path.replace(/^\/sale_don_confiao/, '/tryton/sale_don_confiao'), // Opcional: reescribe la ruta | ||||||
|  |         ws: true | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   build: { | ||||||
|  |     outDir: '../../static/frontend/',  | ||||||
|  |   }, | ||||||
|  |   base: '/frontend/', | ||||||
|  | }) | ||||||
| @@ -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), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -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), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
							
								
								
									
										22
									
								
								tienda_ilusion/don_confiao/migrations/0030_paymentsale.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								tienda_ilusion/don_confiao/migrations/0030_paymentsale.py
									
									
									
									
									
										Normal 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')), | ||||||
|  |             ], | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -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), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -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), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -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), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -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), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -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), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -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, | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -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 = [ | ||||||
|  |     ] | ||||||
							
								
								
									
										20
									
								
								tienda_ilusion/don_confiao/migrations/0037_admincode.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								tienda_ilusion/don_confiao/migrations/0037_admincode.py
									
									
									
									
									
										Normal 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)), | ||||||
|  |             ], | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -3,11 +3,23 @@ from django.utils.translation import gettext_lazy as _ | |||||||
| from django.core.exceptions import ValidationError | from django.core.exceptions import ValidationError | ||||||
|  |  | ||||||
| from decimal import Decimal | 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): | class Customer(models.Model): | ||||||
|     name = models.CharField(max_length=100) |     name = models.CharField(max_length=100, default=None, null=False, blank=False) | ||||||
|     address = models.CharField(max_length=100) |     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): | class MeasuringUnits(models.TextChoices): | ||||||
| @@ -34,21 +46,88 @@ class Product(models.Model): | |||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         return self.name |         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): | class Sale(models.Model): | ||||||
|     customer = models.ForeignKey(Customer, on_delete=models.PROTECT) |     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) |     phone = models.CharField(max_length=13, null=True, blank=True) | ||||||
|     description = models.CharField(max_length=255, 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): |     def __str__(self): | ||||||
|         return f"{self.date} {self.customer}" |         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): | class SaleLine(models.Model): | ||||||
|  |  | ||||||
|     sale = models.ForeignKey(Sale, on_delete=models.CASCADE) |     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) |     quantity = models.IntegerField(null=True) | ||||||
|     unit_price = models.DecimalField(max_digits=9, decimal_places=2) |     unit_price = models.DecimalField(max_digits=9, decimal_places=2) | ||||||
|     description = models.CharField(max_length=255, null=True, blank=True) |     description = models.CharField(max_length=255, null=True, blank=True) | ||||||
| @@ -57,12 +136,6 @@ class SaleLine(models.Model): | |||||||
|         return f"{self.sale} - {self.product}" |         return f"{self.sale} - {self.product}" | ||||||
|  |  | ||||||
|  |  | ||||||
| class PaymentMethods(models.TextChoices): |  | ||||||
|     CASH = 'CASH', _('Cash') |  | ||||||
|     CONFIAR = 'CONFIAR', _('Confiar') |  | ||||||
|     BANCOLOMBIA = 'BANCOLOMBIA', _('Bancolombia') |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class ReconciliationJarSummary(): | class ReconciliationJarSummary(): | ||||||
|     def __init__(self, payments): |     def __init__(self, payments): | ||||||
|         self._validate_payments(payments) |         self._validate_payments(payments) | ||||||
| @@ -80,38 +153,6 @@ class ReconciliationJarSummary(): | |||||||
|         return self._payments |         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): | class Payment(models.Model): | ||||||
|     date_time = models.DateTimeField() |     date_time = models.DateTimeField() | ||||||
|     type_payment = models.CharField( |     type_payment = models.CharField( | ||||||
| @@ -137,3 +178,27 @@ class Payment(models.Model): | |||||||
|                 reconciliation_jar=None |                 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) | ||||||
|   | |||||||
							
								
								
									
										95
									
								
								tienda_ilusion/don_confiao/serializers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								tienda_ilusion/don_confiao/serializers.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | |||||||
|  | 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): | ||||||
|  |     class Meta: | ||||||
|  |         model = Sale | ||||||
|  |         fields = ['id', 'customer', 'date', 'saleline_set'] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 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): | ||||||
|  |     class Meta: | ||||||
|  |         model = ReconciliationJar | ||||||
|  |         fields = [ | ||||||
|  |             'id', | ||||||
|  |             'date_time', | ||||||
|  |             'reconcilier', | ||||||
|  |             'cash_taken', | ||||||
|  |             'cash_discrepancy', | ||||||
|  |             'total_cash_purchases', | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  | 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'] | ||||||
							
								
								
									
										37
									
								
								tienda_ilusion/don_confiao/static/css/main_menu.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								tienda_ilusion/don_confiao/static/css/main_menu.css
									
									
									
									
									
										Normal 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 | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								tienda_ilusion/don_confiao/static/img/recreo_logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								tienda_ilusion/don_confiao/static/img/recreo_logo.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 179 KiB | 
| @@ -25,5 +25,6 @@ document.addEventListener('DOMContentLoaded', function(){ | |||||||
|  |  | ||||||
|     formContainer.appendChild(newForm); |     formContainer.appendChild(newForm); | ||||||
|     totalForms.value = formCount + 1; |     totalForms.value = formCount + 1; | ||||||
|  |     setPriceListeners(); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|   | |||||||
							
								
								
									
										22
									
								
								tienda_ilusion/don_confiao/static/js/buy_general.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								tienda_ilusion/don_confiao/static/js/buy_general.js
									
									
									
									
									
										Normal 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 = ''; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -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); | ||||||
|  |        } | ||||||
|  |      }) | ||||||
|  |    }); | ||||||
|  |  }); | ||||||
							
								
								
									
										17
									
								
								tienda_ilusion/don_confiao/templates/don_confiao/base.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								tienda_ilusion/don_confiao/templates/don_confiao/base.html
									
									
									
									
									
										Normal 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> | ||||||
| @@ -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 %} | ||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | {% extends 'don_confiao/base.html' %} | ||||||
|  | {% block content %} | ||||||
| {% if form.is_multipart %} | {% if form.is_multipart %} | ||||||
|     <form enctype="multipart/form-data" method="post"> |     <form enctype="multipart/form-data" method="post"> | ||||||
| {% else %} | {% else %} | ||||||
| @@ -8,3 +10,4 @@ | |||||||
| {{ form }} | {{ form }} | ||||||
| <input type="submit" value="Importar"> | <input type="submit" value="Importar"> | ||||||
| </form> | </form> | ||||||
|  | {% endblock %} | ||||||
|   | |||||||
| @@ -4,4 +4,5 @@ | |||||||
|     <li><a href='./comprar'>Comprar</a></li> |     <li><a href='./comprar'>Comprar</a></li> | ||||||
|     <li><a href='./productos'>Productos</a></li> |     <li><a href='./productos'>Productos</a></li> | ||||||
|     <li><a href='./importar_productos'>Importar Productos</a></li> |     <li><a href='./importar_productos'>Importar Productos</a></li> | ||||||
|  |     <li><a href='./importar_terceros'>Importar Terceros</a></li> | ||||||
| </ul> | </ul> | ||||||
|   | |||||||
							
								
								
									
										17
									
								
								tienda_ilusion/don_confiao/templates/don_confiao/menu.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								tienda_ilusion/don_confiao/templates/don_confiao/menu.html
									
									
									
									
									
										Normal 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> | ||||||
| @@ -1,9 +1,10 @@ | |||||||
|  | {% extends 'don_confiao/base.html' %} | ||||||
|  | {% block content %} | ||||||
| <form action="" method="get"> | <form action="" method="get"> | ||||||
|     <label>Filtro por nombre:</label> |     <label>Filtro por nombre:</label> | ||||||
|     <input type="text" name="name" value="{{ request.GET.name }}"> |     <input type="text" name="name" value="{{ request.GET.name }}"> | ||||||
|     <button type="submit">Filtrar</button> |     <button type="submit">Filtrar</button> | ||||||
| </form> | </form> | ||||||
| {% block content %} |  | ||||||
|   <h1>Lista de productos</h1> |   <h1>Lista de productos</h1> | ||||||
|   <ul> |   <ul> | ||||||
|     {% for obj in object_list %} |     {% for obj in object_list %} | ||||||
|   | |||||||
| @@ -1,21 +1,39 @@ | |||||||
| <!doctype html> | {% extends 'don_confiao/base.html' %} | ||||||
|  | {% block content %} | ||||||
| {% load static %} | {% load static %} | ||||||
| <form id="complete_form_purchase" method="POST"> | <script> | ||||||
|   {% csrf_token %} |   let listProducts = JSON.parse("{{ list_products|escapejs }}"); | ||||||
|   {{ sale_form }} | </script> | ||||||
|   {{ linea_formset.management_form }} | <div class="flex h-full"> | ||||||
|   <div id="formset-container"> |   <div class="h-full w-10/12 flex flex-col p-5"> | ||||||
|     {% for form in linea_formset %} |     <form id="complete_form_purchase" method="POST" class="h-10/12 w-full max-h-full overflow-auto"> | ||||||
|       <div class="form-container"> |       {% csrf_token %} | ||||||
|         <table style="border: solid 1px blue; margin: 10px"> |       {{ linea_formset.management_form }} | ||||||
|           {{ form.as_table }} |       <div id="formset-container" class="w-full"> | ||||||
|         </table> |         {% for form in linea_formset %} | ||||||
|  |         <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> |       </div> | ||||||
|     {% endfor %} |     <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> | ||||||
|   <button id="add_line" type="button" onclick="add_line">Añadir Linea</button> |   <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"> | ||||||
|   {{ summary_form }} |     <p id="sale_resume_title" class="text-center decoration-solid font-mono font-bold text-xl page_title">Resumen de Venta</p> | ||||||
|   <br/><button name="form" type="submit" >Comprar</button> |     {{ sale_form }} | ||||||
|   <script src="{% static 'js/add_line.js' %}"></script> |     {{ summary_form }} | ||||||
|   <script src="{% static 'js/sale_summary.js' %}"></script> |     <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> | ||||||
| </form> |   </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 %} | ||||||
|   | |||||||
| @@ -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 %} | ||||||
| @@ -1,9 +1,14 @@ | |||||||
|  | {% extends 'don_confiao/base.html' %} | ||||||
|  | {% block content %} | ||||||
|  |  | ||||||
| {% if purchases  %} | {% if purchases  %} | ||||||
| <ul> | <ul> | ||||||
|     {% for purchase in purchases %} |     {% 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 %} |     {% endfor %} | ||||||
| </ul> | </ul> | ||||||
| {% else %} | {% else %} | ||||||
| <p>No hay Compras</p> | <p>No hay Compras</p> | ||||||
| {% endif %} | {% endif %} | ||||||
|  |  | ||||||
|  | {% endblock %} | ||||||
|   | |||||||
| @@ -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 %} |  | ||||||
							
								
								
									
										47
									
								
								tienda_ilusion/don_confiao/tests/Fixtures/sales_fixture.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								tienda_ilusion/don_confiao/tests/Fixtures/sales_fixture.json
									
									
									
									
									
										Normal 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" | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | ] | ||||||
							
								
								
									
										41
									
								
								tienda_ilusion/don_confiao/tests/test_admin_code.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								tienda_ilusion/don_confiao/tests/test_admin_code.py
									
									
									
									
									
										Normal 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) | ||||||
							
								
								
									
										59
									
								
								tienda_ilusion/don_confiao/tests/test_api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								tienda_ilusion/don_confiao/tests/test_api.py
									
									
									
									
									
										Normal 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') | ||||||
| @@ -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] |  | ||||||
							
								
								
									
										22
									
								
								tienda_ilusion/don_confiao/tests/test_buy_form.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								tienda_ilusion/don_confiao/tests/test_buy_form.py
									
									
									
									
									
										Normal 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) | ||||||
							
								
								
									
										50
									
								
								tienda_ilusion/don_confiao/tests/test_export_sales.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								tienda_ilusion/don_confiao/tests/test_export_sales.py
									
									
									
									
									
										Normal 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" | ||||||
|  |                 ] | ||||||
							
								
								
									
										208
									
								
								tienda_ilusion/don_confiao/tests/test_jar_reconciliation.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								tienda_ilusion/don_confiao/tests/test_jar_reconciliation.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,208 @@ | |||||||
|  | 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): | ||||||
|  |         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, | ||||||
|  |             ], | ||||||
|  |         } | ||||||
|  |         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), 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 _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 | ||||||
| @@ -1,5 +1,7 @@ | |||||||
| #!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
|  | from django.db.utils import IntegrityError | ||||||
|  |  | ||||||
| from ..models import Customer | from ..models import Customer | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -12,3 +14,8 @@ class TestCustomer(TestCase): | |||||||
|         customer.save() |         customer.save() | ||||||
|  |  | ||||||
|         self.assertIsInstance(customer, Customer) |         self.assertIsInstance(customer, Customer) | ||||||
|  |  | ||||||
|  |     def test_don_create_customer_without_name(self): | ||||||
|  |         customer = Customer() | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             customer.save() | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								tienda_ilusion/don_confiao/tests/test_payment_methods.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								tienda_ilusion/don_confiao/tests/test_payment_methods.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | from django.test import Client, TestCase | ||||||
|  |  | ||||||
|  | # from ..models import PaymentMethods | ||||||
|  |  | ||||||
|  | class TestPaymentMethods(TestCase): | ||||||
|  |     def setUp(self): | ||||||
|  |         self.client = Client() | ||||||
|  |  | ||||||
|  |     def test_keys_in_payment_methods_to_select(self): | ||||||
|  |         response = self.client.get( | ||||||
|  |             '/don_confiao/payment_methods/all/select_format' | ||||||
|  |         ) | ||||||
|  |         methods = response.json() | ||||||
|  |         for method in methods: | ||||||
|  |             self.assertEqual(set(method.keys()), {'text', 'value'}) | ||||||
|  |  | ||||||
|  |     def test_basic_payment_methods_to_select(self): | ||||||
|  |         methods = self.client.get( | ||||||
|  |             '/don_confiao/payment_methods/all/select_format' | ||||||
|  |         ).json() | ||||||
|  |         self.assertIn('CASH', [method.get('value') for method in methods]) | ||||||
|  |         self.assertIn('CONFIAR', [method.get('value') for method in methods]) | ||||||
|  |         self.assertIn('BANCOLOMBIA', [method.get('value') for method in methods]) | ||||||
| @@ -1,37 +1,51 @@ | |||||||
| from django.test import Client, TestCase | from django.test import Client, TestCase | ||||||
|  |  | ||||||
| from ..models import Payment, Sale | from ..models import Payment, Sale, Product, Customer | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestPurchaseWithPayment(TestCase): | class TestPurchaseWithPayment(TestCase): | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         self.client = Client() |         self.client = Client() | ||||||
|  |         self.product = Product() | ||||||
|  |         self.product.name = "Arroz" | ||||||
|  |         self.product.price = 5000 | ||||||
|  |         self.product.save() | ||||||
|  |         customer = Customer() | ||||||
|  |         customer.name = "Noelba Lopez" | ||||||
|  |         customer.save() | ||||||
|  |         self.customer = customer | ||||||
|  |  | ||||||
|     # def test_generate_payment_when_it_has_payment(self): |     def test_generate_payment_when_it_has_payment(self): | ||||||
|     #     quantity = 2 |         quantity = 2 | ||||||
|     #     unit_price = 2500 |         unit_price = 2500 | ||||||
|     #     total = 5000 |         total = 5000 | ||||||
|     #     response = self.client.post( |         self.client.post( | ||||||
|     #         '/don_confiao/comprar', |             '/don_confiao/comprar', | ||||||
|     #         { |             { | ||||||
|     #             "customer": "Noelba Lopez", |                 "customer": str(self.customer.id), | ||||||
|     #             "date": "2024-07-27", |                 "date": "2024-07-27", | ||||||
|     #             "phone": "3010101000", |                 "phone": "3010101000", | ||||||
|     #             "description": "Venta de contado", |                 "description": "Venta de contado", | ||||||
|     #             "saleline_set-TOTAL_FORMS": "1", |                 "saleline_set-TOTAL_FORMS": "1", | ||||||
|     #             "saleline_set-INITIAL_FORMS": "0", |                 "saleline_set-INITIAL_FORMS": "0", | ||||||
|     #             "saleline_set-MIN_NUM_FORMS": "0", |                 "saleline_set-MIN_NUM_FORMS": "0", | ||||||
|     #             "saleline_set-MAX_NUM_FORMS": "1000", |                 "saleline_set-MAX_NUM_FORMS": "1000", | ||||||
|     #             "saleline_set-0-product": "Papayita", |                 "saleline_set-0-product": str(self.product.id), | ||||||
|     #             "saleline_set-0-quantity": str(quantity), |                 "saleline_set-0-quantity": str(quantity), | ||||||
|     #             "saleline_set-0-unit_price": str(unit_price), |                 "saleline_set-0-unit_price": str(unit_price), | ||||||
|     #             "saleline_set-0-description": "Linea de Venta", |                 "saleline_set-0-description": "Linea de Venta", | ||||||
|     #             "saleline_set-0-sale": "", |                 "saleline_set-0-sale": "", | ||||||
|     #             "saleline_set-0-id": "", |                 "saleline_set-0-id": "", | ||||||
|     #         } |                 "quantity_lines": "1", | ||||||
|     #     ) |                 "quantity_products": str(quantity), | ||||||
|     #     purchases = Sale.objects.all() |                 "ammount": str(quantity * unit_price), | ||||||
|     #     self.assertEqual(1, len(purchases)) |                 "payment_method": "CASH", | ||||||
|     #     payments = Payment.objects.all() |             } | ||||||
|     #     self.assertEqual(1, len(payments)) |         ) | ||||||
|     #     self.assertEqual(total, payments[0].ammount) |  | ||||||
|  |         purchases = Sale.objects.all() | ||||||
|  |         self.assertEqual(1, len(purchases)) | ||||||
|  |         payments = Payment.objects.all() | ||||||
|  |         self.assertEqual(1, len(payments)) | ||||||
|  |         self.assertEqual(total, payments[0].amount) | ||||||
|  |         self.assertEqual('CASH', payments[0].type_payment) | ||||||
|   | |||||||
| @@ -1,45 +0,0 @@ | |||||||
| from django.test import Client, TestCase |  | ||||||
| from django.contrib.auth.models import AnonymousUser, User |  | ||||||
|  |  | ||||||
| from ..models import Payment |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestReconciliationJarClient(TestCase): |  | ||||||
|     def setUp(self): |  | ||||||
|         self.client = Client() |  | ||||||
|  |  | ||||||
|     def test_get_summary_info_on_view(self): |  | ||||||
|         self._generate_two_cash_payments() |  | ||||||
|         response = self.client.get("/don_confiao/cuadrar_tarro") |  | ||||||
|         self.assertEqual(response.status_code, 200) |  | ||||||
|         self.assertEqual(response.context["summary"].total, 160000) |  | ||||||
|         self.assertIn('160000', response.content.decode('utf-8')) |  | ||||||
|  |  | ||||||
|     def test_create_reconciliation_jar(self): |  | ||||||
|         self._generate_two_cash_payments() |  | ||||||
|         response = self.client.post( |  | ||||||
|             "/don_confiao/cuadrar_tarro", |  | ||||||
|             { |  | ||||||
|                 "date_time": "2024-07-20T00:00", |  | ||||||
|                 "description": "Cuadre de prueba", |  | ||||||
|                 "reconcilier": "Jorge", |  | ||||||
|                 "cash_taken": "100000", |  | ||||||
|                 "cash_discrepancy": "60000", |  | ||||||
|             } |  | ||||||
|         ) |  | ||||||
|         self.assertRedirects(response, '/don_confiao/cuadres') |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     def _generate_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 = 130000 |  | ||||||
|         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 = 30000 |  | ||||||
|         cash_payment2.save() |  | ||||||
| @@ -0,0 +1,53 @@ | |||||||
|  | from django.test import TestCase, Client | ||||||
|  | from ..models import Sale, Product, SaleLine, Customer | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestSummaryViewPurchase(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.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 | ||||||
|  |  | ||||||
|  |     def test_summary_has_customer(self): | ||||||
|  |         url = "/don_confiao/resumen_compra/" + str(self.purchase.id) | ||||||
|  |         response = self.client.get(url) | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |         self.assertEqual( | ||||||
|  |             response.context["purchase"].customer, | ||||||
|  |             self.purchase.customer | ||||||
|  |         ) | ||||||
|  |         self.assertIn('Alejo Mono', response.content.decode('utf-8')) | ||||||
|  |  | ||||||
|  |     def test_json_summary(self): | ||||||
|  |         url = f"/don_confiao/resumen_compra_json/{self.purchase.id}" | ||||||
|  |         response = self.client.get(url) | ||||||
|  |         content = response.content.decode('utf-8') | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |         self.assertIn('Alejo Mono', content) | ||||||
|  |         self.assertIn('cafe', content) | ||||||
|  |         self.assertIn('72500', content) | ||||||
|  |         self.assertIn('quantity', content) | ||||||
|  |         self.assertIn('11', content) | ||||||
|  |         self.assertIn('date', content) | ||||||
|  |         self.assertIn(self.purchase.date, content) | ||||||
|  |         self.assertIn('lines', content) | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user