diff options
| -rwxr-xr-x | indoteknik_custom/__manifest__.py | 1 | ||||
| -rwxr-xr-x | indoteknik_custom/models/__init__.py | 1 | ||||
| -rw-r--r-- | indoteknik_custom/models/sourcing_job_order.py | 598 | ||||
| -rwxr-xr-x | indoteknik_custom/security/ir.model.access.csv | 2 | ||||
| -rw-r--r-- | indoteknik_custom/views/ir_sequence.xml | 8 | ||||
| -rw-r--r-- | indoteknik_custom/views/sourcing.xml | 250 |
6 files changed, 860 insertions, 0 deletions
diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index 7a179ce3..961e6a94 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -191,6 +191,7 @@ 'views/sj_tele.xml', 'views/close_tempo_mail_template.xml', 'views/domain_apo.xml', + 'views/sourcing.xml' ], 'demo': [], 'css': [], diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index 165dae4e..7f946b57 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -163,3 +163,4 @@ from . import letter_receivable from . import sj_tele from . import partial_delivery from . import domain_apo +from . import sourcing_job_order diff --git a/indoteknik_custom/models/sourcing_job_order.py b/indoteknik_custom/models/sourcing_job_order.py new file mode 100644 index 00000000..98f356de --- /dev/null +++ b/indoteknik_custom/models/sourcing_job_order.py @@ -0,0 +1,598 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import UserError +import logging + +_logger = logging.getLogger(__name__) + + +class SourcingJobOrder(models.Model): + _name = 'sourcing.job.order' + _description = 'Sourcing Job Order MD' + _rec_name = 'name' + _inherit = ['mail.thread', 'mail.activity.mixin'] + + name = fields.Char(string='Job Number', default='New', copy=False, readonly=True) + leads_id = fields.Many2one('crm.lead', string='Leads Number') + user_id = fields.Many2one('res.users', string='MD Person', tracking=True) + state = fields.Selection([ + ('draft', 'Untaken'), + ('taken', 'On Sourcing'), + ('done', 'Complete'), + ('cancel', 'Cancelled') + ], string='Status', default='draft', tracking=True) + approval_sales = fields.Selection([ + ('draft', 'Requested'), + ('approve', 'Approved'), + ], string='Approval Sales', tracking=True) + takeover_request = fields.Many2one( + 'res.users', + string='Takeover Requested By', + readonly=True, + tracking=True, + help='MD yang meminta takeover' + ) + can_request_takeover = fields.Boolean( + compute="_compute_can_request_takeover" + ) + can_approve_takeover = fields.Boolean( + compute="_compute_can_approve_takeover" + ) + + eta_sales = fields.Date(string='Expected Ready') + sla_product = fields.Date(string='Product Ready Date') + cancel_reason = fields.Text(string="Reason for Cancel", tracking=True) + product_assets = fields.Binary(string="Product Assets (PDF)") + + line_ids = fields.One2many('sourcing.job.order.line', 'order_id', string='Products') + + total_amount = fields.Float(string="Total Purchase", compute='_compute_total_amount') + + line_sales_input_ids = fields.One2many( + 'sourcing.job.order.line', 'order_id', + string='Sales Input Lines', + domain=['|', ('price', '=', 0), ('price', '=', False)] + ) + line_md_edit_ids = fields.One2many( + 'sourcing.job.order.line', 'order_id', + string='MD Edit Lines' + ) + line_sales_view_ids = fields.One2many( + 'sourcing.job.order.line', 'order_id', + string='Sales View Lines', + domain=[('state', 'in', ['sourcing', 'done', 'cancel'])] + ) + line_sales_view_cancel_ids = fields.One2many( + 'sourcing.job.order.line', 'order_id', + string='Sales View Lines', + domain=[('state', '=', 'cancel')] + ) + + has_price_in_lines = fields.Boolean( + string='Has Line with Price', + compute='_compute_has_price_in_lines', + ) + 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") + + + @api.depends('line_ids.subtotal') + def _compute_total_amount(self): + for rec in self: + rec.total_amount = sum(line.subtotal for line in rec.line_ids) + + @api.depends('line_ids.price', 'line_ids.vendor_id') + def _compute_has_price_in_lines(self): + for rec in self: + # Cek apakah ada minimal satu line yang sudah punya price > 0 dan vendor_id + has_price = any( + (line.price and line.price > 0 and line.vendor_id) + for line in rec.line_ids + ) + rec.has_price_in_lines = bool(has_price) + + @api.depends('user_id', 'takeover_request') + def _compute_can_request_takeover(self): + for rec in self: + current_user = self.env.user + rec.can_request_takeover = ( + rec.user_id and rec.user_id != current_user + ) + + @api.depends('user_id', 'takeover_request') + def _compute_can_approve_takeover(self): + for rec in self: + current_user = self.env.user + rec.can_approve_takeover = ( + rec.user_id == current_user and bool(rec.takeover_request) + ) + + @api.depends('create_uid', 'user_id') + def _compute_is_creator_same_user(self): + for rec in self: + current_user = self.env.user + rec.is_creator_same_user = ( + rec.create_uid == current_user and + rec.user_id == current_user + ) + + @api.depends('line_md_edit_ids.state') + def _compute_can_convert_to_product(self): + """Cek apakah ada line dengan state 'done'.""" + for rec in self: + rec.can_convert_to_product = any(line.state == 'done' for line in rec.line_md_edit_ids) + + @api.model + def create(self, vals): + """Hanya Sales & Merchandiser yang boleh membuat job.""" + if not (self.env.user.has_group('indoteknik_custom.group_role_sales') or + self.env.user.has_group('indoteknik_custom.group_role_merchandiser')): + raise UserError("❌ Hanya Sales dan Merchandiser yang boleh membuat Sourcing Job.") + + if vals.get('name', 'New') == 'New': + vals['name'] = self.env['ir.sequence'].next_by_code('sourcing.job.order') or 'New' + + if self.env.user.has_group('indoteknik_custom.group_role_merchandiser'): + vals['user_id'] = self.env.user.id + vals['state'] = 'taken' + + rec = super().create(vals) + return rec + + def write(self, vals): + bypass_actions = ( + self.env.context.get('from_action_take', False) + or self.env.context.get('from_multi_action_take', False) + or self.env.context.get('from_action_takeover', False) + ) + + if not ( + self.env.user.has_group('indoteknik_custom.group_role_sales') + or self.env.user.has_group('indoteknik_custom.group_role_merchandiser') + ): + raise UserError("❌ Hanya Sales dan Merchandiser yang boleh mengedit Sourcing Job.") + + for rec in self: + if ( + not rec.user_id + and rec.create_uid != self.env.user + and not vals.get('user_id') + and not bypass_actions + ): + raise UserError("❌ SJO ini belum memiliki MD Person. Tidak dapat melakukan edit.") + + if ( + rec.user_id != self.env.user + and rec.create_uid != self.env.user + and not bypass_actions + ): + raise UserError("❌ Hanya MD Person dan Creator SJO ini yang bisa melakukan Edit.") + + # --- Simpan data lama sebelum write (buat pembanding) + old_data = {} + for rec in self: + old_data[rec.id] = { + 'state': rec.state, + 'user_id': rec.user_id.id if rec.user_id else False, + 'approval_sales': rec.approval_sales, + 'line_data': { + line.id: { + 'state': line.state, + 'vendor_id': line.vendor_id.id if line.vendor_id else False, + 'price': line.price, + } + for line in rec.line_md_edit_ids + }, + } + + res = super().write(vals) + + # --- Bandingkan setelah write dan buat log + for rec in self: + changes = [] + old = old_data.get(rec.id, {}) + + # === Perubahan di field parent === + if old.get('state') != rec.state: + changes.append(f"State: <b>{old.get('state')}</b> → <b>{rec.state}</b>") + if old.get('user_id') != (rec.user_id.id if rec.user_id else False): + changes.append(f"MD Person: <b>{old.get('user_id')}</b> → <b>{rec.user_id.name if rec.user_id else '-'}</b>") + if old.get('approval_sales') != rec.approval_sales: + changes.append(f"Approval Status: <b>{old.get('approval_sales')}</b> → <b>{rec.approval_sales}</b>") + + # === Perubahan di line === + old_lines = old.get('line_data', {}) + for line in rec.line_md_edit_ids: + old_line = old_lines.get(line.id) + if not old_line: + continue # line baru, skip validasi perubahan + + # 💥 Validasi vendor berubah tapi price tidak berubah + if ( + old_line['vendor_id'] != (line.vendor_id.id if line.vendor_id else False) + and old_line['price'] == line.price + ): + raise UserError( + f"⚠️ Harga untuk produk {line.product_name} belum diperbarui setelah mengganti Vendor." + ) + + # Catat perubahan state, vendor, atau harga untuk log note + sub_changes = [] + + if old_line['state'] != line.state: + sub_changes.append(f"- state: <b>{old_line['state']}</b> → <b>{line.state}</b>") + + if old_line['vendor_id'] != (line.vendor_id.id if line.vendor_id else False): + old_vendor = self.env['res.partner'].browse(old_line['vendor_id']).name if old_line['vendor_id'] else '-' + sub_changes.append(f"- vendor: <b>{old_vendor}</b> → <b>{line.vendor_id.name if line.vendor_id else '-'}</b>") + + if old_line['price'] != line.price: + sub_changes.append(f"- price: <b>{old_line['price']}</b> → <b>{line.price}</b>") + + if sub_changes: + joined = "<br/>".join(sub_changes) + changes.append(f"<b>{line.product_name}</b>:<br/>{joined}") + + # Post ke chatter + if changes: + message = "<br/><br/>".join(changes) + rec.message_post( + body=f"Perubahan pada Sourcing Job:<br/>{message}", + subtype_xmlid="mail.mt_comment", + ) + + return res + + + def action_take(self): + context_action = self.env.context.get('from_action_take', False) + for rec in self: + if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'): + raise UserError("❌ Hanya Merchandiser yang dapat mengambil Sourcing Job.") + if rec.state != 'draft': + continue + rec.with_context(from_action_take=True).write({ + 'state': 'taken', + 'user_id': self.env.user.id + }) + rec.message_post(body=("Job <b>%s</b> diambil oleh %s") % (rec.name, self.env.user.name)) + + def action_multi_take(self): + context_action = self.env.context.get('from_multi_action_take', True) + untaken = self.filtered(lambda r: r.state == 'draft') + if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'): + raise UserError("❌ Hanya Merchandiser yang bisa mengambil Sourcing Job.") + if not untaken: + raise UserError("Tidak ada record Untaken untuk diambil.") + + untaken.write({ + 'state': 'taken', + 'user_id': self.env.user.id, + }) + + 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')) + 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}" + ) + 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.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>).", + subtype_xmlid="mail.mt_comment" + ) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'titl': 'Auto Approved', + 'message': f"Sourcing Job '{rec.name}' otomatis disetujui dan diselesaikan.", + 'type': 'success', + 'sticky': False, + } + } + + def action_cancel(self): + for rec in self: + if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'): + raise UserError("❌ Hanya Merchandiser yang dapat mengcancel Sourcing Job.") + + if rec.user_id and rec.user_id != self.env.user: + raise UserError("❌ Hanya MD Person yang memiliki SJO ini yang boleh melakukan Cancel.") + + if not rec.cancel_reason: + raise UserError("⚠️ Isi alasan pembatalan terlebih dahulu.") + + rec.write({'state': 'cancel'}) + rec.message_post(body=("Job <b>%s</b> dibatalkan oleh %s<br/>Alasan: %s") % + (rec.name, self.env.user.name, rec.cancel_reason)) + + def action_request_takeover(self): + context_action = self.env.context.get('from_action_takeover', True) + for rec in self: + if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'): + raise UserError("❌ Hanya Merchandiser yang dapat Request Takeover Sourcing Job.") + + if rec.takeover_request: + raise UserError(f"SJO ini sudah memiliki request takeover dari {rec.takeover_request.name}. Tunggu approval dulu.") + + rec.takeover_request = self.env.user + + activity_type = self.env.ref('mail.mail_activity_data_todo') + rec.activity_schedule( + activity_type_id=activity_type.id, + user_id=rec.user_id.id, + note=f"{self.env.user.name} meminta approval untuk mengambil alih SJO '{rec.name}'.", + ) + + rec.message_post( + body=f"<b>{self.env.user.name}</b> mengirimkan request takeover kepada <b>{rec.user_id.name}</b>.", + subtype_xmlid="mail.mt_comment" + ) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Request Sent', + 'message': f"Request takeover telah dikirim ke {rec.user_id.name}.", + 'type': 'success', + 'sticky': False, + } + } + + def action_approve_takeover(self): + for rec in self: + if self.env.user != rec.user_id: + raise UserError("Hanya MD person yang saat ini memegang SJO yang dapat menyetujui takeover ini.") + if not rec.takeover_request: + raise UserError("Tidak ada request takeover yang perlu disetujui.") + + new_user = rec.takeover_request + + rec.user_id = new_user + rec.takeover_request = False + + activities = self.env['mail.activity'].search([ + ('res_id', '=', rec.id), + ('res_model', '=', 'sourcing.job.order'), + ('user_id', '=', self.env.user.id) + ]) + activities.unlink() + + rec.message_post( + body=f"Takeover disetujui oleh <b>{self.env.user.name}</b>. Sourcing Job berpindah ke <b>{new_user.name}</b>.", + subtype_xmlid="mail.mt_comment" + ) + + def action_reject_takeover(self): + for rec in self: + if self.env.user != rec.user_id: + raise UserError("Hanya MD person yang saat ini memegang SJO yang dapat menolak takeover ini.") + if not rec.takeover_request: + raise UserError("Tidak ada request takeover yang perlu ditolak.") + + requester = rec.takeover_request + rec.takeover_request = False + + activities = self.env['mail.activity'].search([ + ('res_id', '=', rec.id), + ('res_model', '=', 'sourcing.job.order'), + ('user_id', '=', self.env.user.id) + ]) + activities.unlink() + + rec.message_post( + body=f"Takeover dari <b>{requester.name}</b> ditolak oleh <b>{self.env.user.name}</b>.", + subtype_xmlid="mail.mt_comment" + ) + + def action_ask_approval(self): + for rec in self: + if rec.user_id != self.env.user: + raise UserError("❌ Hanya MD Person Sourcing Job ini yang dapat Request Approval.") + invalid_lines = rec.line_md_edit_ids.filtered(lambda l: l.state not in ('done', 'cancel')) + if invalid_lines: + line_names = ', '.join(invalid_lines.mapped('product_name')) + raise UserError( + f"⚠️ Tidak dapat melakukan Request Approval.\n" + f"Masih ada line yang belum selesai disourcing: {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 Request Approval. Semua line pada SJO ini Unavailable.") + + if rec.create_uid == rec.user_id: + rec.approval_sales = 'approve' + rec.state = 'done' + + 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>).", + subtype_xmlid="mail.mt_comment" + ) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Auto Approved', + 'message': f"Sourcing Job '{rec.name}' otomatis disetujui dan diselesaikan.", + 'type': 'success', + 'sticky': False, + } + } + + rec.approval_sales = 'draft' + + activity_type = self.env.ref('mail.mail_activity_data_todo') + rec.activity_schedule( + activity_type_id=activity_type.id, + user_id=rec.create_uid.id, + note=f"{self.env.user.name} meminta approval untuk SJO '{rec.name}'.", + ) + + rec.message_post( + body=f"<b>{self.env.user.name}</b> mengirimkan request approval kepada <b>{rec.create_uid.name}</b>.", + subtype_xmlid="mail.mt_comment" + ) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Request Sent', + 'message': f"Request Approval telah dikirim ke {rec.create_uid.name}.", + 'type': 'success', + 'sticky': False, + } + } + + def action_confirm_approval(self): + for rec in self: + if rec.create_uid != self.env.user: + raise UserError("❌ Hanya Pembuat Sourcing Job ini yang dapat Confirm Approval.") + + rec.approval_sales = 'approve' + rec.state = 'done' + + rec.activity_feedback(['mail.mail_activity_data_todo']) + + rec.message_post( + body=f"Sourcing Job disetujui oleh <b>{self.env.user.name}</b>.", + subtype_xmlid="mail.mt_comment" + ) + + rec.activity_schedule( + 'mail.mail_activity_data_todo', + user_id=rec.user_id.id, + note=f"✅ Sourcing Job <b>{rec.name}</b> telah disetujui oleh {self.env.user.name}.", + ) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Approval Confirmed', + 'message': f"Sourcing Job '{rec.name}' telah disetujui dan dikirim ke {rec.user_id.name}.", + 'type': 'success', + 'sticky': False, + } + } + + +class SourcingJobOrderLine(models.Model): + _name = 'sourcing.job.order.line' + _description = 'Sourcing Job Order Line' + + order_id = fields.Many2one('sourcing.job.order', string='Job Order', ondelete='cascade') + product_name = fields.Char(string='Nama Barang', required=True) + code = fields.Char(string='SKU') + descriptions = fields.Text(string='Deskripsi / Spesifikasi') + reason = fields.Text(string='Reason Unavailable') + sla = fields.Char(string='SLA Product') + quantity = fields.Float(string='Quantity Product', required=True) + price = fields.Float(string='Purchase Price') + tax_id = fields.Many2one('account.tax', string='Tax', domain=[('active', '=', True)]) + vendor_id = fields.Many2one('res.partner', string="Vendor") + product_category = fields.Many2one('product.category', string="Product Category") + product_class = fields.Many2one('product.public.category', string="Categories") + state = fields.Selection([ + ('draft', 'Unsource'), + ('sourcing', 'On Sourcing'), + ('done', 'Done Sourcing'), + ('convert', 'Converted'), + ('cancel', 'Unavailable') + ], default='draft', tracking=True) + product_type = fields.Selection([ + ('consu', 'Consumable'), + ('servis', 'Service'), + ('product', 'Storable Product'), + ], default='product') + subtotal = fields.Float(string='Subtotal', compute='_compute_subtotal') + show_for_sales = fields.Boolean( + string="Show for Sales", + compute="_compute_show_for_sales", + ) + + @api.depends('quantity', 'price', 'tax_id') + def _compute_subtotal(self): + """Menghitung subtotal termasuk pajak.""" + for line in self: + subtotal = (line.quantity or 0.0) * (line.price or 0.0) + if line.tax_id: + subtotal += subtotal * (line.tax_id.amount / 100) + line.subtotal = subtotal + + @api.constrains('product_type', 'product_category', 'product_class', 'code') + def _check_required_fields_for_md(self): + for rec in self: + is_md = self.env.user.has_group('indoteknik_custom.group_role_merchandiser') + if is_md and (not rec.product_type or not rec.product_category or not rec.product_class or not rec.code): + raise UserError("MD wajib mengisi SKU, Product Type, Product Category, dan Categories!") + + @api.depends('state') + def _check_unavailable_line(self): + for rec in self: + if rec.state == 'cancel' and not rec.reason: + raise UserError("Isi Reason Unavailable") + + @api.depends('price', 'vendor_id', 'order_id') + def _compute_show_for_sales(self): + for rec in self: + rec.show_for_sales = bool( + rec.order_id and rec.price not in (None, 0) and rec.vendor_id + ) + + def action_convert_to_product(self): + type_map = { + 'servis': 'service', + 'product': 'product', + 'consu': 'consu', + } + + for rec in self: + if rec.order_id.user_id != self.env.user: + raise UserError("❌ Hanya MD Person SJO ini yang dapat convert Line ini ke product.") + exsisting = self.env['product.product'].search([('default_code', '=', rec.code)], limit=1) + + if exsisting: + raise UserError(f"⚠️ Produk dengan Internal Reference '{rec.code}' sudah ada di sistem.") + + product = self.env['product.product'].create({ + 'name': rec.product_name, + 'default_code': rec.code or False, + 'description': rec.descriptions or '', + 'categ_id': rec.product_category.id, + 'type': type_map.get(rec.product_type, 'product'), + }) + + rec.state = 'convert' + return True + + @api.onchange('code') + def _oncange_code(self): + for rec in self: + if not rec.code: + continue + + product = self.env['product.product'].search([('default_code', '=', rec.code)], limit=1) + if not product: + return + + rec.product_name = product.name or rec.product_name + + pricelist = self.env['purchase.pricelist'].search([('product_id', '=', product.id), ('is_winner', '=', True)], limit=1) + if pricelist or tax or vendor: + 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 diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index c01271d3..bbcbac84 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -210,3 +210,5 @@ access_unpaid_invoice_view,access.unpaid.invoice.view,model_unpaid_invoice_view, access_surat_piutang_user,surat.piutang user,model_surat_piutang,,1,1,1,1 access_surat_piutang_line_user,surat.piutang.line user,model_surat_piutang_line,,1,1,1,1 access_sj_tele,access.sj.tele,model_sj_tele,base.group_system,1,1,1,1 +access_sourcing_job_order,access.sourcing_job_order,model_sourcing_job_order,base.group_system,1,1,1,1 +access_sourcing_job_order_line_user,sourcing.job.order.line,model_sourcing_job_order_line,base.group_user,1,1,1,1 diff --git a/indoteknik_custom/views/ir_sequence.xml b/indoteknik_custom/views/ir_sequence.xml index dbdbc3c0..543f1fd1 100644 --- a/indoteknik_custom/views/ir_sequence.xml +++ b/indoteknik_custom/views/ir_sequence.xml @@ -218,6 +218,14 @@ <field name="number_next">1</field> <field name="number_increment">1</field> </record> + <record id="seq_sourcing_job_order" model="ir.sequence"> + <field name="name">Sourcing Job Order</field> + <field name="code">sourcing.job.order</field> + <field name="prefix">SJO/%(year)s/</field> + <field name="padding">4</field> + <field name="number_next_actual">1</field> + <field name="number_increment">1</field> + </record> <record id="sequence_advance_payment_request" model="ir.sequence"> <field name="name">Advance Payment Request Sequence</field> diff --git a/indoteknik_custom/views/sourcing.xml b/indoteknik_custom/views/sourcing.xml new file mode 100644 index 00000000..3965c62f --- /dev/null +++ b/indoteknik_custom/views/sourcing.xml @@ -0,0 +1,250 @@ +<?xml version="1.0" encoding="UTF-8"?> +<odoo> + <record id="view_sourcing_job_order_search" model="ir.ui.view"> + <field name="name">sourcing.job.order.search</field> + <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="user_id" string="MD Person"/> + <filter name="my_job" + string="My Sourcing Job" + domain="[('user_id', '=', uid), ('state', '=', 'taken')]"/> + <filter name="untaken" + string="Untaken" + domain="[('state', '=', 'draft')]" /> + <filter name="done" + string="Complete" + domain="[('state', '=', 'done')]" /> + </search> + </field> + </record> + <record id="view_sourcing_job_order_tree" model="ir.ui.view"> + <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" + decoration-success="state=='done'" + decoration-danger="state=='cancel'" + decoration-warning="state=='taken'"> + <field name="name"/> + <!-- <field name="leads_id"/> --> + <field name="user_id" widget="many2one_avatar_user"/> + <field name="state" widget="badge"/> + <field name="create_date"/> + </tree> + </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> + <field name="arch" type="xml"> + <form string="Sourcing Job Order"> + <header> + <button name="action_take" + string="Take" + type="object" + class="btn-primary" + icon="fa-hand-paper-o" + groups="indoteknik_custom.group_role_merchandiser" + attrs="{'invisible': [('state', '!=', 'draft')]}"/> + + <button name="action_cancel" + string="Cancel" + type="object" + class="btn-secondary" + icon="fa-times" + groups="indoteknik_custom.group_role_merchandiser" + attrs="{'invisible': [('state', 'in', ['cancel', 'done'])]}"/> + + <button name="action_ask_approval" + string="Ask Approval" + type="object" + class="btn-primary" + groups="indoteknik_custom.group_role_merchandiser" + attrs="{'invisible': ['|','|', ('has_price_in_lines', '=', False), ('approval_sales', '=', 'approve'), ('is_creator_same_user', '=', True)]}"/> + + <button name="action_confirm_by_md" + string="Confirm" + type="object" + class="btn-primary" + groups="indoteknik_custom.group_role_merchandiser" + attrs="{'invisible': [('is_creator_same_user', '=', False)]}"/> + + <button name="action_confirm_approval" + string="Approve" + type="object" + class="btn-primary" + groups="indoteknik_custom.group_role_sales" + attrs="{'invisible': [('approval_sales', 'in', [False, 'approve'])]}"/> + + <button name="action_request_takeover" + string="Request Takeover" + type="object" + class="btn-primary" + groups="indoteknik_custom.group_role_merchandiser" + attrs="{'invisible': [('can_request_takeover', '=', False)]}"/> + + <button name="action_approve_takeover" + string="Approve Takeover" + type="object" + class="btn-success" + groups="indoteknik_custom.group_role_merchandiser" + attrs="{'invisible': [('can_approve_takeover', '=', False)]}"/> + + <button name="action_reject_takeover" + string="Reject Takeover" + type="object" + class="btn-danger" + groups="indoteknik_custom.group_role_merchandiser" + attrs="{'invisible': [('can_approve_takeover', '=', False)]}"/> + + <field name="state" widget="statusbar" + statusbar_visible="draft,taken,done,cancel" + statusbar_color='{"draft": "blue", "taken": "orange", "done": "green", "cancel": "red"}'/> + </header> + + <sheet> + <widget name="web_ribbon" + title="COMPLETE" + bg_color="bg-success" + attrs="{'invisible': [('state', '!=', 'done')]}"/> + + <widget name="web_ribbon" + title="CANCEL" + bg_color="bg-danger" + attrs="{'invisible': [('state', '!=', 'cancel')]}"/> + <h1> + <field name="name" readonly="1"/> + </h1> + + <group> + <group> + <!-- <field name="leads_id"/> --> + <field name="eta_sales"/> + <field name="is_creator_same_user" invisible="1"/> + <field name="takeover_request" invisible="1"/> + <field name="can_request_takeover" invisible="1"/> + <field name="can_approve_takeover" invisible="1"/> + <field name="has_price_in_lines" invisible="1"/> + <field name="sla_product" groups="indoteknik_custom.group_role_merchandiser"/> + </group> + <group> + <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"/> + </group> + </group> + + <notebook> + <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"> + <field name="product_name"/> + <field name="descriptions"/> + <field name="quantity"/> + </tree> + </field> + </page> + + <!-- MD EDIT --> + <page string="MD Lines" groups="indoteknik_custom.group_role_merchandiser"> + <field name="line_md_edit_ids"> + <tree editable="bottom" + decoration-success="state in ('done', 'convert')" + decoration-danger="state=='cancel'" + decoration-warning="state=='sourcing'"> + <field name="code"/> + <field name="product_name"/> + <field name="descriptions"/> + <field name="quantity"/> + <field name="price"/> + <field name="vendor_id"/> + <field name="tax_id"/> + <field name="subtotal"/> + <field name="product_category"/> + <field name="product_type"/> + <field name="product_class"/> + <field name="state"/> + <field name="reason" attrs="{'invisible': [('state', '!=', 'cancel')]}"/> + <button name="action_convert_to_product" + string="Convert" + type="object" + icon="fa-exchange" + attrs="{'invisible': [('state', '!=', 'done')]}"/> + </tree> + </field> + </page> + + <!-- SALES VIEW --> + <page string="Product Line" groups="indoteknik_custom.group_role_sales" attrs="{'invisible': [('has_price_in_lines', '=', False)]}"> + <field name="line_sales_view_ids"> + <tree> + <field name="code" readonly="1"/> + <field name="product_name" readonly="1"/> + <field name="descriptions" readonly="1"/> + <field name="quantity" readonly="1"/> + <field name="price" readonly="1"/> + <field name="vendor_id" readonly="1"/> + <field name="tax_id" readonly="1"/> + <field name="subtotal" readonly="1"/> + <field name="state" readonly="1"/> + </tree> + </field> + </page> + + <page string="Documents"> + <field name="product_assets" widget="pdf_viewer"/> + </page> + + <page string="Cancel Reason" attrs="{'invisible': [('state', 'in', ['done'])]}"> + <group> + <field name="cancel_reason"/> + </group> + </page> + </notebook> + </sheet> + <div class="oe_chatter"> + <field name="message_follower_ids" widget="mail_followers"/> + <field name="message_ids" widget="mail_thread"/> + <field name="activity_ids" widget="mail_activity"/> + </div> + </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"/> + <field name="binding_model_id" ref="model_sourcing_job_order"/> + <field name="binding_type">action</field> + <field name="state">code</field> + <field name="code">action = records.action_multi_take()</field> + <field name="groups_id" eval="[(4, ref('indoteknik_custom.group_role_merchandiser'))]"/> + </record> + + <record id="action_sourcing_job_order" 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> + </record> + + <menuitem id="menu_md_root" + name="MD" + parent="crm.crm_menu_root" + sequence="80"/> + + <menuitem id="menu_sourcing_job_order" + name="Sourcing Job Order" + parent="menu_md_root" + action="action_sourcing_job_order" + sequence="90"/> +</odoo> |
