Compare commits
	
		
			104 Commits
		
	
	
		
			8a2a568739
			...
			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 | ||
| 49ac668c14 | 
							
								
								
									
										34
									
								
								Rakefile
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								Rakefile
									
									
									
									
									
								
							| @@ -10,7 +10,7 @@ namespace :live do | |||||||
|     compose('up', '--build', '-d', compose: DOCKER_COMPOSE) |     compose('up', '--build', '-d', compose: DOCKER_COMPOSE) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|    desc 'monitorear salida' |   desc 'monitorear salida' | ||||||
|   task :tail do |   task :tail do | ||||||
|     compose('logs', '-f', 'django', compose: DOCKER_COMPOSE) |     compose('logs', '-f', 'django', compose: DOCKER_COMPOSE) | ||||||
|   end |   end | ||||||
| @@ -20,7 +20,12 @@ namespace :live do | |||||||
|     compose('logs', '-f', '-n 50', 'django', compose: DOCKER_COMPOSE) |     compose('logs', '-f', '-n 50', 'django', compose: DOCKER_COMPOSE) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   desc 'detener entorno' |   desc 'iniciar entorno' | ||||||
|  |   task :start do | ||||||
|  |     compose('start', compose: DOCKER_COMPOSE) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   desc 'bajar entorno' | ||||||
|   task :down do |   task :down do | ||||||
|     compose('down', compose: DOCKER_COMPOSE) |     compose('down', compose: DOCKER_COMPOSE) | ||||||
|   end |   end | ||||||
| @@ -52,6 +57,31 @@ namespace :live do | |||||||
|  |  | ||||||
| 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) | def compose(*arg, compose: DOCKER_COMPOSE) | ||||||
|   sh "docker compose -f #{compose} #{arg.join(' ')}" |   sh "docker compose -f #{compose} #{arg.join(' ')}" | ||||||
| end | end | ||||||
|   | |||||||
| @@ -1,9 +1,13 @@ | |||||||
| from rest_framework import viewsets | from rest_framework import viewsets | ||||||
| from rest_framework.response import Response | 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 | from .models import Sale, SaleLine, Customer, Product, ReconciliationJar, PaymentMethods, AdminCode | ||||||
| from .serializers import SaleSerializer, ProductSerializer, CustomerSerializer | from .serializers import SaleSerializer, ProductSerializer, CustomerSerializer, ReconciliationJarSerializer, PaymentMethodSerializer, SaleForRenconciliationSerializer, SaleSummarySerializer | ||||||
|  |  | ||||||
|  | from decimal import Decimal | ||||||
|  | import json | ||||||
|  |  | ||||||
| class SaleView(viewsets.ModelViewSet): | class SaleView(viewsets.ModelViewSet): | ||||||
|     queryset = Sale.objects.all() |     queryset = Sale.objects.all() | ||||||
| @@ -14,7 +18,12 @@ class SaleView(viewsets.ModelViewSet): | |||||||
|         customer = Customer.objects.get(pk=data['customer']) |         customer = Customer.objects.get(pk=data['customer']) | ||||||
|         date = data['date'] |         date = data['date'] | ||||||
|         lines = data['saleline_set'] |         lines = data['saleline_set'] | ||||||
|         sale = Sale.objects.create(customer=customer, date=date) |         payment_method = data['payment_method'] | ||||||
|  |         sale = Sale.objects.create( | ||||||
|  |             customer=customer, | ||||||
|  |             date=date, | ||||||
|  |             payment_method=payment_method | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         for line in lines: |         for line in lines: | ||||||
|             product = Product.objects.get(pk=line['product']) |             product = Product.objects.get(pk=line['product']) | ||||||
| @@ -27,7 +36,10 @@ class SaleView(viewsets.ModelViewSet): | |||||||
|                 unit_price=unit_price |                 unit_price=unit_price | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         return Response({'message': 'Venta creada con exito'}, status=201) |         return Response( | ||||||
|  |             {'id': sale.id, 'message': 'Venta creada con exito'}, | ||||||
|  |             status=201 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class ProductView(viewsets.ModelViewSet): | class ProductView(viewsets.ModelViewSet): | ||||||
| @@ -38,3 +50,84 @@ class ProductView(viewsets.ModelViewSet): | |||||||
| class CustomerView(viewsets.ModelViewSet): | class CustomerView(viewsets.ModelViewSet): | ||||||
|     queryset = Customer.objects.all() |     queryset = Customer.objects.all() | ||||||
|     serializer_class = CustomerSerializer |     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 | ||||||
| 
 | 
| @@ -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 | ||||||
| @@ -60,18 +64,3 @@ SaleLineFormSet = inlineformset_factory( | |||||||
|     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'}) |  | ||||||
|         } |  | ||||||
|   | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,5 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "don-confiao", |   "name": "don-confiao", | ||||||
|  |   "type": "module", | ||||||
|   "version": "0.0.0", |   "version": "0.0.0", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "dev": "vite --host 0.0.0.0", |     "dev": "vite --host 0.0.0.0", | ||||||
| @@ -10,6 +11,7 @@ | |||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@mdi/font": "7.4.47", |     "@mdi/font": "7.4.47", | ||||||
|     "core-js": "^3.37.1", |     "core-js": "^3.37.1", | ||||||
|  |     "cors": "^2.8.5", | ||||||
|     "roboto-fontface": "*", |     "roboto-fontface": "*", | ||||||
|     "vee-validate": "^4.14.6", |     "vee-validate": "^4.14.6", | ||||||
|     "vue": "^3.4.31", |     "vue": "^3.4.31", | ||||||
|   | |||||||
| @@ -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> | ||||||
| @@ -45,9 +45,11 @@ | |||||||
|      data() { |      data() { | ||||||
|          return { |          return { | ||||||
|              showModal: false, |              showModal: false, | ||||||
|  |              api: inject('api'), | ||||||
|              valid: false, |              valid: false, | ||||||
|              customer: { |              customer: { | ||||||
|                  name: '', |                  name: '', | ||||||
|  |                  address: '', | ||||||
|                  email: '', |                  email: '', | ||||||
|                  phone: '' |                  phone: '' | ||||||
|              }, |              }, | ||||||
| @@ -69,36 +71,26 @@ | |||||||
|              this.resetForm(); |              this.resetForm(); | ||||||
|          }, |          }, | ||||||
|          async submitForm() { |          async submitForm() { | ||||||
|  |              console.log(this.customer) | ||||||
|              if (this.$refs.form.validate()) { |              if (this.$refs.form.validate()) { | ||||||
|                  try { |                  this.api.createCustomer(this.customer) | ||||||
|                      console.log(this.customer) |                      .then(data => { | ||||||
|                      const response = await fetch('/don_confiao/api/customers/', { |                        console.log('Cliente Guardado:', data); | ||||||
|                          method: 'POST', |                        this.$emit('customerCreated', data); | ||||||
|                          headers: { |                        this.closeModal(); | ||||||
|                              'Content-Type': 'application/json', |                      }) | ||||||
|                          }, |                      .catch(error => console.error('Error:', error)); | ||||||
|                          body: JSON.stringify(this.customer), |  | ||||||
|                      }); |  | ||||||
|                      if (response.ok) { |  | ||||||
|                          const data = await response.json(); |  | ||||||
|                          console.log('Cliente Guardado:', data); |  | ||||||
|                          this.closeModal(); |  | ||||||
|                      } else { |  | ||||||
|                          console.error('Error al Crear el Cliente:', response.statusText); |  | ||||||
|                      } |  | ||||||
|                  } catch (error) { |  | ||||||
|                      console.error('Error de red:', error); |  | ||||||
|                  } |  | ||||||
|              } |              } | ||||||
|          }, |          }, | ||||||
|          resetForm() { |          resetForm() { | ||||||
|              this.customer = { |              this.customer = { | ||||||
|                  name: '', |                  name: '', | ||||||
|  |                  address: '', | ||||||
|                  email: '', |                  email: '', | ||||||
|                  phone: '' |                  phone: '' | ||||||
|              }; |              }; | ||||||
|              this.$refs.form.reset(); |              this.$refs.form.reset(); | ||||||
|          } |          }, | ||||||
|      } |      } | ||||||
|  }; |  }; | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -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> | ||||||
| @@ -29,7 +29,7 @@ | |||||||
|          menuItems: [ |          menuItems: [ | ||||||
|              { title: 'Inicio', route: '/'}, |              { title: 'Inicio', route: '/'}, | ||||||
|              { title: 'Comprar', route:'/comprar'}, |              { title: 'Comprar', route:'/comprar'}, | ||||||
|              { title: 'Resumen de Compra', route:'/resumen_compra'}, |              { title: 'Cuadrar tarro', route: '/cuadrar_tarro'} | ||||||
|          ], |          ], | ||||||
|      }), |      }), | ||||||
|      watch: { |      watch: { | ||||||
|   | |||||||
| @@ -1,94 +1,106 @@ | |||||||
| <template> | <template> | ||||||
|   <v-container> | <v-container> | ||||||
|       <v-form ref="form" v-model="valid"> |   <v-form ref="purchase" v-model="valid" @change="onFormChange"> | ||||||
|         <v-row> |     <v-row> | ||||||
|           <v-col> |         <v-col> | ||||||
|             <v-autocomplete |             <v-autocomplete | ||||||
|               v-model="purchase.customer" |                 v-model="purchase.customer" | ||||||
|               :items="filteredClients" |                 :items="filteredClients" | ||||||
|               :search="client_search" |                 :search="client_search" | ||||||
|               no-data-text="No se hallaron clientes" |                 no-data-text="No se hallaron clientes" | ||||||
|               item-title="name" |  | ||||||
|               item-value="id" |  | ||||||
|               label="Cliente" |  | ||||||
|               :rules="[rules.required]" |  | ||||||
|               required |  | ||||||
|               class="mr-4" |  | ||||||
|             ></v-autocomplete> |  | ||||||
|             <v-btn color="primary" @click="openModal">Agregar Cliente</v-btn> |  | ||||||
|             <CreateCustomerModal ref="customerModal" /> |  | ||||||
|           </v-col> |  | ||||||
|           <v-col |  | ||||||
|             lg="2" |  | ||||||
|           > |  | ||||||
|           <v-text-field |  | ||||||
|             v-model="purchase.date" |  | ||||||
|             label="Fecha" |  | ||||||
|             type="date" |  | ||||||
|             :rules="[rules.required]" |  | ||||||
|             required |  | ||||||
|             ></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> |  | ||||||
|               <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-title="name" | ||||||
|                 item-value="id" |                 item-value="id" | ||||||
|                 item-subtitle="Price" |                 @update:model-value="onFormChange" | ||||||
|                 label="Producto" |                 label="Cliente" | ||||||
|                 :rules="[rules.required]" |                 :rules="[rules.required]" | ||||||
|                 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 }"> |               <template v-slot:item="{ props, item }"> | ||||||
|                   <v-list-item v-bind="props" :title="item.raw.name" :subtitle="formatPrice(item.raw.price)"></v-list-item> |                 <v-list-item v-bind="props" :title="item.raw.name" :subtitle="formatPrice(item.raw.price)"></v-list-item> | ||||||
|                 </template> |               </template> | ||||||
|               </v-autocomplete> |             </v-autocomplete> | ||||||
|             </v-col> |           </v-col> | ||||||
|             <v-col> |           <v-col | ||||||
|  |               lg="2" | ||||||
|  |           > | ||||||
|               <v-text-field |               <v-text-field | ||||||
|                 v-model.number="line.unit_price" |                   v-model.number="line.quantity" | ||||||
|                 label="Precio" |                   label="Cantidad" | ||||||
|                 type="number" |                   type="number" | ||||||
|                 :rules="[rules.required]" |                   :rules="[rules.required,rules.positive]" | ||||||
|                 prefix="$" |                   required | ||||||
|                 required |  | ||||||
|                 readonly |  | ||||||
|               ></v-text-field> |               ></v-text-field> | ||||||
|             </v-col> |           </v-col> | ||||||
|             <v-col> |         </v-row> | ||||||
|               <v-text-field |         <v-row> | ||||||
|                 v-model.number="line.quantity" |           <v-col> | ||||||
|                 label="Cantidad" |             <v-text-field | ||||||
|                 type="number" |               v-model.number="line.unit_price" | ||||||
|                 :rules="[rules.required]" |               label="Precio" | ||||||
|                 required |               type="number" | ||||||
|  |               :rules="[rules.required]" | ||||||
|  |               prefix="$" | ||||||
|  |               required | ||||||
|  |               readonly | ||||||
|               ></v-text-field> |               ></v-text-field> | ||||||
|             </v-col> |           </v-col> | ||||||
|             <v-col> |           <v-col> | ||||||
|               <v-text-field |             <v-text-field | ||||||
|                 type="number" |               v-model="line.measuring_unit" | ||||||
|                 :value="calculateSubtotal(line)" |               label="UdM" | ||||||
|                 label="Subtotal" |               persistent-placeholder="true" | ||||||
|                 prefix="$" |               readonly | ||||||
|                 readonly |               ></v-text-field> | ||||||
|  |           </v-col> | ||||||
|  |           <v-col> | ||||||
|  |             <v-text-field | ||||||
|  |               type="number" | ||||||
|  |               :value="calculateSubtotal(line)" | ||||||
|  |               label="Subtotal" | ||||||
|  |               prefix="$" | ||||||
|  |               readonly | ||||||
|                 disable |                 disable | ||||||
|                 persistent-placeholder="true" |                 persistent-placeholder="true" | ||||||
|               ></v-text-field> |               ></v-text-field> | ||||||
| @@ -96,8 +108,11 @@ | |||||||
|             <v-col> |             <v-col> | ||||||
|               <v-btn @click="removeLine(index)" color="red">Eliminar</v-btn> |               <v-btn @click="removeLine(index)" color="red">Eliminar</v-btn> | ||||||
|             </v-col> |             </v-col> | ||||||
|           </v-row> |         </v-row> | ||||||
|         </v-container> |         <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-btn @click="addLine" color="blue">Agregar</v-btn> | ||||||
|         </v-container> |         </v-container> | ||||||
|         <v-divider></v-divider> |         <v-divider></v-divider> | ||||||
| @@ -108,53 +123,86 @@ | |||||||
|           readonly |           readonly | ||||||
|           persistent-placeholder="true" |           persistent-placeholder="true" | ||||||
|         ></v-text-field> |         ></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-btn @click="submit" color="green">Comprar</v-btn> | ||||||
|       </v-form> |         <v-alert type="error" :duration="2000" closable v-model="show_alert_purchase"> | ||||||
|     </v-container> |           Verifique los campos obligatorios. | ||||||
|  |         </v-alert> | ||||||
|  |   </v-form> | ||||||
|  | </v-container> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
|  import CustomerForm from './CreateCustomerModal.vue'; |   import CustomerForm from './CreateCustomerModal.vue'; | ||||||
|  |   import CasherModal from './CasherModal.vue'; | ||||||
|  |   import { inject } from 'vue'; | ||||||
|  |  | ||||||
|  export default { |   export default { | ||||||
|    name: 'DonConfiao', |     name: 'DonConfiao', | ||||||
|    components: { |     components: { | ||||||
|      CustomerForm |      CustomerForm, | ||||||
|  |      CasherModal, | ||||||
|    }, |    }, | ||||||
|    props: { |    props: { | ||||||
|      msg: String |      msg: String | ||||||
|    }, |    }, | ||||||
|    data() { |    data() { | ||||||
|      return { |      return { | ||||||
|  |        api: inject('api'), | ||||||
|        valid: false, |        valid: false, | ||||||
|  |        form_changed: false, | ||||||
|  |        show_alert_lines: false, | ||||||
|  |        show_alert_purchase: false, | ||||||
|        client_search: '', |        client_search: '', | ||||||
|        product_search: '', |        product_search: '', | ||||||
|  |        payment_methods: null, | ||||||
|        purchase: { |        purchase: { | ||||||
|          date: this.getCurrentDate(), |          date: this.getCurrentDate(), | ||||||
|          client: null, |          customer: null, | ||||||
|          notes: '', |          notes: '', | ||||||
|          saleline_set: [{product:'', unit_price: 0, quantity: 0}], |          payment_method: null, | ||||||
|  |          saleline_set: [{product:'', unit_price: 0, quantity: 0, unit: ''}], | ||||||
|        }, |        }, | ||||||
|        rules: { |        rules: { | ||||||
|          required: value => !!value || 'Requerido.', |            required: value => !!value || 'Requerido.', | ||||||
|  |            positive: value => value > 0 || 'La cantidad debe ser mayor que 0.', | ||||||
|        }, |        }, | ||||||
|        menuItems: [ |          menuItems: [ | ||||||
|          { title: 'Inicio', route: '/'}, |              { title: 'Inicio', route: '/'}, | ||||||
|          { title: 'Compras', route:'/compras'}, |              { title: 'Compras', route:'/compras'}, | ||||||
|        ], |          ], | ||||||
|        clients: [], |          clients: [], | ||||||
|        products: [], |          products: [], | ||||||
|      }; |      }; | ||||||
|    }, |    }, | ||||||
|    created() { |      created() { | ||||||
|      this.fetchClients(); |      this.fetchClients(); | ||||||
|      this.fetchProducts(); |      this.fetchProducts(); | ||||||
|  |      this.fetchPaymentMethods(); | ||||||
|    }, |    }, | ||||||
|    watch: { |    watch: { | ||||||
|      group () { |      group () { | ||||||
|        this.drawer = false |        this.drawer = false | ||||||
|      }, |      }, | ||||||
|    }, |    }, | ||||||
|  |    beforeMount() { | ||||||
|  |       window.addEventListener('beforeunload', this.confirmLeave); | ||||||
|  |    }, | ||||||
|  |    beforeDestroy() { | ||||||
|  |       window.removeEventListener('beforeunload', this.confirmLeave); | ||||||
|  |    }, | ||||||
|    computed: { |    computed: { | ||||||
|      calculateTotal() { |      calculateTotal() { | ||||||
|        return this.purchase.saleline_set.reduce((total, saleline) => { |        return this.purchase.saleline_set.reduce((total, saleline) => { | ||||||
| @@ -184,69 +232,105 @@ | |||||||
|      openModal() { |      openModal() { | ||||||
|       this.$refs.customerModal.openModal(); |       this.$refs.customerModal.openModal(); | ||||||
|      }, |      }, | ||||||
|      getCurrentDate() { |      onFormChange() { | ||||||
|        const today = new Date(); |        this.form_changed = true; | ||||||
|        const yyyy = today.getFullYear(); |  | ||||||
|        const mm = String(today.getMonth() + 1).padStart(2, '0'); |  | ||||||
|        const dd = String(today.getDate()).padStart(2, '0'); |  | ||||||
|        return `${yyyy}-${mm}-${dd}`; |  | ||||||
|      }, |      }, | ||||||
|  |      openCasherModal() { | ||||||
|      onProductChange(index) { |        this.$refs.casherModal.dialog = true | ||||||
|        const selectedProductId = this.purchase.saleline_set[index].product; |      }, | ||||||
|  |      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); |        const selectedProduct = this.products.find(p => p.id == selectedProductId); | ||||||
|        this.purchase.saleline_set[index].unit_price = selectedProduct.price; |          this.purchase.saleline_set[index].unit_price = selectedProduct.price; | ||||||
|  |          this.purchase.saleline_set[index].measuring_unit = selectedProduct.measuring_unit; | ||||||
|      }, |      }, | ||||||
|      fetchClients() { |      fetchClients() { | ||||||
|        fetch('/don_confiao/api/customers/') |        this.api.getCustomers() | ||||||
|          .then(response => response.json()) |            .then(data => { | ||||||
|          .then(data => { |              this.clients = data; | ||||||
|            this.clients = data; |            }) | ||||||
|          }) |            .catch(error => { | ||||||
|          .catch(error => { |              console.error(error); | ||||||
|            console.error(error); |            }); | ||||||
|          }); |  | ||||||
|      }, |      }, | ||||||
|  |        handleNewCustomer(newCustomer){ | ||||||
|  |            this.clients.push(newCustomer); | ||||||
|  |            this.purchase.customer = newCustomer.id; | ||||||
|  |        }, | ||||||
|      fetchProducts() { |      fetchProducts() { | ||||||
|        fetch('/don_confiao/api/products/') |        this.api.getProducts() | ||||||
|          .then(response => response.json()) |            .then(data => { | ||||||
|          .then(data => { |                console.log(data) | ||||||
|            console.log(data); |                const transformed_products = data.map(item => ({ | ||||||
|            this.products = data; |                    name: item.name, | ||||||
|          }) |                    price: item["template."]?.list_price?.decimal, | ||||||
|          .catch(error => { |                    measuring_unit: item["default_uom"]?.name, | ||||||
|            console.error(error); |                    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() { |      addLine() { | ||||||
|        this.purchase.saleline_set.push({ product: '', unit_price: 0, quantity:0 }); |        this.purchase.saleline_set.push({ product: '', unit_price: 0, quantity:0, measuring_unit: ''}); | ||||||
|      }, |      }, | ||||||
|      removeLine(index) { |      removeLine(index) { | ||||||
|        this.purchase.saleline_set.splice(index, 1); |          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) { |      calculateSubtotal(line) { | ||||||
|        return line.unit_price * line.quantity; |        return line.unit_price * line.quantity; | ||||||
|      }, |      }, | ||||||
|      async submit() { |      async submit() { | ||||||
|        if (this.$refs.form.validate()) { |        this.$refs.purchase.validate(); | ||||||
|          try { |          if (this.valid) { | ||||||
|            const response = await fetch('/don_confiao/api/sales/', { |            this.api.createPurchase(this.purchase) | ||||||
|              method: 'POST', |                .then(data => { | ||||||
|              headers: { |                  console.log('Compra enviada:', data); | ||||||
|                'Content-Type': 'application/json', |                  this.$router.push({ | ||||||
|              }, |                    path: "/summary_purchase", | ||||||
|              body: JSON.stringify(this.purchase), |                    query : {id: parseInt(data.id)} | ||||||
|            }); |                  }); | ||||||
|            if (response.ok) { |                }) | ||||||
|              const data = await response.json(); |            .catch(error => console.error('Error al enviarl la compra:', error)); | ||||||
|              console.log('Compra enviada:', data); |        } else { | ||||||
|              this.$router.push("SummaryPurchase"); |          this.show_alert_purchase = true; | ||||||
|            } else { |            setTimeout(() => { | ||||||
|              console.error('Error al enviar la compra:', response.statusText); |              this.show_alert_purchase = false; | ||||||
|            } |            }, 4000); | ||||||
|          } catch (error) { |  | ||||||
|            console.error('Error de red:', error); |  | ||||||
|          } |  | ||||||
|        } |        } | ||||||
|      }, |      }, | ||||||
|      navigate(route) { |      navigate(route) { | ||||||
| @@ -256,6 +340,9 @@ | |||||||
|        return new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'COP' }).format(price); |        return new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'COP' }).format(price); | ||||||
|      }, |      }, | ||||||
|    }, |    }, | ||||||
|  |      mounted() { | ||||||
|  |          this.fetchClients(); | ||||||
|  |      } | ||||||
|  }; |  }; | ||||||
| </script> | </script> | ||||||
| <style> | <style> | ||||||
|   | |||||||
| @@ -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> | ||||||
| @@ -1,17 +1,106 @@ | |||||||
| <template> | <template> | ||||||
|   <v-app> |  | ||||||
|     <v-navigation-drawer app> |  | ||||||
|     </v-navigation-drawer> |  | ||||||
|     <v-app-bar> |  | ||||||
|       Resumen de la compra |  | ||||||
|     </v-app-bar> |  | ||||||
|     <v-container> |  | ||||||
|       Pon aqui la información de la compra |  | ||||||
|     </v-container> |  | ||||||
|   </v-app> |  | ||||||
| </template> |  | ||||||
| <script> |  | ||||||
|  |  | ||||||
|  |   <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> | </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> | ||||||
| @@ -9,11 +9,25 @@ import { registerPlugins } from '@/plugins' | |||||||
|  |  | ||||||
| // Components | // Components | ||||||
| import App from './App.vue' | import App from './App.vue' | ||||||
|  | import ApiImplementation from './services/api-implementation'; | ||||||
|  |  | ||||||
| // Composables | // Composables | ||||||
| import { createApp } from 'vue' | import { createApp } from 'vue' | ||||||
|  | import cors from 'cors'; | ||||||
|  |  | ||||||
| const app = createApp(App) | // 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) | registerPlugins(app) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| <template> | <template> | ||||||
|   <SummaryPurchase /> |   <Purchase /> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup> | <script setup> | ||||||
|   // | // | ||||||
| </script> | </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> | ||||||
| @@ -1,7 +1,6 @@ | |||||||
| <template> | <template> | ||||||
|   <Purchase /> |   <Wellcome /> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup> | <script setup> | ||||||
| // |  | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | <template> | ||||||
|  |   <SummaryPurchase :id="$route.query.id"/> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  |   // | ||||||
|  | </script> | ||||||
| @@ -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; | ||||||
| @@ -11,6 +11,8 @@ import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify' | |||||||
| import { defineConfig } from 'vite' | import { defineConfig } from 'vite' | ||||||
| import { fileURLToPath, URL } from 'node:url' | import { fileURLToPath, URL } from 'node:url' | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| // https://vitejs.dev/config/ | // https://vitejs.dev/config/ | ||||||
| export default defineConfig({ | export default defineConfig({ | ||||||
|   plugins: [ |   plugins: [ | ||||||
| @@ -63,6 +65,19 @@ export default defineConfig({ | |||||||
|   }, |   }, | ||||||
|   server: { |   server: { | ||||||
|     port: 3000, |     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: { |   build: { | ||||||
|     outDir: '../../static/frontend/',  |     outDir: '../../static/frontend/',  | ||||||
|   | |||||||
| @@ -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)), | ||||||
|  |             ], | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -6,9 +6,17 @@ from decimal import Decimal | |||||||
| from datetime import datetime | 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, default=None, null=False, blank=False) |     name = models.CharField(max_length=100, default=None, null=False, blank=False) | ||||||
|     address = models.CharField(max_length=100, null=True, blank=True) |     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): |     def __str__(self): | ||||||
|         return self.name |         return self.name | ||||||
| @@ -54,11 +62,49 @@ class Product(models.Model): | |||||||
|         return products_list |         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}" | ||||||
| @@ -67,6 +113,10 @@ class Sale(models.Model): | |||||||
|         lines = self.saleline_set.all() |         lines = self.saleline_set.all() | ||||||
|         return sum([l.quantity * l.unit_price for l in lines]) |         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 |     @classmethod | ||||||
|     def sale_header_csv(cls): |     def sale_header_csv(cls): | ||||||
|         sale_header_csv = [field.name for field in cls._meta.fields] |         sale_header_csv = [field.name for field in cls._meta.fields] | ||||||
| @@ -86,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) | ||||||
| @@ -109,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( | ||||||
| @@ -186,3 +198,7 @@ class Payment(models.Model): | |||||||
| class PaymentSale(models.Model): | class PaymentSale(models.Model): | ||||||
|     payment = models.ForeignKey(Payment, on_delete=models.CASCADE) |     payment = models.ForeignKey(Payment, on_delete=models.CASCADE) | ||||||
|     sale = models.ForeignKey(Sale, 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) | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| from rest_framework import serializers | from rest_framework import serializers | ||||||
|  |  | ||||||
| from .models import Sale, SaleLine, Product, Customer | from .models import Sale, SaleLine, Product, Customer, ReconciliationJar | ||||||
|  |  | ||||||
|  |  | ||||||
| class SaleLineSerializer(serializers.ModelSerializer): | class SaleLineSerializer(serializers.ModelSerializer): | ||||||
| @@ -20,7 +20,76 @@ class ProductSerializer(serializers.ModelSerializer): | |||||||
|         model = Product |         model = Product | ||||||
|         fields = ['id', 'name', 'price', 'measuring_unit', 'categories'] |         fields = ['id', 'name', 'price', 'measuring_unit', 'categories'] | ||||||
|  |  | ||||||
|  |  | ||||||
| class CustomerSerializer(serializers.ModelSerializer): | class CustomerSerializer(serializers.ModelSerializer): | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = Customer |         model = Customer | ||||||
|         fields = ['id', 'name', 'address'] |         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'] | ||||||
|   | |||||||
| @@ -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 %} | ||||||
| @@ -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> | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ | |||||||
|             <li><a href='/don_confiao/compras'>Compras</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/lista_productos'>Productos</a></li> | ||||||
|             <li><a href='/don_confiao/importar_productos'>Importar Productos</a></li> |             <li><a href='/don_confiao/importar_productos'>Importar Productos</a></li> | ||||||
|             <li><a href='/don_confiao/cuadrar_tarro'>Cuadrar tarro</a></li> |             <li><a href='/don_confiao/importar_terceros'>Importar Terceros</a></li> | ||||||
|         </ul> |         </ul> | ||||||
|     </nav> |     </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> |     <p id="page_title" class="text-center decoration-solid  font-mono font-bold text-lg page_title">Don Confiao - Tienda la Ilusión</p> | ||||||
|   | |||||||
| @@ -1,34 +0,0 @@ | |||||||
| {% extends 'don_confiao/base.html' %} |  | ||||||
| {% block content %} |  | ||||||
|  |  | ||||||
| {% 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 %} |  | ||||||
|  |  | ||||||
| {% endblock %} |  | ||||||
							
								
								
									
										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) | ||||||
| @@ -17,22 +17,19 @@ class TestAPI(APITestCase): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def test_create_sale(self): |     def test_create_sale(self): | ||||||
|         url = '/don_confiao/api/sales/' |         response = self._create_sale() | ||||||
|         data = { |         content = json.loads(response.content.decode('utf-8')) | ||||||
|             'customer': self.customer.id, |  | ||||||
|             'date': '2024-09-02', |  | ||||||
|             'saleline_set': [ |  | ||||||
|                 {'product': self.product.id, 'quantity': 2, 'unit_price': 3000}, |  | ||||||
|                 {'product': self.product.id, 'quantity': 3, 'unit_price': 5000} |  | ||||||
|             ], |  | ||||||
|         } |  | ||||||
|         response = self.client.post(url, data, format='json') |  | ||||||
|         self.assertEqual(response.status_code, status.HTTP_201_CREATED) |         self.assertEqual(response.status_code, status.HTTP_201_CREATED) | ||||||
|         self.assertEqual(Sale.objects.count(), 1) |         self.assertEqual(Sale.objects.count(), 1) | ||||||
|  |         sale = Sale.objects.all()[0] | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             Sale.objects.all()[0].customer.name, |             sale.customer.name, | ||||||
|             self.customer.name |             self.customer.name | ||||||
|         ) |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             sale.id, | ||||||
|  |             content['id'] | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     def test_get_products(self): |     def test_get_products(self): | ||||||
|         url = '/don_confiao/api/products/' |         url = '/don_confiao/api/products/' | ||||||
| @@ -47,3 +44,16 @@ class TestAPI(APITestCase): | |||||||
|         json_response = json.loads(response.content.decode('utf-8')) |         json_response = json.loads(response.content.decode('utf-8')) | ||||||
|         self.assertEqual(response.status_code, 200) |         self.assertEqual(response.status_code, 200) | ||||||
|         self.assertEqual(self.customer.name, json_response[0]['name']) |         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] |  | ||||||
							
								
								
									
										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 | ||||||
							
								
								
									
										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,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() |  | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| from django.test import TestCase, Client | from django.test import TestCase, Client | ||||||
| from ..models import Sale, Product, SaleLine, Customer | from ..models import Sale, Product, SaleLine, Customer | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestSummaryViewPurchase(TestCase): | class TestSummaryViewPurchase(TestCase): | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         customer = Customer() |         customer = Customer() | ||||||
| @@ -22,13 +23,31 @@ class TestSummaryViewPurchase(TestCase): | |||||||
|         line = SaleLine() |         line = SaleLine() | ||||||
|         line.sale = purchase |         line.sale = purchase | ||||||
|         line.product = product |         line.product = product | ||||||
|         line.quantity = "2" |         line.quantity = "11" | ||||||
|         line.unit_price = "72500" |         line.unit_price = "72500" | ||||||
|         line.save() |         line.save() | ||||||
|         self.purchase = purchase |         self.purchase = purchase | ||||||
|  |  | ||||||
|     def test_summary_has_customer(self): |     def test_summary_has_customer(self): | ||||||
|         response = self.client.get("/don_confiao/resumen_compra/" + str(self.purchase.id)) |         url = "/don_confiao/resumen_compra/" + str(self.purchase.id) | ||||||
|  |         response = self.client.get(url) | ||||||
|         self.assertEqual(response.status_code, 200) |         self.assertEqual(response.status_code, 200) | ||||||
|         self.assertEqual(response.context["purchase"].customer, self.purchase.customer) |         self.assertEqual( | ||||||
|  |             response.context["purchase"].customer, | ||||||
|  |             self.purchase.customer | ||||||
|  |         ) | ||||||
|         self.assertIn('Alejo Mono', response.content.decode('utf-8')) |         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) | ||||||
|   | |||||||
| @@ -1,4 +1,6 @@ | |||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
|  | from django.core.exceptions import ValidationError | ||||||
|  |  | ||||||
| from ..models import Customer, Product, Sale, SaleLine | from ..models import Customer, Product, Sale, SaleLine | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -17,13 +19,24 @@ class ConfiaoTest(TestCase): | |||||||
|     def test_create_sale(self): |     def test_create_sale(self): | ||||||
|         sale = Sale() |         sale = Sale() | ||||||
|         sale.customer = self.customer |         sale.customer = self.customer | ||||||
|         sale.date = "2024-06-22" |         sale.date = "2024-06-22 12:05:00" | ||||||
|         sale.phone = '666666666' |         sale.phone = '666666666' | ||||||
|         sale.description = "Description" |         sale.description = "Description" | ||||||
|         sale.save() |         sale.save() | ||||||
|  |  | ||||||
|         self.assertIsInstance(sale, Sale) |         self.assertIsInstance(sale, Sale) | ||||||
|  |  | ||||||
|  |     def test_can_create_sale_without_payment_method(self): | ||||||
|  |         sale = Sale() | ||||||
|  |         sale.customer = self.customer | ||||||
|  |         sale.date = "2024-06-22 12:05:00" | ||||||
|  |         sale.phone = '666666666' | ||||||
|  |         sale.description = "Description" | ||||||
|  |         sale.payment_method = '' | ||||||
|  |  | ||||||
|  |         with self.assertRaises(ValidationError): | ||||||
|  |             sale.full_clean() | ||||||
|  |  | ||||||
|     def test_create_sale_line(self): |     def test_create_sale_line(self): | ||||||
|         sale = Sale() |         sale = Sale() | ||||||
|         sale.customer = self.customer |         sale.customer = self.customer | ||||||
|   | |||||||
| @@ -19,6 +19,7 @@ class PurchaseFormTest(TestCase): | |||||||
|             "csrfmiddlewaretoken": _csrf_token, |             "csrfmiddlewaretoken": _csrf_token, | ||||||
|             "customer": self.customer.id, |             "customer": self.customer.id, | ||||||
|             "date": "2024-08-03", |             "date": "2024-08-03", | ||||||
|  |             "payment_method": "CASH", | ||||||
|             "phone": "sfasfd", |             "phone": "sfasfd", | ||||||
|             "description": "dasdadad", |             "description": "dasdadad", | ||||||
|             "saleline_set-TOTAL_FORMS": "1", |             "saleline_set-TOTAL_FORMS": "1", | ||||||
|   | |||||||
| @@ -19,11 +19,15 @@ urlpatterns = [ | |||||||
|     path("productos", views.products, name="products"), |     path("productos", views.products, name="products"), | ||||||
|     path("lista_productos", views.ProductListView.as_view(), name='product_list'), |     path("lista_productos", views.ProductListView.as_view(), name='product_list'), | ||||||
|     path("importar_productos", views.import_products, name="import_products"), |     path("importar_productos", views.import_products, name="import_products"), | ||||||
|  |     path("importar_terceros", views.import_customers, name="import_customers"), | ||||||
|     path("exportar_ventas_para_tryton", |     path("exportar_ventas_para_tryton", | ||||||
|          views.exportar_ventas_para_tryton, |          views.exportar_ventas_para_tryton, | ||||||
|          name="exportar_ventas_para_tryton"), |          name="exportar_ventas_para_tryton"), | ||||||
|     path("cuadrar_tarro", views.reconciliate_jar, name="reconciliate_jar"), |  | ||||||
|     path("cuadres", views.reconciliate_jar, name="reconciliations"), |  | ||||||
|     path("resumen_compra/<int:id>", views.purchase_summary, name="purchase_summary"), |     path("resumen_compra/<int:id>", views.purchase_summary, name="purchase_summary"), | ||||||
|  |     path("resumen_compra_json/<int:id>", api_views.SaleSummary.as_view(), name="purchase_json_summary"), | ||||||
|  |     path("payment_methods/all/select_format", api_views.PaymentMethodView.as_view(), name="payment_methods_to_select"), | ||||||
|  |     path('purchases/for_reconciliation', api_views.SalesForReconciliationView.as_view(), name='sales_for_reconciliation'), | ||||||
|  |     path('reconciliate_jar', api_views.ReconciliateJarView.as_view()), | ||||||
|  |     path('api/admin_code/validate/<code>', api_views.AdminCodeValidateView.as_view()), | ||||||
|     path('api/', include(router.urls)), |     path('api/', include(router.urls)), | ||||||
| ] | ] | ||||||
|   | |||||||
| @@ -4,12 +4,12 @@ from django.views.generic import ListView | |||||||
| from django.db import transaction | from django.db import transaction | ||||||
|  |  | ||||||
| from .models import ( | from .models import ( | ||||||
|     Sale, SaleLine, Product, ProductCategory, Payment, PaymentMethods) |     Sale, SaleLine, Product, Customer, ProductCategory, Payment, PaymentMethods, ReconciliationJar) | ||||||
| from .forms import ( | from .forms import ( | ||||||
|     ImportProductsForm, |     ImportProductsForm, | ||||||
|  |     ImportCustomersForm, | ||||||
|     PurchaseForm, |     PurchaseForm, | ||||||
|     SaleLineFormSet, |     SaleLineFormSet, | ||||||
|     ReconciliationJarForm, |  | ||||||
|     PurchaseSummaryForm) |     PurchaseSummaryForm) | ||||||
|  |  | ||||||
| import csv | import csv | ||||||
| @@ -95,28 +95,25 @@ def import_products(request): | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def reconciliate_jar(request): | def import_customers(request): | ||||||
|     summary = Payment.get_reconciliation_jar_summary() |     if request.method == "POST": | ||||||
|     if request.method == 'POST': |         form = ImportCustomersForm(request.POST, request.FILES) | ||||||
|         form = ReconciliationJarForm(request.POST) |  | ||||||
|         if form.is_valid(): |         if form.is_valid(): | ||||||
|             reconciliation = form.save() |             handle_import_customers_file(request.FILES["csv_file"]) | ||||||
|             reconciliation.add_payments(summary.payments) |             return HttpResponseRedirect("productos") | ||||||
|             reconciliation.clean() |  | ||||||
|             reconciliation.save() |  | ||||||
|             return HttpResponseRedirect('cuadres') |  | ||||||
|     else: |     else: | ||||||
|         form = ReconciliationJarForm() |         form = ImportCustomersForm() | ||||||
|     return render( |     return render( | ||||||
|         request, |         request, | ||||||
|         "don_confiao/reconciliate_jar.html", |         "don_confiao/import_customers.html", | ||||||
|         {'summary': summary, 'form': form} |         {'form': form} | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def reconciliations(request): | def reconciliations(request): | ||||||
|     return HttpResponse('<h1>Reconciliaciones</h1>') |     return HttpResponse('<h1>Reconciliaciones</h1>') | ||||||
|  |  | ||||||
|  |  | ||||||
| def purchase_summary(request, id): | def purchase_summary(request, id): | ||||||
|     purchase = Sale.objects.get(pk=id) |     purchase = Sale.objects.get(pk=id) | ||||||
|     return render( |     return render( | ||||||
| @@ -154,6 +151,18 @@ def handle_import_products_file(csv_file): | |||||||
|         for category in categories: |         for category in categories: | ||||||
|             product.categories.add(category) |             product.categories.add(category) | ||||||
|  |  | ||||||
|  | def handle_import_customers_file(csv_file): | ||||||
|  |     data = io.StringIO(csv_file.read().decode('utf-8')) | ||||||
|  |     reader = csv.DictReader(data, quotechar='"') | ||||||
|  |     for row in reader: | ||||||
|  |         customer, created = Customer.objects.update_or_create( | ||||||
|  |             name=row['nombre'], | ||||||
|  |             defaults={ | ||||||
|  |                 'email': row['correo'], | ||||||
|  |                 'phone': row['telefono'] | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def exportar_ventas_para_tryton(request): | def exportar_ventas_para_tryton(request): | ||||||
|     tryton_sales_header = [ |     tryton_sales_header = [ | ||||||
|   | |||||||
| @@ -28,6 +28,10 @@ DEBUG = True | |||||||
| ALLOWED_HOSTS = [] | ALLOWED_HOSTS = [] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | CORS_ALLOWED_ORIGINS = [ | ||||||
|  |     "http://localhost:8000", | ||||||
|  | ] | ||||||
|  |  | ||||||
| # Application definition | # Application definition | ||||||
|  |  | ||||||
| INSTALLED_APPS = [ | INSTALLED_APPS = [ | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user