diff --git a/.gitignore b/.gitignore
index 16c8681..118203d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,4 @@
.env
config/odoo.conf
-addons/
*.log
.DS_Store
diff --git a/addons/kosbar_erp_ext/__init__.py b/addons/kosbar_erp_ext/__init__.py
new file mode 100644
index 0000000..0650744
--- /dev/null
+++ b/addons/kosbar_erp_ext/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/addons/kosbar_erp_ext/__manifest__.py b/addons/kosbar_erp_ext/__manifest__.py
new file mode 100644
index 0000000..b9b1672
--- /dev/null
+++ b/addons/kosbar_erp_ext/__manifest__.py
@@ -0,0 +1,15 @@
+{
+ 'name': 'Kos Barokah ERP Extension',
+ 'version': '1.0',
+ 'category': 'Sales',
+ 'summary': 'Grup akses, Custom Sales/DO Sequence',
+ 'depends': ['sale_management', 'sales_team', 'stock', 'purchase', 'crm'],
+ 'data': [
+ 'security/res_groups.xml',
+ 'security/ir.model.access.csv',
+ 'data/ir_sequence.xml',
+ 'data/stock_picking_type.xml',
+ ],
+ 'installable': True,
+ 'application': False,
+}
\ No newline at end of file
diff --git a/addons/kosbar_erp_ext/data/ir_sequence.xml b/addons/kosbar_erp_ext/data/ir_sequence.xml
new file mode 100644
index 0000000..af326ad
--- /dev/null
+++ b/addons/kosbar_erp_ext/data/ir_sequence.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+ Quotation Kos Barokah
+ sale.quotation.custom
+ 5
+ SQ/%(year)s/
+
+
+
+
+
+ Sales Order Kos Barokah
+ sale.order.custom
+ 5
+ SO/%(year)s/
+
+
+
+
+
+ Delivery Order Kos Barokah
+ stock.picking.do
+ 5
+ DO/%(year)s/
+
+
+
+
diff --git a/addons/kosbar_erp_ext/data/stock_picking_type.xml b/addons/kosbar_erp_ext/data/stock_picking_type.xml
new file mode 100644
index 0000000..75e06df
--- /dev/null
+++ b/addons/kosbar_erp_ext/data/stock_picking_type.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/addons/kosbar_erp_ext/models/__init__.py b/addons/kosbar_erp_ext/models/__init__.py
new file mode 100644
index 0000000..91e7030
--- /dev/null
+++ b/addons/kosbar_erp_ext/models/__init__.py
@@ -0,0 +1,4 @@
+from . import sale_order
+from . import stock_picking
+from . import product_category
+from . import product_template
diff --git a/addons/kosbar_erp_ext/models/product_category.py b/addons/kosbar_erp_ext/models/product_category.py
new file mode 100644
index 0000000..862407e
--- /dev/null
+++ b/addons/kosbar_erp_ext/models/product_category.py
@@ -0,0 +1,19 @@
+from odoo import models, fields, api
+
+
+class ProductCategory(models.Model):
+ _inherit = 'product.category'
+
+ code = fields.Char(
+ string='Category Code',
+ size=10,
+ help='Kode 2-digit otomatis dari ID kategori (contoh ID 1 -> "01")',
+ )
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ records = super().create(vals_list)
+ for r in records:
+ if not r.code:
+ r.code = f'{r.id:02d}'
+ return records
diff --git a/addons/kosbar_erp_ext/models/product_template.py b/addons/kosbar_erp_ext/models/product_template.py
new file mode 100644
index 0000000..40e51d3
--- /dev/null
+++ b/addons/kosbar_erp_ext/models/product_template.py
@@ -0,0 +1,56 @@
+import re
+
+from odoo import models, fields, api
+from odoo.exceptions import ValidationError
+
+
+class ProductTemplate(models.Model):
+ _inherit = 'product.template'
+
+ item_code = fields.Char(
+ string='Item Code',
+ index=True,
+ help='Kombinasi: parent_kode + kategori_kode + id_produk(4digit) + nama_produk_tanpa_spasi(lower)',
+ )
+
+ @api.constrains('item_code')
+ def _check_item_code_unique(self):
+ for rec in self:
+ if not rec.item_code:
+ continue
+ dup = self.search([('item_code', '=', rec.item_code), ('id', '!=', rec.id)], limit=1)
+ if dup:
+ raise ValidationError(
+ f'Item Code "{rec.item_code}" sudah dipakai oleh produk lain '
+ f'({dup.name}). Item Code harus unik.'
+ )
+
+ def _generate_item_code(self):
+ """Generate item_code: parent_code + cat_code + id:04d + name_slug"""
+ for p in self:
+ cat = p.categ_id
+ parent_code = ''
+ cat_code = '00'
+
+ if cat:
+ # Pastikan kategori punya code (calculate jika belum ada)
+ if not cat.code:
+ cat.code = f'{cat.id:02d}'
+ parent_code = cat.parent_id.code if cat.parent_id else ''
+ cat_code = cat.code
+
+ # slug: lowercase, remove semua spasi
+ name_slug = re.sub(r'\s+', '', (p.name or '').lower())
+ p.item_code = f'{parent_code}{cat_code}{p.id:04d}{name_slug}'
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ records = super().create(vals_list)
+ records._generate_item_code()
+ return records
+
+ def write(self, vals):
+ res = super().write(vals)
+ if 'name' in vals or 'categ_id' in vals:
+ self._generate_item_code()
+ return res
\ No newline at end of file
diff --git a/addons/kosbar_erp_ext/models/sale_order.py b/addons/kosbar_erp_ext/models/sale_order.py
new file mode 100644
index 0000000..f27ed53
--- /dev/null
+++ b/addons/kosbar_erp_ext/models/sale_order.py
@@ -0,0 +1,29 @@
+from odoo import models, fields, api
+
+class SaleOrder(models.Model):
+ _inherit = 'sale.order'
+
+ def _get_quotation_sequence_id(self):
+ return self.env.ref('kosbar_erp_ext.seq_quotation').sudo()
+
+ def _get_sales_order_sequence_id(self):
+ return self.env.ref('kosbar_erp_ext.seq_sales_order').sudo()
+
+ def _get_delivery_sequence_id(self):
+ return self.env.ref('kosbar_erp_ext.seq_delivery_order').sudo()
+
+ def action_confirm(self):
+ """Override confirmation to change number from SQ to SO."""
+ for order in self:
+ # Quotation (draft) should have SQ prefix; confirm -> SO
+ seq = self._get_sales_order_sequence_id()
+ order.name = seq.with_date(order.date_order or fields.Date.context_today(self)).next_by_code()
+ return super().action_confirm()
+
+ @api.model
+ def create(self, vals):
+ """When creating a quotation (draft), use SQ sequence."""
+ if vals.get('state') == 'draft' and not vals.get('name'):
+ seq = self._get_quotation_sequence_id()
+ vals['name'] = seq.with_date(vals.get('date_order') or fields.Date.context_today(self)).next_by_code()
+ return super().create(vals)
\ No newline at end of file
diff --git a/addons/kosbar_erp_ext/models/stock_picking.py b/addons/kosbar_erp_ext/models/stock_picking.py
new file mode 100644
index 0000000..c725cd5
--- /dev/null
+++ b/addons/kosbar_erp_ext/models/stock_picking.py
@@ -0,0 +1,10 @@
+from odoo import models, api, fields
+
+class StockPicking(models.Model):
+ _inherit = 'stock.picking'
+
+ @api.model
+ def create(self, vals):
+ """Delivery order (outgoing) memakai sequence DO/2026/00001 dari
+ picking type sequence_id yang sudah diarahkan ke seq_delivery_order."""
+ return super().create(vals)
diff --git a/addons/kosbar_erp_ext/security/ir.model.access.csv b/addons/kosbar_erp_ext/security/ir.model.access.csv
new file mode 100644
index 0000000..97dd8b9
--- /dev/null
+++ b/addons/kosbar_erp_ext/security/ir.model.access.csv
@@ -0,0 +1 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
diff --git a/addons/kosbar_erp_ext/security/res_groups.xml b/addons/kosbar_erp_ext/security/res_groups.xml
new file mode 100644
index 0000000..4f2e34e
--- /dev/null
+++ b/addons/kosbar_erp_ext/security/res_groups.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ Sales Manager
+
+
+
+
+
+ CFO / Finance Director
+
+
+
+
diff --git a/scripts/migrate_item_code.py b/scripts/migrate_item_code.py
new file mode 100644
index 0000000..7cca60b
--- /dev/null
+++ b/scripts/migrate_item_code.py
@@ -0,0 +1,19 @@
+import re
+
+# Migration: isi code kategori & item_code produk untuk data existing
+cats = env['product.category'].search([])
+for c in cats:
+ if not c.code:
+ c.code = f'{c.id:02d}'
+env.cr.commit()
+print(f'Kategori di-update: {len(cats)}')
+
+prods = env['product.template'].search([])
+for p in prods:
+ p._generate_item_code()
+env.cr.commit()
+print(f'Produk di-update: {len(prods)}')
+
+# Verifikasi contoh
+for p in prods[:5]:
+ print(f' {p.name!r} -> {p.item_code!r}')