Pembuatan mrp business category dan scada business category

This commit is contained in:
2026-04-11 22:10:50 +07:00
parent ff09758164
commit 7eacc0bf95
75 changed files with 5731 additions and 46 deletions
@@ -0,0 +1,556 @@
# Blueprint Manufacturing Business Category
## Tujuan
Dokumen ini merancang penerapan `business category` untuk manufacturing agar pemisahan berlaku end-to-end pada:
- master produk bahan baku
- master produk jadi / by-product
- BoM
- Manufacturing Order
- raw material moves
- finished goods moves
- stock valuation
- journal valuation / costing
- overhead costing
- reporting per business category
Target implementasi mengikuti pola modul existing:
- `grt_business_category_base`
- `grt_inventory_business_category`
- `grt_mrp_overhead_costing`
- `grt_scada`
- `manufacturing_serial_number`
Dokumen ini sengaja memilih desain yang bisa diimplementasikan bertahap, aman untuk kondisi production saat ini: `product` dan `BoM` sudah ada, tetapi `MO` belum berjalan.
## Ringkasan Keputusan Desain
### Keputusan utama
1. Manufacturing akan memakai modul baru: `grt_mrp_business_category`.
2. `Business Category` menjadi field eksplisit di `mrp.bom` dan `mrp.production`.
3. `stock.move` manufacturing tidak boleh lagi bergantung ke `picking_id.business_category_id` saja.
4. Costing dan valuation manufacturing harus membawa `business_category_id` langsung dari move / MO.
5. `grt_mrp_overhead_costing` akan diubah agar basis filter memakai `mrp.production.business_category_id`, bukan `product.business_category_id`.
### Prinsip desain
- Satu transaksi manufacturing harus punya satu `business_category_id` yang jelas.
- Semua turunan transaksi harus mewarisi category yang sama: MO, raw move, finished move, SVL, journal valuation.
- Domain, validasi, dan record rule harus konsisten.
- Desain v1 mengutamakan segregasi yang kuat dan implementasi aman, bukan fleksibilitas maksimal.
## Batasan Penting
Arsitektur business category yang sekarang di repo memakai `product.template.business_category_id` tunggal. Itu cocok untuk segregasi ketat, tetapi kurang ideal bila satu master bahan baku ingin dipakai lintas business category.
Karena itu untuk implementasi v1, kebijakan yang direkomendasikan adalah:
- setiap produk operasional punya satu `business_category_id`
- setiap BoM produksi punya satu `business_category_id`
- jika bahan baku yang sama dipakai beberapa business category, buat produk terpisah per business category
Contoh:
- `RM GULA - BC01`
- `RM GULA - BC02`
Pendekatan ini paling konsisten dengan rule inventory yang sudah ada dan paling aman untuk costing, valuation, dan audit trail.
Catatan:
- desain shared-product lintas business category bisa dibuat pada fase lanjutan
- tetapi itu butuh perubahan lebih besar pada rule `product`, `stock.quant`, dan `replenishment`
## Modul Baru
### Nama modul
- `grt_mrp_business_category`
### Dependency
- `mrp`
- `stock_account`
- `grt_business_category_base`
- `grt_inventory_business_category`
- `grt_mrp_overhead_costing`
### Dependency opsional yang harus kompatibel
- `grt_scada`
- `manufacturing_serial_number`
## Cakupan Model
### 1. `mrp.bom`
Tambahan field:
- `business_category_id = fields.Many2one('crm.business.category', required=True, ondelete='restrict', index=True)`
- `manufacturing_analytic_account_id = fields.Many2one(related='business_category_id.inventory_analytic_account_id', store=True, readonly=True)`
Aturan:
- category harus satu company dengan BoM
- produk hasil BoM harus punya `business_category_id` yang sama dengan BoM
- seluruh `bom_line_ids.product_id.business_category_id` harus sama dengan BoM
- jika ada by-product, category harus sama dengan BoM
Alasan:
- BoM adalah resep per business category
- satu produk jadi bisa punya BoM berbeda per business category
### 2. `mrp.production`
Tambahan field:
- `business_category_id = fields.Many2one('crm.business.category', required=True, ondelete='restrict', tracking=True, index=True)`
- `manufacturing_analytic_account_id = fields.Many2one(related='business_category_id.inventory_analytic_account_id', store=True, readonly=True)`
- `mrp_team_id = fields.Many2one('mrp.team', tracking=True)`
Aturan default:
- prioritas default dari `bom_id.business_category_id`
- jika belum ada BoM, fallback ke user `active_business_category_id`
- jika create dari warehouse / operation type, fallback ke `warehouse.business_category_id`
Aturan validasi:
- `business_category_id.company_id` harus sama dengan `company_id`
- `bom_id.business_category_id` harus sama dengan `mrp.production.business_category_id`
- `product_id.business_category_id` harus sama dengan `mrp.production.business_category_id`
- seluruh `move_raw_ids` dan `move_finished_ids` harus punya `business_category_id` yang sama dengan MO
- jika `mrp_team_id` dipakai, team harus sama company dan sama business category
### 3. `mrp.bom.line`
Tambahan field:
- `business_category_id = fields.Many2one(related='bom_id.business_category_id', store=True, readonly=True)`
Tidak perlu field edit terpisah.
Aturan:
- `product_id.business_category_id` harus sama dengan `bom_id.business_category_id`
### 4. `stock.move`
Perubahan paling penting di desain ini.
Field existing dari `grt_inventory_business_category` harus diubah dari related ke `picking_id` saja menjadi field stored yang bisa diisi dari beberapa sumber.
Target desain:
- `business_category_id = fields.Many2one('crm.business.category', compute='_compute_business_category_id', store=True, readonly=False, index=True)`
- `inventory_analytic_account_id = fields.Many2one(related='business_category_id.inventory_analytic_account_id', store=True, readonly=True)`
Sumber nilai `business_category_id`:
1. `picking_id.business_category_id`
2. `raw_material_production_id.business_category_id`
3. `production_id.business_category_id`
4. warehouse / location fallback bila perlu
Aturan:
- raw move MO mewarisi dari `raw_material_production_id.business_category_id`
- finished move MO mewarisi dari `production_id.business_category_id`
- transfer biasa tetap mewarisi dari `picking_id.business_category_id`
Konsekuensi:
- rule access `stock.move` existing jadi aman untuk transaksi manufacturing
- valuation dan reporting inventory bisa konsisten
### 5. `stock.move.line`
Tambahan field:
- `business_category_id = fields.Many2one(related='move_id.business_category_id', store=True, readonly=True, index=True)`
Tujuan:
- memudahkan filter detail operation per category
- memudahkan audit lot/serial per category
### 6. `stock.valuation.layer`
Tambahan field:
- `business_category_id = fields.Many2one('crm.business.category', copy=False, index=True)`
- `inventory_analytic_account_id = fields.Many2one('account.analytic.account', copy=False)`
Sumber nilai:
- dari `stock.move.business_category_id`
Tujuan:
- valuation layer finished goods dan raw material consumption bisa dilaporkan per category
- overhead kapitalisasi bisa disandingkan dengan valuation layer dasar
### 7. `account.move` dan `account.move.line`
Gunakan field inventory existing bila memungkinkan:
- `account.move.inventory_business_category_id`
- `account.move.inventory_analytic_account_id`
Tambahan perilaku:
- valuation journal hasil raw material consumption dan finished goods harus mengisi `inventory_business_category_id`
- journal item valuation harus mengisi analytic sesuai business category bila account bukan receivable/payable
Jika Odoo flow existing belum cukup, tambahkan helper override di `stock.move` untuk menyuntikkan:
- `inventory_business_category_id`
- `inventory_analytic_account_id`
Tujuan:
- cost of consumption dan finished goods capitalization bisa ditelusuri per business category
### 8. `mrp.workorder` dan `mrp.workcenter`
Direkomendasikan untuk v1.1, bukan blocker v1.
Tambahan field:
- `mrp.workcenter.business_category_id` opsional
- `mrp.workorder.business_category_id` related ke production
Aturan:
- jika workcenter punya category, harus sama dengan MO
- jika kosong, workcenter dianggap shared service center
## Model Baru `mrp.team`
### Tujuan
Menyelaraskan manufacturing dengan pola team pada CRM, Sales, Purchase, Expense, dan Inventory.
### Field utama
- `name`
- `company_id`
- `business_category_id`
- `user_id`
- `member_ids`
- `active`
### Fungsi
- membatasi user manufacturing per business category
- defaulting team pada MO
- sumber akses otomatis ke user `effective_business_category_ids`
## Propagasi Data
### Alur master data
`Business Category` -> `Product` -> `BoM` -> `MO` -> `Stock Move` -> `SVL` -> `Account Move`
### Rule propagasi detail
#### Product
- saat membuat product manufacturing, default category dari user active category
- company product mengikuti company category
#### BoM
- saat membuat BoM dari product, default category dari product
- domain product hanya menampilkan product dengan category yang sama
#### MO
- saat pilih product, sistem cari BoM dengan kombinasi:
- `product_tmpl_id/product_id`
- `company_id`
- `business_category_id`
- jika user memilih BoM, category MO ikut dari BoM
- jika category MO diubah, BoM yang tidak match harus direset
#### Raw Move
- `_get_move_raw_values()` harus set `business_category_id = self.business_category_id.id`
- move hasil explode BoM harus tetap satu category dengan MO
#### Finished Move
- `_get_move_finished_values()` atau hook sejenis harus set `business_category_id = self.business_category_id.id`
#### By-product
- by-product move juga wajib ikut category MO
#### Stock Valuation
- valuation raw material consumption dan FG receipt wajib bawa category move
## Record Rules
### Model yang harus punya rule
- `mrp.bom`
- `mrp.production`
- `mrp.workorder`
- `mrp.team`
- `stock.move`
- `stock.move.line`
- `stock.valuation.layer`
### Prinsip rule
Format mengikuti modul existing:
- user hanya bisa baca/create/write record pada `effective_business_category_ids`
- manager manufacturing dapat full access dalam category miliknya
- `base.group_system` tetap full access per company
### Catatan penting
Rule `stock.move` current dari inventory harus di-upgrade. Saat ini move manufacturing berisiko tidak terbaca karena category hanya related dari `picking_id`.
## Form View dan UX
### `mrp.bom`
Tambahkan:
- field `business_category_id` di header
- search filter group by business category
- domain komponen hanya menampilkan product dengan category sama
### `mrp.production`
Tambahkan:
- field `business_category_id`
- field `mrp_team_id`
- search filter business category
- group by business category
Pada raw materials dan finished moves:
- tampilkan `business_category_id` readonly untuk audit
### Report
Tambahkan pivot / graph / search filter business category untuk:
- MO analysis
- production reporting
- valuation reporting bila ada custom report
## Costing dan Value Flow
### Raw material consumption
Saat MO consume bahan:
- `stock.move` raw material punya `business_category_id` dari MO
- SVL keluar bahan baku mewarisi category itu
- journal valuation keluar bahan baku memakai `inventory_business_category_id`
### Finished goods receipt
Saat MO selesai:
- finished move punya `business_category_id` dari MO
- SVL finished goods mewarisi category itu
- journal valuation masuk FG memakai `inventory_business_category_id`
### Overhead costing
Modul `grt_mrp_overhead_costing` perlu disesuaikan:
#### Yang harus diubah
- filter MO done harus memakai `mrp.production.business_category_id`
- bukan lagi `product_id.business_category_id`
#### Dampak
- overhead period akan mencerminkan category operasi aktual
- tidak tergantung apakah product category dipakai sebagai master segregation atau tidak
## Integrasi Dengan Modul Existing
### `grt_inventory_business_category`
Perubahan wajib:
- ubah field `stock.move.business_category_id`
- review rule `stock.quant`
- review product rules bila nanti ada kebutuhan shared product lintas category
### `grt_mrp_overhead_costing`
Perubahan wajib:
- basis filter MO ke `mrp.production.business_category_id`
- allocation line dan jurnal adjustment tetap kompatibel dengan inventory category
### `grt_scada`
Kompatibilitas:
- `_get_move_raw_values()` existing SCADA harus tetap berjalan
- penambahan `business_category_id` di raw move harus merge, bukan overwrite
- validasi SCADA equipment per MO tetap utuh
### `manufacturing_serial_number`
Kompatibilitas:
- serial yang dibuat dari MO secara otomatis mewarisi company dari MO
- jika nanti perlu audit lebih dalam, `manufacturing.serial` bisa ditambah field related `business_category_id`
## Strategi Migrasi
## Kondisi awal production
- product sudah ada
- BoM sudah ada
- MO belum berjalan
Ini adalah kondisi yang paling aman untuk implementasi manufacturing business category.
### Tahap 1. Persiapan master category
1. pastikan semua `crm.business.category` final
2. pastikan user access, default, dan active category benar
3. pastikan warehouse sudah punya `business_category_id`
### Tahap 2. Backfill product
1. identifikasi product manufacturing yang masih kosong category
2. isi category sesuai business unit pemilik
3. validasi company-category match
### Tahap 3. Backfill BoM
1. semua `mrp.bom` existing diisi `business_category_id`
2. default awal mengikuti finished product
3. cek seluruh bom line agar komponen punya category sama
4. jika ada komponen shared lintas BU, putuskan:
- duplicate product per BU, atau
- tunda sampai desain shared-product fase lanjut
### Tahap 4. Aktivasi manufacturing category
1. install `grt_mrp_business_category`
2. upgrade `grt_inventory_business_category`
3. upgrade `grt_mrp_overhead_costing`
4. lakukan smoke test create-confirm-done MO
### Tahap 5. Validasi accounting
Uji:
- raw material consumption
- finished goods receipt
- stock valuation layer
- account move valuation
- overhead allocation
## Query Audit Yang Disarankan
### Produk tanpa category
```sql
SELECT id, name, company_id
FROM product_template
WHERE active IS TRUE
AND business_category_id IS NULL;
```
### BoM tanpa category
```sql
SELECT id, product_tmpl_id, product_id, company_id
FROM mrp_bom
WHERE business_category_id IS NULL;
```
### BoM line beda category dengan header
```sql
SELECT
b.id AS bom_id,
b.business_category_id AS bom_category,
l.id AS bom_line_id,
pt.business_category_id AS component_category
FROM mrp_bom_line l
JOIN mrp_bom b ON b.id = l.bom_id
JOIN product_product pp ON pp.id = l.product_id
JOIN product_template pt ON pt.id = pp.product_tmpl_id
WHERE pt.business_category_id IS DISTINCT FROM b.business_category_id;
```
## Fase Implementasi Yang Direkomendasikan
### Fase A. Minimum safe implementation
Scope:
- `mrp.bom`
- `mrp.production`
- `stock.move`
- `stock.move.line`
- security rules MRP
- costing / valuation propagation
- patch `grt_mrp_overhead_costing`
Outcome:
- segregation manufacturing sudah jalan end-to-end
### Fase B. Operational hardening
Scope:
- `mrp.team`
- `mrp.workorder`
- `mrp.workcenter`
- report business category manufacturing
Outcome:
- operasional user dan monitoring lebih rapi
### Fase C. Advanced flexibility
Scope:
- desain shared raw material lintas business category
- relaksasi rule product dan quant bila dibutuhkan
Outcome:
- fleksibilitas lebih tinggi, tetapi kompleksitas juga naik
## Rekomendasi Final
Untuk kasus saat ini, implementasi yang paling aman dan cepat adalah:
1. bangun `grt_mrp_business_category`
2. pakai segregasi ketat satu product satu business category
3. backfill BoM sebelum MO pertama dibuat
4. ubah propagation `stock.move` agar support manufacturing
5. patch `grt_mrp_overhead_costing` agar filter berdasarkan `mrp.production.business_category_id`
Dengan pendekatan ini:
- raw material terpisah per business category
- finished goods terpisah per business category
- MO terpisah per business category
- costing dan valuation bisa ditelusuri per business category
- migrasi aman karena production belum punya MO aktif
@@ -101,6 +101,10 @@ class ResUsers(models.Model):
"stock.team",
[("user_id", "=", user_id), ("member_ids", "in", user_id)],
)
categories |= self._get_team_business_categories_from_model(
"mrp.team",
[("user_id", "=", user_id), ("member_ids", "in", user_id)],
)
return categories
def _filter_business_categories_by_company(self, categories):
@@ -36,6 +36,12 @@ class AccountMove(models.Model):
def _prepare_inventory_business_category_vals(self, vals):
vals = dict(vals)
if not vals.get("inventory_business_category_id") and vals.get("stock_move_id"):
stock_move = self.env["stock.move"].browse(vals["stock_move_id"])
if stock_move.business_category_id:
vals["inventory_business_category_id"] = stock_move.business_category_id.id
if stock_move.inventory_analytic_account_id and not vals.get("inventory_analytic_account_id"):
vals["inventory_analytic_account_id"] = stock_move.inventory_analytic_account_id.id
if vals.get("inventory_business_category_id") and not vals.get("inventory_analytic_account_id"):
category = self.env["crm.business.category"].browse(vals["inventory_business_category_id"])
if category.inventory_analytic_account_id:
@@ -139,9 +139,8 @@ class StockMove(models.Model):
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
related="picking_id.business_category_id",
store=True,
readonly=True,
index=True,
)
stock_team_id = fields.Many2one(
"stock.team",
@@ -153,11 +152,51 @@ class StockMove(models.Model):
inventory_analytic_account_id = fields.Many2one(
"account.analytic.account",
string="Inventory Analytic Account",
related="picking_id.inventory_analytic_account_id",
related="business_category_id.inventory_analytic_account_id",
store=True,
readonly=True,
)
@api.model
def _get_business_category_id_from_vals(self, vals):
category_id = vals.get("business_category_id")
if category_id:
return category_id
if vals.get("picking_id"):
picking = self.env["stock.picking"].browse(vals["picking_id"])
if picking.business_category_id:
return picking.business_category_id.id
if "raw_material_production_id" in self._fields and vals.get("raw_material_production_id"):
production = self.env["mrp.production"].browse(vals["raw_material_production_id"])
if production.business_category_id:
return production.business_category_id.id
if "production_id" in self._fields and vals.get("production_id"):
production = self.env["mrp.production"].browse(vals["production_id"])
if production.business_category_id:
return production.business_category_id.id
return False
@api.model
def _prepare_business_category_vals(self, vals):
vals = dict(vals)
category_id = self._get_business_category_id_from_vals(vals)
if category_id:
vals["business_category_id"] = category_id
return vals
@api.model_create_multi
def create(self, vals_list):
vals_list = [self._prepare_business_category_vals(vals) for vals in vals_list]
return super().create(vals_list)
def write(self, vals):
vals = self._prepare_business_category_vals(vals)
return super().write(vals)
def _get_new_picking_values(self):
vals = super()._get_new_picking_values()
sale_order = self.sale_line_id.order_id if "sale_line_id" in self._fields else self.env["sale.order"]
+229
View File
@@ -0,0 +1,229 @@
# GRT MRP Business Category
## Ringkasan
`grt_mrp_business_category` menambahkan segregasi `Business Category` pada area Manufacturing Odoo.
Modul ini melengkapi arsitektur business category yang sebelumnya sudah ada di:
- `grt_business_category_base`
- `grt_inventory_business_category`
- `grt_mrp_overhead_costing`
Fokus modul ini adalah memastikan:
- `BoM` punya `business_category_id`
- `Manufacturing Order` punya `business_category_id`
- raw material move dan finished move MO mengikuti category MO
- team manufacturing bisa dipisah per business category
- valuation trace manufacturing tetap konsisten terhadap category inventory
## Tujuan
Modul ini dibuat untuk kebutuhan segregasi manufacturing lintas business unit dengan model yang ketat dan aman.
Tujuan utamanya:
- memisahkan data `BoM` per business category
- memisahkan `MO` per business category
- menjaga raw material dan finished goods tetap berada pada category yang benar
- menjaga costing dan valuation inventory tetap membawa category yang sama
- mencegah user mengakses atau memakai data manufacturing di luar `effective_business_category_ids`
## Dependensi
Modul bergantung pada:
- `mrp`
- `stock_account`
- `grt_business_category_base`
- `grt_inventory_business_category`
- `grt_mrp_overhead_costing`
## Cakupan Model
### `mrp.bom`
Penambahan utama:
- `business_category_id`
- `manufacturing_analytic_account_id`
Perilaku:
- default category diambil dari product atau product template
- `company_id` akan mengikuti company dari category jika belum diisi
- BoM harus konsisten dengan category finished product
- seluruh component dan by-product harus memakai category yang sama dengan BoM
### `mrp.production`
Penambahan utama:
- `business_category_id`
- `manufacturing_analytic_account_id`
- `mrp_team_id`
Perilaku:
- default category diambil dari `mrp_team`, `BoM`, product, atau warehouse picking type
- sistem mencoba mencari `BoM` yang cocok berdasarkan product + business category
- `mrp_team_id` harus berasal dari company dan business category yang sama
- `action_confirm` dan `button_mark_done` akan menolak MO yang belum punya category
### `mrp.team`
Model baru untuk tim manufacturing per category.
Field utama:
- `name`
- `company_id`
- `business_category_id`
- `user_id`
- `member_ids`
Fungsi:
- membatasi pemakaian team sesuai category
- mewariskan akses category ke user melalui mekanisme `effective_business_category_ids`
### `mrp.bom.line`
Penambahan:
- `business_category_id` related ke `bom_id.business_category_id`
Fungsi:
- memudahkan audit dan grouping line berdasarkan category BoM
### `stock.move.line`
Penambahan:
- `business_category_id` related ke `move_id.business_category_id`
### `stock.valuation.layer`
Penambahan:
- `business_category_id` related ke `stock_move_id.business_category_id`
- `inventory_analytic_account_id` related ke `stock_move_id.inventory_analytic_account_id`
Fungsi:
- memperjelas audit valuation layer hasil manufacturing
## Alur Data Category
### Saat membuat BoM
Urutan default category:
1. `product_id.business_category_id`
2. `product_tmpl_id.business_category_id`
3. default dari `business.category.mixin`
### Saat membuat MO
Urutan default category:
1. `mrp_team_id.business_category_id`
2. `bom_id.business_category_id`
3. `product_id.business_category_id`
4. `warehouse.business_category_id`
5. default dari `business.category.mixin`
Jika `bom_id` belum diisi, modul akan mencoba mencari BoM yang match terhadap:
- product
- business category
- company
### Saat flow inventory manufacturing berjalan
Kategori manufacturing dipakai sebagai sumber utama untuk:
- raw material move MO
- finished move MO
- move line
- stock valuation layer
- account move valuation inventory melalui modul inventory business category
## Validasi Utama
Modul ini sengaja ketat.
Constraint penting:
- BoM wajib punya category jika terkait produk
- MO wajib punya category jika terkait produk
- `product.business_category_id` harus sama dengan `mrp.production.business_category_id`
- `bom.business_category_id` harus sama dengan `mrp.production.business_category_id`
- component BoM harus sama category dengan BoM
- raw move dan finished move tidak boleh beda category dengan MO
- `mrp_team.business_category_id` harus sama dengan category MO
- user non-admin hanya boleh memakai category yang ada di `effective_business_category_ids`
## Security Rule
Model yang dibatasi oleh business category:
- `mrp.bom`
- `mrp.bom.line`
- `mrp.bom.byproduct`
- `mrp.production`
- `stock.move.line`
- `stock.valuation.layer`
- `mrp.team`
- `crm.business.category` untuk user MRP
Prinsip rule:
- user MRP hanya bisa akses data pada company yang diizinkan
- user MRP hanya bisa akses data pada `effective_business_category_ids`
- system admin tetap punya akses penuh lintas category dalam company yang diizinkan
## Integrasi dengan Modul Lain
### `grt_inventory_business_category`
Modul ini bergantung pada propagasi `stock.move.business_category_id` agar:
- raw material issue dari MO ikut category MO
- finished goods receipt dari MO ikut category MO
- valuation journal inventory tetap punya `inventory_business_category_id`
### `grt_mrp_overhead_costing`
Modul overhead costing sudah dipatch agar filter MO done lebih mengutamakan:
- `mrp.production.business_category_id`
Bukan hanya:
- `product.business_category_id`
## Migrasi Existing Data
`post_init_hook` melakukan backfill untuk data existing.
Urutan backfill:
1. isi `product_template.business_category_id` kosong berdasarkan default category company
2. isi `mrp_bom.business_category_id` dari product atau product template
3. isi `mrp_production.business_category_id` dari BoM atau product
4. isi `stock_move.business_category_id` dari MO raw/finished
5. isi `account_move.inventory_business_category_id` dari `stock_move`
Catatan:
- pendekatan ini aman untuk database yang sudah punya product dan BoM
- tetap lebih aman diuji dulu di staging sebelum upgrade production
## Batasan Desain V1
Desain v1 sengaja konservatif.
Asumsi utama:
- satu product operasional mewakili satu business category
- satu BoM mewakili satu business category
- satu MO mewakili satu business category
Implikasi:
- jika satu bahan baku ingin dipakai lintas BU, paling aman dibuat product terpisah per BU
- model ini mengutamakan keamanan segregasi, bukan fleksibilitas sharing master data lintas BU
## View dan UI
Modul menambahkan field category pada:
- form dan tree `BoM`
- form dan tree `Manufacturing Order`
- form dan tree `Manufacturing Team`
- search `BoM` (filter dan group by business category)
- search `Manufacturing Order` (filter dan group by business category)
Tujuan:
- memudahkan audit
- memudahkan filter dan grouping
- memastikan user melihat context category saat operasional
- menjaga list, pivot, dan graph MRP tetap bisa dipisah per business category melalui search filter/group by
## Urutan Implementasi yang Disarankan
1. pastikan `grt_business_category_base` dan `grt_inventory_business_category` sudah stabil
2. pastikan product existing sudah memiliki category yang benar
3. upgrade `grt_mrp_business_category` di staging
4. review hasil backfill `BoM`, `MO`, dan `stock.move`
5. uji skenario lengkap raw material sampai finished goods
6. baru lanjut ke production
## Checklist Testing Staging
- buat BoM baru dan pastikan category terisi otomatis
- buat MO dari BoM dan pastikan category mengikuti BoM
- confirm MO dan cek raw move membawa category MO
- mark done MO dan cek finished move membawa category MO
- cek valuation layer hasil MO
- cek journal valuation inventory membawa `inventory_business_category_id`
- cek user BU A tidak bisa baca MO atau BoM BU B
- cek user BU A tidak bisa baca BoM line, by-product, move line, dan valuation layer BU B
- cek filter/group by business category pada list, pivot, dan graph Manufacturing Order
- cek overhead costing tetap membaca MO done sesuai category
## Dokumen Terkait
- [MANUFACTURING_BUSINESS_CATEGORY_BLUEPRINT.md](../MANUFACTURING_BUSINESS_CATEGORY_BLUEPRINT.md)
+2
View File
@@ -0,0 +1,2 @@
from . import models
from .hooks import post_init_hook
+31
View File
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
{
"name": "GRT MRP Business Category",
"summary": "Business category segregation for manufacturing operations",
"description": """
Adds business category segregation to Manufacturing, including BoM and MO
master data, manufacturing teams, and business category propagation to
manufacturing stock moves and valuation traces.
""",
"author": "PT Gagak Rimang Teknologi",
"website": "https://rimang.id",
"category": "Manufacturing",
"version": "14.0.1.0.0",
"depends": [
"mrp",
"stock_account",
"grt_business_category_base",
"grt_inventory_business_category",
"grt_mrp_overhead_costing",
],
"post_init_hook": "post_init_hook",
"data": [
"security/ir.model.access.csv",
"security/ir.rule.csv",
"views/mrp_team_views.xml",
"views/mrp_bom_views.xml",
"views/mrp_production_views.xml",
],
"installable": True,
"application": False,
}
+183
View File
@@ -0,0 +1,183 @@
from odoo import SUPERUSER_ID, api
def _table_has_column(cr, table_name, column_name):
cr.execute(
"""
SELECT 1
FROM information_schema.columns
WHERE table_name = %s
AND column_name = %s
LIMIT 1
""",
[table_name, column_name],
)
return bool(cr.fetchone())
def _pick_default_category(env, company):
categories = env["crm.business.category"].sudo().search(
[("company_id", "=", company.id), ("active", "=", True)],
order="code, id",
)
coded = categories.filtered(lambda category: category.code == "BC01")
return coded[:1] or categories[:1]
def _backfill_product_categories(env):
cr = env.cr
if not _table_has_column(cr, "product_template", "business_category_id"):
return
for company in env["res.company"].sudo().search([]):
default_category = _pick_default_category(env, company)
if not default_category:
continue
cr.execute(
"""
UPDATE product_template
SET business_category_id = %s
WHERE business_category_id IS NULL
AND company_id = %s
""",
[default_category.id, company.id],
)
main_company = env.ref("base.main_company", raise_if_not_found=False) or env.company
default_category = _pick_default_category(env, main_company)
if default_category:
cr.execute(
"""
UPDATE product_template
SET business_category_id = %s
WHERE business_category_id IS NULL
AND company_id IS NULL
""",
[default_category.id],
)
def _backfill_bom_categories(env):
cr = env.cr
if not _table_has_column(cr, "mrp_bom", "business_category_id"):
return
cr.execute(
"""
UPDATE mrp_bom bom
SET business_category_id = pt.business_category_id
FROM product_product product
JOIN product_template pt ON pt.id = product.product_tmpl_id
WHERE bom.business_category_id IS NULL
AND bom.product_id = product.id
AND pt.business_category_id IS NOT NULL
"""
)
cr.execute(
"""
UPDATE mrp_bom bom
SET business_category_id = pt.business_category_id
FROM product_template pt
WHERE bom.business_category_id IS NULL
AND bom.product_tmpl_id = pt.id
AND pt.business_category_id IS NOT NULL
"""
)
cr.execute(
"""
UPDATE mrp_bom bom
SET company_id = category.company_id
FROM crm_business_category category
WHERE bom.business_category_id = category.id
AND bom.company_id IS NULL
"""
)
def _backfill_production_categories(env):
cr = env.cr
if not _table_has_column(cr, "mrp_production", "business_category_id"):
return
if _table_has_column(cr, "mrp_bom", "business_category_id"):
cr.execute(
"""
UPDATE mrp_production production
SET business_category_id = bom.business_category_id
FROM mrp_bom bom
WHERE production.business_category_id IS NULL
AND production.bom_id = bom.id
AND bom.business_category_id IS NOT NULL
"""
)
if _table_has_column(cr, "product_template", "business_category_id"):
cr.execute(
"""
UPDATE mrp_production production
SET business_category_id = pt.business_category_id
FROM product_product product
JOIN product_template pt ON pt.id = product.product_tmpl_id
WHERE production.business_category_id IS NULL
AND production.product_id = product.id
AND pt.business_category_id IS NOT NULL
"""
)
def _backfill_move_categories(env):
cr = env.cr
if not _table_has_column(cr, "stock_move", "business_category_id"):
return
if _table_has_column(cr, "stock_move", "raw_material_production_id"):
cr.execute(
"""
UPDATE stock_move move
SET business_category_id = production.business_category_id
FROM mrp_production production
WHERE move.business_category_id IS NULL
AND move.raw_material_production_id = production.id
AND production.business_category_id IS NOT NULL
"""
)
if _table_has_column(cr, "stock_move", "production_id"):
cr.execute(
"""
UPDATE stock_move move
SET business_category_id = production.business_category_id
FROM mrp_production production
WHERE move.business_category_id IS NULL
AND move.production_id = production.id
AND production.business_category_id IS NOT NULL
"""
)
def _backfill_account_move_categories(env):
cr = env.cr
if not _table_has_column(cr, "account_move", "inventory_business_category_id"):
return
if not _table_has_column(cr, "account_move", "stock_move_id"):
return
cr.execute(
"""
UPDATE account_move move
SET inventory_business_category_id = stock_move.business_category_id
FROM stock_move
WHERE move.inventory_business_category_id IS NULL
AND move.stock_move_id = stock_move.id
AND stock_move.business_category_id IS NOT NULL
"""
)
def post_init_hook(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
_backfill_product_categories(env)
_backfill_bom_categories(env)
_backfill_production_categories(env)
_backfill_move_categories(env)
_backfill_account_move_categories(env)
@@ -0,0 +1,5 @@
from . import mrp_bom
from . import mrp_production
from . import mrp_team
from . import stock_move_line
from . import stock_valuation_layer
+156
View File
@@ -0,0 +1,156 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class MrpBom(models.Model):
_inherit = "mrp.bom"
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
default=lambda self: self._default_business_category_id(),
ondelete="restrict",
index=True,
domain="[('company_id', '=', company_id)]",
)
manufacturing_analytic_account_id = fields.Many2one(
"account.analytic.account",
string="Manufacturing Analytic Account",
related="business_category_id.inventory_analytic_account_id",
store=True,
readonly=True,
)
@api.model
def _default_business_category_id(self):
product = self.env["product.product"].browse(self.env.context.get("default_product_id"))
if product and product.business_category_id:
return product.business_category_id.id
template = self.env["product.template"].browse(self.env.context.get("default_product_tmpl_id"))
if template and template.business_category_id:
return template.business_category_id.id
if "business.category.mixin" in self.env:
return self.env["business.category.mixin"]._default_business_category_id()
return False
@api.onchange("product_tmpl_id", "product_id")
def _onchange_product_business_category(self):
for bom in self:
product = bom.product_id or bom.product_tmpl_id.product_variant_id
template = bom.product_tmpl_id or product.product_tmpl_id
category = product.business_category_id or template.business_category_id
if category:
bom.business_category_id = category
if not bom.company_id and category.company_id:
bom.company_id = category.company_id
@api.onchange("business_category_id")
def _onchange_business_category_company(self):
for bom in self:
if bom.business_category_id and not bom.company_id:
bom.company_id = bom.business_category_id.company_id
@api.model_create_multi
def create(self, vals_list):
vals_list = [self._prepare_business_category_vals(vals) for vals in vals_list]
return super().create(vals_list)
def write(self, vals):
vals = self._prepare_business_category_vals(vals)
return super().write(vals)
@api.model
def _prepare_business_category_vals(self, vals):
vals = dict(vals)
category_id = vals.get("business_category_id")
product = self.env["product.product"].browse(vals["product_id"]) if vals.get("product_id") else False
template = self.env["product.template"].browse(vals["product_tmpl_id"]) if vals.get("product_tmpl_id") else False
if not category_id:
category = (
(product and product.business_category_id)
or (template and template.business_category_id)
or False
)
if category:
category_id = category.id
vals["business_category_id"] = category_id
if category_id and not vals.get("company_id"):
category = self.env["crm.business.category"].browse(category_id)
if category.company_id:
vals["company_id"] = category.company_id.id
return vals
@api.constrains("company_id", "business_category_id", "product_tmpl_id", "product_id", "bom_line_ids", "byproduct_ids")
def _check_business_category_consistency(self):
current_user = self.env.user
effective_categories = current_user.effective_business_category_ids
for bom in self:
if (bom.product_tmpl_id or bom.product_id) and not bom.business_category_id:
raise ValidationError(
_("BoM '%s' requires a Business Category.") % bom.display_name
)
if bom.company_id and bom.business_category_id and bom.business_category_id.company_id != bom.company_id:
raise ValidationError(
_("BoM '%s' must use a Business Category from the same company.") % bom.display_name
)
if bom.product_tmpl_id and bom.product_tmpl_id.business_category_id != bom.business_category_id:
raise ValidationError(
_("BoM '%s' must use the same Business Category as its finished product template.")
% bom.display_name
)
if bom.product_id and bom.product_id.business_category_id != bom.business_category_id:
raise ValidationError(
_("BoM '%s' must use the same Business Category as its finished product.") % bom.display_name
)
invalid_components = bom.bom_line_ids.filtered(
lambda line: line.product_id.business_category_id != bom.business_category_id
)[:1]
if invalid_components:
raise ValidationError(
_(
"Component '%s' on BoM '%s' must use the same Business Category as the BoM."
)
% (invalid_components.product_id.display_name, bom.display_name)
)
if "byproduct_ids" in bom._fields:
invalid_byproduct = bom.byproduct_ids.filtered(
lambda line: line.product_id.business_category_id != bom.business_category_id
)[:1]
if invalid_byproduct:
raise ValidationError(
_(
"By-product '%s' on BoM '%s' must use the same Business Category as the BoM."
)
% (invalid_byproduct.product_id.display_name, bom.display_name)
)
if (
not current_user.has_group("base.group_system")
and bom.business_category_id
and bom.business_category_id not in effective_categories
):
raise ValidationError(
_("You can only use BoMs in your registered Business Category.")
)
class MrpBomLine(models.Model):
_inherit = "mrp.bom.line"
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
related="bom_id.business_category_id",
store=True,
readonly=True,
)
@@ -0,0 +1,243 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class MrpProduction(models.Model):
_inherit = "mrp.production"
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
default=lambda self: self._default_business_category_id(),
ondelete="restrict",
tracking=True,
index=True,
domain="[('company_id', '=', company_id)]",
)
manufacturing_analytic_account_id = fields.Many2one(
"account.analytic.account",
string="Manufacturing Analytic Account",
related="business_category_id.inventory_analytic_account_id",
store=True,
readonly=True,
)
mrp_team_id = fields.Many2one(
"mrp.team",
string="Manufacturing Team",
tracking=True,
domain="[('company_id', '=', company_id), ('business_category_id', '=', business_category_id)]",
)
@api.model
def _default_business_category_id(self):
bom = self.env["mrp.bom"].browse(self.env.context.get("default_bom_id"))
if bom and bom.business_category_id:
return bom.business_category_id.id
product = self.env["product.product"].browse(self.env.context.get("default_product_id"))
if product and product.business_category_id:
return product.business_category_id.id
picking_type = self.env["stock.picking.type"].browse(self.env.context.get("default_picking_type_id"))
if picking_type.warehouse_id and picking_type.warehouse_id.business_category_id:
return picking_type.warehouse_id.business_category_id.id
if "business.category.mixin" in self.env:
return self.env["business.category.mixin"]._default_business_category_id()
return False
def _find_matching_bom(self, product=None, business_category=None, company=None):
self.ensure_one()
product = product or self.product_id
business_category = business_category or self.business_category_id
company = company or self.company_id or self.env.company
if not product or not business_category:
return self.env["mrp.bom"]
return self.env["mrp.bom"].search(
[
("type", "=", "normal"),
("business_category_id", "=", business_category.id),
("company_id", "in", [company.id, False]),
"|",
("product_id", "=", product.id),
"&",
("product_id", "=", False),
("product_tmpl_id", "=", product.product_tmpl_id.id),
],
order="sequence, id",
limit=1,
)
@api.onchange("product_id")
def _onchange_product_business_category(self):
for production in self:
if production.product_id and production.product_id.business_category_id:
production.business_category_id = production.product_id.business_category_id
if production.bom_id and production.bom_id.business_category_id != production.business_category_id:
production.bom_id = False
if not production.bom_id:
production.bom_id = production._find_matching_bom()
@api.onchange("bom_id")
def _onchange_bom_business_category(self):
for production in self:
if production.bom_id and production.bom_id.business_category_id:
production.business_category_id = production.bom_id.business_category_id
@api.onchange("mrp_team_id")
def _onchange_mrp_team_id_business_category(self):
for production in self:
if production.mrp_team_id and production.mrp_team_id.business_category_id:
production.business_category_id = production.mrp_team_id.business_category_id
@api.onchange("business_category_id")
def _onchange_business_category_id_reset_team_and_bom(self):
for production in self:
if production.mrp_team_id and production.mrp_team_id.business_category_id != production.business_category_id:
production.mrp_team_id = False
if production.bom_id and production.bom_id.business_category_id != production.business_category_id:
production.bom_id = False
if production.product_id and not production.bom_id:
production.bom_id = production._find_matching_bom()
@api.model_create_multi
def create(self, vals_list):
vals_list = [self._prepare_business_category_vals(vals) for vals in vals_list]
return super().create(vals_list)
def write(self, vals):
vals = self._prepare_business_category_vals(vals)
return super().write(vals)
@api.model
def _prepare_business_category_vals(self, vals):
vals = dict(vals)
category_id = vals.get("business_category_id")
team = self.env["mrp.team"].browse(vals["mrp_team_id"]) if vals.get("mrp_team_id") else False
bom = self.env["mrp.bom"].browse(vals["bom_id"]) if vals.get("bom_id") else False
product = self.env["product.product"].browse(vals["product_id"]) if vals.get("product_id") else False
if not category_id:
category = (
(team and team.business_category_id)
or (bom and bom.business_category_id)
or (product and product.business_category_id)
or False
)
if category:
category_id = category.id
vals["business_category_id"] = category_id
if category_id and not vals.get("company_id"):
category = self.env["crm.business.category"].browse(category_id)
if category.company_id:
vals["company_id"] = category.company_id.id
if not vals.get("bom_id") and product and category_id:
temp_record = self.new(
{
"product_id": product.id,
"business_category_id": category_id,
"company_id": vals.get("company_id") or self.env.company.id,
}
)
bom = temp_record._find_matching_bom(product=product)
if bom:
vals["bom_id"] = bom.id
return vals
@api.constrains("company_id", "business_category_id", "product_id", "bom_id", "mrp_team_id")
def _check_business_category_consistency(self):
current_user = self.env.user
effective_categories = current_user.effective_business_category_ids
for production in self:
if production.product_id and not production.business_category_id:
raise ValidationError(
_("Manufacturing Order '%s' requires a Business Category.")
% production.display_name
)
if production.business_category_id and production.business_category_id.company_id != production.company_id:
raise ValidationError(
_("Manufacturing Order '%s' must use a Business Category from the same company.")
% production.display_name
)
if production.product_id.business_category_id != production.business_category_id:
raise ValidationError(
_("Manufacturing Order '%s' must use the same Business Category as its product.")
% production.display_name
)
if production.bom_id and production.bom_id.business_category_id != production.business_category_id:
raise ValidationError(
_("Manufacturing Order '%s' must use the same Business Category as its BoM.")
% production.display_name
)
if production.mrp_team_id:
if production.mrp_team_id.company_id != production.company_id:
raise ValidationError(
_("Manufacturing Team '%s' belongs to another company.")
% production.mrp_team_id.display_name
)
if production.mrp_team_id.business_category_id != production.business_category_id:
raise ValidationError(
_("Manufacturing Team '%s' must use the same Business Category as the MO.")
% production.mrp_team_id.display_name
)
invalid_raw_move = production.move_raw_ids.filtered(
lambda move: move.business_category_id
and move.business_category_id != production.business_category_id
)[:1]
if invalid_raw_move:
raise ValidationError(
_("Raw material move '%s' must use the same Business Category as the MO.")
% invalid_raw_move.display_name
)
invalid_finished_move = production.move_finished_ids.filtered(
lambda move: move.business_category_id
and move.business_category_id != production.business_category_id
)[:1]
if invalid_finished_move:
raise ValidationError(
_("Finished move '%s' must use the same Business Category as the MO.")
% invalid_finished_move.display_name
)
if (
not current_user.has_group("base.group_system")
and production.business_category_id
and production.business_category_id not in effective_categories
):
raise ValidationError(
_("You can only use Manufacturing Orders in your registered Business Category.")
)
if production.mrp_team_id:
team_users = production.mrp_team_id.user_id | production.mrp_team_id.member_ids
if team_users and current_user not in team_users and not current_user.has_group("mrp.group_mrp_manager"):
raise ValidationError(
_("You must be registered in the selected Manufacturing Team before using it on an MO.")
)
def action_confirm(self):
for production in self:
if not production.business_category_id:
raise ValidationError(
_("Manufacturing Order '%s' requires a Business Category before confirmation.")
% production.display_name
)
return super().action_confirm()
def button_mark_done(self):
for production in self:
if not production.business_category_id:
raise ValidationError(
_("Manufacturing Order '%s' requires a Business Category before completion.")
% production.display_name
)
return super().button_mark_done()
@@ -0,0 +1,45 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class MrpTeam(models.Model):
_name = "mrp.team"
_description = "Manufacturing Team"
_order = "company_id, business_category_id, name"
name = fields.Char(required=True)
active = fields.Boolean(default=True)
company_id = fields.Many2one(
"res.company",
required=True,
default=lambda self: self.env.company,
)
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
required=True,
ondelete="restrict",
domain="[('company_id', '=', company_id)]",
)
user_id = fields.Many2one(
"res.users",
string="Team Leader",
domain="[('share', '=', False), ('company_ids', 'in', company_id)]",
)
member_ids = fields.Many2many(
"res.users",
"mrp_team_res_users_rel",
"team_id",
"user_id",
string="Members",
domain="[('share', '=', False), ('company_ids', 'in', company_id)]",
)
@api.constrains("company_id", "business_category_id")
def _check_business_category_company(self):
for team in self:
if team.business_category_id.company_id != team.company_id:
raise ValidationError(
_("Manufacturing Team '%s' must use a Business Category from the same company.")
% team.display_name
)
@@ -0,0 +1,14 @@
from odoo import fields, models
class StockMoveLine(models.Model):
_inherit = "stock.move.line"
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
related="move_id.business_category_id",
store=True,
readonly=True,
index=True,
)
@@ -0,0 +1,21 @@
from odoo import fields, models
class StockValuationLayer(models.Model):
_inherit = "stock.valuation.layer"
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
related="stock_move_id.business_category_id",
store=True,
readonly=True,
index=True,
)
inventory_analytic_account_id = fields.Many2one(
"account.analytic.account",
string="Inventory Analytic Account",
related="stock_move_id.inventory_analytic_account_id",
store=True,
readonly=True,
)
@@ -0,0 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_mrp_team_user,mrp.team user,model_mrp_team,mrp.group_mrp_user,1,0,0,0
access_mrp_team_manager,mrp.team manager,model_mrp_team,mrp.group_mrp_manager,1,1,1,1
access_mrp_team_sysadmin,mrp.team sysadmin,model_mrp_team,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_mrp_team_user mrp.team user model_mrp_team mrp.group_mrp_user 1 0 0 0
3 access_mrp_team_manager mrp.team manager model_mrp_team mrp.group_mrp_manager 1 1 1 1
4 access_mrp_team_sysadmin mrp.team sysadmin model_mrp_team base.group_system 1 1 1 1
@@ -0,0 +1,25 @@
id,name,model_id:id,domain_force,groups:id,perm_read,perm_write,perm_create,perm_unlink
mrp_bom_business_category_rule_user,BoM read by effective business category,mrp.model_mrp_bom,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",mrp.group_mrp_user,1,0,0,0
mrp_bom_business_category_rule_manager,BoM full access by effective business category,mrp.model_mrp_bom,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",mrp.group_mrp_manager,1,1,1,1
mrp_bom_business_category_rule_sysadmin,BoM full access for system admin,mrp.model_mrp_bom,"[('company_id','in',user.company_ids.ids)]",base.group_system,1,1,1,1
mrp_bom_line_business_category_rule_user,BoM Line read by effective business category,mrp.model_mrp_bom_line,"[('bom_id.company_id','in',user.company_ids.ids),('bom_id.business_category_id','in',user.effective_business_category_ids.ids)]",mrp.group_mrp_user,1,0,0,0
mrp_bom_line_business_category_rule_manager,BoM Line full access by effective business category,mrp.model_mrp_bom_line,"[('bom_id.company_id','in',user.company_ids.ids),('bom_id.business_category_id','in',user.effective_business_category_ids.ids)]",mrp.group_mrp_manager,1,1,1,1
mrp_bom_line_business_category_rule_sysadmin,BoM Line full access for system admin,mrp.model_mrp_bom_line,"[('bom_id.company_id','in',user.company_ids.ids)]",base.group_system,1,1,1,1
mrp_bom_byproduct_business_category_rule_user,BoM By-Product read by effective business category,mrp.model_mrp_bom_byproduct,"[('bom_id.company_id','in',user.company_ids.ids),('bom_id.business_category_id','in',user.effective_business_category_ids.ids)]",mrp.group_mrp_user,1,0,0,0
mrp_bom_byproduct_business_category_rule_manager,BoM By-Product full access by effective business category,mrp.model_mrp_bom_byproduct,"[('bom_id.company_id','in',user.company_ids.ids),('bom_id.business_category_id','in',user.effective_business_category_ids.ids)]",mrp.group_mrp_manager,1,1,1,1
mrp_bom_byproduct_business_category_rule_sysadmin,BoM By-Product full access for system admin,mrp.model_mrp_bom_byproduct,"[('bom_id.company_id','in',user.company_ids.ids)]",base.group_system,1,1,1,1
mrp_production_business_category_rule_user,Manufacturing Order full access by effective business category,mrp.model_mrp_production,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",mrp.group_mrp_user,1,1,1,1
mrp_production_business_category_rule_manager,Manufacturing Order full access by effective business category manager,mrp.model_mrp_production,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",mrp.group_mrp_manager,1,1,1,1
mrp_production_business_category_rule_sysadmin,Manufacturing Order full access for system admin,mrp.model_mrp_production,"[('company_id','in',user.company_ids.ids)]",base.group_system,1,1,1,1
stock_move_line_business_category_rule_user,Stock Move Line read by effective business category,stock.model_stock_move_line,"[('company_id','in',user.company_ids.ids),('move_id.business_category_id','in',user.effective_business_category_ids.ids)]",mrp.group_mrp_user,1,0,0,0
stock_move_line_business_category_rule_manager,Stock Move Line full access by effective business category,stock.model_stock_move_line,"[('company_id','in',user.company_ids.ids),('move_id.business_category_id','in',user.effective_business_category_ids.ids)]",mrp.group_mrp_manager,1,1,1,1
stock_move_line_business_category_rule_sysadmin,Stock Move Line full access for system admin,stock.model_stock_move_line,"[('company_id','in',user.company_ids.ids)]",base.group_system,1,1,1,1
stock_valuation_layer_business_category_rule_user,Stock Valuation Layer read by effective business category,stock_account.model_stock_valuation_layer,"[('company_id','in',user.company_ids.ids),('stock_move_id.business_category_id','in',user.effective_business_category_ids.ids)]",mrp.group_mrp_user,1,0,0,0
stock_valuation_layer_business_category_rule_manager,Stock Valuation Layer full access by effective business category,stock_account.model_stock_valuation_layer,"[('company_id','in',user.company_ids.ids),('stock_move_id.business_category_id','in',user.effective_business_category_ids.ids)]",mrp.group_mrp_manager,1,1,1,1
stock_valuation_layer_business_category_rule_sysadmin,Stock Valuation Layer full access for system admin,stock_account.model_stock_valuation_layer,"[('company_id','in',user.company_ids.ids)]",base.group_system,1,1,1,1
mrp_team_business_category_rule_user,Manufacturing Team read by effective business category,model_mrp_team,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",mrp.group_mrp_user,1,0,0,0
mrp_team_business_category_rule_manager,Manufacturing Team full access by effective business category,model_mrp_team,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",mrp.group_mrp_manager,1,1,1,1
mrp_team_business_category_rule_sysadmin,Manufacturing Team full access for system admin,model_mrp_team,"[('company_id','in',user.company_ids.ids)]",base.group_system,1,1,1,1
crm_business_category_rule_mrp_user,Business Category read by effective list for MRP user,grt_business_category_base.model_crm_business_category,"[('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)]",mrp.group_mrp_user,1,0,0,0
crm_business_category_rule_mrp_manager,Business Category full access by effective list for MRP manager,grt_business_category_base.model_crm_business_category,"[('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)]",mrp.group_mrp_manager,1,1,1,1
crm_business_category_rule_mrp_sysadmin,Business Category full access for system admin,grt_business_category_base.model_crm_business_category,"[('company_id','in',user.company_ids.ids)]",base.group_system,1,1,1,1
1 id name model_id:id domain_force groups:id perm_read perm_write perm_create perm_unlink
2 mrp_bom_business_category_rule_user BoM read by effective business category mrp.model_mrp_bom [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] mrp.group_mrp_user 1 0 0 0
3 mrp_bom_business_category_rule_manager BoM full access by effective business category mrp.model_mrp_bom [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] mrp.group_mrp_manager 1 1 1 1
4 mrp_bom_business_category_rule_sysadmin BoM full access for system admin mrp.model_mrp_bom [('company_id','in',user.company_ids.ids)] base.group_system 1 1 1 1
5 mrp_bom_line_business_category_rule_user BoM Line read by effective business category mrp.model_mrp_bom_line [('bom_id.company_id','in',user.company_ids.ids),('bom_id.business_category_id','in',user.effective_business_category_ids.ids)] mrp.group_mrp_user 1 0 0 0
6 mrp_bom_line_business_category_rule_manager BoM Line full access by effective business category mrp.model_mrp_bom_line [('bom_id.company_id','in',user.company_ids.ids),('bom_id.business_category_id','in',user.effective_business_category_ids.ids)] mrp.group_mrp_manager 1 1 1 1
7 mrp_bom_line_business_category_rule_sysadmin BoM Line full access for system admin mrp.model_mrp_bom_line [('bom_id.company_id','in',user.company_ids.ids)] base.group_system 1 1 1 1
8 mrp_bom_byproduct_business_category_rule_user BoM By-Product read by effective business category mrp.model_mrp_bom_byproduct [('bom_id.company_id','in',user.company_ids.ids),('bom_id.business_category_id','in',user.effective_business_category_ids.ids)] mrp.group_mrp_user 1 0 0 0
9 mrp_bom_byproduct_business_category_rule_manager BoM By-Product full access by effective business category mrp.model_mrp_bom_byproduct [('bom_id.company_id','in',user.company_ids.ids),('bom_id.business_category_id','in',user.effective_business_category_ids.ids)] mrp.group_mrp_manager 1 1 1 1
10 mrp_bom_byproduct_business_category_rule_sysadmin BoM By-Product full access for system admin mrp.model_mrp_bom_byproduct [('bom_id.company_id','in',user.company_ids.ids)] base.group_system 1 1 1 1
11 mrp_production_business_category_rule_user Manufacturing Order full access by effective business category mrp.model_mrp_production [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] mrp.group_mrp_user 1 1 1 1
12 mrp_production_business_category_rule_manager Manufacturing Order full access by effective business category manager mrp.model_mrp_production [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] mrp.group_mrp_manager 1 1 1 1
13 mrp_production_business_category_rule_sysadmin Manufacturing Order full access for system admin mrp.model_mrp_production [('company_id','in',user.company_ids.ids)] base.group_system 1 1 1 1
14 stock_move_line_business_category_rule_user Stock Move Line read by effective business category stock.model_stock_move_line [('company_id','in',user.company_ids.ids),('move_id.business_category_id','in',user.effective_business_category_ids.ids)] mrp.group_mrp_user 1 0 0 0
15 stock_move_line_business_category_rule_manager Stock Move Line full access by effective business category stock.model_stock_move_line [('company_id','in',user.company_ids.ids),('move_id.business_category_id','in',user.effective_business_category_ids.ids)] mrp.group_mrp_manager 1 1 1 1
16 stock_move_line_business_category_rule_sysadmin Stock Move Line full access for system admin stock.model_stock_move_line [('company_id','in',user.company_ids.ids)] base.group_system 1 1 1 1
17 stock_valuation_layer_business_category_rule_user Stock Valuation Layer read by effective business category stock_account.model_stock_valuation_layer [('company_id','in',user.company_ids.ids),('stock_move_id.business_category_id','in',user.effective_business_category_ids.ids)] mrp.group_mrp_user 1 0 0 0
18 stock_valuation_layer_business_category_rule_manager Stock Valuation Layer full access by effective business category stock_account.model_stock_valuation_layer [('company_id','in',user.company_ids.ids),('stock_move_id.business_category_id','in',user.effective_business_category_ids.ids)] mrp.group_mrp_manager 1 1 1 1
19 stock_valuation_layer_business_category_rule_sysadmin Stock Valuation Layer full access for system admin stock_account.model_stock_valuation_layer [('company_id','in',user.company_ids.ids)] base.group_system 1 1 1 1
20 mrp_team_business_category_rule_user Manufacturing Team read by effective business category model_mrp_team [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] mrp.group_mrp_user 1 0 0 0
21 mrp_team_business_category_rule_manager Manufacturing Team full access by effective business category model_mrp_team [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] mrp.group_mrp_manager 1 1 1 1
22 mrp_team_business_category_rule_sysadmin Manufacturing Team full access for system admin model_mrp_team [('company_id','in',user.company_ids.ids)] base.group_system 1 1 1 1
23 crm_business_category_rule_mrp_user Business Category read by effective list for MRP user grt_business_category_base.model_crm_business_category [('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)] mrp.group_mrp_user 1 0 0 0
24 crm_business_category_rule_mrp_manager Business Category full access by effective list for MRP manager grt_business_category_base.model_crm_business_category [('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)] mrp.group_mrp_manager 1 1 1 1
25 crm_business_category_rule_mrp_sysadmin Business Category full access for system admin grt_business_category_base.model_crm_business_category [('company_id','in',user.company_ids.ids)] base.group_system 1 1 1 1
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_mrp_bom_form_business_category" model="ir.ui.view">
<field name="name">mrp.bom.form.business.category</field>
<field name="model">mrp.bom</field>
<field name="inherit_id" ref="mrp.mrp_bom_form_view"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='code']" position="after">
<field name="company_id" invisible="1"/>
<field name="business_category_id"/>
<field name="manufacturing_analytic_account_id" readonly="1"/>
</xpath>
<xpath expr="//field[@name='bom_line_ids']//tree//field[@name='product_id']" position="after">
<field name="business_category_id" readonly="1"/>
</xpath>
</field>
</record>
<record id="view_mrp_bom_tree_business_category" model="ir.ui.view">
<field name="name">mrp.bom.tree.business.category</field>
<field name="model">mrp.bom</field>
<field name="inherit_id" ref="mrp.mrp_bom_tree_view"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='product_tmpl_id']" position="after">
<field name="business_category_id" optional="show"/>
</xpath>
</field>
</record>
<record id="view_mrp_bom_filter_business_category" model="ir.ui.view">
<field name="name">mrp.bom.filter.business.category</field>
<field name="model">mrp.bom</field>
<field name="inherit_id" ref="mrp.view_mrp_bom_filter"/>
<field name="arch" type="xml">
<xpath expr="//search/field[@name='product_tmpl_id']" position="after">
<field name="business_category_id"/>
</xpath>
<xpath expr="//search/group" position="inside">
<filter string="Business Category" name="group_by_business_category" domain="[]" context="{'group_by': 'business_category_id'}"/>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_mrp_production_form_business_category" model="ir.ui.view">
<field name="name">mrp.production.form.business.category</field>
<field name="model">mrp.production</field>
<field name="inherit_id" ref="mrp.mrp_production_form_view"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='bom_id']" position="after">
<field name="company_id" invisible="1"/>
<field name="business_category_id"/>
<field name="mrp_team_id"/>
<field name="manufacturing_analytic_account_id" readonly="1"/>
</xpath>
<xpath expr="//field[@name='move_raw_ids']//tree//field[@name='product_id']" position="after">
<field name="business_category_id" readonly="1"/>
</xpath>
<xpath expr="//field[@name='move_finished_ids']//tree//field[@name='product_id']" position="after">
<field name="business_category_id" readonly="1"/>
</xpath>
</field>
</record>
<record id="view_mrp_production_tree_business_category" model="ir.ui.view">
<field name="name">mrp.production.tree.business.category</field>
<field name="model">mrp.production</field>
<field name="inherit_id" ref="mrp.mrp_production_tree_view"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='state']" position="before">
<field name="business_category_id" optional="show"/>
</xpath>
</field>
</record>
<record id="view_mrp_production_filter_business_category" model="ir.ui.view">
<field name="name">mrp.production.filter.business.category</field>
<field name="model">mrp.production</field>
<field name="inherit_id" ref="mrp.view_mrp_production_filter"/>
<field name="arch" type="xml">
<xpath expr="//search/field[@name='product_id']" position="after">
<field name="business_category_id"/>
</xpath>
<xpath expr="//search/group" position="inside">
<filter string="Business Category" name="group_by_business_category" domain="[]" context="{'group_by': 'business_category_id'}"/>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_mrp_team_tree" model="ir.ui.view">
<field name="name">mrp.team.tree</field>
<field name="model">mrp.team</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="company_id"/>
<field name="business_category_id"/>
<field name="user_id"/>
<field name="active"/>
</tree>
</field>
</record>
<record id="view_mrp_team_form" model="ir.ui.view">
<field name="name">mrp.team.form</field>
<field name="model">mrp.team</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<group>
<field name="name"/>
<field name="company_id"/>
<field name="business_category_id"/>
<field name="active"/>
</group>
<group>
<field name="user_id"/>
<field name="member_ids" widget="many2many_tags"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_mrp_team" model="ir.actions.act_window">
<field name="name">Manufacturing Teams</field>
<field name="res_model">mrp.team</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="menu_mrp_team"
name="Manufacturing Teams"
parent="mrp.menu_mrp_configuration"
action="action_mrp_team"
sequence="86"/>
</odoo>
@@ -387,8 +387,12 @@ class MrpOverheadPeriod(models.Model):
("date_finished", ">=", fields.Datetime.to_string(start_dt)),
("date_finished", "<=", fields.Datetime.to_string(end_dt)),
]
if self.business_category_id and "business_category_id" in self.env["product.product"]._fields:
domain.append(("product_id.business_category_id", "=", self.business_category_id.id))
production_model = self.env["mrp.production"]
if self.business_category_id:
if "business_category_id" in production_model._fields:
domain.append(("business_category_id", "=", self.business_category_id.id))
elif "business_category_id" in self.env["product.product"]._fields:
domain.append(("product_id.business_category_id", "=", self.business_category_id.id))
return self.env["mrp.production"].search(domain)
+60 -14
View File
@@ -20,29 +20,75 @@ Dokumentasi ini dibuat sebagai referensi operasional dan teknis ringan untuk flo
4. Isi `Amount to Offset`
5. Klik `Confirm Offset`
## Use Case Peternak Lintas Business Category
Contoh kasus operasional:
- Peternak menjual susu ke perusahaan dan transaksi tersebut membentuk hutang perusahaan kepada peternak pada Business Category `A`.
- Peternak membeli pakan dari perusahaan dan transaksi tersebut membentuk piutang perusahaan kepada peternak pada Business Category `B`.
- Peternak ingin pembayaran pembelian pakan dipotong langsung dari hasil penjualan susu.
Dengan kebutuhan ini:
- Dokumen sumber tetap harus memakai business category asal masing-masing.
- Offset tidak boleh memaksa invoice susu dan bill pakan menjadi satu business category yang sama.
- Journal entry offset diperlakukan sebagai settlement antar saldo receivable dan payable partner yang sama.
Rekomendasi penggunaan wizard:
1. Pastikan invoice penjualan susu sudah ter-posting pada Business Category `A`.
2. Pastikan invoice/bill pembelian pakan sudah ter-posting pada Business Category `B`.
3. Buka wizard offset dan klik `Load Open Items`.
4. Verifikasi kolom `Business Category` dan `Analytic Account` pada setiap baris.
5. Jika kategori atau analytic berbeda, aktifkan `Allow Cross Business Category Offset`.
6. Isi nominal yang ingin di-offset sampai total receivable dan payable seimbang.
7. Klik `Confirm Offset`.
Hasil yang diharapkan:
- Open item receivable dan payable partner yang dipilih akan saling reconcile sesuai nominal offset.
- Dokumen asal tetap mempertahankan Business Category `A` dan `B`.
- Journal entry offset menjadi jurnal settlement dan tidak dipaksa membawa satu business category tunggal saat transaksi memang lintas kategori.
## Rekonsiliasi Parsial dan Berulang
Wizard ini mendukung offset parsial dan dapat digunakan berulang kali selama masih ada saldo residual pada receivable atau payable partner yang sama.
Contoh alur:
1. Peternak memiliki hutang pembelian pakan sebesar 1.000.000.
2. Pada periode pertama, hasil penjualan susu yang siap di-offset baru 400.000.
3. Wizard digunakan untuk offset 400.000, sehingga hutang tersisa 600.000.
4. Pada periode berikutnya muncul lagi piutang baru dari penjualan susu sebesar 300.000.
5. Wizard dapat dijalankan kembali untuk offset 300.000 terhadap sisa hutang.
6. Proses ini bisa diulang sampai saldo hutang atau piutang habis.
Perilaku sistem:
- Open item yang masih memiliki `residual amount` akan tetap muncul saat klik `Load Open Items`.
- Open item yang sudah lunas penuh tidak akan muncul lagi.
- Setiap proses offset tetap harus seimbang antara total receivable yang dipilih dan total payable yang dipilih.
- Nilai `Amount to Offset` per baris tidak boleh melebihi saldo residual baris tersebut.
## Business Category Behavior
- Wizard akan membaca business category dan analytic context dari open items yang dipilih.
- Open items yang dipilih harus berada dalam satu business category yang sama.
- Open items yang dipilih harus berada dalam satu analytic account yang sama.
- Jika terdeteksi lebih dari satu business category, offset akan diblokir.
- Jika terdeteksi lebih dari satu analytic account, offset akan diblokir.
- Jika context valid, journal entry hasil offset akan mewarisi business category tersebut.
- Journal entry hasil offset dan line-nya juga akan mewarisi analytic account yang sama.
- Default behavior tetap aman: open items dengan business category / analytic berbeda akan diblokir.
- Jika memang dibutuhkan settlement lintas kategori, aktifkan `Allow Cross Business Category Offset`.
- Saat opsi lintas kategori aktif, journal entry offset diposting sebagai settlement entry dan tidak dipaksa mewarisi satu business category atau analytic account tunggal.
- Jika context hanya terdiri dari satu business category dan satu analytic account, journal entry hasil offset tetap mewarisi context tersebut seperti sebelumnya.
- Wizard line sekarang menampilkan business category dan analytic sumber per dokumen agar user bisa memverifikasi pasangan offset A vs B sebelum confirm.
## Catatan Penting
- Modul ini tidak membuat business category baru; modul hanya mengikuti tagging pada dokumen sumber.
- Untuk hasil paling aman, lakukan offset hanya antar dokumen yang memang berasal dari business category dan analytic account yang sama.
- Jika ada open item lama yang belum punya tagging business category, wizard akan fallback ke analytic information yang tersedia pada move atau move line.
- Untuk skenario peternak lintas kategori, pelaporan kategori tetap mengacu pada dokumen sumber. Jurnal offset hanya berfungsi sebagai settlement saldo.
- Jika perusahaan membutuhkan pelaporan settlement per business category secara eksplisit, tahap berikutnya yang disarankan adalah membuat laporan khusus offset lintas kategori atau menambah field referensi settlement type.
## Recommended Test Scenarios
1. Offset piutang dan hutang partner yang sama dengan business category dan analytic account yang sama.
2. Coba offset dokumen dari business category berbeda dan pastikan wizard menolak.
3. Coba offset dokumen dari analytic account berbeda dan pastikan wizard menolak.
4. Pastikan journal entry hasil offset membawa field business category dan analytic yang sesuai.
2. Coba offset dokumen dari business category berbeda tanpa mengaktifkan opsi lintas kategori dan pastikan wizard menolak.
3. Coba offset dokumen dari business category berbeda dengan mengaktifkan opsi lintas kategori dan pastikan journal entry settlement berhasil diposting.
4. Coba offset dokumen dari analytic account berbeda tanpa mengaktifkan opsi lintas kategori dan pastikan wizard menolak.
5. Pastikan journal entry hasil offset membawa field business category dan analytic yang sesuai saat context-nya tunggal.
6. Coba lakukan offset parsial beberapa kali pada partner yang sama dan pastikan hanya saldo residual yang tersisa yang tetap muncul pada wizard.
## Changelog Summary
- Added documentation for the module
- Added business category dependency
- Added validation for cross-business-category offset
- Added validation for cross-analytic offset
- Added business category and analytic propagation to generated offset journal entry
- Added business category-aware validation on selected open items.
- Added optional cross business category / cross analytic settlement flow via `Allow Cross Business Category Offset`.
- Added source business category and analytic visibility on wizard lines for easier review before reconciliation.
- Updated generated offset journal behavior so single-category selections inherit business context, while mixed-category selections post as settlement entries.
- Expanded documentation with farmer/member cross-category offset use case and operational guidance.
- Added documentation for partial and repeated reconciliation using remaining residual balances.
+1
View File
@@ -1 +1,2 @@
from .hooks import post_init_hook
from . import wizard
+3 -1
View File
@@ -1,11 +1,13 @@
{
"name": "GRT Partner Offset Reconciliation",
"summary": "Offset receivable and payable for the same partner/member",
"version": "14.0.1.0.0",
"version": "14.0.1.1.0",
"category": "Accounting",
"author": "GRT",
"website": "https://rimang.id",
"license": "LGPL-3",
"depends": ["account", "grt_business_category_base"],
"post_init_hook": "post_init_hook",
"data": [
"security/ir.model.access.csv",
"wizard/partner_offset.xml",
+12
View File
@@ -0,0 +1,12 @@
from odoo import SUPERUSER_ID, api
def post_init_hook(cr, registry):
"""Avoid duplicate offset menus when the legacy accountant module is installed."""
env = api.Environment(cr, SUPERUSER_ID, {})
legacy_menu = env.ref("om_account_accountant.menu_partner_offset_wizard", raise_if_not_found=False)
current_menu = env.ref("grt_partner_offset_reconcile.menu_partner_offset_wizard", raise_if_not_found=False)
if legacy_menu and current_menu and legacy_menu.active:
legacy_menu.active = False
@@ -32,6 +32,14 @@ class PartnerOffsetWizard(models.TransientModel):
),
domain="[('company_id', '=', company_id), ('type', '=', 'general')]",
)
allow_mixed_business_context = fields.Boolean(
string="Allow Cross Business Category Offset",
help=(
"Enable this only for settlement between documents from different business "
"categories or analytic accounts. The generated offset entry will not inherit "
"a single business category or analytic account."
),
)
line_ids = fields.One2many(
"grt.partner.offset.wizard.line",
"wizard_id",
@@ -139,42 +147,48 @@ class PartnerOffsetWizard(models.TransientModel):
if field_name in move_model._fields
]
def _extract_line_business_context(self, wizard_line):
self.ensure_one()
move_line = wizard_line.move_line_id
move = move_line.move_id
category_ids = set()
analytic_ids = set()
if move_line.analytic_account_id:
analytic_ids.add(move_line.analytic_account_id.id)
for field_name in self._get_supported_move_business_category_fields():
category = getattr(move, field_name, False)
if category:
category_ids.add(category.id)
for field_name in self._get_supported_move_analytic_fields():
analytic = getattr(move, field_name, False)
if analytic:
analytic_ids.add(analytic.id)
return category_ids, analytic_ids
def _resolve_offset_business_context(self, selected_lines):
category_field_names = self._get_supported_move_business_category_fields()
analytic_field_names = self._get_supported_move_analytic_fields()
category_ids = set()
analytic_ids = set()
for wizard_line in selected_lines:
move_line = wizard_line.move_line_id
move = move_line.move_id
if move_line.analytic_account_id:
analytic_ids.add(move_line.analytic_account_id.id)
line_category_ids, line_analytic_ids = self._extract_line_business_context(wizard_line)
category_ids.update(line_category_ids)
analytic_ids.update(line_analytic_ids)
for field_name in category_field_names:
category = getattr(move, field_name, False)
if category:
category_ids.add(category.id)
for field_name in analytic_field_names:
analytic = getattr(move, field_name, False)
if analytic:
analytic_ids.add(analytic.id)
if len(category_ids) > 1:
has_mixed_business_context = len(category_ids) > 1 or len(analytic_ids) > 1
if has_mixed_business_context and not self.allow_mixed_business_context:
raise UserError(
_(
"Selected open items span multiple Business Categories. "
"Please offset one Business Category at a time."
)
)
if len(analytic_ids) > 1:
raise UserError(
_(
"Selected open items span multiple Analytic Accounts. "
"Please offset one Analytic Account at a time."
"Selected open items span multiple Business Categories or Analytic Accounts. "
"Enable 'Allow Cross Business Category Offset' if this should be posted as a "
"settlement entry across categories."
)
)
if has_mixed_business_context:
return False, False
business_category = False
analytic_account = False
@@ -427,6 +441,18 @@ class PartnerOffsetWizardLine(models.TransientModel):
compute="_compute_residual_amount",
readonly=True,
)
source_business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
compute="_compute_source_business_context",
readonly=True,
)
source_analytic_account_id = fields.Many2one(
"account.analytic.account",
string="Analytic Account",
compute="_compute_source_business_context",
readonly=True,
)
amount_to_offset = fields.Monetary(
string="Amount to Offset",
currency_field="currency_id",
@@ -437,6 +463,21 @@ class PartnerOffsetWizardLine(models.TransientModel):
for line in self:
line.residual_amount = abs(line.move_line_id.amount_residual)
@api.depends("move_line_id", "move_line_id.move_id", "move_line_id.analytic_account_id")
def _compute_source_business_context(self):
for line in self:
wizard = line.wizard_id
line.source_business_category_id = False
line.source_analytic_account_id = False
if not wizard or not line.move_line_id:
continue
category_ids, analytic_ids = wizard._extract_line_business_context(line)
if len(category_ids) == 1:
line.source_business_category_id = self.env["crm.business.category"].browse(next(iter(category_ids)))
if len(analytic_ids) == 1:
line.source_analytic_account_id = self.env["account.analytic.account"].browse(next(iter(analytic_ids)))
@api.constrains("amount_to_offset")
def _check_amount_to_offset(self):
for line in self:
@@ -14,6 +14,7 @@
<field name="partner_id" options="{'no_create': True}"/>
<field name="date"/>
<field name="journal_id" options="{'no_create': True}"/>
<field name="allow_mixed_business_context"/>
<field name="move_id" readonly="1" attrs="{'invisible': [('move_id', '=', False)]}"/>
</group>
<group>
@@ -37,6 +38,8 @@
<field name="ref"/>
<field name="label"/>
<field name="account_id"/>
<field name="source_business_category_id" optional="show"/>
<field name="source_analytic_account_id" optional="show"/>
<field name="residual_amount" widget="monetary" options="{'currency_field': 'currency_id'}" readonly="1"/>
<field name="amount_to_offset" widget="monetary" options="{'currency_field': 'currency_id'}"/>
</tree>
@@ -52,6 +55,8 @@
<field name="ref"/>
<field name="label"/>
<field name="account_id"/>
<field name="source_business_category_id" optional="show"/>
<field name="source_analytic_account_id" optional="show"/>
<field name="residual_amount" widget="monetary" options="{'currency_field': 'currency_id'}" readonly="1"/>
<field name="amount_to_offset" widget="monetary" options="{'currency_field': 'currency_id'}"/>
</tree>
@@ -26,11 +26,68 @@ class PurchaseOrder(models.Model):
domain="[('company_id', '=', company_id), ('business_category_id', '=', business_category_id)]",
tracking=True,
)
payment_status = fields.Selection(
[
("no_invoice", "Belum Ada Bill"),
("not_paid", "Belum Bayar"),
("partial", "Bayar Sebagian"),
("in_payment", "Dalam Proses Bayar"),
("paid", "Terbayar"),
("reversed", "Dibalik"),
],
string="Status Pembayaran",
compute="_compute_payment_status",
store=True,
readonly=True,
copy=False,
)
def _is_purchase_admin_user(self):
self.ensure_one()
return self.env.user.has_group("purchase.group_purchase_manager") or self.env.user.has_group("base.group_system")
@api.model
def _resolve_payment_status_from_moves(self, moves):
payment_states = {
state
for state in moves.mapped("payment_state")
if state and state != "invoicing_legacy"
}
if not payment_states:
return "no_invoice"
if "reversed" in payment_states and len(payment_states) > 1:
payment_states.remove("reversed")
if payment_states == {"paid"}:
return "paid"
if payment_states == {"reversed"}:
return "reversed"
if payment_states == {"not_paid"}:
return "not_paid"
if payment_states == {"in_payment"}:
return "in_payment"
if "partial" in payment_states:
return "partial"
if "paid" in payment_states and len(payment_states) > 1:
return "partial"
if "in_payment" in payment_states and len(payment_states) > 1:
return "partial"
return "not_paid"
@api.depends(
"state",
"order_line.invoice_lines.move_id.state",
"order_line.invoice_lines.move_id.payment_state",
"order_line.invoice_lines.move_id.move_type",
)
def _compute_payment_status(self):
for order in self:
bills = order.order_line.mapped("invoice_lines.move_id").filtered(
lambda move: move.state == "posted" and move.move_type in ("in_invoice", "in_refund")
)
order.payment_status = order._resolve_payment_status_from_moves(bills)
@api.onchange("business_category_id")
def _onchange_business_category_id(self):
for order in self:
@@ -10,6 +10,7 @@
<field name="business_category_id"/>
<field name="purchase_team_id"/>
<field name="analytic_account_id" readonly="1"/>
<field name="payment_status" widget="badge" readonly="1"/>
</xpath>
</field>
</record>
@@ -22,10 +23,15 @@
<xpath expr="//field[@name='user_id']" position="after">
<field name="business_category_id"/>
<field name="purchase_team_id"/>
<field name="payment_status"/>
</xpath>
<xpath expr="//search" position="inside">
<filter string="Business Category" name="group_by_business_category" context="{'group_by': 'business_category_id'}"/>
<filter string="Purchase Team" name="group_by_purchase_team" context="{'group_by': 'purchase_team_id'}"/>
<filter string="Terbayar" name="payment_status_paid" domain="[('payment_status', '=', 'paid')]"/>
<filter string="Bayar Sebagian" name="payment_status_partial" domain="[('payment_status', '=', 'partial')]"/>
<filter string="Belum Bayar" name="payment_status_not_paid" domain="[('payment_status', '=', 'not_paid')]"/>
<filter string="Group by Payment Status" name="group_by_payment_status" context="{'group_by': 'payment_status'}"/>
</xpath>
</field>
</record>
@@ -41,4 +47,37 @@
</xpath>
</field>
</record>
<record id="view_purchase_order_tree_payment_status" model="ir.ui.view">
<field name="name">purchase.order.tree.payment.status</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_order_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='invoice_status']" position="after">
<field name="payment_status" widget="badge" optional="show"/>
</xpath>
</field>
</record>
<record id="view_purchase_order_kpis_tree_payment_status" model="ir.ui.view">
<field name="name">purchase.order.kpis.tree.payment.status</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_order_kpis_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='invoice_status']" position="after">
<field name="payment_status" widget="badge" optional="show"/>
</xpath>
</field>
</record>
<record id="view_purchase_order_confirmed_tree_payment_status" model="ir.ui.view">
<field name="name">purchase.order.confirmed.tree.payment.status</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_order_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='invoice_status']" position="after">
<field name="payment_status" widget="badge" optional="show"/>
</xpath>
</field>
</record>
</odoo>
@@ -46,6 +46,21 @@ class SaleOrder(models.Model):
string="Show Sale Order Shipping",
compute="_compute_show_sale_order_shipping",
)
payment_status = fields.Selection(
[
("no_invoice", "Belum Ada Invoice"),
("not_paid", "Belum Bayar"),
("partial", "Bayar Sebagian"),
("in_payment", "Dalam Proses Bayar"),
("paid", "Terbayar"),
("reversed", "Dibalik"),
],
string="Status Pembayaran",
compute="_compute_payment_status",
store=True,
readonly=True,
copy=False,
)
def _is_sales_admin_user(self):
self.ensure_one()
@@ -138,6 +153,48 @@ class SaleOrder(models.Model):
not order.business_category_id or order.business_category_id.use_sale_order_shipping
)
@api.model
def _resolve_payment_status_from_moves(self, moves):
payment_states = {
state
for state in moves.mapped("payment_state")
if state and state != "invoicing_legacy"
}
if not payment_states:
return "no_invoice"
if "reversed" in payment_states and len(payment_states) > 1:
payment_states.remove("reversed")
if payment_states == {"paid"}:
return "paid"
if payment_states == {"reversed"}:
return "reversed"
if payment_states == {"not_paid"}:
return "not_paid"
if payment_states == {"in_payment"}:
return "in_payment"
if "partial" in payment_states:
return "partial"
if "paid" in payment_states and len(payment_states) > 1:
return "partial"
if "in_payment" in payment_states and len(payment_states) > 1:
return "partial"
return "not_paid"
@api.depends(
"state",
"order_line.invoice_lines.move_id.state",
"order_line.invoice_lines.move_id.payment_state",
"order_line.invoice_lines.move_id.move_type",
)
def _compute_payment_status(self):
for order in self:
invoices = order.order_line.mapped("invoice_lines.move_id").filtered(
lambda move: move.state == "posted" and move.move_type in ("out_invoice", "out_refund")
)
order.payment_status = order._resolve_payment_status_from_moves(invoices)
@api.onchange("opportunity_id")
def _onchange_opportunity_id_business_category(self):
for order in self:
@@ -36,6 +36,7 @@
<field name="show_sale_order_shipping" invisible="1"/>
<field name="business_category_id"/>
<field name="analytic_account_id" readonly="1"/>
<field name="payment_status" widget="badge" readonly="1"/>
<field name="is_frontend_order" readonly="1"/>
<field name="frontend_kecamatan_id"
attrs="{'readonly': [('is_frontend_order', '=', True)], 'invisible': [('show_sale_order_shipping', '=', False)]}"/>
@@ -68,11 +69,16 @@
<field name="arch" type="xml">
<xpath expr="//field[@name='user_id']" position="after">
<field name="business_category_id"/>
<field name="payment_status"/>
<field name="customer_wilayah_kecamatan_id"/>
<field name="customer_ref"/>
</xpath>
<xpath expr="//filter[@name='salesperson']" position="after">
<filter string="Business Category" name="group_by_business_category" context="{'group_by': 'business_category_id'}"/>
<filter string="Terbayar" name="payment_status_paid" domain="[('payment_status', '=', 'paid')]"/>
<filter string="Bayar Sebagian" name="payment_status_partial" domain="[('payment_status', '=', 'partial')]"/>
<filter string="Belum Bayar" name="payment_status_not_paid" domain="[('payment_status', '=', 'not_paid')]"/>
<filter string="Group by Payment Status" name="group_by_payment_status" context="{'group_by': 'payment_status'}"/>
<filter string="Team Sales" name="group_by_sales_team" context="{'group_by': 'team_id'}"/>
<filter string="Wilayah Customer" name="group_by_customer_wilayah" context="{'group_by': 'customer_wilayah_kecamatan_id'}"/>
<filter string="Frontend Orders" name="frontend_orders" domain="[('is_frontend_order', '=', True)]"/>
@@ -90,6 +96,18 @@
<xpath expr="//field[@name='partner_id']" position="after">
<field name="customer_wilayah_kecamatan_id" optional="show"/>
<field name="customer_ref" optional="show"/>
<field name="payment_status" widget="badge" optional="show"/>
</xpath>
</field>
</record>
<record id="view_sales_order_tree_payment_status" model="ir.ui.view">
<field name="name">sale.order.tree.payment.status</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='invoice_status']" position="after">
<field name="payment_status" widget="badge" optional="show"/>
</xpath>
</field>
</record>
@@ -0,0 +1,241 @@
# Dashboard Quick Reference - SCADA Business Category
Referensi singkat untuk tim frontend dashboard SCADA.
Schema JSON machine-readable untuk generate typed client:
- ./schemas/scada_bc_api.schemas.json
Untuk import granular per endpoint:
- ./schemas/index.json
- ./schemas/requests/*.request.schema.json
- ./schemas/responses/*.response.schema.json
## Kontrak Wajib
1. Semua request dashboard ke prefix /api/scada/bc memakai POST JSON.
2. Semua endpoint operasional wajib kirim business category:
- business_category_id (disarankan)
- atau business_category_code
- atau business_category_name
3. Simpan selected business category di state global dashboard dan kirim ke semua request.
## Endpoint untuk Dashboard
### Context Loader
Endpoint: /api/scada/bc/context
Kegunaan widget:
- Dropdown/picker business category
- Context badge di header dashboard
Field response penting:
- active_business_category
- selected_business_category
- effective_business_categories
### Equipment Master
Endpoint: /api/scada/bc/equipments
Kegunaan widget:
- Master equipment table
- Filter equipment by type/code
Payload umum:
{
"business_category_id": 3,
"limit": 100,
"offset": 0,
"equipment_type": "MIXER"
}
Field response penting per item:
- equipment_code
- name
- equipment_type
- is_active
- business_category
### Product Master
Endpoint: /api/scada/bc/products
Alias: /api/scada/bc/products-by-category
Kegunaan widget:
- Product selector
- Product master list
Field response penting per item:
- product_id
- product_tmpl_id
- product_name
- product_category
- business_category
### BoM Browser
Endpoint: /api/scada/bc/boms
Kegunaan widget:
- Detail BoM dan komponen
- Drilldown product to BoM
Field response penting:
- bom_id
- product_name
- business_category
- components[]
## Endpoint untuk Monitoring MO
### MO Confirmed List
Endpoint: /api/scada/bc/mo-list-confirmed
Kegunaan widget:
- Queue MO menunggu eksekusi
Field response penting per item:
- mo_id
- schedule
- schedule_end
- product
- quantity
- state
- equipment
- business_category
### MO Detail
Endpoint: /api/scada/bc/mo-detail
Kegunaan widget:
- Halaman detail 1 MO
- Panel consumption vs target
Payload minimal:
{
"business_category_id": 3,
"mo_id": "MO/2026/00021"
}
Field response penting:
- mo_id
- state
- product_name
- quantity
- produced_qty
- bom_components[]
- components_consumption[]
### MO Detailed List
Endpoint: /api/scada/bc/mo-list-detailed
Kegunaan widget:
- Monitoring MO lintas state
Payload contoh:
{
"business_category_id": 3,
"states": ["confirmed", "progress", "to_close"],
"limit": 20,
"offset": 0
}
## Endpoint untuk KPI dan Report
### OEE Detail
Endpoint: /api/scada/bc/oee-detail
Kegunaan widget:
- Tabel detail OEE
- Drilldown per MO/equipment
Field response penting:
- oee_id
- date_done
- mo_id
- equipment
- qty_planned
- qty_finished
- variance_finished
- consumption_ratio
- lines[]
### OEE Equipment Average
Endpoint: /api/scada/bc/oee-equipment-avg
Kegunaan widget:
- Leaderboard performance per equipment
- KPI card by equipment
Field response penting:
- equipment
- oee_records_count
- avg_yield_percent
- avg_consumption_ratio
- avg_oee_silo_percent
- avg_max_abs_deviation_percent
- total_deviation_alerts
### KPI Product Report
Endpoint: /api/scada/bc/kpi-product-report
Kegunaan widget:
- Product KPI table
- Product performance chart
Field response penting:
- product_id
- product_name
- oee_records_count
- avg_kpi
- last_oee_date
- summary
### Today Reports
Endpoint: /api/scada/bc/today-reports
Kegunaan widget:
- Dashboard harian
- Ringkasan batch dan kualitas hari ini
Field response section:
- batch_status
- total_production_today
- oee_quality_today
- batch_to_batch_deviation_chart
### Periodic Report
Endpoint: /api/scada/bc/periodic-report
Kegunaan widget:
- Dashboard periodik (mingguan/bulanan)
- Raw material dan FG trend
Field response section:
- metrics_total_production
- metrics_total_mo
- metrics_avg_oee_quality
- chart_raw_material_consumption
- table_daily_finished_goods
### Equipment Failure Report
Endpoint: /api/scada/bc/equipment-failure-report
Kegunaan widget:
- Failure log table
- Failure summary by equipment
Field response section:
- data[]
- summary.total_failures
- summary.by_equipment[]
## Error Handling Frontend
1. Jika status=error dan message terkait business category, tampilkan prompt pilih category.
2. Jangan kirim retry tanpa business category.
3. Tampilkan selected_business_category di header agar user tahu context aktif.
## Strategi Integrasi Frontend
1. Saat login dashboard, panggil /api/scada/bc/context.
2. Simpan selected category ke global state.
3. Semua API call berikutnya wajib menyertakan category yang sama.
4. Ketika user mengganti category, reset cache widget dan reload semua data.
## Referensi Lengkap
- MIDDLEWARE_SCADA_BC_API_REFERENCE.md
@@ -0,0 +1,313 @@
# Middleware API Reference - SCADA Business Category
Dokumen ini adalah referensi implementasi middleware dan dashboard untuk endpoint SCADA Business Category.
Schema JSON machine-readable untuk semua endpoint tersedia di:
- ./schemas/scada_bc_api.schemas.json
Schema granular per endpoint tersedia di:
- ./schemas/index.json
- ./schemas/requests/*.request.schema.json
- ./schemas/responses/*.response.schema.json
## Tujuan
- Menyamakan kontrak request antara middleware dan backend Odoo.
- Mencegah request tanpa business category pada endpoint operasional.
- Menjadi acuan cepat untuk tim dashboard SCADA.
## Aturan Umum
1. Semua endpoint di prefix /api/scada/bc wajib method POST dan type JSON.
2. Untuk endpoint operasional, middleware wajib mengirim business category.
3. Endpoint context boleh tanpa business category karena dipakai untuk membaca context user.
4. Semua endpoint mengembalikan struktur dasar:
- status: success atau error
- message: ada saat error
## Prioritas Resolusi Business Category
Middleware disarankan mengirim business_category_id.
Jika tidak tersedia, boleh kirim business_category_code atau business_category_name.
Prioritas backend:
1. business_category_id
2. business_category_code
3. business_category_name
## Endpoint Matrix
### 1) Context
Endpoint: /api/scada/bc/context
Payload opsional:
- business_category_id
- business_category_code
- business_category_name
Fungsi:
- Mengembalikan active_business_category user.
- Mengembalikan selected_business_category hasil resolusi payload.
- Mengembalikan effective_business_categories user.
Contoh payload:
{
"business_category_id": 3
}
### 2) Equipment List
Endpoint: /api/scada/bc/equipments
Payload wajib:
- business_category_id atau business_category_code atau business_category_name
Payload opsional:
- limit (default 100)
- offset (default 0)
- equipment_code
- equipment_type
Contoh payload:
{
"business_category_id": 3,
"limit": 100,
"offset": 0,
"equipment_type": "MIXER"
}
### 3) Product List
Endpoint: /api/scada/bc/products
Payload wajib:
- business_category_id atau business_category_code atau business_category_name
Payload opsional:
- limit (default 100)
- offset (default 0)
- category_id (product category)
- category_name (product category name, ilike)
- active (true/false)
Contoh payload:
{
"business_category_id": 3,
"active": true,
"limit": 100
}
### 4) Product List Alias
Endpoint: /api/scada/bc/products-by-category
Catatan:
- Alias dari /api/scada/bc/products.
- Payload sama persis dengan endpoint products.
### 5) BoM List
Endpoint: /api/scada/bc/boms
Payload wajib:
- business_category_id atau business_category_code atau business_category_name
Payload opsional:
- limit (default 100)
- offset (default 0)
- bom_id
- product_id
- product_tmpl_id
- active (true/false)
Contoh payload:
{
"business_category_id": 3,
"product_tmpl_id": 452,
"active": true
}
### 6) MO Confirmed List
Endpoint: /api/scada/bc/mo-list-confirmed
Payload wajib:
- business_category_id atau business_category_code atau business_category_name
Payload opsional:
- limit (default 50)
- offset (default 0)
Contoh payload:
{
"business_category_id": 3,
"limit": 50
}
### 7) MO Detail
Endpoint: /api/scada/bc/mo-detail
Payload wajib:
- business_category_id atau business_category_code atau business_category_name
- salah satu dari:
- mo_id (ID integer atau nama MO string)
- manufacturing_order_id (ID integer atau nama MO string)
Contoh payload:
{
"business_category_id": 3,
"mo_id": "MO/2026/00021"
}
### 8) MO Detailed List
Endpoint: /api/scada/bc/mo-list-detailed
Payload wajib:
- business_category_id atau business_category_code atau business_category_name
Payload opsional:
- limit (default 10)
- offset (default 0)
- states (array atau string comma separated)
Contoh payload:
{
"business_category_id": 3,
"states": ["confirmed", "progress"],
"limit": 20
}
### 9) OEE Detail
Endpoint: /api/scada/bc/oee-detail
Payload wajib:
- business_category_id atau business_category_code atau business_category_name
Payload opsional:
- limit (default 50)
- offset (default 0)
- oee_id
- mo_id
- equipment_code
- date_from, date_to
- period (today, yesterday, this_week, last_7_days, this_month, last_month, this_year)
Contoh payload:
{
"business_category_id": 3,
"period": "this_month",
"equipment_code": "MIX-01"
}
### 10) OEE Equipment Average
Endpoint: /api/scada/bc/oee-equipment-avg
Payload wajib:
- business_category_id atau business_category_code atau business_category_name
Payload opsional:
- limit (default 100)
- offset (default 0)
- equipment_code
- equipment_type
- is_active (true/false)
- date_from, date_to
- period
Contoh payload:
{
"business_category_id": 3,
"period": "last_7_days",
"is_active": true
}
### 11) KPI Product Report
Endpoint: /api/scada/bc/kpi-product-report
Payload wajib:
- business_category_id atau business_category_code atau business_category_name
Payload opsional:
- limit (default 100)
- offset (default 0)
- product_id
- product_tmpl_id
- date_from, date_to
- period
Contoh payload:
{
"business_category_id": 3,
"product_tmpl_id": 452,
"period": "this_month"
}
### 12) Today Reports
Endpoint: /api/scada/bc/today-reports
Payload wajib:
- business_category_id atau business_category_code atau business_category_name
Payload opsional:
- limit (default 200) untuk chart data
- date (format tanggal harian sesuai parser backend)
Contoh payload:
{
"business_category_id": 3,
"date": "2026-04-11",
"limit": 200
}
### 13) Periodic Report
Endpoint: /api/scada/bc/periodic-report
Payload wajib:
- business_category_id atau business_category_code atau business_category_name
Payload opsional:
- date_from, date_to
- period (default this_month bila tanggal tidak dikirim)
- limit (default 1000) untuk tabel chart
Contoh payload:
{
"business_category_id": 3,
"period": "this_month",
"limit": 1000
}
### 14) Equipment Failure Report
Endpoint: /api/scada/bc/equipment-failure-report
Payload wajib:
- business_category_id atau business_category_code atau business_category_name
Payload opsional:
- limit (default 100)
- offset (default 0)
- equipment_code
- date_from, date_to
- period
Contoh payload:
{
"business_category_id": 3,
"period": "last_7_days",
"equipment_code": "MIX-01"
}
## Standard Error Handling untuk Middleware
Jika backend mengembalikan status error:
1. Simpan message dari backend ke log middleware.
2. Jika message menyebut business category required, middleware wajib retry dengan payload category yang valid.
3. Jangan fallback ke endpoint lama secara otomatis tanpa flag migrasi eksplisit.
## Rekomendasi Payload Wajib dari Middleware
Agar konsisten lintas endpoint, middleware disarankan selalu menyertakan:
- business_category_id
- limit
- offset
- period atau date_from/date_to (untuk endpoint report)
## Rekomendasi Kontrak untuk Dashboard
1. Dashboard simpan selected business category pada state global.
2. Semua request API BC menggunakan category yang sama sampai user mengganti context.
3. Tampilkan selected_business_category dari response sebagai indikator audit di UI.
## Catatan Migrasi
- Endpoint BC dipakai untuk mode segregasi ketat per business category.
- Data legacy tanpa business category tidak boleh dijadikan acuan operasional dashboard BC.
- Jika masih butuh validasi data legacy, lakukan lewat proses audit terpisah di backend.
+259
View File
@@ -0,0 +1,259 @@
# GRT SCADA Business Category
## Ringkasan
`grt_scada_business_category` adalah modul layer terpisah untuk menambahkan `Business Category` pada area SCADA tanpa memaksa perubahan agresif ke modul `grt_scada`.
Tujuan utama modul ini adalah:
- mempersiapkan segregasi SCADA per business category
- menjaga migrasi production bisa dilakukan bertahap
- mempertahankan endpoint lama tetap hidup
- menyediakan endpoint baru khusus business category dengan prefix `/api/scada/bc/...`
## Mengapa Dipisah dari `grt_scada`
Modul ini sengaja dipisah agar rollout lebih aman.
Keuntungan pendekatan ini:
- frontend lama tidak perlu langsung diubah
- route lama di `grt_scada` tetap berjalan
- tim bisa migrasi per halaman atau per dashboard
- data legacy yang belum punya category masih bisa dibaca jika diperlukan
- risiko regresi di production lebih rendah
## Dependensi
Modul bergantung pada:
- `grt_scada`
- `grt_business_category_base`
- `grt_inventory_business_category`
- `grt_mrp_business_category`
## Cakupan Model
### `scada.equipment`
Penambahan:
- `business_category_id`
Fungsi:
- menandai equipment SCADA per business category
- menjadi sumber category untuk sensor, log, failure, dan beberapa record SCADA lain
Default:
- mencoba ambil dari `mrp.workcenter` jika work center nantinya punya category
- fallback ke default category user melalui mixin
### Record SCADA yang kini BC-aware
Model berikut disiapkan dengan `business_category_id`:
- `scada.equipment.oee`
- `scada.equipment.oee.line`
- `scada.material.consumption`
- `scada.quality.control`
- `scada.sensor.reading`
- `scada.api.log`
- `scada.equipment.failure`
- `scada.equipment.material`
- `scada.mo.data`
- `scada.mo.weight`
Sumber nilai category mengikuti konteks record, misalnya:
- dari `mrp.production.business_category_id`
- dari `scada.equipment.business_category_id`
- dari `product.business_category_id`
- dari `stock.move.business_category_id`
### Integrasi dengan MRP dan Inventory
Modul juga menambahkan validasi category pada object yang terhubung ke SCADA:
- `mrp.bom.scada_equipment_id`
- `mrp.bom.line.scada_equipment_id`
- `mrp.production.scada_equipment_id`
- `stock.move.scada_equipment_id`
Tujuan:
- equipment SCADA yang dipakai pada BoM, MO, dan stock move harus tetap konsisten dengan business category manufacturing
## Alur Category pada Record SCADA
### `scada.equipment.oee`
Category mengikuti:
- `manufacturing_order_id.business_category_id`
### `scada.equipment.oee.line`
Category mengikuti:
- `oee_id.business_category_id`
### `scada.material.consumption`
Urutan fallback category:
1. `manufacturing_order_id.business_category_id`
2. `equipment_id.business_category_id`
3. `material_id.business_category_id`
4. `move_id.business_category_id`
### `scada.quality.control`
Category mengikuti:
- `product_id.business_category_id`
### `scada.sensor.reading`, `scada.api.log`, `scada.equipment.failure`
Category mengikuti:
- `equipment_id.business_category_id`
### `scada.equipment.material`
Urutan fallback category:
1. `manufacturing_order_id.business_category_id`
2. `equipment_id.business_category_id`
3. `product_id.business_category_id`
### `scada.mo.data` dan `scada.mo.weight`
Category mengikuti:
- `manufacturing_order_id.business_category_id`
## Security Rule dan Strategi Migrasi Bertahap
Security rule modul ini sekarang berjalan ketat:
- record harus memiliki `business_category_id` yang masuk `effective_business_category_ids`
- record legacy `business_category_id = False` tidak lagi dibuka untuk role operasional
Artinya:
- segregasi antar business category menjadi lebih kuat sejak awal
- migrasi data legacy harus diselesaikan sebelum user operasional memakai endpoint BC secara penuh
Model yang sudah dibatasi:
- `scada.equipment`
- `scada.equipment.oee`
- `scada.equipment.oee.line`
- `scada.quality.control`
- `scada.sensor.reading`
- `scada.api.log`
- `scada.equipment.failure`
- `scada.material.consumption`
- `scada.equipment.material`
- `scada.mo.data`
- `scada.mo.weight`
## Endpoint Baru untuk Frontend
Modul ini tidak mengganti endpoint lama.
Endpoint baru yang disiapkan:
- `/api/scada/bc/context`
- `/api/scada/bc/equipments`
- `/api/scada/bc/products`
- `/api/scada/bc/products-by-category`
- `/api/scada/bc/boms`
- `/api/scada/bc/mo-list-confirmed`
- `/api/scada/bc/mo-detail`
- `/api/scada/bc/mo-list-detailed`
- `/api/scada/bc/oee-detail`
- `/api/scada/bc/oee-equipment-avg`
- `/api/scada/bc/kpi-product-report`
- `/api/scada/bc/today-reports`
- `/api/scada/bc/periodic-report`
- `/api/scada/bc/equipment-failure-report`
## Parameter Business Category pada Endpoint
Endpoint BC mendukung context category melalui:
- `business_category_id`
- `business_category_code`
- `business_category_name`
Untuk endpoint operasional `/api/scada/bc/...` (selain endpoint context), business category dari middleware sekarang wajib dikirim.
Request tanpa business category tidak akan diproses.
Fallback active/effective category hanya dipertahankan untuk endpoint context:
- `request.env.user.active_business_category_id`
- jika user hanya punya satu effective category, category itu dipakai otomatis
Parameter tambahan:
- `include_unassigned`
Fungsi `include_unassigned`:
- pada mode ketat saat ini, record legacy tanpa category tidak lagi diekspos ke role operasional
- parameter ini dipertahankan hanya untuk kompatibilitas payload middleware lama
## Response Context
Sebagian besar endpoint BC mengembalikan:
- `selected_business_category`
Tujuannya:
- frontend tahu category mana yang sedang aktif
- audit request lebih mudah
- debugging cross-BU menjadi lebih jelas
## View dan UI
Modul memperluas view SCADA yang sudah ada.
Field, filter, atau group by category ditambahkan pada:
- equipment
- OEE summary
- quality control
- sensor reading
- API log
- equipment failure
Tujuan:
- user backend bisa mulai review data per category
- tim implementasi bisa audit hasil backfill tanpa query manual
## Migrasi Existing Data
`post_init_hook` melakukan backfill bertahap untuk record existing.
Urutan utamanya:
1. isi `scada.equipment.business_category_id` dari histori MO jika equipment tersebut hanya konsisten ke satu category
2. isi `scada.equipment.oee` dan `scada.equipment.oee.line` dari MO
3. isi `scada.material.consumption` dari MO, equipment, product, atau stock move
4. isi `scada.quality.control` dari product
5. isi `scada.sensor.reading`, `scada.api.log`, `scada.equipment.failure` dari equipment
6. isi `scada.equipment.material` dari MO, equipment, atau product
7. isi `scada.mo.data` dan `scada.mo.weight` dari MO
Catatan penting:
- equipment hanya di-backfill otomatis jika histori MO menunjukkan satu category yang konsisten
- ini sengaja dibuat hati-hati agar equipment yang ambigu tidak salah dipetakan
## Validasi Utama
Constraint penting yang dijaga:
- equipment SCADA pada BoM harus sama category dengan BoM
- equipment SCADA pada BoM line harus sama category dengan BoM
- equipment SCADA pada MO harus sama category dengan MO
- equipment SCADA pada stock move harus sama category dengan stock move
- material consumption dan equipment material tidak boleh berbeda category dari referensi utamanya
## Batasan Desain Saat Ini
Modul ini adalah fase aman untuk rollout bertahap, belum fase final paling ketat.
Karakter desain saat ini:
- route lama tidak dipaksa ikut BC
- endpoint BC mewajibkan business category dari middleware
- record legacy `False` tidak lagi dibuka untuk role operasional
- beberapa report BC adalah versi minimum-safe yang fokus pada filtering category, bukan redesign total seluruh analitik lama
## Urutan Rollout yang Disarankan
1. pastikan `grt_mrp_business_category` sudah stabil di staging
2. upgrade `grt_scada_business_category` di staging
3. review hasil backfill category pada equipment dan record SCADA utama
4. ubah frontend tertentu ke endpoint `/api/scada/bc/...`
5. jalankan periode dual-run antara endpoint lama dan endpoint BC
6. setelah hasil konsisten, baru lakukan migrasi frontend lebih luas
7. jika seluruh data sudah bersih, pertimbangkan memperketat rule legacy `business_category_id = False`
## Checklist Testing Staging
- cek equipment existing yang sudah terisi category otomatis
- cek equipment ambigu yang tetap kosong dan review manual
- cek OEE record lama bisa difilter per category
- cek QC, sensor, log, dan failure report muncul sesuai category user
- cek endpoint `/api/scada/bc/context` mengembalikan context yang benar
- cek endpoint `/api/scada/bc/mo-detail` hanya menampilkan MO pada category yang sesuai
- cek endpoint `/api/scada/bc/periodic-report` dan `/api/scada/bc/today-reports` bisa dipakai frontend
- cek user BU A tidak bisa melihat data BU B melalui endpoint BC
- cek request endpoint BC tanpa parameter business category ditolak
- cek data legacy `business_category_id = False` tidak lagi terbaca oleh operator/technician
## Hubungan dengan Modul Lain
- `grt_mrp_business_category` menjadi sumber category untuk `MO`, `BoM`, dan move manufacturing
- `grt_inventory_business_category` menjadi sumber category untuk stock move dan valuation inventory
- `grt_scada` tetap menjadi modul core SCADA
- `grt_scada_business_category` adalah layer segregasi dan migrasi aman di atasnya
## Dokumen Terkait
- [MIDDLEWARE_SCADA_BC_API_REFERENCE.md](./MIDDLEWARE_SCADA_BC_API_REFERENCE.md)
- [DASHBOARD_SCADA_BC_QUICK_REFERENCE.md](./DASHBOARD_SCADA_BC_QUICK_REFERENCE.md)
- [schemas/scada_bc_api.schemas.json](./schemas/scada_bc_api.schemas.json)
- [schemas/index.json](./schemas/index.json)
- [schemas/requests](./schemas/requests)
- [schemas/responses](./schemas/responses)
+4
View File
@@ -0,0 +1,4 @@
from . import hooks
from . import controllers
from . import models
from .hooks import post_init_hook
@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
{
"name": "GRT SCADA Business Category",
"summary": "Business category layer for SCADA equipment, reports, and dashboard endpoints",
"description": """
Adds a separate business category layer for SCADA so production migration can be
performed safely and gradually. The module prepares SCADA equipment, reports,
and frontend dashboard endpoints with explicit business category context
without forcing a risky cutover on the original grt_scada module.
""",
"author": "PT Gagak Rimang Teknologi",
"website": "https://rimang.id",
"category": "Manufacturing",
"version": "14.0.1.0.0",
"depends": [
"grt_scada",
"grt_business_category_base",
"grt_inventory_business_category",
"grt_mrp_business_category",
],
"post_init_hook": "post_init_hook",
"data": [
"security/ir.rule.csv",
"views/scada_business_category_core_views.xml",
"views/scada_business_category_operational_views.xml",
],
"installable": True,
"application": False,
}
@@ -0,0 +1 @@
from . import business_category_controller
File diff suppressed because it is too large Load Diff
+216
View File
@@ -0,0 +1,216 @@
# -*- coding: utf-8 -*-
def _table_has_column(cr, table_name, column_name):
cr.execute(
"""
SELECT 1
FROM information_schema.columns
WHERE table_name = %s
AND column_name = %s
""",
[table_name, column_name],
)
return bool(cr.fetchone())
def _backfill_scada_equipment_business_category(cr):
if not _table_has_column(cr, "scada_equipment", "business_category_id"):
return
cr.execute(
"""
UPDATE scada_equipment equipment
SET business_category_id = source.business_category_id
FROM (
SELECT production.scada_equipment_id AS equipment_id,
MIN(production.business_category_id) AS business_category_id
FROM mrp_production production
WHERE production.scada_equipment_id IS NOT NULL
AND production.business_category_id IS NOT NULL
GROUP BY production.scada_equipment_id
HAVING COUNT(DISTINCT production.business_category_id) = 1
) source
WHERE equipment.id = source.equipment_id
AND equipment.business_category_id IS NULL
"""
)
def _backfill_scada_records_business_category(cr):
statements = [
(
"scada_equipment_oee",
"""
UPDATE scada_equipment_oee record
SET business_category_id = production.business_category_id
FROM mrp_production production
WHERE record.business_category_id IS NULL
AND record.manufacturing_order_id = production.id
AND production.business_category_id IS NOT NULL
""",
),
(
"scada_equipment_oee_line",
"""
UPDATE scada_equipment_oee_line line
SET business_category_id = oee.business_category_id
FROM scada_equipment_oee oee
WHERE line.business_category_id IS NULL
AND line.oee_id = oee.id
AND oee.business_category_id IS NOT NULL
""",
),
(
"scada_material_consumption",
"""
UPDATE scada_material_consumption record
SET business_category_id = production.business_category_id
FROM mrp_production production
WHERE record.business_category_id IS NULL
AND record.manufacturing_order_id = production.id
AND production.business_category_id IS NOT NULL
""",
),
(
"scada_material_consumption",
"""
UPDATE scada_material_consumption record
SET business_category_id = equipment.business_category_id
FROM scada_equipment equipment
WHERE record.business_category_id IS NULL
AND record.equipment_id = equipment.id
AND equipment.business_category_id IS NOT NULL
""",
),
(
"scada_material_consumption",
"""
UPDATE scada_material_consumption record
SET business_category_id = product.business_category_id
FROM product_product product
WHERE record.business_category_id IS NULL
AND record.material_id = product.id
AND product.business_category_id IS NOT NULL
""",
),
(
"scada_material_consumption",
"""
UPDATE scada_material_consumption record
SET business_category_id = move.business_category_id
FROM stock_move move
WHERE record.business_category_id IS NULL
AND record.move_id = move.id
AND move.business_category_id IS NOT NULL
""",
),
(
"scada_quality_control",
"""
UPDATE scada_quality_control record
SET business_category_id = product.business_category_id
FROM product_product product
WHERE record.business_category_id IS NULL
AND record.product_id = product.id
AND product.business_category_id IS NOT NULL
""",
),
(
"scada_sensor_reading",
"""
UPDATE scada_sensor_reading record
SET business_category_id = equipment.business_category_id
FROM scada_equipment equipment
WHERE record.business_category_id IS NULL
AND record.equipment_id = equipment.id
AND equipment.business_category_id IS NOT NULL
""",
),
(
"scada_api_log",
"""
UPDATE scada_api_log record
SET business_category_id = equipment.business_category_id
FROM scada_equipment equipment
WHERE record.business_category_id IS NULL
AND record.equipment_id = equipment.id
AND equipment.business_category_id IS NOT NULL
""",
),
(
"scada_equipment_failure",
"""
UPDATE scada_equipment_failure record
SET business_category_id = equipment.business_category_id
FROM scada_equipment equipment
WHERE record.business_category_id IS NULL
AND record.equipment_id = equipment.id
AND equipment.business_category_id IS NOT NULL
""",
),
(
"scada_equipment_material",
"""
UPDATE scada_equipment_material record
SET business_category_id = production.business_category_id
FROM mrp_production production
WHERE record.business_category_id IS NULL
AND record.manufacturing_order_id = production.id
AND production.business_category_id IS NOT NULL
""",
),
(
"scada_equipment_material",
"""
UPDATE scada_equipment_material record
SET business_category_id = equipment.business_category_id
FROM scada_equipment equipment
WHERE record.business_category_id IS NULL
AND record.equipment_id = equipment.id
AND equipment.business_category_id IS NOT NULL
""",
),
(
"scada_equipment_material",
"""
UPDATE scada_equipment_material record
SET business_category_id = product.business_category_id
FROM product_product product
WHERE record.business_category_id IS NULL
AND record.product_id = product.id
AND product.business_category_id IS NOT NULL
""",
),
(
"scada_mo_data",
"""
UPDATE scada_mo_data record
SET business_category_id = production.business_category_id
FROM mrp_production production
WHERE record.business_category_id IS NULL
AND record.manufacturing_order_id = production.id
AND production.business_category_id IS NOT NULL
""",
),
(
"scada_mo_weight",
"""
UPDATE scada_mo_weight record
SET business_category_id = production.business_category_id
FROM mrp_production production
WHERE record.business_category_id IS NULL
AND record.manufacturing_order_id = production.id
AND production.business_category_id IS NOT NULL
""",
),
]
for table_name, statement in statements:
if _table_has_column(cr, table_name, "business_category_id"):
cr.execute(statement)
def post_init_hook(cr, registry):
_backfill_scada_equipment_business_category(cr)
_backfill_scada_records_business_category(cr)
@@ -0,0 +1,3 @@
from . import mrp_scada
from . import scada_equipment
from . import scada_records
@@ -0,0 +1,100 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class MrpBom(models.Model):
_inherit = "mrp.bom"
scada_equipment_id = fields.Many2one(
"scada.equipment",
string="Default SCADA Equipment",
domain="[('business_category_id', 'in', [business_category_id, False])]",
help="Default equipment to prefill on MO created from this BoM.",
)
@api.constrains("business_category_id", "scada_equipment_id")
def _check_scada_equipment_business_category(self):
for bom in self:
if (
bom.business_category_id
and bom.scada_equipment_id
and bom.scada_equipment_id.business_category_id
and bom.scada_equipment_id.business_category_id != bom.business_category_id
):
raise ValidationError(
_(
"BoM '%s' and SCADA Equipment '%s' must use the same Business Category."
)
% (bom.display_name, bom.scada_equipment_id.display_name)
)
class MrpBomLine(models.Model):
_inherit = "mrp.bom.line"
scada_equipment_id = fields.Many2one(
"scada.equipment",
string="SCADA Equipment (Optional)",
domain="[('business_category_id', 'in', [business_category_id, False])]",
help="Optional equipment mapping for this component.",
)
@api.constrains("bom_id", "scada_equipment_id")
def _check_scada_equipment_business_category(self):
for line in self:
business_category = line.bom_id.business_category_id
equipment_category = line.scada_equipment_id.business_category_id
if business_category and equipment_category and business_category != equipment_category:
raise ValidationError(
_(
"BoM Line '%s' uses SCADA Equipment '%s' from a different Business Category."
)
% (line.product_id.display_name, line.scada_equipment_id.display_name)
)
class MrpProduction(models.Model):
_inherit = "mrp.production"
scada_equipment_id = fields.Many2one(
"scada.equipment",
string="SCADA Equipment",
domain="[('business_category_id', 'in', [business_category_id, False])]",
help="Defaulted from BoM, can be changed per MO for operational needs.",
)
@api.constrains("business_category_id", "scada_equipment_id")
def _check_scada_equipment_business_category(self):
for production in self:
if (
production.business_category_id
and production.scada_equipment_id
and production.scada_equipment_id.business_category_id
and production.scada_equipment_id.business_category_id != production.business_category_id
):
raise ValidationError(
_(
"Manufacturing Order '%s' and SCADA Equipment '%s' must use the same Business Category."
)
% (production.display_name, production.scada_equipment_id.display_name)
)
class StockMove(models.Model):
_inherit = "stock.move"
@api.constrains("business_category_id", "scada_equipment_id")
def _check_scada_equipment_business_category(self):
for move in self:
if (
move.business_category_id
and move.scada_equipment_id
and move.scada_equipment_id.business_category_id
and move.scada_equipment_id.business_category_id != move.business_category_id
):
raise ValidationError(
_(
"Stock Move '%s' and SCADA Equipment '%s' must use the same Business Category."
)
% (move.display_name, move.scada_equipment_id.display_name)
)
@@ -0,0 +1,49 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class ScadaEquipment(models.Model):
_inherit = "scada.equipment"
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
default=lambda self: self.env["business.category.mixin"]._default_business_category_id(),
ondelete="restrict",
index=True,
help="Business category assigned to this SCADA equipment for safe staged segregation.",
)
@api.model_create_multi
def create(self, vals_list):
vals_list = [self._prepare_business_category_vals(vals) for vals in vals_list]
return super().create(vals_list)
def write(self, vals):
vals = self._prepare_business_category_vals(vals)
return super().write(vals)
def _prepare_business_category_vals(self, vals):
vals = dict(vals)
if vals.get("business_category_id"):
return vals
production_line = self.env["mrp.workcenter"].browse(vals.get("production_line_id"))
if production_line and hasattr(production_line, "business_category_id") and production_line.business_category_id:
vals["business_category_id"] = production_line.business_category_id.id
return vals
@api.constrains("business_category_id")
def _check_business_category_user_access(self):
current_user = self.env.user
if current_user.has_group("base.group_system"):
return
effective_categories = current_user.effective_business_category_ids
for record in self:
if record.business_category_id and record.business_category_id not in effective_categories:
raise ValidationError(
_(
"Equipment '%s' uses Business Category '%s' outside your effective access."
)
% (record.display_name, record.business_category_id.display_name)
)
@@ -0,0 +1,233 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class ScadaEquipmentOee(models.Model):
_inherit = "scada.equipment.oee"
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
related="manufacturing_order_id.business_category_id",
store=True,
readonly=True,
index=True,
)
class ScadaEquipmentOeeLine(models.Model):
_inherit = "scada.equipment.oee.line"
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
related="oee_id.business_category_id",
store=True,
readonly=True,
index=True,
)
class ScadaMaterialConsumption(models.Model):
_inherit = "scada.material.consumption"
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
ondelete="restrict",
index=True,
)
inventory_analytic_account_id = fields.Many2one(
"account.analytic.account",
string="Inventory Analytic Account",
related="business_category_id.inventory_analytic_account_id",
store=True,
readonly=True,
)
@api.model_create_multi
def create(self, vals_list):
vals_list = [self._prepare_business_category_vals(vals) for vals in vals_list]
return super().create(vals_list)
def write(self, vals):
vals = self._prepare_business_category_vals(vals)
return super().write(vals)
def _prepare_business_category_vals(self, vals):
vals = dict(vals)
if vals.get("business_category_id"):
return vals
manufacturing_order = self.env["mrp.production"].browse(vals.get("manufacturing_order_id"))
equipment = self.env["scada.equipment"].browse(vals.get("equipment_id"))
material = self.env["product.product"].browse(vals.get("material_id"))
move = self.env["stock.move"].browse(vals.get("move_id"))
category = (
(manufacturing_order and manufacturing_order.business_category_id)
or (equipment and equipment.business_category_id)
or (material and material.business_category_id)
or (move and move.business_category_id)
)
if category:
vals["business_category_id"] = category.id
return vals
@api.constrains("business_category_id", "manufacturing_order_id", "equipment_id", "material_id", "move_id")
def _check_business_category_consistency(self):
for record in self:
expected_categories = (
record.manufacturing_order_id.business_category_id
| record.equipment_id.business_category_id
| record.material_id.business_category_id
| record.move_id.business_category_id
).filtered(lambda category: category)
if record.business_category_id and expected_categories and record.business_category_id not in expected_categories:
raise ValidationError(
_(
"Material Consumption '%s' has Business Category '%s' that does not match its SCADA references."
)
% (record.display_name, record.business_category_id.display_name)
)
class ScadaQualityControl(models.Model):
_inherit = "scada.quality.control"
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
related="product_id.business_category_id",
store=True,
readonly=True,
index=True,
)
class ScadaSensorReading(models.Model):
_inherit = "scada.sensor.reading"
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
related="equipment_id.business_category_id",
store=True,
readonly=True,
index=True,
)
class ScadaApiLog(models.Model):
_inherit = "scada.api.log"
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
related="equipment_id.business_category_id",
store=True,
readonly=True,
index=True,
)
class ScadaEquipmentFailure(models.Model):
_inherit = "scada.equipment.failure"
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
related="equipment_id.business_category_id",
store=True,
readonly=True,
index=True,
)
class ScadaEquipmentMaterial(models.Model):
_inherit = "scada.equipment.material"
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
ondelete="restrict",
index=True,
)
inventory_analytic_account_id = fields.Many2one(
"account.analytic.account",
string="Inventory Analytic Account",
related="business_category_id.inventory_analytic_account_id",
store=True,
readonly=True,
)
@api.model_create_multi
def create(self, vals_list):
vals_list = [self._prepare_business_category_vals(vals) for vals in vals_list]
return super().create(vals_list)
def write(self, vals):
vals = self._prepare_business_category_vals(vals)
return super().write(vals)
def _prepare_business_category_vals(self, vals):
vals = dict(vals)
if vals.get("business_category_id"):
return vals
manufacturing_order = self.env["mrp.production"].browse(vals.get("manufacturing_order_id"))
equipment = self.env["scada.equipment"].browse(vals.get("equipment_id"))
product = self.env["product.product"].browse(vals.get("product_id"))
category = (
(manufacturing_order and manufacturing_order.business_category_id)
or (equipment and equipment.business_category_id)
or (product and product.business_category_id)
)
if category:
vals["business_category_id"] = category.id
return vals
@api.constrains("business_category_id", "manufacturing_order_id", "equipment_id", "product_id")
def _check_equipment_material_business_category_consistency(self):
for record in self:
expected_categories = (
record.manufacturing_order_id.business_category_id
| record.equipment_id.business_category_id
| record.product_id.business_category_id
).filtered(lambda category: category)
if record.business_category_id and expected_categories and record.business_category_id not in expected_categories:
raise ValidationError(
_(
"Equipment Material '%s' has Business Category '%s' that does not match its SCADA references."
)
% (record.display_name, record.business_category_id.display_name)
)
class ScadaMoData(models.Model):
_inherit = "scada.mo.data"
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
related="manufacturing_order_id.business_category_id",
store=True,
readonly=True,
index=True,
)
class ScadaMoWeight(models.Model):
_inherit = "scada.mo.weight"
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
related="manufacturing_order_id.business_category_id",
store=True,
readonly=True,
index=True,
)
@@ -0,0 +1,77 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/index.json",
"title": "SCADA BC API endpoint schema index",
"endpoints": [
{
"endpoint": "/api/scada/bc/context",
"request_schema": "./requests/context.request.schema.json",
"response_schema": "./responses/context.response.schema.json"
},
{
"endpoint": "/api/scada/bc/equipments",
"request_schema": "./requests/equipments.request.schema.json",
"response_schema": "./responses/equipments.response.schema.json"
},
{
"endpoint": "/api/scada/bc/products",
"request_schema": "./requests/products.request.schema.json",
"response_schema": "./responses/products.response.schema.json"
},
{
"endpoint": "/api/scada/bc/products-by-category",
"request_schema": "./requests/products-by-category.request.schema.json",
"response_schema": "./responses/products-by-category.response.schema.json"
},
{
"endpoint": "/api/scada/bc/boms",
"request_schema": "./requests/boms.request.schema.json",
"response_schema": "./responses/boms.response.schema.json"
},
{
"endpoint": "/api/scada/bc/mo-list-confirmed",
"request_schema": "./requests/mo-list-confirmed.request.schema.json",
"response_schema": "./responses/mo-list-confirmed.response.schema.json"
},
{
"endpoint": "/api/scada/bc/mo-detail",
"request_schema": "./requests/mo-detail.request.schema.json",
"response_schema": "./responses/mo-detail.response.schema.json"
},
{
"endpoint": "/api/scada/bc/mo-list-detailed",
"request_schema": "./requests/mo-list-detailed.request.schema.json",
"response_schema": "./responses/mo-list-detailed.response.schema.json"
},
{
"endpoint": "/api/scada/bc/oee-detail",
"request_schema": "./requests/oee-detail.request.schema.json",
"response_schema": "./responses/oee-detail.response.schema.json"
},
{
"endpoint": "/api/scada/bc/oee-equipment-avg",
"request_schema": "./requests/oee-equipment-avg.request.schema.json",
"response_schema": "./responses/oee-equipment-avg.response.schema.json"
},
{
"endpoint": "/api/scada/bc/kpi-product-report",
"request_schema": "./requests/kpi-product-report.request.schema.json",
"response_schema": "./responses/kpi-product-report.response.schema.json"
},
{
"endpoint": "/api/scada/bc/today-reports",
"request_schema": "./requests/today-reports.request.schema.json",
"response_schema": "./responses/today-reports.response.schema.json"
},
{
"endpoint": "/api/scada/bc/periodic-report",
"request_schema": "./requests/periodic-report.request.schema.json",
"response_schema": "./responses/periodic-report.response.schema.json"
},
{
"endpoint": "/api/scada/bc/equipment-failure-report",
"request_schema": "./requests/equipment-failure-report.request.schema.json",
"response_schema": "./responses/equipment-failure-report.response.schema.json"
}
]
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/requests/boms.request.schema.json",
"title": "Request schema for /api/scada/bc/boms",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1boms/request"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/requests/context.request.schema.json",
"title": "Request schema for /api/scada/bc/context",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1context/request"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/requests/equipment-failure-report.request.schema.json",
"title": "Request schema for /api/scada/bc/equipment-failure-report",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1equipment-failure-report/request"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/requests/equipments.request.schema.json",
"title": "Request schema for /api/scada/bc/equipments",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1equipments/request"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/requests/kpi-product-report.request.schema.json",
"title": "Request schema for /api/scada/bc/kpi-product-report",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1kpi-product-report/request"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/requests/mo-detail.request.schema.json",
"title": "Request schema for /api/scada/bc/mo-detail",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1mo-detail/request"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/requests/mo-list-confirmed.request.schema.json",
"title": "Request schema for /api/scada/bc/mo-list-confirmed",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1mo-list-confirmed/request"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/requests/mo-list-detailed.request.schema.json",
"title": "Request schema for /api/scada/bc/mo-list-detailed",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1mo-list-detailed/request"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/requests/oee-detail.request.schema.json",
"title": "Request schema for /api/scada/bc/oee-detail",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1oee-detail/request"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/requests/oee-equipment-avg.request.schema.json",
"title": "Request schema for /api/scada/bc/oee-equipment-avg",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1oee-equipment-avg/request"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/requests/periodic-report.request.schema.json",
"title": "Request schema for /api/scada/bc/periodic-report",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1periodic-report/request"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/requests/products-by-category.request.schema.json",
"title": "Request schema for /api/scada/bc/products-by-category",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1products-by-category/request"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/requests/products.request.schema.json",
"title": "Request schema for /api/scada/bc/products",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1products/request"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/requests/today-reports.request.schema.json",
"title": "Request schema for /api/scada/bc/today-reports",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1today-reports/request"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/responses/boms.response.schema.json",
"title": "Response schema for /api/scada/bc/boms",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1boms/response"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/responses/context.response.schema.json",
"title": "Response schema for /api/scada/bc/context",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1context/response"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/responses/equipment-failure-report.response.schema.json",
"title": "Response schema for /api/scada/bc/equipment-failure-report",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1equipment-failure-report/response"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/responses/equipments.response.schema.json",
"title": "Response schema for /api/scada/bc/equipments",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1equipments/response"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/responses/kpi-product-report.response.schema.json",
"title": "Response schema for /api/scada/bc/kpi-product-report",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1kpi-product-report/response"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/responses/mo-detail.response.schema.json",
"title": "Response schema for /api/scada/bc/mo-detail",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1mo-detail/response"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/responses/mo-list-confirmed.response.schema.json",
"title": "Response schema for /api/scada/bc/mo-list-confirmed",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1mo-list-confirmed/response"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/responses/mo-list-detailed.response.schema.json",
"title": "Response schema for /api/scada/bc/mo-list-detailed",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1mo-list-detailed/response"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/responses/oee-detail.response.schema.json",
"title": "Response schema for /api/scada/bc/oee-detail",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1oee-detail/response"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/responses/oee-equipment-avg.response.schema.json",
"title": "Response schema for /api/scada/bc/oee-equipment-avg",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1oee-equipment-avg/response"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/responses/periodic-report.response.schema.json",
"title": "Response schema for /api/scada/bc/periodic-report",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1periodic-report/response"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/responses/products-by-category.response.schema.json",
"title": "Response schema for /api/scada/bc/products-by-category",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1products-by-category/response"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/responses/products.response.schema.json",
"title": "Response schema for /api/scada/bc/products",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1products/response"
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/responses/today-reports.response.schema.json",
"title": "Response schema for /api/scada/bc/today-reports",
"$ref": "../scada_bc_api.schemas.json#/endpoints/~1api~1scada~1bc~1today-reports/response"
}
@@ -0,0 +1,669 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grt_scada_business_category/schemas/scada_bc_api.schemas.json",
"title": "SCADA Business Category API Schemas",
"type": "object",
"description": "Template JSON schema for request/response of /api/scada/bc endpoints.",
"definitions": {
"BusinessCategorySelectorRequired": {
"type": "object",
"oneOf": [
{
"required": ["business_category_id"],
"properties": {
"business_category_id": {
"type": "integer",
"minimum": 1
}
}
},
{
"required": ["business_category_code"],
"properties": {
"business_category_code": {
"type": "string",
"minLength": 1
}
}
},
{
"required": ["business_category_name"],
"properties": {
"business_category_name": {
"type": "string",
"minLength": 1
}
}
}
]
},
"BusinessCategorySelectorOptional": {
"type": "object",
"properties": {
"business_category_id": {
"type": "integer",
"minimum": 1
},
"business_category_code": {
"type": "string"
},
"business_category_name": {
"type": "string"
}
}
},
"Pagination": {
"type": "object",
"properties": {
"limit": {
"type": "integer",
"minimum": 1
},
"offset": {
"type": "integer",
"minimum": 0
}
}
},
"PeriodFilter": {
"type": "object",
"properties": {
"date_from": {
"type": "string"
},
"date_to": {
"type": "string"
},
"period": {
"type": "string",
"enum": [
"today",
"yesterday",
"this_week",
"last_7_days",
"this_month",
"last_month",
"this_year"
]
}
}
},
"ApiErrorResponse": {
"type": "object",
"required": ["status", "message"],
"properties": {
"status": {
"const": "error"
},
"message": {
"type": "string"
}
},
"additionalProperties": true
},
"BusinessCategory": {
"type": ["object", "null"],
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"code": {
"type": "string"
},
"company_id": {
"type": ["integer", "null"]
},
"company_name": {
"type": ["string", "null"]
}
},
"additionalProperties": true
},
"StandardSuccessList": {
"type": "object",
"required": ["status", "count", "data"],
"properties": {
"status": {
"const": "success"
},
"count": {
"type": "integer",
"minimum": 0
},
"selected_business_category": {
"$ref": "#/definitions/BusinessCategory"
},
"data": {
"type": "array"
}
},
"additionalProperties": true
},
"ContextResponse": {
"type": "object",
"required": ["status", "effective_business_categories"],
"properties": {
"status": {
"const": "success"
},
"active_business_category": {
"$ref": "#/definitions/BusinessCategory"
},
"selected_business_category": {
"$ref": "#/definitions/BusinessCategory"
},
"effective_business_categories": {
"type": "array",
"items": {
"$ref": "#/definitions/BusinessCategory"
}
}
},
"additionalProperties": true
}
},
"endpoints": {
"/api/scada/bc/context": {
"request": {
"$ref": "#/definitions/BusinessCategorySelectorOptional"
},
"response": {
"$ref": "#/definitions/ContextResponse"
},
"error": {
"$ref": "#/definitions/ApiErrorResponse"
}
},
"/api/scada/bc/equipments": {
"request": {
"allOf": [
{
"$ref": "#/definitions/BusinessCategorySelectorRequired"
},
{
"$ref": "#/definitions/Pagination"
},
{
"type": "object",
"properties": {
"equipment_code": {
"type": "string"
},
"equipment_type": {
"type": "string"
}
}
}
]
},
"response": {
"$ref": "#/definitions/StandardSuccessList"
},
"error": {
"$ref": "#/definitions/ApiErrorResponse"
}
},
"/api/scada/bc/products": {
"request": {
"allOf": [
{
"$ref": "#/definitions/BusinessCategorySelectorRequired"
},
{
"$ref": "#/definitions/Pagination"
},
{
"type": "object",
"properties": {
"category_id": {
"type": "integer"
},
"category_name": {
"type": "string"
},
"active": {
"type": ["boolean", "string"]
}
}
}
]
},
"response": {
"$ref": "#/definitions/StandardSuccessList"
},
"error": {
"$ref": "#/definitions/ApiErrorResponse"
}
},
"/api/scada/bc/products-by-category": {
"request": {
"$ref": "#/endpoints/~1api~1scada~1bc~1products/request"
},
"response": {
"$ref": "#/endpoints/~1api~1scada~1bc~1products/response"
},
"error": {
"$ref": "#/definitions/ApiErrorResponse"
}
},
"/api/scada/bc/boms": {
"request": {
"allOf": [
{
"$ref": "#/definitions/BusinessCategorySelectorRequired"
},
{
"$ref": "#/definitions/Pagination"
},
{
"type": "object",
"properties": {
"bom_id": {
"type": "integer"
},
"product_id": {
"type": "integer"
},
"product_tmpl_id": {
"type": "integer"
},
"active": {
"type": ["boolean", "string"]
}
}
}
]
},
"response": {
"$ref": "#/definitions/StandardSuccessList"
},
"error": {
"$ref": "#/definitions/ApiErrorResponse"
}
},
"/api/scada/bc/mo-list-confirmed": {
"request": {
"allOf": [
{
"$ref": "#/definitions/BusinessCategorySelectorRequired"
},
{
"$ref": "#/definitions/Pagination"
}
]
},
"response": {
"$ref": "#/definitions/StandardSuccessList"
},
"error": {
"$ref": "#/definitions/ApiErrorResponse"
}
},
"/api/scada/bc/mo-detail": {
"request": {
"allOf": [
{
"$ref": "#/definitions/BusinessCategorySelectorRequired"
},
{
"type": "object",
"oneOf": [
{
"required": ["mo_id"],
"properties": {
"mo_id": {
"type": ["string", "integer"]
}
}
},
{
"required": ["manufacturing_order_id"],
"properties": {
"manufacturing_order_id": {
"type": ["string", "integer"]
}
}
}
]
}
]
},
"response": {
"type": "object",
"required": ["status", "data"],
"properties": {
"status": {
"const": "success"
},
"selected_business_category": {
"$ref": "#/definitions/BusinessCategory"
},
"data": {
"type": "object",
"additionalProperties": true
}
}
},
"error": {
"$ref": "#/definitions/ApiErrorResponse"
}
},
"/api/scada/bc/mo-list-detailed": {
"request": {
"allOf": [
{
"$ref": "#/definitions/BusinessCategorySelectorRequired"
},
{
"$ref": "#/definitions/Pagination"
},
{
"type": "object",
"properties": {
"states": {
"type": ["array", "string"],
"items": {
"type": "string"
}
}
}
}
]
},
"response": {
"$ref": "#/definitions/StandardSuccessList"
},
"error": {
"$ref": "#/definitions/ApiErrorResponse"
}
},
"/api/scada/bc/oee-detail": {
"request": {
"allOf": [
{
"$ref": "#/definitions/BusinessCategorySelectorRequired"
},
{
"$ref": "#/definitions/Pagination"
},
{
"$ref": "#/definitions/PeriodFilter"
},
{
"type": "object",
"properties": {
"oee_id": {
"type": "integer"
},
"mo_id": {
"type": ["string", "integer"]
},
"equipment_code": {
"type": "string"
}
}
}
]
},
"response": {
"$ref": "#/definitions/StandardSuccessList"
},
"error": {
"$ref": "#/definitions/ApiErrorResponse"
}
},
"/api/scada/bc/oee-equipment-avg": {
"request": {
"allOf": [
{
"$ref": "#/definitions/BusinessCategorySelectorRequired"
},
{
"$ref": "#/definitions/Pagination"
},
{
"$ref": "#/definitions/PeriodFilter"
},
{
"type": "object",
"properties": {
"equipment_code": {
"type": "string"
},
"equipment_type": {
"type": "string"
},
"is_active": {
"type": ["boolean", "string"]
}
}
}
]
},
"response": {
"$ref": "#/definitions/StandardSuccessList"
},
"error": {
"$ref": "#/definitions/ApiErrorResponse"
}
},
"/api/scada/bc/kpi-product-report": {
"request": {
"allOf": [
{
"$ref": "#/definitions/BusinessCategorySelectorRequired"
},
{
"$ref": "#/definitions/Pagination"
},
{
"$ref": "#/definitions/PeriodFilter"
},
{
"type": "object",
"properties": {
"product_id": {
"type": "integer"
},
"product_tmpl_id": {
"type": "integer"
}
}
}
]
},
"response": {
"type": "object",
"required": ["status", "count", "data", "summary"],
"properties": {
"status": {
"const": "success"
},
"count": {
"type": "integer"
},
"selected_business_category": {
"$ref": "#/definitions/BusinessCategory"
},
"data": {
"type": "array"
},
"summary": {
"type": "object",
"additionalProperties": true
}
},
"additionalProperties": true
},
"error": {
"$ref": "#/definitions/ApiErrorResponse"
}
},
"/api/scada/bc/today-reports": {
"request": {
"allOf": [
{
"$ref": "#/definitions/BusinessCategorySelectorRequired"
},
{
"type": "object",
"properties": {
"limit": {
"type": "integer",
"minimum": 1
},
"date": {
"type": "string"
}
}
}
]
},
"response": {
"type": "object",
"required": ["status", "report_date"],
"properties": {
"status": {
"const": "success"
},
"selected_business_category": {
"$ref": "#/definitions/BusinessCategory"
},
"report_date": {
"type": "string"
},
"batch_status": {
"type": "object",
"additionalProperties": true
},
"total_production_today": {
"type": "object",
"additionalProperties": true
},
"oee_quality_today": {
"type": "object",
"additionalProperties": true
},
"batch_to_batch_deviation_chart": {
"type": "object",
"additionalProperties": true
}
},
"additionalProperties": true
},
"error": {
"$ref": "#/definitions/ApiErrorResponse"
}
},
"/api/scada/bc/periodic-report": {
"request": {
"allOf": [
{
"$ref": "#/definitions/BusinessCategorySelectorRequired"
},
{
"$ref": "#/definitions/PeriodFilter"
},
{
"type": "object",
"properties": {
"limit": {
"type": "integer",
"minimum": 1
}
}
}
]
},
"response": {
"type": "object",
"required": ["status", "filters"],
"properties": {
"status": {
"const": "success"
},
"filters": {
"type": "object",
"additionalProperties": true
},
"metrics_total_production": {
"type": "object",
"additionalProperties": true
},
"metrics_total_mo": {
"type": "object",
"additionalProperties": true
},
"metrics_avg_oee_quality": {
"type": "object",
"additionalProperties": true
},
"chart_raw_material_consumption": {
"type": "object",
"additionalProperties": true
},
"table_daily_finished_goods": {
"type": "object",
"additionalProperties": true
}
},
"additionalProperties": true
},
"error": {
"$ref": "#/definitions/ApiErrorResponse"
}
},
"/api/scada/bc/equipment-failure-report": {
"request": {
"allOf": [
{
"$ref": "#/definitions/BusinessCategorySelectorRequired"
},
{
"$ref": "#/definitions/Pagination"
},
{
"$ref": "#/definitions/PeriodFilter"
},
{
"type": "object",
"properties": {
"equipment_code": {
"type": "string"
}
}
}
]
},
"response": {
"type": "object",
"required": ["status", "count", "data", "summary"],
"properties": {
"status": {
"const": "success"
},
"count": {
"type": "integer"
},
"selected_business_category": {
"$ref": "#/definitions/BusinessCategory"
},
"data": {
"type": "array"
},
"summary": {
"type": "object",
"additionalProperties": true
}
},
"additionalProperties": true
},
"error": {
"$ref": "#/definitions/ApiErrorResponse"
}
}
}
}
@@ -0,0 +1,33 @@
id,name,model_id:id,domain_force,groups:id,perm_read,perm_write,perm_create,perm_unlink
scada_equipment_business_category_rule_manager,SCADA Equipment by effective business category for manager,grt_scada.model_scada_equipment,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_manager,1,1,1,1
scada_equipment_business_category_rule_operator,SCADA Equipment by effective business category for operator,grt_scada.model_scada_equipment,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_operator,1,1,1,1
scada_equipment_business_category_rule_technician,SCADA Equipment by effective business category for technician,grt_scada.model_scada_equipment,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_technician,1,1,1,1
scada_equipment_business_category_rule_sysadmin,SCADA Equipment full access for system admin,grt_scada.model_scada_equipment,"[(1,'=',1)]",base.group_system,1,1,1,1
scada_equipment_oee_business_category_rule_manager,SCADA OEE by effective business category for manager,grt_scada.model_scada_equipment_oee,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_manager,1,1,1,1
scada_equipment_oee_business_category_rule_operator,SCADA OEE by effective business category for operator,grt_scada.model_scada_equipment_oee,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_operator,1,1,1,1
scada_equipment_oee_business_category_rule_technician,SCADA OEE by effective business category for technician,grt_scada.model_scada_equipment_oee,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_technician,1,1,1,1
scada_equipment_oee_line_business_category_rule_manager,SCADA OEE Line by effective business category for manager,grt_scada.model_scada_equipment_oee_line,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_manager,1,1,1,1
scada_equipment_oee_line_business_category_rule_operator,SCADA OEE Line by effective business category for operator,grt_scada.model_scada_equipment_oee_line,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_operator,1,1,1,1
scada_equipment_oee_line_business_category_rule_technician,SCADA OEE Line by effective business category for technician,grt_scada.model_scada_equipment_oee_line,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_technician,1,1,1,1
scada_quality_control_business_category_rule_manager,SCADA QC by effective business category for manager,grt_scada.model_scada_quality_control,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_manager,1,1,1,1
scada_quality_control_business_category_rule_operator,SCADA QC by effective business category for operator,grt_scada.model_scada_quality_control,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_operator,1,1,1,1
scada_quality_control_business_category_rule_technician,SCADA QC by effective business category for technician,grt_scada.model_scada_quality_control,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_technician,1,1,1,1
scada_sensor_reading_business_category_rule_manager,SCADA Sensor Reading by effective business category for manager,grt_scada.model_scada_sensor_reading,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_manager,1,1,1,1
scada_sensor_reading_business_category_rule_operator,SCADA Sensor Reading by effective business category for operator,grt_scada.model_scada_sensor_reading,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_operator,1,1,1,1
scada_sensor_reading_business_category_rule_technician,SCADA Sensor Reading by effective business category for technician,grt_scada.model_scada_sensor_reading,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_technician,1,1,1,1
scada_api_log_business_category_rule_manager,SCADA API Log by effective business category for manager,grt_scada.model_scada_api_log,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_manager,1,1,1,1
scada_api_log_business_category_rule_operator,SCADA API Log by effective business category for operator,grt_scada.model_scada_api_log,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_operator,1,1,1,1
scada_api_log_business_category_rule_sysadmin,SCADA API Log full access for system admin,grt_scada.model_scada_api_log,"[(1,'=',1)]",base.group_system,1,1,1,1
scada_equipment_failure_business_category_rule_manager,SCADA Equipment Failure by effective business category for manager,grt_scada.model_scada_equipment_failure,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_manager,1,1,1,1
scada_equipment_failure_business_category_rule_operator,SCADA Equipment Failure by effective business category for operator,grt_scada.model_scada_equipment_failure,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_operator,1,1,1,1
scada_equipment_failure_business_category_rule_technician,SCADA Equipment Failure by effective business category for technician,grt_scada.model_scada_equipment_failure,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_technician,1,1,1,1
scada_material_consumption_business_category_rule_manager,SCADA Material Consumption by effective business category for manager,grt_scada.model_scada_material_consumption,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_manager,1,1,1,1
scada_material_consumption_business_category_rule_operator,SCADA Material Consumption by effective business category for operator,grt_scada.model_scada_material_consumption,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_operator,1,1,1,1
scada_equipment_material_business_category_rule_manager,SCADA Equipment Material by effective business category for manager,grt_scada.model_scada_equipment_material,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_manager,1,1,1,1
scada_equipment_material_business_category_rule_operator,SCADA Equipment Material by effective business category for operator,grt_scada.model_scada_equipment_material,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_operator,1,1,1,1
scada_equipment_material_business_category_rule_technician,SCADA Equipment Material by effective business category for technician,grt_scada.model_scada_equipment_material,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_technician,1,1,1,1
scada_mo_data_business_category_rule_manager,SCADA MO Data by effective business category for manager,grt_scada.model_scada_mo_data,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_manager,1,1,1,1
scada_mo_data_business_category_rule_operator,SCADA MO Data by effective business category for operator,grt_scada.model_scada_mo_data,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_operator,1,1,1,1
scada_mo_weight_business_category_rule_manager,SCADA MO Weight by effective business category for manager,grt_scada.model_scada_mo_weight,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_manager,1,1,1,1
scada_mo_weight_business_category_rule_operator,SCADA MO Weight by effective business category for operator,grt_scada.model_scada_mo_weight,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_operator,1,1,1,1
scada_mo_weight_business_category_rule_technician,SCADA MO Weight by effective business category for technician,grt_scada.model_scada_mo_weight,"[('business_category_id','in',user.effective_business_category_ids.ids)]",grt_scada.group_scada_technician,1,1,1,1
1 id name model_id:id domain_force groups:id perm_read perm_write perm_create perm_unlink
2 scada_equipment_business_category_rule_manager SCADA Equipment by effective business category for manager grt_scada.model_scada_equipment [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_manager 1 1 1 1
3 scada_equipment_business_category_rule_operator SCADA Equipment by effective business category for operator grt_scada.model_scada_equipment [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_operator 1 1 1 1
4 scada_equipment_business_category_rule_technician SCADA Equipment by effective business category for technician grt_scada.model_scada_equipment [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_technician 1 1 1 1
5 scada_equipment_business_category_rule_sysadmin SCADA Equipment full access for system admin grt_scada.model_scada_equipment [(1,'=',1)] base.group_system 1 1 1 1
6 scada_equipment_oee_business_category_rule_manager SCADA OEE by effective business category for manager grt_scada.model_scada_equipment_oee [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_manager 1 1 1 1
7 scada_equipment_oee_business_category_rule_operator SCADA OEE by effective business category for operator grt_scada.model_scada_equipment_oee [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_operator 1 1 1 1
8 scada_equipment_oee_business_category_rule_technician SCADA OEE by effective business category for technician grt_scada.model_scada_equipment_oee [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_technician 1 1 1 1
9 scada_equipment_oee_line_business_category_rule_manager SCADA OEE Line by effective business category for manager grt_scada.model_scada_equipment_oee_line [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_manager 1 1 1 1
10 scada_equipment_oee_line_business_category_rule_operator SCADA OEE Line by effective business category for operator grt_scada.model_scada_equipment_oee_line [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_operator 1 1 1 1
11 scada_equipment_oee_line_business_category_rule_technician SCADA OEE Line by effective business category for technician grt_scada.model_scada_equipment_oee_line [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_technician 1 1 1 1
12 scada_quality_control_business_category_rule_manager SCADA QC by effective business category for manager grt_scada.model_scada_quality_control [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_manager 1 1 1 1
13 scada_quality_control_business_category_rule_operator SCADA QC by effective business category for operator grt_scada.model_scada_quality_control [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_operator 1 1 1 1
14 scada_quality_control_business_category_rule_technician SCADA QC by effective business category for technician grt_scada.model_scada_quality_control [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_technician 1 1 1 1
15 scada_sensor_reading_business_category_rule_manager SCADA Sensor Reading by effective business category for manager grt_scada.model_scada_sensor_reading [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_manager 1 1 1 1
16 scada_sensor_reading_business_category_rule_operator SCADA Sensor Reading by effective business category for operator grt_scada.model_scada_sensor_reading [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_operator 1 1 1 1
17 scada_sensor_reading_business_category_rule_technician SCADA Sensor Reading by effective business category for technician grt_scada.model_scada_sensor_reading [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_technician 1 1 1 1
18 scada_api_log_business_category_rule_manager SCADA API Log by effective business category for manager grt_scada.model_scada_api_log [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_manager 1 1 1 1
19 scada_api_log_business_category_rule_operator SCADA API Log by effective business category for operator grt_scada.model_scada_api_log [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_operator 1 1 1 1
20 scada_api_log_business_category_rule_sysadmin SCADA API Log full access for system admin grt_scada.model_scada_api_log [(1,'=',1)] base.group_system 1 1 1 1
21 scada_equipment_failure_business_category_rule_manager SCADA Equipment Failure by effective business category for manager grt_scada.model_scada_equipment_failure [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_manager 1 1 1 1
22 scada_equipment_failure_business_category_rule_operator SCADA Equipment Failure by effective business category for operator grt_scada.model_scada_equipment_failure [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_operator 1 1 1 1
23 scada_equipment_failure_business_category_rule_technician SCADA Equipment Failure by effective business category for technician grt_scada.model_scada_equipment_failure [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_technician 1 1 1 1
24 scada_material_consumption_business_category_rule_manager SCADA Material Consumption by effective business category for manager grt_scada.model_scada_material_consumption [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_manager 1 1 1 1
25 scada_material_consumption_business_category_rule_operator SCADA Material Consumption by effective business category for operator grt_scada.model_scada_material_consumption [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_operator 1 1 1 1
26 scada_equipment_material_business_category_rule_manager SCADA Equipment Material by effective business category for manager grt_scada.model_scada_equipment_material [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_manager 1 1 1 1
27 scada_equipment_material_business_category_rule_operator SCADA Equipment Material by effective business category for operator grt_scada.model_scada_equipment_material [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_operator 1 1 1 1
28 scada_equipment_material_business_category_rule_technician SCADA Equipment Material by effective business category for technician grt_scada.model_scada_equipment_material [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_technician 1 1 1 1
29 scada_mo_data_business_category_rule_manager SCADA MO Data by effective business category for manager grt_scada.model_scada_mo_data [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_manager 1 1 1 1
30 scada_mo_data_business_category_rule_operator SCADA MO Data by effective business category for operator grt_scada.model_scada_mo_data [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_operator 1 1 1 1
31 scada_mo_weight_business_category_rule_manager SCADA MO Weight by effective business category for manager grt_scada.model_scada_mo_weight [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_manager 1 1 1 1
32 scada_mo_weight_business_category_rule_operator SCADA MO Weight by effective business category for operator grt_scada.model_scada_mo_weight [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_operator 1 1 1 1
33 scada_mo_weight_business_category_rule_technician SCADA MO Weight by effective business category for technician grt_scada.model_scada_mo_weight [('business_category_id','in',user.effective_business_category_ids.ids)] grt_scada.group_scada_technician 1 1 1 1
@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_scada_equipment_list_business_category" model="ir.ui.view">
<field name="name">scada.equipment.list.business.category</field>
<field name="model">scada.equipment</field>
<field name="inherit_id" ref="grt_scada.view_scada_equipment_list"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='manufacturer']" position="after">
<field name="business_category_id" optional="show"/>
</xpath>
</field>
</record>
<record id="view_scada_equipment_form_business_category" model="ir.ui.view">
<field name="name">scada.equipment.form.business.category</field>
<field name="model">scada.equipment</field>
<field name="inherit_id" ref="grt_scada.view_scada_equipment_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='production_line_id']" position="before">
<field name="business_category_id"/>
</xpath>
</field>
</record>
<record id="view_scada_equipment_search_business_category" model="ir.ui.view">
<field name="name">scada.equipment.search.business.category</field>
<field name="model">scada.equipment</field>
<field name="inherit_id" ref="grt_scada.view_scada_equipment_search"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='manufacturer']" position="after">
<field name="business_category_id"/>
</xpath>
<xpath expr="//group[@string='Group By']" position="inside">
<filter string="Business Category" name="group_by_business_category" context="{'group_by': 'business_category_id'}"/>
</xpath>
</field>
</record>
<record id="view_scada_equipment_oee_tree_business_category" model="ir.ui.view">
<field name="name">scada.equipment.oee.tree.business.category</field>
<field name="model">scada.equipment.oee</field>
<field name="inherit_id" ref="grt_scada.view_scada_equipment_oee_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='product_id']" position="after">
<field name="business_category_id" optional="show"/>
</xpath>
</field>
</record>
<record id="view_scada_equipment_oee_form_business_category" model="ir.ui.view">
<field name="name">scada.equipment.oee.form.business.category</field>
<field name="model">scada.equipment.oee</field>
<field name="inherit_id" ref="grt_scada.view_scada_equipment_oee_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='product_id']" position="after">
<field name="business_category_id" readonly="1"/>
</xpath>
</field>
</record>
<record id="view_scada_equipment_oee_search_business_category" model="ir.ui.view">
<field name="name">scada.equipment.oee.search.business.category</field>
<field name="model">scada.equipment.oee</field>
<field name="inherit_id" ref="grt_scada.view_scada_equipment_oee_search"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='date_done']" position="after">
<field name="business_category_id"/>
</xpath>
<xpath expr="//group[@string='Group By']" position="inside">
<filter string="Business Category" name="group_business_category" context="{'group_by':'business_category_id'}"/>
</xpath>
</field>
</record>
<record id="view_scada_quality_control_tree_business_category" model="ir.ui.view">
<field name="name">scada.quality.control.tree.business.category</field>
<field name="model">scada.quality.control</field>
<field name="inherit_id" ref="grt_scada.view_scada_quality_control_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='lot_id']" position="after">
<field name="business_category_id" optional="show"/>
</xpath>
</field>
</record>
<record id="view_scada_quality_control_form_business_category" model="ir.ui.view">
<field name="name">scada.quality.control.form.business.category</field>
<field name="model">scada.quality.control</field>
<field name="inherit_id" ref="grt_scada.view_scada_quality_control_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='kode']" position="after">
<field name="business_category_id" readonly="1"/>
</xpath>
</field>
</record>
<record id="view_scada_quality_control_search_business_category" model="ir.ui.view">
<field name="name">scada.quality.control.search.business.category</field>
<field name="model">scada.quality.control</field>
<field name="inherit_id" ref="grt_scada.view_scada_quality_control_search"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='tanggal_uji']" position="after">
<field name="business_category_id"/>
</xpath>
<xpath expr="//group[@string='Group By']" position="inside">
<filter string="Business Category" name="group_business_category" context="{'group_by':'business_category_id'}"/>
</xpath>
</field>
</record>
</data>
</odoo>
@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_scada_sensor_reading_list_business_category" model="ir.ui.view">
<field name="name">scada.sensor.reading.list.business.category</field>
<field name="model">scada.sensor.reading</field>
<field name="inherit_id" ref="grt_scada.view_scada_sensor_reading_list"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='sensor_type']" position="after">
<field name="business_category_id" optional="show"/>
</xpath>
</field>
</record>
<record id="view_scada_sensor_reading_form_business_category" model="ir.ui.view">
<field name="name">scada.sensor.reading.form.business.category</field>
<field name="model">scada.sensor.reading</field>
<field name="inherit_id" ref="grt_scada.view_scada_sensor_reading_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='sensor_type']" position="after">
<field name="business_category_id" readonly="1"/>
</xpath>
</field>
</record>
<record id="view_scada_sensor_reading_search_business_category" model="ir.ui.view">
<field name="name">scada.sensor.reading.search.business.category</field>
<field name="model">scada.sensor.reading</field>
<field name="inherit_id" ref="grt_scada.view_scada_sensor_reading_search"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='sensor_name']" position="after">
<field name="business_category_id"/>
</xpath>
<xpath expr="//group[@string='Group By']" position="inside">
<filter string="Business Category" name="group_by_business_category" context="{'group_by': 'business_category_id'}"/>
</xpath>
</field>
</record>
<record id="view_scada_api_log_list_business_category" model="ir.ui.view">
<field name="name">scada.api.log.list.business.category</field>
<field name="model">scada.api.log</field>
<field name="inherit_id" ref="grt_scada.view_scada_api_log_list"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='equipment_id']" position="after">
<field name="business_category_id" optional="show"/>
</xpath>
</field>
</record>
<record id="view_scada_api_log_form_business_category" model="ir.ui.view">
<field name="name">scada.api.log.form.business.category</field>
<field name="model">scada.api.log</field>
<field name="inherit_id" ref="grt_scada.view_scada_api_log_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='equipment_id']" position="after">
<field name="business_category_id" readonly="1"/>
</xpath>
</field>
</record>
<record id="view_scada_api_log_search_business_category" model="ir.ui.view">
<field name="name">scada.api.log.search.business.category</field>
<field name="model">scada.api.log</field>
<field name="inherit_id" ref="grt_scada.view_scada_api_log_search"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='request_id']" position="after">
<field name="business_category_id"/>
</xpath>
<xpath expr="//group[@string='Group By']" position="inside">
<filter string="Business Category" name="group_by_business_category" context="{'group_by': 'business_category_id'}"/>
</xpath>
</field>
</record>
<record id="view_scada_equipment_failure_tree_business_category" model="ir.ui.view">
<field name="name">scada.equipment.failure.tree.business.category</field>
<field name="model">scada.equipment.failure</field>
<field name="inherit_id" ref="grt_scada.view_scada_equipment_failure_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='equipment_code']" position="after">
<field name="business_category_id" optional="show"/>
</xpath>
</field>
</record>
<record id="view_scada_equipment_failure_form_business_category" model="ir.ui.view">
<field name="name">scada.equipment.failure.form.business.category</field>
<field name="model">scada.equipment.failure</field>
<field name="inherit_id" ref="grt_scada.view_scada_equipment_failure_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='reported_by']" position="after">
<field name="business_category_id" readonly="1"/>
</xpath>
</field>
</record>
<record id="view_scada_equipment_failure_search_business_category" model="ir.ui.view">
<field name="name">scada.equipment.failure.search.business.category</field>
<field name="model">scada.equipment.failure</field>
<field name="inherit_id" ref="grt_scada.view_scada_equipment_failure_search"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='date']" position="after">
<field name="business_category_id"/>
</xpath>
<xpath expr="//search" position="inside">
<filter name="group_by_business_category" string="Business Category" context="{'group_by': 'business_category_id'}"/>
</xpath>
</field>
</record>
</data>
</odoo>