Merge pull request 'Generando cuadre del tarro en vuetify' (#83) from streamline_reconciliation_jar_process_#69 into main
Reviewed-on: #83
This commit is contained in:
		
							
								
								
									
										27
									
								
								Rakefile
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								Rakefile
									
									
									
									
									
								
							| @@ -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,26 @@ 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 '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 | ||||||
| from .serializers import SaleSerializer, ProductSerializer, CustomerSerializer | from .serializers import SaleSerializer, ProductSerializer, CustomerSerializer, ReconciliationJarSerializer | ||||||
|  |  | ||||||
|  | from decimal import Decimal | ||||||
|  | import json | ||||||
|  |  | ||||||
| class SaleView(viewsets.ModelViewSet): | class SaleView(viewsets.ModelViewSet): | ||||||
|     queryset = Sale.objects.all() |     queryset = Sale.objects.all() | ||||||
| @@ -46,3 +50,53 @@ 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() | ||||||
|   | |||||||
| @@ -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'}) | ||||||
|  |  | ||||||
| @@ -64,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'}) |  | ||||||
|         } |  | ||||||
|   | |||||||
| @@ -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,6 +29,7 @@ | |||||||
|          menuItems: [ |          menuItems: [ | ||||||
|              { title: 'Inicio', route: '/'}, |              { title: 'Inicio', route: '/'}, | ||||||
|              { title: 'Comprar', route:'/comprar'}, |              { title: 'Comprar', route:'/comprar'}, | ||||||
|  |              { title: 'Cuadrar tarro', route: '/cuadrar_tarro'} | ||||||
|          ], |          ], | ||||||
|      }), |      }), | ||||||
|      watch: { |      watch: { | ||||||
|   | |||||||
| @@ -0,0 +1,213 @@ | |||||||
|  | <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 CurrencyText from './CurrencyText.vue'; | ||||||
|  |   import SummaryPurchaseModal from './SummaryPurchaseModal.vue'; | ||||||
|  |  | ||||||
|  |   export default { | ||||||
|  |     name: 'ReconciliationJar', | ||||||
|  |     props: { | ||||||
|  |       msg: String, | ||||||
|  |     }, | ||||||
|  |     components: { | ||||||
|  |       SummaryPurchaseModal, | ||||||
|  |     }, | ||||||
|  |     data () { | ||||||
|  |       return { | ||||||
|  |         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() { | ||||||
|  |         const endpoint = '/don_confiao/purchases/for_reconciliation'; | ||||||
|  |         fetch(endpoint) | ||||||
|  |           .then(response => response.json()) | ||||||
|  |           .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) { | ||||||
|  |           try { | ||||||
|  |             const response = await fetch('/don_confiao/reconciliate_jar', { | ||||||
|  |               method: 'POST', | ||||||
|  |               headers: { | ||||||
|  |                 'Content-Type': 'application/json' | ||||||
|  |               }, | ||||||
|  |               body: JSON.stringify(this.reconciliation), | ||||||
|  |             }); | ||||||
|  |             if (response.ok) { | ||||||
|  |               const data = await response.json(); | ||||||
|  |               console.log('Cuadre enviado:', data); | ||||||
|  |               this.$router.push({path: "/"}); | ||||||
|  |             } else { | ||||||
|  |               console.error('Error al enviar el cuadre', response.statusText); | ||||||
|  |  | ||||||
|  |             } | ||||||
|  |           } catch (error) { | ||||||
|  |             console.error('Error de red:', error); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   } | ||||||
|  | </script> | ||||||
| @@ -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,7 @@ | |||||||
|  | <template> | ||||||
|  |   <ReconciliationJar /> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  |   // | ||||||
|  | </script> | ||||||
| @@ -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 = [ | ||||||
|  |     ] | ||||||
| @@ -62,6 +62,31 @@ 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.DateTimeField("Date") |     date = models.DateTimeField("Date") | ||||||
| @@ -74,6 +99,12 @@ class Sale(models.Model): | |||||||
|         blank=False, |         blank=False, | ||||||
|         null=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}" | ||||||
| @@ -122,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( | ||||||
|   | |||||||
| @@ -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,21 @@ 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', 'email', 'phone'] |         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', | ||||||
|  |         ] | ||||||
|   | |||||||
| @@ -10,7 +10,6 @@ | |||||||
|             <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/importar_terceros'>Importar Terceros</a></li> |             <li><a href='/don_confiao/importar_terceros'>Importar Terceros</a></li> | ||||||
|             <li><a href='/don_confiao/cuadrar_tarro'>Cuadrar tarro</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 %} |  | ||||||
| @@ -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 | ||||||
| @@ -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() |  | ||||||
| @@ -23,10 +23,10 @@ urlpatterns = [ | |||||||
|     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>", views.purchase_json_summary, name="purchase_json_summary"), |     path("resumen_compra_json/<int:id>", views.purchase_json_summary, name="purchase_json_summary"), | ||||||
|     path("payment_methods/all/select_format", views.payment_methods_to_select, name="payment_methods_to_select"), |     path("payment_methods/all/select_format", views.payment_methods_to_select, name="payment_methods_to_select"), | ||||||
|  |     path('purchases/for_reconciliation', views.sales_for_reconciliation, name='sales_for_reconciliation'), | ||||||
|  |     path('reconciliate_jar', api_views.ReconciliateJarView.as_view()), | ||||||
|     path('api/', include(router.urls)), |     path('api/', include(router.urls)), | ||||||
| ] | ] | ||||||
|   | |||||||
| @@ -4,13 +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, Customer, ProductCategory, Payment, PaymentMethods) |     Sale, SaleLine, Product, Customer, ProductCategory, Payment, PaymentMethods, ReconciliationJar) | ||||||
| from .forms import ( | from .forms import ( | ||||||
|     ImportProductsForm, |     ImportProductsForm, | ||||||
|     ImportCustomersForm, |     ImportCustomersForm, | ||||||
|     PurchaseForm, |     PurchaseForm, | ||||||
|     SaleLineFormSet, |     SaleLineFormSet, | ||||||
|     ReconciliationJarForm, |  | ||||||
|     PurchaseSummaryForm) |     PurchaseSummaryForm) | ||||||
|  |  | ||||||
| import csv | import csv | ||||||
| @@ -95,6 +94,7 @@ def import_products(request): | |||||||
|         {'form': form} |         {'form': form} | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def import_customers(request): | def import_customers(request): | ||||||
|     if request.method == "POST": |     if request.method == "POST": | ||||||
|         form = ImportCustomersForm(request.POST, request.FILES) |         form = ImportCustomersForm(request.POST, request.FILES) | ||||||
| @@ -109,24 +109,6 @@ def import_customers(request): | |||||||
|         {'form': form} |         {'form': form} | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
| def reconciliate_jar(request): |  | ||||||
|     summary = Payment.get_reconciliation_jar_summary() |  | ||||||
|     if request.method == 'POST': |  | ||||||
|         form = ReconciliationJarForm(request.POST) |  | ||||||
|         if form.is_valid(): |  | ||||||
|             reconciliation = form.save() |  | ||||||
|             reconciliation.add_payments(summary.payments) |  | ||||||
|             reconciliation.clean() |  | ||||||
|             reconciliation.save() |  | ||||||
|             return HttpResponseRedirect('cuadres') |  | ||||||
|     else: |  | ||||||
|         form = ReconciliationJarForm() |  | ||||||
|     return render( |  | ||||||
|         request, |  | ||||||
|         "don_confiao/reconciliate_jar.html", |  | ||||||
|         {'summary': summary, 'form': form} |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def reconciliations(request): | def reconciliations(request): | ||||||
|     return HttpResponse('<h1>Reconciliaciones</h1>') |     return HttpResponse('<h1>Reconciliaciones</h1>') | ||||||
| @@ -178,6 +160,24 @@ def payment_methods_to_select(request): | |||||||
|     return JsonResponse(methods, safe=False) |     return JsonResponse(methods, safe=False) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def sales_for_reconciliation(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] = [] | ||||||
|  |         grouped_sales[sale.payment_method].append({ | ||||||
|  |             'id': sale.id, | ||||||
|  |             'date': sale.date, | ||||||
|  |             'payment_method': sale.payment_method, | ||||||
|  |             'customer': { | ||||||
|  |                 'id': sale.customer.id, | ||||||
|  |                 'name': sale.customer.name, | ||||||
|  |             }, | ||||||
|  |             'total': sale.get_total(), | ||||||
|  |         }) | ||||||
|  |     return JsonResponse(grouped_sales, safe=False) | ||||||
|  |  | ||||||
| def _mask_phone(phone): | def _mask_phone(phone): | ||||||
|     digits = str(phone)[-3:] if phone else " " * 3 |     digits = str(phone)[-3:] if phone else " " * 3 | ||||||
|     return "X" * 7 + digits |     return "X" * 7 + digits | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user