Membuat KPI Triggers untuk Modules sales
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
SCADA Failure Reporting
|
||||
======================
|
||||
========================
|
||||
|
||||
Custom module untuk pencatatan laporan failure equipment SCADA.
|
||||
|
||||
@@ -31,32 +31,13 @@ HTTP Routes
|
||||
|
||||
POST /api/scada/failure-report
|
||||
|
||||
Auth: user session (auth='user')
|
||||
Auth: user session
|
||||
|
||||
Dokumentasi frontend yang lebih detail tersedia di:
|
||||
|
||||
- grt_scada_failure_report/FRONTEND_API_DOCUMENTATION.md
|
||||
|
||||
Body example::
|
||||
|
||||
{
|
||||
"equipment_code": "PLC01",
|
||||
"description": "Motor overload saat proses mixing",
|
||||
"date": "2026-02-15 08:30:00"
|
||||
}
|
||||
|
||||
Response success example::
|
||||
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Failure report created",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"equipment_code": "PLC01",
|
||||
"description": "Motor overload saat proses mixing",
|
||||
"date": "2026-02-15 08:30:00"
|
||||
}
|
||||
}
|
||||
Contoh request dan response tersedia di file dokumentasi frontend.
|
||||
|
||||
2. Form Input
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
'name': 'SCADA Failure Reporting',
|
||||
'version': '14.0.1.0.1',
|
||||
'version': '14.0.1.0.2',
|
||||
'category': 'manufacturing',
|
||||
'license': 'LGPL-3',
|
||||
'author': 'Custom',
|
||||
|
||||
+1149
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,412 @@
|
||||
# Odoo KPI Customer Behavior Trigger
|
||||
|
||||
## Overview
|
||||
|
||||
Modul `odoo_kpi_customer_behavior_trigger` digunakan untuk membuat `kpi.value` otomatis berdasarkan hasil segmentasi perilaku pelanggan dari modul customer behavior.
|
||||
|
||||
Tujuan utamanya adalah menghubungkan kualitas portofolio pelanggan milik salesperson dengan sistem KPI. Dengan pendekatan ini, salesperson tidak hanya dinilai dari transaksi yang terjadi, tetapi juga dari kondisi pelanggan yang mereka tangani.
|
||||
|
||||
Contoh kebijakan yang bisa diterapkan:
|
||||
|
||||
- pelanggan `repeat` memberi score tambahan
|
||||
- pelanggan `reactivated` memberi score positif
|
||||
- pelanggan `at_risk` mulai memberi pengurangan
|
||||
- pelanggan `inactive` memberi pengurangan lebih besar
|
||||
- pelanggan `dormant` memberi pengurangan lebih berat
|
||||
- pelanggan `lost` memberi penalti paling besar
|
||||
|
||||
Modul ini terintegrasi dengan:
|
||||
|
||||
- `odoo_kpi`
|
||||
- `grt_sales_business_category`
|
||||
- `sale_management`
|
||||
- `hr`
|
||||
|
||||
---
|
||||
|
||||
# Objective
|
||||
|
||||
Tujuan modul ini:
|
||||
|
||||
- memberi KPI berdasarkan kualitas relasi pelanggan
|
||||
- memberi insentif untuk mempertahankan pelanggan aktif dan repeat order
|
||||
- memberi penalti saat pelanggan mulai memburuk sampai hilang
|
||||
- menghubungkan hasil customer behavior ke `kpi.assignment`
|
||||
- menghasilkan `kpi.value` secara otomatis dari data analisis customer behavior
|
||||
|
||||
---
|
||||
|
||||
# Business Logic
|
||||
|
||||
Setiap kali `customer.behavior.analysis` dibuat atau diperbarui, sistem akan mengevaluasi apakah hasil analisis tersebut perlu dikonversi menjadi nilai KPI.
|
||||
|
||||
Sistem akan:
|
||||
|
||||
1. membaca hasil analisis perilaku pelanggan
|
||||
2. mengambil segment customer
|
||||
3. mengambil business category dari analisis
|
||||
4. mencari salesperson yang bertanggung jawab atas customer tersebut
|
||||
5. mencari `KPI Customer Behavior Trigger Rule` yang cocok
|
||||
6. mencocokkan segment dengan rule line
|
||||
7. membuat atau meng-update `kpi.value`
|
||||
|
||||
---
|
||||
|
||||
# Trigger Event
|
||||
|
||||
Trigger KPI dijalankan saat record `customer.behavior.analysis`:
|
||||
|
||||
- dibuat (`create`)
|
||||
- diperbarui (`write`) pada field penting
|
||||
|
||||
Field yang memicu proses ulang:
|
||||
|
||||
- `segment_id`
|
||||
- `business_category_id`
|
||||
- `partner_id`
|
||||
- `analysis_date`
|
||||
|
||||
Sumber event biasanya berasal dari:
|
||||
|
||||
- cron `Daily Customer Behavior Analysis`
|
||||
- wizard recompute customer behavior
|
||||
- pemanggilan manual `compute_customer_behavior()`
|
||||
|
||||
---
|
||||
|
||||
# Data Flow
|
||||
|
||||
Flow modul:
|
||||
|
||||
```text
|
||||
Customer Behavior Analysis Created / Updated
|
||||
|
|
||||
v
|
||||
Read Customer Segment + Business Category
|
||||
|
|
||||
v
|
||||
Find Responsible Salesperson
|
||||
|
|
||||
v
|
||||
Match KPI Behavior Trigger Rule
|
||||
|
|
||||
v
|
||||
Create / Update KPI Value
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Rule Structure
|
||||
|
||||
Model utama:
|
||||
|
||||
- `kpi.customer.behavior.trigger.rule`
|
||||
- `kpi.customer.behavior.trigger.rule.line`
|
||||
|
||||
## kpi.customer.behavior.trigger.rule
|
||||
|
||||
Header rule digunakan untuk:
|
||||
|
||||
- nama rule
|
||||
- sequence
|
||||
- status active
|
||||
- `business_category_id`
|
||||
|
||||
Rule dibuat per `Business Category` agar score perilaku pelanggan dapat dipisahkan antar lini bisnis.
|
||||
|
||||
## kpi.customer.behavior.trigger.rule.line
|
||||
|
||||
Setiap line menentukan score untuk kombinasi:
|
||||
|
||||
- segment pelanggan
|
||||
- employee
|
||||
- KPI assignment
|
||||
|
||||
Field penting:
|
||||
|
||||
- `segment_id`
|
||||
- `employee_id`
|
||||
- `assignment_id`
|
||||
- `score_value`
|
||||
- `source_module`
|
||||
- `note`
|
||||
|
||||
Nilai `score_value` boleh:
|
||||
|
||||
- positif untuk reward
|
||||
- negatif untuk penalti
|
||||
- nol jika hanya ingin menandai segment tertentu tanpa dampak score
|
||||
|
||||
Constraint:
|
||||
|
||||
- `employee_id` pada line harus sama dengan `employee_id` pada `assignment_id`
|
||||
|
||||
---
|
||||
|
||||
# Segment Scoring
|
||||
|
||||
Modul ini tidak mengunci formula matematis seperti KPI payment trigger. Score ditentukan langsung per segment.
|
||||
|
||||
Contoh konfigurasi:
|
||||
|
||||
- `repeat` = `+5`
|
||||
- `reactivated` = `+3`
|
||||
- `at_risk` = `-2`
|
||||
- `inactive` = `-4`
|
||||
- `dormant` = `-6`
|
||||
- `lost` = `-10`
|
||||
|
||||
Dengan model ini, perusahaan bisa bebas menentukan kebijakan scoring sendiri sesuai strategi customer retention.
|
||||
|
||||
---
|
||||
|
||||
# Employee Resolution Logic
|
||||
|
||||
Salesperson yang akan menerima KPI dicari dengan urutan berikut:
|
||||
|
||||
1. `partner.user_id`
|
||||
2. salesman dari Sales Order terakhir customer pada `business_category_id` yang sama
|
||||
|
||||
Penjelasan:
|
||||
|
||||
- jika partner sudah memiliki salesperson tetap pada field `user_id`, maka salesperson tersebut dipakai
|
||||
- jika tidak ada, sistem mencari Sales Order terakhir customer dalam kategori bisnis yang sama
|
||||
- jika tetap tidak ditemukan employee yang valid, KPI tidak dibuat
|
||||
|
||||
Relasi ke employee dilakukan melalui mapping `res.users -> hr.employee`.
|
||||
|
||||
---
|
||||
|
||||
# KPI Output
|
||||
|
||||
Output modul ini adalah record pada model `kpi.value`.
|
||||
|
||||
Field yang ditulis:
|
||||
|
||||
- `assignment_id`
|
||||
- `value`
|
||||
- `source_module`
|
||||
- `reference_model`
|
||||
- `reference_id`
|
||||
|
||||
Mekanisme yang dipakai adalah upsert:
|
||||
|
||||
- jika kombinasi assignment + source + reference sudah ada, nilai di-update
|
||||
- jika belum ada, record baru dibuat
|
||||
|
||||
Ini penting untuk mencegah duplikasi saat analisis customer dijalankan berulang pada hari yang sama.
|
||||
|
||||
---
|
||||
|
||||
# Reference Model
|
||||
|
||||
Untuk membedakan sumber data KPI, modul ini menggunakan:
|
||||
|
||||
```text
|
||||
reference_model = customer.behavior.analysis.rule.line.<line_id>
|
||||
reference_id = <analysis_id>
|
||||
```
|
||||
|
||||
Dengan pola ini:
|
||||
|
||||
- satu hasil analisis customer hanya menghasilkan satu KPI value per line rule
|
||||
- update analisis yang sama tidak membuat duplikasi
|
||||
|
||||
---
|
||||
|
||||
# Example Configuration
|
||||
|
||||
Contoh kebijakan untuk salesperson A:
|
||||
|
||||
- `repeat` = `+5`
|
||||
- `reactivated` = `+3`
|
||||
- `at_risk` = `-2`
|
||||
- `inactive` = `-4`
|
||||
- `dormant` = `-6`
|
||||
- `lost` = `-10`
|
||||
|
||||
## Scenario 1
|
||||
|
||||
Customer masuk segment `repeat`
|
||||
|
||||
Hasil:
|
||||
|
||||
- KPI value `+5`
|
||||
|
||||
## Scenario 2
|
||||
|
||||
Customer masuk segment `reactivated`
|
||||
|
||||
Hasil:
|
||||
|
||||
- KPI value `+3`
|
||||
|
||||
## Scenario 3
|
||||
|
||||
Customer masuk segment `at_risk`
|
||||
|
||||
Hasil:
|
||||
|
||||
- KPI value `-2`
|
||||
|
||||
## Scenario 4
|
||||
|
||||
Customer masuk segment `inactive`
|
||||
|
||||
Hasil:
|
||||
|
||||
- KPI value `-4`
|
||||
|
||||
## Scenario 5
|
||||
|
||||
Customer masuk segment `dormant`
|
||||
|
||||
Hasil:
|
||||
|
||||
- KPI value `-6`
|
||||
|
||||
## Scenario 6
|
||||
|
||||
Customer masuk segment `lost`
|
||||
|
||||
Hasil:
|
||||
|
||||
- KPI value `-10`
|
||||
|
||||
---
|
||||
|
||||
# Setup in Odoo
|
||||
|
||||
Langkah konfigurasi:
|
||||
|
||||
1. pastikan modul `odoo_kpi`, `grt_sales_business_category`, dan `odoo_kpi_customer_behavior_trigger` sudah terinstall
|
||||
2. pastikan data segment customer behavior sudah tersedia
|
||||
3. buat `KPI Definition` untuk KPI customer behavior
|
||||
4. buat `KPI Period`
|
||||
5. buat `KPI Assignment` untuk masing-masing salesperson
|
||||
6. buka menu `Sales > Customer Behavior > KPI Customer Behavior Triggers`
|
||||
7. buat rule per `Business Category`
|
||||
8. tambahkan line untuk setiap segment yang ingin diberi score
|
||||
9. isi `score_value` sesuai kebijakan perusahaan
|
||||
|
||||
---
|
||||
|
||||
# Recommended Master Data Setup
|
||||
|
||||
Agar modul berjalan dengan benar, sebaiknya:
|
||||
|
||||
- setiap salesperson memiliki relasi `res.users` ke `hr.employee`
|
||||
- partner customer memiliki `user_id` jika ada owner tetap
|
||||
- sales order customer tersimpan dengan benar pada business category yang relevan
|
||||
- KPI assignment tersedia untuk periode aktif
|
||||
- cron customer behavior berjalan normal
|
||||
|
||||
---
|
||||
|
||||
# Integration with Customer Behavior Module
|
||||
|
||||
Modul ini bergantung pada hasil dari `grt_sales_business_category`, khususnya model:
|
||||
|
||||
- `customer.behavior.analysis`
|
||||
- `customer.behavior.segment`
|
||||
- `res.partner.customer_segment_id`
|
||||
|
||||
Artinya, kualitas output KPI sepenuhnya bergantung pada kualitas analisis customer behavior yang sudah ada.
|
||||
|
||||
Jika segment customer belum terhitung, maka KPI behavior juga belum bisa dibuat.
|
||||
|
||||
---
|
||||
|
||||
# Assumptions
|
||||
|
||||
Asumsi implementasi saat ini:
|
||||
|
||||
- satu hasil analisis customer menghasilkan satu KPI value per line rule yang cocok
|
||||
- score diberikan ke satu salesperson yang dianggap paling relevan
|
||||
- business category pada analisis menjadi filter utama rule
|
||||
- score per segment bersifat statis, tidak dihitung dari formula tambahan
|
||||
|
||||
---
|
||||
|
||||
# Limitation
|
||||
|
||||
Batasan implementasi saat ini:
|
||||
|
||||
- belum mendukung pembagian score ke lebih dari satu salesperson
|
||||
- belum mendukung weighting tambahan berdasarkan total amount customer
|
||||
- belum mendukung scoring berdasarkan perubahan segment dari bulan sebelumnya
|
||||
- belum ada simulasi dampak segment ke KPI
|
||||
- belum ada recompute KPI historis terpisah dari recompute customer behavior
|
||||
|
||||
Pengembangan lanjutan yang mungkin:
|
||||
|
||||
- score berbeda berdasarkan total revenue customer
|
||||
- score berbeda berdasarkan jumlah repeat order
|
||||
- penalty tambahan jika customer turun beberapa level sekaligus
|
||||
- rule berbasis team sales
|
||||
- snapshot trend segment per period KPI
|
||||
|
||||
---
|
||||
|
||||
# Technical Notes
|
||||
|
||||
Hook utama ada di:
|
||||
|
||||
- `models/customer_behavior_analysis.py`
|
||||
- `models/kpi_customer_behavior_trigger_rule.py`
|
||||
|
||||
Logika utama:
|
||||
|
||||
- intercept create dan write pada `customer.behavior.analysis`
|
||||
- cari employee yang relevan
|
||||
- cari rule yang cocok berdasarkan business category dan segment
|
||||
- kirim hasil ke `kpi.value`
|
||||
|
||||
---
|
||||
|
||||
# Security
|
||||
|
||||
Akses konfigurasi rule diberikan ke:
|
||||
|
||||
- `odoo_kpi.group_kpi_manager`
|
||||
- `odoo_kpi.group_kpi_admin`
|
||||
- `sales_team.group_sale_manager`
|
||||
|
||||
---
|
||||
|
||||
# Upgrade Module
|
||||
|
||||
Contoh command upgrade:
|
||||
|
||||
```bat
|
||||
c:\odoo14c\python\python.exe C:\odoo14c\server\odoo-bin -c C:\addon14\odoo.conf -d kanjabung_MRP -u odoo_kpi_customer_behavior_trigger --stop-after-init
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Testing Checklist
|
||||
|
||||
Checklist test manual:
|
||||
|
||||
1. buat KPI assignment untuk salesperson
|
||||
2. buat rule customer behavior pada business category yang sesuai
|
||||
3. atur score per segment
|
||||
4. jalankan analisis customer behavior
|
||||
5. pastikan customer yang `repeat` membuat `kpi.value` positif
|
||||
6. pastikan customer yang `at_risk` atau lebih buruk membuat `kpi.value` negatif
|
||||
7. pastikan analisis ulang tidak membuat duplikasi KPI value
|
||||
8. pastikan employee yang menerima score sesuai owner customer atau sales order terakhir
|
||||
|
||||
---
|
||||
|
||||
# Summary
|
||||
|
||||
`odoo_kpi_customer_behavior_trigger` menambahkan KPI trigger berbasis kualitas perilaku pelanggan.
|
||||
|
||||
Dengan modul ini, sistem KPI sales dapat menangkap sinyal penting seperti:
|
||||
|
||||
- pelanggan yang loyal dan repeat
|
||||
- pelanggan yang menurun kualitasnya
|
||||
- pelanggan yang sudah dorman atau lost
|
||||
|
||||
Ini membuat KPI sales lebih dekat ke objective retensi pelanggan, bukan hanya transaksi sesaat.
|
||||
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
"name": "Odoo KPI Customer Behavior Trigger",
|
||||
"summary": "Customer behavior event rules to send KPI values by assignment",
|
||||
"version": "14.0.1.0.0",
|
||||
"category": "Sales/Sales",
|
||||
"author": "Custom",
|
||||
"depends": ["sale_management", "hr", "odoo_kpi", "grt_sales_business_category"],
|
||||
"data": [
|
||||
"security/ir.model.access.csv",
|
||||
"views/kpi_customer_behavior_trigger_rule_views.xml",
|
||||
],
|
||||
"installable": True,
|
||||
"application": False,
|
||||
"license": "LGPL-3",
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
from . import kpi_customer_behavior_trigger_rule
|
||||
from . import customer_behavior_analysis
|
||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class CustomerBehaviorAnalysis(models.Model):
|
||||
_inherit = "customer.behavior.analysis"
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super().create(vals_list)
|
||||
self.env["kpi.customer.behavior.trigger.rule"].sudo().process_behavior_analyses(records)
|
||||
return records
|
||||
|
||||
def write(self, vals):
|
||||
result = super().write(vals)
|
||||
trigger_fields = {"segment_id", "business_category_id", "partner_id", "analysis_date"}
|
||||
if trigger_fields.intersection(vals):
|
||||
self.env["kpi.customer.behavior.trigger.rule"].sudo().process_behavior_analyses(self)
|
||||
return result
|
||||
@@ -0,0 +1,133 @@
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class KpiCustomerBehaviorTriggerRule(models.Model):
|
||||
_name = "kpi.customer.behavior.trigger.rule"
|
||||
_description = "KPI Customer Behavior Trigger Rule"
|
||||
_order = "sequence, id"
|
||||
|
||||
name = fields.Char(required=True)
|
||||
sequence = fields.Integer(default=10)
|
||||
active = fields.Boolean(default=True)
|
||||
business_category_id = fields.Many2one("crm.business.category", required=True, ondelete="cascade", index=True)
|
||||
line_ids = fields.One2many(
|
||||
"kpi.customer.behavior.trigger.rule.line",
|
||||
"rule_id",
|
||||
string="Trigger Lines",
|
||||
)
|
||||
|
||||
def _find_employee_from_user(self, user):
|
||||
if not user:
|
||||
return self.env["hr.employee"]
|
||||
return self.env["hr.employee"].search([("user_id", "=", user.id)], limit=1)
|
||||
|
||||
@api.model
|
||||
def _assignment_matches_date(self, assignment, event_date):
|
||||
period = assignment.period_id
|
||||
return bool(
|
||||
period
|
||||
and period.date_start
|
||||
and period.date_end
|
||||
and period.date_start <= event_date <= period.date_end
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _upsert_value(self, line, value, ref_model, ref_id):
|
||||
value_model = self.env["kpi.value"].sudo()
|
||||
existing = value_model.search(
|
||||
[
|
||||
("assignment_id", "=", line.assignment_id.id),
|
||||
("reference_model", "=", ref_model),
|
||||
("reference_id", "=", ref_id),
|
||||
("source_module", "=", line.source_module),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
vals = {
|
||||
"assignment_id": line.assignment_id.id,
|
||||
"value": value,
|
||||
"source_module": line.source_module,
|
||||
"reference_model": ref_model,
|
||||
"reference_id": ref_id,
|
||||
}
|
||||
if existing:
|
||||
existing.write({"value": value})
|
||||
return existing
|
||||
return value_model.create(vals)
|
||||
|
||||
@api.model
|
||||
def _find_sales_employee_for_analysis(self, analysis):
|
||||
partner = analysis.partner_id.commercial_partner_id
|
||||
if partner.user_id:
|
||||
employee = self._find_employee_from_user(partner.user_id)
|
||||
if employee:
|
||||
return employee
|
||||
|
||||
domain = [
|
||||
("partner_id", "child_of", partner.id),
|
||||
("state", "=", "sale"),
|
||||
("user_id", "!=", False),
|
||||
]
|
||||
if analysis.business_category_id:
|
||||
domain.append(("business_category_id", "=", analysis.business_category_id.id))
|
||||
last_order = self.env["sale.order"].sudo().search(domain, order="date_order desc, id desc", limit=1)
|
||||
return self._find_employee_from_user(last_order.user_id) if last_order else self.env["hr.employee"]
|
||||
|
||||
@api.model
|
||||
def process_behavior_analyses(self, analyses):
|
||||
if not analyses:
|
||||
return False
|
||||
|
||||
for analysis in analyses:
|
||||
if not analysis.segment_id or not analysis.business_category_id or not analysis.partner_id:
|
||||
continue
|
||||
|
||||
event_date = analysis.analysis_date or fields.Date.context_today(self)
|
||||
employee = self._find_sales_employee_for_analysis(analysis)
|
||||
if not employee:
|
||||
continue
|
||||
|
||||
rules = self.search(
|
||||
[
|
||||
("active", "=", True),
|
||||
("business_category_id", "=", analysis.business_category_id.id),
|
||||
]
|
||||
)
|
||||
for rule in rules:
|
||||
lines = rule.line_ids.filtered(
|
||||
lambda l: l.active
|
||||
and l.employee_id.id == employee.id
|
||||
and l.segment_id.id == analysis.segment_id.id
|
||||
and self._assignment_matches_date(l.assignment_id, event_date)
|
||||
)
|
||||
for line in lines:
|
||||
self._upsert_value(
|
||||
line=line,
|
||||
value=line.score_value,
|
||||
ref_model="customer.behavior.analysis.rule.line.%s" % line.id,
|
||||
ref_id=analysis.id,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
class KpiCustomerBehaviorTriggerRuleLine(models.Model):
|
||||
_name = "kpi.customer.behavior.trigger.rule.line"
|
||||
_description = "KPI Customer Behavior Trigger Rule Line"
|
||||
_order = "sequence, id"
|
||||
|
||||
rule_id = fields.Many2one("kpi.customer.behavior.trigger.rule", required=True, ondelete="cascade", index=True)
|
||||
sequence = fields.Integer(default=10)
|
||||
active = fields.Boolean(default=True)
|
||||
segment_id = fields.Many2one("customer.behavior.segment", required=True, ondelete="restrict", index=True)
|
||||
employee_id = fields.Many2one("hr.employee", required=True, ondelete="cascade", index=True)
|
||||
assignment_id = fields.Many2one("kpi.assignment", required=True, ondelete="cascade", index=True)
|
||||
score_value = fields.Float(required=True, default=0.0)
|
||||
source_module = fields.Char(default="sale.customer_behavior", required=True)
|
||||
note = fields.Char()
|
||||
|
||||
@api.constrains("employee_id", "assignment_id")
|
||||
def _check_assignment_employee(self):
|
||||
for rec in self:
|
||||
if rec.assignment_id.employee_id != rec.employee_id:
|
||||
raise ValidationError(_("Employee must match KPI Assignment employee."))
|
||||
@@ -0,0 +1,7 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_kpi_customer_behavior_trigger_rule_kpi_manager,kpi.customer.behavior.trigger.rule.kpi.manager,model_kpi_customer_behavior_trigger_rule,odoo_kpi.group_kpi_manager,1,1,1,1
|
||||
access_kpi_customer_behavior_trigger_rule_kpi_admin,kpi.customer.behavior.trigger.rule.kpi.admin,model_kpi_customer_behavior_trigger_rule,odoo_kpi.group_kpi_admin,1,1,1,1
|
||||
access_kpi_customer_behavior_trigger_rule_sale_manager,kpi.customer.behavior.trigger.rule.sale.manager,model_kpi_customer_behavior_trigger_rule,sales_team.group_sale_manager,1,1,1,1
|
||||
access_kpi_customer_behavior_trigger_rule_line_kpi_manager,kpi.customer.behavior.trigger.rule.line.kpi.manager,model_kpi_customer_behavior_trigger_rule_line,odoo_kpi.group_kpi_manager,1,1,1,1
|
||||
access_kpi_customer_behavior_trigger_rule_line_kpi_admin,kpi.customer.behavior.trigger.rule.line.kpi.admin,model_kpi_customer_behavior_trigger_rule_line,odoo_kpi.group_kpi_admin,1,1,1,1
|
||||
access_kpi_customer_behavior_trigger_rule_line_sale_manager,kpi.customer.behavior.trigger.rule.line.sale.manager,model_kpi_customer_behavior_trigger_rule_line,sales_team.group_sale_manager,1,1,1,1
|
||||
|
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_kpi_customer_behavior_trigger_rule_tree" model="ir.ui.view">
|
||||
<field name="name">kpi.customer.behavior.trigger.rule.tree</field>
|
||||
<field name="model">kpi.customer.behavior.trigger.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="sequence"/>
|
||||
<field name="name"/>
|
||||
<field name="business_category_id"/>
|
||||
<field name="active"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_kpi_customer_behavior_trigger_rule_form" model="ir.ui.view">
|
||||
<field name="name">kpi.customer.behavior.trigger.rule.form</field>
|
||||
<field name="model">kpi.customer.behavior.trigger.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="sequence"/>
|
||||
<field name="business_category_id"/>
|
||||
<field name="active"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Trigger Lines">
|
||||
<field name="line_ids">
|
||||
<tree editable="bottom">
|
||||
<field name="sequence"/>
|
||||
<field name="active"/>
|
||||
<field name="segment_id"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="assignment_id"/>
|
||||
<field name="score_value"/>
|
||||
<field name="source_module"/>
|
||||
<field name="note"/>
|
||||
</tree>
|
||||
<form>
|
||||
<group>
|
||||
<field name="sequence"/>
|
||||
<field name="active"/>
|
||||
<field name="segment_id"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="assignment_id" domain="[('employee_id', '=', employee_id)]"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="score_value"/>
|
||||
<field name="source_module"/>
|
||||
<field name="note"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_kpi_customer_behavior_trigger_rule" model="ir.actions.act_window">
|
||||
<field name="name">KPI Customer Behavior Triggers</field>
|
||||
<field name="res_model">kpi.customer.behavior.trigger.rule</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_kpi_customer_behavior_trigger_rule"
|
||||
name="KPI Customer Behavior Triggers"
|
||||
parent="grt_sales_business_category.menu_customer_behavior_analysis"
|
||||
action="action_kpi_customer_behavior_trigger_rule"
|
||||
sequence="4"
|
||||
groups="odoo_kpi.group_kpi_manager,odoo_kpi.group_kpi_admin,sales_team.group_sale_manager"/>
|
||||
</odoo>
|
||||
@@ -0,0 +1,404 @@
|
||||
# Odoo KPI Sales Trigger
|
||||
|
||||
## Overview
|
||||
|
||||
Modul `odoo_kpi_sales_trigger` digunakan untuk membuat `KPI Value` otomatis dari transaksi Sales Order berdasarkan perilaku pembayaran customer.
|
||||
|
||||
Fokus utama modul ini adalah memberi score KPI kepada salesperson berdasarkan:
|
||||
|
||||
- pembayaran tepat waktu
|
||||
- keterlambatan pembayaran berdasarkan jumlah hari
|
||||
- nilai transaksi di atas threshold tertentu
|
||||
|
||||
Modul ini terintegrasi dengan:
|
||||
|
||||
- `odoo_kpi`
|
||||
- `sale_management`
|
||||
- `account`
|
||||
- `grt_sales_business_category`
|
||||
|
||||
---
|
||||
|
||||
# Objective
|
||||
|
||||
Tujuan modul ini:
|
||||
|
||||
- mengukur kualitas penjualan, bukan hanya jumlah transaksi
|
||||
- memberi reward untuk transaksi yang dibayar tepat waktu
|
||||
- memberi penalti untuk transaksi yang terlambat dibayar
|
||||
- memberi bonus untuk transaksi dengan nilai besar
|
||||
- mengirim hasil perhitungan ke `kpi.value` berdasarkan `kpi.assignment`
|
||||
|
||||
---
|
||||
|
||||
# Business Logic
|
||||
|
||||
Setiap Sales Order yang sudah lunas penuh akan dievaluasi.
|
||||
|
||||
Sistem akan:
|
||||
|
||||
1. mengambil Sales Order
|
||||
2. mengecek apakah semua invoice customer terkait sudah `paid`
|
||||
3. mengambil tanggal pelunasan terakhir
|
||||
4. mengambil tanggal jatuh tempo invoice
|
||||
5. menghitung `late_days`
|
||||
6. mencari `KPI Sales Trigger Rule` yang cocok
|
||||
7. membuat atau meng-update `kpi.value`
|
||||
|
||||
---
|
||||
|
||||
# Score Formula
|
||||
|
||||
Rumus score per `Trigger Line`:
|
||||
|
||||
```text
|
||||
score = on_time_score - (late_days x late_penalty_per_day)
|
||||
```
|
||||
|
||||
Kemudian:
|
||||
|
||||
- score tidak boleh lebih kecil dari `minimum_score`
|
||||
- jika `amount_total >= transaction_amount_threshold`, maka score ditambah `transaction_bonus_score`
|
||||
|
||||
Formula final:
|
||||
|
||||
```text
|
||||
score = max(on_time_score - (late_days x late_penalty_per_day), minimum_score)
|
||||
if amount_total >= transaction_amount_threshold:
|
||||
score += transaction_bonus_score
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Trigger Event
|
||||
|
||||
KPI tidak dibuat saat quotation dibuat atau saat Sales Order dikonfirmasi.
|
||||
|
||||
KPI dibuat saat transaksi sudah benar-benar lunas.
|
||||
|
||||
Event yang dipantau:
|
||||
|
||||
- `account.payment.action_post()`
|
||||
- perubahan `account.move.payment_state` menjadi `paid`
|
||||
|
||||
Ini dipilih agar KPI merepresentasikan realisasi pembayaran, bukan sekadar penjualan.
|
||||
|
||||
---
|
||||
|
||||
# Data Flow
|
||||
|
||||
Flow modul:
|
||||
|
||||
```text
|
||||
Customer Payment Posted
|
||||
|
|
||||
v
|
||||
Invoice becomes Paid
|
||||
|
|
||||
v
|
||||
Find linked Sale Order
|
||||
|
|
||||
v
|
||||
Check Business Category + Salesperson + Assignment Period
|
||||
|
|
||||
v
|
||||
Calculate KPI Score
|
||||
|
|
||||
v
|
||||
Create / Update KPI Value
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Rule Structure
|
||||
|
||||
Model utama:
|
||||
|
||||
- `kpi.sales.trigger.rule`
|
||||
- `kpi.sales.trigger.rule.line`
|
||||
|
||||
## kpi.sales.trigger.rule
|
||||
|
||||
Header rule dipakai untuk:
|
||||
|
||||
- nama rule
|
||||
- sequence
|
||||
- status active
|
||||
- `business_category_id`
|
||||
|
||||
Rule dibuat per business category agar scoring dapat dibedakan untuk tiap kategori bisnis.
|
||||
|
||||
## kpi.sales.trigger.rule.line
|
||||
|
||||
Setiap line menentukan scoring untuk satu employee dan satu assignment KPI.
|
||||
|
||||
Field penting:
|
||||
|
||||
- `employee_id`
|
||||
- `assignment_id`
|
||||
- `on_time_score`
|
||||
- `late_penalty_per_day`
|
||||
- `minimum_score`
|
||||
- `transaction_amount_threshold`
|
||||
- `transaction_bonus_score`
|
||||
- `source_module`
|
||||
- `note`
|
||||
|
||||
Constraint:
|
||||
|
||||
- `employee_id` pada line harus sama dengan `employee_id` pada `assignment_id`
|
||||
|
||||
---
|
||||
|
||||
# Payment and Late Day Calculation
|
||||
|
||||
## Fully Paid
|
||||
|
||||
Sales Order dianggap selesai untuk evaluasi KPI jika:
|
||||
|
||||
- memiliki invoice customer (`out_invoice`) yang sudah `posted`
|
||||
- semua invoice customer terkait memiliki `payment_state = paid`
|
||||
|
||||
## Payment Completion Date
|
||||
|
||||
Tanggal pelunasan yang dipakai adalah tanggal pelunasan terakhir dari invoice yang terkait dengan Sales Order.
|
||||
|
||||
Jika satu Sales Order memiliki beberapa invoice, maka sistem mengambil tanggal pembayaran paling akhir.
|
||||
|
||||
## Due Date
|
||||
|
||||
Tanggal jatuh tempo yang dipakai adalah tanggal jatuh tempo paling akhir dari invoice customer terkait.
|
||||
|
||||
Jika `invoice_date_due` tidak ada, sistem memakai `invoice_date`.
|
||||
|
||||
## Late Days
|
||||
|
||||
```text
|
||||
late_days = payment_completion_date - due_date
|
||||
```
|
||||
|
||||
Jika pembayaran dilakukan sebelum atau sama dengan jatuh tempo, maka `late_days = 0`.
|
||||
|
||||
---
|
||||
|
||||
# KPI Output
|
||||
|
||||
Output modul ini adalah record di model `kpi.value`.
|
||||
|
||||
Field yang diisi:
|
||||
|
||||
- `assignment_id`
|
||||
- `value`
|
||||
- `source_module`
|
||||
- `reference_model`
|
||||
- `reference_id`
|
||||
|
||||
Modul menggunakan mekanisme upsert:
|
||||
|
||||
- jika KPI untuk kombinasi assignment + reference + source sudah ada, maka nilai di-update
|
||||
- jika belum ada, maka dibuat record baru
|
||||
|
||||
Ini mencegah duplikasi KPI saat event pembayaran terpanggil lebih dari sekali.
|
||||
|
||||
---
|
||||
|
||||
# Example Configuration
|
||||
|
||||
Contoh rule untuk salesperson A:
|
||||
|
||||
- `on_time_score`: `10`
|
||||
- `late_penalty_per_day`: `1`
|
||||
- `minimum_score`: `0`
|
||||
- `transaction_amount_threshold`: `100000000`
|
||||
- `transaction_bonus_score`: `5`
|
||||
|
||||
## Scenario 1
|
||||
|
||||
- nilai transaksi: `80.000.000`
|
||||
- telat: `0` hari
|
||||
|
||||
Perhitungan:
|
||||
|
||||
```text
|
||||
score = max(10 - (0 x 1), 0) = 10
|
||||
```
|
||||
|
||||
Hasil:
|
||||
|
||||
- score `10`
|
||||
|
||||
## Scenario 2
|
||||
|
||||
- nilai transaksi: `80.000.000`
|
||||
- telat: `3` hari
|
||||
|
||||
Perhitungan:
|
||||
|
||||
```text
|
||||
score = max(10 - (3 x 1), 0) = 7
|
||||
```
|
||||
|
||||
Hasil:
|
||||
|
||||
- score `7`
|
||||
|
||||
## Scenario 3
|
||||
|
||||
- nilai transaksi: `120.000.000`
|
||||
- telat: `0` hari
|
||||
|
||||
Perhitungan:
|
||||
|
||||
```text
|
||||
score = max(10 - (0 x 1), 0) + 5 = 15
|
||||
```
|
||||
|
||||
Hasil:
|
||||
|
||||
- score `15`
|
||||
|
||||
## Scenario 4
|
||||
|
||||
- nilai transaksi: `120.000.000`
|
||||
- telat: `20` hari
|
||||
|
||||
Perhitungan:
|
||||
|
||||
```text
|
||||
score = max(10 - (20 x 1), 0) + 5 = 5
|
||||
```
|
||||
|
||||
Hasil:
|
||||
|
||||
- score `5`
|
||||
|
||||
---
|
||||
|
||||
# Setup in Odoo
|
||||
|
||||
Langkah konfigurasi:
|
||||
|
||||
1. pastikan modul `odoo_kpi`, `grt_sales_business_category`, dan `odoo_kpi_sales_trigger` sudah terinstall
|
||||
2. buat `KPI Definition` pada modul KPI
|
||||
3. buat `KPI Period`
|
||||
4. buat `KPI Assignment` untuk employee sales
|
||||
5. buka menu `Sales > KPI Sales Triggers`
|
||||
6. buat rule per `Business Category`
|
||||
7. tambahkan `Trigger Lines` untuk setiap salesperson
|
||||
8. isi parameter score sesuai kebijakan perusahaan
|
||||
|
||||
---
|
||||
|
||||
# Recommended Master Data Setup
|
||||
|
||||
Agar modul berjalan dengan benar, sebaiknya:
|
||||
|
||||
- setiap salesperson memiliki relasi `res.users` ke `hr.employee`
|
||||
- Sales Order memiliki `business_category_id`
|
||||
- invoice berasal dari Sales Order yang sama
|
||||
- KPI Assignment dibuat untuk periode aktif
|
||||
- period KPI mencakup tanggal pelunasan transaksi
|
||||
|
||||
---
|
||||
|
||||
# Assumptions
|
||||
|
||||
Asumsi implementasi saat ini:
|
||||
|
||||
- score diberikan ke salesperson pada `sale.order.user_id`
|
||||
- evaluasi dilakukan saat order lunas penuh
|
||||
- invoice yang dihitung hanya `out_invoice`
|
||||
- refund belum dijadikan trigger KPI terpisah
|
||||
- bonus nilai transaksi bersifat tambahan di atas score dasar
|
||||
|
||||
---
|
||||
|
||||
# Limitation
|
||||
|
||||
Batasan implementasi saat ini:
|
||||
|
||||
- belum ada pembeda rule berdasarkan team sales
|
||||
- belum ada pembeda rule berdasarkan produk atau product category
|
||||
- belum ada tier threshold bertingkat dalam satu line
|
||||
- belum ada wizard simulasi score dari transaksi
|
||||
- belum ada scheduled recompute untuk data historis
|
||||
|
||||
Jika dibutuhkan, pengembangan berikutnya bisa menambahkan:
|
||||
|
||||
- multi-threshold bonus
|
||||
- penalty maksimum
|
||||
- rule per sales team
|
||||
- rule per customer segment
|
||||
- recompute KPI existing transaction
|
||||
|
||||
---
|
||||
|
||||
# Technical Notes
|
||||
|
||||
Hook utama ada di:
|
||||
|
||||
- `models/account_payment.py`
|
||||
- `models/account_move.py`
|
||||
- `models/sale_order.py`
|
||||
- `models/kpi_sales_trigger_rule.py`
|
||||
|
||||
Logika inti:
|
||||
|
||||
- mendeteksi invoice customer yang telah lunas
|
||||
- menelusuri Sales Order terkait
|
||||
- menghitung score berdasarkan konfigurasi
|
||||
- menulis hasil ke `kpi.value`
|
||||
|
||||
---
|
||||
|
||||
# Security
|
||||
|
||||
Akses konfigurasi rule diberikan ke:
|
||||
|
||||
- `odoo_kpi.group_kpi_manager`
|
||||
- `odoo_kpi.group_kpi_admin`
|
||||
- `sales_team.group_sale_manager`
|
||||
|
||||
---
|
||||
|
||||
# Upgrade Module
|
||||
|
||||
Contoh command upgrade:
|
||||
|
||||
```bat
|
||||
c:\odoo14c\python\python.exe C:\odoo14c\server\odoo-bin -c C:\addon14\odoo.conf -d kanjabung_MRP -u odoo_kpi_sales_trigger --stop-after-init
|
||||
```
|
||||
|
||||
Atau gunakan script:
|
||||
|
||||
```bat
|
||||
upgrade_odoo_kpi_sales_trigger.bat
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Testing Checklist
|
||||
|
||||
Checklist test manual:
|
||||
|
||||
1. buat KPI assignment untuk salesperson
|
||||
2. buat rule pada business category yang sesuai
|
||||
3. buat Sales Order dengan salesperson tersebut
|
||||
4. konfirmasi Sales Order dan generate invoice
|
||||
5. lakukan pembayaran sebelum due date
|
||||
6. pastikan `kpi.value` terbentuk dengan score sesuai
|
||||
7. ulangi dengan pembayaran terlambat
|
||||
8. ulangi dengan nilai transaksi di atas threshold
|
||||
9. pastikan tidak ada duplikasi `kpi.value` untuk order yang sama
|
||||
|
||||
---
|
||||
|
||||
# Summary
|
||||
|
||||
`odoo_kpi_sales_trigger` menambahkan mekanisme KPI sales berbasis kualitas pembayaran customer.
|
||||
|
||||
Dengan modul ini, KPI sales tidak hanya menghitung volume penjualan, tetapi juga:
|
||||
|
||||
- ketepatan pembayaran
|
||||
- dampak keterlambatan pembayaran
|
||||
- nilai strategis transaksi
|
||||
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
"name": "Odoo KPI Sales Trigger",
|
||||
"summary": "Sales event rules to send KPI values by assignment",
|
||||
"version": "14.0.1.0.2",
|
||||
"category": "Sales/Sales",
|
||||
"author": "Custom",
|
||||
"depends": ["sale_management", "account", "hr", "odoo_kpi", "grt_sales_business_category"],
|
||||
"data": [
|
||||
"security/ir.model.access.csv",
|
||||
"views/kpi_sales_trigger_rule_views.xml",
|
||||
],
|
||||
"installable": True,
|
||||
"application": False,
|
||||
"license": "LGPL-3",
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
from . import kpi_sales_trigger_rule
|
||||
from . import sale_order
|
||||
from . import account_move
|
||||
from . import account_payment
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,30 @@
|
||||
from odoo import models
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = "account.move"
|
||||
|
||||
def _get_kpi_sales_payment_dates(self):
|
||||
self.ensure_one()
|
||||
receivable_lines = self.line_ids.filtered(lambda line: line.account_id.internal_type == "receivable")
|
||||
partials = receivable_lines.mapped("matched_debit_ids") | receivable_lines.mapped("matched_credit_ids")
|
||||
counterpart_lines = (partials.mapped("debit_move_id") | partials.mapped("credit_move_id")) - receivable_lines
|
||||
return [move.date for move in counterpart_lines.mapped("move_id") if move.date]
|
||||
|
||||
def write(self, vals):
|
||||
previous_payment_state = {}
|
||||
if "payment_state" in vals:
|
||||
previous_payment_state = {move.id: move.payment_state for move in self}
|
||||
|
||||
result = super().write(vals)
|
||||
|
||||
if "payment_state" in vals:
|
||||
paid_moves = self.filtered(
|
||||
lambda move: move.move_type == "out_invoice"
|
||||
and previous_payment_state.get(move.id) != "paid"
|
||||
and move.payment_state == "paid"
|
||||
)
|
||||
orders = paid_moves.mapped("invoice_line_ids.sale_line_ids.order_id")
|
||||
if orders:
|
||||
self.env["kpi.sales.trigger.rule"].sudo().process_paid_orders(orders)
|
||||
return result
|
||||
@@ -0,0 +1,18 @@
|
||||
from odoo import models
|
||||
|
||||
|
||||
class AccountPayment(models.Model):
|
||||
_inherit = "account.payment"
|
||||
|
||||
def action_post(self):
|
||||
result = super().action_post()
|
||||
receivable_lines = self.mapped("move_id.line_ids").filtered(lambda line: line.account_id.internal_type == "receivable")
|
||||
partials = receivable_lines.mapped("matched_debit_ids") | receivable_lines.mapped("matched_credit_ids")
|
||||
counterpart_lines = (partials.mapped("debit_move_id") | partials.mapped("credit_move_id")) - receivable_lines
|
||||
invoice_moves = counterpart_lines.mapped("move_id").filtered(
|
||||
lambda move: move.move_type == "out_invoice" and move.state == "posted" and move.payment_state == "paid"
|
||||
)
|
||||
orders = invoice_moves.mapped("invoice_line_ids.sale_line_ids.order_id")
|
||||
if orders:
|
||||
self.env["kpi.sales.trigger.rule"].sudo().process_paid_orders(orders)
|
||||
return result
|
||||
@@ -0,0 +1,137 @@
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class KpiSalesTriggerRule(models.Model):
|
||||
_name = "kpi.sales.trigger.rule"
|
||||
_description = "KPI Sales Trigger Rule"
|
||||
_order = "sequence, id"
|
||||
|
||||
name = fields.Char(required=True)
|
||||
sequence = fields.Integer(default=10)
|
||||
active = fields.Boolean(default=True)
|
||||
business_category_id = fields.Many2one("crm.business.category", required=True, ondelete="cascade", index=True)
|
||||
line_ids = fields.One2many("kpi.sales.trigger.rule.line", "rule_id", string="Trigger Lines")
|
||||
|
||||
def _find_employee_from_user(self, user):
|
||||
if not user:
|
||||
return self.env["hr.employee"]
|
||||
return self.env["hr.employee"].search([("user_id", "=", user.id)], limit=1)
|
||||
|
||||
@api.model
|
||||
def _assignment_matches_date(self, assignment, event_date):
|
||||
period = assignment.period_id
|
||||
return bool(period and period.date_start and period.date_end and period.date_start <= event_date <= period.date_end)
|
||||
|
||||
@api.model
|
||||
def _calculate_line_score(self, line, order, late_days):
|
||||
score = line.on_time_score
|
||||
if late_days > 0:
|
||||
score -= late_days * line.late_penalty_per_day
|
||||
score = max(score, line.minimum_score)
|
||||
if line.transaction_amount_threshold and order.amount_total >= line.transaction_amount_threshold:
|
||||
score += line.transaction_bonus_score
|
||||
return score
|
||||
|
||||
@api.model
|
||||
def _upsert_value(self, line, value, ref_model, ref_id):
|
||||
value_model = self.env["kpi.value"].sudo()
|
||||
existing = value_model.search(
|
||||
[
|
||||
("assignment_id", "=", line.assignment_id.id),
|
||||
("reference_model", "=", ref_model),
|
||||
("reference_id", "=", ref_id),
|
||||
("source_module", "=", line.source_module),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
vals = {
|
||||
"assignment_id": line.assignment_id.id,
|
||||
"value": value,
|
||||
"source_module": line.source_module,
|
||||
"reference_model": ref_model,
|
||||
"reference_id": ref_id,
|
||||
}
|
||||
if existing:
|
||||
existing.write({"value": value})
|
||||
return existing
|
||||
return value_model.create(vals)
|
||||
|
||||
@api.model
|
||||
def process_paid_orders(self, orders):
|
||||
if not orders:
|
||||
return False
|
||||
|
||||
for order in orders:
|
||||
if not order.business_category_id or not order.user_id or not order._is_kpi_sales_fully_paid():
|
||||
continue
|
||||
|
||||
payment_date = order._get_kpi_sales_payment_completion_date()
|
||||
if not payment_date:
|
||||
continue
|
||||
|
||||
employee = self._find_employee_from_user(order.user_id)
|
||||
if not employee:
|
||||
continue
|
||||
|
||||
rules = self.search(
|
||||
[
|
||||
("active", "=", True),
|
||||
("business_category_id", "=", order.business_category_id.id),
|
||||
]
|
||||
)
|
||||
late_days = order._get_kpi_sales_late_days(payment_date)
|
||||
for rule in rules:
|
||||
lines = rule.line_ids.filtered(
|
||||
lambda l: l.active
|
||||
and l.employee_id.id == employee.id
|
||||
and self._assignment_matches_date(l.assignment_id, payment_date)
|
||||
)
|
||||
for line in lines:
|
||||
score = self._calculate_line_score(line, order, late_days)
|
||||
self._upsert_value(
|
||||
line=line,
|
||||
value=score,
|
||||
ref_model="sale.order.payment.rule.line.%s" % line.id,
|
||||
ref_id=order.id,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
class KpiSalesTriggerRuleLine(models.Model):
|
||||
_name = "kpi.sales.trigger.rule.line"
|
||||
_description = "KPI Sales Trigger Rule Line"
|
||||
_order = "sequence, id"
|
||||
|
||||
rule_id = fields.Many2one("kpi.sales.trigger.rule", required=True, ondelete="cascade", index=True)
|
||||
sequence = fields.Integer(default=10)
|
||||
active = fields.Boolean(default=True)
|
||||
employee_id = fields.Many2one("hr.employee", required=True, ondelete="cascade", index=True)
|
||||
assignment_id = fields.Many2one("kpi.assignment", required=True, ondelete="cascade", index=True)
|
||||
on_time_score = fields.Float(required=True, default=1.0)
|
||||
late_penalty_per_day = fields.Float(default=0.0)
|
||||
minimum_score = fields.Float(default=0.0)
|
||||
transaction_amount_threshold = fields.Float(default=0.0)
|
||||
transaction_bonus_score = fields.Float(default=0.0)
|
||||
source_module = fields.Char(default="sale", required=True)
|
||||
note = fields.Char()
|
||||
|
||||
@api.constrains("employee_id", "assignment_id")
|
||||
def _check_assignment_employee(self):
|
||||
for rec in self:
|
||||
if rec.assignment_id.employee_id != rec.employee_id:
|
||||
raise ValidationError(_("Employee must match KPI Assignment employee."))
|
||||
|
||||
@api.constrains("on_time_score", "late_penalty_per_day", "minimum_score", "transaction_amount_threshold", "transaction_bonus_score")
|
||||
def _check_score_values(self):
|
||||
for rec in self:
|
||||
if rec.on_time_score < 0:
|
||||
raise ValidationError(_("On-time score must be zero or greater."))
|
||||
if rec.late_penalty_per_day < 0:
|
||||
raise ValidationError(_("Late penalty per day must be zero or greater."))
|
||||
if rec.minimum_score < 0:
|
||||
raise ValidationError(_("Minimum score must be zero or greater."))
|
||||
if rec.transaction_amount_threshold < 0:
|
||||
raise ValidationError(_("Transaction amount threshold must be zero or greater."))
|
||||
if rec.transaction_bonus_score < 0:
|
||||
raise ValidationError(_("Transaction bonus score must be zero or greater."))
|
||||
@@ -0,0 +1,37 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = "sale.order"
|
||||
|
||||
def _get_kpi_sales_customer_invoices(self):
|
||||
self.ensure_one()
|
||||
return self.invoice_ids.filtered(lambda inv: inv.move_type == "out_invoice" and inv.state == "posted")
|
||||
|
||||
def _is_kpi_sales_fully_paid(self):
|
||||
self.ensure_one()
|
||||
invoices = self._get_kpi_sales_customer_invoices()
|
||||
return bool(invoices) and all(invoice.payment_state == "paid" for invoice in invoices)
|
||||
|
||||
def _get_kpi_sales_payment_completion_date(self):
|
||||
self.ensure_one()
|
||||
invoices = self._get_kpi_sales_customer_invoices().filtered(lambda inv: inv.payment_state == "paid")
|
||||
payment_dates = []
|
||||
for invoice in invoices:
|
||||
payment_dates.extend(invoice._get_kpi_sales_payment_dates())
|
||||
payment_dates = [date_value for date_value in payment_dates if date_value]
|
||||
return max(payment_dates) if payment_dates else False
|
||||
|
||||
def _get_kpi_sales_due_date(self):
|
||||
self.ensure_one()
|
||||
invoices = self._get_kpi_sales_customer_invoices()
|
||||
due_dates = [invoice.invoice_date_due or invoice.invoice_date for invoice in invoices if invoice.invoice_date_due or invoice.invoice_date]
|
||||
return max(due_dates) if due_dates else False
|
||||
|
||||
def _get_kpi_sales_late_days(self, payment_date=False):
|
||||
self.ensure_one()
|
||||
payment_date = payment_date or self._get_kpi_sales_payment_completion_date()
|
||||
due_date = self._get_kpi_sales_due_date()
|
||||
if not payment_date or not due_date or payment_date <= due_date:
|
||||
return 0
|
||||
return (fields.Date.to_date(payment_date) - fields.Date.to_date(due_date)).days
|
||||
@@ -0,0 +1,7 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_kpi_sales_trigger_rule_kpi_manager,kpi.sales.trigger.rule.kpi.manager,model_kpi_sales_trigger_rule,odoo_kpi.group_kpi_manager,1,1,1,1
|
||||
access_kpi_sales_trigger_rule_kpi_admin,kpi.sales.trigger.rule.kpi.admin,model_kpi_sales_trigger_rule,odoo_kpi.group_kpi_admin,1,1,1,1
|
||||
access_kpi_sales_trigger_rule_sale_manager,kpi.sales.trigger.rule.sale.manager,model_kpi_sales_trigger_rule,sales_team.group_sale_manager,1,1,1,1
|
||||
access_kpi_sales_trigger_rule_line_kpi_manager,kpi.sales.trigger.rule.line.kpi.manager,model_kpi_sales_trigger_rule_line,odoo_kpi.group_kpi_manager,1,1,1,1
|
||||
access_kpi_sales_trigger_rule_line_kpi_admin,kpi.sales.trigger.rule.line.kpi.admin,model_kpi_sales_trigger_rule_line,odoo_kpi.group_kpi_admin,1,1,1,1
|
||||
access_kpi_sales_trigger_rule_line_sale_manager,kpi.sales.trigger.rule.line.sale.manager,model_kpi_sales_trigger_rule_line,sales_team.group_sale_manager,1,1,1,1
|
||||
|
@@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_kpi_sales_trigger_rule_tree" model="ir.ui.view">
|
||||
<field name="name">kpi.sales.trigger.rule.tree</field>
|
||||
<field name="model">kpi.sales.trigger.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="sequence"/>
|
||||
<field name="name"/>
|
||||
<field name="business_category_id"/>
|
||||
<field name="active"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_kpi_sales_trigger_rule_form" model="ir.ui.view">
|
||||
<field name="name">kpi.sales.trigger.rule.form</field>
|
||||
<field name="model">kpi.sales.trigger.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="sequence"/>
|
||||
<field name="business_category_id"/>
|
||||
<field name="active"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Trigger Lines">
|
||||
<field name="line_ids">
|
||||
<tree editable="bottom">
|
||||
<field name="sequence"/>
|
||||
<field name="active"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="assignment_id"/>
|
||||
<field name="on_time_score"/>
|
||||
<field name="late_penalty_per_day"/>
|
||||
<field name="minimum_score"/>
|
||||
<field name="transaction_amount_threshold"/>
|
||||
<field name="transaction_bonus_score"/>
|
||||
<field name="source_module"/>
|
||||
<field name="note"/>
|
||||
</tree>
|
||||
<form>
|
||||
<group>
|
||||
<field name="sequence"/>
|
||||
<field name="active"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="assignment_id" domain="[('employee_id', '=', employee_id)]"/>
|
||||
<field name="source_module"/>
|
||||
</group>
|
||||
<group string="Scoring">
|
||||
<field name="on_time_score"/>
|
||||
<field name="late_penalty_per_day"/>
|
||||
<field name="minimum_score"/>
|
||||
<field name="transaction_amount_threshold"/>
|
||||
<field name="transaction_bonus_score"/>
|
||||
<field name="note"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_kpi_sales_trigger_rule" model="ir.actions.act_window">
|
||||
<field name="name">KPI Sales Triggers</field>
|
||||
<field name="res_model">kpi.sales.trigger.rule</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_kpi_sales_trigger_rule"
|
||||
name="KPI Sales Triggers"
|
||||
parent="sale.sale_order_menu"
|
||||
action="action_kpi_sales_trigger_rule"
|
||||
sequence="96"
|
||||
groups="odoo_kpi.group_kpi_manager,odoo_kpi.group_kpi_admin,sales_team.group_sale_manager"/>
|
||||
</odoo>
|
||||
@@ -0,0 +1,26 @@
|
||||
@echo off
|
||||
echo ================================================================
|
||||
echo UPGRADE ODOO KPI SALES TRIGGER
|
||||
echo ================================================================
|
||||
echo.
|
||||
echo Module : odoo_kpi_sales_trigger
|
||||
echo Database: kanjabung_MRP
|
||||
echo Port : 8070
|
||||
echo.
|
||||
pause
|
||||
|
||||
echo.
|
||||
echo [1/2] Upgrading module...
|
||||
c:\odoo14c\python\python.exe C:\odoo14c\server\odoo-bin -c C:\addon14\odoo.conf -d kanjabung_MRP -u odoo_kpi_sales_trigger --stop-after-init
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
echo Upgrade gagal.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo [2/2] Selesai.
|
||||
echo Buka Odoo di http://localhost:8070 lalu cek menu Sales ^> KPI Sales Triggers
|
||||
echo.
|
||||
pause
|
||||
Reference in New Issue
Block a user