Compare commits

...

53 Commits

Author SHA1 Message Date
03d38f0b64 Tryton Api Client Endpoints Tryton 2025-03-08 10:12:28 -05:00
a097bf7141 Fix(WIP): CORS 2025-01-18 20:18:06 -05:00
eb75a13857 Feat: Implementacion consumo de Api Tryton 2025-01-17 23:27:12 -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
6aca2007e0 #69 feat(View): add reconciliation jar components. 2024-11-15 17:44:30 -05:00
38 changed files with 2615 additions and 1240 deletions

View File

@@ -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,9 +1,13 @@
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework.status import HTTP_400_BAD_REQUEST
from rest_framework.views import APIView
from .models import Sale, SaleLine, Customer, Product
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 SaleView(viewsets.ModelViewSet):
queryset = Sale.objects.all()
@@ -46,3 +50,84 @@ 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)})

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
{
"name": "don-confiao",
"type": "module",
"version": "0.0.0",
"scripts": {
"dev": "vite --host 0.0.0.0",
@@ -10,6 +11,7 @@
"dependencies": {
"@mdi/font": "7.4.47",
"core-js": "^3.37.1",
"cors": "^2.8.5",
"roboto-fontface": "*",
"vee-validate": "^4.14.6",
"vue": "^3.4.31",

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() {
return {
showModal: false,
api: inject('api'),
valid: false,
customer: {
name: '',
@@ -72,25 +73,13 @@
async submitForm() {
console.log(this.customer)
if (this.$refs.form.validate()) {
try {
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();
this.api.createCustomer(this.customer)
.then(data => {
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);
}
})
.catch(error => console.error('Error:', error));
}
},
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,7 @@
menuItems: [
{ title: 'Inicio', route: '/'},
{ title: 'Comprar', route:'/comprar'},
{ title: 'Cuadrar tarro', route: '/cuadrar_tarro'}
],
}),
watch: {

View File

@@ -19,19 +19,17 @@
<v-btn color="primary" @click="openModal">Agregar Cliente</v-btn>
<CreateCustomerModal ref="customerModal" @customerCreated="handleNewCustomer"/>
</v-col>
<v-col
lg="2"
>
<v-col lg="4">
<v-text-field
v-model="purchase.date"
label="Fecha"
type="date"
type="datetime-local"
:rules="[rules.required]"
required
readonly
></v-text-field>
</v-col>
</v-row>
<v-textarea
v-model="purchase.notes"
label="Notas"
@@ -149,6 +147,7 @@
<script>
import CustomerForm from './CreateCustomerModal.vue';
import CasherModal from './CasherModal.vue';
import { inject } from 'vue';
export default {
name: 'DonConfiao',
@@ -161,6 +160,7 @@
},
data() {
return {
api: inject('api'),
valid: false,
form_changed: false,
show_alert_lines: false,
@@ -248,12 +248,12 @@
},
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}`;
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);
@@ -261,8 +261,7 @@
this.purchase.saleline_set[index].measuring_unit = selectedProduct.measuring_unit;
},
fetchClients() {
fetch('/don_confiao/api/customers/')
.then(response => response.json())
this.api.getCustomers()
.then(data => {
this.clients = data;
})
@@ -275,18 +274,23 @@
this.purchase.customer = newCustomer.id;
},
fetchProducts() {
fetch('/don_confiao/api/products/')
.then(response => response.json())
this.api.getProducts()
.then(data => {
this.products = data;
console.log(data)
const transformed_products = data.map(item => ({
name: item.name,
price: item["template."]?.list_price?.decimal,
measuring_unit: item["default_uom"]?.name,
categories: []
}));
this.products = transformed_products;
})
.catch(error => {
console.error(error);
});
},
fetchPaymentMethods() {
fetch('/don_confiao/payment_methods/all/select_format')
.then(response => response.json())
this.api.getPaymentMethods()
.then(data => {
this.payment_methods = data;
})
@@ -313,27 +317,15 @@
async submit() {
this.$refs.purchase.validate();
if (this.valid) {
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();
this.api.createPurchase(this.purchase)
.then(data => {
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);
}
})
.catch(error => console.error('Error al enviarl la compra:', error));
} else {
this.show_alert_purchase = true;
setTimeout(() => {
@@ -349,7 +341,7 @@
},
},
mounted() {
this.fetchClients(); // Llama a fetchClients al montar el componente
this.fetchClients();
}
};
</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

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

@@ -9,11 +9,25 @@ import { registerPlugins } from '@/plugins'
// Components
import App from './App.vue'
import ApiImplementation from './services/api-implementation';
// Composables
import { createApp } from 'vue'
import cors from 'cors';
const app = createApp(App)
// const cors = require('cors');
process.env.API_IMPLEMENTATION = 'tryton';
// process.env.API_IMPLEMENTATION = 'django';
let apiImplementation = new ApiImplementation();
const api = apiImplementation.getApi();
const app = createApp(App);
// app.use(cors({
// origin: '*', // Permitir todas las solicitudes de origen
// exposedHeaders: ['X-Custom-Header', 'Content-Length'], // Exponer headers específicos
// }));
app.provide('api', api);
registerPlugins(app)

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,29 @@
import DjangoApi from './django-api';
import TrytonApiClient from './tryton-api';
import Api from './api';
class ApiImplementation {
constructor() {
const implementation = process.env.API_IMPLEMENTATION;
let apiImplementation;
if (implementation === 'django') {
apiImplementation = new DjangoApi();
} else if (implementation === 'tryton'){
const url = 'http://192.168.0.114:18030';
const key = '9a9ffc430146447d81e6698240199a4be2b0e774cb18474999d0f60e33b5b1eb1cfff9d9141346a98844879b5a9e787489c891ddc8fb45cc903b7244cab64fb1';
const db = 'tryton';
const applicationName = 'sale_don_confiao';
apiImplementation = new TrytonApiClient(
url, key, db, applicationName);
} else {
throw new Error("API implementation don't configured");
}
this.api = new Api(apiImplementation);
}
getApi() {
return this.api;
}
}
export default ApiImplementation;

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,8 @@ import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
@@ -63,6 +65,19 @@ export default defineConfig({
},
server: {
port: 3000,
cors: {
origin: '*',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
},
proxy: {
'/sale_don_confiao': {
target: "http://127.0.0.1:8000/tryton/sale_don_confiao", // Cambia esto a la URL de tu API
changeOrigin: true,
rewrite: (path) => path.replace(/^\/sale_don_confiao/, '/tryton/sale_don_confiao'), // Opcional: reescribe la ruta
ws: true
},
},
},
build: {
outDir: '../../static/frontend/',

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
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(
@@ -74,6 +99,12 @@ class Sale(models.Model):
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}"
@@ -122,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(
@@ -199,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):
@@ -20,7 +20,76 @@ 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', 'email', 'phone']
class ReconciliationJarSerializer(serializers.ModelSerializer):
class Meta:
model = ReconciliationJar
fields = [
'id',
'date_time',
'reconcilier',
'cash_taken',
'cash_discrepancy',
'total_cash_purchases',
]
class PaymentMethodSerializer(serializers.Serializer):
text = serializers.CharField()
value = serializers.CharField()
def to_representation(self, instance):
return {
'text': instance[1],
'value': instance[0],
}
class SaleForRenconciliationSerializer(serializers.Serializer):
id = serializers.IntegerField()
date = serializers.DateTimeField()
payment_method = serializers.CharField()
customer = serializers.SerializerMethodField()
total = serializers.SerializerMethodField()
def get_customer(self, sale):
return {
'id': sale.customer.id,
'name': sale.customer.name,
}
def get_total(self, sale):
return sale.get_total()
class ListCustomerSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = ['id', 'name']
class ListProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ['id', 'name']
class SummarySaleLineSerializer(serializers.ModelSerializer):
product = ListProductSerializer()
class Meta:
model = SaleLine
fields = ['product', 'quantity', 'unit_price', 'description']
class SaleSummarySerializer(serializers.ModelSerializer):
customer = ListCustomerSerializer()
lines = SummarySaleLineSerializer(many=True, source='saleline_set')
class Meta:
model = Sale
fields = ['id', 'date', 'customer', 'payment_method', 'lines']

View File

@@ -10,7 +10,6 @@
<li><a href='/don_confiao/lista_productos'>Productos</a></li>
<li><a href='/don_confiao/importar_productos'>Importar Productos</a></li>
<li><a href='/don_confiao/importar_terceros'>Importar Terceros</a></li>
<li><a href='/don_confiao/cuadrar_tarro'>Cuadrar tarro</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

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

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):
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', response.content.decode('utf-8'))
self.assertIn('cafe', response.content.decode('utf-8'))
self.assertIn('72500', response.content.decode('utf-8'))
self.assertIn('quantity', response.content.decode('utf-8'))
self.assertIn('11', response.content.decode('utf-8'))
self.assertIn('date', response.content.decode('utf-8'))
self.assertIn(self.purchase.date, response.content.decode('utf-8'))
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

@@ -19,7 +19,7 @@ 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()
@@ -29,7 +29,7 @@ class ConfiaoTest(TestCase):
def test_can_create_sale_without_payment_method(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.payment_method = ''

View File

@@ -23,10 +23,11 @@ urlpatterns = [
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>", views.purchase_json_summary, name="purchase_json_summary"),
path("payment_methods/all/select_format", views.payment_methods_to_select, name="payment_methods_to_select"),
path("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,13 +4,12 @@ from django.views.generic import ListView
from django.db import transaction
from .models import (
Sale, SaleLine, Product, Customer, ProductCategory, Payment, PaymentMethods)
Sale, SaleLine, Product, Customer, ProductCategory, Payment, PaymentMethods, ReconciliationJar)
from .forms import (
ImportProductsForm,
ImportCustomersForm,
PurchaseForm,
SaleLineFormSet,
ReconciliationJarForm,
PurchaseSummaryForm)
import csv
@@ -95,6 +94,7 @@ def import_products(request):
{'form': form}
)
def import_customers(request):
if request.method == "POST":
form = ImportCustomersForm(request.POST, request.FILES)
@@ -109,24 +109,6 @@ def import_customers(request):
{'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):
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="&"):
categories = categories_string.split(separator)
clean_categories = [c.strip() for c in categories]

View File

@@ -28,6 +28,10 @@ DEBUG = True
ALLOWED_HOSTS = []
CORS_ALLOWED_ORIGINS = [
"http://localhost:8000",
]
# Application definition
INSTALLED_APPS = [