summaryrefslogtreecommitdiff
path: root/indoteknik_custom/models
diff options
context:
space:
mode:
authorHafidBuroiroh <hafidburoiroh09@gmail.com>2025-11-17 08:39:51 +0700
committerHafidBuroiroh <hafidburoiroh09@gmail.com>2025-11-17 08:39:51 +0700
commitbfd20e54cb88f04ff1a338bdc58984241c8a83a2 (patch)
tree6e5fe387144fdce968814650b8f8842ae165a987 /indoteknik_custom/models
parent4f11653e57d4f2e4163b5ef69c0731a675a5e2bd (diff)
<hafid> done sjo
Diffstat (limited to 'indoteknik_custom/models')
-rwxr-xr-xindoteknik_custom/models/purchase_pricelist.py2
-rw-r--r--indoteknik_custom/models/sourcing_job_order.py427
-rw-r--r--indoteknik_custom/models/unpaid_invoice_view.py2
3 files changed, 376 insertions, 55 deletions
diff --git a/indoteknik_custom/models/purchase_pricelist.py b/indoteknik_custom/models/purchase_pricelist.py
index b3a473b6..a87f03d4 100755
--- a/indoteknik_custom/models/purchase_pricelist.py
+++ b/indoteknik_custom/models/purchase_pricelist.py
@@ -32,7 +32,7 @@ class PurchasePricelist(models.Model):
for promotion in promotion_product:
promotion.program_line_id.get_price_tier(promotion.product_id, promotion.qty)
- @api.depends('product_id', 'vendor_id')
+ @api.depends('product_id', 'vendor_id')
def _compute_name(self):
self.name = self.vendor_id.name + ', ' + self.product_id.name
diff --git a/indoteknik_custom/models/sourcing_job_order.py b/indoteknik_custom/models/sourcing_job_order.py
index cc80d684..75307ee2 100644
--- a/indoteknik_custom/models/sourcing_job_order.py
+++ b/indoteknik_custom/models/sourcing_job_order.py
@@ -3,6 +3,8 @@ from odoo.exceptions import UserError
from datetime import date, datetime
import requests
import logging
+import pytz
+from pytz import timezone
_logger = logging.getLogger(__name__)
@@ -12,11 +14,12 @@ class SourcingJobOrder(models.Model):
_description = 'Sourcing Job Order MD'
_rec_name = 'name'
_inherit = ['mail.thread', 'mail.activity.mixin']
+ _order = 'is_priority desc, state asc, create_date desc'
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)
- so_id = fields.Many2one('sale.order', string='SO Number', tracking=True, required=True)
+ so_id = fields.Many2one('sale.order', string='SO Number', tracking=True, domain="[('state', '=', 'draft')]")
product_assets_filename = fields.Char(string="Nama File PDF")
state = fields.Selection([
('draft', 'Untaken'),
@@ -27,6 +30,7 @@ class SourcingJobOrder(models.Model):
approval_sales = fields.Selection([
('draft', 'Requested'),
('approve', 'Approved'),
+ ('reject', 'Rejected'),
], string='Approval Sales', tracking=True)
takeover_request = fields.Many2one(
'res.users',
@@ -35,6 +39,12 @@ class SourcingJobOrder(models.Model):
tracking=True,
help='MD yang meminta takeover'
)
+ is_priority = fields.Boolean(
+ string="Priority",
+ default=False,
+ tracking=True,
+ help="Otomatis aktif jika request approval ditolak oleh sales."
+ )
can_request_takeover = fields.Boolean(
compute="_compute_can_request_takeover"
)
@@ -43,6 +53,7 @@ class SourcingJobOrder(models.Model):
)
eta_sales = fields.Date(string='Expected Ready')
+ eta_complete = fields.Date(string='Completed Date')
cancel_reason = fields.Text(string="Reason for Cancel", tracking=True)
product_assets = fields.Binary(string="Product Assets (PDF)")
@@ -69,6 +80,11 @@ class SourcingJobOrder(models.Model):
string='Sales View Lines',
domain=[('state', '=', 'cancel')]
)
+ exported_line_ids = fields.One2many(
+ "sourcing.job.order.line",
+ "order_id",
+ string="Lines"
+ )
converted_product_ids = fields.One2many(
"product.product",
@@ -86,9 +102,50 @@ class SourcingJobOrder(models.Model):
string='Has Line with Price',
compute='_compute_has_price_in_lines',
)
+ progress_status = fields.Char(
+ string='Progress Status',
+ compute='_compute_progress_status',
+ default=''
+ )
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")
+ def _get_jakarta_today(self):
+ jakarta_tz = pytz.timezone('Asia/Jakarta')
+ now_jakarta = datetime.now(jakarta_tz)
+ return now_jakarta.date()
+
+ @api.depends('eta_sales', 'eta_complete', 'create_date', 'state')
+ def _compute_progress_status(self):
+ for rec in self:
+ if rec.eta_sales:
+ # Ada tanggal expected
+ if rec.state == 'taken':
+ rec.progress_status = '๐ŸŸก On Track'
+ elif rec.state == 'done' and rec.eta_complete:
+ delta = (rec.eta_complete - rec.eta_sales).days
+ if delta < 0:
+ rec.progress_status = f'๐ŸŸข Early {abs(delta)} hari'
+ elif delta == 0:
+ rec.progress_status = '๐Ÿ”ต Ontime'
+ else:
+ rec.progress_status = f'๐Ÿ”ด Delay {delta} hari'
+ elif rec.state == 'cancel':
+ rec.progress_status = 'โšซ Cancelled'
+ else:
+ rec.progress_status = '๐ŸŸก On Track'
+ else:
+ # Tidak ada ETA, hitung durasi
+ if rec.state == 'done' and rec.eta_complete:
+ if rec.create_date:
+ durasi = (rec.eta_complete - rec.create_date.date()).days
+ rec.progress_status = f'โœ… Selesai dalam {durasi} hari'
+ else:
+ rec.progress_status = 'โœ… Selesai'
+ elif rec.state == 'cancel':
+ rec.progress_status = 'โšซ Cancelled'
+ else:
+ rec.progress_status = '๐ŸŸก On Track'
@api.depends('line_ids.subtotal')
def _compute_total_amount(self):
@@ -135,17 +192,14 @@ class SourcingJobOrder(models.Model):
for rec in self:
rec.converted_product_count = len(rec.converted_product_ids)
- def action_open_converted_products(self):
- """Open converted products related to this SJO."""
- self.ensure_one()
- return {
- 'name': 'Converted Products',
- 'type': 'ir.actions.act_window',
- 'view_mode': 'tree,form',
- 'res_model': 'product.product',
- 'domain': [('id', 'in', self.converted_product_ids.ids)],
- 'context': {'default_sourcing_job_id': self.id},
- }
+ @api.onchange('approval_sales')
+ def _onchange_approval_sales_priority(self):
+ """Otomatis tandai priority jika approval_sales = reject"""
+ for rec in self:
+ if rec.approval_sales == 'reject':
+ rec.is_priority = True
+ else:
+ rec.is_priority = False
@api.depends('line_md_edit_ids.state')
def _compute_can_convert_to_product(self):
@@ -172,6 +226,9 @@ class SourcingJobOrder(models.Model):
if vals.get('product_assets'):
rec._log_product_assets_upload()
+ if rec.create_uid.id == rec.user_id.id and rec.line_md_edit_ids:
+ rec.line_md_edit_ids.write({'state': 'sourcing'})
+
return rec
def write(self, vals):
@@ -219,6 +276,12 @@ class SourcingJobOrder(models.Model):
for line in rec.line_md_edit_ids
},
}
+ if rec.create_uid.id == rec.user_id.id and rec.line_md_edit_ids:
+ for line in rec.line_md_edit_ids:
+ if line.state == 'draft':
+ line.write({'state': 'sourcing'})
+ elif all([line.vendor_id, line.price, line.tax_id]) and line.state in ('draft', 'sourcing'):
+ line.write({'state': 'done'})
res = super().write(vals)
if vals.get('product_assets'):
@@ -291,6 +354,9 @@ class SourcingJobOrder(models.Model):
'state': 'taken',
'user_id': self.env.user.id
})
+ if rec.line_md_edit_ids:
+ rec.line_md_edit_ids.write({'state': 'sourcing'})
+
rec.message_post(body=("Job <b>%s</b> diambil oleh %s") % (rec.name, self.env.user.name))
def action_multi_take(self):
@@ -305,24 +371,28 @@ class SourcingJobOrder(models.Model):
'state': 'taken',
'user_id': self.env.user.id,
})
+ for rec in untaken:
+ if rec.line_md_edit_ids:
+ rec.line_md_edit_ids.write({'state': 'sourcing'})
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', 'convert'))
+ invalid_lines = rec.line_md_edit_ids.filtered(lambda l: l.state not in ('cancel', 'convert'))
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}"
+ f"Masih ada line yang belum selesai disourcing & diconvert: {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.eta_complete = self._get_jakarta_today()
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>).",
@@ -336,6 +406,41 @@ class SourcingJobOrder(models.Model):
return {'type': 'ir.actions.client','tag': 'reload',}
+ def action_confirm_after_approval(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.")
+ done_lines = rec.line_ids.filtered(lambda l: l.state == 'convert')
+ if not done_lines:
+ raise UserError("โš ๏ธ Confirm Line hanya bisa dilakukan setelah Convert Line.")
+ 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.state = 'done'
+ rec.eta_complete = self._get_jakarta_today()
+ if rec.is_priority == True:
+ rec.is_priority = False
+
+ self.env.user.notify_success(
+ message=f"Sourcing Job '{rec.name}' Confirmed.",
+ title="Confirmed",
+ )
+
+ return {'type': 'ir.actions.client','tag': 'reload',}
+
+ def action_open_converted_products(self):
+ """Open converted products related to this SJO."""
+ self.ensure_one()
+ return {
+ 'name': 'Converted Products',
+ 'type': 'ir.actions.act_window',
+ 'view_mode': 'tree,form',
+ 'res_model': 'product.product',
+ 'domain': [('id', 'in', self.converted_product_ids.ids)],
+ 'context': {'default_sourcing_job_id': self.id},
+ }
+
+
def action_cancel(self):
for rec in self:
if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
@@ -360,7 +465,7 @@ class SourcingJobOrder(models.Model):
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
+ rec.with_context(from_action_takeover=True).write({'takeover_request': self.env.user.id})
activity_type = self.env.ref('mail.mail_activity_data_todo')
rec.activity_schedule(
@@ -404,6 +509,12 @@ class SourcingJobOrder(models.Model):
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"
)
+ self.env.user.notify_success(
+ message=f"Request takeover telah Disetujui dan Dialihkan ke {rec.user_id.name}.",
+ title="Request Sent",
+ )
+
+ return {'type': 'ir.actions.client','tag': 'reload',}
def action_reject_takeover(self):
for rec in self:
@@ -431,37 +542,93 @@ class SourcingJobOrder(models.Model):
for rec in self:
if rec.user_id != self.env.user:
raise UserError("โŒ Hanya MD Person dari Sourcing Job ini yang dapat melakukan konversi produk.")
+
done_lines = rec.line_ids.filtered(lambda l: l.state == 'done')
+ if rec.create_uid != rec.user_id and rec.approval_sales != 'approve':
+ raise UserError("โš ๏ธ Convert Line hanya bisa dilakukan setelah sales approve.")
+
if not done_lines:
raise UserError("โš ๏ธ Tidak ada line dengan status 'Done Sourcing' untuk dikonversi.")
ProductProduct = self.env['product.product']
+ ProductTemplate = self.env['product.template']
+ PurchasePricelist = self.env['purchase.pricelist']
existing_skus = []
created_products = []
for line in done_lines:
- if not line.code:
- continue
- existing = ProductProduct.search([('default_code', '=', line.code)], limit=1)
+ existing = False
+ if line.code:
+ existing = ProductProduct.search([('default_code', '=', line.code)], limit=1)
+
if existing:
existing_skus.append(line.code)
+ line.state = 'convert'
continue
+
type_map = {
'servis': 'service',
'product': 'product',
'consu': 'consu',}
+ manufactures = self.env['x_manufactures']
+ if line.brand:
+ manufactures = manufactures.search([('x_name', 'ilike', line.brand)], limit=1)
+
new_product = ProductProduct.create({
'name': line.product_name,
- 'default_code': line.code,
+ 'default_code': line.code or False,
'description': line.descriptions or '',
'type': type_map.get(line.product_type, 'product'),
'categ_id': line.product_category.id if line.product_category else False,
+ 'x_manufacture': manufactures.id if manufactures else False,
'standard_price': line.price if line.price else 0,
'public_categ_ids': [(6, 0, [line.product_class.id])] if line.product_class else False,
'active': True,
'sourcing_job_id': rec.id,
})
+
+ if not line.code:
+ sku_auto = 'IT.' + str(new_product.id)
+ new_product.default_code = sku_auto
+ line.code = sku_auto
+ _logger.info(f"SKU otomatis di-set: {sku_auto} untuk produk {new_product.name}")
+
+ if new_product:
+ jakarta_tz = fields.Datetime.now(timezone('Asia/Jakarta')).strftime('%Y-%m-%d %H:%M:%S')
+
+ pricelist_vals = {
+ 'product_id': new_product.id,
+ 'vendor_id': line.vendor_id.id,
+ 'system_price': line.price if line.price else 0,
+ 'product_price': line.price if line.price else 0,
+ 'include_price': line.price if line.price else 0,
+ 'taxes_system_id': line.tax_id.id if line.tax_id else False,
+ 'taxes_product_id': line.tax_id.id if line.tax_id else False,
+ 'brand_id': new_product.x_manufacture.id if new_product.x_manufacture else False,
+ 'system_last_update': jakarta_tz,
+ 'human_last_update': jakarta_tz,
+ 'is_winner': True,
+ }
+
+ new_pricelist = PurchasePricelist.create(pricelist_vals)
+ rec.message_post(
+ body=(
+ f"๐Ÿงพ <b>Purchase Pricelist berhasil dibuat</b><br/>"
+ f"<ul>"
+ f"<li><b>Produk:</b> {new_product.name}</li>"
+ f"<li><b>Vendor:</b> {line.vendor_id.display_name}</li>"
+ f"<li><b>Harga Sistem:</b> {line.price or 0.0}</li>"
+ f"<li><b>Tanggal:</b> {jakarta_tz}</li>"
+ f"</ul>"
+ ),
+ subtype_xmlid="mail.mt_comment"
+ )
+
+ _logger.info(
+ f"๐Ÿงพ Purchase Pricelist dibuat untuk produk {new_product.name} "
+ f"dengan vendor {line.vendor_id.name} dan harga {line.price}"
+ )
line.state = 'convert'
created_products.append(new_product.name)
@@ -476,16 +643,17 @@ class SourcingJobOrder(models.Model):
)
if existing_skus:
- raise UserError(
- "โš ๏ธ SKU berikut sudah ada dan tidak dikonversi:\n- "
- + "\n- ".join(existing_skus)
+ rec.message_post(
+ body=(
+ f"โ„น๏ธ <b>SKU berikut sudah ada di sistem dan tidak dibuat ulang:</b><br/>"
+ + "<br/>".join(existing_skus)
+ ),
+ subtype_xmlid="mail.mt_comment",
)
- if not created_products:
- raise UserError("โ„น๏ธ Tidak ada produk baru yang dikonversi (semua SKU sudah ada).")
-
self.env.user.notify_success(
- message=f"{len(created_products)} produk berhasil dibuat di Master Product.",
+ message=f"{len(created_products)} produk baru berhasil dikonversi. "
+ f"{len(existing_skus)} SKU sudah ada di sistem.",
title="Konversi Selesai",
)
@@ -504,6 +672,10 @@ class SourcingJobOrder(models.Model):
)
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.")
+
+ bot_sjo = '8335015210:AAGbObP0jQf7ptyqJhYdBYn5Rm0CWOd_yIM'
+ chat_sjo = '6076436058'
+ api_base = f'https://api.telegram.org/bot{bot_sjo}/sendMessage'
rec.approval_sales = 'draft'
@@ -523,6 +695,30 @@ class SourcingJobOrder(models.Model):
message=f"Request Approval telah dikirim ke {rec.create_uid.name}.",
title="Request Sent",
)
+
+ base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
+ url = f"{base_url}web#id={rec.id}&model=sourcing.job.order&view_type=form"
+
+ try:
+ msg_text = (
+ f"๐Ÿ“ข <b>Request Approval Baru</b>\n\n"
+ f"๐Ÿงพ <b>Sourcing Job:</b> <a href='{url}'>๐Ÿ“Ž {rec.name}</a>\n"
+ f"๐Ÿ‘ค <b>Dari:</b> {self.env.user.name}\n"
+ f"๐Ÿ“… <b>Tanggal:</b> {fields.Datetime.now().strftime('%d-%m-%Y %H:%M')}\n\n"
+ f"Silakan lakukan Review di Odoo."
+ )
+
+ payload = {
+ 'chat_id': chat_sjo,
+ 'text': msg_text,
+ 'parse_mode': 'HTML'
+ }
+
+ response = requests.post(api_base, data=payload)
+ response.raise_for_status()
+ except Exception as e:
+ _logger.warning(f"Gagal kirim pesan Telegram: {e}")
+
return {'type': 'ir.actions.client', 'tag': 'reload'}
def action_confirm_approval(self):
@@ -557,31 +753,21 @@ class SourcingJobOrder(models.Model):
if rec.create_uid != self.env.user:
raise UserError("โŒ Hanya Sales (pembuat SJO ini) yang dapat melakukan Reject Approval.")
- rec.approval_sales = False
-
- activities = self.env['mail.activity'].search([
- ('res_model', '=', rec._name),
- ('res_id', '=', rec.id),
- ])
- activities.unlink()
-
- rec.message_post(
- body=f"<b>{self.env.user.name}</b> menolak approval untuk SJO '<b>{rec.name}</b>'. "
- f"Status dikembalikan untuk MD dapat melakukan Sourcing ulang.",
- subtype_xmlid="mail.mt_comment"
- )
-
- self.env.user.notify_info(
- message=f"Approval SJO '{rec.name}' telah ditolak. MD dapat melakukan Sourcing ulang.",
- title="Approval Ditolak",
- )
-
- return {'type': 'ir.actions.client', 'tag': 'reload'}
+ return {
+ 'name': 'Reason for Reject',
+ 'type': 'ir.actions.act_window',
+ 'view_mode': 'form',
+ 'res_model': 'sourcing.reject.wizard',
+ 'target': 'new',
+ 'context': {
+ 'default_sjo_id': rec.id,
+ }
+ }
def action_send_untaken_to_telegram(self):
bot_sjo = '8335015210:AAGbObP0jQf7ptyqJhYdBYn5Rm0CWOd_yIM'
- # chat_group_sjo = '-5081839952'
- chat_sjo = '6076436058'
+ chat_group_sjo = '-5081839952'
+ # chat_sjo = '6076436058'
api_base = f'https://api.telegram.org/bot{bot_sjo}'
data = self.search([('state', '=', 'draft')], order='create_date asc')
@@ -593,7 +779,7 @@ class SourcingJobOrder(models.Model):
text += f"- {sjo.name} | Requested By: {sjo.create_uid.name}\n"
payload = {
- 'chat_id': chat_sjo,
+ 'chat_id': chat_group_sjo,
'text': text
}
@@ -628,6 +814,16 @@ class SourcingJobOrder(models.Model):
subtype_xmlid='mail.mt_note'
)
+ def action_export_to_so(self):
+ return {
+ "type": "ir.actions.act_window",
+ "name": "Select Products to Export",
+ "res_model": "wizard.export.sjo.to.so",
+ "view_mode": "form",
+ "target": "new",
+ "context": {"default_sjo_id": self.id},
+ }
+
class SourcingJobOrderLine(models.Model):
_name = 'sourcing.job.order.line'
@@ -636,6 +832,10 @@ class SourcingJobOrderLine(models.Model):
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')
+ budget = fields.Char(string='Expected Price')
+ note = fields.Text(string='Note Sourcing')
+ brand = fields.Char(string='Brand')
+ product_image = fields.Binary(string="Product Image")
descriptions = fields.Text(string='Deskripsi / Spesifikasi')
reason = fields.Text(string='Reason Unavailable')
sla = fields.Char(string='SLA Product')
@@ -645,6 +845,7 @@ class SourcingJobOrderLine(models.Model):
vendor_id = fields.Many2one('res.partner', string="Vendor")
product_category = fields.Many2one('product.category', string="Product Category")
product_class = fields.Many2many('product.public.category', string="Categories")
+ exported_to_so = fields.Boolean(string="Exported to SO", default=False)
state = fields.Selection([
('draft', 'Unsource'),
('sourcing', 'On Sourcing'),
@@ -662,6 +863,7 @@ class SourcingJobOrderLine(models.Model):
string="Show for Sales",
compute="_compute_show_for_sales",
)
+ selected = fields.Boolean(string="Pilih")
@api.depends('quantity', 'price', 'tax_id')
def _compute_subtotal(self):
@@ -672,11 +874,13 @@ class SourcingJobOrderLine(models.Model):
subtotal += subtotal * (line.tax_id.amount / 100)
line.subtotal = subtotal
- @api.constrains('product_type', 'product_category', 'product_class', 'code')
+ @api.constrains('product_type', 'product_category', 'product_class')
def _check_required_fields_for_md(self):
for rec in self:
+ if rec.state == 'cancel':
+ continue
is_md = self.env.user.has_group('indoteknik_custom.group_role_merchandiser')
- if is_md and (not rec.product_type or rec.product_category == False or rec.product_class == False or not rec.code):
+ if is_md and (not rec.product_type or rec.product_category == False or rec.product_class == False):
raise UserError("MD wajib mengisi SKU, Product Type, Product Category, dan Categories!")
@api.depends('state')
@@ -717,6 +921,13 @@ class SourcingJobOrderLine(models.Model):
rec.state = 'convert'
return True
+
+ def action_cancel_line(self):
+ for rec in self:
+ if rec.order_id.user_id != self.env.user:
+ raise UserError("โŒ Hanya MD Person SJO ini yang dapat cancel line.")
+
+ rec.state = 'cancel'
@api.onchange('code')
def _oncange_code(self):
@@ -727,11 +938,121 @@ class SourcingJobOrderLine(models.Model):
product = self.env['product.product'].search([('default_code', '=', rec.code)], limit=1)
if not product:
return
+ template = product.product_tmpl_id
rec.product_name = product.name or rec.product_name
+ rec.product_name = product.name or rec.product_name
+ rec.product_type = template.type or rec.product_type
+ rec.brand = product.x_manufacture.x_name or rec.brand
+ rec.product_category = template.categ_id.id or rec.product_category
+ rec.product_class = [(6, 0, template.public_categ_ids.ids)] if template.public_categ_ids else []
pricelist = self.env['purchase.pricelist'].search([('product_id', '=', product.id), ('is_winner', '=', True)], limit=1)
- if pricelist or tax or vendor:
+ if pricelist:
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
+ rec.tax_id = pricelist.taxes_product_id.id or pricelist.taxes_system_id.id or False
+
+ @api.onchange('vendor_id', 'price', 'tax_id')
+ def _onchange_auto_done(self):
+ """Jika semua field wajib terisi, ubah state ke 'done'."""
+ for rec in self:
+ if all([rec.vendor_id, rec.price, rec.tax_id]) and rec.state in ('draft', 'sourcing'):
+ rec.state = 'done'
+
+
+class SourcingRejectWizard(models.TransientModel):
+ _name = 'sourcing.reject.wizard'
+ _description = 'Wizard untuk alasan reject SJO oleh Sales'
+
+ sjo_id = fields.Many2one('sourcing.job.order', string='Sourcing Job Order')
+ reason = fields.Text(string='Alasan Penolakan', required=True)
+
+ def action_confirm_reject(self):
+ self.ensure_one()
+ sjo = self.sjo_id
+
+ # Reset approval sales
+ sjo.approval_sales = 'reject'
+ sjo.is_priority = True
+
+ # Hapus semua aktivitas terkait
+ activities = self.env['mail.activity'].search([
+ ('res_model', '=', sjo._name),
+ ('res_id', '=', sjo.id),
+ ])
+ activities.unlink()
+
+ # Posting reason ke log note
+ sjo.message_post(
+ body=f"โŒ <b>{self.env.user.name}</b> menolak approval untuk SJO '<b>{sjo.name}</b>'.<br/>"
+ f"<b>Alasan:</b> {self.reason}",
+ subtype_xmlid="mail.mt_comment"
+ )
+
+ # Kirim notifikasi ke user
+ self.env.user.notify_info(
+ message=f"Approval SJO '{sjo.name}' telah ditolak. MD dapat melakukan Sourcing ulang.",
+ title="Approval Ditolak",
+ )
+
+ return {'type': 'ir.actions.client', 'tag': 'reload'}
+
+class WizardExportSJOtoSO(models.TransientModel):
+ _name = "wizard.export.sjo.to.so"
+ _description = "Wizard Export SJO Products to SO"
+
+ sjo_id = fields.Many2one("sourcing.job.order")
+ line_ids = fields.Many2many("product.product", string="Products")
+
+ @api.model
+ def default_get(self, fields):
+ res = super().default_get(fields)
+ sjo_id = self.env.context.get("default_sjo_id")
+
+ if sjo_id:
+ # Ambil product sudah convert
+ products = self.env["product.product"].search([
+ ("sourcing_job_id", "=", sjo_id)
+ ])
+ res["line_ids"] = [(6, 0, products.ids)]
+
+ return res
+
+ def action_confirm(self):
+ self.ensure_one()
+ sjo = self.sjo_id
+
+ if not sjo.so_id:
+ raise UserError("Sales Order belum dipilih di SJO!")
+
+ so = sjo.so_id
+ SaleOrderLine = self.env["sale.order.line"]
+
+ for product in self.line_ids:
+ so_line_new = SaleOrderLine.new({
+ "order_id": so.id,
+ "product_id": product.id,
+ })
+
+ so_line_new.product_id_change()
+
+ vals = SaleOrderLine._convert_to_write(so_line_new._cache)
+
+ new_line = SaleOrderLine.create(vals)
+
+ sjo_line = self.env["sourcing.job.order.line"].search([
+ ("order_id", "=", sjo.id),
+ ("code", "=", product.default_code)
+ ], limit=1)
+
+ if sjo_line:
+ sjo_line.exported_to_so = True
+
+ return {
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'sale.order',
+ 'view_mode': 'form',
+ 'res_id': so.id,
+ 'target': 'current',
+ } \ No newline at end of file
diff --git a/indoteknik_custom/models/unpaid_invoice_view.py b/indoteknik_custom/models/unpaid_invoice_view.py
index 3eb6efc7..b7fcf28e 100644
--- a/indoteknik_custom/models/unpaid_invoice_view.py
+++ b/indoteknik_custom/models/unpaid_invoice_view.py
@@ -43,7 +43,7 @@ class UnpaidInvoiceView(models.Model):
def action_create_surat_piutang(self):
self.ensure_one()
- return {
+ return {
'type': 'ir.actions.act_window',
'res_model': 'surat.piutang',
'view_mode': 'form',