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/', 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')