343 lines
13 KiB
Python
343 lines
13 KiB
Python
import xmlrpc.client
|
|
import psycopg2
|
|
import pymysql
|
|
import base64
|
|
import re
|
|
from psycopg2.extras import RealDictCursor
|
|
from flask import Flask, render_template, request, session, redirect, url_for, jsonify
|
|
from datetime import datetime, timedelta
|
|
|
|
app = Flask(__name__)
|
|
# Secret key for sessions
|
|
app.secret_key = 'super_secret_tada_migration_key_change_me_in_prod'
|
|
app.permanent_session_lifetime = timedelta(hours=8)
|
|
|
|
# --- Configurations ---
|
|
# PostgreSQL (Hardcoded as requested)
|
|
DB_HOST = "192.169.0.10"
|
|
DB_PORT = "5432"
|
|
DB_NAME = "postgres"
|
|
DB_USER = "postgres"
|
|
DB_PASS = "Mulut!23Berkomunikasi"
|
|
|
|
# Odoo 19
|
|
ODOO_URL = "http://localhost:1900"
|
|
ODOO_DB = "odoo19_OT_v3"
|
|
ODOO_USER = "admin"
|
|
ODOO_PASS = "admin"
|
|
|
|
# MySQL (Login DB)
|
|
MYSQL_HOST = "192.169.0.13"
|
|
MYSQL_PORT = 3306
|
|
MYSQL_DB = "klaim_db"
|
|
MYSQL_USER = "u8975009_root"
|
|
MYSQL_PASS = "Jempol&1992Mutu_hosting"
|
|
|
|
def get_mysql_connection():
|
|
return pymysql.connect(
|
|
host=MYSQL_HOST,
|
|
port=MYSQL_PORT,
|
|
user=MYSQL_USER,
|
|
password=MYSQL_PASS,
|
|
database=MYSQL_DB,
|
|
cursorclass=pymysql.cursors.DictCursor
|
|
)
|
|
|
|
def normalize_phone(phone):
|
|
if not phone:
|
|
return ""
|
|
# Remove all non-digits
|
|
num = re.sub(r'\D', '', str(phone))
|
|
if num.startswith('62'):
|
|
num = num[2:]
|
|
elif num.startswith('0'):
|
|
num = num[1:]
|
|
return num
|
|
|
|
def get_db_connection():
|
|
conn = psycopg2.connect(
|
|
host=DB_HOST,
|
|
port=DB_PORT,
|
|
dbname=DB_NAME,
|
|
user=DB_USER,
|
|
password=DB_PASS
|
|
)
|
|
return conn
|
|
|
|
@app.route('/')
|
|
def index():
|
|
if 'employee_id' not in session:
|
|
return redirect(url_for('login'))
|
|
return render_template('index.html', employee_name=session.get('employee_name', session['employee_id']))
|
|
|
|
@app.route('/login', methods=['GET', 'POST'])
|
|
def login():
|
|
error = None
|
|
if request.method == 'POST':
|
|
employee_id = request.form.get('employee_id')
|
|
password = request.form.get('password')
|
|
|
|
if employee_id and password:
|
|
try:
|
|
conn = get_mysql_connection()
|
|
with conn.cursor() as cursor:
|
|
cursor.execute("SELECT EMP_FULL_NAME, EMP_PASS FROM m_employee WHERE EMP_F_NUM = %s", (employee_id,))
|
|
user = cursor.fetchone()
|
|
|
|
if user:
|
|
db_pass_encoded = user['EMP_PASS']
|
|
|
|
# Check default unencrypted password first
|
|
if password == "11111" and db_pass_encoded == "11111":
|
|
is_valid = True
|
|
else:
|
|
input_pass_encoded = base64.b64encode(password.encode('utf-8')).decode('utf-8')
|
|
is_valid = (input_pass_encoded == db_pass_encoded)
|
|
|
|
if is_valid:
|
|
session.permanent = True
|
|
session['employee_id'] = employee_id
|
|
session['employee_name'] = user['EMP_FULL_NAME']
|
|
return redirect(url_for('index'))
|
|
else:
|
|
error = "Invalid password."
|
|
else:
|
|
error = "Employee ID not found."
|
|
except Exception as e:
|
|
error = f"Database error: {str(e)}"
|
|
finally:
|
|
if 'conn' in locals() and conn.open:
|
|
conn.close()
|
|
else:
|
|
error = "Please provide both Employee ID and Password."
|
|
|
|
return render_template('login.html', error=error)
|
|
|
|
@app.route('/logout')
|
|
def logout():
|
|
session.pop('employee_id', None)
|
|
session.pop('employee_name', None)
|
|
return redirect(url_for('login'))
|
|
|
|
@app.route('/api/customers')
|
|
def get_customers():
|
|
if 'employee_id' not in session:
|
|
return jsonify({"error": "Unauthorized"}), 401
|
|
|
|
# DataTables parameters
|
|
draw = request.args.get('draw', type=int, default=1)
|
|
start = request.args.get('start', type=int, default=0)
|
|
length = request.args.get('length', type=int, default=10)
|
|
search_value = request.args.get('search[value]', default="")
|
|
|
|
order_column_index = request.args.get('order[0][column]', type=int, default=0)
|
|
order_dir = request.args.get('order[0][dir]', default='asc')
|
|
|
|
columns_map = ['id', 'name', 'phone_number', 'email', 'level', 'point_amount', 'responsible']
|
|
order_column = columns_map[order_column_index] if order_column_index < len(columns_map) else 'id'
|
|
if order_dir not in ['asc', 'desc']:
|
|
order_dir = 'asc'
|
|
|
|
conn = None
|
|
try:
|
|
conn = get_db_connection()
|
|
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
|
|
|
# Base query
|
|
query = "SELECT * FROM tada_member"
|
|
count_query = "SELECT COUNT(*) FROM tada_member"
|
|
params = []
|
|
|
|
if search_value:
|
|
search_pattern = f"%{search_value}%"
|
|
where_clause = " WHERE name ILIKE %s OR phone_number ILIKE %s OR email ILIKE %s"
|
|
query += where_clause
|
|
count_query += where_clause
|
|
params.extend([search_pattern, search_pattern, search_pattern])
|
|
|
|
# Get total filtered count
|
|
cursor.execute(count_query, params)
|
|
total_filtered = cursor.fetchone()['count']
|
|
|
|
# Total records without filter
|
|
cursor.execute("SELECT COUNT(*) FROM tada_member")
|
|
total_records = cursor.fetchone()['count']
|
|
|
|
# Add pagination and sorting
|
|
query += f" ORDER BY {order_column} {order_dir.upper()} LIMIT %s OFFSET %s"
|
|
params.extend([length, start])
|
|
|
|
cursor.execute(query, params)
|
|
customers = cursor.fetchall()
|
|
|
|
# Convert date fields to string if any to allow JSON serialization
|
|
for row in customers:
|
|
if row.get('birthday'):
|
|
row['birthday'] = str(row['birthday'])
|
|
if row.get('updated_at'):
|
|
row['updated_at'] = str(row['updated_at'])
|
|
|
|
return jsonify({
|
|
"draw": draw,
|
|
"recordsTotal": total_records,
|
|
"recordsFiltered": total_filtered,
|
|
"data": customers
|
|
})
|
|
except Exception as e:
|
|
print(f"Error fetching customers: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
finally:
|
|
if conn:
|
|
conn.close()
|
|
|
|
@app.route('/api/migrate/<int:customer_id>', methods=['POST'])
|
|
def migrate_customer(customer_id):
|
|
if 'employee_id' not in session:
|
|
return jsonify({"success": False, "message": "Unauthorized"}), 401
|
|
|
|
employee_id = session['employee_id']
|
|
conn = None
|
|
try:
|
|
# Fetch customer data
|
|
conn = get_db_connection()
|
|
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
|
cursor.execute("SELECT * FROM tada_member WHERE id = %s", (customer_id,))
|
|
customer = cursor.fetchone()
|
|
|
|
if not customer:
|
|
return jsonify({"success": False, "message": "Customer not found."}), 404
|
|
|
|
# Odoo connection
|
|
common = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/common')
|
|
uid = common.authenticate(ODOO_DB, ODOO_USER, ODOO_PASS, {})
|
|
|
|
if not uid:
|
|
return jsonify({"success": False, "message": "Failed to authenticate with Odoo."}), 500
|
|
|
|
models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object')
|
|
|
|
# 1. Normalize phone and check if partner exists
|
|
phone_raw = customer.get('phone_number') or ''
|
|
norm_phone = normalize_phone(phone_raw)
|
|
|
|
partner_id = False
|
|
existing_partner = None
|
|
|
|
if norm_phone:
|
|
partner_ids = models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'res.partner', 'search_read',
|
|
[[('phone', 'ilike', norm_phone)]],
|
|
{'fields': ['name', 'phone', 'email', 'gender', 'birth_date', 'city', 'total_spend', 'membership_level_id'], 'limit': 1}
|
|
)
|
|
if partner_ids:
|
|
existing_partner = partner_ids[0]
|
|
partner_id = existing_partner['id']
|
|
|
|
# 2. Find loyalty.program based on level
|
|
level = customer.get('level', '')
|
|
level_map = {
|
|
'Silver': 'Membership Silver',
|
|
'Gold': 'Membership Gold',
|
|
'Platinum': 'Membership Platinum'
|
|
}
|
|
target_program_name = level_map.get(level, f"Membership {level}")
|
|
|
|
# 1. Search by exact mapped name
|
|
program_ids = models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'loyalty.program', 'search', [[('name', '=', target_program_name)]])
|
|
if not program_ids:
|
|
# 2. Search by exact level name
|
|
program_ids = models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'loyalty.program', 'search', [[('name', '=', level)]])
|
|
if not program_ids:
|
|
# 3. Fallback to ilike
|
|
program_ids = models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'loyalty.program', 'search', [[('name', 'ilike', level)]])
|
|
|
|
if not program_ids:
|
|
return jsonify({"success": False, "message": f"Loyalty program containing '{level}' not found in Odoo."}), 404
|
|
|
|
program_id = program_ids[0]
|
|
|
|
# Map gender safely (e.g. 'female' -> 'Female')
|
|
gender_val = customer.get('gender')
|
|
if isinstance(gender_val, str) and gender_val:
|
|
gender_val = gender_val.title()
|
|
|
|
pg_total_spend = float(customer.get('total_spending') or 0.0)
|
|
|
|
if existing_partner:
|
|
# Update existing partner (fill empty fields)
|
|
update_data = {}
|
|
if not existing_partner.get('email') and customer.get('email'):
|
|
update_data['email'] = customer.get('email')
|
|
if not existing_partner.get('gender') and gender_val:
|
|
update_data['gender'] = gender_val
|
|
if not existing_partner.get('birth_date') and customer.get('birthday'):
|
|
update_data['birth_date'] = str(customer.get('birthday'))
|
|
if not existing_partner.get('city') and customer.get('city'):
|
|
update_data['city'] = customer.get('city')
|
|
if not existing_partner.get('membership_level_id'):
|
|
update_data['membership_level_id'] = program_id
|
|
|
|
# Accumulate total_spend
|
|
current_spend = float(existing_partner.get('total_spend') or 0.0)
|
|
if pg_total_spend > 0:
|
|
update_data['total_spend'] = current_spend + pg_total_spend
|
|
|
|
if update_data:
|
|
models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'res.partner', 'write', [[partner_id], update_data], {'context': {'no_loyalty_auto_assign': True}})
|
|
else:
|
|
# Create new partner
|
|
partner_data = {
|
|
'name': customer.get('name', ''),
|
|
'phone': phone_raw,
|
|
'email': customer.get('email', ''),
|
|
'gender': gender_val or '',
|
|
'birth_date': str(customer.get('birthday', '')) if customer.get('birthday') else False,
|
|
'city': customer.get('city', ''),
|
|
'membership_level_id': program_id,
|
|
'total_spend': pg_total_spend
|
|
}
|
|
# Replace None with False for Odoo XML-RPC compatibility
|
|
partner_data = {k: (v if v is not None else False) for k, v in partner_data.items()}
|
|
partner_id = models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'res.partner', 'create', [partner_data], {'context': {'no_loyalty_auto_assign': True}})
|
|
|
|
# 3. Handle loyalty.card
|
|
point_amount = float(customer.get('point_amount') or 0.0)
|
|
|
|
card_ids = models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'loyalty.card', 'search_read',
|
|
[[('partner_id', '=', partner_id), ('program_id', '=', program_id)]],
|
|
{'fields': ['points'], 'limit': 1}
|
|
)
|
|
|
|
if card_ids:
|
|
# Accumulate points
|
|
existing_card = card_ids[0]
|
|
current_points = float(existing_card.get('points') or 0.0)
|
|
if point_amount > 0:
|
|
models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'loyalty.card', 'write', [[existing_card['id']], {'points': current_points + point_amount}])
|
|
else:
|
|
# Create new card
|
|
card_data = {
|
|
'partner_id': partner_id,
|
|
'program_id': program_id,
|
|
'points': point_amount
|
|
}
|
|
models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'loyalty.card', 'create', [card_data])
|
|
|
|
# 4. Update tada_member
|
|
now = datetime.now()
|
|
cursor.execute("UPDATE tada_member SET responsible = %s, updated_at = %s WHERE id = %s", (employee_id, now, customer_id))
|
|
conn.commit()
|
|
|
|
return jsonify({"success": True, "message": "Migrasi data berhasil!"})
|
|
|
|
except Exception as e:
|
|
if conn:
|
|
conn.rollback()
|
|
print(f"Migration Error: {e}")
|
|
return jsonify({"success": False, "message": str(e)}), 500
|
|
finally:
|
|
if conn:
|
|
conn.close()
|
|
|
|
if __name__ == '__main__':
|
|
app.run(debug=True, port=5000, host='0.0.0.0')
|