first push message
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
from . import controllers
|
||||
from . import models
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
'name': "cpp_entry",
|
||||
|
||||
'summary': "Short (1 phrase/line) summary of the module's purpose",
|
||||
|
||||
'description': """
|
||||
Long description of module's purpose
|
||||
""",
|
||||
|
||||
'author': "My Company",
|
||||
'website': "https://www.yourcompany.com",
|
||||
|
||||
# Categories can be used to filter modules in modules listing
|
||||
# Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml
|
||||
# for the full list
|
||||
'category': 'Uncategorized',
|
||||
'version': '0.1',
|
||||
|
||||
# any module necessary for this one to work correctly
|
||||
'depends': ['base','web','address_kh','mail','portal','website','youth_and_scholarship'],
|
||||
|
||||
# always loaded
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'security/portal_rules.xml',
|
||||
'views/portal_templates.xml',
|
||||
'views/dashboard_voter.xml',
|
||||
'views/portal_menu.xml',
|
||||
'data/gender_data.xml',
|
||||
'views/name_party.xml',
|
||||
'views/party_list.xml',
|
||||
'views/party_voters_detail.xml',
|
||||
],
|
||||
# only loaded in demonstration mode
|
||||
'assets': {
|
||||
'web.assets_frontend': [
|
||||
'cpp_entry/static/src/scss/cpp_portal.scss',
|
||||
'cpp_entry/static/src/js/cpp_portal.js',
|
||||
'cpp_entry/static/src/js/location_cascade.js',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': False,
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from . import portal
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,660 @@
|
||||
import base64
|
||||
import datetime
|
||||
import pytz
|
||||
from datetime import datetime
|
||||
from odoo import http, _,fields
|
||||
from odoo.http import request
|
||||
from odoo.addons.portal.controllers.portal import CustomerPortal
|
||||
from odoo.exceptions import AccessError, ValidationError
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CppPortal(CustomerPortal):
|
||||
|
||||
@http.route(['/my/cpp_entries'], type='http', auth="user", website=True)
|
||||
def portal_cpp_entries(self, **kw):
|
||||
"""List all CPP entries for current portal user"""
|
||||
domain = [('create_uid', '=', request.env.user.id)]
|
||||
entries = request.env['cpp.entry'].search(domain, order='create_date desc')
|
||||
|
||||
return request.render('cpp_entry.portal_my_cpp_entries', {
|
||||
'entries': entries,
|
||||
'page_name': 'cpp_entries',
|
||||
'error': kw.get('error'),
|
||||
})
|
||||
|
||||
@http.route(['/my/cpp_entries/new', '/my/cpp_entries/<int:entry_id>'],
|
||||
type='http', auth="user", website=True)
|
||||
def portal_cpp_form(self, entry_id=None, **kw):
|
||||
if entry_id:
|
||||
entry = request.env['cpp.entry'].browse(int(entry_id))
|
||||
if not entry.exists():
|
||||
return request.redirect('/my/cpp_entries?error=Entry+not+found')
|
||||
if entry.create_uid != request.env.user and not request.env.user.has_group('base.group_user'):
|
||||
raise AccessError(_("Access denied"))
|
||||
else:
|
||||
entry = request.env['cpp.entry']
|
||||
|
||||
# Load provinces
|
||||
provinces = request.env['address.address'].sudo().search([
|
||||
('parent_location', '=', False)
|
||||
], order='location_name asc')
|
||||
|
||||
# Load districts for selected province
|
||||
districts = request.env['address.address']
|
||||
if entry.province_id:
|
||||
districts = request.env['address.address'].sudo().search([
|
||||
('parent_location', '=', entry.province_id.id)
|
||||
], order='location_name asc')
|
||||
|
||||
# Load communes for selected district
|
||||
communes = request.env['address.address']
|
||||
if entry.district_id:
|
||||
communes = request.env['address.address'].sudo().search([
|
||||
('parent_location', '=', entry.district_id.id)
|
||||
], order='location_name asc')
|
||||
|
||||
# SEPARATE VOTERS BY STATUS
|
||||
voters_not_voted = request.env['info.voter']
|
||||
voters_voted = request.env['info.voter']
|
||||
|
||||
if entry and entry.exists():
|
||||
# Get all voters for this entry
|
||||
all_voters = request.env['info.voter'].search([
|
||||
('cpp_entry', '=', entry.id)
|
||||
], order='name asc')
|
||||
|
||||
# Separate into two lists
|
||||
voters_not_voted = all_voters.filtered(lambda v: not v.status_vote)
|
||||
voters_voted = all_voters.filtered(lambda v: v.status_vote)
|
||||
|
||||
return request.render('cpp_entry.portal_cpp_form', {
|
||||
'entry': entry,
|
||||
'provinces': provinces,
|
||||
'districts': districts,
|
||||
'communes': communes,
|
||||
'voters_not_voted': voters_not_voted, # Pass to template
|
||||
'voters_voted': voters_voted, # Pass to template
|
||||
'page_name': 'cpp_entries',
|
||||
})
|
||||
|
||||
@http.route(['/my/cpp_entries/submit'], type='http', auth="user", website=True, csrf=True, methods=['POST'])
|
||||
def submit_cpp_entry(self, **post):
|
||||
"""Handle main entry form submission"""
|
||||
entry_id = post.get('entry_id')
|
||||
|
||||
try:
|
||||
# ✅ Validation: Ensure Province is selected
|
||||
if not post.get('province_id') or not str(post['province_id']).isdigit():
|
||||
return request.redirect('/my/cpp_entries/new?error=Please+select+a+Province+(ខេត្ដ/ក្រុង)')
|
||||
|
||||
vals = {}
|
||||
if post.get('province_id') and str(post['province_id']).isdigit():
|
||||
vals['province_id'] = int(post['province_id'])
|
||||
if post.get('district_id') and str(post['district_id']).isdigit():
|
||||
vals['district_id'] = int(post['district_id'])
|
||||
if post.get('commune_id') and str(post['commune_id']).isdigit():
|
||||
vals['commune_id'] = int(post['commune_id'])
|
||||
|
||||
# ✅ Handle al_office field from model
|
||||
if post.get('al_office'):
|
||||
vals['al_office'] = post.get('al_office').strip()
|
||||
|
||||
if entry_id and str(entry_id) != '0' and str(entry_id).isdigit():
|
||||
entry = request.env['cpp.entry'].browse(int(entry_id))
|
||||
if not entry.exists():
|
||||
return request.redirect('/my/cpp_entries?error=Entry+not+found')
|
||||
entry.write(vals)
|
||||
else:
|
||||
entry = request.env['cpp.entry'].create(vals)
|
||||
|
||||
return request.redirect(f'/my/cpp_entries/{entry.id}')
|
||||
except Exception as e:
|
||||
error_msg = str(e).replace(' ', '+')
|
||||
return request.redirect(f'/my/cpp_entries?error={error_msg}')
|
||||
|
||||
@http.route(['/my/cpp_entries/voter/new'], type='http', auth="user", website=True)
|
||||
def portal_voter_new(self, entry_id=0, status_vote=0, **kw):
|
||||
"""Show form to add new voter"""
|
||||
try:
|
||||
entry_id = int(entry_id) if entry_id else 0
|
||||
except (ValueError, TypeError):
|
||||
entry_id = 0
|
||||
|
||||
entry = request.env['cpp.entry'].browse(entry_id) if entry_id else None
|
||||
|
||||
if not entry or not entry.exists():
|
||||
return request.redirect('/my/cpp_entries?error=Please+create+CPP+entry+first')
|
||||
|
||||
if entry.create_uid != request.env.user and not request.env.user.has_group('base.group_user'):
|
||||
raise AccessError(_("Access denied"))
|
||||
|
||||
genders = []
|
||||
try:
|
||||
if 'gender.gender' in request.env:
|
||||
genders = request.env['gender.gender'].search([], order='name')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return request.render('cpp_entry.portal_voter_form', {
|
||||
'entry': entry,
|
||||
'voter': request.env['info.voter'],
|
||||
'genders': genders,
|
||||
'status_vote': int(status_vote),
|
||||
'page_name': 'cpp_entries',
|
||||
})
|
||||
|
||||
@http.route(['/my/cpp_entries/voter/<int:voter_id>/edit'], type='http', auth="user", website=True)
|
||||
def portal_voter_edit(self, voter_id, **kw):
|
||||
"""Show form to edit existing voter"""
|
||||
voter = request.env['info.voter'].browse(voter_id)
|
||||
if not voter.exists():
|
||||
return request.redirect('/my/cpp_entries?error=Voter+not+found')
|
||||
|
||||
if voter.cpp_entry.create_uid != request.env.user and not request.env.user.has_group('base.group_user'):
|
||||
raise AccessError(_("Access denied"))
|
||||
|
||||
genders = []
|
||||
try:
|
||||
if 'gender.gender' in request.env:
|
||||
genders = request.env['gender.gender'].search([], order='name')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return request.render('cpp_entry.portal_voter_form', {
|
||||
'entry': voter.cpp_entry,
|
||||
'voter': voter,
|
||||
'genders': genders,
|
||||
'status_vote': 1 if voter.status_vote else 0,
|
||||
'page_name': 'cpp_entries',
|
||||
})
|
||||
|
||||
@http.route(['/my/cpp_entries/voter/submit'],
|
||||
type='http', auth="user", website=True, csrf=True, methods=['POST'])
|
||||
def submit_voter(self, **post):
|
||||
"""Handle voter form submission with strict validation."""
|
||||
entry_id = post.get('entry_id', '')
|
||||
voter_id = post.get('voter_id', '0')
|
||||
|
||||
try:
|
||||
raw_status = post.get('status_vote')
|
||||
if raw_status:
|
||||
# Convert to string, make lowercase, and check if it matches "checked" values
|
||||
is_voted = str(raw_status).lower() in ('on', '1', 'true', 'yes')
|
||||
else:
|
||||
# If unchecked, the form sends nothing (None or empty string)
|
||||
is_voted = False
|
||||
# 1. VALIDATE ENTRY ID
|
||||
if not entry_id or not str(entry_id).isdigit():
|
||||
raise ValueError("Invalid or missing Entry ID. Cannot save voter.")
|
||||
|
||||
entry = request.env['cpp.entry'].browse(int(entry_id))
|
||||
if not entry.exists():
|
||||
raise ValueError("CPP Entry not found.")
|
||||
|
||||
if entry.create_uid != request.env.user and not request.env.user.has_group('base.group_user'):
|
||||
raise AccessError(_("Permission denied"))
|
||||
|
||||
# 2. VALIDATE NAME (Matches @api.constrains in model)
|
||||
name = post.get('name', '').strip()
|
||||
if len(name) < 2:
|
||||
raise ValidationError(_("ឈ្មោះត្រូវមានយ៉ាងតិច ២ តួអក្សរ (Name must be at least 2 characters)"))
|
||||
|
||||
vals = {
|
||||
'cpp_entry': entry.id,
|
||||
'name': name,
|
||||
'address': post.get('address', '').strip(),
|
||||
'phone': post.get('phone', '').strip(),
|
||||
'status_vote': is_voted,
|
||||
}
|
||||
|
||||
# 3. SAFE GENDER CONVERSION
|
||||
gender_id = post.get('gender', '')
|
||||
if gender_id and str(gender_id).isdigit():
|
||||
vals['gender'] = int(gender_id)
|
||||
|
||||
# 4. DATE OF BIRTH (Optional)
|
||||
dob = post.get('dob')
|
||||
if dob:
|
||||
vals['dob'] = dob
|
||||
|
||||
# 5. PHOTO UPLOAD / DELETION
|
||||
photo = request.httprequest.files.get('photo')
|
||||
if photo and photo.filename:
|
||||
# Check file extension
|
||||
ext = photo.filename.rsplit('.', 1)[-1].lower()
|
||||
if ext not in ('jpg', 'jpeg', 'png', 'gif', 'bmp'):
|
||||
raise ValidationError(_("Invalid file type. Only images allowed."))
|
||||
|
||||
# Check file size (2MB max)
|
||||
photo.seek(0, 2) # Seek to end
|
||||
size = photo.tell() # Get size
|
||||
photo.seek(0) # Reset to beginning
|
||||
|
||||
if size > 2 * 1024 * 1024: # 2MB
|
||||
raise ValidationError(_("File size exceeds 2MB limit."))
|
||||
|
||||
# Read and encode
|
||||
photo_data = photo.read()
|
||||
vals['photo'] = base64.b64encode(photo_data)
|
||||
|
||||
# Debug: Log if photo is being saved
|
||||
_logger.info(f"Photo uploaded: {photo.filename}, Size: {size} bytes")
|
||||
|
||||
# 6. CREATE OR UPDATE
|
||||
if voter_id and str(voter_id) != '0' and str(voter_id).isdigit():
|
||||
voter = request.env['info.voter'].browse(int(voter_id))
|
||||
if not voter.exists():
|
||||
raise ValueError("Voter not found.")
|
||||
voter.write(vals)
|
||||
else:
|
||||
request.env['info.voter'].create(vals)
|
||||
|
||||
return request.redirect(f'/my/cpp_entries/{entry.id}')
|
||||
except Exception as e:
|
||||
error_msg = str(e).replace(' ', '+')
|
||||
# Redirect back to the entry page with the error message
|
||||
if entry_id and str(entry_id).isdigit():
|
||||
return request.redirect(f'/my/cpp_entries/{entry_id}?error={error_msg}')
|
||||
return request.redirect(f'/my/cpp_entries?error={error_msg}')
|
||||
|
||||
@http.route(['/my/cpp_entries/voter/<int:voter_id>/delete'],
|
||||
type='http', auth="user", website=True, csrf=True)
|
||||
def delete_voter(self, voter_id, **kw):
|
||||
"""Delete a voter"""
|
||||
voter = request.env['info.voter'].browse(voter_id)
|
||||
if not voter.exists():
|
||||
return request.redirect('/my/cpp_entries?error=Voter+not+found')
|
||||
|
||||
entry = voter.cpp_entry
|
||||
if entry.create_uid != request.env.user and not request.env.user.has_group('base.group_user'):
|
||||
raise AccessError(_("Permission denied"))
|
||||
|
||||
voter.unlink()
|
||||
return request.redirect(f'/my/cpp_entries/{entry.id}')
|
||||
|
||||
@http.route(['/cpp_entry/get_districts'], type='http', auth='user', website=True)
|
||||
def get_districts(self, province_id, **kw):
|
||||
if not province_id or not str(province_id).isdigit():
|
||||
return request.make_json_response([])
|
||||
|
||||
districts = request.env['address.address'].sudo().search([
|
||||
('parent_location', '=', int(province_id))
|
||||
], order='location_name asc')
|
||||
|
||||
result = [{'id': d.id, 'name': d.location_name or d.name} for d in districts]
|
||||
return request.make_json_response(result)
|
||||
|
||||
@http.route(['/cpp_entry/get_communes'], type='http', auth='user', website=True)
|
||||
def get_communes(self, district_id, **kw):
|
||||
if not district_id or not str(district_id).isdigit():
|
||||
return request.make_json_response([])
|
||||
|
||||
communes = request.env['address.address'].sudo().search([
|
||||
('parent_location', '=', int(district_id))
|
||||
], order='location_name asc')
|
||||
|
||||
result = [{'id': c.id, 'name': c.location_name or c.name} for c in communes]
|
||||
return request.make_json_response(result)
|
||||
|
||||
@http.route(['/my/cpp_entries/voter/<int:voter_id>/toggle_vote'],
|
||||
type='http', auth="user", website=True, csrf=True)
|
||||
def toggle_voter_status(self, voter_id, **kw):
|
||||
"""Quick toggle for status_vote"""
|
||||
voter = request.env['info.voter'].browse(voter_id)
|
||||
if not voter.exists():
|
||||
return request.redirect('/my/cpp_entries?error=Voter+not+found')
|
||||
|
||||
# Security check
|
||||
if voter.cpp_entry.create_uid != request.env.user and not request.env.user.has_group('base.group_user'):
|
||||
raise AccessError(_("Permission denied"))
|
||||
|
||||
# Toggle the boolean value
|
||||
voter.write({'status_vote': not voter.status_vote})
|
||||
|
||||
# Redirect back to the entry page
|
||||
return request.redirect(f'/my/cpp_entries/{voter.cpp_entry.id}')
|
||||
|
||||
@http.route(['/my/cpp_entries/dashboard'], type='http', auth="user", website=True)
|
||||
def cpp_dashboard(self, **kw):
|
||||
user = request.env.user
|
||||
|
||||
# ✅ FIX: Get correct local datetime
|
||||
utc_now = datetime.utcnow()
|
||||
user_tz = request.env.user.tz or 'UTC'
|
||||
tz = pytz.timezone(user_tz)
|
||||
current_dt = pytz.utc.localize(utc_now).astimezone(tz)
|
||||
|
||||
# 1. BASE DOMAIN
|
||||
domain = []
|
||||
if not user.has_group('base.group_system'):
|
||||
domain.append(('create_uid', '=', user.id))
|
||||
|
||||
# 2. GET FILTER VALUES
|
||||
province_id = kw.get('province_id')
|
||||
district_id = kw.get('district_id')
|
||||
commune_id = kw.get('commune_id')
|
||||
al_office = kw.get('al_office', '').strip()
|
||||
company_id = kw.get('company_id')
|
||||
|
||||
# 3. APPLY FILTERS TO DOMAIN
|
||||
if province_id and province_id.isdigit():
|
||||
domain.append(('province_id', '=', int(province_id)))
|
||||
if district_id and district_id.isdigit():
|
||||
domain.append(('district_id', '=', int(district_id)))
|
||||
if commune_id and commune_id.isdigit():
|
||||
domain.append(('commune_id', '=', int(commune_id)))
|
||||
if al_office:
|
||||
domain.append(('al_office', 'ilike', al_office))
|
||||
if company_id and company_id.isdigit():
|
||||
domain.append(('company_id', '=', int(company_id)))
|
||||
|
||||
all_entries = request.env['cpp.entry'].sudo().search(domain)
|
||||
|
||||
# 4. DATA AGGREGATION (Same logic as before)
|
||||
hours = list(range(8, 16))
|
||||
commune_data = {}
|
||||
totals = {
|
||||
'voters': 0, 'voted': 0,
|
||||
'members': 0, 'members_voted': 0,
|
||||
'non_members': 0, 'non_members_voted': 0
|
||||
}
|
||||
|
||||
def safe_pct(num, den):
|
||||
return (num / den * 100) if den > 0 else 0.0
|
||||
|
||||
for entry in all_entries:
|
||||
voters = entry.info_ids
|
||||
commune_name = entry.commune_id.location_name if entry.commune_id else "មិនកំណត់"
|
||||
|
||||
if commune_name not in commune_data:
|
||||
commune_data[commune_name] = {
|
||||
'total': 0, 'voted': 0,
|
||||
'members': 0, 'members_voted': 0,
|
||||
'non_members': 0, 'non_members_voted': 0,
|
||||
'hourly': {h: 0 for h in hours}
|
||||
}
|
||||
|
||||
voted_voters = voters.filtered(lambda v: v.status_vote)
|
||||
members = voters.filtered(lambda v: v.status == '1')
|
||||
non_members = voters.filtered(lambda v: v.status == '2')
|
||||
members_voted = members.filtered(lambda v: v.status_vote)
|
||||
non_members_voted = non_members.filtered(lambda v: v.status_vote)
|
||||
|
||||
c = commune_data[commune_name]
|
||||
c['total'] += len(voters)
|
||||
c['voted'] += len(voted_voters)
|
||||
c['members'] += len(members)
|
||||
c['members_voted'] += len(members_voted)
|
||||
c['non_members'] += len(non_members)
|
||||
c['non_members_voted'] += len(non_members_voted)
|
||||
|
||||
totals['voters'] += len(voters)
|
||||
totals['voted'] += len(voted_voters)
|
||||
totals['members'] += len(members)
|
||||
totals['members_voted'] += len(members_voted)
|
||||
totals['non_members'] += len(non_members)
|
||||
totals['non_members_voted'] += len(non_members_voted)
|
||||
|
||||
for v in voted_voters:
|
||||
if v.create_date:
|
||||
h = v.create_date.hour
|
||||
if h in hours:
|
||||
c['hourly'][h] += 1
|
||||
|
||||
# Calculate percentages
|
||||
totals['members_pct'] = safe_pct(totals['members_voted'], totals['members'])
|
||||
totals['non_members_pct'] = safe_pct(totals['non_members_voted'], totals['non_members'])
|
||||
|
||||
for data in commune_data.values():
|
||||
data['pct'] = safe_pct(data['voted'], data['total'])
|
||||
data['members_pct'] = safe_pct(data['members_voted'], data['members'])
|
||||
data['non_members_pct'] = safe_pct(data['non_members_voted'], data['non_members'])
|
||||
|
||||
cum = 0
|
||||
for h in hours:
|
||||
cum += data['hourly'][h]
|
||||
data['hourly'][h] = {
|
||||
'count': data['hourly'][h],
|
||||
'cum': cum,
|
||||
'pct': safe_pct(cum, data['total'])
|
||||
}
|
||||
|
||||
overall_pct = safe_pct(totals['voted'], totals['voters'])
|
||||
|
||||
# 5. PREPARE DROPDOWN DATA
|
||||
# Provinces (Top level addresses)
|
||||
provinces = request.env['address.address'].sudo().search(
|
||||
[('parent_location', '=', False)], order='location_name asc'
|
||||
)
|
||||
|
||||
# Districts (Children of selected province)
|
||||
districts = request.env['address.address']
|
||||
if province_id and province_id.isdigit():
|
||||
districts = request.env['address.address'].sudo().search(
|
||||
[('parent_location', '=', int(province_id))], order='location_name asc'
|
||||
)
|
||||
else:
|
||||
# If no province selected, show all districts (optional, or keep empty)
|
||||
# districts = request.env['address.address'].sudo().search([('parent_location', '!=', False)])
|
||||
pass
|
||||
|
||||
# Communes (Children of selected district)
|
||||
communes = request.env['address.address']
|
||||
if district_id and district_id.isdigit():
|
||||
communes = request.env['address.address'].sudo().search(
|
||||
[('parent_location', '=', int(district_id))], order='location_name asc'
|
||||
)
|
||||
|
||||
# Companies
|
||||
companies = request.env['res.company'].sudo().search([], order='name asc')
|
||||
|
||||
return request.render('cpp_entry.cpp_dashboard', {
|
||||
'current_dt': current_dt,
|
||||
'hours': hours,
|
||||
'commune_data': commune_data,
|
||||
'totals': totals,
|
||||
'overall_pct': overall_pct,
|
||||
'provinces': provinces,
|
||||
'districts': districts,
|
||||
'communes': communes,
|
||||
'companies': companies,
|
||||
'sel_province': province_id,
|
||||
'sel_district': district_id,
|
||||
'sel_commune': commune_id,
|
||||
'sel_al_office': al_office,
|
||||
'sel_company': company_id,
|
||||
'page_name': 'cpp_dashboard',
|
||||
})
|
||||
|
||||
@http.route(['/my/cpp_entries/party_list'], type='http', auth="user", website=True)
|
||||
def party_list(self, **kw):
|
||||
"""List all parties with voter counts"""
|
||||
user = request.env.user
|
||||
current_dt = datetime.now()
|
||||
|
||||
# Get user's timezone
|
||||
user_tz = request.env.user.tz or 'UTC'
|
||||
tz = pytz.timezone(user_tz)
|
||||
current_dt = pytz.utc.localize(current_dt).astimezone(tz)
|
||||
|
||||
# Base domain
|
||||
domain = []
|
||||
if not user.has_group('base.group_system'):
|
||||
domain.append(('create_uid', '=', user.id))
|
||||
|
||||
# Apply location filters
|
||||
district_id = kw.get('district_id')
|
||||
commune_id = kw.get('commune_id')
|
||||
|
||||
if district_id and district_id.isdigit():
|
||||
domain.append(('district_id', '=', int(district_id)))
|
||||
if commune_id and commune_id.isdigit():
|
||||
domain.append(('commune_id', '=', int(commune_id)))
|
||||
|
||||
all_entries = request.env['cpp.entry'].sudo().search(domain)
|
||||
|
||||
# Aggregate voters by party/status
|
||||
party_stats = {}
|
||||
total_voters = 0
|
||||
|
||||
for entry in all_entries:
|
||||
voters = entry.info_ids
|
||||
|
||||
for voter in voters:
|
||||
# Determine party
|
||||
if voter.status == '1':
|
||||
party_name = "គណបក្សប្រជាជនកម្ពុជា (CPP)"
|
||||
party_key = 'cpp'
|
||||
elif voter.status == '2':
|
||||
party_name = "មិនមែនសមាជិកគណបក្ស"
|
||||
party_key = 'non_member'
|
||||
else:
|
||||
party_name = "មិនកំណត់"
|
||||
party_key = 'unknown'
|
||||
|
||||
if party_key not in party_stats:
|
||||
party_stats[party_key] = {
|
||||
'name': party_name,
|
||||
'total': 0,
|
||||
'voted': 0,
|
||||
'not_voted': 0,
|
||||
'members': 0,
|
||||
'male': 0,
|
||||
'female': 0
|
||||
}
|
||||
|
||||
stats = party_stats[party_key]
|
||||
stats['total'] += 1
|
||||
|
||||
if voter.status_vote:
|
||||
stats['voted'] += 1
|
||||
else:
|
||||
stats['not_voted'] += 1
|
||||
|
||||
if voter.gender:
|
||||
if voter.gender.name == 'ប្រុស':
|
||||
stats['male'] += 1
|
||||
elif voter.gender.name == 'ស្រី':
|
||||
stats['female'] += 1
|
||||
|
||||
total_voters += 1
|
||||
|
||||
# Get location filters for template
|
||||
districts = request.env['address.address'].sudo().search(
|
||||
[('parent_location', '=', False)], order='location_name asc'
|
||||
)
|
||||
communes = request.env['address.address']
|
||||
if district_id and district_id.isdigit():
|
||||
communes = request.env['address.address'].sudo().search(
|
||||
[('parent_location', '=', int(district_id))], order='location_name asc'
|
||||
)
|
||||
|
||||
return request.render('cpp_entry.party_list', {
|
||||
'current_dt': current_dt,
|
||||
'party_stats': party_stats,
|
||||
'total_voters': total_voters,
|
||||
'districts': districts,
|
||||
'communes': communes,
|
||||
'sel_district': district_id,
|
||||
'sel_commune': commune_id,
|
||||
'page_name': 'party_list',
|
||||
})
|
||||
|
||||
@http.route(['/my/cpp_entries/party_voters/<string:party_key>'], type='http', auth="user", website=True)
|
||||
def party_voters_detail(self, party_key, **kw):
|
||||
"""Show detailed list of voters for a specific party"""
|
||||
user = request.env.user
|
||||
current_dt = datetime.now()
|
||||
|
||||
# Get user's timezone
|
||||
user_tz = request.env.user.tz or 'UTC'
|
||||
tz = pytz.timezone(user_tz)
|
||||
current_dt = pytz.utc.localize(current_dt).astimezone(tz)
|
||||
|
||||
# Base domain
|
||||
domain = []
|
||||
if not user.has_group('base.group_system'):
|
||||
domain.append(('create_uid', '=', user.id))
|
||||
|
||||
# Apply location filters
|
||||
district_id = kw.get('district_id')
|
||||
commune_id = kw.get('commune_id')
|
||||
|
||||
if district_id and district_id.isdigit():
|
||||
domain.append(('district_id', '=', int(district_id)))
|
||||
if commune_id and commune_id.isdigit():
|
||||
domain.append(('commune_id', '=', int(commune_id)))
|
||||
|
||||
all_entries = request.env['cpp.entry'].sudo().search(domain)
|
||||
|
||||
# Collect voters by party
|
||||
voters_list = []
|
||||
party_name = ""
|
||||
|
||||
for entry in all_entries:
|
||||
for voter in entry.info_ids:
|
||||
# Determine party
|
||||
if voter.status == '1':
|
||||
voter_party = 'cpp'
|
||||
voter_party_name = "គណបក្សប្រជាជនកម្ពុជា (CPP)"
|
||||
elif voter.status == '2':
|
||||
voter_party = 'non_member'
|
||||
voter_party_name = "មិនមែនសមាជិកគណបក្ស"
|
||||
else:
|
||||
voter_party = 'unknown'
|
||||
voter_party_name = "មិនកំណត់"
|
||||
|
||||
# Filter by requested party
|
||||
if voter_party == party_key:
|
||||
party_name = voter_party_name
|
||||
voters_list.append({
|
||||
'id': voter.id,
|
||||
'name': voter.name,
|
||||
'gender': voter.gender.name if voter.gender else '-',
|
||||
'dob': voter.dob,
|
||||
'phone': voter.phone,
|
||||
'address': voter.address,
|
||||
'status_vote': voter.status_vote,
|
||||
'commune': entry.commune_id.location_name if entry.commune_id else '-',
|
||||
'district': entry.district_id.location_name if entry.district_id else '-',
|
||||
})
|
||||
|
||||
# Sort by name
|
||||
voters_list.sort(key=lambda x: x['name'])
|
||||
|
||||
# Statistics
|
||||
total = len(voters_list)
|
||||
voted = len([v for v in voters_list if v['status_vote']])
|
||||
not_voted = total - voted
|
||||
male = len([v for v in voters_list if v['gender'] == 'ប្រុស'])
|
||||
female = len([v for v in voters_list if v['gender'] == 'ស្រី'])
|
||||
|
||||
# Get location filters
|
||||
districts = request.env['address.address'].sudo().search(
|
||||
[('parent_location', '=', False)], order='location_name asc'
|
||||
)
|
||||
communes = request.env['address.address']
|
||||
if district_id and district_id.isdigit():
|
||||
communes = request.env['address.address'].sudo().search(
|
||||
[('parent_location', '=', int(district_id))], order='location_name asc'
|
||||
)
|
||||
|
||||
return request.render('cpp_entry.party_voters_detail', {
|
||||
'current_dt': current_dt,
|
||||
'party_key': party_key,
|
||||
'party_name': party_name,
|
||||
'voters_list': voters_list,
|
||||
'total': total,
|
||||
'voted': voted,
|
||||
'not_voted': not_voted,
|
||||
'male': male,
|
||||
'female': female,
|
||||
'districts': districts,
|
||||
'communes': communes,
|
||||
'sel_district': district_id,
|
||||
'sel_commune': commune_id,
|
||||
'page_name': 'party_voters',
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="gender_1" model="gender.gender">
|
||||
<field name="name">ប្រុស</field>
|
||||
</record>
|
||||
<record id="gender_2" model="gender.gender">
|
||||
<field name="name">ស្រី</field>
|
||||
</record>
|
||||
<record id="gender_3" model="gender.gender">
|
||||
<field name="name">ព្រះសង្ឃ</field>
|
||||
</record>
|
||||
<record id="p_1" model="person.member">
|
||||
<field name="name">គណបក្សប្រជាជន</field>
|
||||
</record>
|
||||
<record id="p_2" model="person.member">
|
||||
<field name="name">គណបក្សខ្មែរជំនាន់ថ្មី</field>
|
||||
</record>
|
||||
<record id="p_3" model="person.member">
|
||||
<field name="name">គណបក្សខ្មែរក្រោក</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,10 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="gender_1" model="gender.gender">
|
||||
<field name="name">ប្រុស</field>
|
||||
</record>
|
||||
<record id="gender_2" model="gender.gender">
|
||||
<field name="name">ស្រី</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1 @@
|
||||
from . import cpp_entry
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,88 @@
|
||||
# cpp_entry/models/cpp_entry.py
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class CppEntry(models.Model):
|
||||
_name = 'cpp.entry'
|
||||
_description = 'CPP Patient Treatment Entry'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'create_date desc'
|
||||
|
||||
active = fields.Boolean(default=True, string="សកម្ម")
|
||||
|
||||
# Location fields (adjust model names to match your address module)
|
||||
province_id = fields.Many2one('address.address', string="ខេត្ត/ក្រុង (Province)")
|
||||
district_id = fields.Many2one('address.address', string="ស្រុក/ខណ្ឌ (District)")
|
||||
commune_id = fields.Many2one('address.address', string="ឃុំ/សង្កាត់ (Commune)")
|
||||
al_office = fields.Char(string="ការិយាល័យបោះឆ្នោត")
|
||||
company_id = fields.Many2one('res.company', string="Company")
|
||||
|
||||
# One2many to voters - note: field name ends with _ids per Odoo convention
|
||||
info_ids = fields.One2many(
|
||||
'info.voter',
|
||||
'cpp_entry',
|
||||
string="បញ្ជីឈ្មោះអ្នកមិនទាន់បោះឆ្នោត"
|
||||
)
|
||||
|
||||
# Computed fields for display
|
||||
voter_count = fields.Integer(
|
||||
string="ចំនួនអ្នកបោះឆ្នោត",
|
||||
compute='_compute_voter_counts',
|
||||
store=False
|
||||
)
|
||||
non_voter_count = fields.Integer(
|
||||
string="ចំនួនអ្នកមិនទាន់បោះឆ្នោត",
|
||||
compute='_compute_voter_counts',
|
||||
store=False
|
||||
)
|
||||
|
||||
@api.depends('info_ids', 'info_ids.status_vote')
|
||||
def _compute_voter_counts(self):
|
||||
for record in self:
|
||||
record.voter_count = len(record.info_ids.filtered(lambda r: r.status_vote))
|
||||
record.non_voter_count = len(record.info_ids.filtered(lambda r: not r.status_vote))
|
||||
|
||||
class MemberCpp(models.Model):
|
||||
_name = 'person.member'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_description = 'CPP Patient Treatment Entry'
|
||||
|
||||
name = fields.Char(string="ឈ្មោះគណបក្ស")
|
||||
short_name = fields.Char(string="ឈ្មោះខ្លី")
|
||||
|
||||
class InfoVoter(models.Model):
|
||||
_name = 'info.voter'
|
||||
_description = 'Voter Information'
|
||||
_order = 'name'
|
||||
|
||||
cpp_entry = fields.Many2one(
|
||||
'cpp.entry',
|
||||
string="CPP Entry",
|
||||
)
|
||||
|
||||
# Personal Info
|
||||
photo = fields.Binary(string="រូបថត", attachment=True)
|
||||
name = fields.Char(string="ឈ្មោះ", required=True, index=True)
|
||||
gender = fields.Many2one('gender.gender', string="ភេទ")
|
||||
dob = fields.Date(string="ថ្ងៃខែឆ្នាំកំណើត")
|
||||
phone = fields.Char(string="លេខទូរស័ព្ទ")
|
||||
address = fields.Char(string="អាសយដ្ឋាន")
|
||||
status = fields.Selection([('1','សមាជិក'),('2','មិនមែនសមាជិក')],string="ស្ថានភាពសមាជិក")
|
||||
cpp_member=fields.Many2one('person.member',string="សមាជិកគណបក្ស")
|
||||
current_time = fields.Datetime(string="Date Checked")
|
||||
# Voting Status
|
||||
status_vote = fields.Boolean(
|
||||
string="បានបោះឆ្នោត",
|
||||
default=False,
|
||||
help="Check if this person has already voted"
|
||||
)
|
||||
|
||||
# Metadata
|
||||
create_date = fields.Datetime(string="ថ្ងៃបង្កើត", readonly=True)
|
||||
|
||||
@api.constrains('name')
|
||||
def _check_name(self):
|
||||
for record in self:
|
||||
if record.name and len(record.name.strip()) < 2:
|
||||
raise ValidationError(_('ឈ្មោះត្រូវមានយ៉ាងតិច ២ តួអក្សរ'))
|
||||
@@ -0,0 +1,6 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_cpp_entry_user,cpp.entry.user,model_cpp_entry,base.group_user,1,1,1,1
|
||||
access_cpp_entry_portal,cpp.entry.portal,model_cpp_entry,base.group_portal,1,1,1,0
|
||||
access_info_voter_user,info.voter.user,model_info_voter,base.group_user,1,1,1,1
|
||||
access_person_member,info_person_member,model_person_member,base.group_user,1,1,1,1
|
||||
access_info_voter_portal,info.voter.portal,model_info_voter,base.group_portal,1,1,1,0
|
||||
|
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="cpp_entry_portal_rule" model="ir.rule">
|
||||
<field name="name">CPP Entry: Portal users see only own records</field>
|
||||
<field name="model_id" ref="model_cpp_entry"/>
|
||||
<field name="domain_force">[('create_uid', '=', user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="info_voter_portal_rule" model="ir.rule">
|
||||
<field name="name">Voter Info: Portal users see only own entries' voters</field>
|
||||
<field name="model_id" ref="model_info_voter"/>
|
||||
<field name="domain_force">[('cpp_entry.create_uid', '=', user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,167 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { jsonrpc } from "@web/core/network/rpc_service";
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('=== CPP Portal JS Loaded ===');
|
||||
setupCascadingDropdowns();
|
||||
setupPhotoPreview();
|
||||
});
|
||||
|
||||
function setupCascadingDropdowns() {
|
||||
const provinceSelect = document.getElementById('province_select');
|
||||
const districtSelect = document.getElementById('district_select');
|
||||
const communeSelect = document.getElementById('commune_select');
|
||||
|
||||
if (!provinceSelect || !districtSelect || !communeSelect) return;
|
||||
|
||||
// Province change handler
|
||||
provinceSelect.addEventListener('change', async function() {
|
||||
const provinceId = this.value;
|
||||
|
||||
districtSelect.innerHTML = '<option value="">-- ជ្រើសរើសស្រុក/ខណ្ឌ --</option>';
|
||||
communeSelect.innerHTML = '<option value="">-- ជ្រើសរើសឃុំ/សង្កាត់ --</option>';
|
||||
|
||||
if (!provinceId) return;
|
||||
|
||||
try {
|
||||
const districts = await jsonrpc('/cpp_entry/get_districts', {
|
||||
province_id: provinceId
|
||||
});
|
||||
|
||||
districts.forEach(district => {
|
||||
const option = document.createElement('option');
|
||||
option.value = district.id;
|
||||
option.textContent = district.name;
|
||||
districtSelect.appendChild(option);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading districts:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// District change handler
|
||||
districtSelect.addEventListener('change', async function() {
|
||||
const districtId = this.value;
|
||||
|
||||
communeSelect.innerHTML = '<option value="">-- ជ្រើសរើសឃុំ/សង្កាត់ --</option>';
|
||||
|
||||
if (!districtId) return;
|
||||
|
||||
try {
|
||||
const communes = await jsonrpc('/cpp_entry/get_communes', {
|
||||
district_id: districtId
|
||||
});
|
||||
|
||||
communes.forEach(commune => {
|
||||
const option = document.createElement('option');
|
||||
option.value = commune.id;
|
||||
option.textContent = commune.name;
|
||||
communeSelect.appendChild(option);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading communes:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ PHOTO PREVIEW FUNCTION
|
||||
function setupPhotoPreview() {
|
||||
const photoInput = document.getElementById('photo_upload');
|
||||
const photoPreview = document.getElementById('photo_preview');
|
||||
const photoPlaceholder = document.querySelector('.photo-placeholder');
|
||||
|
||||
console.log('Photo Input:', photoInput);
|
||||
console.log('Photo Preview:', photoPreview);
|
||||
|
||||
if (!photoInput) {
|
||||
console.log('No photo upload input found');
|
||||
return;
|
||||
}
|
||||
|
||||
photoInput.addEventListener('change', function(e) {
|
||||
console.log('Photo file selected');
|
||||
const file = e.target.files[0];
|
||||
|
||||
if (!file) {
|
||||
console.log('No file selected');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('File:', file.name, 'Size:', file.size, 'Type:', file.type);
|
||||
|
||||
// Validate file type
|
||||
if (!file.type.match('image.*')) {
|
||||
alert('⚠️ សូមជ្រើសរើសផ្ទាំងរូបភាព (JPEG, PNG, GIF)');
|
||||
this.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file size (2MB max)
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
alert('⚠️ រូបភាពត្រូវតែតូចជាង 2MB');
|
||||
this.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Read and preview the file
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function(event) {
|
||||
console.log('File loaded, previewing...');
|
||||
|
||||
if (photoPreview) {
|
||||
// Update existing preview image
|
||||
photoPreview.src = event.target.result;
|
||||
photoPreview.style.display = 'block';
|
||||
console.log('Updated existing photo preview');
|
||||
} else {
|
||||
// Create new preview image if it doesn't exist
|
||||
console.log('Creating new photo preview element');
|
||||
|
||||
const container = photoInput.closest('.photo-upload-container');
|
||||
if (!container) {
|
||||
console.error('Photo upload container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove placeholder if exists
|
||||
if (photoPlaceholder) {
|
||||
photoPlaceholder.remove();
|
||||
}
|
||||
|
||||
// Create new img element
|
||||
const newPreview = document.createElement('img');
|
||||
newPreview.id = 'photo_preview';
|
||||
newPreview.className = 'rounded-circle border border-3 border-light shadow';
|
||||
newPreview.style.cssText = 'width:120px;height:120px;object-fit:cover;display:block;';
|
||||
newPreview.src = event.target.result;
|
||||
newPreview.alt = 'Profile Photo';
|
||||
|
||||
// Insert before the camera button
|
||||
const cameraLabel = container.querySelector('label[for="photo_upload"]');
|
||||
if (cameraLabel) {
|
||||
container.insertBefore(newPreview, cameraLabel);
|
||||
} else {
|
||||
container.appendChild(newPreview);
|
||||
}
|
||||
|
||||
console.log('New photo preview created and inserted');
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = function(error) {
|
||||
console.error('Error reading file:', error);
|
||||
alert('⚠️ មានបញ្ហាក្នុងការអានឯកសារ');
|
||||
};
|
||||
|
||||
console.log('Reading file as Data URL...');
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
// Export for potential use
|
||||
export const CppPortalUtils = {
|
||||
setupPhotoPreview,
|
||||
setupCascadingDropdowns,
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* CPP Location Cascade - Plain Vanilla JS
|
||||
* NO @odoo-module tag, NO imports.
|
||||
* This script works natively in the browser.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Wait for DOM to be ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
function init() {
|
||||
const provinceSelect = document.getElementById('province_select');
|
||||
const districtSelect = document.getElementById('district_select');
|
||||
const communeSelect = document.getElementById('commune_select');
|
||||
|
||||
if (!provinceSelect) return; // Stop if not on the right page
|
||||
|
||||
// 1. Province Change Event
|
||||
provinceSelect.addEventListener('change', function() {
|
||||
const provinceId = this.value;
|
||||
|
||||
// Clear District and Commune
|
||||
districtSelect.innerHTML = '<option value="">-- កំពុងផ្ទុក... --</option>';
|
||||
districtSelect.disabled = true;
|
||||
|
||||
communeSelect.innerHTML = '<option value="">-- ជ្រើសរើសស្រុក/ខណ្ឌជាមុន --</option>';
|
||||
communeSelect.disabled = true;
|
||||
|
||||
if (!provinceId) {
|
||||
districtSelect.innerHTML = '<option value="">-- ជ្រើសរើសស្ុក/ខណ្ឌ --</option>';
|
||||
districtSelect.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch Districts using standard browser fetch
|
||||
fetch(`/cpp_entry/get_districts?province_id=${provinceId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
districtSelect.innerHTML = '<option value="">-- ជ្រើសរើសស្រុក/ខណ្ --</option>';
|
||||
data.forEach(item => {
|
||||
let option = document.createElement('option');
|
||||
option.value = item.id;
|
||||
option.textContent = item.name;
|
||||
districtSelect.appendChild(option);
|
||||
});
|
||||
districtSelect.disabled = false;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading districts:', error);
|
||||
districtSelect.innerHTML = '<option value="">-- បរាជ័យ --</option>';
|
||||
});
|
||||
});
|
||||
|
||||
// 2. District Change Event
|
||||
districtSelect.addEventListener('change', function() {
|
||||
const districtId = this.value;
|
||||
|
||||
// Clear Commune
|
||||
communeSelect.innerHTML = '<option value="">-- កំពុងផ្ទុក... --</option>';
|
||||
communeSelect.disabled = true;
|
||||
|
||||
if (!districtId) {
|
||||
communeSelect.innerHTML = '<option value="">-- ជ្រើសរើសឃុំ/សង្កាត់ --</option>';
|
||||
communeSelect.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch Communes
|
||||
fetch(`/cpp_entry/get_communes?district_id=${districtId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
communeSelect.innerHTML = '<option value="">-- ជ្រើសរើសឃុំ/សង្កាត់ --</option>';
|
||||
data.forEach(item => {
|
||||
let option = document.createElement('option');
|
||||
option.value = item.id;
|
||||
option.textContent = item.name;
|
||||
communeSelect.appendChild(option);
|
||||
});
|
||||
communeSelect.disabled = false;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading communes:', error);
|
||||
communeSelect.innerHTML = '<option value="">-- បរាជ័យ --</option>';
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,87 @@
|
||||
// cpp_entry/static/src/scss/cpp_portal.scss
|
||||
|
||||
.o_portal_cpp_form {
|
||||
// Photo upload enhancements
|
||||
.photo-upload-container {
|
||||
.photo-preview,
|
||||
.photo-placeholder {
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
.photo-preview:hover {
|
||||
transform: scale(1.03);
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
label[for="photo_upload"] {
|
||||
transition: all 0.2s ease;
|
||||
&:hover {
|
||||
background-color: var(--bs-primary) !important;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Table improvements
|
||||
.table-responsive {
|
||||
@media (max-width: 767px) {
|
||||
font-size: 0.85rem;
|
||||
th, td {
|
||||
padding: 0.4rem !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn-sm {
|
||||
padding: 0.15rem 0.4rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Form enhancements
|
||||
.form-control-lg,
|
||||
.form-select-lg {
|
||||
@media (max-width: 576px) {
|
||||
font-size: 1rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Badge colors for gender
|
||||
.badge {
|
||||
&.bg-info {
|
||||
background-color: #0dcaf0 !important;
|
||||
}
|
||||
&.bg-danger {
|
||||
background-color: #dc3545 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Card header icons
|
||||
.card-header {
|
||||
i {
|
||||
margin-right: 0.5rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive spacing
|
||||
@media (max-width: 991px) {
|
||||
.container {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global portal enhancements for Khmer typography
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans Khmer", "Khmer OS", sans-serif;
|
||||
}
|
||||
|
||||
// Ensure Khmer text renders properly
|
||||
[lang="km"],
|
||||
.khmer-text {
|
||||
font-family: "Noto Sans Khmer", "Khmer OS", "Khmer OS System", sans-serif;
|
||||
line-height: 1.6;
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="cpp_dashboard" name="CPP Election Dashboard">
|
||||
<t t-call="portal.portal_layout">
|
||||
<div class="container-fluid mt-4">
|
||||
<!-- Auto-refresh meta tag -->
|
||||
<meta http-equiv="refresh" content="3600"/>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card bg-primary text-white shadow-sm">
|
||||
<div class="card-body py-3">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-8">
|
||||
<h3 class="mb-0 fw-bold">
|
||||
<i class="fa fa-chart-line me-2"/>
|
||||
លទ្ធផលដំណើរការបោះឆ្នោត
|
||||
</h3>
|
||||
<small>Dashboard - Election Process Results</small>
|
||||
</div>
|
||||
<div class="col-md-4 text-md-end">
|
||||
<h4 class="mb-0 fw-bold">
|
||||
<i class="fa fa-clock me-2"/>
|
||||
ម៉ោង: <t t-esc="current_dt.strftime('%H:%M')"/>
|
||||
</h4>
|
||||
<small>ថ្ងៃ: <t t-esc="current_dt.strftime('%d/%m/%Y')"/></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ UPDATED FILTERS SECTION -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<form method="get" action="/my/cpp_entries/dashboard" class="row g-2">
|
||||
|
||||
<!-- 1. Province Filter -->
|
||||
<div class="col-md-2">
|
||||
<label class="form-label fw-bold small">រាជធានី/ខេត្ដ</label>
|
||||
<select name="province_id" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||
<option value="">-- ទាំងអស់ --</option>
|
||||
<t t-foreach="provinces" t-as="prov">
|
||||
<option t-att-value="prov.id" t-att-selected="sel_province == str(prov.id)">
|
||||
<t t-esc="prov.location_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 2. District Filter -->
|
||||
<div class="col-md-2">
|
||||
<label class="form-label fw-bold small">ស្រុក/ខណ្ឌ</label>
|
||||
<select name="district_id" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||
<option value="">-- ទាំងអស់ --</option>
|
||||
<t t-foreach="districts" t-as="dist">
|
||||
<option t-att-value="dist.id" t-att-selected="sel_district == str(dist.id)">
|
||||
<t t-esc="dist.location_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 3. Commune Filter -->
|
||||
<div class="col-md-2">
|
||||
<label class="form-label fw-bold small">ឃុំ/សង្កាត់</label>
|
||||
<select name="commune_id" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||
<option value="">-- ទាំងអស់ --</option>
|
||||
<t t-foreach="communes" t-as="com">
|
||||
<option t-att-value="com.id" t-att-selected="sel_commune == str(com.id)">
|
||||
<t t-esc="com.location_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 4. Al Office Filter (Text Input) -->
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-bold small">ការិយាល័យបោះឆ្នោត</label>
|
||||
<input type="text" name="al_office" class="form-control form-control-sm"
|
||||
placeholder="ស្វែងរកការិយាល័យ..."
|
||||
t-att-value="sel_al_office or ''"/>
|
||||
</div>
|
||||
|
||||
<!-- 5. Company Filter -->
|
||||
<div class="col-md-2">
|
||||
<label class="form-label fw-bold small">ក្រុមហ៊ុន</label>
|
||||
<select name="company_id" class="form-select form-select-sm">
|
||||
<option value="">-- ទាំងអស់ --</option>
|
||||
<t t-foreach="companies" t-as="comp">
|
||||
<option t-att-value="comp.id" t-att-selected="sel_company == str(comp.id)">
|
||||
<t t-esc="comp.name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 6. Action Buttons -->
|
||||
<div class="col-md-1 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary btn-sm w-100" title="តម្រង">
|
||||
<i class="fa fa-filter"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Reset Button (Full width below or separate) -->
|
||||
<div class="col-12 mt-2 text-end">
|
||||
<a href="/my/cpp_entries/dashboard" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fa fa-refresh me-1"/> រីផ្រេស (Reset)
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overall Statistics Cards -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h4 class="text-primary mb-3">
|
||||
<i class="fa fa-chart-pie me-2"/> លទ្ធផលទូទាំងស្រុក
|
||||
</h4>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="card bg-info text-white h-100 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<h6 class="card-title">ចំនួនអ្នកបោះឆ្នោតសរុប</h6>
|
||||
<h2 class="display-6 fw-bold"><t t-esc="totals['voters']"/></h2>
|
||||
<p class="mb-0 small">នាក់</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="card bg-success text-white h-100 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<h6 class="card-title">បោះឆ្នោតរួច</h6>
|
||||
<h2 class="display-6 fw-bold"><t t-esc="totals['voted']"/></h2>
|
||||
<p class="mb-0 small"><t t-esc="'%.2f' % overall_pct"/>%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="card bg-warning text-dark h-100 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<h6 class="card-title">សមាជិក</h6>
|
||||
<h3 class="fw-bold"><t t-esc="totals['members_voted']"/> / <t t-esc="totals['members']"/></h3>
|
||||
<p class="mb-0 small"><t t-esc="'%.2f' % totals.get('members_pct', 0)"/>%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="card bg-danger text-white h-100 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<h6 class="card-title">មិនមែនសមាជិក</h6>
|
||||
<h3 class="fw-bold"><t t-esc="totals['non_members_voted']"/> / <t t-esc="totals['non_members']"/></h3>
|
||||
<p class="mb-0 small"><t t-esc="'%.2f' % totals.get('non_members_pct', 0)"/>%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hourly Progress Table -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0"><i class="fa fa-clock me-2"/> តារាងម៉ោងបោះឆ្នោត</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped mb-0 text-center align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="text-start ps-3">ឃុំ/សង្កាត់</th>
|
||||
<th t-foreach="hours" t-as="hour" class="text-center"><t t-esc="hour"/>:00</th>
|
||||
<th class="text-center bg-warning text-dark">សរុប</th>
|
||||
<th class="text-center bg-primary text-white">ភាគរយ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="commune_data.items()" t-as="commune">
|
||||
<tr>
|
||||
<td class="text-start fw-bold ps-3"><t t-esc="commune[0]"/></td>
|
||||
<t t-foreach="hours" t-as="hour">
|
||||
<td>
|
||||
<t t-if="commune[1]['hourly'][hour]['count'] > 0">
|
||||
<span class="badge bg-success"><t t-esc="commune[1]['hourly'][hour]['count']"/></span>
|
||||
</t>
|
||||
<t t-else="">-</t>
|
||||
</td>
|
||||
</t>
|
||||
<td class="fw-bold"><t t-esc="commune[1]['voted']"/> / <t t-esc="commune[1]['total']"/></td>
|
||||
<td><span class="badge bg-primary"><t t-esc="'%.2f' % commune[1]['pct']"/>%</span></td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detailed Statistics Table -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0"><i class="fa fa-table me-2"/> តារាងស្ថិតិលម្អិត</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover mb-0 text-center align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th rowspan="2" class="align-middle">ល.រ</th>
|
||||
<th rowspan="2" class="align-middle text-start ps-3">ឃុំ/សង្កាត់</th>
|
||||
<th colspan="3" class="bg-warning bg-opacity-25">សមាជិកគណបក្ស</th>
|
||||
<th colspan="3" class="bg-danger bg-opacity-25">មិនមែនសមាជិក</th>
|
||||
<th rowspan="2" class="align-middle">សរុប</th>
|
||||
<th rowspan="2" class="align-middle">ភាគរយ</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>សរុប</th><th>បោះឆ្នោតរួច</th><th>%</th>
|
||||
<th>សរុប</th><th>បោះឆ្នោតរួច</th><th>%</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="commune_data.items()" t-as="commune">
|
||||
<tr>
|
||||
<td><t t-esc="commune_index + 1"/></td>
|
||||
<td class="text-start fw-bold ps-3"><t t-esc="commune[0]"/></td>
|
||||
<td><t t-esc="commune[1]['members']"/></td>
|
||||
<td class="text-success fw-bold"><t t-esc="commune[1]['members_voted']"/></td>
|
||||
<td><t t-esc="'%.2f' % commune[1]['members_pct']"/>%</td>
|
||||
<td><t t-esc="commune[1]['non_members']"/></td>
|
||||
<td class="text-success fw-bold"><t t-esc="commune[1]['non_members_voted']"/></td>
|
||||
<td><t t-esc="'%.2f' % commune[1]['non_members_pct']"/>%</td>
|
||||
<td class="fw-bold"><t t-esc="commune[1]['voted']"/> / <t t-esc="commune[1]['total']"/></td>
|
||||
<td>
|
||||
<div class="progress" style="height: 20px;">
|
||||
<div class="progress-bar bg-success" t-attf-style="width: {{commune[1]['pct']}}%">
|
||||
<t t-esc="'%.1f' % commune[1]['pct']"/>%
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer Info -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-light border text-center small">
|
||||
<i class="fa fa-info-circle text-primary me-1"/>
|
||||
ទិន្នន័យនឹងធ្វើបចចុប្បន្នភាពដោយស្វ័យប្រវត្តិរងរាល់ ១ ម៉ោងម្តង |
|
||||
ព័ត៌មានចុងក្រោយ: <t t-esc="current_dt.strftime('%d/%m/%Y %H:%M:%S')"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="id_party_views" model="ir.ui.view">
|
||||
<field name="name">Party View</field>
|
||||
<field name="model">person.member</field>
|
||||
<field name="arch" type="xml">
|
||||
<list editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="short_name"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.actions.act_window" id="party_name_action">
|
||||
<field name="name">ឈ្មោះគណបក្ស</field>
|
||||
<field name="res_model">person.member</field>
|
||||
<field name="view_mode">list</field>
|
||||
</record>
|
||||
<menuitem name="Party Name" id="party_name_menu" action="party_name_action" parent="youth_and_scholarship.cpp_setting"/>
|
||||
</odoo>
|
||||
@@ -0,0 +1,193 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="party_list" name="Party Voter Statistics">
|
||||
<t t-call="portal.portal_layout">
|
||||
<div class="container-fluid mt-4">
|
||||
<!-- Auto-refresh -->
|
||||
<meta http-equiv="refresh" content="3600"/>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card bg-primary text-white shadow-sm">
|
||||
<div class="card-body py-3">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-8">
|
||||
<h3 class="mb-0 fw-bold">
|
||||
<i class="fa fa-users me-2"/>
|
||||
ទម្រង់បញ្ជីតាមការវិភាគលើគណបក្សនយោបាយ
|
||||
</h3>
|
||||
<small>(មានប្រភេទសមាជិកគណបក្សនយោបាយ)</small>
|
||||
</div>
|
||||
<div class="col-md-4 text-md-end">
|
||||
<h4 class="mb-0 fw-bold">
|
||||
<i class="fa fa-clock me-2"/>
|
||||
ម៉ោង: <t t-esc="current_dt.strftime('%H:%M')"/>
|
||||
</h4>
|
||||
<small>ថ្ងៃ: <t t-esc="current_dt.strftime('%d/%m/%Y')"/></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<form method="get" action="/my/cpp_entries/party_list" class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold">ស្រុក/ខណ្ឌ</label>
|
||||
<select name="district_id" class="form-select" onchange="this.form.submit()">
|
||||
<option value="">-- ទាំងអស់ --</option>
|
||||
<t t-foreach="districts" t-as="district">
|
||||
<option t-att-value="district.id" t-att-selected="sel_district == str(district.id)">
|
||||
<t t-esc="district.location_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold">ឃុំ/សង្កាត់</label>
|
||||
<select name="commune_id" class="form-select" onchange="this.form.submit()">
|
||||
<option value="">-- ទាំងអស់ --</option>
|
||||
<t t-foreach="communes" t-as="commune">
|
||||
<option t-att-value="commune.id" t-att-selected="sel_commune == str(commune.id)">
|
||||
<t t-esc="commune.location_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 d-flex align-items-end">
|
||||
<a href="/my/cpp_entries/party_list" class="btn btn-outline-primary w-100">
|
||||
<i class="fa fa-refresh me-1"/> រីផ្រេស
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Box -->
|
||||
<div class="alert alert-info mb-4">
|
||||
<i class="fa fa-info-circle me-2"/>
|
||||
<strong>ការព្រមាន:</strong> បញ្ជីនេះបង្ហាញពីចំនួនអ្នកបោះឆ្នោតតាមគណបក្សនយោបាយ។
|
||||
ចុចលើឈ្មោះគណបក្ស ដើម្បីមើលបញ្ជីឈ្មោះអ្នកបោះឆ្នោតនីមួយៗ។
|
||||
</div>
|
||||
|
||||
<!-- Party Statistics Table -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="fa fa-chart-bar me-2"/>
|
||||
ទម្រង់បញ្ជីលទ្ធផលរាប់សន្លឹកឆ្នោត
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="text-center" style="width: 10%;">ល.រ.</th>
|
||||
<th class="text-center" style="width: 60%;">ឈ្មោះគណបក្សនយោបាយ</th>
|
||||
<th class="text-center" style="width: 15%;">សម្គាល់</th>
|
||||
<th class="text-center" style="width: 15%;">សកម្មភាព</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="party_stats.items()" t-as="party">
|
||||
<tr class="table-hover" style="cursor: pointer;">
|
||||
<td class="text-center fw-bold"><t t-esc="party_index + 1"/></td>
|
||||
<td class="fw-bold text-primary">
|
||||
<a t-attf-href="/my/cpp_entries/party_voters/#{party[0]}"
|
||||
class="text-decoration-none"
|
||||
style="color: inherit;">
|
||||
<i class="fa fa-users me-2"/>
|
||||
<t t-esc="party[1]['name']"/>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-primary fs-6">
|
||||
<t t-esc="party[1]['total']"/> នាក់
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a t-attf-href="/my/cpp_entries/party_voters/#{party[0]}"
|
||||
class="btn btn-sm btn-primary">
|
||||
<i class="fa fa-eye"/> មើលលម្អិត
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<tr class="table-primary fw-bold">
|
||||
<td class="text-center" colspan="2">សរុបទាំងអស់</td>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-success fs-6">
|
||||
<t t-esc="total_voters"/> នាក់
|
||||
</span>
|
||||
</td>
|
||||
<td/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="row mb-4">
|
||||
<t t-foreach="party_stats.items()" t-as="party">
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card h-100 shadow-sm border-primary">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h6 class="mb-0"><t t-esc="party[1]['name']"/></h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-6 mb-2">
|
||||
<h4 class="text-primary"><t t-esc="party[1]['total']"/></h4>
|
||||
<small class="text-muted">សរុប</small>
|
||||
</div>
|
||||
<div class="col-6 mb-2">
|
||||
<h4 class="text-success"><t t-esc="party[1]['voted']"/></h4>
|
||||
<small class="text-muted">បានបោះឆ្នោត</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<small>ប្រុស: <strong><t t-esc="party[1]['male']"/></strong></small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<small>ស្រី: <strong><t t-esc="party[1]['female']"/></strong></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-white">
|
||||
<a t-attf-href="/my/cpp_entries/party_voters/#{party[0]}"
|
||||
class="btn btn-sm btn-outline-primary w-100">
|
||||
<i class="fa fa-list me-1"/> មើលបញ្ជី
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Back to Dashboard -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<a href="/my/cpp_entries/dashboard" class="btn btn-secondary">
|
||||
<i class="fa fa-arrow-left me-1"/> ត្រឡប់ទៅ Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -0,0 +1,200 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="party_voters_detail" name="Party Voters Detail">
|
||||
<t t-call="portal.portal_layout">
|
||||
<div class="container-fluid mt-4">
|
||||
<!-- Auto-refresh -->
|
||||
<meta http-equiv="refresh" content="3600"/>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card bg-success text-white shadow-sm">
|
||||
<div class="card-body py-3">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-8">
|
||||
<h3 class="mb-0 fw-bold">
|
||||
<i class="fa fa-users me-2"/>
|
||||
<t t-esc="party_name"/>
|
||||
</h3>
|
||||
<small>បញ្ជី្មោះអនកបោះឆនោត</small>
|
||||
</div>
|
||||
<div class="col-md-4 text-md-end">
|
||||
<h4 class="mb-0 fw-bold">
|
||||
<i class="fa fa-clock me-2"/>
|
||||
ម៉ោង: <t t-esc="current_dt.strftime('%H:%M')"/>
|
||||
</h4>
|
||||
<small>ថ្ងៃ: <t t-esc="current_dt.strftime('%d/%m/%Y')"/></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<form method="get" t-attf-action="/my/cpp_entries/party_voters/#{party_key}" class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold">ស្រុក/ខណ្ឌ</label>
|
||||
<select name="district_id" class="form-select" onchange="this.form.submit()">
|
||||
<option value="">-- ទាំងអស់ --</option>
|
||||
<t t-foreach="districts" t-as="district">
|
||||
<option t-att-value="district.id" t-att-selected="sel_district == str(district.id)">
|
||||
<t t-esc="district.location_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold">ឃុំ/សង្កាត់</label>
|
||||
<select name="commune_id" class="form-select" onchange="this.form.submit()">
|
||||
<option value="">-- ទាំងអស់ --</option>
|
||||
<t t-foreach="communes" t-as="commune">
|
||||
<option t-att-value="commune.id" t-att-selected="sel_commune == str(commune.id)">
|
||||
<t t-esc="commune.location_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 d-flex align-items-end">
|
||||
<a t-attf-href="/my/cpp_entries/party_voters/#{party_key}" class="btn btn-outline-primary w-100">
|
||||
<i class="fa fa-refresh me-1"/> រីផ្រេស
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Summary -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="card bg-primary text-white h-100">
|
||||
<div class="card-body text-center">
|
||||
<h5>សរុប</h5>
|
||||
<h2 class="display-4 fw-bold"><t t-esc="total"/></h2>
|
||||
<small>នាក់</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="card bg-success text-white h-100">
|
||||
<div class="card-body text-center">
|
||||
<h5>បានបោះឆ្ោត</h5>
|
||||
<h2 class="display-4 fw-bold"><t t-esc="voted"/></h2>
|
||||
<small><t t-esc="'%.1f' % (voted/total*100 if total > 0 else 0)"/>%</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="card bg-info text-white h-100">
|
||||
<div class="card-body text-center">
|
||||
<h5>ប្រុស</h5>
|
||||
<h2 class="display-4 fw-bold"><t t-esc="male"/></h2>
|
||||
<small>នាក់</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="card bg-danger text-white h-100">
|
||||
<div class="card-body text-center">
|
||||
<h5>ស្រី</h5>
|
||||
<h2 class="display-4 fw-bold"><t t-esc="female"/></h2>
|
||||
<small>នាក់</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Voters List Table -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="fa fa-list me-2"/>
|
||||
បញ្ជីឈ្មោះអ្នកបោះឆ្នោត
|
||||
</h5>
|
||||
<span class="badge bg-light text-primary">
|
||||
<t t-esc="total"/> នាក់
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="text-center">ល.រ.</th>
|
||||
<th>ឈ្មោះ</th>
|
||||
<th class="text-center">ភេទ</th>
|
||||
<th>ថ្ងៃខែឆ្នាំកំណើត</th>
|
||||
<th>លេខទូរស័ព្ទ</th>
|
||||
<th>ឃុំ/សង្កាត់</th>
|
||||
<th>ស្រុក/ខណ្ឌ</th>
|
||||
<th class="text-center">ស្ថានភាព</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="voters_list" t-as="voter">
|
||||
<tr>
|
||||
<td class="text-center"><t t-esc="voter_index + 1"/></td>
|
||||
<td class="fw-bold"><t t-esc="voter['name']"/></td>
|
||||
<td class="text-center">
|
||||
<t t-if="voter['gender'] == 'ប្រុស'">
|
||||
<span class="badge bg-info"><t t-esc="voter['gender']"/></span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="badge bg-danger"><t t-esc="voter['gender']"/></span>
|
||||
</t>
|
||||
</td>
|
||||
<td><t t-esc="voter['dob'] or '-'"/></td>
|
||||
<td><t t-esc="voter['phone'] or ' '"/></td>
|
||||
<td><t t-esc="voter['commune']"/></td>
|
||||
<td><t t-esc="voter['district']"/></td>
|
||||
<td class="text-center">
|
||||
<t t-if="voter['status_vote']">
|
||||
<span class="badge bg-success">បានបោះឆ្នោត</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="badge bg-warning text-dark">មិនទាន់</span>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-if="not voters_list">
|
||||
<tr>
|
||||
<td colspan="8" class="text-center text-muted py-4">
|
||||
<i class="fa fa-info-circle me-2"/>
|
||||
មិនមានទិន្ននយ
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Buttons -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<a href="/my/cpp_entries/party_list" class="btn btn-secondary me-2">
|
||||
<i class="fa fa-arrow-left me-1"/> ត្រឡប់ទៅបញ្ជីគណបក្ស
|
||||
</a>
|
||||
<a href="/my/cpp_entries/dashboard" class="btn btn-outline-primary">
|
||||
<i class="fa fa-chart-line me-1"/> ទៅ Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Add CPP Entries link to portal home dashboard -->
|
||||
<template id="portal_my_home_cpp" inherit_id="portal.portal_my_home" name="CPP Entries Link" priority="30">
|
||||
<xpath expr="//div[hasclass('o_portal_docs')]" position="inside">
|
||||
<t t-call="portal.portal_docs_entry">
|
||||
<t t-set="title">CPP Entries</t>
|
||||
<t t-set="url" t-value="'/my/cpp_entries'"/>
|
||||
<t t-set="placeholder_count" t-value="'cpp_entries_count'"/>
|
||||
<t t-set="icon" t-value="'fa fa-list-alt'"/>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -0,0 +1,434 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- =======================================================
|
||||
TEMPLATE: List of CPP Entries
|
||||
======================================================= -->
|
||||
<template id="portal_my_cpp_entries" name="My CPP Entries">
|
||||
<t t-call="portal.portal_layout">
|
||||
<div class="container mt-4 khmer-text">
|
||||
<!-- Error Alert -->
|
||||
<t t-if="error">
|
||||
<div class="alert alert-danger alert-dismissible fade show">
|
||||
<i class="fa fa-exclamation-triangle me-2"/>
|
||||
<strong>Error:</strong> <t t-esc="error"/>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="fa fa-list-alt me-2"/>បញ្ជី</h2>
|
||||
<a href="/my/cpp_entries/new" class="btn btn-primary">
|
||||
<i class="fa fa-plus me-1"/>បង្កើតថ្មី
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<t t-if="entries">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>ថ្ងៃបង្កើត</th>
|
||||
<th>រាជធានី/ខេត្ដ</th>
|
||||
<th>ស្រុក/ខណ្ឌ</th>
|
||||
<th>ឃុំ/សង្កាត់</th>
|
||||
<th>ការិយាល័យបោះឆ្នោត</th>
|
||||
<th class="text-center">អ្នកបោះឆ្នោត</th>
|
||||
<th class="text-center">មិនទាន់បោះឆ្នោត</th>
|
||||
<th class="text-end">សកម្មភាព</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="entries" t-as="entry">
|
||||
<tr>
|
||||
<td><t t-esc="entry.create_date.strftime('%d/%m/%Y') if entry.create_date else '-'"/></td>
|
||||
<td><t t-esc="entry.province_id.location_name or '-'"/></td>
|
||||
<td><t t-esc="entry.district_id.location_name or '-'"/></td>
|
||||
<td><t t-esc="entry.commune_id.location_name or '-'"/></td>
|
||||
<td><t t-esc="entry.al_office or '-'"/></td>
|
||||
<td class="text-center"><span class="badge bg-success"><t t-esc="entry.voter_count or 0"/></span></td>
|
||||
<td class="text-center"><span class="badge bg-warning text-dark"><t t-esc="entry.non_voter_count or 0"/></span></td>
|
||||
<td class="text-end">
|
||||
<a t-attf-href="/my/cpp_entries/#{entry.id}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fa fa-eye"/> មើល
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="alert alert-info text-center">
|
||||
<i class="fa fa-info-circle me-2"/>
|
||||
មិនទាន់មានទិន្នន័យ។ <a href="/my/cpp_entries/new">ចុចទីនេះ</a> ដើម្បីបង្កើតថ្មី។
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- =======================================================
|
||||
TEMPLATE: Main CPP Entry Form
|
||||
======================================================= -->
|
||||
<template id="portal_cpp_form" name="CPP Entry Form">
|
||||
<t t-call="portal.portal_layout">
|
||||
<div class="container mt-4 o_portal_cpp_form">
|
||||
<!-- Error Alert -->
|
||||
<t t-if="error">
|
||||
<div class="alert alert-danger alert-dismissible fade show">
|
||||
<i class="fa fa-exclamation-triangle me-2"/>
|
||||
<strong>Error:</strong> <t t-esc="error"/>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<h2 class="mb-4 text-primary">
|
||||
<i class="fa fa-edit me-2"/>
|
||||
<t t-if="entry.id">កែបញ្ជីឈ្មោះ</t>
|
||||
<t t-else="">បង្កើតបញ្ជីឈ្មោះថ្មី</t>
|
||||
</h2>
|
||||
|
||||
<form t-attf-action="/my/cpp_entries/submit" method="post" class="bg-white p-4 rounded shadow-sm border">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<input type="hidden" name="entry_id" t-att-value="entry.id or 0"/>
|
||||
|
||||
<!-- Location Section -->
|
||||
<div class="row mb-4 p-3 bg-light rounded">
|
||||
<div class="col-12 mb-2"><h5 class="fw-bold text-secondary">📍 ព័ត៌មានទីតាំង</h5></div>
|
||||
<!-- Province -->
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-bold">រាជធានី/ខេត្ដ <span class="text-danger">*</span></label>
|
||||
<select name="province_id" class="form-select" required="1" id="province_select">
|
||||
<option value="">-- ជ្រើសរើសរាជធានី/ខេត្ដ --</option>
|
||||
<t t-foreach="provinces" t-as="prov">
|
||||
<option t-att-value="prov.id" t-att-selected="entry.province_id.id == prov.id">
|
||||
<t t-esc="prov.location_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- District -->
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-bold">ស្រុក/ខណ្ឌ</label>
|
||||
<select name="district_id" class="form-select" id="district_select">
|
||||
<option value="">-- ជ្រើសរើសស្រុក/ខណ្ឌ --</option>
|
||||
<t t-if="entry.district_id">
|
||||
<option t-att-value="entry.district_id.id" selected="selected">
|
||||
<t t-esc="entry.district_id.location_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Commune -->
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-bold">ឃុំ/សង្កាត់</label>
|
||||
<select name="commune_id" class="form-select" id="commune_select">
|
||||
<option value="">-- ជ្រើសរើសឃុំ/សង្កាត់ --</option>
|
||||
<t t-if="entry.commune_id">
|
||||
<option t-att-value="entry.commune_id.id" selected="selected">
|
||||
<t t-esc="entry.commune_id.location_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-bold">លេខការិយាល័យ</label>
|
||||
<input name="al_office" class="form-control form-control-lg" placeholder="លេខការិយាល័យ" t-att-value="entry.al_office or ''"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ CONDITIONAL: Only show tables/buttons IF Entry is Saved -->
|
||||
<t t-if="entry.id">
|
||||
<!-- Table 1: Non-Voters List (Pink/Red) -->
|
||||
<div class="card mb-4 border-danger">
|
||||
<div class="card-header bg-danger text-white d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">📋 បញ្ជីឈ្មោះអ្នកមិនទាន់បោះឆ្នោត</h5>
|
||||
<span class="badge bg-light text-danger fs-6">
|
||||
<t t-esc="len(entry.info_ids.filtered(lambda r: not r.status_vote))"/> នាក់
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped mb-0 align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:5%" class="text-center">ល.រ.</th>
|
||||
<th style="width:8%">ភេទ</th>
|
||||
<th style="width:20%">ឈ្មោះ</th>
|
||||
<th style="width:12%">ថ្ងៃកំណើត</th>
|
||||
<th style="width:25%">អាសយដ្ឋាន</th>
|
||||
<!-- <th style="width:10%" class="text-center">ទូរសព្ទ</th>-->
|
||||
<th style="width:10%" class="text-center">ជាសមាជិក</th>
|
||||
<th style="width:12%" class="text-center">ស្ថានភាព</th>
|
||||
<th style="width:12%" class="text-center">សកម្មភាព</th>
|
||||
<th style="width:8%" class="text-center">រូប</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-if="entry.info_ids.filtered(lambda r: not r.status_vote)">
|
||||
<t t-foreach="entry.info_ids.filtered(lambda r: not r.status_vote)" t-as="voter">
|
||||
<tr>
|
||||
<td class="text-center fw-bold"><t t-esc="voter_index + 1"/></td>
|
||||
<td>
|
||||
<span t-if="voter.gender" t-attf-class="badge bg-#{'info' if voter.gender.name == 'ប្រុស' else 'danger'}">
|
||||
<t t-esc="voter.gender.name or '-'"/>
|
||||
</span>
|
||||
<span t-else="" class="text-muted">-</span>
|
||||
</td>
|
||||
<td class="fw-medium"><t t-esc="voter.name or '–'"/></td>
|
||||
<td><t t-esc="voter.dob or '-'"/></td>
|
||||
<td><t t-esc="voter.address or '-'"/></td>
|
||||
<!-- <td class="text-center"><t t-esc="voter.phone or '-'"/></td>-->
|
||||
<td><t t-esc="voter.cpp_member or '-'"/> </td>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-warning text-dark">មិនទាន់បោះឆ្នោត</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<!-- ✅ TOGGLE BUTTON: Mark as Voted -->
|
||||
<a t-attf-href="/my/cpp_entries/voter/#{voter.id}/toggle_vote?csrf_token=#{request.csrf_token()}"
|
||||
class="btn btn-sm btn-success py-0 me-1"
|
||||
title="កំណត់ថាបានបោះឆ្នោត">
|
||||
<i class="fa fa-check"/> បានបោះរួច
|
||||
</a>
|
||||
<a t-attf-href="/my/cpp_entries/voter/#{voter.id}/edit" class="btn btn-sm btn-outline-primary py-0 me-1"><i class="fa fa-pencil"/></a>
|
||||
<!-- <a t-attf-href="/my/cpp_entries/voter/#{voter.id}/delete?csrf_token=#{request.csrf_token()}" class="btn btn-sm btn-outline-danger py-0" onclick="return confirm('តើអ្នកពិតជាចង់លុកឈ្មោះនេះមែនទេ?');"><i class="fa fa-trash"/></a>-->
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-if="voter.photo">
|
||||
<img t-att-src="image_data_uri(voter.photo)" class="rounded" style="width:40px;height:40px;object-fit:cover;" alt="Photo"/>
|
||||
</t>
|
||||
<t t-else=""><span class="text-muted small">–</span></t>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<tr><td colspan="9" class="text-center text-muted py-4"><i class="fa fa-info-circle me-1"/> មិនទាន់មានទិន្នន័យ</td></tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-white">
|
||||
<a t-attf-href="/my/cpp_entries/voter/new?entry_id=#{entry.id}&status_vote=0" class="btn btn-success btn-sm">
|
||||
<i class="fa fa-plus me-1"/> បន្ថែមអ្នកមិនទាន់បោះឆ្នោត
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table 2: Voters List (Green) -->
|
||||
<div class="card mb-4 border-success">
|
||||
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">✅ បញ្ជីឈ្មោះអនកបានបោះឆ្នោត</h5>
|
||||
<span class="badge bg-light text-success fs-6">
|
||||
<t t-esc="len(entry.info_ids.filtered(lambda r: r.status_vote))"/> នាក់
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped mb-0 align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:5%" class="text-center">ល.រ.</th>
|
||||
<th style="width:8%">ភេទ</th>
|
||||
<th style="width:20%">ឈ្មោះ</th>
|
||||
<th style="width:12%">ថ្ងៃកំណើត</th>
|
||||
<th style="width:25%">អាសយដ្ឋាន</th>
|
||||
<!-- <th style="width:10%" class="text-center">ទូរស័ព្ទ</th>-->
|
||||
<th style="width:12%" class="text-center">ស្ថានភាព</th>
|
||||
<th style="width:12%" class="text-center">សកម្មភាព</th>
|
||||
<th style="width:8%" class="text-center">រូប</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-if="entry.info_ids.filtered(lambda r: r.status_vote)">
|
||||
<t t-foreach="entry.info_ids.filtered(lambda r: r.status_vote)" t-as="voter">
|
||||
<tr>
|
||||
<td class="text-center fw-bold"><t t-esc="voter_index + 1"/></td>
|
||||
<td>
|
||||
<span t-if="voter.gender" t-attf-class="badge bg-#{'info' if voter.gender.name == 'ប្រុស' else 'danger'}">
|
||||
<t t-esc="voter.gender.name or '-'"/>
|
||||
</span>
|
||||
<span t-else="" class="text-muted">-</span>
|
||||
</td>
|
||||
<td class="fw-medium"><t t-esc="voter.name or '–'"/></td>
|
||||
<td><t t-esc="voter.dob or '-'"/></td>
|
||||
<td><t t-esc="voter.address or '-'"/></td>
|
||||
<!-- <td class="text-center"><t t-esc="voter.phone or '-'"/></td>-->
|
||||
<td class="text-center">
|
||||
<span class="badge bg-success">បោះឆ្នោតរួច</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<!-- ✅ TOGGLE BUTTON: Mark as Not Voted -->
|
||||
<a t-attf-href="/my/cpp_entries/voter/#{voter.id}/toggle_vote?csrf_token=#{request.csrf_token()}"
|
||||
class="btn btn-sm btn-warning py-0 me-1"
|
||||
title="កំណត់ថាមិនទាន់បោះឆ្នោត">
|
||||
<i class="fa fa-undo"/> មិនទាន់បោះឆ្នោត
|
||||
</a>
|
||||
<a t-attf-href="/my/cpp_entries/voter/#{voter.id}/edit" class="btn btn-sm btn-outline-primary py-0 me-1"><i class="fa fa-pencil"/></a>
|
||||
<a t-attf-href="/my/cpp_entries/voter/#{voter.id}/delete?csrf_token=#{request.csrf_token()}" class="btn btn-sm btn-outline-danger py-0" onclick="return confirm('តើអ្នកពិតជាចង់លុកឈ្មោះនេះមែនទេ?');"><i class="fa fa-trash"/></a>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-if="voter.photo">
|
||||
<img t-att-src="image_data_uri(voter.photo)" class="rounded" style="width:40px;height:40px;object-fit:cover;" alt="Photo"/>
|
||||
</t>
|
||||
<t t-else=""><span class="text-muted small">–</span></t>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<tr><td colspan="9" class="text-center text-muted py-4"><i class="fa fa-info-circle me-1"/> មិនទាន់មានទិន្នន័យ</td></tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="card-footer bg-white">-->
|
||||
<!-- <a t-attf-href="/my/cpp_entries/voter/new?entry_id=#{entry.id}&status_vote=1" class="btn btn-success btn-sm">-->
|
||||
<!-- <i class="fa fa-plus me-1"/> បន្ថែមអ្នកបានបោះឆ្នោត-->
|
||||
<!-- </a>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<!-- ✅ Info Message if entry is NOT saved yet -->
|
||||
<div class="alert alert-info">
|
||||
<i class="fa fa-info-circle me-2"/>
|
||||
<strong>ចំណាំ:</strong> សូមចុច <b>"រក្សាទុក"</b> ដើម្បីបង្កើតបញ្ជីជាមុនសិន ទើបអាចបន្ថែមអ្នកបោះឆ្នោតបាន។
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="d-flex justify-content-between mt-4 pt-3 border-top">
|
||||
<a href="/my/cpp_entries" class="btn btn-outline-secondary px-4">
|
||||
<i class="fa fa-arrow-left me-1"/> ត្រប់
|
||||
</a>
|
||||
<div>
|
||||
<button type="submit" class="btn btn-primary px-5">
|
||||
<i class="fa fa-save me-1"/> រក្សាទុកព័ត៌មាន
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- =======================================================
|
||||
TEMPLATE: Voter Add/Edit Form
|
||||
======================================================= -->
|
||||
<template id="portal_voter_form" name="Voter Information Form">
|
||||
<t t-call="portal.portal_layout">
|
||||
<div class="container mt-4" style="max-width: 750px;">
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/my">ផ្ទាំងគ្រប់គ្រង</a></li>
|
||||
<li class="breadcrumb-item"><a href="/my/cpp_entries">CPP Entries</a></li>
|
||||
<li class="breadcrumb-item"><a t-attf-href="/my/cpp_entries/#{entry.id}">បញ្ជី</a></li>
|
||||
<li class="breadcrumb-item active">
|
||||
<t t-if="voter.id">កែព័ត៌មាន</t>
|
||||
<t t-else="">បន្ថែមថ្មី</t>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h4 class="mb-0"><i class="fa fa-user me-2"/>
|
||||
<t t-if="voter.id">កែព័ត៌មានអ្នកបោះឆ្នោត</t>
|
||||
<t t-else="">បន្ថែមអ្នកបោះឆ្នោតថ្មី</t>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<form t-attf-action="/my/cpp_entries/voter/submit" method="post" enctype="multipart/form-data" class="card-body">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
|
||||
<!-- ✅ Hidden Fields for ID tracking -->
|
||||
<input type="hidden" name="entry_id" t-att-value="entry.id"/>
|
||||
<input type="hidden" name="voter_id" t-att-value="voter.id or 0"/>
|
||||
|
||||
<!-- Photo Upload Section -->
|
||||
<div class="photo-upload-container text-center mb-4">
|
||||
<div class="position-relative d-inline-block">
|
||||
<!-- ✅ Show existing photo if available -->
|
||||
<t t-if="voter.photo">
|
||||
<img t-att-src="image_data_uri(voter.photo)"
|
||||
id="photo_preview"
|
||||
class="rounded-circle border border-3 border-light shadow"
|
||||
style="width:120px;height:120px;object-fit:cover;display:block;"
|
||||
alt="Profile Photo"/>
|
||||
</t>
|
||||
<!-- ✅ Show placeholder if no photo -->
|
||||
<t t-else="">
|
||||
<div class="photo-placeholder rounded-circle bg-light d-flex align-items-center justify-content-center border border-3 border-light shadow"
|
||||
style="width:120px;height:120px;">
|
||||
<i class="fa fa-user fa-3x text-muted"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- ✅ Camera button with file input -->
|
||||
<label for="photo_upload"
|
||||
class="position-absolute bottom-0 end-0 btn btn-sm btn-primary rounded-circle shadow"
|
||||
style="width:36px;height:36px;padding:0;line-height:36px;text-align:center;cursor:pointer;z-index:10;">
|
||||
<i class="fa fa-camera" style="font-size:14px;"/>
|
||||
<input type="file"
|
||||
name="photo"
|
||||
id="photo_upload"
|
||||
class="d-none"
|
||||
accept="image/*"/>
|
||||
</label>
|
||||
</div>
|
||||
<small class="text-muted d-block mt-2">រូបថត (JPEG, PNG - អតិបរមា 2MB)</small>
|
||||
</div>
|
||||
|
||||
<!-- Personal Info -->
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">ឈ្មោះ <span class="text-danger">*</span></label>
|
||||
<input type="text" name="name" class="form-control form-control-lg" required="1" placeholder="បញ្ចូល្មោះពេញ" t-att-value="voter.name or ''"/>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">ភេទ</label>
|
||||
<select name="gender" class="form-select form-select-lg">
|
||||
<option value="">-- ជ្រើសរើសភេទ --</option>
|
||||
<t t-foreach="genders" t-as="g">
|
||||
<option t-att-value="g.id" t-att-selected="voter.gender.id == g.id"><t t-esc="g.name"/></option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">ថ្ងៃខែឆ្នាំកំណើត</label>
|
||||
<input type="date" name="dob" class="form-control form-control-lg" t-att-value="voter.dob"/>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">លេខទូរស័ព្ទ</label>
|
||||
<input type="tel" name="phone" class="form-control form-control-lg" placeholder="012 345 678" t-att-value="voter.phone or ''"/>
|
||||
</div>
|
||||
<div class="col-12 mb-3">
|
||||
<label class="form-label fw-bold">អាសយដ្ឋាន</label>
|
||||
<textarea name="address" class="form-control" rows="3" placeholder="បញ្ចូលអាសយដ្ឋានពេញ" t-esc="voter.address or ''"/>
|
||||
</div>
|
||||
<div class="col-12 mb-4">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="status_vote" id="status_vote" t-att-checked="'checked' if status_vote == 1 else None"/>
|
||||
<label class="form-check-label fw-bold" for="status_vote">✅ បោះឆ្នោតរួច</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4 pt-3 border-top">
|
||||
<a t-attf-href="/my/cpp_entries/#{entry.id}" class="btn btn-outline-secondary px-4"><i class="fa fa-times me-1"/> បោះបង់</a>
|
||||
<button type="submit" class="btn btn-primary px-5">
|
||||
<i class="fa fa-save me-1"/> <t t-if="voter.id">កែប្រែ</t><t t-else="">បន្ថែម</t>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user