summaryrefslogtreecommitdiff
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
parent4f11653e57d4f2e4163b5ef69c0731a675a5e2bd (diff)
<hafid> done sjo
-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
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv2
-rw-r--r--indoteknik_custom/views/sourcing.xml141
5 files changed, 504 insertions, 70 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',
diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv
index bbcbac84..90aa634c 100755
--- a/indoteknik_custom/security/ir.model.access.csv
+++ b/indoteknik_custom/security/ir.model.access.csv
@@ -212,3 +212,5 @@ access_surat_piutang_line_user,surat.piutang.line user,model_surat_piutang_line,
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
+access_sourcing_reject_wizard,sourcing.reject.wizard,model_sourcing_reject_wizard,base.group_user,1,1,1,1
+access_wizard_export_sjo_to_so,wizard.export.sjo.to.so,model_wizard_export_sjo_to_so,base.group_user,1,1,1,1 \ No newline at end of file
diff --git a/indoteknik_custom/views/sourcing.xml b/indoteknik_custom/views/sourcing.xml
index 582a1b3f..839b7bfb 100644
--- a/indoteknik_custom/views/sourcing.xml
+++ b/indoteknik_custom/views/sourcing.xml
@@ -5,8 +5,9 @@
<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="state" string="Status"/>
<field name="user_id" string="MD Person"/>
+
<filter name="my_job"
string="My Sourcing Job"
domain="[('user_id', '=', uid), ('state', '=', 'taken')]"/>
@@ -16,6 +17,9 @@
<filter name="done"
string="Complete"
domain="[('state', '=', 'done')]" />
+ <filter name="by_create_uid"
+ string="Created by Me"
+ domain="[('create_uid', '=', uid)]"/>
</search>
</field>
</record>
@@ -23,19 +27,48 @@
<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"
+ <tree string="Sourcing Job Orders"
decoration-success="state=='done'"
decoration-danger="state=='cancel'"
decoration-warning="state=='taken'">
+ <field name="is_priority" optional="hide" readonly="1"/>
<field name="name"/>
+ <field name="eta_sales" optional="hide"/>
+ <field name="eta_complete" optional="hide"/>
<!-- <field name="leads_id"/> -->
+ <field name="create_uid" widget="many2one_avatar_user"/>
<field name="user_id" widget="many2one_avatar_user"/>
<field name="state" widget="badge"/>
+ <field name="progress_status"
+ decoration-info="progress_status in ['๐ŸŸก On Track', '๐Ÿ”ต Ontime']"
+ decoration-success="'๐ŸŸข' in progress_status"
+ decoration-danger="'๐Ÿ”ด' in progress_status"
+ decoration-muted="'โšซ' in progress_status"/>
<field name="create_date"/>
</tree>
</field>
</record>
+ <record id="view_wizard_export_sjo_to_so_form" model="ir.ui.view">
+ <field name="name">wizard.export.sjo.to.so.form</field>
+ <field name="model">wizard.export.sjo.to.so</field>
+ <field name="arch" type="xml">
+ <form>
+ <group>
+ <field name="line_ids" widget="many2many_tags"/>
+ </group>
+
+ <footer>
+ <button string="Export"
+ type="object"
+ name="action_confirm"
+ class="btn-primary"/>
+ <button string="Cancel" class="btn-secondary" special="cancel"/>
+ </footer>
+ </form>
+ </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>
@@ -93,6 +126,13 @@
groups="indoteknik_custom.group_role_merchandiser"
attrs="{'invisible': [('can_approve_takeover', '=', False)]}"/>
+ <button name="action_confirm_after_approval"
+ string="Confirm"
+ type="object"
+ class="btn-primary"
+ groups="indoteknik_custom.group_role_merchandiser"
+ attrs="{'invisible': ['|', ('approval_sales', '!=', 'approve'), ('state', '=', 'done')]}"/>
+
<button name="action_convert_all_lines"
string="Convert Lines"
type="object"
@@ -113,6 +153,13 @@
class="btn-danger"
groups="indoteknik_custom.group_role_merchandiser"
attrs="{'invisible': [('can_approve_takeover', '=', False)]}"/>
+
+ <button name="action_export_to_so"
+ string="Insert to SO"
+ type="object"
+ class="btn-primary"
+ groups="indoteknik_custom.group_role_merchandiser"
+ attrs="{'invisible': ['|', ('state', '!=', 'done'), ('so_id','=', False)]}"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,taken,done,cancel"
@@ -146,6 +193,7 @@
<group>
<!-- <field name="leads_id"/> -->
<field name="eta_sales"/>
+ <field name="eta_complete" readonly="1"/>
<field name="so_id"/>
<field name="is_creator_same_user" invisible="1"/>
<field name="takeover_request" invisible="1"/>
@@ -158,17 +206,38 @@
<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"/>
+ <field name="progress_status"
+ decoration-info="progress_status in ['๐ŸŸก On Track', '๐Ÿ”ต Ontime']"
+ decoration-success="'๐ŸŸข' in progress_status"
+ decoration-danger="'๐Ÿ”ด' in progress_status"
+ decoration-muted="'โšซ' in progress_status"/>
</group>
</group>
<notebook>
- <page string="Product Line" groups="indoteknik_custom.group_role_sales" attrs="{'invisible': [('has_price_in_lines', '=', True)]}">
+ <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">
+ <tree>
+ <field name="brand"/>
<field name="product_name"/>
<field name="descriptions"/>
<field name="quantity"/>
+ <field name="note" optional="hide"/>
+ <field name="budget" optional="hide"/>
+ <field name="product_image" optional="hide"/>
</tree>
+ <form string="Product Line">
+ <group>
+ <field name="brand" placeholder="Jika tidak mengetahui Brandnya isi saja *No Brand" required="1"/>
+ <field name="product_name"/>
+ <field name="descriptions" required="1"/>
+ <field name="quantity"/>
+ <field name="note"/>
+ <field name="budget"/>
+ <field name="product_image"/>
+ </group>
+ </form>
</field>
</page>
@@ -179,7 +248,9 @@
decoration-success="state in ('done', 'convert')"
decoration-danger="state=='cancel'"
decoration-warning="state=='sourcing'">
+ <field name="selected" widget="boolean_toggle"/>
<field name="code"/>
+ <field name="brand"/>
<field name="product_name"/>
<field name="descriptions"/>
<field name="quantity"/>
@@ -194,11 +265,18 @@
type="object"
icon="fa-exchange"
attrs="{'invisible': [('state', '!=', 'done')]}"/>
+ <button name="action_cancel_line"
+ string="Unavailable"
+ type="object"
+ class="btn-danger"
+ groups="indoteknik_custom.group_role_merchandiser"
+ attrs="{'invisible': [('state', '=', 'cancel')]}"/>
</tree>
<form string="MD Line">
<group>
<group>
<field name="code"/>
+ <field name="brand"/>
<field name="product_name"/>
<field name="descriptions"/>
<field name="quantity"/>
@@ -212,8 +290,11 @@
<field name="product_category"/>
<field name="product_type"/>
<field name="product_class" widget="many2many_tags"/>
- <field name="state"/>
- <field name="reason" attrs="{'invisible': [('state', '!=', 'cancel')]}"/>
+ <field name="note"/>
+ <field name="budget"/>
+ <field name="product_image" widget="image"/>
+ <field name="state" readonly="1" force_save="1"/>
+ <field name="reason" attrs="{'invisible': [('state', 'in', ['cancel', 'done', 'convert'])]}"/>
</group>
</group>
</form>
@@ -225,6 +306,7 @@
<field name="line_sales_view_ids">
<tree>
<field name="code" readonly="1"/>
+ <field name="brand"/>
<field name="product_name" readonly="1"/>
<field name="descriptions" readonly="1"/>
<field name="quantity" readonly="1"/>
@@ -257,6 +339,22 @@
</field>
</record>
+ <record id="view_sourcing_reject_wizard_form" model="ir.ui.view">
+ <field name="name">sourcing.reject.wizard.form</field>
+ <field name="model">sourcing.reject.wizard</field>
+ <field name="arch" type="xml">
+ <form string="Reason for Reject">
+ <group>
+ <field name="reason" placeholder="Tuliskan alasan penolakan..." nolabel="1"/>
+ </group>
+ <footer>
+ <button string="Confirm Reject" type="object" name="action_confirm_reject" class="btn-primary"/>
+ <button string="Cancel" special="cancel" class="btn-secondary"/>
+ </footer>
+ </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"/>
@@ -277,17 +375,22 @@
<field name="groups_id" eval="[(4, ref('indoteknik_custom.group_role_merchandiser'))]"/>
</record>
- <record id="action_sourcing_job_order" model="ir.actions.act_window">
+ <record id="action_sourcing_job_order_md" 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>
+ <field name="groups_id" eval="[(4, ref('indoteknik_custom.group_role_merchandiser'))]"/>
+ </record>
+
+ <record id="action_sourcing_job_order_sales" 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_by_create_uid': 1}</field>
+ <field name="groups_id" eval="[(4, ref('indoteknik_custom.group_role_sales'))]"/>
</record>
<menuitem id="menu_md_root"
@@ -295,9 +398,17 @@
parent="crm.crm_menu_root"
sequence="80"/>
- <menuitem id="menu_sourcing_job_order"
- name="Sourcing Job Order"
+ <menuitem id="menu_sourcing_job_order_md"
+ name="Sourcing Job Orders"
parent="menu_md_root"
- action="action_sourcing_job_order"
+ action="action_sourcing_job_order_md"
+ groups="indoteknik_custom.group_role_merchandiser"
sequence="90"/>
+
+ <menuitem id="menu_sourcing_job_order_sales"
+ name="Sourcing Job Orders"
+ parent="menu_md_root"
+ action="action_sourcing_job_order_sales"
+ groups="indoteknik_custom.group_role_sales"
+ sequence="91"/>
</odoo>