Files

660 lines
26 KiB
Python
Raw Permalink Normal View History

2026-07-01 14:41:49 +07:00
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',
})