Skip to content

1. Bảng mapping chính xác

Các trường yêu cầu (11 field gốc + ID)

Hiển thị popupField BitrixGiá trị mẫuLoại
Tên Công tyTITLE" ĐẦM XINH Shop - 1000069494"string
Company ID (Haravan)UF_CRM_COMPANY_ID1000069494string/integer
Bitrix ID (nội bộ)ID63962integer
Current_HSI_SegmentUF_CRM_CURRENT_HSI_SEGMENT"HSI_1"string
Current_HSI_DetailUF_CRM_CURRENT_HSI_DETAIL"0E-7"string (số dạng scientific)
Last_HSI_SegmentUF_CRM_LAST_HSI_SEGMENT"HSI_1"string
Last_HSI_DetailUF_CRM_LAST_HSI_DETAIL"0E-7"string
Gói Shopplan hiện tạiUF_CRM_CURRENT_SHOPPLAN"Chuyên nghiệp"string
Ngày tạo shopUF_CRM_DATE_CREATED_SHOP"2016-01-19T11:12:14+03:00"datetime
Ngày ký gói Shopplan đầu tiênUF_CRM_FIRST_PAID_DATE"2016-01-20T12:42:35+03:00"datetime
Ngày ký gói Shopplan hiện tạiUF_CRM_DATE_SIGNED_CURRENT_SHOPPLAN"2016-01-20T12:43:00+03:00"datetime
Ngày hết hạn ShopplanUF_CRM_DATE_EXPIRED_SHOPPLAN"2020-01-20T12:42:37+03:00"datetime

Các trường bonus đáng cân nhắc thêm vào popup

Hiển thịField BitrixGiá trị mẫuGhi chú
Chủ shopUF_CRM_SHOP_OWNER_NAME"Thảo Vũ Phương"Hữu ích cho agent
Email chủ shopUF_CRM_SHOP_OWNER_EMAIL"damdepdamxinh@gmail.com"
SĐT chủ shopUF_CRM_SHOP_OWNER_PHONE_NUMBER"0906665140"
Tên storeUF_CRM_STORENAME" ĐẦM XINH Shop"Phần tên không có ID
MembershipUF_CRM_HARAVAN_MEMBERSHIP"Member"
Update noteUF_CRM_661742530FDE6"UPDATE ALL 06-05-2024"Ghi chú gần nhất
Ngày tạo BitrixDATE_CREATE"2024-03-31T03:12:32+03:00"Khác với "Ngày tạo shop"
Phụ trách (Assigned)ASSIGNED_BY_ID338Cần resolve qua user.get

2. Quan sát quan trọng từ dữ liệu

2.1 Timezone Bitrix là +03:00 (Moscow)

Dù portal là .vn, server Bitrix trả timezone Moscow. Khi hiển thị cho agent Việt Nam:

  • Convert sang +07:00 (Asia/Ho_Chi_Minh) hoặc
  • Hiển thị đúng ngày (bỏ giờ) vì các field này về bản chất là ngày, không phải timestamp.

Khuyến nghị: chỉ hiển thị ngày (dd/mm/yyyy), tránh confusion về timezone.

2.2 UF_CRM_CURRENT_HSI_DETAIL = "0E-7" là số khoa học

Đây là cách Python/PHP serialize Decimal(0) ở precision 7 → "0.0000000". Có 2 hướng xử lý:

Cách A: Convert về float để hiển thị đẹp:

python
try:
    v = float(value)
    value = f"{v:.4f}"  # "0.0000"
except (ValueError, TypeError):
    pass

Cách B: Hiển thị raw, agent quen với format này.

Khuyến nghị: dùng Cách A, format thành "0.00" hoặc "—" nếu = 0.

2.3 UF_CRM_CURRENT_SHOPPLAN đã là text "Chuyên nghiệp"

→ Field này là string thường, không phải enumeration. KHÔNG cần resolve qua userfield.list để lấy label. Hiển thị trực tiếp.

2.4 TITLE chứa cả tên + Company ID

" ĐẦM XINH Shop - 1000069494" — có khoảng trắng đầu và đuôi là Haravan ID. Dùng UF_CRM_STORENAME (" ĐẦM XINH Shop") sẽ sạch hơn cho hiển thị tên thuần. Tốt nhất .strip() trước khi render.

2.5 Nhiều UF rỗng — không vấn đề

Phần lớn UF_CRM_xxxxxxxxx là rỗng (do legacy field cũ chưa dùng). Code chỉ map những field cần là đủ.


3. Code Server Script Frappe (đã update với mapping thật)

python
import frappe
import requests
from datetime import datetime

# === 1. Validate ===
orgid = frappe.form_dict.get("orgid")
if not orgid:
    frappe.throw("Thiếu orgid")

allowed_roles = {"Agent", "HD Agent", "HD Manager", "System Manager"}
if not (set(frappe.get_roles()) & allowed_roles):
    frappe.throw("Không có quyền xem hồ sơ khách hàng", frappe.PermissionError)

webhook_url = frappe.conf.get("bitrix_webhook_url")
if not webhook_url:
    frappe.throw("Chưa cấu hình bitrix_webhook_url")

# === 2. Cache ===
cache_key = f"bitrix_company_orgid_{orgid}"
cached = frappe.cache().get_value(cache_key)
if cached:
    frappe.response["message"] = cached
    return

# === 3. Field mapping (CHÍNH XÁC theo data thật) ===
FIELD_MAP = {
    # Định danh
    "ID":                                  "bitrix_id",
    "TITLE":                               "title_full",
    "UF_CRM_STORENAME":                    "company_name",
    "UF_CRM_COMPANY_ID":                   "company_id",

    # HSI
    "UF_CRM_CURRENT_HSI_SEGMENT":          "current_hsi_segment",
    "UF_CRM_CURRENT_HSI_DETAIL":           "current_hsi_detail",
    "UF_CRM_LAST_HSI_SEGMENT":             "last_hsi_segment",
    "UF_CRM_LAST_HSI_DETAIL":              "last_hsi_detail",

    # Shopplan
    "UF_CRM_CURRENT_SHOPPLAN":             "current_shopplan",
    "UF_CRM_DATE_CREATED_SHOP":            "shop_created_date",
    "UF_CRM_FIRST_PAID_DATE":              "first_shopplan_date",
    "UF_CRM_DATE_SIGNED_CURRENT_SHOPPLAN": "current_shopplan_date",
    "UF_CRM_DATE_EXPIRED_SHOPPLAN":        "shopplan_expiry",

    # Bonus — chủ shop & metadata
    "UF_CRM_SHOP_OWNER_NAME":              "owner_name",
    "UF_CRM_SHOP_OWNER_EMAIL":             "owner_email",
    "UF_CRM_SHOP_OWNER_PHONE_NUMBER":      "owner_phone",
    "UF_CRM_HARAVAN_MEMBERSHIP":           "membership",
    "ASSIGNED_BY_ID":                      "assigned_by_id",
}

DATE_FIELDS = {
    "shop_created_date",
    "first_shopplan_date",
    "current_shopplan_date",
    "shopplan_expiry",
}

DECIMAL_FIELDS = {
    "current_hsi_detail",
    "last_hsi_detail",
}

# === 4. Gọi Bitrix ===
try:
    resp = requests.post(
        webhook_url + "crm.company.list.json",
        json={
            "FILTER": {"UF_CRM_COMPANY_ID": str(orgid)},
            "SELECT": list(FIELD_MAP.keys()),
        },
        timeout=10,
    )
    resp.raise_for_status()
    data = resp.json()
except requests.exceptions.Timeout:
    frappe.throw("Bitrix24 timeout, vui lòng thử lại")
except requests.exceptions.RequestException as e:
    frappe.log_error(f"Bitrix call failed: {e}", "Bitrix Integration")
    frappe.throw("Không kết nối được Bitrix24")

if data.get("error"):
    frappe.throw(f"Bitrix error: {data.get('error_description', data['error'])}")

results = data.get("result", [])
if not results:
    frappe.throw(f"Không tìm thấy công ty với Company ID = {orgid}")
if len(results) > 1:
    frappe.log_error(
        f"Multiple companies for UF_CRM_COMPANY_ID={orgid}: "
        f"{[r['ID'] for r in results]}",
        "Bitrix Integration - Duplicate"
    )

raw = results[0]

# === 5. Transform ===
def parse_bitrix_date(value):
    """Bitrix trả ISO 8601 với timezone +03:00. Trả về 'dd/mm/yyyy' theo VN."""
    if not value:
        return None
    try:
        # parse ISO, giữ timezone gốc, chỉ lấy phần ngày
        dt = datetime.fromisoformat(value)
        return dt.strftime("%d/%m/%Y")
    except (ValueError, TypeError):
        return value

def format_decimal(value):
    """Convert '0E-7' → '0.00' để dễ đọc."""
    if value in (None, ""):
        return None
    try:
        f = float(value)
        if f == 0:
            return "0"
        return f"{f:.4f}".rstrip("0").rstrip(".")
    except (ValueError, TypeError):
        return value

result = {}
for bx_field, out_key in FIELD_MAP.items():
    value = raw.get(bx_field)

    # Strip whitespace cho string
    if isinstance(value, str):
        value = value.strip()

    # Multi-value (PHONE/EMAIL gốc, không áp với UF_CRM_SHOP_OWNER_*)
    if isinstance(value, list) and value and isinstance(value[0], dict):
        value = value[0].get("VALUE")

    # Format
    if out_key in DATE_FIELDS:
        value = parse_bitrix_date(value)
    elif out_key in DECIMAL_FIELDS:
        value = format_decimal(value)

    result[out_key] = value

# === 6. (Tuỳ chọn) Resolve assigned user → tên ===
# Cần scope user_basic. Nếu chưa cấp, comment block này.
if result.get("assigned_by_id"):
    try:
        user_resp = requests.post(
            webhook_url + "user.get.json",
            json={"ID": result["assigned_by_id"]},
            timeout=5,
        ).json()
        if user_resp.get("result"):
            u = user_resp["result"][0]
            result["assigned_by_name"] = f"{u.get('NAME','')} {u.get('LAST_NAME','')}".strip()
    except Exception as e:
        frappe.log_error(f"Resolve user failed: {e}", "Bitrix Integration")

# === 7. Cache & return ===
frappe.cache().set_value(cache_key, result, expires_in_sec=300)
frappe.response["message"] = result

4. Update Form Script (popup HTML)

javascript
function render_profile_dialog(data) {
    const fmt = (v) => v || '<span class="text-muted">—</span>';

    const html = `
    <div class="customer-profile">
        <table class="table table-bordered" style="margin-bottom:0;">
            <tr>
                <th colspan="2" style="background:#1f6feb;color:white;">
                    Thông tin chung
                </th>
            </tr>
            <tr>
                <th style="width:40%;">Tên Công ty</th>
                <td><b>${fmt(data.company_name)}</b></td>
            </tr>
            <tr>
                <th>Company ID (Haravan)</th>
                <td><code>${fmt(data.company_id)}</code></td>
            </tr>
            <tr>
                <th>Membership</th>
                <td>${fmt(data.membership)}</td>
            </tr>

            <tr>
                <th colspan="2" style="background:#f5f7fa;">Chủ shop</th>
            </tr>
            <tr><th>Họ tên</th><td>${fmt(data.owner_name)}</td></tr>
            <tr><th>Email</th><td>${fmt(data.owner_email)}</td></tr>
            <tr><th>SĐT</th><td>${fmt(data.owner_phone)}</td></tr>

            <tr>
                <th colspan="2" style="background:#f5f7fa;">Phân khúc HSI</th>
            </tr>
            <tr>
                <th>Current HSI Segment</th>
                <td><span class="badge badge-info">${fmt(data.current_hsi_segment)}</span></td>
            </tr>
            <tr><th>Current HSI Detail</th><td>${fmt(data.current_hsi_detail)}</td></tr>
            <tr>
                <th>Last HSI Segment</th>
                <td><span class="badge badge-secondary">${fmt(data.last_hsi_segment)}</span></td>
            </tr>
            <tr><th>Last HSI Detail</th><td>${fmt(data.last_hsi_detail)}</td></tr>

            <tr>
                <th colspan="2" style="background:#f5f7fa;">Shopplan</th>
            </tr>
            <tr>
                <th>Gói hiện tại</th>
                <td><b style="color:#1f6feb;">${fmt(data.current_shopplan)}</b></td>
            </tr>
            <tr><th>Ngày tạo shop</th><td>${fmt(data.shop_created_date)}</td></tr>
            <tr><th>Ngày ký gói đầu tiên</th><td>${fmt(data.first_shopplan_date)}</td></tr>
            <tr><th>Ngày ký gói hiện tại</th><td>${fmt(data.current_shopplan_date)}</td></tr>
            <tr><th>Ngày hết hạn</th><td>${fmt(data.shopplan_expiry)}</td></tr>
        </table>
    </div>`;

    const d = new frappe.ui.Dialog({
        title: __('Hồ sơ khách hàng - {0}', [data.company_name || data.company_id]),
        size: 'large',
        fields: [{ fieldtype: 'HTML', fieldname: 'profile_html' }],
        primary_action_label: __('Mở trên Bitrix24'),
        primary_action() {
            window.open(
                `https://haravan.bitrix24.vn/crm/company/details/${data.bitrix_id}/`,
                '_blank'
            );
            d.hide();
        },
        secondary_action_label: __('Đóng'),
        secondary_action() { d.hide(); }
    });

    d.fields_dict.profile_html.$wrapper.html(html);
    d.show();
}

Đã sửa: nút "Mở trên Bitrix24" dùng data.bitrix_id (= 63962), KHÔNG dùng company_id (Haravan ID, không phải Bitrix ID nội bộ).


5. Output dự kiến của API sau khi transform

Khi gọi GET /api/method/get_bitrix_company?orgid=1000069494:

json
{
  "message": {
    "bitrix_id": "63962",
    "title_full": "ĐẦM XINH Shop - 1000069494",
    "company_name": "ĐẦM XINH Shop",
    "company_id": "1000069494",

    "current_hsi_segment": "HSI_1",
    "current_hsi_detail": "0",
    "last_hsi_segment": "HSI_1",
    "last_hsi_detail": "0",

    "current_shopplan": "Chuyên nghiệp",
    "shop_created_date": "19/01/2016",
    "first_shopplan_date": "20/01/2016",
    "current_shopplan_date": "20/01/2016",
    "shopplan_expiry": "20/01/2020",

    "owner_name": "Thảo Vũ Phương",
    "owner_email": "damdepdamxinh@gmail.com",
    "owner_phone": "0906665140",
    "membership": "Member",

    "assigned_by_id": "338",
    "assigned_by_name": "Nguyễn Văn A"
  }
}

6. Một vài cảnh báo từ data này

  1. Shop đã hết hạn từ 2020 (shopplan_expiry: 20/01/2020). Có thể agent cần thấy cảnh báo hết hạn ngay trên popup. Gợi ý: thêm logic so sánh ngày hiện tại với shopplan_expiry, nếu < today → hiển thị badge đỏ "ĐÃ HẾT HẠN".

  2. HSI Detail = 0E-7 ở cả current và last → khách hàng có HSI score = 0, có thể là tài khoản không active. Đáng để team CS biết.

  3. COMPANY_TYPE rỗng trên record này. Nếu logic Frappe có filter "chỉ show CUSTOMER", sẽ bỏ sót những record như thế này. Kiểm tra lại.

  4. HAS_PHONE = N, HAS_EMAIL = N ở field chuẩn của Bitrix, nhưng UF_CRM_SHOP_OWNER_PHONE_NUMBERUF_CRM_SHOP_OWNER_EMAIL thì có. Tức là bên Haravan-Bitrix lưu contact ở UF chứ không dùng field PHONE/EMAIL chuẩn. → Đúng, popup nên lấy từ UF như code đã làm.


7. Logic gợi ý: cảnh báo Shopplan hết hạn

Thêm vào Server Script:

python
from datetime import datetime, timezone

# Sau khi build result, thêm:
expiry_str = raw.get("UF_CRM_DATE_EXPIRED_SHOPPLAN")
if expiry_str:
    try:
        expiry = datetime.fromisoformat(expiry_str)
        now = datetime.now(timezone.utc)
        days_left = (expiry - now).days
        result["shopplan_status"] = (
            "expired" if days_left < 0
            else "expiring_soon" if days_left < 30
            else "active"
        )
        result["shopplan_days_left"] = days_left
    except Exception:
        pass

Phía client render thêm badge trạng thái:

javascript
const status_badge = {
  expired: '<span class="badge badge-danger">ĐÃ HẾT HẠN</span>',
  expiring_soon: '<span class="badge badge-warning">SẮP HẾT HẠN</span>',
  active: '<span class="badge badge-success">CÒN HIỆU LỰC</span>'
}[data.shopplan_status] || '';