660 lines
26 KiB
Python
660 lines
26 KiB
Python
|
|
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',
|
||
|
|
})
|