Pembuatan mrp business category dan scada business category
This commit is contained in:
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from .hooks import post_init_hook
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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 +1,2 @@
|
||||
from .hooks import post_init_hook
|
||||
from . import wizard
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
+6
@@ -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"
|
||||
}
|
||||
+6
@@ -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"
|
||||
}
|
||||
+6
@@ -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
|
||||
|
@@ -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>
|
||||
Reference in New Issue
Block a user