feat: intake page rework, new Pallet fields, address fix, button rename, COR generator, theme CSS
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,98 @@
|
|||||||
|
import frappe
|
||||||
|
from reportlab.lib.pagesizes import letter
|
||||||
|
from reportlab.lib.units import inch
|
||||||
|
from reportlab.lib.colors import HexColor
|
||||||
|
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable
|
||||||
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||||
|
from reportlab.lib.enums import TA_CENTER
|
||||||
|
from reportlab.lib import colors
|
||||||
|
import sqlite3
|
||||||
|
import io
|
||||||
|
|
||||||
|
DB_PATH = '/opt/eim/eim.db'
|
||||||
|
DARK_BLUE = HexColor('#2F5496')
|
||||||
|
LIGHT_BLUE = HexColor('#D6E4F0')
|
||||||
|
GRAY = HexColor('#666666')
|
||||||
|
|
||||||
|
def get_device_counts(pallet_number):
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute('SELECT device_type, COUNT(*) as count FROM devices WHERE pallet_number = ? GROUP BY device_type', (pallet_number,))
|
||||||
|
counts = {row['device_type']: row['count'] for row in cur.fetchall()}
|
||||||
|
conn.close()
|
||||||
|
return counts
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def generate_cor(pallet_number):
|
||||||
|
pallet = frappe.get_doc('Pallet', pallet_number)
|
||||||
|
device_counts = get_device_counts(pallet_number)
|
||||||
|
|
||||||
|
output = io.BytesIO()
|
||||||
|
doc = SimpleDocTemplate(output, pagesize=letter, topMargin=0.75*inch, bottomMargin=0.75*inch, leftMargin=0.75*inch, rightMargin=0.75*inch)
|
||||||
|
styles = getSampleStyleSheet()
|
||||||
|
|
||||||
|
title_style = ParagraphStyle('CertTitle', parent=styles['Title'], fontSize=20, textColor=DARK_BLUE, spaceAfter=6, alignment=TA_CENTER)
|
||||||
|
body_style = ParagraphStyle('CertBody', parent=styles['Normal'], fontSize=11, spaceAfter=4)
|
||||||
|
section_style = ParagraphStyle('SectionHeader', parent=styles['Heading2'], fontSize=14, textColor=DARK_BLUE, spaceBefore=12, spaceAfter=6)
|
||||||
|
|
||||||
|
elements = []
|
||||||
|
elements.append(Paragraph('CERTIFICATE OF RECYCLING', title_style))
|
||||||
|
elements.append(Spacer(1, 6))
|
||||||
|
|
||||||
|
intro = 'Full Circle Electronics AZ, LLC (dba Westech Recyclers) certifies that the materials submitted for recycling are received and will be properly recycled in accordance with all state and federal recycling regulations and in accordance with the R2 Standard.'
|
||||||
|
elements.append(Paragraph(intro, body_style))
|
||||||
|
elements.append(Spacer(1, 8))
|
||||||
|
elements.append(Paragraph('Materials Submitted by:', section_style))
|
||||||
|
|
||||||
|
pallet_data = [
|
||||||
|
['Company:', pallet.company_name or pallet.customer_number or 'N/A'],
|
||||||
|
['Pallet Number:', pallet.pallet_number or pallet_number],
|
||||||
|
['Date Received:', str(pallet.received_date or 'N/A')],
|
||||||
|
['Weight:', str(pallet.inbound_weight or 'N/A') + ' lbs'],
|
||||||
|
['Technician:', pallet.tester or 'N/A'],
|
||||||
|
]
|
||||||
|
|
||||||
|
pallet_table = Table(pallet_data, colWidths=[1.5*inch, 4*inch])
|
||||||
|
pallet_table.setStyle(TableStyle([
|
||||||
|
('BACKGROUND', (0, 0), (0, -1), LIGHT_BLUE),
|
||||||
|
('TEXTCOLOR', (0, 0), (0, -1), DARK_BLUE),
|
||||||
|
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
|
||||||
|
('FONTSIZE', (0, 0), (-1, -1), 10),
|
||||||
|
('ALIGN', (0, 0), (0, -1), 'RIGHT'),
|
||||||
|
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
|
||||||
|
('TOPPADDING', (0, 0), (-1, -1), 4),
|
||||||
|
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
|
||||||
|
]))
|
||||||
|
elements.append(pallet_table)
|
||||||
|
elements.append(Spacer(1, 12))
|
||||||
|
|
||||||
|
if device_counts:
|
||||||
|
elements.append(Paragraph('Device Summary:', section_style))
|
||||||
|
device_data = [['Device Type', 'Count']]
|
||||||
|
for dtype, count in sorted(device_counts.items()):
|
||||||
|
device_data.append([dtype or 'Unknown', str(count)])
|
||||||
|
device_table = Table(device_data, colWidths=[3*inch, 2*inch])
|
||||||
|
device_table.setStyle(TableStyle([
|
||||||
|
('BACKGROUND', (0, 0), (-1, 0), DARK_BLUE),
|
||||||
|
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
||||||
|
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||||
|
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
|
||||||
|
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, HexColor('#F2F2F2')]),
|
||||||
|
]))
|
||||||
|
elements.append(device_table)
|
||||||
|
elements.append(Spacer(1, 12))
|
||||||
|
|
||||||
|
elements.append(HRFlowable(width='100%', thickness=1, color=DARK_BLUE, spaceBefore=12))
|
||||||
|
footer = Paragraph('Full Circle Electronics AZ, LLC | 220 S 9th St Phoenix, AZ 85034 | www.westechrecyclers.com | 602.256.7626',
|
||||||
|
ParagraphStyle('Footer', parent=styles['Normal'], fontSize=9, textColor=GRAY, alignment=TA_CENTER))
|
||||||
|
elements.append(Spacer(1, 6))
|
||||||
|
elements.append(footer)
|
||||||
|
|
||||||
|
doc.build(elements)
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
frappe.response.filename = 'COR_' + pallet_number + '.pdf'
|
||||||
|
frappe.response.filecontent = output.getvalue()
|
||||||
|
frappe.response.type = 'download'
|
||||||
|
frappe.response.display_content_as = 'attachment'
|
||||||
@@ -11,6 +11,15 @@ frappe.ui.form.on('Pallet', {
|
|||||||
'pallet': frm.doc.pallet_number || frm.doc.name
|
'pallet': frm.doc.pallet_number || frm.doc.name
|
||||||
});
|
});
|
||||||
}, __('View'));
|
}, __('View'));
|
||||||
|
|
||||||
|
frm.add_custom_button(__('Generate COR'), function() {
|
||||||
|
if (!frm.doc.pallet_number) {
|
||||||
|
frappe.msgprint('Please save the Pallet first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var url = '/api/method/westech_r2.api.cor_generator.generate_cor?pallet_number=' + encodeURIComponent(frm.doc.pallet_number);
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}, __('Actions'));
|
||||||
},
|
},
|
||||||
|
|
||||||
customer_number: function(frm) {
|
customer_number: function(frm) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ frappe.ui.form.on('Scheduled Pickup', {
|
|||||||
customer_number: function(frm) {
|
customer_number: function(frm) {
|
||||||
var customer = frm.doc.customer_number;
|
var customer = frm.doc.customer_number;
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
clear_supplier_fields(frm);
|
clear_customer_fields(frm);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
frappe.call({
|
frappe.call({
|
||||||
@@ -52,7 +52,7 @@ frappe.ui.form.on('Scheduled Pickup', {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function clear_supplier_fields(frm) {
|
function clear_customer_fields(frm) {
|
||||||
frm.set_value('company_name', '');
|
frm.set_value('company_name', '');
|
||||||
frm.set_value('contact_name', '');
|
frm.set_value('contact_name', '');
|
||||||
frm.set_value('contact_phone', '');
|
frm.set_value('contact_phone', '');
|
||||||
|
|||||||
@@ -31,3 +31,6 @@ doc_events = {
|
|||||||
"before_save": "westech_r2.doctype.load.load.calculate_totals",
|
"before_save": "westech_r2.doctype.load.load.calculate_totals",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
app_include_css = "/assets/westech_r2/css/westech_theme.css"
|
||||||
|
|||||||
+222
-433
@@ -1,11 +1,12 @@
|
|||||||
frappe.pages['intake'].on_page_load = function(wrapper) {
|
frappe.pages['intake'].on_page_load = function(wrapper) {
|
||||||
|
try {
|
||||||
var page = frappe.ui.make_app_page({
|
var page = frappe.ui.make_app_page({
|
||||||
parent: wrapper,
|
parent: wrapper,
|
||||||
title: 'Intake Station',
|
title: 'Customer Management',
|
||||||
single_column: true
|
single_column: true
|
||||||
});
|
});
|
||||||
|
|
||||||
page.set_primary_action('New Intake', function() {
|
page.set_primary_action('New Customer', function() {
|
||||||
show_intake_form();
|
show_intake_form();
|
||||||
}, 'add');
|
}, 'add');
|
||||||
|
|
||||||
@@ -18,13 +19,44 @@ frappe.pages['intake'].on_page_load = function(wrapper) {
|
|||||||
<div id="intake-form-container" style="display:none;">
|
<div id="intake-form-container" style="display:none;">
|
||||||
<div class="card" style="margin-bottom: 20px;">
|
<div class="card" style="margin-bottom: 20px;">
|
||||||
<div class="card-header" style="background: linear-gradient(135deg, #6f42c1, #28a745); color: white; padding: 15px;">
|
<div class="card-header" style="background: linear-gradient(135deg, #6f42c1, #28a745); color: white; padding: 15px;">
|
||||||
<img src="/files/COR_html_952bb51d.png" style="max-height: 32px; vertical-align: middle; margin-right: 10px;"><h4 style="margin:0; color: white; display: inline; vertical-align: middle;">New Intake</h4>
|
<h4 style="margin:0; color: white;">New Customer</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body" style="padding: 20px;">
|
<div class="card-body" style="padding: 20px;">
|
||||||
<form id="intake-form">
|
<form id="intake-form">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<h5 style="color:#6f42c1;">📅 Dates & Source</h5>
|
<h5 style="color:#6f42c1;">Customer</h5>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Customer <span class="text-danger">*</span></label>
|
||||||
|
<div id="customer-number-control"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Company Name</label>
|
||||||
|
<input type="text" id="company_name" class="form-control" readonly style="background:#f8f9fa;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Driver</label>
|
||||||
|
<div id="driver-control"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Contact Name</label>
|
||||||
|
<input type="text" id="contact_name" class="form-control" readonly style="background:#f8f9fa;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Contact #</label>
|
||||||
|
<input type="tel" id="contact_number" class="form-control" readonly style="background:#f8f9fa;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Contact Email</label>
|
||||||
|
<input type="email" id="contact_email" class="form-control" readonly style="background:#f8f9fa;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Address</label>
|
||||||
|
<input type="text" id="address_line" class="form-control" readonly style="background:#f8f9fa;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h5 style="color:#6f42c1;">Dates & Source</h5>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Received Date <span class="text-danger">*</span></label>
|
<label>Received Date <span class="text-danger">*</span></label>
|
||||||
<input type="date" id="received_date" class="form-control" required>
|
<input type="date" id="received_date" class="form-control" required>
|
||||||
@@ -33,18 +65,6 @@ frappe.pages['intake'].on_page_load = function(wrapper) {
|
|||||||
<label>Weekday</label>
|
<label>Weekday</label>
|
||||||
<input type="text" id="weekday" class="form-control" readonly style="background:#f8f9fa;">
|
<input type="text" id="weekday" class="form-control" readonly style="background:#f8f9fa;">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label>Customer Number <span class="text-danger">*</span></label>
|
|
||||||
<div id="customer-number-control"></div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Driver</label>
|
|
||||||
<div id="supplier-control"></div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Company Name</label>
|
|
||||||
<input type="text" id="company_name" class="form-control">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Pickup / Drop-off</label>
|
<label>Pickup / Drop-off</label>
|
||||||
<select id="pickup" class="form-control">
|
<select id="pickup" class="form-control">
|
||||||
@@ -54,16 +74,9 @@ frappe.pages['intake'].on_page_load = function(wrapper) {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Notes</label>
|
<label>Hours of Operation</label>
|
||||||
<textarea id="notes" class="form-control" rows="3" placeholder="Any additional notes..."></textarea>
|
<input type="text" id="hours_of_operation" class="form-control" readonly style="background:#f8f9fa;" placeholder="Auto-filled from Customer">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label>Legacy Notes</label>
|
|
||||||
<textarea id="legacy_notes" class="form-control" rows="2" style="background:#fafafa;" readonly title="Auto-filled from Customer record"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<h5 style="color:#6f42c1;">📦 Data & Labels</h5>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Data Status</label>
|
<label>Data Status</label>
|
||||||
<select id="data_status" class="form-control">
|
<select id="data_status" class="form-control">
|
||||||
@@ -79,17 +92,20 @@ frappe.pages['intake'].on_page_load = function(wrapper) {
|
|||||||
<option value="RED">RED</option>
|
<option value="RED">RED</option>
|
||||||
<option value="R2">R2</option>
|
<option value="R2">R2</option>
|
||||||
<option value="Both">Both</option>
|
<option value="Both">Both</option>
|
||||||
<option value="Neither">Neither</option>
|
<option value="Clear">Clear</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="checkbox">
|
<label>Notes</label>
|
||||||
<label><input type="checkbox" id="needs_aor"> <strong>Needs AoR</strong> <small class="text-muted">(Agreement of Recycling)</small></label>
|
<textarea id="notes" class="form-control" rows="3"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="form-group">
|
||||||
<label><input type="checkbox" id="needs_cod"> <strong>Needs COD</strong> <small class="text-muted">(Certificate of Destruction)</small></label>
|
<label>Legacy Notes</label>
|
||||||
|
<textarea id="legacy_notes" class="form-control" rows="2" readonly style="background:#fafafa;" title="Auto-filled from Customer record"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h5 style="color:#6f42c1;">Items & Weight</h5>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Barcode</label>
|
<label>Barcode</label>
|
||||||
<input type="text" id="barcode" class="form-control" placeholder="Scan barcode...">
|
<input type="text" id="barcode" class="form-control" placeholder="Scan barcode...">
|
||||||
@@ -102,33 +118,6 @@ frappe.pages['intake'].on_page_load = function(wrapper) {
|
|||||||
<label>Number of Labels</label>
|
<label>Number of Labels</label>
|
||||||
<input type="number" id="num_labels" class="form-control" value="1" min="1" max="20">
|
<input type="number" id="num_labels" class="form-control" value="1" min="1" max="20">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<h5 style="color:#6f42c1;">👤 Contact & Financial</h5>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Contact Name</label>
|
|
||||||
<input type="text" id="contact_name" class="form-control">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Contact Persons</label>
|
|
||||||
<input type="text" id="contact_persons" class="form-control" placeholder="e.g. John Doe, Jane Smith">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Contact #</label>
|
|
||||||
<input type="tel" id="contact_number" class="form-control">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Contact Email</label>
|
|
||||||
<input type="email" id="contact_email" class="form-control">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Address</label>
|
|
||||||
<input type="text" id="address_line" class="form-control">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Hours of Operation</label>
|
|
||||||
<input type="text" id="hours_of_operation" class="form-control" placeholder="e.g. Mon-Fri 8am-5pm">
|
|
||||||
</div>
|
|
||||||
<hr>
|
<hr>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Weight <span class="text-danger">*</span></label>
|
<label>Weight <span class="text-danger">*</span></label>
|
||||||
@@ -156,10 +145,13 @@ frappe.pages['intake'].on_page_load = function(wrapper) {
|
|||||||
<div class="row" style="margin-top: 20px;">
|
<div class="row" style="margin-top: 20px;">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<button type="submit" class="btn btn-primary btn-lg" style="background: linear-gradient(135deg, #6f42c1, #28a745); border: none;">
|
<button type="submit" class="btn btn-primary btn-lg" style="background: linear-gradient(135deg, #6f42c1, #28a745); border: none;">
|
||||||
<i class="fa fa-save"></i> Save Intake
|
Save Customer
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-default btn-lg" id="btn-print-labels" disabled>
|
<button type="button" class="btn btn-default btn-lg" id="btn-print-labels" disabled>
|
||||||
<i class="fa fa-print"></i> Print Labels
|
Print Labels
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-default btn-lg" id="btn-generate-cor" disabled>
|
||||||
|
CoR/AoR
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-default btn-lg" id="btn-cancel">
|
<button type="button" class="btn btn-default btn-lg" id="btn-cancel">
|
||||||
Cancel
|
Cancel
|
||||||
@@ -182,7 +174,7 @@ frappe.pages['intake'].on_page_load = function(wrapper) {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Customer #</th>
|
<th>Customer</th>
|
||||||
<th>Driver</th>
|
<th>Driver</th>
|
||||||
<th>Received</th>
|
<th>Received</th>
|
||||||
<th>RED/R2</th>
|
<th>RED/R2</th>
|
||||||
@@ -201,10 +193,19 @@ frappe.pages['intake'].on_page_load = function(wrapper) {
|
|||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Build ERPNext Link controls for Customer Number and Supplier
|
|
||||||
setup_link_controls();
|
setup_link_controls();
|
||||||
|
|
||||||
set_today_date();
|
set_today_date();
|
||||||
|
|
||||||
|
// Trigger weekday calculation after date field is set
|
||||||
|
setTimeout(function() {
|
||||||
|
var dateVal = $('#received_date').val();
|
||||||
|
if (dateVal) {
|
||||||
|
var d = new Date(dateVal + 'T12:00:00');
|
||||||
|
var days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
|
||||||
|
$('#weekday').val(days[d.getDay()] || '');
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
|
||||||
load_recent_pallets();
|
load_recent_pallets();
|
||||||
|
|
||||||
$('#received_date').on('change', function() {
|
$('#received_date').on('change', function() {
|
||||||
@@ -225,31 +226,44 @@ frappe.pages['intake'].on_page_load = function(wrapper) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#btn-print-labels').on('click', function() {
|
$('#btn-print-labels').on('click', function() {
|
||||||
print_labels();
|
frappe.msgprint('Label printing coming soon.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#btn-generate-cor').on('click', function() {
|
||||||
|
generate_cor_report();
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
console.error('[INTAKE] FATAL:', e.message, e.stack);
|
||||||
|
$(wrapper).find('.layout-main-section').html(
|
||||||
|
'<div style="padding:20px;"><h3>Error Loading Page</h3><pre>' + e.message + '</pre></div>'
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── ERPNext Link Controls ──────────────────────────────────────────────
|
// ── Link Controls ──────────────────────────────────────────────
|
||||||
|
|
||||||
var customer_number_control = null;
|
var customer_number_control = null;
|
||||||
var supplier_control = null;
|
var driver_control = null;
|
||||||
|
var selected_customer_name = null;
|
||||||
|
|
||||||
function setup_link_controls() {
|
function setup_link_controls() {
|
||||||
// Customer Number — Link to Supplier, searching by name (which IS the customer number)
|
|
||||||
customer_number_control = frappe.ui.form.make_control({
|
customer_number_control = frappe.ui.form.make_control({
|
||||||
parent: $('#customer-number-control'),
|
parent: $('#customer-number-control'),
|
||||||
df: {
|
df: {
|
||||||
fieldtype: 'Link',
|
fieldtype: 'Link',
|
||||||
fieldname: 'customer_number',
|
fieldname: 'customer_number',
|
||||||
options: 'Customer',
|
options: 'Customer',
|
||||||
label: 'Customer Number',
|
label: 'Customer',
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
placeholder: 'Search customer number...',
|
placeholder: 'Search customer...',
|
||||||
onchange: function() {
|
onchange: function() {
|
||||||
var val = customer_number_control.get_value();
|
var val = customer_number_control.get_value();
|
||||||
if (val) {
|
if (val) {
|
||||||
|
selected_customer_name = val;
|
||||||
fetch_customer_details(val);
|
fetch_customer_details(val);
|
||||||
} else {
|
} else {
|
||||||
|
selected_customer_name = null;
|
||||||
clear_customer_fields();
|
clear_customer_fields();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,31 +271,24 @@ function setup_link_controls() {
|
|||||||
only_input: true,
|
only_input: true,
|
||||||
});
|
});
|
||||||
customer_number_control.refresh();
|
customer_number_control.refresh();
|
||||||
// Style to match form
|
|
||||||
$('#customer-number-control .control-input').css('margin', '0');
|
$('#customer-number-control .control-input').css('margin', '0');
|
||||||
$('#customer-number-control .help-box').remove();
|
$('#customer-number-control .help-box').remove();
|
||||||
|
|
||||||
// Supplier (Driver) — Link to Supplier for driver name
|
driver_control = frappe.ui.form.make_control({
|
||||||
supplier_control = frappe.ui.form.make_control({
|
parent: $('#driver-control'),
|
||||||
parent: $('#supplier-control'),
|
|
||||||
df: {
|
df: {
|
||||||
fieldtype: 'Link',
|
fieldtype: 'Link',
|
||||||
fieldname: 'supplier',
|
fieldname: 'driver',
|
||||||
options: 'Customer',
|
options: 'Customer',
|
||||||
label: 'Driver',
|
label: 'Driver',
|
||||||
placeholder: 'Search driver...',
|
placeholder: 'Search driver...',
|
||||||
onchange: function() {
|
onchange: function() {}
|
||||||
var val = supplier_control.get_value();
|
|
||||||
if (val && val !== customer_number_control.get_value()) {
|
|
||||||
// Driver selected separately — don't override customer_number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
only_input: true,
|
only_input: true,
|
||||||
});
|
});
|
||||||
supplier_control.refresh();
|
driver_control.refresh();
|
||||||
$('#supplier-control .control-input').css('margin', '0');
|
$('#driver-control .control-input').css('margin', '0');
|
||||||
$('#supplier-control .help-box').remove();
|
$('#driver-control .help-box').remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetch_customer_details(customer_name) {
|
function fetch_customer_details(customer_name) {
|
||||||
@@ -291,115 +298,61 @@ function fetch_customer_details(customer_name) {
|
|||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if (r.message) {
|
if (r.message) {
|
||||||
var c = r.message;
|
var c = r.message;
|
||||||
// Auto-fill company name
|
$('#company_name').val(c.customer_name || '');
|
||||||
if (c.customer_name && !$('#company_name').val()) {
|
$('#contact_name').val(c.contact_persons || '');
|
||||||
$('#company_name').val(c.customer_name);
|
$('#hours_of_operation').val(c.hours_of_operation || '');
|
||||||
}
|
$('#legacy_notes').val(c.legacy_notes || '');
|
||||||
// Auto-fill custom CRM fields
|
|
||||||
if (c.contact_persons && !$('#contact_persons').val()) {
|
|
||||||
$('#contact_persons').val(c.contact_persons);
|
|
||||||
}
|
|
||||||
if (c.hours_of_operation && !$('#hours_of_operation').val()) {
|
|
||||||
$('#hours_of_operation').val(c.hours_of_operation);
|
|
||||||
}
|
|
||||||
if (c.legacy_notes && !$('#legacy_notes').val()) {
|
|
||||||
$('#legacy_notes').val(c.legacy_notes);
|
|
||||||
}
|
|
||||||
// Driver is independent — don't auto-populate from customer number
|
|
||||||
|
|
||||||
// Fill contact fields from Supplier doc's native fields first
|
// Parse phone and email from contact_persons if not in dedicated fields
|
||||||
// ERPNext auto-populates these from the primary Contact/Address
|
var phone = c.mobile_no || '';
|
||||||
if (c.customer_primary_contact) {
|
var email = c.email_id || '';
|
||||||
// Fetch the Contact doc for name/phone/email
|
var addressLine = '';
|
||||||
frappe.call({
|
|
||||||
method: 'frappe.client.get',
|
// Extract phone from contact_persons text blob
|
||||||
args: {doctype: 'Contact', name: c.customer_primary_contact},
|
if (!phone && c.contact_persons) {
|
||||||
callback: function(cr) {
|
var phoneMatch = c.contact_persons.match(/\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/);
|
||||||
if (cr.message) {
|
if (phoneMatch) phone = phoneMatch[0];
|
||||||
var ct = cr.message;
|
|
||||||
var full_name = [ct.first_name, ct.last_name].filter(Boolean).join(' ');
|
|
||||||
if (full_name && !$('#contact_name').val()) {
|
|
||||||
$('#contact_name').val(full_name);
|
|
||||||
}
|
}
|
||||||
if (ct.email_id && !$('#contact_email').val()) {
|
// Extract email from contact_persons text blob
|
||||||
$('#contact_email').val(ct.email_id);
|
if (!email && c.contact_persons) {
|
||||||
}
|
var emailMatch = c.contact_persons.match(/[\w.+-]+@[\w.-]+\.\w+/);
|
||||||
if (ct.phone && !$('#contact_number').val()) {
|
if (emailMatch) email = emailMatch[0];
|
||||||
$('#contact_number').val(ct.phone);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Fallback: use Supplier-level fields (mobile_no, email_id)
|
|
||||||
if (c.mobile_no && !$('#contact_number').val()) {
|
|
||||||
$('#contact_number').val(c.mobile_no);
|
|
||||||
}
|
|
||||||
if (c.email_id && !$('#contact_email').val()) {
|
|
||||||
$('#contact_email').val(c.email_id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Last resort: search for any linked Contact
|
$('#contact_number').val(phone);
|
||||||
frappe.call({
|
$('#contact_email').val(email);
|
||||||
method: 'frappe.client.get_list',
|
|
||||||
args: {
|
|
||||||
doctype: 'Contact',
|
|
||||||
filters: [['Dynamic Link', 'link_name', '=', customer_name]],
|
|
||||||
fields: ['name', 'first_name', 'last_name', 'email_id', 'phone'],
|
|
||||||
limit_page_length: 1
|
|
||||||
},
|
|
||||||
callback: function(cr) {
|
|
||||||
if (cr.message && cr.message.length > 0) {
|
|
||||||
var c = cr.message[0];
|
|
||||||
var full_name = [c.first_name, c.last_name].filter(Boolean).join(' ');
|
|
||||||
if (full_name && !$('#contact_name').val()) {
|
|
||||||
$('#contact_name').val(full_name);
|
|
||||||
}
|
|
||||||
if (c.email_id && !$('#contact_email').val()) {
|
|
||||||
$('#contact_email').val(c.email_id);
|
|
||||||
}
|
|
||||||
if (c.phone && !$('#contact_number').val()) {
|
|
||||||
$('#contact_number').val(c.phone);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill address from Supplier doc's primary_address or linked Address
|
// Get address — always fetch from Address record for full street+city+state+zip
|
||||||
if (c.primary_address && !$('#address_line').val()) {
|
if (c.customer_primary_address) {
|
||||||
$('#address_line').val(c.primary_address);
|
|
||||||
} else if (c.customer_primary_address) {
|
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: 'frappe.client.get',
|
method: 'frappe.client.get',
|
||||||
args: {doctype: 'Address', name: c.customer_primary_address},
|
args: {doctype: 'Address', name: c.customer_primary_address},
|
||||||
callback: function(ar) {
|
callback: function(r3) {
|
||||||
if (ar.message) {
|
if (r3.message) {
|
||||||
var a = ar.message;
|
var addr = r3.message;
|
||||||
var addr = [a.address_line1, a.address_line2, a.city, a.state].filter(Boolean).join(', ');
|
var line = (addr.address_line1 || '') + (addr.address_line2 ? ', ' + addr.address_line2 : '');
|
||||||
if (addr && !$('#address_line').val()) {
|
var full = line + (addr.city ? ', ' + addr.city : '') + (addr.state ? ', ' + addr.state : '') + (addr.pincode ? ' ' + addr.pincode : '');
|
||||||
$('#address_line').val(addr);
|
$('#address_line').val(full);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else if (c.primary_address) {
|
||||||
// Fallback: search for any linked Address
|
// Fallback: parse text blob (less reliable)
|
||||||
frappe.call({
|
var lines = c.primary_address.split('\n');
|
||||||
method: 'frappe.client.get_list',
|
addressLine = lines.join(', ');
|
||||||
args: {
|
$('#address_line').val(addressLine);
|
||||||
doctype: 'Address',
|
|
||||||
filters: [['Dynamic Link', 'link_name', '=', customer_name]],
|
|
||||||
fields: ['name', 'address_line1', 'city', 'state'],
|
|
||||||
limit_page_length: 1
|
|
||||||
},
|
|
||||||
callback: function(ar) {
|
|
||||||
if (ar.message && ar.message.length > 0) {
|
|
||||||
var a = ar.message[0];
|
|
||||||
var addr = [a.address_line1, a.city, a.state].filter(Boolean).join(', ');
|
|
||||||
if (addr && !$('#address_line').val()) {
|
|
||||||
$('#address_line').val(addr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If still missing phone/email, try linked Contact
|
||||||
|
if ((!phone || !email) && c.customer_primary_contact) {
|
||||||
|
frappe.call({
|
||||||
|
method: 'frappe.client.get',
|
||||||
|
args: {doctype: 'Contact', name: c.customer_primary_contact},
|
||||||
|
callback: function(r2) {
|
||||||
|
if (r2.message) {
|
||||||
|
var ct = r2.message;
|
||||||
|
if (!phone) $('#contact_number').val(ct.phone || ct.mobile_no || '');
|
||||||
|
if (!email) $('#contact_email').val(ct.email_id || '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -415,10 +368,36 @@ function clear_customer_fields() {
|
|||||||
$('#contact_number').val('');
|
$('#contact_number').val('');
|
||||||
$('#contact_email').val('');
|
$('#contact_email').val('');
|
||||||
$('#address_line').val('');
|
$('#address_line').val('');
|
||||||
if (supplier_control) supplier_control.set_value('');
|
$('#hours_of_operation').val('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Form Helpers ──────────────────────────────────────────────────────
|
function clear_form() {
|
||||||
|
if (customer_number_control) customer_number_control.set_value('');
|
||||||
|
if (driver_control) driver_control.set_value('');
|
||||||
|
$('#company_name').val('');
|
||||||
|
$('#received_date').val('');
|
||||||
|
$('#weekday').val('');
|
||||||
|
$('#pickup').val('');
|
||||||
|
$('#data_status').val('');
|
||||||
|
$('#red_r2').val('');
|
||||||
|
$('#barcode').val('');
|
||||||
|
$('#total_items').val(0);
|
||||||
|
$('#num_labels').val(1);
|
||||||
|
$('#contact_name').val('');
|
||||||
|
$('#contact_number').val('');
|
||||||
|
$('#contact_email').val('');
|
||||||
|
$('#address_line').val('');
|
||||||
|
$('#hours_of_operation').val('');
|
||||||
|
$('#legacy_notes').val('');
|
||||||
|
$('#weights').val('');
|
||||||
|
$('#invoice_check_request').val('');
|
||||||
|
$('#amount').val(0);
|
||||||
|
$('#paid_received').val('');
|
||||||
|
$('#notes').val('');
|
||||||
|
$('#btn-print-labels').prop('disabled', true);
|
||||||
|
$('#btn-generate-cor').prop('disabled', true);
|
||||||
|
$('#save-status').html('');
|
||||||
|
}
|
||||||
|
|
||||||
function set_today_date() {
|
function set_today_date() {
|
||||||
var today = new Date().toISOString().split('T')[0];
|
var today = new Date().toISOString().split('T')[0];
|
||||||
@@ -429,96 +408,6 @@ function set_today_date() {
|
|||||||
function show_intake_form() {
|
function show_intake_form() {
|
||||||
$('#intake-form-container').show();
|
$('#intake-form-container').show();
|
||||||
$('#recent-pallets').hide();
|
$('#recent-pallets').hide();
|
||||||
set_today_date();
|
|
||||||
// Focus the customer number control
|
|
||||||
setTimeout(function() {
|
|
||||||
if (customer_number_control) {
|
|
||||||
customer_number_control.$input.focus();
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clear_form() {
|
|
||||||
$('#intake-form input[type="text"], #intake-form input[type="tel"], #intake-form input[type="email"], #intake-form input[type="number"], #intake-form input[type="date"], #intake-form textarea').val('');
|
|
||||||
$('#legacy_notes').val('');
|
|
||||||
$('#intake-form select').val('');
|
|
||||||
$('#total_items').val('0');
|
|
||||||
$('#num_labels').val('1');
|
|
||||||
$('#amount').val('0');
|
|
||||||
$('#needs_aor').prop('checked', false);
|
|
||||||
$('#needs_cod').prop('checked', false);
|
|
||||||
$('#btn-print-labels').prop('disabled', true);
|
|
||||||
$('#save-status').text('');
|
|
||||||
$('#intake-form-container').data('pallet-name', '');
|
|
||||||
if (customer_number_control) customer_number_control.set_value('');
|
|
||||||
if (supplier_control) supplier_control.set_value('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function save_pallet() {
|
|
||||||
var pallet_name = $('#intake-form-container').data('pallet-name');
|
|
||||||
var is_new = !pallet_name;
|
|
||||||
|
|
||||||
var data = {
|
|
||||||
doctype: 'Pallet',
|
|
||||||
received_date: $('#received_date').val(),
|
|
||||||
customer_number: customer_number_control ? customer_number_control.get_value() : '',
|
|
||||||
customer: customer_number_control ? customer_number_control.get_value() : '',
|
|
||||||
supplier: supplier_control ? supplier_control.get_value() : '',
|
|
||||||
company_name: $('#company_name').val(),
|
|
||||||
pickup: $('#pickup').val(),
|
|
||||||
data_status: $('#data_status').val(),
|
|
||||||
red_r2: $('#red_r2').val(),
|
|
||||||
barcode: $('#barcode').val(),
|
|
||||||
total_items: parseInt($('#total_items').val()) || 0,
|
|
||||||
num_labels: parseInt($('#num_labels').val()) || 1,
|
|
||||||
contact_name: $('#contact_name').val(),
|
|
||||||
contact_persons: $('#contact_persons').val(),
|
|
||||||
contact_number: $('#contact_number').val(),
|
|
||||||
contact_email: $('#contact_email').val(),
|
|
||||||
address_line: $('#address_line').val(),
|
|
||||||
hours_of_operation: $('#hours_of_operation').val(),
|
|
||||||
legacy_notes: $('#legacy_notes').val(),
|
|
||||||
weights: $('#weights').val(),
|
|
||||||
invoice_check_request: $('#invoice_check_request').val(),
|
|
||||||
amount: parseFloat($('#amount').val()) || 0,
|
|
||||||
paid_received: $('#paid_received').val(),
|
|
||||||
needs_aor: $('#needs_aor').is(':checked') ? 1 : 0,
|
|
||||||
needs_cod: $('#needs_cod').is(':checked') ? 1 : 0,
|
|
||||||
notes: $('#notes').val(),
|
|
||||||
status: 'Received'
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!is_new) {
|
|
||||||
data.name = pallet_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
frappe.call({
|
|
||||||
method: is_new ? 'frappe.client.insert' : 'frappe.client.update',
|
|
||||||
args: { doc: data },
|
|
||||||
callback: function(r) {
|
|
||||||
if (r.message) {
|
|
||||||
var name = r.message.name;
|
|
||||||
$('#save-status').html('<span class="text-success"><i class="fa fa-check"></i> Saved as Intake ' + name + '</span>');
|
|
||||||
$('#btn-print-labels').prop('disabled', false);
|
|
||||||
if (is_new) {
|
|
||||||
$('#intake-form-container').data('pallet-name', name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: function(r) {
|
|
||||||
var msg = 'Unknown error';
|
|
||||||
if (r && r._server_messages) {
|
|
||||||
try {
|
|
||||||
var msgs = JSON.parse(r._server_messages);
|
|
||||||
if (msgs && msgs.length) {
|
|
||||||
var errObj = JSON.parse(msgs[0]);
|
|
||||||
msg = errObj.message || msg;
|
|
||||||
}
|
|
||||||
} catch(e) {}
|
|
||||||
}
|
|
||||||
$('#save-status').html('<span class="text-danger"><i class="fa fa-exclamation"></i> Error: ' + msg + '</span>');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function load_recent_pallets() {
|
function load_recent_pallets() {
|
||||||
@@ -526,7 +415,7 @@ function load_recent_pallets() {
|
|||||||
method: 'frappe.client.get_list',
|
method: 'frappe.client.get_list',
|
||||||
args: {
|
args: {
|
||||||
doctype: 'Pallet',
|
doctype: 'Pallet',
|
||||||
fields: ['name', 'pallet_number', 'status', 'customer_number', 'company_name', 'supplier', 'received_date', 'red_r2', 'total_items', 'weights', 'notes', 'needs_aor', 'needs_cod'],
|
fields: ['name', 'pallet_number', 'status', 'customer_number', 'company_name', 'driver', 'received_date', 'red_r2', 'total_items', 'weights', 'notes'],
|
||||||
limit_page_length: 20,
|
limit_page_length: 20,
|
||||||
order_by: 'creation desc'
|
order_by: 'creation desc'
|
||||||
},
|
},
|
||||||
@@ -534,7 +423,7 @@ function load_recent_pallets() {
|
|||||||
var tbody = $('#pallet-tbody');
|
var tbody = $('#pallet-tbody');
|
||||||
tbody.empty();
|
tbody.empty();
|
||||||
if (!r.message || r.message.length === 0) {
|
if (!r.message || r.message.length === 0) {
|
||||||
tbody.append('<tr><td colspan="8" class="text-center">No pallets yet</td></tr>');
|
tbody.append('<tr><td colspan="8" class="text-center">No pallets yet. Click "New Customer" to create one.</td></tr>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
r.message.forEach(function(p) {
|
r.message.forEach(function(p) {
|
||||||
@@ -550,17 +439,14 @@ function load_recent_pallets() {
|
|||||||
'<tr>' +
|
'<tr>' +
|
||||||
'<td><span class="label label-' + status_class + '">' + (p.status || 'Received') + '</span></td>' +
|
'<td><span class="label label-' + status_class + '">' + (p.status || 'Received') + '</span></td>' +
|
||||||
'<td><strong>' + (p.customer_number || '') + '</strong></td>' +
|
'<td><strong>' + (p.customer_number || '') + '</strong></td>' +
|
||||||
'<td>' + (p.supplier || '') + '</td>' +
|
'<td>' + (p.driver || '') + '</td>' +
|
||||||
'<td>' + (p.received_date || '') + '</td>' +
|
'<td>' + (p.received_date || '') + '</td>' +
|
||||||
'<td>' + (p.red_r2 || '') +
|
'<td>' + (p.red_r2 || '') + '</td>' +
|
||||||
(p.needs_aor ? ' <span class="label label-warning">AoR</span>' : '') +
|
|
||||||
(p.needs_cod ? ' <span class="label label-danger">COD</span>' : '') +
|
|
||||||
'</td>' +
|
|
||||||
'<td>' + (p.total_items || 0) + '</td>' +
|
'<td>' + (p.total_items || 0) + '</td>' +
|
||||||
'<td>' + (p.weights || '') + '</td>' +
|
'<td>' + (p.weights || '') + '</td>' +
|
||||||
'<td>' +
|
'<td>' +
|
||||||
'<button class="btn btn-xs btn-default" onclick="edit_pallet(\'' + p.name + '\')"><i class="fa fa-edit"></i></button> ' +
|
'<button class="btn btn-xs btn-default" onclick="edit_pallet(\'' + p.name + '\')"><i class="fa fa-edit"></i></button> ' +
|
||||||
'<button class="btn btn-xs btn-default" onclick="print_pallet_labels(\'' + p.name + '\')"><i class="fa fa-print"></i></button>' +
|
'<button class="btn btn-xs btn-default" onclick="window.open(\'https://eim.diagalon.com/report/data-tracking?pallet=' + p.name + '\', \'_blank\')"><i class="fa fa-file-pdf-o"></i></button>' +
|
||||||
'</td>' +
|
'</td>' +
|
||||||
'</tr>'
|
'</tr>'
|
||||||
);
|
);
|
||||||
@@ -575,13 +461,12 @@ function edit_pallet(name) {
|
|||||||
args: {doctype: 'Pallet', name: name},
|
args: {doctype: 'Pallet', name: name},
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if (r.message) {
|
if (r.message) {
|
||||||
|
show_intake_form();
|
||||||
var d = r.message;
|
var d = r.message;
|
||||||
$('#intake-form-container').show();
|
|
||||||
$('#recent-pallets').hide();
|
|
||||||
$('#intake-form-container').data('pallet-name', name);
|
$('#intake-form-container').data('pallet-name', name);
|
||||||
$('#received_date').val(d.received_date || '');
|
$('#received_date').val(d.received_date || '');
|
||||||
if (customer_number_control) customer_number_control.set_value(d.customer_number || '');
|
if (customer_number_control) customer_number_control.set_value(d.customer_number || '');
|
||||||
if (supplier_control) supplier_control.set_value(d.supplier || '');
|
if (driver_control) driver_control.set_value(d.driver || '');
|
||||||
$('#company_name').val(d.company_name || '');
|
$('#company_name').val(d.company_name || '');
|
||||||
$('#pickup').val(d.pickup || '');
|
$('#pickup').val(d.pickup || '');
|
||||||
$('#data_status').val(d.data_status || '');
|
$('#data_status').val(d.data_status || '');
|
||||||
@@ -593,180 +478,84 @@ function edit_pallet(name) {
|
|||||||
$('#contact_number').val(d.contact_number || '');
|
$('#contact_number').val(d.contact_number || '');
|
||||||
$('#contact_email').val(d.contact_email || '');
|
$('#contact_email').val(d.contact_email || '');
|
||||||
$('#address_line').val(d.address_line || '');
|
$('#address_line').val(d.address_line || '');
|
||||||
|
$('#hours_of_operation').val(d.hours_of_operation || '');
|
||||||
|
$('#legacy_notes').val(d.legacy_notes || '');
|
||||||
$('#weights').val(d.weights || '');
|
$('#weights').val(d.weights || '');
|
||||||
$('#invoice_check_request').val(d.invoice_check_request || '');
|
$('#invoice_check_request').val(d.invoice_check_request || '');
|
||||||
$('#amount').val(d.amount || 0);
|
$('#amount').val(d.amount || 0);
|
||||||
$('#paid_received').val(d.paid_received || '');
|
$('#paid_received').val(d.paid_received || '');
|
||||||
$('#needs_aor').prop('checked', d.needs_aor === 1);
|
|
||||||
$('#needs_cod').prop('checked', d.needs_cod === 1);
|
|
||||||
$('#notes').val(d.notes || '');
|
$('#notes').val(d.notes || '');
|
||||||
$('#received_date').trigger('change');
|
$('#received_date').trigger('change');
|
||||||
$('#btn-print-labels').prop('disabled', false);
|
$('#btn-print-labels').prop('disabled', false);
|
||||||
|
$('#btn-generate-cor').prop('disabled', false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function print_labels() {
|
function save_pallet() {
|
||||||
var pallet_name = $('#intake-form-container').data('pallet-name');
|
var pallet_name = $('#intake-form-container').data('pallet-name');
|
||||||
if (!pallet_name) return;
|
var doc = {
|
||||||
print_pallet_labels(pallet_name);
|
doctype: 'Pallet',
|
||||||
}
|
received_date: $('#received_date').val(),
|
||||||
|
customer_number: customer_number_control ? customer_number_control.get_value() : '',
|
||||||
|
driver: driver_control ? driver_control.get_value() : '',
|
||||||
|
company_name: $('#company_name').val(),
|
||||||
|
pickup: $('#pickup').val(),
|
||||||
|
data_status: $('#data_status').val(),
|
||||||
|
red_r2: $('#red_r2').val(),
|
||||||
|
barcode: $('#barcode').val(),
|
||||||
|
total_items: parseInt($('#total_items').val()) || 0,
|
||||||
|
num_labels: parseInt($('#num_labels').val()) || 1,
|
||||||
|
contact_name: $('#contact_name').val(),
|
||||||
|
contact_number: $('#contact_number').val(),
|
||||||
|
contact_email: $('#contact_email').val(),
|
||||||
|
address_line: $('#address_line').val(),
|
||||||
|
weights: $('#weights').val(),
|
||||||
|
invoice_check_request: $('#invoice_check_request').val(),
|
||||||
|
amount: parseFloat($('#amount').val()) || 0,
|
||||||
|
paid_received: $('#paid_received').val(),
|
||||||
|
notes: $('#notes').val(),
|
||||||
|
legacy_notes: $('#legacy_notes').val(),
|
||||||
|
hours_of_operation: $('#hours_of_operation').val(),
|
||||||
|
};
|
||||||
|
|
||||||
function print_pallet_labels(name) {
|
if (pallet_name) {
|
||||||
|
doc.name = pallet_name;
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: 'frappe.client.get',
|
method: 'frappe.client.update',
|
||||||
args: {doctype: 'Pallet', name: name},
|
args: {doc: doc},
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if (r.message) {
|
if (r.message) {
|
||||||
generate_zpl_label(r.message);
|
frappe.msgprint('Pallet updated successfully!');
|
||||||
|
$('#save-status').html('<span style="color:green;">Saved!</span>');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
frappe.call({
|
||||||
|
method: 'frappe.client.insert',
|
||||||
|
args: {doc: doc},
|
||||||
|
callback: function(r) {
|
||||||
|
if (r.message) {
|
||||||
|
$('#intake-form-container').data('pallet-name', r.message.name);
|
||||||
|
frappe.msgprint('Pallet created: ' + r.message.name);
|
||||||
|
$('#save-status').html('<span style="color:green;">Created!</span>');
|
||||||
|
$('#btn-print-labels').prop('disabled', false);
|
||||||
|
$('#btn-generate-cor').prop('disabled', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function generate_zpl_label(d) {
|
function generate_cor_report() {
|
||||||
var label_count = d.num_labels || 1;
|
var pallet_name = $('#intake-form-container').data('pallet-name');
|
||||||
var date_str = d.received_date || '';
|
if (!pallet_name) {
|
||||||
var driver = d.supplier || '';
|
frappe.msgprint('Please save the intake first before generating CoR/AoR report.');
|
||||||
var customer_num = d.customer_number || '';
|
return;
|
||||||
var red_r2 = d.red_r2 || '';
|
|
||||||
var weight = d.weights || '';
|
|
||||||
var pallet_num = d.pallet_number || d.name || '';
|
|
||||||
var qr_url = window.location.origin + '/app/pallet/' + (d.name || pallet_num);
|
|
||||||
|
|
||||||
var logo_url = window.location.origin + '/files/COR_html_952bb51d.png';
|
|
||||||
|
|
||||||
var labels_html = '';
|
|
||||||
for (var i = 1; i <= label_count; i++) {
|
|
||||||
labels_html += '<div class="label-card">';
|
|
||||||
labels_html += '<div class="label-header"><img src="' + logo_url + '" style="max-height: 36px; vertical-align: middle;"> <span style="vertical-align: middle;">WESTECH RECYCLERS</span></div>';
|
|
||||||
labels_html += '<div class="label-body">';
|
|
||||||
labels_html += '<div class="label-top">';
|
|
||||||
labels_html += '<div class="label-info">';
|
|
||||||
labels_html += '<div class="label-row"><span class="label-field">Pallet #:</span> <span class="label-value">' + pallet_num + '</span></div>';
|
|
||||||
if (customer_num) {
|
|
||||||
labels_html += '<div class="label-row"><span class="label-field">Customer #:</span> <span class="label-value">' + customer_num + '</span></div>';
|
|
||||||
}
|
}
|
||||||
labels_html += '<div class="label-row"><span class="label-field">Received:</span> <span class="label-value">' + date_str + '</span></div>';
|
window.open('https://eim.diagalon.com/report/data-tracking?pallet=' + pallet_name, '_blank');
|
||||||
labels_html += '<div class="label-row"><span class="label-field">Driver:</span> <span class="label-value">' + driver + '</span></div>';
|
|
||||||
labels_html += '<div class="label-row"><span class="label-field">Weight:</span> <span class="label-value">' + weight + '</span></div>';
|
|
||||||
labels_html += '<div class="label-row"><span class="label-field">Items:</span> <span class="label-value">' + (d.total_items || 0) + '</span></div>';
|
|
||||||
labels_html += '</div>';
|
|
||||||
labels_html += '<div class="label-qr" id="qr-' + i + '"></div>';
|
|
||||||
labels_html += '</div>';
|
|
||||||
if (red_r2) {
|
|
||||||
labels_html += '<div class="label-badge">' + red_r2 + '</div>';
|
|
||||||
}
|
|
||||||
labels_html += '<div class="label-footer">Label ' + i + ' of ' + label_count + '</div>';
|
|
||||||
labels_html += '</div></div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
var existing = document.getElementById('label-preview-overlay');
|
|
||||||
if (existing) existing.remove();
|
|
||||||
|
|
||||||
var overlay = document.createElement('div');
|
|
||||||
overlay.id = 'label-preview-overlay';
|
|
||||||
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);z-index:9999;display:flex;align-items:center;justify-content:center;';
|
|
||||||
|
|
||||||
var modal = document.createElement('div');
|
|
||||||
modal.style.cssText = 'background:#fff;border-radius:8px;padding:20px;max-height:90vh;overflow-y:auto;box-shadow:0 4px 20px rgba(0,0,0,0.3);max-width:95vw;';
|
|
||||||
|
|
||||||
var title = document.createElement('h3');
|
|
||||||
title.textContent = 'Label Preview — Pallet ' + pallet_num + ' (' + label_count + ' label' + (label_count > 1 ? 's' : '') + ')';
|
|
||||||
title.style.cssText = 'margin:0 0 15px 0;font-size:16pt;';
|
|
||||||
|
|
||||||
var btnGroup = document.createElement('div');
|
|
||||||
btnGroup.style.cssText = 'margin-bottom:15px;display:flex;gap:10px;';
|
|
||||||
|
|
||||||
var printBtn = document.createElement('button');
|
|
||||||
printBtn.className = 'btn btn-primary';
|
|
||||||
printBtn.innerHTML = '<i class="fa fa-print"></i> Print Labels';
|
|
||||||
printBtn.onclick = function() {
|
|
||||||
var pw = window.open('', '_blank');
|
|
||||||
pw.document.write('<html><head><title>Labels — Pallet ' + pallet_num + '</title>');
|
|
||||||
pw.document.write('<script src="https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js"><\/script>');
|
|
||||||
pw.document.write('<style>');
|
|
||||||
pw.document.write('@page { size: 4in 6in; margin: 0; }');
|
|
||||||
pw.document.write('body { font-family: Arial, Helvetica, sans-serif; margin: 0; padding: 5mm; }');
|
|
||||||
pw.document.write('.label-card { width: 100%; max-width: 3.5in; border: 2px solid #000; margin-bottom: 10px; page-break-inside: avoid; }');
|
|
||||||
pw.document.write('.label-header { background: #000; color: #fff; font-size: 18pt; font-weight: bold; text-align: center; padding: 6px 0; letter-spacing: 2px; }');
|
|
||||||
pw.document.write('.label-body { padding: 8px 12px; }');
|
|
||||||
pw.document.write('.label-top { display: flex; justify-content: space-between; align-items: flex-start; }');
|
|
||||||
pw.document.write('.label-info { flex: 1; }');
|
|
||||||
pw.document.write('.label-qr { flex-shrink: 0; margin-left: 10px; }');
|
|
||||||
pw.document.write('.label-qr table { border-collapse: collapse; }');
|
|
||||||
pw.document.write('.label-qr td { width: 4px; height: 4px; }');
|
|
||||||
pw.document.write('.label-row { font-size: 13pt; padding: 2px 0; }');
|
|
||||||
pw.document.write('.label-field { font-weight: bold; display: inline-block; min-width: 100px; }');
|
|
||||||
pw.document.write('.label-value { }');
|
|
||||||
pw.document.write('.label-badge { margin-top: 6px; padding: 4px 12px; background: #d9534f; color: #fff; font-size: 14pt; font-weight: bold; text-align: center; border-radius: 3px; display: inline-block; }');
|
|
||||||
pw.document.write('.label-footer { font-size: 10pt; color: #666; text-align: center; margin-top: 4px; }');
|
|
||||||
pw.document.write('@media print { body { padding: 0; } .label-card { margin-bottom: 0; page-break-after: always; } .label-card:last-child { page-break-after: auto; } }');
|
|
||||||
pw.document.write('</style></head><body>');
|
|
||||||
pw.document.write(labels_html);
|
|
||||||
pw.document.write('<script>');
|
|
||||||
pw.document.write('var qr_url = ' + JSON.stringify(qr_url) + ';');
|
|
||||||
pw.document.write('var qr = qrcode(0, "M"); qr.addData(qr_url); qr.make();');
|
|
||||||
pw.document.write('for (var i = 1; i <= ' + label_count + '; i++) { var el = document.getElementById("qr-" + i); if (el) { el.innerHTML = qr.createImgTag(3, 0); } }');
|
|
||||||
pw.document.write('setTimeout(function() { window.print(); }, 500);');
|
|
||||||
pw.document.write('<\/script>');
|
|
||||||
pw.document.write('</body></html>');
|
|
||||||
pw.document.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
var closeBtn = document.createElement('button');
|
|
||||||
closeBtn.className = 'btn btn-default';
|
|
||||||
closeBtn.textContent = 'Cancel';
|
|
||||||
closeBtn.onclick = function() {
|
|
||||||
overlay.remove();
|
|
||||||
};
|
|
||||||
|
|
||||||
btnGroup.appendChild(printBtn);
|
|
||||||
btnGroup.appendChild(closeBtn);
|
|
||||||
|
|
||||||
var previewArea = document.createElement('div');
|
|
||||||
previewArea.style.cssText = 'display:flex;flex-wrap:wrap;gap:15px;justify-content:center;';
|
|
||||||
previewArea.innerHTML = labels_html;
|
|
||||||
|
|
||||||
var previewStyles = document.createElement('style');
|
|
||||||
previewStyles.textContent =
|
|
||||||
'#label-preview-overlay .label-card { width: 4in; border: 2px solid #000; background: #fff; box-shadow: 1px 1px 6px rgba(0,0,0,0.15); }' +
|
|
||||||
'#label-preview-overlay .label-header { background: #000; color: #fff; font-size: 18pt; font-weight: bold; text-align: center; padding: 6px 0; letter-spacing: 2px; }' +
|
|
||||||
'#label-preview-overlay .label-body { padding: 8px 12px; }' +
|
|
||||||
'#label-preview-overlay .label-top { display: flex; justify-content: space-between; align-items: flex-start; }' +
|
|
||||||
'#label-preview-overlay .label-info { flex: 1; }' +
|
|
||||||
'#label-preview-overlay .label-qr { flex-shrink: 0; margin-left: 10px; }' +
|
|
||||||
'#label-preview-overlay .label-row { font-size: 13pt; padding: 2px 0; }' +
|
|
||||||
'#label-preview-overlay .label-field { font-weight: bold; display: inline-block; min-width: 100px; }' +
|
|
||||||
'#label-preview-overlay .label-badge { margin-top: 6px; padding: 4px 12px; background: #d9534f; color: #fff; font-size: 14pt; font-weight: bold; text-align: center; border-radius: 3px; display: inline-block; }' +
|
|
||||||
'#label-preview-overlay .label-footer { font-size: 10pt; color: #666; text-align: center; margin-top: 4px; }';
|
|
||||||
|
|
||||||
modal.appendChild(title);
|
|
||||||
modal.appendChild(btnGroup);
|
|
||||||
modal.appendChild(previewStyles);
|
|
||||||
modal.appendChild(previewArea);
|
|
||||||
overlay.appendChild(modal);
|
|
||||||
document.body.appendChild(overlay);
|
|
||||||
|
|
||||||
// Generate QR codes in preview
|
|
||||||
var script = document.createElement('script');
|
|
||||||
script.src = 'https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js';
|
|
||||||
script.onload = function() {
|
|
||||||
var qr = qrcode(0, 'M');
|
|
||||||
qr.addData(qr_url);
|
|
||||||
qr.make();
|
|
||||||
for (var i = 1; i <= label_count; i++) {
|
|
||||||
var el = document.getElementById('qr-' + i);
|
|
||||||
if (el) el.innerHTML = qr.createImgTag(3, 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.head.appendChild(script);
|
|
||||||
|
|
||||||
overlay.addEventListener('click', function(e) {
|
|
||||||
if (e.target === overlay) overlay.remove();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.edit_pallet = edit_pallet;
|
window.edit_pallet = edit_pallet;
|
||||||
window.print_pallet_labels = print_pallet_labels;
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Page",
|
"doctype": "Page",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"modified": "2026-05-09 15:09:48.807066",
|
"modified": "2026-05-21 18:32:29.966134",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Westech R2",
|
"module": "Westech R2",
|
||||||
"name": "intake",
|
"name": "intake",
|
||||||
@@ -16,8 +16,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"script": null,
|
"script": null,
|
||||||
"standard": "No",
|
"standard": "Yes",
|
||||||
"style": null,
|
"style": null,
|
||||||
"system_page": 0,
|
"system_page": 0,
|
||||||
"title": "Intake Station"
|
"title": "Customer Management"
|
||||||
}
|
}
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
<style>
|
|
||||||
.pallet-list-page { font-family: Helvetica Neue, Arial, sans-serif; }
|
|
||||||
.pallet-list-page h2 { color: #2F5496; margin-bottom: 16px; font-size: 20px; }
|
|
||||||
.pallet-search-box { margin-bottom: 16px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
|
||||||
.pallet-search-box input, .pallet-search-box select {
|
|
||||||
padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px;
|
|
||||||
}
|
|
||||||
.pallet-search-box button {
|
|
||||||
padding: 6px 14px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer;
|
|
||||||
font-size: 13px; background: white;
|
|
||||||
}
|
|
||||||
.pallet-search-box button.btn-primary { background: #2F5496; color: white; border-color: #2F5496; }
|
|
||||||
.pallet-search-box button.btn-success { background: #548235; color: white; border-color: #548235; }
|
|
||||||
.pallet-table-container { padding: 0; overflow-x: auto; background: white; border-radius: 4px; }
|
|
||||||
.pallet-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
||||||
.pallet-table th {
|
|
||||||
background: #2F5496; color: white; padding: 8px 10px; text-align: left;
|
|
||||||
cursor: pointer; white-space: nowrap; font-weight: 500;
|
|
||||||
}
|
|
||||||
.pallet-table td { padding: 5px 10px; border-bottom: 1px solid #eee; }
|
|
||||||
.pallet-table tr:hover { background: #f5f5f5; }
|
|
||||||
.pallet-table tr.new-row { background: #FFF9E6 !important; }
|
|
||||||
.pallet-table a { color: #2F5496; text-decoration: none; font-weight: 600; }
|
|
||||||
.status-received { color: #2196F3; font-weight: 600; }
|
|
||||||
.status-sorting { color: #FF9800; font-weight: 600; }
|
|
||||||
.status-processing { color: #9C27B0; font-weight: 600; }
|
|
||||||
.status-complete { color: #4CAF50; font-weight: 600; }
|
|
||||||
.status-shipped { color: #607D8B; font-weight: 600; }
|
|
||||||
.pallet-pagination { margin-top: 12px; display: flex; gap: 5px; align-items: center; }
|
|
||||||
.pallet-pagination button {
|
|
||||||
padding: 5px 12px; border: 1px solid #ddd; background: white; cursor: pointer;
|
|
||||||
border-radius: 4px; font-size: 13px; color: #2F5496;
|
|
||||||
}
|
|
||||||
.pallet-pagination button.active { background: #2F5496; color: white; border-color: #2F5496; }
|
|
||||||
.pallet-pagination button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
||||||
.pallet-count { margin-left: auto; color: #666; font-size: 13px; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="pallet-list-page">
|
|
||||||
<h2>📦 Pallet List</h2>
|
|
||||||
|
|
||||||
<div class="pallet-search-box">
|
|
||||||
<input type="text" id="pallet-search" placeholder="Search pallet #..." style="max-width:200px;">
|
|
||||||
<select id="status-filter">
|
|
||||||
<option value="">All Statuses</option>
|
|
||||||
<option value="Received">Received</option>
|
|
||||||
<option value="Sorting">Sorting</option>
|
|
||||||
<option value="Processing">Processing</option>
|
|
||||||
<option value="Complete">Complete</option>
|
|
||||||
<option value="Shipped">Shipped</option>
|
|
||||||
</select>
|
|
||||||
<button class="btn btn-default" id="btn-clear">✕ Clear</button>
|
|
||||||
<button class="btn btn-success" id="btn-export">⬇ Export CSV</button>
|
|
||||||
<span class="pallet-count" id="pallet-count"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pallet-table-container">
|
|
||||||
<table class="pallet-table" id="pallet-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th data-sort="pallet_number">Pallet # ⇅</th>
|
|
||||||
<th data-sort="date_reserved">Date Reserved ⇅</th>
|
|
||||||
<th data-sort="received_date">Rec. Date ⇅</th>
|
|
||||||
<th data-sort="customer_number">Customer # ⇅</th>
|
|
||||||
<th data-sort="inbound_weight">Lbs ⇅</th>
|
|
||||||
<th data-sort="tester">Who ⇅</th>
|
|
||||||
<th data-sort="description">Items Tested ⇅</th>
|
|
||||||
<th data-sort="qty_to_sales">QTY Sale ⇅</th>
|
|
||||||
<th data-sort="weight_to_sales">Lbs Sale ⇅</th>
|
|
||||||
<th data-sort="finish_date">Finish Date ⇅</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Notes</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="pallet-tbody">
|
|
||||||
<tr><td colspan="12" style="text-align:center;padding:40px;color:#666;">Loading...</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pallet-pagination" id="pallet-pagination"></div>
|
|
||||||
</div>
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
frappe.pages["pallet-list"].on_page_load = function(wrapper) {
|
|
||||||
var page = frappe.ui.make_app_page({
|
|
||||||
parent: wrapper,
|
|
||||||
title: __("Pallet List"),
|
|
||||||
single_column: true
|
|
||||||
});
|
|
||||||
|
|
||||||
var content = frappe.render_template("pallet-list", {});
|
|
||||||
$(page.body).append(content);
|
|
||||||
|
|
||||||
var currentPage = 1;
|
|
||||||
var pageSize = 100;
|
|
||||||
var currentSort = "pallet_number";
|
|
||||||
var sortDir = "desc";
|
|
||||||
var searchTerm = "";
|
|
||||||
var statusFilter = "";
|
|
||||||
|
|
||||||
function loadPallets() {
|
|
||||||
frappe.call({
|
|
||||||
method: "westech_r2.page.pallet-list.pallet-list.get_pallets",
|
|
||||||
args: {
|
|
||||||
page: currentPage,
|
|
||||||
page_size: pageSize,
|
|
||||||
sort_field: currentSort,
|
|
||||||
sort_dir: sortDir,
|
|
||||||
status_filter: statusFilter,
|
|
||||||
search: searchTerm
|
|
||||||
},
|
|
||||||
callback: function(r) {
|
|
||||||
if (r.message) {
|
|
||||||
renderPallets(r.message.pallets, r.message.total);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPallets(pallets, total) {
|
|
||||||
var tbody = $("#pallet-tbody");
|
|
||||||
tbody.empty();
|
|
||||||
|
|
||||||
if (!pallets || pallets.length === 0) {
|
|
||||||
tbody.append('<tr><td colspan="13" style="text-align:center;padding:40px;">No pallets found</td></tr>');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pallets.forEach(function(p) {
|
|
||||||
var statusClass = "status-" + (p.status || "").toLowerCase().replace(/\s+/g, "-");
|
|
||||||
var link = "/app/pallet/" + encodeURIComponent(p.name);
|
|
||||||
var pn = (p.pallet_number || "").replace(/</g, "<").replace(/>/g, ">");
|
|
||||||
|
|
||||||
var row = '<tr>' +
|
|
||||||
'<td><a href="' + link + '" style="color:#3cc062;text-decoration:none;font-weight:600;">' + pn + '</a></td>' +
|
|
||||||
'<td>' + fmtDate(p.date_reserved) + '</td>' +
|
|
||||||
'<td>' + fmtDate(p.received_date) + '</td>' +
|
|
||||||
'<td>' + (p.customer_number || "") + '</td>' +
|
|
||||||
'<td>' + (p.company_name || "") + '</td>' +
|
|
||||||
'<td>' + (p.inbound_weight || "") + '</td>' +
|
|
||||||
'<td>' + (p.tester || "") + '</td>' +
|
|
||||||
'<td>' + (p.description || "") + '</td>' +
|
|
||||||
'<td>' + (p.qty_to_sales || "") + '</td>' +
|
|
||||||
'<td>' + (p.weight_to_sales || "") + '</td>' +
|
|
||||||
'<td>' + fmtDate(p.finish_date) + '</td>' +
|
|
||||||
'<td class="' + statusClass + '">' + (p.status || "") + '</td>' +
|
|
||||||
'<td>' + (p.notes || "").substring(0, 50) + '</td>' +
|
|
||||||
'</tr>';
|
|
||||||
tbody.append(row);
|
|
||||||
});
|
|
||||||
|
|
||||||
renderPagination(total);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPagination(total) {
|
|
||||||
var totalPages = Math.ceil(total / pageSize);
|
|
||||||
var pagination = $("#pallet-pagination");
|
|
||||||
pagination.empty();
|
|
||||||
|
|
||||||
if (totalPages <= 1) return;
|
|
||||||
|
|
||||||
var prevBtn = $('<button>« Prev</button>').attr("disabled", currentPage === 1)
|
|
||||||
.css({padding: "5px 12px", border: "1px solid #ddd", background: "white", cursor: "pointer", marginRight: "5px"})
|
|
||||||
.on("click", function() {
|
|
||||||
if (currentPage > 1) { currentPage--; loadPallets(); }
|
|
||||||
});
|
|
||||||
pagination.append(prevBtn);
|
|
||||||
|
|
||||||
var startPage = Math.max(1, currentPage - 2);
|
|
||||||
var endPage = Math.min(totalPages, startPage + 4);
|
|
||||||
|
|
||||||
for (var i = startPage; i <= endPage; i++) {
|
|
||||||
var btn = $('<button>' + i + '</button>')
|
|
||||||
.css({padding: "5px 12px", border: "1px solid #ddd", background: i === currentPage ? "#3cc062" : "white",
|
|
||||||
color: i === currentPage ? "white" : "#333", cursor: "pointer", marginRight: "5px"})
|
|
||||||
.on("click", function(page) {
|
|
||||||
return function() { currentPage = page; loadPallets(); };
|
|
||||||
}(i));
|
|
||||||
pagination.append(btn);
|
|
||||||
}
|
|
||||||
|
|
||||||
var nextBtn = $('<button>Next »</button>').attr("disabled", currentPage === totalPages)
|
|
||||||
.css({padding: "5px 12px", border: "1px solid #ddd", background: "white", cursor: "pointer", marginRight: "5px"})
|
|
||||||
.on("click", function() {
|
|
||||||
if (currentPage < totalPages) { currentPage++; loadPallets(); }
|
|
||||||
});
|
|
||||||
pagination.append(nextBtn);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fmtDate(v) {
|
|
||||||
if (!v) return "";
|
|
||||||
var s = String(v);
|
|
||||||
if (s.indexOf("T") > -1) s = s.split("T")[0];
|
|
||||||
if (s.indexOf(" ") > -1) s = s.split(" ")[0];
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#pallet-search").on("input", function() {
|
|
||||||
searchTerm = $(this).val();
|
|
||||||
currentPage = 1;
|
|
||||||
loadPallets();
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#status-filter").on("change", function() {
|
|
||||||
statusFilter = $(this).val();
|
|
||||||
currentPage = 1;
|
|
||||||
loadPallets();
|
|
||||||
});
|
|
||||||
|
|
||||||
loadPallets();
|
|
||||||
};
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
content: null,
|
|
||||||
creation: 2026-05-19 13:00:00.000000,
|
|
||||||
docstatus: 0,
|
|
||||||
doctype: Page,
|
|
||||||
idx: 0,
|
|
||||||
modified: 2026-05-19 13:00:00.000000,
|
|
||||||
modified_by: Administrator,
|
|
||||||
module: Westech R2,
|
|
||||||
name: pallet-list,
|
|
||||||
owner: Administrator,
|
|
||||||
page_name: pallet-list,
|
|
||||||
roles: [],
|
|
||||||
script: null,
|
|
||||||
standard: Yes,
|
|
||||||
style: null,
|
|
||||||
system_page: 0,
|
|
||||||
title: Pallet List
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import frappe
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def get_pallets(page=1, page_size=100, sort_field="pallet_number", sort_dir="desc", status_filter="", search=""):
|
|
||||||
page = int(page)
|
|
||||||
page_size = int(page_size)
|
|
||||||
offset = (page - 1) * page_size
|
|
||||||
|
|
||||||
conditions = [
|
|
||||||
"pallet_number IS NOT NULL",
|
|
||||||
"pallet_number != ''",
|
|
||||||
"date_reserved IS NOT NULL",
|
|
||||||
"customer_number IS NOT NULL",
|
|
||||||
"customer_number != ''"
|
|
||||||
]
|
|
||||||
|
|
||||||
junk = ["", "0", "0000", "N/A", "TBD", "null", "999990", "999995"]
|
|
||||||
junk_list = "', '".join(junk)
|
|
||||||
conditions.append("pallet_number NOT IN ('" + junk_list + "')")
|
|
||||||
conditions.append("pallet_number NOT LIKE '999%'")
|
|
||||||
conditions.append("pallet_number REGEXP '^[0-9]'")
|
|
||||||
|
|
||||||
if status_filter:
|
|
||||||
conditions.append("status = '" + frappe.db.escape(status_filter) + "'")
|
|
||||||
if search:
|
|
||||||
conditions.append("pallet_number LIKE '%" + frappe.db.escape(search) + "%'")
|
|
||||||
|
|
||||||
where_clause = " AND ".join(conditions)
|
|
||||||
|
|
||||||
total = frappe.db.sql("SELECT COUNT(*) FROM tabPallet WHERE " + where_clause)[0][0]
|
|
||||||
|
|
||||||
pallets = frappe.db.sql("SELECT name, pallet_number, date_reserved, received_date, customer_number, company_name, inbound_weight, tester, description, qty_to_sales, weight_to_sales, finish_date, notes, status FROM tabPallet WHERE " + where_clause + " ORDER BY CAST(pallet_number AS UNSIGNED) " + sort_dir + " LIMIT " + str(page_size) + " OFFSET " + str(offset), as_dict=True)
|
|
||||||
|
|
||||||
empty_pallet = frappe.db.sql("SELECT name, pallet_number, date_reserved, received_date, customer_number, company_name, inbound_weight, tester, description, qty_to_sales, weight_to_sales, finish_date, notes, status FROM tabPallet WHERE date_reserved IS NULL AND (customer_number IS NULL OR customer_number = '') AND pallet_number NOT IN ('" + junk_list + "') AND pallet_number NOT LIKE '999%' AND pallet_number REGEXP '^[0-9]' ORDER BY CAST(pallet_number AS UNSIGNED) DESC LIMIT 1", as_dict=True)
|
|
||||||
|
|
||||||
result = []
|
|
||||||
if empty_pallet:
|
|
||||||
empty_pallet[0]["_is_new"] = True
|
|
||||||
result.append(empty_pallet[0])
|
|
||||||
result.extend(pallets)
|
|
||||||
|
|
||||||
return {"pallets": result, "total": total, "page": page, "page_size": page_size}
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def update_pallet(docname, field, value):
|
|
||||||
pallet = frappe.get_doc("Pallet", docname)
|
|
||||||
pallet.set(field, value)
|
|
||||||
pallet.save(ignore_permissions=True)
|
|
||||||
frappe.db.commit()
|
|
||||||
return {"status": "ok", "message": "Updated " + field}
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def create_pallet(data):
|
|
||||||
data = frappe.parse_json(data)
|
|
||||||
pallet = frappe.new_doc("Pallet")
|
|
||||||
pallet.pallet_number = data.get("pallet_number")
|
|
||||||
pallet.status = data.get("status", "Received")
|
|
||||||
pallet.date_reserved = data.get("date_reserved")
|
|
||||||
for field, value in data.items():
|
|
||||||
if field not in ["pallet_number", "status", "date_reserved"] and value:
|
|
||||||
pallet.set(field, value)
|
|
||||||
pallet.insert(ignore_permissions=True)
|
|
||||||
frappe.db.commit()
|
|
||||||
return {"status": "ok", "name": pallet.name}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<style>
|
<style>
|
||||||
.pallet-list-page { font-family: Helvetica Neue, Arial, sans-serif; }
|
.pallet-list-page { font-family: Helvetica Neue, Arial, sans-serif; }
|
||||||
.pallet-list-page h2 { color: #2F5496; margin-bottom: 16px; font-size: 20px; }
|
.pallet-list-page h2 { color: #6f42c1; margin-bottom: 16px; font-size: 20px; }
|
||||||
.pallet-search-box { margin-bottom: 16px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
.pallet-search-box { margin-bottom: 16px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
||||||
.pallet-search-box input, .pallet-search-box select {
|
.pallet-search-box input, .pallet-search-box select {
|
||||||
padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px;
|
padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px;
|
||||||
@@ -9,18 +9,18 @@
|
|||||||
padding: 6px 14px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer;
|
padding: 6px 14px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer;
|
||||||
font-size: 13px; background: white;
|
font-size: 13px; background: white;
|
||||||
}
|
}
|
||||||
.pallet-search-box button.btn-primary { background: #2F5496; color: white; border-color: #2F5496; }
|
.pallet-search-box button.btn-primary { background: #6f42c1; color: white; border-color: #6f42c1; }
|
||||||
.pallet-search-box button.btn-success { background: #548235; color: white; border-color: #548235; }
|
.pallet-search-box button.btn-success { background: #28a745; color: white; border-color: #28a745; }
|
||||||
.pallet-table-container { padding: 0; overflow-x: auto; background: white; border-radius: 4px; }
|
.pallet-table-container { padding: 0; overflow-x: auto; background: white; border-radius: 4px; }
|
||||||
.pallet-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
.pallet-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||||
.pallet-table th {
|
.pallet-table th {
|
||||||
background: #2F5496; color: white; padding: 8px 10px; text-align: left;
|
background: #6f42c1; color: white; padding: 8px 10px; text-align: left;
|
||||||
cursor: pointer; white-space: nowrap; font-weight: 500;
|
cursor: pointer; white-space: nowrap; font-weight: 500;
|
||||||
}
|
}
|
||||||
.pallet-table td { padding: 5px 10px; border-bottom: 1px solid #eee; }
|
.pallet-table td { padding: 5px 10px; border-bottom: 1px solid #eee; }
|
||||||
.pallet-table tr:hover { background: #f5f5f5; }
|
.pallet-table tr:hover { background: #f5f5f5; }
|
||||||
.pallet-table tr.new-row { background: #FFF9E6 !important; }
|
.pallet-table tr.new-row { background: #FFF9E6 !important; }
|
||||||
.pallet-table a { color: #2F5496; text-decoration: none; font-weight: 600; }
|
.pallet-table a { color: #6f42c1; text-decoration: none; font-weight: 600; }
|
||||||
.status-received { color: #2196F3; font-weight: 600; }
|
.status-received { color: #2196F3; font-weight: 600; }
|
||||||
.status-sorting { color: #FF9800; font-weight: 600; }
|
.status-sorting { color: #FF9800; font-weight: 600; }
|
||||||
.status-processing { color: #9C27B0; font-weight: 600; }
|
.status-processing { color: #9C27B0; font-weight: 600; }
|
||||||
@@ -29,9 +29,9 @@
|
|||||||
.pallet-pagination { margin-top: 12px; display: flex; gap: 5px; align-items: center; }
|
.pallet-pagination { margin-top: 12px; display: flex; gap: 5px; align-items: center; }
|
||||||
.pallet-pagination button {
|
.pallet-pagination button {
|
||||||
padding: 5px 12px; border: 1px solid #ddd; background: white; cursor: pointer;
|
padding: 5px 12px; border: 1px solid #ddd; background: white; cursor: pointer;
|
||||||
border-radius: 4px; font-size: 13px; color: #2F5496;
|
border-radius: 4px; font-size: 13px; color: #6f42c1;
|
||||||
}
|
}
|
||||||
.pallet-pagination button.active { background: #2F5496; color: white; border-color: #2F5496; }
|
.pallet-pagination button.active { background: #6f42c1; color: white; border-color: #6f42c1; }
|
||||||
.pallet-pagination button:disabled { opacity: 0.5; cursor: not-allowed; }
|
.pallet-pagination button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
.pallet-count { margin-left: auto; color: #666; font-size: 13px; }
|
.pallet-count { margin-left: auto; color: #666; font-size: 13px; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ frappe.pages["pallet-list"].on_page_load = function(wrapper) {
|
|||||||
|
|
||||||
function loadPallets() {
|
function loadPallets() {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "westech_r2.page.pallet-list.pallet-list.get_pallets",
|
method: "westech_r2.page.pallet_list.pallet_list.get_pallets",
|
||||||
args: {
|
args: {
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
page_size: pageSize,
|
page_size: pageSize,
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
content: null,
|
"content": null,
|
||||||
creation: 2026-05-19 13:00:00.000000,
|
"creation": "2026-05-19 13:00:00.000000",
|
||||||
docstatus: 0,
|
"docstatus": 0,
|
||||||
doctype: Page,
|
"doctype": "Page",
|
||||||
idx: 0,
|
"idx": 0,
|
||||||
modified: 2026-05-19 13:00:00.000000,
|
"modified": "2026-05-19 13:00:00.000000",
|
||||||
modified_by: Administrator,
|
"modified_by": "Administrator",
|
||||||
module: Westech R2,
|
"module": "Westech R2",
|
||||||
name: pallet-list,
|
"name": "pallet-list",
|
||||||
owner: Administrator,
|
"owner": "Administrator",
|
||||||
page_name: pallet-list,
|
"page_name": "pallet-list",
|
||||||
roles: [],
|
"roles": [],
|
||||||
script: null,
|
"script": null,
|
||||||
standard: Yes,
|
"standard": "Yes",
|
||||||
style: null,
|
"style": null,
|
||||||
system_page: 0,
|
"system_page": 0,
|
||||||
title: Pallet List
|
"title": "Pallet List"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,10 +27,6 @@ frappe.pages['receiving'].on_page_load = function(wrapper) {
|
|||||||
<div class="panel-heading">📅 Pickup Calendar — Next 30 Days</div>
|
<div class="panel-heading">📅 Pickup Calendar — Next 30 Days</div>
|
||||||
<div class="panel-body" id="pickup-calendar"><div class="text-muted text-center">Loading...</div></div>
|
<div class="panel-body" id="pickup-calendar"><div class="text-muted text-center">Loading...</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-info">
|
|
||||||
<div class="panel-heading">📊 Weekly Pickup Volume</div>
|
|
||||||
<div class="panel-body" style="padding: 5px;"><canvas id="weekly-chart" height="180"></canvas></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
@@ -268,7 +264,7 @@ frappe.pages['receiving'].on_page_load = function(wrapper) {
|
|||||||
if (r.message) {
|
if (r.message) {
|
||||||
renderPickupTable(r.message.pickups || []);
|
renderPickupTable(r.message.pickups || []);
|
||||||
renderCalendar(r.message.calendar || []);
|
renderCalendar(r.message.calendar || []);
|
||||||
renderWeeklyChart(r.message.weekly || []);
|
// weekly chart removed
|
||||||
$("#pickup-count-label").text((r.message.pickups || []).length + " pickups");
|
$("#pickup-count-label").text((r.message.pickups || []).length + " pickups");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,34 +330,6 @@ frappe.pages['receiving'].on_page_load = function(wrapper) {
|
|||||||
el.html(h);
|
el.html(h);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderWeeklyChart(weekly) {
|
|
||||||
var canvas = document.getElementById("weekly-chart");
|
|
||||||
if (!canvas || !weekly || !weekly.length) return;
|
|
||||||
var ctx = canvas.getContext("2d");
|
|
||||||
var W = canvas.width = canvas.parentElement.clientWidth - 20;
|
|
||||||
var H = canvas.height = 180;
|
|
||||||
ctx.clearRect(0, 0, W, H);
|
|
||||||
var maxVal = Math.max.apply(null, weekly.map(function(w) { return w.count; }));
|
|
||||||
if (maxVal < 1) maxVal = 1;
|
|
||||||
var barW = Math.min(50, (W - 40) / weekly.length - 8);
|
|
||||||
var startX = 40;
|
|
||||||
var barArea = H - 40;
|
|
||||||
weekly.forEach(function(w, i) {
|
|
||||||
var x = startX + i * ((W - 60) / weekly.length);
|
|
||||||
var barH = (w.count / maxVal) * barArea;
|
|
||||||
var y = H - 30 - barH;
|
|
||||||
ctx.fillStyle = "#2F5496";
|
|
||||||
ctx.fillRect(x, y, barW, barH);
|
|
||||||
ctx.fillStyle = "#333";
|
|
||||||
ctx.font = "11px sans-serif";
|
|
||||||
ctx.textAlign = "center";
|
|
||||||
ctx.fillText(w.count, x + barW / 2, y - 4);
|
|
||||||
ctx.fillStyle = "#999";
|
|
||||||
ctx.font = "10px sans-serif";
|
|
||||||
ctx.fillText(w.label, x + barW / 2, H - 10);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Stage A: New Pickup ──
|
// ── Stage A: New Pickup ──
|
||||||
$("#btn-new-pickup").on("click", function() {
|
$("#btn-new-pickup").on("click", function() {
|
||||||
$("#new-pickup-form").show();
|
$("#new-pickup-form").show();
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/* Westech R2 Theme */
|
||||||
|
:root {
|
||||||
|
--westech-gradient: linear-gradient(135deg, #6f42c1, #28a745);
|
||||||
|
--westech-gradient-hover: linear-gradient(135deg, #5a32a3, #1e7e34);
|
||||||
|
--westech-purple: #6f42c1;
|
||||||
|
--westech-green: #28a745;
|
||||||
|
--westech-gray-fill: #f3f4f6;
|
||||||
|
--westech-gray-border: #e5e7eb;
|
||||||
|
}
|
||||||
|
.navbar { background: #ffffff !important; border-bottom: 1px solid #e5e7eb !important; }
|
||||||
|
.form-section .section-head, .form-dashboard-section .section-head {
|
||||||
|
background: linear-gradient(135deg, #6f42c1, #28a745) !important;
|
||||||
|
color: #ffffff !important; font-weight: 600 !important;
|
||||||
|
padding: 10px 15px !important; border-radius: 6px 6px 0 0 !important; margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
.form-section .section-head .collapse-indicator { color: #ffffff !important; }
|
||||||
|
.form-section .section-head::before, .form-dashboard-section .section-head::before {
|
||||||
|
content: ''; display: inline-block; width: 32px; height: 32px;
|
||||||
|
margin-right: 8px; vertical-align: middle;
|
||||||
|
background-image: url('/files/COR_html_952bb51d.png');
|
||||||
|
background-size: contain; background-repeat: no-repeat; background-position: center;
|
||||||
|
}
|
||||||
|
.card-header { background: linear-gradient(135deg, #6f42c1, #28a745) !important; color: #ffffff !important; padding: 12px 15px !important; border-bottom: none !important; }
|
||||||
|
.card-header h4, .card-header h5, .card-header h6, .card-header .card-title { color: #ffffff !important; }
|
||||||
|
.btn-primary, .btn-primary:not(.btn-default) {
|
||||||
|
background: linear-gradient(135deg, #6f42c1, #28a745) !important;
|
||||||
|
border: none !important; color: #ffffff !important; border-radius: 6px !important;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.06) !important;
|
||||||
|
}
|
||||||
|
.btn-primary:hover, .btn-primary:focus { background: linear-gradient(135deg, #5a32a3, #1e7e34) !important; color: #ffffff !important; }
|
||||||
|
.btn-default, .btn-secondary { background-color: #f3f4f6 !important; color: #374151 !important; border: 1px solid #e5e7eb !important; border-radius: 6px !important; }
|
||||||
|
.frappe-control input.form-control, .frappe-control textarea.form-control, .form-control {
|
||||||
|
background-color: #f3f4f6 !important; border: 1px solid #e5e7eb !important; border-radius: 6px !important;
|
||||||
|
}
|
||||||
|
.frappe-control input.form-control:focus, .form-control:focus {
|
||||||
|
border-color: #6f42c1 !important; box-shadow: 0 0 0 0.2rem rgba(111,66,193,0.25) !important; background-color: #ffffff !important;
|
||||||
|
}
|
||||||
|
.sidebar-item, .sidebar-item a { color: #4b5563 !important; }
|
||||||
|
.sidebar-item:hover, .sidebar-item:hover a { color: #171717 !important; }
|
||||||
|
.sidebar-item.active, .sidebar-item.active a { color: #171717 !important; }
|
||||||
|
input[type="checkbox"]:checked { accent-color: #6f42c1 !important; }
|
||||||
|
.progress-bar { background: linear-gradient(135deg, #6f42c1, #28a745) !important; }
|
||||||
|
.modal-header { border-bottom: 2px solid transparent; border-image: linear-gradient(135deg, #6f42c1, #28a745) 1 !important; }
|
||||||
|
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link { color: #6f42c1 !important; border-bottom-color: #6f42c1 !important; }
|
||||||
|
.datatable .dt-header { background: linear-gradient(180deg, rgba(111,66,193,0.06), rgba(40,167,69,0.04)) !important; }
|
||||||
Reference in New Issue
Block a user