summaryrefslogtreecommitdiff
path: root/indoteknik_custom/models
diff options
context:
space:
mode:
authorHafidBuroiroh <hafidburoiroh09@gmail.com>2025-10-24 11:24:58 +0700
committerHafidBuroiroh <hafidburoiroh09@gmail.com>2025-10-24 11:24:58 +0700
commitf4a1e2917d550eb205e33b058f07e7edbf8029c8 (patch)
tree757cef98c3c94ce7327fff3bbadd06a269f4fa88 /indoteknik_custom/models
parent51ebcf6f9b57c61efabb9e4e0b4fc8d5bc058512 (diff)
<hafid> sjo half
Diffstat (limited to 'indoteknik_custom/models')
-rwxr-xr-xindoteknik_custom/models/__init__.py1
-rw-r--r--indoteknik_custom/models/sourcing.py65
-rw-r--r--indoteknik_custom/models/sourcing_job_order.py339
3 files changed, 340 insertions, 65 deletions
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 <b>%s</b> 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 <b>%s</b> dibatalkan oleh %s<br/>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"<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_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"<b>{self.env.user.name}</b> mengirimkan request approval 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"
+ )
+
+
+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
+ )