Compare commits

...

116 Commits

Author SHA1 Message Date
43756952fb Merge pull request 'Creada vista para listar los cuadres de tarro #90' (#91) from view_for_jar_reconciliation_#90 into main
Reviewed-on: #91
2025-02-02 23:08:00 -05:00
6a346bdf8a #90 fix(Frontend): set default tab in Reconciliation summary. 2025-02-02 23:04:54 -05:00
2a983c8056 #90 fix(Frontend): fix datetime in Reconciliation summary. 2025-02-02 22:49:38 -05:00
2c6449c727 #90 feat(Frontend): add Purchases in cuadres_de_tarro. 2025-02-02 22:48:21 -05:00
77a0c4ac0d #90 feat(Frontend): add Purchases in cuadres_de_tarro. 2025-02-02 22:40:31 -05:00
2fcf884cce #90 feat(api): add payment method to sales. 2025-01-28 09:17:48 -05:00
75d39c6ca7 #90 feat(frontend): add modal to ReconciliationJarIndex. 2025-01-18 12:53:53 -05:00
a5d4c1977a #90 feat(frontend): minor fix. 2025-01-18 12:43:50 -05:00
d85ad7cc38 #90 feat(frontend): getReconciliation api method. 2025-01-18 12:43:01 -05:00
aa45ea44ac #90 feat(frontend): getReconciliation api method. 2025-01-18 12:42:33 -05:00
1b06818583 #90 feat(frontend): add ReconciliationJarView.vue. 2025-01-18 12:22:45 -05:00
baa0677e7a #90 feat(frontend): add cuadres_de_tarro page. 2025-01-12 01:23:17 -05:00
d9d3239662 #90 api(frontend): add getListReconcliations. 2025-01-12 01:20:33 -05:00
8bc2d02572 #90 test: minor fix. 2025-01-12 01:18:53 -05:00
c9cfc7f873 #90 feat(ReconcilaitionJar): create api view with pagination. 2025-01-12 00:24:30 -05:00
2a937653df Merge pull request 'asegurnado paginas administrativas #88' (#89) from secure_admin_pages_#88 into main
Reviewed-on: #89
2025-01-11 19:24:43 -05:00
e5ae1bb142 feat(frontend): using api for admin valid code. 2025-01-11 19:22:48 -05:00
9ff1deb687 dev(rakeFile): add terminal django. 2025-01-11 19:21:00 -05:00
0caa6fbb56 #88 feat(AdminCode): create model and api view. 2025-01-11 19:08:05 -05:00
a7e3b9aaa8 #88 refactor(frontend): remove method. 2025-01-11 18:10:44 -05:00
0308da7370 #88 refactor(frontend): extract to Component. 2025-01-11 18:02:28 -05:00
0dec800637 #88 feat(frontend): initial secure pages. 2025-01-11 17:05:39 -05:00
0a88641d34 Merge pull request 'Generado repositorio para consultar la api #86' (#87) from generate_repository_code_#86 into main
Reviewed-on: #87
2025-01-11 16:05:31 -05:00
871a82eee5 #84 refactor(front): moved to api createCustomer. 2025-01-11 15:44:07 -05:00
9ad8ff8706 #84 refactor(front): moved to api create reconciliation jar. 2025-01-11 14:05:01 -05:00
d9f6be8b54 #84 refactor(front): moved to api purchases_for_reconciliation to. 2025-01-11 12:57:25 -05:00
34259921d9 #84 refactor(frontend): extact to method django-api. 2025-01-11 12:46:29 -05:00
8f9917c3a4 #84 refactor(frontend): summaryPurchase moved to repository. 2025-01-11 12:14:48 -05:00
fcb83d05fb #84 refactor(frontend): send purchase moved to repository. 2025-01-11 11:34:37 -05:00
6d6322b0cd #84 refactor(frontend): extract methods to repository. 2025-01-11 10:37:17 -05:00
a4048473ae Merge pull request 'refactor_endpoints_to_api_views #84' (#85) from refactor_endpoints_to_api_views_#84 into main
Reviewed-on: #85
2024-12-31 14:51:05 -05:00
9a6a931481 fix(Frontend): remove non existing component. 2024-12-31 14:35:44 -05:00
432b7dad74 #84 refactor(SaleSummary): rename saleline_set to lines at serializer. 2024-12-31 14:30:20 -05:00
69d8b1d2ad #84 refactor(SaleSummary): move to apiview. 2024-12-31 13:51:11 -05:00
8b7c2efcb3 #84 refactor(SalesForReconciliation): move to apiview. 2024-12-31 13:26:47 -05:00
5dff7565f4 #84 refactor(paymentMethods): move to apiview. 2024-12-31 13:09:16 -05:00
023beaa0ee Merge pull request 'Generando cuadre del tarro en vuetify' (#83) from streamline_reconciliation_jar_process_#69 into main
Reviewed-on: #83
2024-12-28 17:13:35 -05:00
b5fdd7fefd #69 fix(migrations): conflict. 2024-12-28 17:13:55 -05:00
eaf1afdcb4 Merge branch 'main' into streamline_reconciliation_jar_process_#69 2024-12-28 17:07:18 -05:00
5cfefdf91a #69 feat(ReconciliationJar): link purchases with other payment methods. 2024-12-28 17:01:51 -05:00
9c0eebd07d #69 refactor(ReconciliationJar): extract to method. 2024-12-14 22:34:58 -05:00
8ab7903a0a #69 test(ReconciliationJar): fix tests. 2024-12-14 21:37:24 -05:00
1b425542b3 #69 feat(ReconciliationJar): send reconciliation jar from frontend 2024-12-14 19:42:59 -05:00
f6620db6e2 ci(Rakefile): add start command. 2024-12-14 10:00:11 -05:00
1f2f484e95 #69 test(ReconciliationJar): add failed case. 2024-12-14 09:59:26 -05:00
ef721a6b53 #69 feat(ReconciliationJar): add purchases to list. 2024-12-13 17:21:29 -05:00
f0201a86b2 #69 feat(ReconciliationJar): accept post creation. 2024-12-02 22:52:04 -05:00
bea08da17d #69 feat(ReconciliationJar): Add total_cash_purchases field. 2024-12-02 22:00:05 -05:00
a3d5fb1b45 ci(Rake): add dev migrations tasks. 2024-12-02 21:56:23 -05:00
4679170ab9 #69 refactor(ReconciliationJar): remove old form. 2024-12-02 21:44:21 -05:00
0d61e457c7 #69 refactor(ReconciliationJar): remove old view. 2024-12-02 21:43:48 -05:00
3294b8e814 refactor(test): rename method. 2024-12-02 21:28:20 -05:00
3189363ba9 style: minor fix. 2024-12-02 21:22:36 -05:00
0a64373037 ci(Test): add task tests to Rake. 2024-12-02 21:22:06 -05:00
9a20212b27 #69 feat(Reconciliation):get purchases from backend. 2024-11-17 23:55:54 -05:00
a6b4c1c5b6 #69 feat(Reconciliation):get purchases for reconciliation endpoint. 2024-11-17 23:15:21 -05:00
b7984f7556 #69 style(Reconciliation): format total on sales. 2024-11-17 00:07:18 -05:00
ef1a520838 #69 feat(Reconciliation): show summary on modal. 2024-11-17 00:02:08 -05:00
bd6d4221b2 #69 feat(Reconciliation): improve datetime on form. 2024-11-16 23:10:43 -05:00
9aa543662b #69 feat(Reconciliation): improve form. 2024-11-16 22:58:28 -05:00
746686afcc #69 style(Reconciliation): cards to tabs. 2024-11-16 20:36:38 -05:00
c709dad36e #69 base ReconciliationJar. 2024-11-16 16:48:07 -05:00
Rodia
543e927fb0 Merge pull request 'Fix: closed #79' (#81) from PurchaseDateTimeField into main
Reviewed-on: #81
2024-11-16 16:00:27 -05:00
fe1e6e8336 Fix: closed #79 2024-11-16 15:59:56 -05:00
Rodia
00c6bac1d2 Merge pull request 'Feat: closed #74' (#80) from LineaCompraCantidadDiferenteCero into main
Reviewed-on: #80
2024-11-16 15:59:44 -05:00
4ed6bb9024 Feat: closed #74 2024-11-16 13:09:01 -05:00
Rodia
b4467e0292 Merge pull request 'Fix: #75' (#78) from BotonIrAComprarResumenDeCompra into main
Reviewed-on: #78
2024-11-16 12:30:08 -05:00
7c2a7fb1a1 Fix: #75 2024-11-16 12:30:17 -05:00
Rodia
8f48accb4f Merge pull request 'Fix: #76' (#77) from AumentarCampoProductoLineaDeCompra into main
Reviewed-on: #77
2024-11-16 11:43:15 -05:00
d0edba9c28 Fix: #76 2024-11-16 11:43:13 -05:00
6aca2007e0 #69 feat(View): add reconciliation jar components. 2024-11-15 17:44:30 -05:00
b2756ac7ce Merge pull request 'Se ajusta validaciones de formulario y advierte al usuario cuando abandona la página por iteraciones en el navegador.' (#73) from warn_before_leave_purchase_#67 into main
Reviewed-on: #73
2024-11-11 23:08:53 -05:00
159bd737c4 feat(Purchase): warn user whit browser actions #67. 2024-11-11 22:45:47 -05:00
201333ab4b feat(Purchase): warn user invalid form #71 2024-11-11 21:47:46 -05:00
a5ac06704b feat(Purchase): warn user when try eliminate the last line 2024-11-11 21:42:13 -05:00
f1b8cbdce1 fix(Purchase): validation of form. 2024-11-11 21:17:12 -05:00
2d86aba3e5 Merge pull request 'Habilitando la selección de método de pago en la compra' (#72) from chose_pay_method_on_purchase_#47 into main
Reviewed-on: #72
2024-11-11 16:27:21 -05:00
edea82e77b refactor(Purchase): remove unused methods. 2024-11-11 16:23:21 -05:00
d047edaa3f machete(Purchase): translate Cash to Efectivo. 2024-11-11 16:16:33 -05:00
5c24266e7b fix(Purchase): remove fixed payment Methods. 2024-11-11 16:15:24 -05:00
9d602c8ddc feat(PaymentMethods): create endpoint. 2024-11-11 16:09:16 -05:00
99f2f77b78 fix(test): fix create sale api test. 2024-11-11 16:08:20 -05:00
4f0f899c70 fix(Purchase): create Purchase with other payments methods. 2024-11-11 15:36:21 -05:00
c85f0554fb refactor(Purchase): move change cash to componet. 2024-11-11 15:28:08 -05:00
acd9bf53c6 feat(Purchase): add change cash. 2024-11-11 14:33:51 -05:00
8f62dfb9ec feat (frontend): add payment method on summary purchase. 2024-11-11 13:47:11 -05:00
ea77124ee4 feat (frontend): add payment method. 2024-11-09 14:38:38 -05:00
66495e25ff style(Purchase) 2024-11-09 13:55:59 -05:00
b7c4cf5d44 refactoring(Purchase): simplify payment_method #47 2024-11-09 13:49:24 -05:00
d5b7c99b79 Merge pull request 'Arreglar enlaces del menú' (#66) from fix_menu_links_#65 into main
Reviewed-on: #66
2024-11-04 23:20:36 -05:00
6bef38e457 fix(Menu): #65 2024-11-04 23:18:10 -05:00
b80e415393 rename(frontend): index to compra 2024-11-04 19:27:55 -05:00
8ba20acaea Merge pull request 'Redirigiendo al resumen cuando se finaliza una compra' (#64) from redirect_to_summary_when_end_purchase_#55 into main
Reviewed-on: #64
2024-11-02 16:55:01 -05:00
65e99b7ce2 view: fix total on summary purchase. 2024-11-02 16:54:07 -05:00
f6146c177b view: fix subtotal on summary purchase. 2024-11-02 16:53:01 -05:00
7c36524763 Redirect to summary after purchase #55 2024-11-02 16:46:45 -05:00
ac5c962ca7 Merge pull request 'Habilitando Resumen de compra' (#63) from enable_summary_purchase_#55 into main
Reviewed-on: #63
2024-11-02 15:31:23 -05:00
4472d8b6b8 view(Frontend): make dinamic summary_purchase. 2024-11-02 15:27:26 -05:00
8b15d9dd9d view (Frontend): purchase summary. 2024-11-02 14:52:44 -05:00
Rodia
02a010b50d Merge pull request 'Feat: issue #26' (#61) from MeasurementUnitField into main
Reviewed-on: #61
2024-11-02 13:59:12 -05:00
50534ef5b1 Feat: issue #26 2024-11-02 13:58:40 -05:00
8c00b89fb8 frontend: rename summary_purchase page. 2024-11-02 13:58:10 -05:00
b134b88791 summary purchase: generate draft. 2024-11-02 13:54:09 -05:00
1519b3c8bb django(View): add purchase summary json endpoint. 2024-11-02 12:51:26 -05:00
Rodia
589b7c0bfb Merge pull request 'Feat: Datos de contacto customer #46' (#59) from DatosDeClienteAPI into main
Reviewed-on: #59
2024-11-02 12:19:22 -05:00
2ed8b5b3a5 Feat: Datos de contacto customer #46 2024-11-02 12:18:39 -05:00
Rodia
ffa1622870 Merge pull request 'ValidarQueNoSePuedaCrearVentaSinLineas' (#58) from ValidarQueNoSePuedaCrearVentaSinLineas into main
Reviewed-on: #58
2024-11-02 11:46:12 -05:00
a90fb4d937 Fix: Linea de Cero debe ser mayor a cero issue RedEcovida#57 2024-11-02 11:40:51 -05:00
2a908d4e05 Fix: No permitir eliminar linea de venta issue #4 2024-11-02 11:28:10 -05:00
4cbeaa2560 refactor(test): sentence to method. 2024-11-02 11:05:39 -05:00
eeb9821675 Feat: Cargar ultimo cliente creado 2024-11-02 10:54:14 -05:00
2ea8cc7fd8 Feat: Actualizar listado de Clientes al crear uno nuevo 2024-11-02 10:28:15 -05:00
95fab71898 Merge pull request 'Importación de clientes por CSV' (#56) from import_customers into main
Reviewed-on: #56
2024-10-26 17:24:21 -05:00
cosmos
83f3bbdc85 Add migrate 2024-10-26 17:22:58 -05:00
cosmos
5910c0c227 Import Customers 2024-10-26 17:16:27 -05:00
49ac668c14 begining purchase summar. 2024-10-26 16:06:47 -05:00
53 changed files with 2124 additions and 450 deletions

View File

@@ -10,7 +10,7 @@ namespace :live do
compose('up', '--build', '-d', compose: DOCKER_COMPOSE)
end
desc 'monitorear salida'
desc 'monitorear salida'
task :tail do
compose('logs', '-f', 'django', compose: DOCKER_COMPOSE)
end
@@ -20,7 +20,12 @@ namespace :live do
compose('logs', '-f', '-n 50', 'django', compose: DOCKER_COMPOSE)
end
desc 'detener entorno'
desc 'iniciar entorno'
task :start do
compose('start', compose: DOCKER_COMPOSE)
end
desc 'bajar entorno'
task :down do
compose('down', compose: DOCKER_COMPOSE)
end
@@ -52,6 +57,31 @@ namespace :live do
end
desc 'Desarrollo'
namespace :dev do
desc 'correr test de django'
task :test do
compose('exec', 'django', 'python', '/app/manage.py', 'test', '/app/don_confiao')
end
desc 'terminal django'
task :djangoShell do
compose('exec', 'django', 'python', '/app/manage.py', 'shell')
end
desc 'crear migraciones'
task :makemigrations do
compose('exec', 'django', 'python', '/app/manage.py', 'makemigrations')
end
desc 'aplicar migraciones'
task :migrate do
compose('exec', 'django', 'python', '/app/manage.py', 'migrate')
end
end
def compose(*arg, compose: DOCKER_COMPOSE)
sh "docker compose -f #{compose} #{arg.join(' ')}"
end

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ from django.forms.models import inlineformset_factory
from django.forms.widgets import DateInput, DateTimeInput
from .models import Sale, SaleLine, ReconciliationJar, PaymentMethods
from .models import Sale, SaleLine, PaymentMethods
readonly_number_widget = forms.NumberInput(attrs={'readonly': 'readonly'})
@@ -12,6 +12,10 @@ class ImportProductsForm(forms.Form):
csv_file = forms.FileField()
class ImportCustomersForm(forms.Form):
csv_file = forms.FileField()
class PurchaseForm(forms.ModelForm):
class Meta:
model = Sale
@@ -60,18 +64,3 @@ SaleLineFormSet = inlineformset_factory(
extra=1,
fields='__all__'
)
class ReconciliationJarForm(forms.ModelForm):
class Meta:
model = ReconciliationJar
fields = [
'date_time',
'description',
'reconcilier',
'cash_taken',
'cash_discrepancy',
]
widgets = {
'date_time': DateTimeInput(attrs={'type': 'datetime-local'})
}

View File

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

View File

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

View File

@@ -45,9 +45,11 @@
data() {
return {
showModal: false,
api: inject('api'),
valid: false,
customer: {
name: '',
address: '',
email: '',
phone: ''
},
@@ -69,36 +71,26 @@
this.resetForm();
},
async submitForm() {
console.log(this.customer)
if (this.$refs.form.validate()) {
try {
console.log(this.customer)
const response = await fetch('/don_confiao/api/customers/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
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);
}
this.api.createCustomer(this.customer)
.then(data => {
console.log('Cliente Guardado:', data);
this.$emit('customerCreated', data);
this.closeModal();
})
.catch(error => console.error('Error:', error));
}
},
resetForm() {
this.customer = {
name: '',
address: '',
email: '',
phone: ''
};
this.$refs.form.reset();
}
},
}
};
</script>

View File

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

View File

@@ -29,7 +29,8 @@
menuItems: [
{ title: 'Inicio', route: '/'},
{ title: 'Comprar', route:'/comprar'},
{ title: 'Resumen de Compra', route:'/resumen_compra'},
{ title: 'Cuadrar tarro', route: '/cuadrar_tarro'},
{ title: 'Cuadres de tarro', route: '/cuadres_de_tarro'},
],
}),
watch: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,106 @@
<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: Number
},
data () {
return {
api: inject('api'),
purchase: {},
headers: [
{ title: 'Producto', value: 'product.name' },
{ title: 'Precio', value: 'unit_price' },
{ title: 'Cantidad', value: 'quantity' },
{ title: 'Subtotal', value: 'subtotal' },
],
};
},
created() {
if (this.id) {
this.fetchPurchase(this.id);
} else {
console.error('No se proporcionó un ID de compra.');
}
},
methods: {
fetchPurchase(purchaseId) {
this.api.getSummaryPurchase(purchaseId)
.then(data => {
this.purchase = data;
})
.catch(error => {
console.error(error);
});
},
currencyFormat(value) {
return new Intl.NumberFormat('es-CO', { style: 'currency', currency: 'COP' }).format(value);
},
calculateSubtotal(price, quantity) {
price = parseFloat(price || 0);
quantity = parseFloat(quantity || 0);
return price * quantity;
},
calculateTotal(lines) {
let total = 0;
lines.forEach(line => {
total += this.calculateSubtotal(line.unit_price, line.quantity);
});
return total;
}
},
};
</script>
<style>

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,11 +9,17 @@ import { registerPlugins } from '@/plugins'
// Components
import App from './App.vue'
import ApiImplementation from './services/api-implementation';
// Composables
import { createApp } from 'vue'
const app = createApp(App)
process.env.API_IMPLEMENTATION = 'django';
let apiImplementation = new ApiImplementation();
const api = apiImplementation.getApi();
const app = createApp(App);
app.provide('api', api);
registerPlugins(app)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,9 +6,17 @@ from decimal import Decimal
from datetime import datetime
class PaymentMethods(models.TextChoices):
CASH = 'CASH', _('Efectivo')
CONFIAR = 'CONFIAR', _('Confiar')
BANCOLOMBIA = 'BANCOLOMBIA', _('Bancolombia')
class Customer(models.Model):
name = models.CharField(max_length=100, default=None, null=False, blank=False)
address = models.CharField(max_length=100, null=True, blank=True)
email = models.CharField(max_length=100, null=True, blank=True)
phone = models.CharField(max_length=100, null=True, blank=True)
def __str__(self):
return self.name
@@ -54,11 +62,49 @@ class Product(models.Model):
return products_list
class ReconciliationJar(models.Model):
is_valid = models.BooleanField(default=False)
date_time = models.DateTimeField()
description = models.CharField(max_length=255, null=True, blank=True)
reconcilier = models.CharField(max_length=255, null=False, blank=False)
cash_taken = models.DecimalField(max_digits=9, decimal_places=2)
cash_discrepancy = models.DecimalField(max_digits=9, decimal_places=2)
total_cash_purchases = models.DecimalField(max_digits=9, decimal_places=2)
def clean(self):
self._validate_taken_ammount()
def add_payments(self, payments):
for payment in payments:
self.payment_set.add(payment)
self.is_valid = True
def _validate_taken_ammount(self):
ammount_cash = self.cash_taken + self.cash_discrepancy
if not self.total_cash_purchases == ammount_cash:
raise ValidationError(
{"cash_taken": _("The taken ammount has discrepancy.")}
)
class Sale(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.PROTECT)
date = models.DateField("Date")
date = models.DateTimeField("Date")
phone = models.CharField(max_length=13, null=True, blank=True)
description = models.CharField(max_length=255, null=True, blank=True)
payment_method = models.CharField(
max_length=30,
choices=PaymentMethods.choices,
default=PaymentMethods.CASH,
blank=False,
null=False
)
reconciliation = models.ForeignKey(
ReconciliationJar,
on_delete=models.RESTRICT,
related_name='Sales',
null=True
)
def __str__(self):
return f"{self.date} {self.customer}"
@@ -67,6 +113,10 @@ class Sale(models.Model):
lines = self.saleline_set.all()
return sum([l.quantity * l.unit_price for l in lines])
def clean(self):
if self.payment_method not in PaymentMethods.values:
raise ValidationError({'payment_method': "Invalid payment method"})
@classmethod
def sale_header_csv(cls):
sale_header_csv = [field.name for field in cls._meta.fields]
@@ -86,12 +136,6 @@ class SaleLine(models.Model):
return f"{self.sale} - {self.product}"
class PaymentMethods(models.TextChoices):
CASH = 'CASH', _('Cash')
CONFIAR = 'CONFIAR', _('Confiar')
BANCOLOMBIA = 'BANCOLOMBIA', _('Bancolombia')
class ReconciliationJarSummary():
def __init__(self, payments):
self._validate_payments(payments)
@@ -109,38 +153,6 @@ class ReconciliationJarSummary():
return self._payments
class ReconciliationJar(models.Model):
is_valid = models.BooleanField(default=False)
date_time = models.DateTimeField()
description = models.CharField(max_length=255, null=True, blank=True)
reconcilier = models.CharField(max_length=255, null=False, blank=False)
cash_taken = models.DecimalField(max_digits=9, decimal_places=2)
cash_discrepancy = models.DecimalField(max_digits=9, decimal_places=2)
def clean(self):
if not self.is_valid:
payments = Payment.get_reconciliation_jar_summary().payments
else:
payments = self.payment_set.all()
payments_amount = Decimal(sum([p.amount for p in payments]))
reconciliation_ammount = Decimal(sum([
self.cash_taken,
self.cash_discrepancy,
]))
equal_ammounts = reconciliation_ammount.compare(payments_amount) == Decimal('0')
if not equal_ammounts:
raise ValidationError(
{"cash_taken": _("The taken ammount has discrepancy.")}
)
def add_payments(self, payments):
for payment in payments:
self.payment_set.add(payment)
self.is_valid = True
class Payment(models.Model):
date_time = models.DateTimeField()
type_payment = models.CharField(
@@ -186,3 +198,7 @@ class Payment(models.Model):
class PaymentSale(models.Model):
payment = models.ForeignKey(Payment, on_delete=models.CASCADE)
sale = models.ForeignKey(Sale, on_delete=models.CASCADE)
class AdminCode(models.Model):
value = models.CharField(max_length=255, null=False, blank=False)

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@
<li><a href='/don_confiao/compras'>Compras</a></li>
<li><a href='/don_confiao/lista_productos'>Productos</a></li>
<li><a href='/don_confiao/importar_productos'>Importar Productos</a></li>
<li><a href='/don_confiao/cuadrar_tarro'>Cuadrar tarro</a></li>
<li><a href='/don_confiao/importar_terceros'>Importar Terceros</a></li>
</ul>
</nav>
<p id="page_title" class="text-center decoration-solid font-mono font-bold text-lg page_title">Don Confiao - Tienda la Ilusión</p>

View File

@@ -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 %}

View File

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

View File

@@ -17,22 +17,19 @@ class TestAPI(APITestCase):
)
def test_create_sale(self):
url = '/don_confiao/api/sales/'
data = {
'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')
response = self._create_sale()
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Sale.objects.count(), 1)
sale = Sale.objects.all()[0]
self.assertEqual(
Sale.objects.all()[0].customer.name,
sale.customer.name,
self.customer.name
)
self.assertEqual(
sale.id,
content['id']
)
def test_get_products(self):
url = '/don_confiao/api/products/'
@@ -47,3 +44,16 @@ class TestAPI(APITestCase):
json_response = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, 200)
self.assertEqual(self.customer.name, json_response[0]['name'])
def _create_sale(self):
url = '/don_confiao/api/sales/'
data = {
'customer': self.customer.id,
'date': '2024-09-02',
'payment_method': 'CASH',
'saleline_set': [
{'product': self.product.id, 'quantity': 2, 'unit_price': 3000},
{'product': self.product.id, 'quantity': 3, 'unit_price': 5000}
],
}
return self.client.post(url, data, format='json')

View File

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

View File

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

View File

@@ -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])

View File

@@ -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()

View File

@@ -1,6 +1,7 @@
from django.test import TestCase, Client
from ..models import Sale, Product, SaleLine, Customer
class TestSummaryViewPurchase(TestCase):
def setUp(self):
customer = Customer()
@@ -22,13 +23,31 @@ class TestSummaryViewPurchase(TestCase):
line = SaleLine()
line.sale = purchase
line.product = product
line.quantity = "2"
line.quantity = "11"
line.unit_price = "72500"
line.save()
self.purchase = purchase
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.context["purchase"].customer, self.purchase.customer)
self.assertEqual(
response.context["purchase"].customer,
self.purchase.customer
)
self.assertIn('Alejo Mono', response.content.decode('utf-8'))
def test_json_summary(self):
url = f"/don_confiao/resumen_compra_json/{self.purchase.id}"
response = self.client.get(url)
content = response.content.decode('utf-8')
self.assertEqual(response.status_code, 200)
self.assertIn('Alejo Mono', content)
self.assertIn('cafe', content)
self.assertIn('72500', content)
self.assertIn('quantity', content)
self.assertIn('11', content)
self.assertIn('date', content)
self.assertIn(self.purchase.date, content)
self.assertIn('lines', content)

View File

@@ -1,4 +1,6 @@
from django.test import TestCase
from django.core.exceptions import ValidationError
from ..models import Customer, Product, Sale, SaleLine
@@ -17,13 +19,24 @@ class ConfiaoTest(TestCase):
def test_create_sale(self):
sale = Sale()
sale.customer = self.customer
sale.date = "2024-06-22"
sale.date = "2024-06-22 12:05:00"
sale.phone = '666666666'
sale.description = "Description"
sale.save()
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):
sale = Sale()
sale.customer = self.customer

View File

@@ -19,6 +19,7 @@ class PurchaseFormTest(TestCase):
"csrfmiddlewaretoken": _csrf_token,
"customer": self.customer.id,
"date": "2024-08-03",
"payment_method": "CASH",
"phone": "sfasfd",
"description": "dasdadad",
"saleline_set-TOTAL_FORMS": "1",

View File

@@ -10,7 +10,8 @@ router = DefaultRouter()
router.register(r'sales', api_views.SaleView, basename='sale')
router.register(r'customers', api_views.CustomerView, basename='customer')
router.register(r'products', api_views.ProductView, basename='product')
router.register(r'reconciliate_jar', api_views.ReconciliateJarModelView,
basename='reconciliate_jar')
urlpatterns = [
path("", views.index, name="wellcome"),
@@ -19,11 +20,15 @@ urlpatterns = [
path("productos", views.products, name="products"),
path("lista_productos", views.ProductListView.as_view(), name='product_list'),
path("importar_productos", views.import_products, name="import_products"),
path("importar_terceros", views.import_customers, name="import_customers"),
path("exportar_ventas_para_tryton",
views.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_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)),
]

View File

@@ -4,12 +4,12 @@ from django.views.generic import ListView
from django.db import transaction
from .models import (
Sale, SaleLine, Product, ProductCategory, Payment, PaymentMethods)
Sale, SaleLine, Product, Customer, ProductCategory, Payment, PaymentMethods, ReconciliationJar)
from .forms import (
ImportProductsForm,
ImportCustomersForm,
PurchaseForm,
SaleLineFormSet,
ReconciliationJarForm,
PurchaseSummaryForm)
import csv
@@ -95,28 +95,25 @@ def import_products(request):
)
def reconciliate_jar(request):
summary = Payment.get_reconciliation_jar_summary()
if request.method == 'POST':
form = ReconciliationJarForm(request.POST)
def import_customers(request):
if request.method == "POST":
form = ImportCustomersForm(request.POST, request.FILES)
if form.is_valid():
reconciliation = form.save()
reconciliation.add_payments(summary.payments)
reconciliation.clean()
reconciliation.save()
return HttpResponseRedirect('cuadres')
handle_import_customers_file(request.FILES["csv_file"])
return HttpResponseRedirect("productos")
else:
form = ReconciliationJarForm()
form = ImportCustomersForm()
return render(
request,
"don_confiao/reconciliate_jar.html",
{'summary': summary, 'form': form}
"don_confiao/import_customers.html",
{'form': form}
)
def reconciliations(request):
return HttpResponse('<h1>Reconciliaciones</h1>')
def purchase_summary(request, id):
purchase = Sale.objects.get(pk=id)
return render(
@@ -154,6 +151,18 @@ def handle_import_products_file(csv_file):
for category in categories:
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):
tryton_sales_header = [