Reputation: 1
I have an invoiceform to collect the date for postgresql database and Im having trouble with endpoints working properly and populating the drop down list for Salesman field and Product name field.
I checked the json response and it is providing the frontend with with sets whereas with my other backend code although the json responses were valid the select2 couldn't populate then retain the typed out name in the field.
Frontend
// Initialize Select2 components
$(document).ready(function() {
$('#salesman').select2({
placeholder: 'Select Salesman',
ajax: {
url: '/api/salesmen',
dataType: 'json',
delay: 250,
processResults: data => ({
results: data
})
}
});
initProductSelect($('.product-row').first().find('.product-name'));
});
function initProductSelect(element) {
element.select2({
placeholder: 'Select Product',
ajax: {
url: '/api/products',
dataType: 'json',
delay: 250,
processResults: data => ({
results: data
})
}
}).on('select2:select', async function(e) {
const row = $(this).closest('.product-row');
try {
const response = await fetch(`/api/product/${e.params.data.id}`);
const product = await response.json();
row.data('conversion', product.conversion_rate)
.find('.stock-ctn, .added-ctn, .return-ctn').attr('placeholder', product.primary_unit)
.closest('tr').find('.total-dispatch-ctn, .sold-ctn').text(`0 ${product.primary_unit}`);
} catch (error) {
console.error('Error loading product:', error);
}
});
}
function convertToPieces(ctn, pcs, row) {
return (ctn * row.data('conversion')) + (parseInt(pcs) || 0);
}
function updateTotalDispatch(row) {
const $row = $(row);
const conversion = $row.data('conversion') || 1;
const stockCtn = parseInt($row.find('.stock-ctn').val()) || 0;
const stockPcs = parseInt($row.find('.stock-pcs').val()) || 0;
const addedCtn = parseInt($row.find('.added-ctn').val()) || 0;
const addedPcs = parseInt($row.find('.added-pcs').val()) || 0;
const totalPieces = (stockCtn + addedCtn) * conversion + stockPcs + addedPcs;
$row.find('.total-dispatch-ctn').text(`${Math.floor(totalPieces/conversion)} ${$row.find('.stock-ctn').attr('placeholder')}`);
$row.find('.total-dispatch-pcs').text(`${totalPieces % conversion} Pcs`);
}
function updateSoldStock(row) {
const $row = $(row);
const conversion = $row.data('conversion') || 1;
// Parse inputs with fallback to 0 if empty or invalid
const stockCtn = parseInt($row.find('.stock-ctn').val()) || 0;
const stockPcs = parseInt($row.find('.stock-pcs').val()) || 0;
const addedCtn = parseInt($row.find('.added-ctn').val()) || 0;
const addedPcs = parseInt($row.find('.added-pcs').val()) || 0;
const returnCtn = parseInt($row.find('.return-ctn').val()) || 0;
const returnPcs = parseInt($row.find('.return-pcs').val()) || 0;
// Calculate total stock and return
const totalStock = (stockCtn + addedCtn) * conversion + (stockPcs + addedPcs);
const totalReturn = returnCtn * conversion + returnPcs;
// Validate return doesn't exceed stock
if (totalReturn > totalStock) {
const maxReturn = totalStock;
$row.find('.return-ctn').val(Math.floor(maxReturn / conversion));
$row.find('.return-pcs').val(maxReturn % conversion);
alert("Return exceeds available stock!");
return;
}
// Calculate sold stock
const soldPcs = totalStock - totalReturn;
const soldCtn = Math.floor(soldPcs / conversion);
const soldRemainingPcs = soldPcs % conversion;
// Update display
$row.find('.sold-ctn').text(`${soldCtn} ${$row.find('.stock-ctn').attr('placeholder')}`);
$row.find('.sold-pcs').text(`${soldRemainingPcs} Pcs`);
// Disable "Add Rate" button if all stock is allocated
let totalAllocated = 0;
$row.find('.rate-rows tr').each(function() {
const rateCtn = parseInt($(this).find('.rate-ctn').val()) || 0;
const ratePcs = parseInt($(this).find('.rate-pcs').val()) || 0;
totalAllocated += rateCtn * conversion + ratePcs;
});
$row.find('.add-rate').prop('disabled', totalAllocated >= soldPcs);
}
function addRate(button) {
const row = $(button).closest('.product-row');
const rateRows = row.find('.rate-rows');
const soldPcs = convertToPieces(
parseInt(row.find('.sold-ctn').text()),
parseInt(row.find('.sold-pcs').text()),
row
);
let totalAllocated = 0;
rateRows.find('tr').each(function() {
totalAllocated += convertToPieces(
parseInt($(this).find('.rate-ctn').val()),
parseInt($(this).find('.rate-pcs').val()),
row
);
});
if (totalAllocated >= soldPcs) {
alert("Allocated quantity exceeds sold stock!");
return;
}
const newRow = $('<tr>').html(`
<td>
<div class="rate-qty-inputs">
<input type="number" class="rate-ctn" min="0" placeholder="Unit">
<span>-</span>
<input type="number" class="rate-pcs" min="0" placeholder="Pcs">
</div>
</td>
<td><input type="number" class="rate-value" min="0" step="0.01" placeholder="रू"></td>
<td>${rateRows.children().length === 0 ?
'<input type="checkbox" class="rate-all" onchange="selectAllQuantity(this)">' : ''}
</td>
`);
rateRows.append(newRow);
updateSoldStock(row);
}
function removeRate(button) {
const row = $(button).closest('.product-row');
const rateRows = row.find('.rate-rows');
const lastRateRow = rateRows.find('tr').last();
if (lastRateRow.length > 0) {
lastRateRow.remove(); // Remove the last rate row
updateSoldStock(row); // Update sold stock after removal
} else {
alert("No rate rows to remove!");
}
}
function selectAllQuantity(checkbox) {
const row = $(checkbox).closest('.product-row');
const rateRow = $(checkbox).closest('tr');
const conversion = row.data('conversion');
const soldCtn = parseInt(row.find('.sold-ctn').text());
const soldPcs = parseInt(row.find('.sold-pcs').text());
if (checkbox.checked) {
rateRow.find('.rate-ctn').val(soldCtn).prop('disabled', true);
rateRow.find('.rate-pcs').val(soldPcs).prop('disabled', true);
} else {
rateRow.find('.rate-ctn, .rate-pcs').val('').prop('disabled', false);
}
}
function addProductRow() {
const newRow = $('.product-row').first().clone();
newRow.find('input').val('');
newRow.find('.select2-product').remove();
newRow.find('td:first').html('<select class="product-name select2-product"></select>');
newRow.find('.serial-number').text($('.product-row').length + 1);
newRow.find('.rate-rows').empty();
newRow.find('.total-amount, .sold-ctn, .sold-pcs, .total-dispatch-ctn, .total-dispatch-pcs').text('0');
$('#products > tbody').append(newRow);
initProductSelect(newRow.find('.product-name'));
updateSerialNumbers();
}
function removeProductRow(button) {
const rows = $('.product-row');
if (rows.length === 1) {
rows.find('input').val('');
rows.find('.rate-rows').empty();
rows.find('.select2-product').val(null).trigger('change');
updateSoldStock(rows);
updateTotalDispatch(rows);
} else {
$(button).closest('.product-row').remove();
}
updateSerialNumbers();
}
function updateSerialNumbers() {
$('.product-row .serial-number').each((i, el) => $(el).text(i + 1));
}
function calculateTotal() {
let grandTotal = 0;
$('.product-row').each(function() {
let rowTotal = 0;
$(this).find('.rate-rows tr').each(function() {
const ctn = parseInt($(this).find('.rate-ctn').val()) || 0;
const pcs = parseInt($(this).find('.rate-pcs').val()) || 0;
const rate = parseFloat($(this).find('.rate-value').val()) || 0;
const conversion = $(this).closest('.product-row').data('conversion') || 1;
rowTotal += ((ctn * conversion) + pcs) * rate;
});
$(this).find('.total-amount').text(`रू ${rowTotal.toFixed(2)}`);
grandTotal += rowTotal;
});
$('#grand-total').text(`रू ${grandTotal.toFixed(2)}`);
}
async function saveChallan() {
const challanData = {
date: $('#date').val(),
miti: $('#challanMiti').val(),
challan_no: $('#challanNo').val(),
salesman_id: $('#salesman').val(),
items: [],
total_amount: parseFloat($('#grand-total').text().replace('रू ', ''))
};
$('.product-row').each(function() {
const $row = $(this);
const item = {
product_id: $row.find('.product-name').val(),
stock_van: {
ctn: parseInt($row.find('.stock-ctn').val()) || 0,
pcs: parseInt($row.find('.stock-pcs').val()) || 0
},
stock_added: {
ctn: parseInt($row.find('.added-ctn').val()) || 0,
pcs: parseInt($row.find('.added-pcs').val()) || 0
},
stock_return: {
ctn: parseInt($row.find('.return-ctn').val()) || 0,
pcs: parseInt($row.find('.return-pcs').val()) || 0
},
rates: []
};
$row.find('.rate-rows tr').each(function() {
item.rates.push({
ctn: parseInt($(this).find('.rate-ctn').val()) || 0,
pcs: parseInt($(this).find('.rate-pcs').val()) || 0,
rate: parseFloat($(this).find('.rate-value').val()) || 0
});
});
challanData.items.push(item);
});
try {
const response = await fetch('/api/challan', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(challanData)
});
const result = await response.json();
alert(result.message);
if (response.ok) cancelForm();
} catch (error) {
console.error('Error:', error);
alert('Error saving challan');
}
}
function cancelForm() {
$('#salesman').val(null).trigger('change');
$('#date, #challanMiti, #challanNo').val('');
$('.product-row').each(function(index) {
if (index > 0) $(this).remove();
else {
$(this).find('input').val('');
$(this).find('.rate-rows').empty();
$(this).find('.select2-product').val(null).trigger('change');
$(this).find('.total-amount, .sold-ctn, .sold-pcs, .total-dispatch-ctn, .total-dispatch-pcs').text('0');
}
});
$('#grand-total').text('रू 0.00');
updateSerialNumbers();
}
// Event listeners for dynamic updates
$('#products').on('input', 'input', function() {
const row = $(this).closest('.product-row');
updateTotalDispatch(row);
updateSoldStock(row);
});
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap');
body {
font-family: 'Roboto', sans-serif;
margin: 20px;
font-size: 14px;
color: #333;
background-color: beige;
}
.header {
margin-bottom: 20px;
background-color: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.center-title {
text-align: center;
margin: 5px 0;
}
h2 {
font-size: 18px;
font-weight: 500;
}
h3,
h4 {
font-size: 16px;
font-weight: 400;
}
table {
width: 70%;
border-collapse: collapse;
margin-top: 10px;
background-color: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
th,
td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
font-size: 13px;
}
th {
background-color: #f2f2f2;
font-weight: 500;
}
input[type="text"],
input[type="number"],
input[type="date"] {
border: 1px solid #ccc;
padding: 5px;
border-radius: 4px;
font-size: 13px;
background-color: #f9f9f9;
}
input:focus {
border-color: #007BFF;
outline: none;
background-color: white;
}
.total-row {
font-weight: bold;
text-align: right;
}
.buttons {
margin-top: 20px;
display: flex;
gap: 10px;
}
button {
padding: 4px 8px;
font-size: 13px;
border: none;
border-radius: 4px;
background-color: #007BFF;
color: white;
cursor: pointer;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #0056b3;
}
.total-amount {
font-weight: bold;
color: #007BFF;
}
.stock-inputs,
.added-inputs,
.return-inputs,
.rate-qty-inputs {
display: flex;
gap: 5px;
align-items: center;
}
.rate-table {
width: 100%;
margin-top: 10px;
border-collapse: collapse;
}
.rate-table th,
.rate-table td {
border: 1px solid #ddd;
padding: 5px;
font-size: 12px;
}
.rate-table th {
background-color: #f2f2f2;
}
.stacked {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.stacked span {
display: block;
text-align: center;
}
.select2-container {
width: 200px !important;
margin-bottom: 5px;
}
.select2-product {
width: 180px !important;
}
/* Fix for button spacing */
.product-row td:last-child {
display: flex;
gap: 5px;
/* Adds spacing between buttons */
}
/* Style for Add Rate and Remove Rate buttons */
.add-rate,
.remove-rate {
margin-right: 5px;
/* Adds spacing between buttons */
}
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/select2.min.css" rel="stylesheet" />
<div class="header">
<h2 class="center-title"> Store</h2>
<h3 class="center-title">Bau</h3>
<h4 class="center-title">Daily Challan</h4>
<div>
<label>Salesman Name: <select id="salesman" class="select2-salesman"></select></label>
<label>Challan Date: <input type="date" id="date"></label>
<label>Challan Miti: <input type="text" id="challanMiti" placeholder="YYYY-MM-DD (Nepali)"></label>
<label>Challan No: <input type="text" id="challanNo"></label>
</div>
</div>
<table id="products">
<thead>
<tr>
<th>S.No.</th>
<th>Product Name</th>
<th>Stock in Van (Unit/Pcs)</th>
<th>Stock Added (Unit/Pcs)</th>
<th>Stock Return (Unit/Pcs)</th>
<th>Total Dispatch</th>
<th>Stock Sold (Unit/Pcs)</th>
<th>Rate per Unit (रू)</th>
<th>Total Amount (रू)</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr class="product-row">
<td class="serial-number">1</td>
<td><select class="product-name select2-product"></select></td>
<td>
<div class="stock-inputs">
<input type="number" class="stock-ctn" min="0" placeholder="Unit">
<span>-</span>
<input type="number" class="stock-pcs" min="0" placeholder="Pcs">
</div>
</td>
<td>
<div class="added-inputs">
<input type="number" class="added-ctn" min="0" placeholder="Unit">
<span>-</span>
<input type="number" class="added-pcs" min="0" placeholder="Pcs">
</div>
</td>
<td>
<div class="return-inputs">
<input type="number" class="return-ctn" min="0" placeholder="Unit">
<span>-</span>
<input type="number" class="return-pcs" min="0" placeholder="Pcs">
</div>
</td>
<td class="total-dispatch">
<div class="stacked">
<span class="total-dispatch-ctn">0 Unit</span>
<span class="total-dispatch-pcs">0 Pcs</span>
</div>
</td>
<td class="sold-stock">
<div class="stacked">
<span class="sold-ctn">0 Unit</span>
<span class="sold-pcs">0 Pcs</span>
</div>
</td>
<td>
<table class="rate-table">
<thead>
<tr>
<th>Quantity (Unit/Pcs)</th>
<th>Rate (रू)</th>
<th>All</th>
</tr>
</thead>
<tbody class="rate-rows"></tbody>
</table>
<div>
<button class="add-rate" onclick="addRate(this)">Add Rate</button>
<button class="remove-rate" onclick="removeRate(this)">Remove Rate</button>
</div>
</td>
<td class="total-amount">रू 0.00</td>
<td>
<button onclick="addProductRow()">Add Product</button>
<button onclick="removeProductRow(this)">Remove Product</button>
</td>
</tr>
</tbody>
</table>
<div class="buttons">
<button onclick="history.back()">Back to Challan CRUD</button>
<button onclick="calculateTotal()">Calculate Total</button>
<button onclick="saveChallan()">Save Challan</button>
<button onclick="cancelForm()">Cancel</button>
</div>
<table>
<tr>
<td colspan="7" class="total-row">Grand Total:</td>
<td id="grand-total" class="total-amount">रू 0.00</td>
</tr>
</table>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/select2.min.js"></script>
my backend:
from flask import Flask, request, jsonify, render_template
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
app = Flask(__name__)
CORS(app)
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://postgres:Twlight20@localhost/inventory_db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
# Models
class Product(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
primary_unit = db.Column(db.String(20), nullable=False) # ctn, sck, etc.
conversion_rate = db.Column(db.Integer, nullable=False) # pieces per unit
class Salesman(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
class Challan(db.Model):
id = db.Column(db.Integer, primary_key=True)
date = db.Column(db.Date, nullable=False)
miti = db.Column(db.String(10), nullable=False)
challan_no = db.Column(db.String(50), unique=True, nullable=False)
salesman_id = db.Column(db.Integer, db.ForeignKey('salesman.id'), nullable=False)
total_amount = db.Column(db.Float, nullable=False)
items = db.relationship('ChallanItem', backref='challan', cascade="all, delete-orphan")
class ChallanItem(db.Model):
id = db.Column(db.Integer, primary_key=True)
challan_id = db.Column(db.Integer, db.ForeignKey('challan.id'), nullable=False)
product_id = db.Column(db.Integer, db.ForeignKey('product.id'), nullable=False)
stock_van = db.Column(db.JSON, nullable=False)
stock_added = db.Column(db.JSON, nullable=False)
stock_return = db.Column(db.JSON, nullable=False)
rates = db.Column(db.JSON, nullable=False)
# Routes
@app.route('/')
def index():
return render_template('challan.html')
# Get all or search products
@app.route('/api/products', methods=['GET'])
def get_products():
search = request.args.get('q', '').strip()
if search:
products = Product.query.filter(Product.name.ilike(f'%{search}%')).all()
else:
products = Product.query.all()
if not products:
return jsonify({'message': 'No products found'}), 404
return jsonify([{'id': p.id, 'name': p.name, 'primary_unit': p.primary_unit, 'conversion_rate': p.conversion_rate} for p in products])
# Get a single product by ID
@app.route('/api/product/<int:product_id>', methods=['GET'])
def get_product(product_id):
product = Product.query.get(product_id)
if not product:
return jsonify({'error': 'Product not found'}), 404
return jsonify({
'id': product.id,
'name': product.name,
'primary_unit': product.primary_unit,
'conversion_rate': product.conversion_rate
})
# Get all or search salesmen
@app.route('/api/salesmen', methods=['GET'])
def get_salesmen():
search = request.args.get('q', '').strip()
if search:
salesmen = Salesman.query.filter(Salesman.name.ilike(f'%{search}%')).all()
else:
salesmen = Salesman.query.all()
if not salesmen:
return jsonify({'message': 'No salesmen found'}), 404
return jsonify([{'id': s.id, 'name': s.name} for s in salesmen])
# Get a single salesman by ID
@app.route('/api/salesman/<int:salesman_id>', methods=['GET'])
def get_salesman(salesman_id):
salesman = Salesman.query.get(salesman_id)
if not salesman:
return jsonify({'error': 'Salesman not found'}), 404
return jsonify({'id': salesman.id, 'name': salesman.name})
# Save a new Challan
@app.route('/api/challan', methods=['POST'])
def save_challan():
data = request.json
# Validate required fields
required_fields = ['date', 'miti', 'challan_no', 'salesman_id', 'total_amount', 'items']
for field in required_fields:
if field not in data:
return jsonify({'error': f'Missing field: {field}'}), 400
if not isinstance(data['items'], list) or len(data['items']) == 0:
return jsonify({'error': 'Items must be a non-empty list'}), 400
# Ensure the salesman exists
salesman = Salesman.query.get(data['salesman_id'])
if not salesman:
return jsonify({'error': 'Invalid salesman ID'}), 400
# Ensure the challan number is unique
existing_challan = Challan.query.filter_by(challan_no=data['challan_no']).first()
if existing_challan:
return jsonify({'error': 'Challan number already exists'}), 400
challan = Challan(
date=data['date'],
miti=data['miti'],
challan_no=data['challan_no'],
salesman_id=data['salesman_id'],
total_amount=data['total_amount']
)
for item in data['items']:
# Validate each item
required_item_fields = ['product_id', 'stock_van', 'stock_added', 'stock_return', 'rates']
for field in required_item_fields:
if field not in item:
return jsonify({'error': f'Missing item field: {field}'}), 400
# Ensure the product exists
product = Product.query.get(item['product_id'])
if not product:
return jsonify({'error': f'Invalid product ID: {item["product_id"]}'}), 400
challan_item = ChallanItem(
product_id=item['product_id'],
stock_van=item['stock_van'],
stock_added=item['stock_added'],
stock_return=item['stock_return'],
rates=item['rates']
)
challan.items.append(challan_item)
db.session.add(challan)
db.session.commit()
return jsonify({'message': 'Challan saved successfully', 'challan_id': challan.id})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
Upvotes: 0
Views: 31