diff options
| author | HafidBuroiroh <hafidburoiroh09@gmail.com> | 2025-11-17 08:39:51 +0700 |
|---|---|---|
| committer | HafidBuroiroh <hafidburoiroh09@gmail.com> | 2025-11-17 08:39:51 +0700 |
| commit | bfd20e54cb88f04ff1a338bdc58984241c8a83a2 (patch) | |
| tree | 6e5fe387144fdce968814650b8f8842ae165a987 | |
| parent | 4f11653e57d4f2e4163b5ef69c0731a675a5e2bd (diff) | |
<hafid> done sjo
| -rwxr-xr-x | indoteknik_custom/models/purchase_pricelist.py | 2 | ||||
| -rw-r--r-- | indoteknik_custom/models/sourcing_job_order.py | 427 | ||||
| -rw-r--r-- | indoteknik_custom/models/unpaid_invoice_view.py | 2 | ||||
| -rwxr-xr-x | indoteknik_custom/security/ir.model.access.csv | 2 | ||||
| -rw-r--r-- | indoteknik_custom/views/sourcing.xml | 141 |
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> |
