From f4a1e2917d550eb205e33b058f07e7edbf8029c8 Mon Sep 17 00:00:00 2001 From: HafidBuroiroh Date: Fri, 24 Oct 2025 11:24:58 +0700 Subject: sjo half --- indoteknik_custom/__manifest__.py | 3 +- indoteknik_custom/models/__init__.py | 1 + indoteknik_custom/models/sourcing.py | 65 ----- indoteknik_custom/models/sourcing_job_order.py | 339 +++++++++++++++++++++++++ indoteknik_custom/security/ir.model.access.csv | 2 + indoteknik_custom/views/ir_sequence.xml | 9 +- indoteknik_custom/views/sourcing.xml | 182 +++++++++++++ 7 files changed, 534 insertions(+), 67 deletions(-) delete mode 100644 indoteknik_custom/models/sourcing.py create mode 100644 indoteknik_custom/models/sourcing_job_order.py diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index cf7cf1e4..3c3c9a95 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -183,7 +183,8 @@ 'views/letter_receivable.xml', 'views/letter_receivable_mail_template.xml', # 'views/reimburse.xml', - 'views/sj_tele.xml' + 'views/sj_tele.xml', + 'views/sourcing.xml' ], 'demo': [], 'css': [], diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index 6dc61277..cb110342 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -160,3 +160,4 @@ from . import update_date_planned_po_wizard from . import unpaid_invoice_view from . import letter_receivable from . import sj_tele +from . import sourcing_job_order diff --git a/indoteknik_custom/models/sourcing.py b/indoteknik_custom/models/sourcing.py deleted file mode 100644 index 62540700..00000000 --- a/indoteknik_custom/models/sourcing.py +++ /dev/null @@ -1,65 +0,0 @@ -from odoo import models, fields, api -from odoo.exceptions import UserError -import requests -import json -import logging, subprocess - -_logger = logging.getLogger(__name__) - -class SourcingOrderJob(models.Model): - _name = 'sourcing.job.order' - _descriptions = 'Sourcing Job Order MD' - _rec_name = 'name' - _inherit = ['mail.thread', 'mail.activity.mixin'] - - name = fields.Char(string='Job Number', default='name', copy=False, readonly=True) - leads_id = fields.Many2One('crm.lead', string='Leads Number') - product_name = fields.Char(string='Nama Barang', required=True) - descriptions = fields.Text(string='Deskripsi / Spesifikasi', required=True) - quantity = fields.Float(string='Quantity Product', required=True) - eta_sales = fiels.Date(string='Expected Ready') - sla_product = fields.Date(string='Product Ready Date') - price = fields.Float(string='Harga Product Vendor') - vendor_id = fields.Many2One('res.partner', string="Vendor") - tax_id = fields.Many2One('account.tax', string='Tax', domain=['|', ('active', '=', False), ('active', '=', True)]) - user_id = fields.Many2One('res.users', string='MD Person') - subtotal = fields.Float(string='Total Purchase', compute='_compute_subtotal_purchase') - state = fields.Selection([ - ('draft', 'Untaken'), - ('taken', 'On Sourcing'), - ('done', 'Complete'), - ('cancel', 'Cancel') - ], string='Status') - - @api.depends('quantity', 'price', 'tax_id') - def _compute_subtotal_purchase(self): - """Menghitung subtotal termasuk pajak.""" - for record in self: - subtotal = (record.quantity or 0.0) * (record.price or 0.0) - if record.tax_id: - tax_amount = subtotal * (record.tax_id.amount / 100) - subtotal += tax_amount - - record.subtotal = subtotal - - @api.model - def create(self, vals): - 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' - - return super().create(vals) - - def write(self, vals): - 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.") - - return super().write(vals) - - diff --git a/indoteknik_custom/models/sourcing_job_order.py b/indoteknik_custom/models/sourcing_job_order.py new file mode 100644 index 00000000..3d4a404e --- /dev/null +++ b/indoteknik_custom/models/sourcing_job_order.py @@ -0,0 +1,339 @@ +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=[('price', '>', 0)] + ) + + has_price_in_lines = fields.Boolean( + string='Has Line with Price', + compute='_compute_has_price_in_lines', + ) + + + @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.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): + context_action = self.env.context.get('from_action_take', 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 context_action: + 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 context_action: + raise UserError("❌ Hanya MD Person dan Creator SJO ini yang bisa melakukan Edit.") + + return super().write(vals) + + def action_take(self): + 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 %s diambil oleh %s") % (rec.name, self.env.user.name)) + + def action_multi_take(self): + 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 draft untuk diambil.") + + untaken.write({ + 'state': 'taken', + 'user_id': self.env.user.id, + }) + + 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 %s dibatalkan oleh %s
Alasan: %s") % + (rec.name, self.env.user.name, rec.cancel_reason)) + + def action_request_takeover(self): + 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') + self.env['mail.activity'].create({ + 'activity_type_id': activity_type.id, + 'note': f"{self.env.user.name} meminta approval untuk mengambil alih SJO '{rec.name}'.", + 'res_id': rec.id, + 'res_model_id': self.env['ir.model']._get_id('sourcing.job.order'), + 'user_id': rec.user_id.id, + }) + + rec.message_post( + body=f"{self.env.user.name} mengirimkan request takeover kepada {rec.user_id.name}.", + 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_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.") + + rec.approval_sales = 'requested' + + activity_type = self.env.ref('mail.mail_activity_data_todo') + self.env['mail.activity'].create({ + 'activity_type_id': activity_type.id, + 'note': f"{self.env.user.name} meminta approval untuk mengambil alih SJO '{rec.name}'.", + 'res_id': rec.id, + 'res_model_id': self.env['ir.model']._get_id('sourcing.job.order'), + 'user_id': rec.user_id.id, + }) + + rec.message_post( + body=f"{self.env.user.name} mengirimkan request approval kepada {rec.user_id.name}.", + 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 {self.env.user.name}. Sourcing Job berpindah ke {new_user.name}.", + 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 {requester.name} ditolak oleh {self.env.user.name}.", + subtype_xmlid="mail.mt_comment" + ) + + +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') + 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") + state = fields.Selection([ + ('draft', 'Unsource'), + ('sourcing', 'On Sourcing'), + ('done', 'Done Sourcing'), + ('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') + 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): + raise ValidationError("MD wajib mengisi Product Type dan Product Category!") + + @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 + ) diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index ea6670eb..17372e48 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -202,3 +202,5 @@ access_unpaid_invoice_view,access.unpaid.invoice.view,model_unpaid_invoice_view, access_surat_piutang_user,surat.piutang user,model_surat_piutang,base.group_user,1,1,1,1 access_surat_piutang_line_user,surat.piutang.line user,model_surat_piutang_line,base.group_user,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 4b8fec53..5ea7324b 100644 --- a/indoteknik_custom/views/ir_sequence.xml +++ b/indoteknik_custom/views/ir_sequence.xml @@ -218,7 +218,14 @@ 1 1 - + + Sourcing Job Order + sourcing.job.order + SJO/%(year)s/ + 4 + 1 + 1 + Refund Sales Order refund.sale.order diff --git a/indoteknik_custom/views/sourcing.xml b/indoteknik_custom/views/sourcing.xml index e69de29b..f9f8f386 100644 --- a/indoteknik_custom/views/sourcing.xml +++ b/indoteknik_custom/views/sourcing.xml @@ -0,0 +1,182 @@ + + + + sourcing.job.order.tree + sourcing.job.order + + + + + + + + + + + + + sourcing.job.order.form + sourcing.job.order + +
+
+
+ + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+
+
+ + + Take Selected Jobs + + + action + code + action = records.action_multi_take() + + + + + Sourcing Job Orders + sourcing.job.order + tree,form + +

+ Buat Sourcing Job Order baru di sini ✨ +

+
+
+ + + + +
-- cgit v1.2.3