Compare commits

..

67 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
6aca2007e0 #69 feat(View): add reconciliation jar components. 2024-11-15 17:44:30 -05:00
38 changed files with 1567 additions and 424 deletions

View File

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

View File

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

View File

@@ -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'})
}

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,6 +45,7 @@
data() { data() {
return { return {
showModal: false, showModal: false,
api: inject('api'),
valid: false, valid: false,
customer: { customer: {
name: '', name: '',
@@ -72,25 +73,13 @@
async submitForm() { async submitForm() {
console.log(this.customer) console.log(this.customer)
if (this.$refs.form.validate()) { if (this.$refs.form.validate()) {
try { this.api.createCustomer(this.customer)
const response = await fetch('/don_confiao/api/customers/', { .then(data => {
method: 'POST', console.log('Cliente Guardado:', data);
headers: { this.$emit('customerCreated', data);
'Content-Type': 'application/json', this.closeModal();
}, })
body: JSON.stringify(this.customer), .catch(error => console.error('Error:', error));
});
if (response.ok) {
const data = await response.json();
console.log('Cliente Guardado:', data);
this.$emit('customerCreated', data);
this.closeModal();
} else {
console.error('Error al Crear el Cliente:', response.statusText);
}
} catch (error) {
console.error('Error de red:', error);
}
} }
}, },
resetForm() { resetForm() {

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,6 +29,8 @@
menuItems: [ menuItems: [
{ title: 'Inicio', route: '/'}, { title: 'Inicio', route: '/'},
{ title: 'Comprar', route:'/comprar'}, { title: 'Comprar', route:'/comprar'},
{ title: 'Cuadrar tarro', route: '/cuadrar_tarro'},
{ title: 'Cuadres de tarro', route: '/cuadres_de_tarro'},
], ],
}), }),
watch: { watch: {

View File

@@ -2,36 +2,34 @@
<v-container> <v-container>
<v-form ref="purchase" v-model="valid" @change="onFormChange"> <v-form ref="purchase" v-model="valid" @change="onFormChange">
<v-row> <v-row>
<v-col> <v-col>
<v-autocomplete <v-autocomplete
v-model="purchase.customer" v-model="purchase.customer"
:items="filteredClients" :items="filteredClients"
:search="client_search" :search="client_search"
no-data-text="No se hallaron clientes" no-data-text="No se hallaron clientes"
item-title="name" item-title="name"
item-value="id" item-value="id"
@update:model-value="onFormChange" @update:model-value="onFormChange"
label="Cliente" label="Cliente"
:rules="[rules.required]" :rules="[rules.required]"
required required
class="mr-4" class="mr-4"
></v-autocomplete> ></v-autocomplete>
<v-btn color="primary" @click="openModal">Agregar Cliente</v-btn> <v-btn color="primary" @click="openModal">Agregar Cliente</v-btn>
<CreateCustomerModal ref="customerModal" @customerCreated="handleNewCustomer"/> <CreateCustomerModal ref="customerModal" @customerCreated="handleNewCustomer"/>
</v-col> </v-col>
<v-col <v-col lg="4">
lg="2" <v-text-field
> v-model="purchase.date"
<v-text-field label="Fecha"
v-model="purchase.date" type="datetime-local"
label="Fecha" :rules="[rules.required]"
type="date" required
:rules="[rules.required]" readonly
required
></v-text-field> ></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-textarea <v-textarea
v-model="purchase.notes" v-model="purchase.notes"
label="Notas" label="Notas"
@@ -71,7 +69,7 @@
v-model.number="line.quantity" v-model.number="line.quantity"
label="Cantidad" label="Cantidad"
type="number" type="number"
:rules="[rules.required]" :rules="[rules.required,rules.positive]"
required required
></v-text-field> ></v-text-field>
</v-col> </v-col>
@@ -149,10 +147,11 @@
<script> <script>
import CustomerForm from './CreateCustomerModal.vue'; import CustomerForm from './CreateCustomerModal.vue';
import CasherModal from './CasherModal.vue'; import CasherModal from './CasherModal.vue';
import { inject } from 'vue';
export default { export default {
name: 'DonConfiao', name: 'DonConfiao',
components: { components: {
CustomerForm, CustomerForm,
CasherModal, CasherModal,
}, },
@@ -161,6 +160,7 @@
}, },
data() { data() {
return { return {
api: inject('api'),
valid: false, valid: false,
form_changed: false, form_changed: false,
show_alert_lines: false, show_alert_lines: false,
@@ -176,17 +176,18 @@
saleline_set: [{product:'', unit_price: 0, quantity: 0, unit: ''}], saleline_set: [{product:'', unit_price: 0, quantity: 0, unit: ''}],
}, },
rules: { rules: {
required: value => !!value || 'Requerido.', required: value => !!value || 'Requerido.',
positive: value => value > 0 || 'La cantidad debe ser mayor que 0.',
}, },
menuItems: [ menuItems: [
{ title: 'Inicio', route: '/'}, { title: 'Inicio', route: '/'},
{ title: 'Compras', route:'/compras'}, { title: 'Compras', route:'/compras'},
], ],
clients: [], clients: [],
products: [], products: [],
}; };
}, },
created() { created() {
this.fetchClients(); this.fetchClients();
this.fetchProducts(); this.fetchProducts();
this.fetchPaymentMethods(); this.fetchPaymentMethods();
@@ -246,52 +247,49 @@
} }
}, },
getCurrentDate() { getCurrentDate() {
const today = new Date(); const today = new Date();
const yyyy = today.getFullYear(); const gmtOffSet = -5;
const mm = String(today.getMonth() + 1).padStart(2, '0'); const localDate = new Date(today.getTime() + (gmtOffSet * 60 * 60 * 1000));
const dd = String(today.getDate()).padStart(2, '0'); // Formatear la fecha y hora en el formato YYYY-MM-DDTHH:MM
return `${yyyy}-${mm}-${dd}`; const formattedDate = localDate.toISOString().slice(0,16);
return formattedDate;
}, },
onProductChange(index) {
onProductChange(index) { const selectedProductId = this.purchase.saleline_set[index].product;
const selectedProductId = this.purchase.saleline_set[index].product;
const selectedProduct = this.products.find(p => p.id == selectedProductId); const selectedProduct = this.products.find(p => p.id == selectedProductId);
this.purchase.saleline_set[index].unit_price = selectedProduct.price; this.purchase.saleline_set[index].unit_price = selectedProduct.price;
this.purchase.saleline_set[index].measuring_unit = selectedProduct.measuring_unit; this.purchase.saleline_set[index].measuring_unit = selectedProduct.measuring_unit;
}, },
fetchClients() { fetchClients() {
fetch('/don_confiao/api/customers/') this.api.getCustomers()
.then(response => response.json()) .then(data => {
.then(data => { this.clients = data;
this.clients = data; })
}) .catch(error => {
.catch(error => { console.error(error);
console.error(error); });
});
}, },
handleNewCustomer(newCustomer){ handleNewCustomer(newCustomer){
this.clients.push(newCustomer); this.clients.push(newCustomer);
this.purchase.customer = newCustomer.id; this.purchase.customer = newCustomer.id;
}, },
fetchProducts() { fetchProducts() {
fetch('/don_confiao/api/products/') this.api.getProducts()
.then(response => response.json()) .then(data => {
.then(data => { this.products = data;
this.products = data; })
}) .catch(error => {
.catch(error => { console.error(error);
console.error(error); });
});
}, },
fetchPaymentMethods() { fetchPaymentMethods() {
fetch('/don_confiao/payment_methods/all/select_format') this.api.getPaymentMethods()
.then(response => response.json()) .then(data => {
.then(data => { this.payment_methods = data;
this.payment_methods = data; })
}) .catch(error => {
.catch(error => { console.error(error);
console.error(error); });
});
}, },
addLine() { addLine() {
this.purchase.saleline_set.push({ product: '', unit_price: 0, quantity:0, measuring_unit: ''}); this.purchase.saleline_set.push({ product: '', unit_price: 0, quantity:0, measuring_unit: ''});
@@ -311,28 +309,16 @@
}, },
async submit() { async submit() {
this.$refs.purchase.validate(); this.$refs.purchase.validate();
if (this.valid) { if (this.valid) {
try { this.api.createPurchase(this.purchase)
const response = await fetch('/don_confiao/api/sales/', { .then(data => {
method: 'POST', console.log('Compra enviada:', data);
headers: { this.$router.push({
'Content-Type': 'application/json', path: "/summary_purchase",
}, query : {id: parseInt(data.id)}
body: JSON.stringify(this.purchase), });
}); })
if (response.ok) { .catch(error => console.error('Error al enviarl la compra:', error));
const data = await response.json();
console.log('Compra enviada:', data);
this.$router.push({
path: "/summary_purchase",
query : {id: parseInt(data.id)}
});
} else {
console.error('Error al enviar la compra:', response.statusText);
}
} catch (error) {
console.error('Error de red:', error);
}
} else { } else {
this.show_alert_purchase = true; this.show_alert_purchase = true;
setTimeout(() => { setTimeout(() => {
@@ -348,7 +334,7 @@
}, },
}, },
mounted() { mounted() {
this.fetchClients(); // Llama a fetchClients al montar el componente this.fetchClients();
} }
}; };
</script> </script>

View File

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

View File

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

View File

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

View File

@@ -12,33 +12,25 @@
</v-toolbar> </v-toolbar>
<v-list> <v-list>
<v-list-item> <v-list-item>
<v-list-item-content>
<v-list-item-title>Fecha:</v-list-item-title> <v-list-item-title>Fecha:</v-list-item-title>
<v-list-item-subtitle>{{ purchase.date }}</v-list-item-subtitle> <v-list-item-subtitle>{{ purchase.date }}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<v-list-item-content>
<v-list-item-title>Cliente:</v-list-item-title> <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-subtitle v-if="purchase.customer">{{ purchase.customer.name }}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<v-list-item-content>
<v-list-item-title>Pagado en:</v-list-item-title> <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-subtitle v-if="purchase.payment_method">{{ purchase.payment_method }}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<v-list-item-content>
<v-list-item-title>Total:</v-list-item-title> <v-list-item-title>Total:</v-list-item-title>
<v-list-item-subtitle v-if="purchase.set_lines">{{ currencyFormat(calculateTotal(purchase.set_lines)) }}</v-list-item-subtitle> <v-list-item-subtitle v-if="purchase.lines">{{ currencyFormat(calculateTotal(purchase.lines)) }}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item> </v-list-item>
</v-list> </v-list>
<v-data-table-virtual <v-data-table-virtual
:headers="headers" :headers="headers"
:items="purchase.set_lines" :items="purchase.lines"
> >
<template v-slot:item.unit_price="{ item }"> <template v-slot:item.unit_price="{ item }">
{{ currencyFormat(item.unit_price) }} {{ currencyFormat(item.unit_price) }}
@@ -55,14 +47,17 @@
</template> </template>
<script> <script>
import { inject } from 'vue';
export default { export default {
name: 'SummaryPurchase', name: 'SummaryPurchase',
props: { props: {
msg: String, msg: String,
id: String id: Number
}, },
data () { data () {
return { return {
api: inject('api'),
purchase: {}, purchase: {},
headers: [ headers: [
{ title: 'Producto', value: 'product.name' }, { title: 'Producto', value: 'product.name' },
@@ -81,8 +76,7 @@
}, },
methods: { methods: {
fetchPurchase(purchaseId) { fetchPurchase(purchaseId) {
fetch(`/don_confiao/resumen_compra_json/${purchaseId}`) this.api.getSummaryPurchase(purchaseId)
.then(response => response.json())
.then(data => { .then(data => {
this.purchase = data; this.purchase = data;
}) })

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

@@ -9,11 +9,17 @@ import { registerPlugins } from '@/plugins'
// Components // Components
import App from './App.vue' import App from './App.vue'
import ApiImplementation from './services/api-implementation';
// Composables // Composables
import { createApp } from 'vue' import { createApp } from 'vue'
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) registerPlugins(app)

View File

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

View File

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

View File

@@ -0,0 +1,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,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

@@ -62,9 +62,34 @@ class Product(models.Model):
return products_list return products_list
class ReconciliationJar(models.Model):
is_valid = models.BooleanField(default=False)
date_time = models.DateTimeField()
description = models.CharField(max_length=255, null=True, blank=True)
reconcilier = models.CharField(max_length=255, null=False, blank=False)
cash_taken = models.DecimalField(max_digits=9, decimal_places=2)
cash_discrepancy = models.DecimalField(max_digits=9, decimal_places=2)
total_cash_purchases = models.DecimalField(max_digits=9, decimal_places=2)
def clean(self):
self._validate_taken_ammount()
def add_payments(self, payments):
for payment in payments:
self.payment_set.add(payment)
self.is_valid = True
def _validate_taken_ammount(self):
ammount_cash = self.cash_taken + self.cash_discrepancy
if not self.total_cash_purchases == ammount_cash:
raise ValidationError(
{"cash_taken": _("The taken ammount has discrepancy.")}
)
class Sale(models.Model): class Sale(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.PROTECT) customer = models.ForeignKey(Customer, on_delete=models.PROTECT)
date = models.DateField("Date") date = models.DateTimeField("Date")
phone = models.CharField(max_length=13, null=True, blank=True) phone = models.CharField(max_length=13, null=True, blank=True)
description = models.CharField(max_length=255, null=True, blank=True) description = models.CharField(max_length=255, null=True, blank=True)
payment_method = models.CharField( payment_method = models.CharField(
@@ -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(
@@ -199,3 +198,7 @@ class Payment(models.Model):
class PaymentSale(models.Model): class PaymentSale(models.Model):
payment = models.ForeignKey(Payment, on_delete=models.CASCADE) payment = models.ForeignKey(Payment, on_delete=models.CASCADE)
sale = models.ForeignKey(Sale, on_delete=models.CASCADE) sale = models.ForeignKey(Sale, on_delete=models.CASCADE)
class AdminCode(models.Model):
value = models.CharField(max_length=255, null=False, blank=False)

View File

@@ -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):
@@ -10,9 +10,12 @@ class SaleLineSerializer(serializers.ModelSerializer):
class SaleSerializer(serializers.ModelSerializer): class SaleSerializer(serializers.ModelSerializer):
total = serializers.ReadOnlyField(source='get_total')
class Meta: class Meta:
model = Sale model = Sale
fields = ['id', 'customer', 'date', 'saleline_set'] fields = ['id', 'customer', 'date', 'saleline_set',
'total', 'payment_method']
class ProductSerializer(serializers.ModelSerializer): class ProductSerializer(serializers.ModelSerializer):
@@ -20,7 +23,81 @@ 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):
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

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

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

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

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

@@ -41,11 +41,13 @@ class TestSummaryViewPurchase(TestCase):
def test_json_summary(self): def test_json_summary(self):
url = f"/don_confiao/resumen_compra_json/{self.purchase.id}" url = f"/don_confiao/resumen_compra_json/{self.purchase.id}"
response = self.client.get(url) response = self.client.get(url)
content = response.content.decode('utf-8')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIn('Alejo Mono', response.content.decode('utf-8')) self.assertIn('Alejo Mono', content)
self.assertIn('cafe', response.content.decode('utf-8')) self.assertIn('cafe', content)
self.assertIn('72500', response.content.decode('utf-8')) self.assertIn('72500', content)
self.assertIn('quantity', response.content.decode('utf-8')) self.assertIn('quantity', content)
self.assertIn('11', response.content.decode('utf-8')) self.assertIn('11', content)
self.assertIn('date', response.content.decode('utf-8')) self.assertIn('date', content)
self.assertIn(self.purchase.date, response.content.decode('utf-8')) self.assertIn(self.purchase.date, content)
self.assertIn('lines', content)

View File

@@ -19,7 +19,7 @@ class ConfiaoTest(TestCase):
def test_create_sale(self): def test_create_sale(self):
sale = Sale() sale = Sale()
sale.customer = self.customer sale.customer = self.customer
sale.date = "2024-06-22" sale.date = "2024-06-22 12:05:00"
sale.phone = '666666666' sale.phone = '666666666'
sale.description = "Description" sale.description = "Description"
sale.save() sale.save()
@@ -29,7 +29,7 @@ class ConfiaoTest(TestCase):
def test_can_create_sale_without_payment_method(self): def test_can_create_sale_without_payment_method(self):
sale = Sale() sale = Sale()
sale.customer = self.customer sale.customer = self.customer
sale.date = "2024-06-22" sale.date = "2024-06-22 12:05:00"
sale.phone = '666666666' sale.phone = '666666666'
sale.description = "Description" sale.description = "Description"
sale.payment_method = '' sale.payment_method = ''

View File

@@ -10,7 +10,8 @@ router = DefaultRouter()
router.register(r'sales', api_views.SaleView, basename='sale') router.register(r'sales', api_views.SaleView, basename='sale')
router.register(r'customers', api_views.CustomerView, basename='customer') router.register(r'customers', api_views.CustomerView, basename='customer')
router.register(r'products', api_views.ProductView, basename='product') router.register(r'products', api_views.ProductView, basename='product')
router.register(r'reconciliate_jar', api_views.ReconciliateJarModelView,
basename='reconciliate_jar')
urlpatterns = [ urlpatterns = [
path("", views.index, name="wellcome"), path("", views.index, name="wellcome"),
@@ -23,10 +24,11 @@ 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>", api_views.SaleSummary.as_view(), 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", api_views.PaymentMethodView.as_view(), name="payment_methods_to_select"),
path('purchases/for_reconciliation', api_views.SalesForReconciliationView.as_view(), name='sales_for_reconciliation'),
path('reconciliate_jar', api_views.ReconciliateJarView.as_view()),
path('api/admin_code/validate/<code>', api_views.AdminCodeValidateView.as_view()),
path('api/', include(router.urls)), path('api/', include(router.urls)),
] ]

View File

@@ -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>')
@@ -143,46 +125,6 @@ def purchase_summary(request, id):
) )
def purchase_json_summary(request, id):
purchase = Sale.objects.get(pk=id)
lines = []
for line in purchase.saleline_set.all():
lines.append({
'product': {
'id': line.product.id,
'name': line.product.name,
},
'quantity': line.quantity,
'unit_price': line.unit_price,
'description': line.description,
})
to_response = {
'id': purchase.id,
'date': purchase.date,
'customer': {
'id': purchase.customer.id,
'name': purchase.customer.name,
# 'phone': _mask_phone(purchase.customer.phone)
},
'payment_method': purchase.payment_method,
'set_lines': lines,
}
return JsonResponse(to_response, safe=False)
def payment_methods_to_select(request):
methods = [
{'text': choice[1], 'value': choice[0]}
for choice in PaymentMethods.choices
]
return JsonResponse(methods, safe=False)
def _mask_phone(phone):
digits = str(phone)[-3:] if phone else " " * 3
return "X" * 7 + digits
def _categories_from_csv_string(categories_string, separator="&"): def _categories_from_csv_string(categories_string, separator="&"):
categories = categories_string.split(separator) categories = categories_string.split(separator)
clean_categories = [c.strip() for c in categories] clean_categories = [c.strip() for c in categories]