summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xindoteknik_custom/__manifest__.py1
-rwxr-xr-xindoteknik_custom/models/__init__.py1
-rw-r--r--indoteknik_custom/models/sourcing_job_order.py598
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv2
-rw-r--r--indoteknik_custom/views/ir_sequence.xml8
-rw-r--r--indoteknik_custom/views/sourcing.xml250
6 files changed, 860 insertions, 0 deletions
diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py
index 7a179ce3..961e6a94 100755
--- a/indoteknik_custom/__manifest__.py
+++ b/indoteknik_custom/__manifest__.py
@@ -191,6 +191,7 @@
'views/sj_tele.xml',
'views/close_tempo_mail_template.xml',
'views/domain_apo.xml',
+ 'views/sourcing.xml'
],
'demo': [],
'css': [],
diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py
index 165dae4e..7f946b57 100755
--- a/indoteknik_custom/models/__init__.py
+++ b/indoteknik_custom/models/__init__.py
@@ -163,3 +163,4 @@ from . import letter_receivable
from . import sj_tele
from . import partial_delivery
from . import domain_apo
+from . import sourcing_job_order
diff --git a/indoteknik_custom/models/sourcing_job_order.py b/indoteknik_custom/models/sourcing_job_order.py
new file mode 100644
index 00000000..98f356de
--- /dev/null
+++ b/indoteknik_custom/models/sourcing_job_order.py
@@ -0,0 +1,598 @@
+from odoo import models, fields, api, _
+from odoo.exceptions import UserError
+import logging
+
+_logger = logging.getLogger(__name__)
+
+
+class SourcingJobOrder(models.Model):
+ _name = 'sourcing.job.order'
+ _description = 'Sourcing Job Order MD'
+ _rec_name = 'name'
+ _inherit = ['mail.thread', 'mail.activity.mixin']
+
+ name = fields.Char(string='Job Number', default='New', copy=False, readonly=True)
+ leads_id = fields.Many2one('crm.lead', string='Leads Number')
+ user_id = fields.Many2one('res.users', string='MD Person', tracking=True)
+ state = fields.Selection([
+ ('draft', 'Untaken'),
+ ('taken', 'On Sourcing'),
+ ('done', 'Complete'),
+ ('cancel', 'Cancelled')
+ ], string='Status', default='draft', tracking=True)
+ approval_sales = fields.Selection([
+ ('draft', 'Requested'),
+ ('approve', 'Approved'),
+ ], string='Approval Sales', tracking=True)
+ takeover_request = fields.Many2one(
+ 'res.users',
+ string='Takeover Requested By',
+ readonly=True,
+ tracking=True,
+ help='MD yang meminta takeover'
+ )
+ can_request_takeover = fields.Boolean(
+ compute="_compute_can_request_takeover"
+ )
+ can_approve_takeover = fields.Boolean(
+ compute="_compute_can_approve_takeover"
+ )
+
+ eta_sales = fields.Date(string='Expected Ready')
+ sla_product = fields.Date(string='Product Ready Date')
+ cancel_reason = fields.Text(string="Reason for Cancel", tracking=True)
+ product_assets = fields.Binary(string="Product Assets (PDF)")
+
+ line_ids = fields.One2many('sourcing.job.order.line', 'order_id', string='Products')
+
+ total_amount = fields.Float(string="Total Purchase", compute='_compute_total_amount')
+
+ line_sales_input_ids = fields.One2many(
+ 'sourcing.job.order.line', 'order_id',
+ string='Sales Input Lines',
+ domain=['|', ('price', '=', 0), ('price', '=', False)]
+ )
+ line_md_edit_ids = fields.One2many(
+ 'sourcing.job.order.line', 'order_id',
+ string='MD Edit Lines'
+ )
+ line_sales_view_ids = fields.One2many(
+ 'sourcing.job.order.line', 'order_id',
+ string='Sales View Lines',
+ domain=[('state', 'in', ['sourcing', 'done', 'cancel'])]
+ )
+ line_sales_view_cancel_ids = fields.One2many(
+ 'sourcing.job.order.line', 'order_id',
+ string='Sales View Lines',
+ domain=[('state', '=', 'cancel')]
+ )
+
+ has_price_in_lines = fields.Boolean(
+ string='Has Line with Price',
+ compute='_compute_has_price_in_lines',
+ )
+ is_creator_same_user = fields.Boolean(compute='_compute_is_creator_same_user')
+ can_convert_to_product = fields.Boolean(string="Can Convert", compute="_compute_can_convert_to_product")
+
+
+ @api.depends('line_ids.subtotal')
+ def _compute_total_amount(self):
+ for rec in self:
+ rec.total_amount = sum(line.subtotal for line in rec.line_ids)
+
+ @api.depends('line_ids.price', 'line_ids.vendor_id')
+ def _compute_has_price_in_lines(self):
+ for rec in self:
+ # Cek apakah ada minimal satu line yang sudah punya price > 0 dan vendor_id
+ has_price = any(
+ (line.price and line.price > 0 and line.vendor_id)
+ for line in rec.line_ids
+ )
+ rec.has_price_in_lines = bool(has_price)
+
+ @api.depends('user_id', 'takeover_request')
+ def _compute_can_request_takeover(self):
+ for rec in self:
+ current_user = self.env.user
+ rec.can_request_takeover = (
+ rec.user_id and rec.user_id != current_user
+ )
+
+ @api.depends('user_id', 'takeover_request')
+ def _compute_can_approve_takeover(self):
+ for rec in self:
+ current_user = self.env.user
+ rec.can_approve_takeover = (
+ rec.user_id == current_user and bool(rec.takeover_request)
+ )
+
+ @api.depends('create_uid', 'user_id')
+ def _compute_is_creator_same_user(self):
+ for rec in self:
+ current_user = self.env.user
+ rec.is_creator_same_user = (
+ rec.create_uid == current_user and
+ rec.user_id == current_user
+ )
+
+ @api.depends('line_md_edit_ids.state')
+ def _compute_can_convert_to_product(self):
+ """Cek apakah ada line dengan state 'done'."""
+ for rec in self:
+ rec.can_convert_to_product = any(line.state == 'done' for line in rec.line_md_edit_ids)
+
+ @api.model
+ def create(self, vals):
+ """Hanya Sales & Merchandiser yang boleh membuat job."""
+ if not (self.env.user.has_group('indoteknik_custom.group_role_sales') or
+ self.env.user.has_group('indoteknik_custom.group_role_merchandiser')):
+ raise UserError("❌ Hanya Sales dan Merchandiser yang boleh membuat Sourcing Job.")
+
+ if vals.get('name', 'New') == 'New':
+ vals['name'] = self.env['ir.sequence'].next_by_code('sourcing.job.order') or 'New'
+
+ if self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
+ vals['user_id'] = self.env.user.id
+ vals['state'] = 'taken'
+
+ rec = super().create(vals)
+ return rec
+
+ def write(self, vals):
+ bypass_actions = (
+ self.env.context.get('from_action_take', False)
+ or self.env.context.get('from_multi_action_take', False)
+ or self.env.context.get('from_action_takeover', False)
+ )
+
+ if not (
+ self.env.user.has_group('indoteknik_custom.group_role_sales')
+ or self.env.user.has_group('indoteknik_custom.group_role_merchandiser')
+ ):
+ raise UserError("❌ Hanya Sales dan Merchandiser yang boleh mengedit Sourcing Job.")
+
+ for rec in self:
+ if (
+ not rec.user_id
+ and rec.create_uid != self.env.user
+ and not vals.get('user_id')
+ and not bypass_actions
+ ):
+ raise UserError("❌ SJO ini belum memiliki MD Person. Tidak dapat melakukan edit.")
+
+ if (
+ rec.user_id != self.env.user
+ and rec.create_uid != self.env.user
+ and not bypass_actions
+ ):
+ raise UserError("❌ Hanya MD Person dan Creator SJO ini yang bisa melakukan Edit.")
+
+ # --- Simpan data lama sebelum write (buat pembanding)
+ old_data = {}
+ for rec in self:
+ old_data[rec.id] = {
+ 'state': rec.state,
+ 'user_id': rec.user_id.id if rec.user_id else False,
+ 'approval_sales': rec.approval_sales,
+ 'line_data': {
+ line.id: {
+ 'state': line.state,
+ 'vendor_id': line.vendor_id.id if line.vendor_id else False,
+ 'price': line.price,
+ }
+ for line in rec.line_md_edit_ids
+ },
+ }
+
+ res = super().write(vals)
+
+ # --- Bandingkan setelah write dan buat log
+ for rec in self:
+ changes = []
+ old = old_data.get(rec.id, {})
+
+ # === Perubahan di field parent ===
+ if old.get('state') != rec.state:
+ changes.append(f"State: <b>{old.get('state')}</b> → <b>{rec.state}</b>")
+ if old.get('user_id') != (rec.user_id.id if rec.user_id else False):
+ changes.append(f"MD Person: <b>{old.get('user_id')}</b> → <b>{rec.user_id.name if rec.user_id else '-'}</b>")
+ if old.get('approval_sales') != rec.approval_sales:
+ changes.append(f"Approval Status: <b>{old.get('approval_sales')}</b> → <b>{rec.approval_sales}</b>")
+
+ # === Perubahan di line ===
+ old_lines = old.get('line_data', {})
+ for line in rec.line_md_edit_ids:
+ old_line = old_lines.get(line.id)
+ if not old_line:
+ continue # line baru, skip validasi perubahan
+
+ # 💥 Validasi vendor berubah tapi price tidak berubah
+ if (
+ old_line['vendor_id'] != (line.vendor_id.id if line.vendor_id else False)
+ and old_line['price'] == line.price
+ ):
+ raise UserError(
+ f"⚠️ Harga untuk produk {line.product_name} belum diperbarui setelah mengganti Vendor."
+ )
+
+ # Catat perubahan state, vendor, atau harga untuk log note
+ sub_changes = []
+
+ if old_line['state'] != line.state:
+ sub_changes.append(f"- state: <b>{old_line['state']}</b> → <b>{line.state}</b>")
+
+ if old_line['vendor_id'] != (line.vendor_id.id if line.vendor_id else False):
+ old_vendor = self.env['res.partner'].browse(old_line['vendor_id']).name if old_line['vendor_id'] else '-'
+ sub_changes.append(f"- vendor: <b>{old_vendor}</b> → <b>{line.vendor_id.name if line.vendor_id else '-'}</b>")
+
+ if old_line['price'] != line.price:
+ sub_changes.append(f"- price: <b>{old_line['price']}</b> → <b>{line.price}</b>")
+
+ if sub_changes:
+ joined = "<br/>".join(sub_changes)
+ changes.append(f"<b>{line.product_name}</b>:<br/>{joined}")
+
+ # Post ke chatter
+ if changes:
+ message = "<br/><br/>".join(changes)
+ rec.message_post(
+ body=f"Perubahan pada Sourcing Job:<br/>{message}",
+ subtype_xmlid="mail.mt_comment",
+ )
+
+ return res
+
+
+ def action_take(self):
+ context_action = self.env.context.get('from_action_take', False)
+ for rec in self:
+ if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
+ raise UserError("❌ Hanya Merchandiser yang dapat mengambil Sourcing Job.")
+ if rec.state != 'draft':
+ continue
+ rec.with_context(from_action_take=True).write({
+ 'state': 'taken',
+ 'user_id': self.env.user.id
+ })
+ rec.message_post(body=("Job <b>%s</b> diambil oleh %s") % (rec.name, self.env.user.name))
+
+ def action_multi_take(self):
+ context_action = self.env.context.get('from_multi_action_take', True)
+ untaken = self.filtered(lambda r: r.state == 'draft')
+ if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
+ raise UserError("❌ Hanya Merchandiser yang bisa mengambil Sourcing Job.")
+ if not untaken:
+ raise UserError("Tidak ada record Untaken untuk diambil.")
+
+ untaken.write({
+ 'state': 'taken',
+ 'user_id': self.env.user.id,
+ })
+
+ def action_confirm_by_md(self):
+ for rec in self:
+ if rec.user_id and rec.user_id != self.env.user:
+ raise UserError("❌ Hanya MD Person yang memiliki SJO ini yang boleh melakukan Confirm.")
+
+ invalid_lines = rec.line_md_edit_ids.filtered(lambda l: l.state not in ('done', 'cancel'))
+ if invalid_lines:
+ line_names = ', '.join(invalid_lines.mapped('product_name'))
+ raise UserError(
+ f"⚠️ Tidak dapat melakukan Confirm SJO.\n"
+ f"Masih ada line yang belum selesai disourcing: {line_names}"
+ )
+ if rec.line_md_edit_ids and all(line.state == 'cancel' for line in rec.line_md_edit_ids):
+ raise UserError("⚠️ Tidak dapat melakukan Confirm SJO. Semua line pada SJO ini Unavailable.")
+
+ rec.approval_sales = 'approve'
+ rec.state = 'done'
+
+ rec.message_post(
+ body=f"Sourcing Job <b>{rec.name}</b> otomatis disetujui karena pembuat dan MD adalah orang yang sama (<b>{self.env.user.name}</b>).",
+ subtype_xmlid="mail.mt_comment"
+ )
+
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'titl': 'Auto Approved',
+ 'message': f"Sourcing Job '{rec.name}' otomatis disetujui dan diselesaikan.",
+ 'type': 'success',
+ 'sticky': False,
+ }
+ }
+
+ def action_cancel(self):
+ for rec in self:
+ if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
+ raise UserError("❌ Hanya Merchandiser yang dapat mengcancel Sourcing Job.")
+
+ if rec.user_id and rec.user_id != self.env.user:
+ raise UserError("❌ Hanya MD Person yang memiliki SJO ini yang boleh melakukan Cancel.")
+
+ if not rec.cancel_reason:
+ raise UserError("⚠️ Isi alasan pembatalan terlebih dahulu.")
+
+ rec.write({'state': 'cancel'})
+ rec.message_post(body=("Job <b>%s</b> dibatalkan oleh %s<br/>Alasan: %s") %
+ (rec.name, self.env.user.name, rec.cancel_reason))
+
+ def action_request_takeover(self):
+ context_action = self.env.context.get('from_action_takeover', True)
+ for rec in self:
+ if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
+ raise UserError("❌ Hanya Merchandiser yang dapat Request Takeover Sourcing Job.")
+
+ if rec.takeover_request:
+ raise UserError(f"SJO ini sudah memiliki request takeover dari {rec.takeover_request.name}. Tunggu approval dulu.")
+
+ rec.takeover_request = self.env.user
+
+ activity_type = self.env.ref('mail.mail_activity_data_todo')
+ rec.activity_schedule(
+ activity_type_id=activity_type.id,
+ user_id=rec.user_id.id,
+ note=f"{self.env.user.name} meminta approval untuk mengambil alih SJO '{rec.name}'.",
+ )
+
+ rec.message_post(
+ body=f"<b>{self.env.user.name}</b> mengirimkan request takeover kepada <b>{rec.user_id.name}</b>.",
+ subtype_xmlid="mail.mt_comment"
+ )
+
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': 'Request Sent',
+ 'message': f"Request takeover telah dikirim ke {rec.user_id.name}.",
+ 'type': 'success',
+ 'sticky': False,
+ }
+ }
+
+ def action_approve_takeover(self):
+ for rec in self:
+ if self.env.user != rec.user_id:
+ raise UserError("Hanya MD person yang saat ini memegang SJO yang dapat menyetujui takeover ini.")
+ if not rec.takeover_request:
+ raise UserError("Tidak ada request takeover yang perlu disetujui.")
+
+ new_user = rec.takeover_request
+
+ rec.user_id = new_user
+ rec.takeover_request = False
+
+ activities = self.env['mail.activity'].search([
+ ('res_id', '=', rec.id),
+ ('res_model', '=', 'sourcing.job.order'),
+ ('user_id', '=', self.env.user.id)
+ ])
+ activities.unlink()
+
+ rec.message_post(
+ body=f"Takeover disetujui oleh <b>{self.env.user.name}</b>. Sourcing Job berpindah ke <b>{new_user.name}</b>.",
+ subtype_xmlid="mail.mt_comment"
+ )
+
+ def action_reject_takeover(self):
+ for rec in self:
+ if self.env.user != rec.user_id:
+ raise UserError("Hanya MD person yang saat ini memegang SJO yang dapat menolak takeover ini.")
+ if not rec.takeover_request:
+ raise UserError("Tidak ada request takeover yang perlu ditolak.")
+
+ requester = rec.takeover_request
+ rec.takeover_request = False
+
+ activities = self.env['mail.activity'].search([
+ ('res_id', '=', rec.id),
+ ('res_model', '=', 'sourcing.job.order'),
+ ('user_id', '=', self.env.user.id)
+ ])
+ activities.unlink()
+
+ rec.message_post(
+ body=f"Takeover dari <b>{requester.name}</b> ditolak oleh <b>{self.env.user.name}</b>.",
+ subtype_xmlid="mail.mt_comment"
+ )
+
+ def action_ask_approval(self):
+ for rec in self:
+ if rec.user_id != self.env.user:
+ raise UserError("❌ Hanya MD Person Sourcing Job ini yang dapat Request Approval.")
+ invalid_lines = rec.line_md_edit_ids.filtered(lambda l: l.state not in ('done', 'cancel'))
+ if invalid_lines:
+ line_names = ', '.join(invalid_lines.mapped('product_name'))
+ raise UserError(
+ f"⚠️ Tidak dapat melakukan Request Approval.\n"
+ f"Masih ada line yang belum selesai disourcing: {line_names}"
+ )
+ if rec.line_md_edit_ids and all(line.state == 'cancel' for line in rec.line_md_edit_ids):
+ raise UserError("⚠️ Tidak dapat melakukan Request Approval. Semua line pada SJO ini Unavailable.")
+
+ if rec.create_uid == rec.user_id:
+ rec.approval_sales = 'approve'
+ rec.state = 'done'
+
+ rec.message_post(
+ body=f"Sourcing Job <b>{rec.name}</b> otomatis disetujui karena pembuat dan MD adalah orang yang sama (<b>{self.env.user.name}</b>).",
+ subtype_xmlid="mail.mt_comment"
+ )
+
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': 'Auto Approved',
+ 'message': f"Sourcing Job '{rec.name}' otomatis disetujui dan diselesaikan.",
+ 'type': 'success',
+ 'sticky': False,
+ }
+ }
+
+ rec.approval_sales = 'draft'
+
+ activity_type = self.env.ref('mail.mail_activity_data_todo')
+ rec.activity_schedule(
+ activity_type_id=activity_type.id,
+ user_id=rec.create_uid.id,
+ note=f"{self.env.user.name} meminta approval untuk SJO '{rec.name}'.",
+ )
+
+ rec.message_post(
+ body=f"<b>{self.env.user.name}</b> mengirimkan request approval kepada <b>{rec.create_uid.name}</b>.",
+ subtype_xmlid="mail.mt_comment"
+ )
+
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': 'Request Sent',
+ 'message': f"Request Approval telah dikirim ke {rec.create_uid.name}.",
+ 'type': 'success',
+ 'sticky': False,
+ }
+ }
+
+ def action_confirm_approval(self):
+ for rec in self:
+ if rec.create_uid != self.env.user:
+ raise UserError("❌ Hanya Pembuat Sourcing Job ini yang dapat Confirm Approval.")
+
+ rec.approval_sales = 'approve'
+ rec.state = 'done'
+
+ rec.activity_feedback(['mail.mail_activity_data_todo'])
+
+ rec.message_post(
+ body=f"Sourcing Job disetujui oleh <b>{self.env.user.name}</b>.",
+ subtype_xmlid="mail.mt_comment"
+ )
+
+ rec.activity_schedule(
+ 'mail.mail_activity_data_todo',
+ user_id=rec.user_id.id,
+ note=f"✅ Sourcing Job <b>{rec.name}</b> telah disetujui oleh {self.env.user.name}.",
+ )
+
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': 'Approval Confirmed',
+ 'message': f"Sourcing Job '{rec.name}' telah disetujui dan dikirim ke {rec.user_id.name}.",
+ 'type': 'success',
+ 'sticky': False,
+ }
+ }
+
+
+class SourcingJobOrderLine(models.Model):
+ _name = 'sourcing.job.order.line'
+ _description = 'Sourcing Job Order Line'
+
+ order_id = fields.Many2one('sourcing.job.order', string='Job Order', ondelete='cascade')
+ product_name = fields.Char(string='Nama Barang', required=True)
+ code = fields.Char(string='SKU')
+ descriptions = fields.Text(string='Deskripsi / Spesifikasi')
+ reason = fields.Text(string='Reason Unavailable')
+ sla = fields.Char(string='SLA Product')
+ quantity = fields.Float(string='Quantity Product', required=True)
+ price = fields.Float(string='Purchase Price')
+ tax_id = fields.Many2one('account.tax', string='Tax', domain=[('active', '=', True)])
+ vendor_id = fields.Many2one('res.partner', string="Vendor")
+ product_category = fields.Many2one('product.category', string="Product Category")
+ product_class = fields.Many2one('product.public.category', string="Categories")
+ state = fields.Selection([
+ ('draft', 'Unsource'),
+ ('sourcing', 'On Sourcing'),
+ ('done', 'Done Sourcing'),
+ ('convert', 'Converted'),
+ ('cancel', 'Unavailable')
+ ], default='draft', tracking=True)
+ product_type = fields.Selection([
+ ('consu', 'Consumable'),
+ ('servis', 'Service'),
+ ('product', 'Storable Product'),
+ ], default='product')
+ subtotal = fields.Float(string='Subtotal', compute='_compute_subtotal')
+ show_for_sales = fields.Boolean(
+ string="Show for Sales",
+ compute="_compute_show_for_sales",
+ )
+
+ @api.depends('quantity', 'price', 'tax_id')
+ def _compute_subtotal(self):
+ """Menghitung subtotal termasuk pajak."""
+ for line in self:
+ subtotal = (line.quantity or 0.0) * (line.price or 0.0)
+ if line.tax_id:
+ subtotal += subtotal * (line.tax_id.amount / 100)
+ line.subtotal = subtotal
+
+ @api.constrains('product_type', 'product_category', 'product_class', 'code')
+ def _check_required_fields_for_md(self):
+ for rec in self:
+ is_md = self.env.user.has_group('indoteknik_custom.group_role_merchandiser')
+ if is_md and (not rec.product_type or not rec.product_category or not rec.product_class or not rec.code):
+ raise UserError("MD wajib mengisi SKU, Product Type, Product Category, dan Categories!")
+
+ @api.depends('state')
+ def _check_unavailable_line(self):
+ for rec in self:
+ if rec.state == 'cancel' and not rec.reason:
+ raise UserError("Isi Reason Unavailable")
+
+ @api.depends('price', 'vendor_id', 'order_id')
+ def _compute_show_for_sales(self):
+ for rec in self:
+ rec.show_for_sales = bool(
+ rec.order_id and rec.price not in (None, 0) and rec.vendor_id
+ )
+
+ def action_convert_to_product(self):
+ type_map = {
+ 'servis': 'service',
+ 'product': 'product',
+ 'consu': 'consu',
+ }
+
+ for rec in self:
+ if rec.order_id.user_id != self.env.user:
+ raise UserError("❌ Hanya MD Person SJO ini yang dapat convert Line ini ke product.")
+ exsisting = self.env['product.product'].search([('default_code', '=', rec.code)], limit=1)
+
+ if exsisting:
+ raise UserError(f"⚠️ Produk dengan Internal Reference '{rec.code}' sudah ada di sistem.")
+
+ product = self.env['product.product'].create({
+ 'name': rec.product_name,
+ 'default_code': rec.code or False,
+ 'description': rec.descriptions or '',
+ 'categ_id': rec.product_category.id,
+ 'type': type_map.get(rec.product_type, 'product'),
+ })
+
+ rec.state = 'convert'
+ return True
+
+ @api.onchange('code')
+ def _oncange_code(self):
+ for rec in self:
+ if not rec.code:
+ continue
+
+ product = self.env['product.product'].search([('default_code', '=', rec.code)], limit=1)
+ if not product:
+ return
+
+ rec.product_name = product.name or rec.product_name
+
+ pricelist = self.env['purchase.pricelist'].search([('product_id', '=', product.id), ('is_winner', '=', True)], limit=1)
+ if pricelist or tax or vendor:
+ rec.vendor_id = pricelist.vendor_id.id or False
+ rec.price = pricelist.include_price or 0.0
+ rec.tax_id = pricelist.taxes_product_id.id or pricelist.taxes_system_id.id or False \ No newline at end of file
diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv
index c01271d3..bbcbac84 100755
--- a/indoteknik_custom/security/ir.model.access.csv
+++ b/indoteknik_custom/security/ir.model.access.csv
@@ -210,3 +210,5 @@ access_unpaid_invoice_view,access.unpaid.invoice.view,model_unpaid_invoice_view,
access_surat_piutang_user,surat.piutang user,model_surat_piutang,,1,1,1,1
access_surat_piutang_line_user,surat.piutang.line user,model_surat_piutang_line,,1,1,1,1
access_sj_tele,access.sj.tele,model_sj_tele,base.group_system,1,1,1,1
+access_sourcing_job_order,access.sourcing_job_order,model_sourcing_job_order,base.group_system,1,1,1,1
+access_sourcing_job_order_line_user,sourcing.job.order.line,model_sourcing_job_order_line,base.group_user,1,1,1,1
diff --git a/indoteknik_custom/views/ir_sequence.xml b/indoteknik_custom/views/ir_sequence.xml
index dbdbc3c0..543f1fd1 100644
--- a/indoteknik_custom/views/ir_sequence.xml
+++ b/indoteknik_custom/views/ir_sequence.xml
@@ -218,6 +218,14 @@
<field name="number_next">1</field>
<field name="number_increment">1</field>
</record>
+ <record id="seq_sourcing_job_order" model="ir.sequence">
+ <field name="name">Sourcing Job Order</field>
+ <field name="code">sourcing.job.order</field>
+ <field name="prefix">SJO/%(year)s/</field>
+ <field name="padding">4</field>
+ <field name="number_next_actual">1</field>
+ <field name="number_increment">1</field>
+ </record>
<record id="sequence_advance_payment_request" model="ir.sequence">
<field name="name">Advance Payment Request Sequence</field>
diff --git a/indoteknik_custom/views/sourcing.xml b/indoteknik_custom/views/sourcing.xml
new file mode 100644
index 00000000..3965c62f
--- /dev/null
+++ b/indoteknik_custom/views/sourcing.xml
@@ -0,0 +1,250 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+ <record id="view_sourcing_job_order_search" model="ir.ui.view">
+ <field name="name">sourcing.job.order.search</field>
+ <field name="model">sourcing.job.order</field>
+ <field name="arch" type="xml">
+ <search string="Search Sourcing Job Order">
+ <field name="state" string="Name"/>
+ <field name="user_id" string="MD Person"/>
+ <filter name="my_job"
+ string="My Sourcing Job"
+ domain="[('user_id', '=', uid), ('state', '=', 'taken')]"/>
+ <filter name="untaken"
+ string="Untaken"
+ domain="[('state', '=', 'draft')]" />
+ <filter name="done"
+ string="Complete"
+ domain="[('state', '=', 'done')]" />
+ </search>
+ </field>
+ </record>
+ <record id="view_sourcing_job_order_tree" model="ir.ui.view">
+ <field name="name">sourcing.job.order.tree</field>
+ <field name="model">sourcing.job.order</field>
+ <field name="arch" type="xml">
+ <tree string="Sourcing Job Orders" default_order="state asc, create_date desc"
+ decoration-success="state=='done'"
+ decoration-danger="state=='cancel'"
+ decoration-warning="state=='taken'">
+ <field name="name"/>
+ <!-- <field name="leads_id"/> -->
+ <field name="user_id" widget="many2one_avatar_user"/>
+ <field name="state" widget="badge"/>
+ <field name="create_date"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="view_sourcing_job_order_form" model="ir.ui.view">
+ <field name="name">sourcing.job.order.form</field>
+ <field name="model">sourcing.job.order</field>
+ <field name="arch" type="xml">
+ <form string="Sourcing Job Order">
+ <header>
+ <button name="action_take"
+ string="Take"
+ type="object"
+ class="btn-primary"
+ icon="fa-hand-paper-o"
+ groups="indoteknik_custom.group_role_merchandiser"
+ attrs="{'invisible': [('state', '!=', 'draft')]}"/>
+
+ <button name="action_cancel"
+ string="Cancel"
+ type="object"
+ class="btn-secondary"
+ icon="fa-times"
+ groups="indoteknik_custom.group_role_merchandiser"
+ attrs="{'invisible': [('state', 'in', ['cancel', 'done'])]}"/>
+
+ <button name="action_ask_approval"
+ string="Ask Approval"
+ type="object"
+ class="btn-primary"
+ groups="indoteknik_custom.group_role_merchandiser"
+ attrs="{'invisible': ['|','|', ('has_price_in_lines', '=', False), ('approval_sales', '=', 'approve'), ('is_creator_same_user', '=', True)]}"/>
+
+ <button name="action_confirm_by_md"
+ string="Confirm"
+ type="object"
+ class="btn-primary"
+ groups="indoteknik_custom.group_role_merchandiser"
+ attrs="{'invisible': [('is_creator_same_user', '=', False)]}"/>
+
+ <button name="action_confirm_approval"
+ string="Approve"
+ type="object"
+ class="btn-primary"
+ groups="indoteknik_custom.group_role_sales"
+ attrs="{'invisible': [('approval_sales', 'in', [False, 'approve'])]}"/>
+
+ <button name="action_request_takeover"
+ string="Request Takeover"
+ type="object"
+ class="btn-primary"
+ groups="indoteknik_custom.group_role_merchandiser"
+ attrs="{'invisible': [('can_request_takeover', '=', False)]}"/>
+
+ <button name="action_approve_takeover"
+ string="Approve Takeover"
+ type="object"
+ class="btn-success"
+ groups="indoteknik_custom.group_role_merchandiser"
+ attrs="{'invisible': [('can_approve_takeover', '=', False)]}"/>
+
+ <button name="action_reject_takeover"
+ string="Reject Takeover"
+ type="object"
+ class="btn-danger"
+ groups="indoteknik_custom.group_role_merchandiser"
+ attrs="{'invisible': [('can_approve_takeover', '=', False)]}"/>
+
+ <field name="state" widget="statusbar"
+ statusbar_visible="draft,taken,done,cancel"
+ statusbar_color='{"draft": "blue", "taken": "orange", "done": "green", "cancel": "red"}'/>
+ </header>
+
+ <sheet>
+ <widget name="web_ribbon"
+ title="COMPLETE"
+ bg_color="bg-success"
+ attrs="{'invisible': [('state', '!=', 'done')]}"/>
+
+ <widget name="web_ribbon"
+ title="CANCEL"
+ bg_color="bg-danger"
+ attrs="{'invisible': [('state', '!=', 'cancel')]}"/>
+ <h1>
+ <field name="name" readonly="1"/>
+ </h1>
+
+ <group>
+ <group>
+ <!-- <field name="leads_id"/> -->
+ <field name="eta_sales"/>
+ <field name="is_creator_same_user" invisible="1"/>
+ <field name="takeover_request" invisible="1"/>
+ <field name="can_request_takeover" invisible="1"/>
+ <field name="can_approve_takeover" invisible="1"/>
+ <field name="has_price_in_lines" invisible="1"/>
+ <field name="sla_product" groups="indoteknik_custom.group_role_merchandiser"/>
+ </group>
+ <group>
+ <field name="create_uid" readonly="1" widget="many2one_avatar_user"/>
+ <field name="user_id" readonly="1" widget="many2one_avatar_user"/>
+ <field name="approval_sales" readonly="1"/>
+ </group>
+ </group>
+
+ <notebook>
+ <page string="Product Line" groups="indoteknik_custom.group_role_sales" attrs="{'invisible': [('has_price_in_lines', '=', True)]}">
+ <field name="line_sales_input_ids">
+ <tree editable="bottom">
+ <field name="product_name"/>
+ <field name="descriptions"/>
+ <field name="quantity"/>
+ </tree>
+ </field>
+ </page>
+
+ <!-- MD EDIT -->
+ <page string="MD Lines" groups="indoteknik_custom.group_role_merchandiser">
+ <field name="line_md_edit_ids">
+ <tree editable="bottom"
+ decoration-success="state in ('done', 'convert')"
+ decoration-danger="state=='cancel'"
+ decoration-warning="state=='sourcing'">
+ <field name="code"/>
+ <field name="product_name"/>
+ <field name="descriptions"/>
+ <field name="quantity"/>
+ <field name="price"/>
+ <field name="vendor_id"/>
+ <field name="tax_id"/>
+ <field name="subtotal"/>
+ <field name="product_category"/>
+ <field name="product_type"/>
+ <field name="product_class"/>
+ <field name="state"/>
+ <field name="reason" attrs="{'invisible': [('state', '!=', 'cancel')]}"/>
+ <button name="action_convert_to_product"
+ string="Convert"
+ type="object"
+ icon="fa-exchange"
+ attrs="{'invisible': [('state', '!=', 'done')]}"/>
+ </tree>
+ </field>
+ </page>
+
+ <!-- SALES VIEW -->
+ <page string="Product Line" groups="indoteknik_custom.group_role_sales" attrs="{'invisible': [('has_price_in_lines', '=', False)]}">
+ <field name="line_sales_view_ids">
+ <tree>
+ <field name="code" readonly="1"/>
+ <field name="product_name" readonly="1"/>
+ <field name="descriptions" readonly="1"/>
+ <field name="quantity" readonly="1"/>
+ <field name="price" readonly="1"/>
+ <field name="vendor_id" readonly="1"/>
+ <field name="tax_id" readonly="1"/>
+ <field name="subtotal" readonly="1"/>
+ <field name="state" readonly="1"/>
+ </tree>
+ </field>
+ </page>
+
+ <page string="Documents">
+ <field name="product_assets" widget="pdf_viewer"/>
+ </page>
+
+ <page string="Cancel Reason" attrs="{'invisible': [('state', 'in', ['done'])]}">
+ <group>
+ <field name="cancel_reason"/>
+ </group>
+ </page>
+ </notebook>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="message_ids" widget="mail_thread"/>
+ <field name="activity_ids" widget="mail_activity"/>
+ </div>
+ </form>
+ </field>
+ </record>
+
+ <record id="action_sourcing_job_order_multi_take" model="ir.actions.server">
+ <field name="name">Take Selected Jobs</field>
+ <field name="model_id" ref="model_sourcing_job_order"/>
+ <field name="binding_model_id" ref="model_sourcing_job_order"/>
+ <field name="binding_type">action</field>
+ <field name="state">code</field>
+ <field name="code">action = records.action_multi_take()</field>
+ <field name="groups_id" eval="[(4, ref('indoteknik_custom.group_role_merchandiser'))]"/>
+ </record>
+
+ <record id="action_sourcing_job_order" model="ir.actions.act_window">
+ <field name="name">Sourcing Job Orders</field>
+ <field name="res_model">sourcing.job.order</field>
+ <field name="view_mode">tree,form</field>
+ <field name="search_view_id" ref="view_sourcing_job_order_search"/>
+ <field name="context">{'search_default_untaken': 1, 'search_default_my_job': 1}</field>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_smiling_face">
+ Buat Sourcing Job Order baru di sini ✨
+ </p>
+ </field>
+ </record>
+
+ <menuitem id="menu_md_root"
+ name="MD"
+ parent="crm.crm_menu_root"
+ sequence="80"/>
+
+ <menuitem id="menu_sourcing_job_order"
+ name="Sourcing Job Order"
+ parent="menu_md_root"
+ action="action_sourcing_job_order"
+ sequence="90"/>
+</odoo>