diff options
| author | HafidBuroiroh <hafidburoiroh09@gmail.com> | 2026-02-28 09:56:26 +0700 |
|---|---|---|
| committer | HafidBuroiroh <hafidburoiroh09@gmail.com> | 2026-02-28 09:56:26 +0700 |
| commit | 8d953f913aceb97faa026253b65d6159759f5a62 (patch) | |
| tree | 0ff82faa484cae7c87f75df556966e0da49ee088 | |
| parent | 7bfc92fdb73a89c5bc0b4c711315cbd5ea3ff268 (diff) | |
| parent | 7cd31c8dab49c59f8c6e67528d528514cc13932c (diff) | |
<hafid> change request sourcing job order
30 files changed, 1394 insertions, 75 deletions
diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py index cff1921d..f4a2a9d4 100644 --- a/indoteknik_api/controllers/api_v1/sale_order.py +++ b/indoteknik_api/controllers/api_v1/sale_order.py @@ -154,13 +154,13 @@ class SaleOrder(controller.Controller): elif status == 'belum_bayar': domain += [ ('state', '=', 'draft'), - ('approval_status', 'in', ['pengajuan1', 'pengajuan2']), + ('approval_status', 'in', ['pengajuan0','pengajuan1', 'pengajuan2']), ('payment_status', 'in', [False, None, '', 'pending', 'expire']) ] elif status == 'diproses': domain += [ ('state', '=', 'draft'), - ('approval_status', 'in', ['pengajuan1', 'pengajuan2']), + ('approval_status', 'in', ['pengajuan0','pengajuan1', 'pengajuan2']), ('payment_status', '!=', False), ('payment_status', 'not in', ['', 'pending', 'expire']), ] diff --git a/indoteknik_api/models/sale_order.py b/indoteknik_api/models/sale_order.py index 23be358a..7951de45 100644 --- a/indoteknik_api/models/sale_order.py +++ b/indoteknik_api/models/sale_order.py @@ -66,7 +66,7 @@ class SaleOrder(models.Model): elif sale_order.state == 'draft': if not sale_order.approval_status: data['status'] = 'draft' - elif sale_order.approval_status in ('pengajuan1', 'pengajuan2'): + elif sale_order.approval_status in ('pengajuan0', 'pengajuan1', 'pengajuan2'): if sale_order.payment_status in ('', 'pending', False, None, 'expire'): data['status'] = 'belum_bayar' elif sale_order.payment_status not in ['', 'pending', False, None, 'expire']: diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index 66cbd995..9458cc41 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -8,7 +8,7 @@ 'author': 'Rafi Zadanly', 'website': '', 'images': ['assets/favicon.ico'], - 'depends': ['base', 'coupon', 'delivery', 'sale', 'sale_management', 'vit_kelurahan', 'vit_efaktur'], + 'depends': ['base', 'coupon', 'delivery', 'sale', 'sale_management', 'vit_kelurahan', 'vit_efaktur', 'proweb_kartu_stok'], 'data': [ 'views/assets.xml', 'security/ir.model.access.csv', @@ -193,6 +193,9 @@ 'views/commission_internal.xml', 'views/keywords.xml', 'views/sourcing.xml' + 'views/token_log.xml', + 'views/gudang_service.xml', + 'views/kartu_stock.xml', ], 'demo': [], 'css': [], diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index 9d9c0ec5..fb93e0a3 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -165,6 +165,9 @@ from . import partial_delivery from . import domain_apo from . import uom_uom from . import commission_internal +from . import gudang_service from . import update_depreciation_move_wizard from . import keywords from . import sourcing_job_order +from . import token_log +from . import kartu_stock diff --git a/indoteknik_custom/models/advance_payment_request.py b/indoteknik_custom/models/advance_payment_request.py index 8cadb1b6..039d18a5 100644 --- a/indoteknik_custom/models/advance_payment_request.py +++ b/indoteknik_custom/models/advance_payment_request.py @@ -155,6 +155,18 @@ class AdvancePaymentRequest(models.Model): compute='_compute_is_current_user_ap' ) + estimate_line_ids = fields.One2many('advance.payment.request.estimate.line', 'request_id', string='Rincian Estimasi') + + @api.constrains('nominal', 'estimate_line_ids') + def _check_nominal_vs_estimate_total(self): + for rec in self: + if rec.type_request == 'pum': + if not rec.estimate_line_ids: + raise UserError("Rincian estimasi wajib diisi untuk PUM. Silakan tambahkan rincian estimasi.") + total_estimate = sum(line.nominal for line in rec.estimate_line_ids) + if round(total_estimate, 2) != round(rec.nominal, 2): + raise UserError("Total estimasi harus sama dengan nominal PUM. Silakan sesuaikan rincian estimasi atau nominal PUM.") + @api.onchange('grand_total_reimburse', 'type_request') def _onchange_reimburse_line_update_nominal(self): if self.type_request == 'reimburse': @@ -751,7 +763,7 @@ class AdvancePaymentRequest(models.Model): pum_ids = self.search([ ('user_id', '=', user.id), - ('status', '!=', 'reject'), + ('status', '!=', 'cancel'), ('type_request', '=', 'pum') ]) @@ -911,6 +923,35 @@ class AdvancePaymentUsageLine(models.Model): compute='_compute_is_current_user_ap' ) + category_usage = fields.Selection([ + ('parkir', 'Parkir'), + ('tol', 'Tol'), + ('bbm', 'BBM'), + ('kuli', 'Kuli'), + ('konsumsi', 'Konsumsi'), + ('lain_lain', 'Lain-lain'), + ], string='Kategori System', compute='_compute_category_usage') + + @api.depends('account_id') + def _compute_category_usage(self): + for rec in self: + if not rec.account_id: + rec.category_usage = False + continue + name = rec.account_id.name.lower() + if 'bbm' in name or 'bahan bakar' in name: + rec.category_usage = 'bbm' + elif 'tol' in name: + rec.category_usage = 'tol' + elif 'parkir' in name: + rec.category_usage = 'parkir' + elif 'kuli' in name or 'bongkar' in name: + rec.category_usage = 'kuli' + elif 'konsumsi' in name or 'makan' in name or 'minum' in name: + rec.category_usage = 'konsumsi' + else: + rec.category_usage = 'lain_lain' + def _compute_is_current_user_ap(self): ap_user_ids = [23, 9468, 16729] is_ap = self.env.user.id in ap_user_ids @@ -1144,6 +1185,11 @@ class AdvancePaymentSettlement(models.Model): string="Is Current User AP", compute='_compute_is_current_user_ap' ) + pum_estimate_line_ids = fields.One2many( + related='pum_id.estimate_line_ids', + string='Rincian Estimasi PUM', + readonly=True + ) def _compute_is_current_user_ap(self): ap_user_ids = [23, 9468, 16729] @@ -1612,4 +1658,50 @@ class CreateReimburseCabWizard(models.TransientModel): 'type': 'ir.actions.act_window', 'res_id': move.id, 'target': 'current', - }
\ No newline at end of file + } + +class AdvancePaymentRequestEstimateLine(models.Model): + _name = 'advance.payment.request.estimate.line' + _description = 'Advance Payment Request Estimate Line' + + request_id = fields.Many2one('advance.payment.request', string='Request') + category_estimate = fields.Selection([ + ('parkir', 'Parkir'), + ('tol', 'Tol'), + ('bbm', 'BBM'), + ('kuli', 'Kuli'), + ('konsumsi', 'Konsumsi'), + ('lain_lain', 'Lain-lain'), + ], string='Kategori Estimasi', required=True) + description = fields.Text(string='Description', help='Deskripsi tambahan untuk estimasi biaya yang diperlukan.') + nominal = fields.Float(string='Nominal Estimasi', required=True, help='Masukkan nominal estimasi untuk kategori ini (tidak mesti akurat, hanya untuk gambaran umum).') + currency_id = fields.Many2one(related='request_id.currency_id') + + total_actual = fields.Float(string='Nominal Realisasi', compute='_compute_actual_data') + frequency = fields.Integer(string='Qty Realisasi', compute='_compute_actual_data') + + @api.depends('request_id.settlement_ids.penggunaan_line_ids.nominal', + 'request_id.settlement_ids.penggunaan_line_ids.account_id') + def _compute_actual_data(self): + for rec in self: + total_act = 0 + freq = 0 + if rec.request_id and rec.request_id.settlement_ids: + all_usage_lines = rec.request_id.settlement_ids.mapped('penggunaan_line_ids') + valid_lines = all_usage_lines.filtered(lambda l: l.account_id and l.category_usage) + planned_category = rec.request_id.estimate_line_ids.mapped('category_estimate') + if rec.category_estimate == 'lain_lain': + matched_lines = valid_lines.filtered( + lambda l: l.category_usage == 'lain_lain' or \ + l.category_usage not in planned_category + ) + else: + matched_lines = valid_lines.filtered( + lambda l: l.category_usage == rec.category_estimate + ) + + total_act = sum(matched_lines.mapped('nominal')) + freq = len(matched_lines) + rec.total_actual = total_act + rec.frequency = freq + diff --git a/indoteknik_custom/models/automatic_purchase.py b/indoteknik_custom/models/automatic_purchase.py index 0b2f7d1b..f7c0d75e 100644 --- a/indoteknik_custom/models/automatic_purchase.py +++ b/indoteknik_custom/models/automatic_purchase.py @@ -314,7 +314,74 @@ class AutomaticPurchase(models.Model): sale_ids_set = set() sale_ids_name = set() + retur_cache = {} + incoming_cache = {} for sale_order in matches_so: + exist = self.env['purchase.order.sales.match'].search([ + ('product_id', '=', sale_order.product_id.id), + ('sale_line_id', '=', sale_order.sale_line_id.id), + ('sale_id', '=', sale_order.sale_id.id), + ('purchase_order_id.state', '!=', 'cancel'), + ]) + + skip_line = False + + sale_line_id = sale_order.sale_line_id.id + + if sale_line_id not in incoming_cache: + + qty_incoming = 0 + + for existing in exist: + if existing.purchase_order_id.state in ['done', 'purchase']: + + incoming_moves = self.env['stock.move'].search([ + ('reference', 'ilike', 'BU/INPUT'), + ('state', 'not in', ['done','cancel']), + ('product_id', '=', existing.product_id.id), + ('purchase_line_id', '=', existing.purchase_line_id.id), + ]) + + qty_incoming += sum(incoming_moves.mapped('product_uom_qty')) + + incoming_cache[sale_line_id] = qty_incoming + + + qty_need = sale_order.sale_line_id.product_uom_qty + + if incoming_cache[sale_line_id] >= qty_need: + skip_line = True + + sale_line_id = sale_order.sale_line_id.id + + if sale_line_id not in retur_cache: + + fully_received = True + + for existing in exist: + if existing.purchase_order_id.state in ['done', 'purchase']: + + if existing.purchase_line_id.qty_received != existing.purchase_line_id.product_qty: + fully_received = False + break + + retur_cache[sale_line_id] = fully_received + + + if retur_cache[sale_line_id] and exist: + skip_line = True + + if skip_line: + continue + + stock_move = self.env['stock.move'].search([ + ('reference', 'ilike', 'BU/PICK'), + ('state', 'in', ['confirmed','waiting','partially_available']), + ('product_id', '=', sale_order.product_id.id), + ('sale_line_id', '=', sale_order.sale_line_id.id), + ]) + if not stock_move: + continue # @stephan skip so line yang sudah pernah ada di purchase order sales match sebelumnya salesperson_name = sale_order.sale_id.user_id.name @@ -740,11 +807,5 @@ class SaleNotInMatchPO(models.Model): apsm.create_date, apsm.write_uid, apsm.write_date, apsm.purchase_price, apsm.purchase_tax_id, apsm.note_procurement from automatic_purchase_sales_match apsm - where apsm.sale_line_id not in ( - select distinct coalesce(posm.sale_line_id,0) - from purchase_order_sales_match posm - join purchase_order po on po.id = posm.purchase_order_id - where po.state not in ('cancel') - ) ) """ % self._table)
\ No newline at end of file diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py new file mode 100644 index 00000000..d699ccf4 --- /dev/null +++ b/indoteknik_custom/models/gudang_service.py @@ -0,0 +1,273 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import UserError, ValidationError +import logging +from datetime import datetime +from collections import defaultdict + + +class GudangService(models.Model): + _name = "gudang.service" + _description = "Gudang Service" + _inherit = ["mail.thread", "mail.activity.mixin"] + _order = "id asc" + + name = fields.Char("Name", readonly=True) + partner_id = fields.Many2one("res.partner", string="Customer", readonly=True) + vendor_id = fields.Many2one("res.partner", string="Vendor Service", required=True) + origin = fields.Many2one( + "sale.order", + string="Origin SO", + required=True, + domain=[("state", "in", ["done", "sale"])], + ) + schedule_date = fields.Date(string="Schedule Date", required=True, tracking=True) + start_date = fields.Datetime(string="Date Processed", copy=False, tracking=True) + create_date = fields.Datetime( + string="Create Date", copy=False, tracking=True, default=fields.Datetime.now() + ) + done_date = fields.Datetime(string="Date Done", copy=False, tracking=True) + gudang_service_lines = fields.One2many( + "gudang.service.line", "gudang_service_id", string="Gudang Service Lines" + ) + # unprocessed_date = fields.Char( + # string='Unprocessed Since', + # compute='_compute_unprocessed_date' + # ) + remaining_date = fields.Char( + compute="_compute_remaining_date", string="Date Status" + ) + state = fields.Selection( + [ + ("draft", "Backlog"), + ("received_from_cust", "Received From Customer"), + ("sent_to_vendor", "Sent to Service Vendor"), + ("received_from_vendor", "Received From Service Vendor"), + ("delivered_to_cust", "Delivered to Customer"), + ("cancel", "Cancel"), + ], + default="draft", + tracking=True, + ) + cancel_reason = fields.Text("Cancel Reason", tracking=True) + + def check_duplicate_docs(self): + for rec in self: + found = self.env["gudang.service"].search( + [ + ("id", "!=", self.id), + ("origin.id", "=", self.origin.id), + ("partner_id.id", "=", rec.partner_id.id), + ("vendor_id.id", "=", rec.vendor_id.id), + ("schedule_date", "=", rec.schedule_date), + ( + "gudang_service_lines.product_id.name", + "=", + rec.gudang_service_lines.product_id.name, + ), + ( + "gudang_service_lines.quantity", + "=", + rec.gudang_service_lines.quantity, + ), + ] + ) + if found: + raise UserError("This Document has duplicate with %s" % found.name) + + def _send_logistic_notification(self): + group = self.env.ref( + "indoteknik_custom.group_role_logistic", raise_if_not_found=False + ) + if not group: + return + + users = group.users + # MD + md = self.env["res.users"].browse([3425, 4801, 1036]) + # send to logistic and MD + users = users | md + + if not users: + return + + # Logistic users to be excluded + excluded_users = [7, 17098, 216, 28, 15710] + + for rec in self: + for user in users: + if user.id in excluded_users: + continue + self.env["mail.activity"].create( + { + "res_model_id": self.env["ir.model"]._get_id("gudang.service"), + "res_id": rec.id, + "activity_type_id": self.env.ref( + "mail.mail_activity_data_todo" + ).id, + "user_id": user.id, + "summary": "Gudang Service On Progress", + "note": _( + "Ada Jadwal Service Barang di Document <b>%s</b> Jadwal Service 📅 <b>%s</b>" + ) + % (rec.name, rec.schedule_date), + # 'date_deadline': fields.Date.today(), + } + ) + + # kirim ke private message odoo + channel = ( + self.env["mail.channel"] + .channel_get([self.env.user.partner_id.id, user.partner_id.id]) + .get("id") + ) + if not channel: + continue + res = self.env["mail.channel"].browse(channel) + res.with_user(self.env.user.browse(25)).message_post( + body=_( + "Ada Jadwal Service Barang di Document <b>%s</b> Jadwal Service 📅 <b>%s</b>" + ) + % (rec.name, rec.schedule_date), + message_type="comment", + subtype_xmlid="mail.mt_comment", + ) + + @api.model + def cron_notify_onprogress_gudang_service(self): + records = self.search( + [ + ("state", "in", ["draft", "received_from_customer"]), + ] + ) + + if records: + records._send_logistic_notification() + + @api.depends("schedule_date", "create_date") + def _compute_remaining_date(self): + today = fields.Date.today() + + for rec in self: + if not rec.schedule_date: + rec.remaining_date = "-" + continue + + base_date = rec.create_date.date() if rec.create_date else today + + schedule = rec.schedule_date + days = (schedule - base_date).days + + if days > 0: + rec.remaining_date = _("In %s days") % days + elif days == 0: + rec.remaining_date = _("Today") + else: + rec.remaining_date = _("Overdue %s days") % abs(days) + + def action_submit(self): + self.ensure_one() + self.check_duplicate_docs() + for rec in self: + if rec.state == "draft": + rec.state = "received_from_cust" + elif rec.state == "received_from_cust": + rec.state = "sent_to_vendor" + rec.start_date = fields.Datetime.now() + elif rec.state == "sent_to_vendor": + rec.state = "received_from_vendor" + + def action_done(self): + for rec in self: + if rec.state != "received_from_vendor": + raise UserError("Only 'Received From Vendor' state can be set to Done") + + rec.activity_ids.unlink() + + rec.write( + {"state": "delivered_to_cust", "done_date": fields.Datetime.now()} + ) + + def action_draft(self): + """Reset to draft state""" + for rec in self: + rec.cancel_reason = False + if rec.state == "cancel": + rec.write({"state": "draft"}) + else: + raise UserError("Only Canceled Record Can Be Reset To Draft") + + def action_cancel(self): + for rec in self: + activities = self.env["mail.activity"].search( + [ + ("res_id", "=", rec.id), + ("res_model", "=", "gudang.service"), + ] + ) + activities.unlink() + if rec.state == "delivered_to_cust": + raise UserError("You cannot cancel a done record") + if not rec.cancel_reason: + raise UserError("Cancel Reason must be filled") + rec.start_date = False + rec.done_date = False + rec.state = "cancel" + + @api.model + def create(self, vals): + # sequence + if not vals.get("name") or vals["name"] == "New": + vals["name"] = self.env["ir.sequence"].next_by_code("gudang.service") + + # partner dari SO + so = self.env["sale.order"].browse(vals["origin"]) + vals["partner_id"] = so.partner_id.id + + res = super(GudangService, self).create(vals) + + res.check_duplicate_docs() + res._send_logistic_notification() + return res + + def write(self, vals): + if vals.get("origin"): + so = self.env["sale.order"].browse(vals["origin"]) + vals["partner_id"] = so.partner_id.id + vals.check_duplicate_docs() + + return super(GudangService, self).write(vals) + + @api.onchange("origin") + def _onchange_origin(self): + if not self.origin: + self.gudang_service_lines = [(5, 0, 0)] + return + + self.partner_id = self.origin.partner_id + + lines = [] + for line in self.origin.order_line: + lines.append( + ( + 0, + 0, + { + "product_id": line.product_id.id, + "quantity": line.product_uom_qty, + "origin_so": self.origin.id, + }, + ) + ) + + # hapus line lama lalu isi baru + self.gudang_service_lines = [(5, 0, 0)] + lines + + +class GudangServiceLine(models.Model): + _name = "gudang.service.line" + _inherit = ["mail.thread", "mail.activity.mixin"] + + product_id = fields.Many2one("product.product", string="Product") + quantity = fields.Float(string="Quantity") + origin_so = fields.Many2one("sale.order", string="Origin SO") + gudang_service_id = fields.Many2one("gudang.service", string="Gudang Service ID") diff --git a/indoteknik_custom/models/kartu_stock.py b/indoteknik_custom/models/kartu_stock.py new file mode 100644 index 00000000..22f90df0 --- /dev/null +++ b/indoteknik_custom/models/kartu_stock.py @@ -0,0 +1,186 @@ +from odoo import models +from io import BytesIO +import datetime +from base64 import encodebytes +import xlsxwriter + + +class KartuStokWizardInherit(models.TransientModel): + _inherit = 'kartu.stok.wizard' + + + def action_kartu_stok_excel_single_sheet(self): + + active_ids_tmp = self.env.context.get('active_ids') + active_model = self.env.context.get('active_model') + + if active_model == 'product.template': + active_ids = self.env['product.product'].search( + [('product_tmpl_id', 'in', active_ids_tmp), + ('active', '=', True)]).ids + else: + active_ids = active_ids_tmp + + data = { + 'location_id': self.location_id.id, + 'day_date': self.day_date, + 'previous_number_days': self.previous_number_days, + 'date_from': self.date_from, + 'date_to': self.date_to, + 'ids': active_ids, + 'context': {'active_model': active_model} + } + + file_io = BytesIO() + workbook = xlsxwriter.Workbook(file_io) + + self.generate_xlsx_single_sheet(workbook, data) + + workbook.close() + + fout = encodebytes(file_io.getvalue()) + datetime_string = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f'Kartu_Stok_Single_{datetime_string}.xlsx' + + self.write({ + 'fileout': fout, + 'fileout_filename': filename + }) + + file_io.close() + + return { + 'type': 'ir.actions.act_url', + 'target': 'new', + 'url': 'web/content/?model=' + self._name + + '&id=' + str(self.id) + + '&field=fileout&download=true&filename=' + filename, + } + + def generate_xlsx_single_sheet(self, workbook, data): + + bold = workbook.add_format({'bold': True}) + border_date_right = workbook.add_format({'border':1, 'num_format': 'DD-MM', 'bg_color': '#dddddd', 'align': 'right'}) + border_int_right = workbook.add_format({'border':1, 'num_format': '#,##0', 'bg_color': '#dddddd', 'align': 'right'}) + border_int_right_bold = workbook.add_format({'border':1, 'num_format': '#,##0', 'bg_color': '#dddddd', 'align': 'right', 'bold': True}) + border_text_center = workbook.add_format({'border':1, 'bg_color': '#dddddd', 'align': 'center'}) + header_format = workbook.add_format({'bold': True, 'border':1, 'bg_color': '#808080', 'align': 'center'}) + + sheet = workbook.add_worksheet('Kartu Stok') + + docs = self.env['product.product'].browse(data['ids']) + location = self.env['stock.location'].browse(data['location_id']) + location_name = location.display_name.split('/')[0] + + row = 0 + + for doc in docs: + + # ========================= + # HEADER PRODUCT + # ========================= + sheet.write(row, 0, doc.display_name, bold) + row += 1 + sheet.write(row, 0, location_name, bold) + row += 2 + + # ========================= + # TABLE HEADER + # ========================= + sheet.write(row, 0, 'Date', header_format) + sheet.write(row, 1, 'In', header_format) + sheet.write(row, 2, 'Out', header_format) + sheet.write(row, 3, 'Stock', header_format) + sheet.write(row, 4, 'Distributor', header_format) + sheet.write(row, 5, 'Buyer', header_format) + sheet.write(row, 6, 'Document', header_format) + sheet.write(row, 7, 'Source Document', header_format) + row += 1 + + stock_total = 0 + stock_show_initial = False + + # ========================= + # MOVE LOOP (SAMA LOGIC ASLI) + # ========================= + for move in doc.stock_move_ids.sorted(key=lambda sm: sm.date): + for line in move.move_line_ids: + + if line.state != 'done': + continue + + if line.location_id.id != data['location_id'] and \ + line.location_dest_id.id != data['location_id']: + continue + + if not stock_show_initial: + sheet.write(row, 3, stock_total, border_int_right_bold) + sheet.write(row, 4, 'Initial Stock', border_text_center) + stock_show_initial = True + row += 1 + + qty_in = 0 + qty_out = 0 + + if line.location_dest_id.id == data['location_id']: + qty_in = line.qty_done + stock_total += qty_in + + if line.location_id.id == data['location_id']: + qty_out = line.qty_done + stock_total -= qty_out + + sheet.write(row, 0, line.date, border_date_right) + sheet.write(row, 1, qty_in, border_int_right) + sheet.write(row, 2, qty_out, border_int_right) + sheet.write(row, 3, stock_total, border_int_right) + # Distributor + col = 4 + if line.location_dest_id.id == data['location_id']: + if line.picking_id and line.picking_id.origin: + sheet.write(row, col, line.picking_id.partner_id.display_name, border_text_center) + else: + if line.location_id: + if line.location_id.name == 'Inventory adjustment': + sheet.write(row, col, 'Adjust *', border_text_center) + else: + sheet.write(row, col, line.location_id.location_id.name + ' *', border_text_center) + else: + sheet.write(row, col, doc.seller_ids[0].name + ' *' if doc.seller_ids else '', border_text_center) + else: + sheet.write(row, col, '', border_text_center) + # Buyer + col = 5 + if line.location_id.id == data['location_id']: + if line.picking_id and line.picking_id.origin: + sheet.write(row, col, line.picking_id.partner_id.display_name, border_text_center) + else: + if line.location_dest_id: + if line.location_dest_id.name == 'Inventory adjustment': + sheet.write(row, col, 'Adjust *', border_text_center) + else: + sheet.write(row, col, line.location_dest_id.location_id.name + ' *', border_text_center) + else: + sheet.write(row, col, doc.seller_ids[0].name + ' *' if doc.seller_ids else '', border_text_center) + else: + sheet.write(row, col, '', border_text_center) + # Document + col = 6 + if line.picking_id and line.picking_id.origin: + sheet.write(row, col, line.picking_id.name, border_text_center) + else: + sheet.write(row, col, line.reference or '', border_text_center) + # Source Document + col = 7 + if line.picking_id and line.picking_id.origin: + sheet.write(row, col, line.picking_id.origin, border_text_center) + else: + sheet.write(row, col, line.reference or '', border_text_center) + + row += 1 + + row += 3 # jarak antar product + + + + diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 4ab649dc..3fa9dd72 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -133,7 +133,7 @@ class Keywords(models.Model): def _onchange_solr_flag(self): """Set solr_flag=2 when tracked fields change to trigger queue sync""" for record in self: - if not record.skip: + if len(record.product_ids) > 0: record.solr_flag = 2 def solr_flag_to_queue(self, limit=500): @@ -161,10 +161,18 @@ class Keywords(models.Model): return True def _sync_keywords_queue_callback(self): - """Callback method executed by apache.solr.queue - syncs keyword data to Solr""" - documents = [] + success_keywords = self.browse() + for keyword in self: + if not keyword.product_ids: + _logger.info( + 'Skipping Solr sync for keyword "%s" - no products found', + keyword.keywords + ) + continue + searchkey = (keyword.keywords or '').strip().lower().replace(' ', '-') + try: doc = { 'id': keyword.id, @@ -173,13 +181,19 @@ class Keywords(models.Model): 'url_s': keyword.url, 'product_ids_is': [p.product_tmpl_id.id for p in keyword.product_ids], } - documents.append(doc) + + solr.add([doc]) + + success_keywords |= keyword + except Exception as e: - _logger.error('failed %s', e) - _logger.error('doc data: %s', doc) + _logger.error( + "Solr sync failed for keyword ID %s: %s", + keyword.id, e + ) - if documents: - solr.add(documents) + if success_keywords: + success_keywords.write({'solr_flag': 0}) return True @@ -194,6 +208,11 @@ class Keywords(models.Model): documents = [] for keyword in keywords: + # Skip syncing if product count is 0 + if len(keyword.product_ids) == 0: + _logger.info('Skipping Solr sync for keyword "%s" - no products found', keyword.keywords) + continue + searchkey = (keyword.keywords or '').strip().lower().replace(' ', '-') try: doc = { @@ -223,10 +242,12 @@ class Keywords(models.Model): def write(self, vals): result = super().write(vals) - tracked_fields = ['keywords', 'category_id', 'product_ids'] + tracked_fields = ['keywords', 'category_id', 'product_ids', 'skip', 'name'] neded_sync = any(field in vals for field in tracked_fields) - if neded_sync and self.skip == False: + if neded_sync: for record in self: - record.solr_flag = 2 + # Only flag for sync if there are products + if len(record.product_ids) > 0: + record.solr_flag = 2 return result diff --git a/indoteknik_custom/models/price_group.py b/indoteknik_custom/models/price_group.py index fce78fff..1a4f1bd6 100644 --- a/indoteknik_custom/models/price_group.py +++ b/indoteknik_custom/models/price_group.py @@ -10,16 +10,16 @@ class PriceGroup(models.Model): name = fields.Char(string='Name') pricelist_id = fields.Many2one('product.pricelist', string='Price List') - group1 = fields.Float(string='Kelompok 1 (%)') - group2 = fields.Float(string='Kelompok 2 (%)') - group3 = fields.Float(string='Kelompok 3 (%)') - group4 = fields.Float(string='Kelompok 4 (%)') - group5 = fields.Float(string='Kelompok 5 (%)') - group6 = fields.Float(string='Kelompok 6 (%)') - group7 = fields.Float(string='Kelompok 7 (%)') - group8 = fields.Float(string='Kelompok 8 (%)') - group9 = fields.Float(string='Kelompok 9 (%)') - group10 = fields.Float(string='Kelompok 10 (%)') + group1 = fields.Float(string='Kelompok 1 (%)', digits=(16, 12)) + group2 = fields.Float(string='Kelompok 2 (%)', digits=(16, 12)) + group3 = fields.Float(string='Kelompok 3 (%)', digits=(16, 12)) + group4 = fields.Float(string='Kelompok 4 (%)', digits=(16, 12)) + group5 = fields.Float(string='Kelompok 5 (%)', digits=(16, 12)) + group6 = fields.Float(string='Kelompok 6 (%)', digits=(16, 12)) + group7 = fields.Float(string='Kelompok 7 (%)', digits=(16, 12)) + group8 = fields.Float(string='Kelompok 8 (%)', digits=(16, 12)) + group9 = fields.Float(string='Kelompok 9 (%)', digits=(16, 12)) + group10 = fields.Float(string='Kelompok 10 (%)', digits=(16, 12)) def collect_price_group(self): PRICE_GROUP_ID = { diff --git a/indoteknik_custom/models/product_template.py b/indoteknik_custom/models/product_template.py index 969c9765..56022d06 100755 --- a/indoteknik_custom/models/product_template.py +++ b/indoteknik_custom/models/product_template.py @@ -94,7 +94,6 @@ class ProductTemplate(models.Model): if self.env.user.id not in users_in_group.mapped('id') and active_model == None: raise UserError('Hanya MD yang bisa membuat Product') result = super(ProductTemplate, self).create(vals) - self.env['product.product']._add_product_to_keywords(result) return result # def write(self, values): @@ -895,11 +894,6 @@ class ProductTemplate(models.Model): # Log changes self._log_field_changes_product(vals, old_values) - # Add product to keywords - keyword_trigger = ['name', 'website_description', 'unpublished'] - if any(field in vals for field in keyword_trigger): - for product in self: - self.env['product.product']._add_product_to_keywords(product) return result # def write(self, vals): @@ -952,6 +946,7 @@ class ProductProduct(models.Model): string="Sourcing Job", readonly=True, ) + has_magento = fields.Boolean(string='Has Magento?', default=False, readonly=True) def _add_product_to_keywords(self,product): keywords_model = self.env['keywords'] @@ -971,8 +966,6 @@ class ProductProduct(models.Model): return True - has_magento = fields.Boolean(string='Has Magento?', default=False, readonly=True) - def generate_product_sla(self): product_variant_ids = self.env.context.get('active_ids', []) product_variant = self.search([('id', 'in', product_variant_ids)]) @@ -993,7 +986,6 @@ class ProductProduct(models.Model): if self.env.user.id not in users_in_group.mapped('id') and active_model == None: raise UserError('Hanya MD yang bisa membuat Product') result = super(ProductProduct, self).create(vals) - self._add_product_to_keywords(result) return result # def write(self, values): @@ -1360,9 +1352,6 @@ class ProductProduct(models.Model): ] # pake ini kalau mau Cek semua field # if vals: - trigger_fields = ['name', 'website_description', 'unpublished'] - if any(f in vals for f in trigger_fields): - self._add_product_to_keywords(vals) if any(field in vals for field in tracked_fields): old_values = self._collect_old_values(vals) result = super().write(vals) diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index b3ecca56..a345b96b 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -8,6 +8,8 @@ import io import base64 from odoo.tools import lazy_property import socket +import requests +import json try: from odoo.tools.misc import xlsxwriter @@ -125,7 +127,308 @@ class PurchaseOrder(models.Model): ) overseas_po = fields.Boolean(string='PO Luar Negeri?', tracking=3, help='Centang jika PO untuk pembelian luar negeri') + order_altama_id = fields.Integer('Req Order Altama', copy=False) + soo_number = fields.Char('SOO Number', copy=False) + soo_price = fields.Float('SOO Price', copy=False) + soo_discount = fields.Float('SOO Discount', copy=False) + soo_tax = fields.Float('SOO Tax', copy=False) + + def _get_altama_token(self, source='auto'): + ICP = self.env['ir.config_parameter'].sudo() + TokenLog = self.env['token.log'].sudo() + + token_url = ICP.get_param('token.adempiere.altama') + client_id = ICP.get_param('client.adempiere.altama') + client_secret = ICP.get_param('secret_key.adempiere.altama') + + active_token = TokenLog.search([ + ('is_active', '=', True), + ('token_from', '=', 'Adempiere Altama'), + ('expires_at', '>', datetime.utcnow()), + ], limit=1, order='id desc') + + if active_token: + return active_token.token + + headers = { + "Authorization": "Basic " + base64.b64encode(f"{client_id}:{client_secret}".encode()).decode(), + "Content-Type": "application/x-www-form-urlencoded", + } + data = {"grant_type": "client_credentials"} + + response = requests.post(token_url, data=data, headers=headers, timeout=15) + if response.status_code == 200: + result = response.json() + token = result.get("access_token") + expires_in = result.get("expires_in", 3600) + expiry_time = datetime.utcnow() + timedelta(seconds=expires_in - 60) + + TokenLog.search([ + ('token_from', '=', 'Adempiere Altama'), + ('is_active', '=', True), + ]).write({'is_active': False}) + + TokenLog.create({ + 'token': token, + 'expires_at': expiry_time, + 'is_active': True, + 'created_by': self.env.user.id if self.env.user else None, + 'source': source, + 'token_from': 'Adempiere Altama', + }) + + return token + + else: + raise Exception(f"Gagal ambil token: {response.status_code} - {response.text}") + + def action_create_order_altama(self): + ICP = self.env['ir.config_parameter'].sudo() + for order in self: + try: + token = self._get_altama_token(source='manual') + url = ICP.get_param('endpoint.create.order.adempiere.altama') + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + + payload = { + "date_po": order.date_approve.strftime("%Y%m%d%H%M%S"), + "no_po": order.name, + "details": [ + { + "item_code": line.product_id.default_code or "", + "price": line.price_unit, + "qty": line.product_qty, + } + for line in order.order_line + ], + } + + response = requests.post(url, json=payload, headers=headers, timeout=20) + + try: + result = response.json() + except json.JSONDecodeError: + raise Exception(f"Response bukan JSON valid: {response.text}") + + if response.status_code == 200 and result.get("code") == "00": + contents = result.get("contents", {}) + if isinstance(contents, dict): + order.order_altama_id = contents.get("req_id") + else: + order.order_altama_id = contents.get("req_id") + + elif response.status_code == 404: + raise Exception("URL endpoint gak ditemukan (404). Pastikan path-nya benar di Altama API.") + elif response.status_code == 401: + token = self._get_altama_token(source='auto') + headers["Authorization"] = f"Bearer {token}" + response = requests.post(url, json=payload, headers=headers, timeout=20) + elif response.status_code not in (200, 201): + raise Exception(f"Gagal kirim ke Altama: {response.status_code} - {response.text}") + + self.message_post(body=f"✅ PO berhasil dikirim ke Altama!\nResponse: {json.dumps(result, indent=2)}") + + except Exception as e: + self.message_post(body=f"❌ Gagal kirim ke Altama:<br/><pre>{str(e)}</pre>") + + + def action_get_order_altama(self): + ICP = self.env['ir.config_parameter'].sudo() + + for order in self: + try: + # ============================ + # Get Token + # ============================ + token = self._get_altama_token(source='manual') + + url = ICP.get_param('endpoint.get.order.adempiere.altama') + if not url: + raise Exception("Parameter 'endpoint.adempiere.altama' belum diset di System Parameters.") + + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + + params = { + "orderreq_id": order.order_altama_id or 0, + } + + # ============================ + # Request ke API + # ============================ + response = requests.get(url, headers=headers, params=params, timeout=20) + + if response.status_code == 401: + token = self._get_altama_token(source='auto') + headers["Authorization"] = f"Bearer {token}" + response = requests.get(url, headers=headers, params=params, timeout=20) + + if response.status_code not in (200, 201): + raise Exception(f"Gagal ambil data dari Altama: {response.status_code} - {response.text}") + + data = response.json() + + # ============================ + # Extract Data + # ============================ + contents_root = data.get("contents", {}) + contents_list = contents_root.get("contents", []) + + if not isinstance(contents_list, list): + raise Exception("Format data contents dari Altama tidak sesuai (expected list).") + + order.message_post( + body=f"✅ Berhasil ambil data dari Altama. Ditemukan {len(contents_list)} record." + ) + + # ===================================================== + # LOOP MAIN DATA + # ===================================================== + for item in contents_list: + + req_id = item.get("req_id") + no_po = item.get("no_po") + list_item_po = item.get("list_Item_po", []) + list_soo = item.get("list_soo", []) + + # ============================ + # Isi Data SOO Ke Order + # ============================ + soo_numbers = [s.get("no_soo") for s in list_soo if s.get("no_soo")] + unique_soo = list(set(soo_numbers)) + if len(unique_soo) == 1: + order.soo_number = unique_soo[0] + if not order.picking_ids.number_soo: + # order.picking_ids[0].number_soo = unique_soo[0] + for picking in order.picking_ids: + picking.number_soo = unique_soo[0] + elif len(unique_soo) > 1: + order.soo_number = ", ".join(unique_soo) + if not order.picking_ids.number_soo: + # order.picking_ids[0].number_soo = ", ".join(unique_soo) + for picking in order.picking_ids: + picking.number_soo = ", ".join(unique_soo) + else: + order.soo_number = False + + if list_soo: + first_soo = list_soo[0] + order.soo_price = first_soo.get("totalprice") + order.soo_discount = first_soo.get("diskon") + order.soo_tax = first_soo.get("ppn") + + order.order_altama_id = req_id + + # ============================ + # Update Order Lines + # ============================ + for item_line in list_item_po: + + line = order.order_line.filtered( + lambda l: l.product_id.default_code == item_line.get("item_code") + ) + + if line: + line.write({ + "description": item_line.get("description", ""), + "altama_ordered": item_line.get("qtyordered", 0), + "altama_delivered": item_line.get("qtydelivered", 0), + "altama_invoiced": item_line.get("qtyinvoiced", 0), + "docstatus_altama": item_line.get("docstatus", ""), + }) + + # ===================================================== + # BUILD HTML TABLES FOR CHATTER + # ===================================================== + + # ---- SOO TABLE ---- + soo_rows = "" + for s in list_soo: + soo_rows += f""" + <tr> + <td>{s.get('no_soo')}</td> + <td>{s.get('totalprice')}</td> + <td>{s.get('diskon')}</td> + <td>{s.get('ppn')}</td> + </tr> + """ + + soo_table = f""" + <table style="width:100%; border-collapse: collapse; margin-top: 10px;"> + <thead> + <tr style="background:#f1f1f1;"> + <th style="border:1px solid #ccc; padding:6px;">SOO Number</th> + <th style="border:1px solid #ccc; padding:6px;">Total Price</th> + <th style="border:1px solid #ccc; padding:6px;">Diskon</th> + <th style="border:1px solid #ccc; padding:6px;">PPN</th> + </tr> + </thead> + <tbody> + {soo_rows or '<tr><td colspan="4">Tidak ada data SOO</td></tr>'} + </tbody> + </table> + """ + + # ---- ITEM PO TABLE ---- + po_rows = "" + for l in list_item_po: + + desc = l.get("description") or "" + + # Flag: row error kalau description tidak mulai dengan SOO/ + is_error = desc and not desc.startswith("SOO/") + + # Style row merah + row_style = "color:red; font-weight:bold;" if is_error else "" + + po_rows += f""" + <tr style="{row_style}"> + <td>{l.get('item_code')}</td> + <td>{desc}</td> + <td>{l.get('qtyordered')}</td> + <td>{l.get('qtydelivered')}</td> + <td>{l.get('qtyinvoiced')}</td> + <td>{l.get('docstatus')}</td> + </tr> + """ + + + po_table = f""" + <table style="width:100%; border-collapse: collapse; margin-top: 10px;"> + <thead> + <tr style="background:#f1f1f1;"> + <th style="border:1px solid #ccc; padding:6px;">Item Code</th> + <th style="border:1px solid #ccc; padding:6px;">Description</th> + <th style="border:1px solid #ccc; padding:6px;">Ordered</th> + <th style="border:1px solid #ccc; padding:6px;">Delivered</th> + <th style="border:1px solid #ccc; padding:6px;">Invoiced</th> + <th style="border:1px solid #ccc; padding:6px;">Status</th> + </tr> + </thead> + <tbody> + {po_rows or '<tr><td colspan="6">Tidak ada item PO</td></tr>'} + </tbody> + </table> + """ + # ---- POST TO CHATTER ---- + order.message_post( + body=f""" + <b>📦 Data SOO</b><br/>{soo_table} + <br/><br/> + <b>📦 Data Item PO</b><br/>{po_table} + """ + ) + + except Exception as e: + order.message_post( + body=f"❌ Gagal ambil data dari Altama:<br/><pre>{str(e)}</pre>" + ) @staticmethod def is_local_env(): hostname = socket.gethostname().lower() @@ -632,7 +935,8 @@ class PurchaseOrder(models.Model): date_done = self.date_approve - day_extension = int(self.payment_term_id.line_ids.days) + # day_extension = int(self.payment_term_id.line_ids.days) + day_extension = int(max(self.payment_term_id.line_ids.mapped('days'), default=0)) payment_schedule = date_done + timedelta(days=day_extension) if payment_schedule.weekday() == 0: @@ -1142,6 +1446,9 @@ class PurchaseOrder(models.Model): send_email = True break + if self.partner_id.id == 5571 and not self.revisi_po: + self.action_create_order_altama() + if send_email: if self.is_local_env(): _logger.warning("📪 Local environment detected — skip sending email reminders.") @@ -1159,6 +1466,7 @@ class PurchaseOrder(models.Model): self.calculate_line_no() self.approve_by = self.env.user.id + # override date planned added with two days # leadtime = self.partner_id.leadtime # delta_time = current_time + timedelta(days=leadtime) diff --git a/indoteknik_custom/models/purchase_order_line.py b/indoteknik_custom/models/purchase_order_line.py index 76dcc09e..c6a49481 100755 --- a/indoteknik_custom/models/purchase_order_line.py +++ b/indoteknik_custom/models/purchase_order_line.py @@ -55,6 +55,23 @@ class PurchaseOrderLine(models.Model): ending_price = fields.Float(string='Ending Price', compute='_compute_doc_delivery_amt') show_description = fields.Boolean(string='Show Description', help="Show Description when print po", default=True) price_unit_before = fields.Float(string='Unit Price Before', help="Harga awal yang sebelumnya telah diinputkan") + altama_ordered = fields.Float( + string='Altama Ordered', + default=0.0, + copy=False + ) + altama_delivered = fields.Float( + string='Altama Delivered', + default=0.0, + copy=False + ) + altama_invoiced = fields.Float( + string='Altama Invoiced', + default=0.0, + copy=False + ) + description = fields.Text(string='Description', readonly=True, copy=False) + docstatus_altama = fields.Text(string='Status Altama', readonly=True, copy=False) @api.onchange('price_unit') def _onchange_price_unit_before(self): diff --git a/indoteknik_custom/models/purchase_pricelist.py b/indoteknik_custom/models/purchase_pricelist.py index b3a473b6..83e06f55 100755 --- a/indoteknik_custom/models/purchase_pricelist.py +++ b/indoteknik_custom/models/purchase_pricelist.py @@ -118,15 +118,16 @@ class PurchasePricelist(models.Model): product_domain = [('product_id', '=', rec.product_id.id)] markup_pricelist = price_group['markup'].pricelist_id - base_price = price_incl + (price_incl * markup_percentage / 100) + # base_price = price_incl + (price_incl * markup_percentage / 100) + base_price = round(price_incl + (price_incl * markup_percentage / 100), 12) base_prod_pricelist = self.env['product.pricelist.item'].search(product_domain + [('pricelist_id', '=', markup_pricelist.id)], limit=1) base_prod_pricelist.fixed_price = base_price tier_percentages = [price_group[f'tier_{i}'][product_group] for i in range(1, 6)] for i, tier_percentage in enumerate(tier_percentages): tier_pricelist = price_group[f'tier_{i + 1}'].pricelist_id - tier_price = price_incl + (price_incl * tier_percentage / 100) - tier_perc = (base_price - tier_price) / base_price * 100 + tier_price = round(price_incl + (price_incl * tier_percentage / 100), 12) + tier_perc = round((base_price - tier_price) / base_price * 100, 12) tier_prod_pricelist = self.env['product.pricelist.item'].search(product_domain + [('pricelist_id', '=', tier_pricelist.id)], limit=1) tier_prod_pricelist.price_discount = tier_perc diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index 7f4feb75..5bbaaf1c 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -298,26 +298,36 @@ class ResPartner(models.Model): def _check_npwp(self): for record in self: npwp = record.npwp.strip() if record.npwp else '' - # Abaikan validasi jika NPWP kosong atau diisi "0" + if not npwp or npwp == '0' or npwp == '00.000.000.0-000.000': continue - - # Validasi untuk NPWP 15 digit (format: 99.999.999.9-999.999) - if len(npwp) == 20: - # Regex untuk 15 digit dengan format titik dan tanda hubung + + elif len(npwp) == 15 and npwp.isdigit(): + formatted = f"{npwp[0:2]}.{npwp[2:5]}.{npwp[5:8]}.{npwp[8]}-{npwp[9:12]}.{npwp[12:15]}" + record.npwp = formatted + + elif len(npwp) == 20: pattern_15_digit = r'^\d{2}\.\d{3}\.\d{3}\.\d{1}-\d{3}\.\d{3}$' if not re.match(pattern_15_digit, npwp): - raise ValidationError("Format NPWP 15 digit yang dimasukkan salah. Pastikan format yang benar adalah: 99.999.999.9-999.999") - - # Validasi untuk NPWP 16 digit (hanya angka tanpa titik atau tanda hubung) + raise ValidationError( + "Format NPWP 15 digit yang dimasukkan salah. " + "Pastikan format yang benar adalah: 99.999.999.9-999.999" + ) + elif len(npwp) == 16: pattern_16_digit = r'^\d{16}$' if not re.match(pattern_16_digit, npwp): - raise ValidationError("Format NPWP 16 digit yang dimasukkan salah. Format yang benar adalah 16 digit angka tanpa titik atau tanda hubung.") - - # Validasi panjang NPWP jika lebih atau kurang dari 15 atau 16 digit + raise ValidationError( + "Format NPWP 16 digit yang dimasukkan salah. " + "Format yang benar adalah 16 digit angka tanpa titik atau tanda hubung." + ) + else: - raise ValidationError("Digit NPWP yang dimasukkan tidak sesuai. Pastikan NPWP memiliki 15 digit dengan format tertentu (99.999.999.9-999.999) atau 16 digit tanpa tanda hubung.") + raise ValidationError( + "Digit NPWP yang dimasukkan tidak sesuai. " + "Pastikan NPWP memiliki 15 digit dengan format tertentu " + "(99.999.999.9-999.999) atau 16 digit tanpa tanda hubung." + ) # def write(self, vals): diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 3382d95b..92efa3bd 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -156,6 +156,7 @@ class SaleOrder(models.Model): total_margin_excl_third_party = fields.Float('Before Margin', help="Before Margin in Sales Order Header") approval_status = fields.Selection([ + ('pengajuan0', 'Approval Team Sales'), ('pengajuan1', 'Approval Manager'), ('pengajuan2', 'Approval Pimpinan'), ('approved', 'Approved'), @@ -647,7 +648,7 @@ class SaleOrder(models.Model): def _get_biteship_courier_codes(self): return [ - 'gojek','grab','deliveree','lalamove','jne','ninja','lion','rara','sicepat','jnt','pos','idexpress','rpx','wahana','jdl','anteraja','sap','paxel','borzo' + 'gojek','grab','deliveree','lalamove','jne','ninja','lion','rara','sicepat','jnt','idexpress','rpx','wahana','jdl','anteraja','sap','paxel','borzo' ] @api.onchange('carrier_id') @@ -2384,8 +2385,15 @@ class SaleOrder(models.Model): # if order.validate_partner_invoice_due(): # return self._create_notification_action('Notification', # 'Terdapat invoice yang telah melewati batas waktu, mohon perbarui pada dokumen Due Extension') - - if order._requires_approval_margin_leader(): + value_trigger = order._requires_approval_by_value() + if value_trigger: + self.check_product_bom() + self.check_credit_limit() + self.check_limit_so_to_invoice() + order.approval_status = 'pengajuan0' + order.message_post(body="Mengajukan approval ke Team Sales_") + return self._create_approval_notification('Team Sales') + elif order._requires_approval_margin_leader(): order.approval_status = 'pengajuan2' order.message_post(body="Mengajukan approval ke Pimpinan") return self._create_approval_notification('Pimpinan') @@ -2400,9 +2408,16 @@ class SaleOrder(models.Model): self.check_product_bom() self.check_credit_limit() self.check_limit_so_to_invoice() - order.approval_status = 'pengajuan1' + order.approval_status = 'pengajuan0' order.message_post(body="Mengajukan approval ke Team Sales") return self._create_approval_notification('Team Sales') + # elif value_trigger: + # self.check_product_bom() + # self.check_credit_limit() + # self.check_limit_so_to_invoice() + # order.approval_status = 'pengajuan0' + # order.message_post(body="Mengajukan approval ke Team Sales_") + # return self._create_approval_notification('Team Sales') if not order.with_context(ask_approval=True)._is_request_to_own_team_leader(): return self._create_notification_action( @@ -2601,12 +2616,22 @@ class SaleOrder(models.Model): def check_archived_product(self): for order in self: for line in order.order_line: - if line.product_id.active == False: - raise UserError("Terdapat Product yang sudah di Archive pada Product: {}".format(line.product_id.display_name)) + # Skip section & note + if line.display_type: + continue + + if line.product_id and not line.product_id.active: + raise UserError( + "Terdapat Product yang sudah di Archive pada Product: {}".format( + line.product_id.display_name + ) + ) def check_archived_uom(self): for order in self: for line in order.order_line: + if line.display_type: + continue if line.product_uom.active == False: raise UserError("Terdapat UoM yang sudah di Archive pada UoM {} di Product {}".format(line.product_uom.name, line.product_id.display_name)) @@ -2657,16 +2682,25 @@ class SaleOrder(models.Model): 'Warning', 'Hanya bisa konfirmasi SO tim Anda.' ) - if order._requires_approval_margin_leader(): + value_trigger = order._requires_approval_by_value() + if value_trigger: + order.approval_status = 'pengajuan0' + order.message_post(body="Mengajukan approval ke Team Sales") + return self._create_approval_notification('Team Sales') + elif order._requires_approval_margin_leader(): order.approval_status = 'pengajuan2' return self._create_approval_notification('Pimpinan') elif order._requires_approval_margin_manager(): order.approval_status = 'pengajuan1' return self._create_approval_notification('Sales Manager') - elif order._requires_approval_team_sales(): - order.approval_status = 'pengajuan1' + elif value_trigger or order._requires_approval_team_sales(): + order.approval_status = 'pengajuan0' order.message_post(body="Mengajukan approval ke Team Sales") return self._create_approval_notification('Team Sales') + # elif value_trigger: + # order.approval_status = 'pengajuan0' + # order.message_post(body="Mengajukan approval ke Team Sales (Total SO > 50jt)") + # return self._create_approval_notification('Team Sales') order.approval_status = 'approved' order._set_sppkp_npwp_contact() @@ -2778,7 +2812,15 @@ class SaleOrder(models.Model): and not self.env.user.is_leader ) - + def _requires_approval_by_value(self): + # LIMIT_VALUE = 50000000 + LIMIT_VALUE = float(self.env['ir.config_parameter'].sudo().get_param('so.limit_value_approve', default='50000000')) + return ( + self.amount_total >= LIMIT_VALUE + and self.env.user.id not in [11, 9, 375] # Eko, Ade, Putra + and not self.env.user.is_sales_manager + and not self.env.user.is_leader + ) def _is_request_to_own_team_leader(self): user = self.env.user @@ -3397,7 +3439,7 @@ class SaleOrder(models.Model): #payment term vals if 'payment_term_id' in vals and any( - order.approval_status in ['pengajuan1', 'pengajuan2', 'approved'] for order in self): + order.approval_status in ['pengajuan0','pengajuan1', 'pengajuan2', 'approved'] for order in self): raise UserError( "Payment Term tidak dapat diubah karena Sales Order sedang dalam proses approval atau sudah diapprove.") diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 065b1484..ab366fd6 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -459,6 +459,7 @@ class StockPicking(models.Model): ('urgent', 'Urgent Delivery'), ], string='Reason Change Date Planned', tracking=True) delivery_date = fields.Datetime(string='Delivery Date', copy=False) + number_soo = fields.Char(string='Number SOO Altama') def _get_kgx_awb_number(self): """Menggabungkan name dan origin untuk membuat AWB Number""" diff --git a/indoteknik_custom/models/token_log.py b/indoteknik_custom/models/token_log.py new file mode 100644 index 00000000..fdc0c03e --- /dev/null +++ b/indoteknik_custom/models/token_log.py @@ -0,0 +1,17 @@ +from odoo import models, fields, api + +class FixcoTokenLog(models.Model): + _name = 'token.log' + _description = 'Log Token Fixco' + _order = 'create_date desc' + + token = fields.Text(string="Access Token", readonly=True) + expires_at = fields.Datetime(string="Expires At", readonly=True) + created_at = fields.Datetime(string="Created At", default=lambda self: fields.Datetime.now(), readonly=True) + created_by = fields.Many2one('res.users', string="Generated By", readonly=True) + source = fields.Selection([ + ('manual', 'Manual Request'), + ('auto', 'Auto Refresh'), + ], string="Token Source", default='auto', readonly=True) + token_from = fields.Char(string="From", readonly=True) + is_active = fields.Boolean("Active", default=True)
\ No newline at end of file diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 682c478a..619e7c99 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -62,6 +62,7 @@ class TukarGuling(models.Model): notes = fields.Text('Notes') return_type = fields.Selection(String='Return Type', selection=[ ('tukar_guling', 'Tukar Guling'), # -> barang yang sama + # ('service', 'Service'), # -> barang yang sama ('retur_so', 'Retur SO')], required=True, tracking=3, help='Retur SO (ORT-SRT),\n Tukar Guling (ORT-SRT-PICK-OUT)') state = fields.Selection(string='Status', selection=[ ('draft', 'Draft'), @@ -931,6 +932,66 @@ class TukarGuling(models.Model): _logger.info(f"✅ BU/PICK Baru dari ORT created: {new_pick.name}") record.message_post( body=f"📦 <b>{new_pick.name}</b> created by <b>{self.env.user.name}</b> (state: <b>{new_pick.state}</b>)") + + # if record.return_type == 'service': + # GUDANG_SERVICE_LOCATION_ID = 98 + # # From STOCK to OUTPUT + # done_service = self.env['stock.picking'].create({ + # 'group_id': bu_out.group_id.id, + # 'tukar_guling_id': record.id, + # 'sale_order': record.origin, + # 'note': record.notes, + # 'picking_type_id': 32, + # 'location_id': GUDANG_SERVICE_LOCATION_ID, + # 'location_dest_id': BU_STOCK_LOCATION_ID, + # 'partner_id': bu_out.partner_id.id, + # 'move_ids_without_package': [(0, 0, { + # 'product_id': line.product_id.id, + # 'product_uom_qty': line.product_uom_qty, + # 'product_uom': line.product_uom.id, + # 'name': line.product_id.display_name, + # 'location_id': GUDANG_SERVICE_LOCATION_ID, + # 'location_dest_id': BU_STOCK_LOCATION_ID, + # }) for line in record.line_ids], + # }) + # if done_service: + # done_service.action_confirm() + # done_service.action_assign() + # else: + # raise UserError("Gagal membuat picking service") + + # service_to_output = self.env['stock.picking'].create({ + # 'group_id': bu_out.group_id.id, + # 'tukar_guling_id': record.id, + # 'sale_order': record.origin, + # 'note': record.notes, + # 'picking_type_id': 32, + # 'location_id': BU_STOCK_LOCATION_ID, + # 'location_dest_id': BU_OUTPUT_LOCATION_ID, + # 'partner_id': bu_out.partner_id.id, + # 'move_lines': [(0, 0, { + # 'product_id': line.product_id.id, + # 'product_uom_qty': line.product_uom_qty, + # 'product_uom': line.product_uom.id, + # 'name': line.product_id.display_name, + # 'location_id':BU_STOCK_LOCATION_ID, + # 'location_dest_id': BU_STOCK_LOCATION_ID, + # }) for line in record.line_ids], + # 'move_ids_without_package': [(0, 0, { + # 'product_id': line.product_id.id, + # 'product_uom_qty': line.product_uom_qty, + # 'product_uom': line.product_uom.id, + # 'name': line.product_id.display_name, + # 'location_id': BU_STOCK_LOCATION_ID, + # 'location_dest_id': BU_OUTPUT_LOCATION_ID, + # }) for line in record.line_ids], + # }) + # if service_to_output: + # service_to_output.action_confirm() + # service_to_output.action_assign() + # else: + # raise UserError("Gagal membuat picking service") + # BU/OUT Baru dari SRT if srt_picking: diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index 24604eeb..fed6193a 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -200,6 +200,7 @@ access_advance_payment_usage_line,access.advance.payment.usage.line,model_advanc access_advance_payment_create_bill,access.advance.payment.create.bill,model_advance_payment_create_bill,,1,1,1,1 access_create_reimburse_cab_wizard_user,create.reimburse.cab.wizard user,model_create_reimburse_cab_wizard,,1,1,1,1 access_advance_payment_cancel_wizard,advance.payment.cancel.wizard,model_advance_payment_cancel_wizard,,1,1,1,1 +access_advance_payment_request_estimate_line,advance.payment.request.estimate.line,model_advance_payment_request_estimate_line,,1,1,1,1 access_purchasing_job_seen,purchasing.job.seen,model_purchasing_job_seen,,1,1,1,1 access_tukar_guling_all_users,tukar.guling.all.users,model_tukar_guling,base.group_user,1,1,1,1 @@ -215,7 +216,10 @@ 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_stock_picking_sj_document,stock.picking.sj.document,model_stock_picking_sj_document,base.group_user,1,1,1,1 +access_gudang_service,gudang.service,model_gudang_service,base.group_user,1,1,1,1 +access_gudang_service_line,gudang.service.line,model_gudang_service_line,base.group_user,1,1,1,1 access_update_depreciation_move_wizard,access.update.depreciation.move.wizard,model_update_depreciation_move_wizard,,1,1,1,1 + access_keywords,keywords,model_keywords,base.group_user,1,1,1,1 access_sourcing_job_order,access.sourcing_job_order,model_sourcing_job_order,base.group_user,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 @@ -224,3 +228,4 @@ access_wizard_export_sjo_to_so,wizard.export.sjo.to.so,model_wizard_export_sjo_t access_sourcing_job_order_line_import_wizard,sourcing.job.order.line.import.wizard,model_sourcing_job_order_line_import_wizard,base.group_user,1,1,1,1 access_sourcing_job_order_line_export_wizard,sourcing.job.order.line.export.wizard,model_sourcing_job_order_line_export_wizard,base.group_user,1,1,1,1 access_sourcing_job_order_line_template_wizard,sourcing.job.order.line.template.wizard,model_sourcing_job_order_line_template_wizard,base.group_user,1,1,1,1 +access_token_log,access.token.log,model_token_log,,1,1,1,1 diff --git a/indoteknik_custom/views/account_move_line.xml b/indoteknik_custom/views/account_move_line.xml index 838596c8..346494f3 100644 --- a/indoteknik_custom/views/account_move_line.xml +++ b/indoteknik_custom/views/account_move_line.xml @@ -9,6 +9,9 @@ <!-- <xpath expr="//page[@id='aml_tab']/field[@name='line_ids']" position="attributes"> <attribute name="attrs">{'readonly': [('refund_id','!=',False)]}</attribute> </xpath> --> + <xpath expr="//field[@name='line_ids']/tree/field[@name='credit']" position="after"> + <field name="date_maturity" optional="hide"/> + </xpath> <xpath expr="//page[@id='aml_tab']/field[@name='line_ids']/tree/field[@name='currency_id']" position="before"> <field name="is_required" invisible="1"/> </xpath> diff --git a/indoteknik_custom/views/advance_payment_request.xml b/indoteknik_custom/views/advance_payment_request.xml index 340e0caf..4faf905e 100644 --- a/indoteknik_custom/views/advance_payment_request.xml +++ b/indoteknik_custom/views/advance_payment_request.xml @@ -134,8 +134,28 @@ <br/> </group> </group> - <notebook attrs="{'invisible': [('type_request', '!=', 'reimburse')]}"> - <page string="Rincian Reimburse"> + <notebook> + <page string="Rincian Estimasi PUM" attrs="{'invisible': [('type_request', '!=', 'pum')]}"> + <p style="font-size: 12px; color: grey; font-style: italic">*Masukkan estimasi alokasi biaya sebagai gambaran rencana penggunaan dana, tidak harus diisi dengan nominal yang akurat</p> + <field name="estimate_line_ids"> + <tree> + <field name="category_estimate"/> + <field name="description"/> + <field name="nominal" sum="Total"/> + <field name="currency_id" invisible="1"/> + </tree> + <form> + <group col="2"> + <field name="request_id" invisible="1"/> + <field name="category_estimate"/> + <field name="description" placeholder="Deskripsi tambahan untuk rincian estimasi..."/> + <field name="nominal"/> + <field name="currency_id" invisible="1"/> + </group> + </form> + </field> + </page> + <page string="Rincian Reimburse" attrs="{'invisible': [('type_request', '!=', 'reimburse')]}"> <field name="reimburse_line_ids"> <tree> <field name="sequence" widget="handle"/> diff --git a/indoteknik_custom/views/advance_payment_settlement.xml b/indoteknik_custom/views/advance_payment_settlement.xml index a8bf1de7..352c5b96 100644 --- a/indoteknik_custom/views/advance_payment_settlement.xml +++ b/indoteknik_custom/views/advance_payment_settlement.xml @@ -118,12 +118,26 @@ <group string="Finance"> <field name="is_current_user_ap" invisible="1"/> <field name="account_id" attrs="{'readonly': [('is_current_user_ap', '=', False)]}"/> + <field name="category_usage" invisible="1"/> <field name="done_attachment" attrs="{'readonly': [('is_current_user_ap', '=', False)]}"/> </group> </group> </form> </field> </page> + <page string="Rincian Estimasi PUM"> + <p style="font-size: 12px; color: grey; font-style: italic">*Rincian estimasi PUM ini hanya sebagai gambaran umum untuk realisasi yang dilakukan, tidak harus diisi dengan nominal yang akurat.</p> + <field name="pum_estimate_line_ids" nolabel="1"> + <tree> + <field name="category_estimate"/> + <field name="description"/> + <field name="nominal" sum="Total Estimasi"/> + <field name="frequency"/> + <field name="total_actual" sum="Total Actual"/> + <field name="currency_id" invisible="1"/> + </tree> + </field> + </page> </notebook> <div style="text-align:right;"> diff --git a/indoteknik_custom/views/gudang_service.xml b/indoteknik_custom/views/gudang_service.xml new file mode 100644 index 00000000..769664c5 --- /dev/null +++ b/indoteknik_custom/views/gudang_service.xml @@ -0,0 +1,110 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<odoo> + <data> + <!-- Tree --> + <record id="view_gudang_service_tree" model="ir.ui.view"> + <field name="name">gudang.serivice.tree</field> + <field name="model">gudang.service</field> + <field name="arch" type="xml"> + <tree string="Monitoring Barang Service" decoration-danger="state in ('draft', 'received_from_cust')" decoration-warning="state in ('sent_to_vendor', 'received_from_vendor')" + decoration-success="state == 'delivered_to_cust'" decoration-muted="state == 'cancel'" > + <field name="name"/> + <field name="partner_id"/> + <field name="vendor_id"/> + <field name="origin"/> + <field name="schedule_date"/> + <field name="start_date" optional="hide"/> + <field name="remaining_date"/> + <field name="state" widget="badge" decoration-danger="state in ('draft', 'received_from_cust')" decoration-warning="state in ('sent_to_vendor', 'received_from_vendor')" + decoration-success="state == 'delivered_to_cust'" decoration-muted="state == 'cancel'" /> + <field name="cancel_reason" optional="hide"/> + <field name="create_date" optional="hide"/> + </tree> + </field> + </record> + <!-- Form --> + <record id="view_gudang_service_form" model="ir.ui.view"> + <field name="name">gudang.service.form</field> + <field name="model">gudang.service</field> + <field name="arch" type="xml"> + <form> + <header> + <button name="action_submit" string="Proceed" type="object" + class="btn-primary" + attrs="{'invisible': [('state', 'in', ['cancel', 'done', 'received_from_vendor', 'delivered_to_cust'])]}"/> + <button name="action_done" string="Set Done" type="object" + class="btn-primary" + attrs="{'invisible': [('state', 'not in', ['received_from_vendor'])]}"/> + <button name="action_cancel" string="Cancel" type="object" + class="btn-secondary" + attrs="{'invisible': [('state', 'in', ['cancel', 'delivered_to_cust'])]}"/> + <button name="action_draft" string="Set to Backlog" type="object" + class="btn-secondary" + attrs="{'invisible': [('state', 'not in', ['cancel'])]}"/> + <field name="state" widget="statusbar" readonly="1"/> + </header> + <sheet> + <div class="oe_title"> + <h1> + <field name="name" readonly="1" class="oe_inline"/> + </h1> + </div> + <group> + <field name="origin" attrs="{'readonly': [('state', 'not in', ['draft'])]}"/> + <field name="partner_id"/> + <field name="vendor_id"/> + <field name="remaining_date"/> + <field name="schedule_date" attrs="{'readonly': [('state', 'not in', ['draft', 'reveived_from_cust'])]}"/> + <field name="start_date" readonly="1"/> + <field name="done_date" attrs="{'invisible': [('state', 'not in', ['delivered_to_cust'])]}"/> + <field name="create_uid"/> + <field name="cancel_reason" + attrs="{'invisible': [('state', 'in', ['delivered_to_cust', 'draft'])]}"/> + </group> + <notebook> + <page string="Product Lines" name="product_lines"> + <field name="gudang_service_lines"> + <tree string="Product Lines" editable="top" create="0" delete="1"> + <field name="product_id"/> + <field name="quantity"/> + </tree> + </field> + </page> + </notebook> + </sheet> + <div class="oe_chatter"> + <field name="message_follower_ids" widget="mail_followers"/> + <field name="message_ids" widget="mail_thread"/> + </div> + </form> + </field> + </record> + <!-- Action --> + <record id="action_gudang_service" model="ir.actions.act_window"> + <field name="name">Monitoring Barang Service</field> + <field name="type">ir.actions.act_window</field> + <field name="res_model">gudang.service</field> + <field name="view_mode">tree,form</field> + </record> + + <!-- Menu --> + <menuitem + id="menu_gudang_service" + name="Monitoring Barang Service" + parent="indoteknik_custom.menu_monitoring_in_sale" + sequence="10" + action="action_gudang_service" + /> + </data> + <!-- Cron --> + <record id="ir_cron_gudang_service_logistik_notify" model="ir.cron"> + <field name="name">Gudang Service Daily Notification</field> + <field name="model_id" ref="model_gudang_service"/> + <field name="state">code</field> + <field name="code">model.cron_notify_onprogress_gudang_service()</field> + <field name="interval_number">1</field> + <field name="interval_type">days</field> + <field name="numbercall">-1</field> + <field name="active">False</field> + </record> +</odoo> diff --git a/indoteknik_custom/views/ir_sequence.xml b/indoteknik_custom/views/ir_sequence.xml index 46148606..55e48300 100644 --- a/indoteknik_custom/views/ir_sequence.xml +++ b/indoteknik_custom/views/ir_sequence.xml @@ -260,5 +260,14 @@ <field name="number_next">1</field> <field name="number_increment">1</field> </record> + + <record id="seq_gudang_service" model="ir.sequence"> + <field name="name">Gudang Service</field> + <field name="code">gudang.service</field> + <field name="prefix">MGS/%(year)s/%(month)s/</field> + <field name="padding">4</field> + <field name="number_next">1</field> + <field name="number_increment">1</field> + </record> </data> </odoo>
\ No newline at end of file diff --git a/indoteknik_custom/views/kartu_stock.xml b/indoteknik_custom/views/kartu_stock.xml new file mode 100644 index 00000000..705d86a2 --- /dev/null +++ b/indoteknik_custom/views/kartu_stock.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<odoo> + <data> + <record id="kartu_stok_wizard_form_inherit_single_excel" model="ir.ui.view"> + <field name="name">kartu.stok.wizard.form.inherit.single.excel</field> + <field name="model">kartu.stok.wizard</field> + <field name="inherit_id" ref="proweb_kartu_stok.print_kartu_stok_view_form"/> + <field name="arch" type="xml"> + <xpath expr="//footer" position="inside"> + + <button name="action_kartu_stok_excel_single_sheet" type="object" string="Print Excel (Single Sheet)" class="btn-primary"/> + + </xpath> + </field> + </record> + </data> +</odoo>
\ No newline at end of file diff --git a/indoteknik_custom/views/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml index 59e317d2..9651cdd6 100755 --- a/indoteknik_custom/views/purchase_order.xml +++ b/indoteknik_custom/views/purchase_order.xml @@ -46,6 +46,19 @@ </button> </xpath> <button id="draft_confirm" position="after"> + <button name="action_create_order_altama" + type="object" + string="Create Order Altama" + class="oe_highlight" + icon="fa-cloud-upload" + attrs="{'invisible': [('partner_id', '!=', 5571)]}" + /> + <button name="action_get_order_altama" + type="object" + string="Get Order Altama" + class="oe_highlight" + attrs="{'invisible': [('partner_id', '!=', 5571)]}" + icon="fa-cloud-download"/> <button name="po_approve" string="Ask Approval" type="object" @@ -76,6 +89,10 @@ </field> <field name="approval_status" position="after"> <field name="revisi_po"/> + <field name="soo_number" attrs="{'invisible': [('partner_id', '!=', 5571)]}"/> + <field name="soo_price" attrs="{'invisible': [('partner_id', '!=', 5571)]}"/> + <field name="soo_discount" attrs="{'invisible': [('partner_id', '!=', 5571)]}"/> + <field name="soo_tax" attrs="{'invisible': [('partner_id', '!=', 5571)]}"/> <field name="not_update_purchasepricelist"/> </field> <field name="approval_status" position="after"> @@ -153,8 +170,13 @@ </field> <field name="price_unit" position="after"> <field name="price_vendor" attrs="{'readonly': 1}" optional="hide"/> + <field name="description" optional="hide"/> + <field name="docstatus_altama" optional="hide"/> </field> <field name="price_subtotal" position="after"> + <field name="altama_ordered" optional="hide" readonly="1"/> + <field name="altama_delivered" optional="hide" readonly="1"/> + <field name="altama_invoiced" optional="hide" readonly="1"/> <field name="so_item_margin" attrs="{'readonly': 1}" optional="hide"/> <field name="so_item_percent_margin" attrs="{'readonly': 1}" optional="hide"/> <field name="item_margin" attrs="{'readonly': 1}" optional="hide"/> diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml index ac871ead..33cd51e4 100755 --- a/indoteknik_custom/views/sale_order.xml +++ b/indoteknik_custom/views/sale_order.xml @@ -391,7 +391,7 @@ { 'readonly': [ '|', - ('approval_status', 'in', ['pengajuan1', 'pengajuan2', 'approved']), + ('approval_status', 'in', ['pengajuan0','pengajuan1', 'pengajuan2', 'approved']), ('state', 'not in', ['cancel', 'draft']) ] } diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml index 9cd63e25..29a95626 100644 --- a/indoteknik_custom/views/stock_picking.xml +++ b/indoteknik_custom/views/stock_picking.xml @@ -133,6 +133,7 @@ <field name="total_mapping_koli" attrs="{'invisible': [('location_id', '!=', 60)]}"/> <field name="total_koli_display" readonly="1" attrs="{'invisible': [('location_id', '!=', 60)]}"/> <field name="linked_out_picking_id" readonly="1" attrs="{'invisible': [('location_id', '=', 60)]}"/> + <field name="number_soo" attrs="{'invisible': [('picking_type_id', 'in', [29,30])]}"/> </field> <field name="weight_uom_name" position="after"> <group> diff --git a/indoteknik_custom/views/token_log.xml b/indoteknik_custom/views/token_log.xml new file mode 100644 index 00000000..77e6dd48 --- /dev/null +++ b/indoteknik_custom/views/token_log.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<odoo> + <record id="token_log_tree" model="ir.ui.view"> + <field name="name">token.log.tree</field> + <field name="model">token.log</field> + <field name="arch" type="xml"> + <tree editable="top" default_order="create_date desc"> + <field name="token"/> + <field name="expires_at"/> + <field name="token_from"/> + <field name="created_at"/> + <field name="created_by"/> + <field name="source"/> + <field name="is_active" widget="boolean_toggle"/> + </tree> + </field> + </record> + + <record id="token_log_action" model="ir.actions.act_window"> + <field name="name">Token Log</field> + <field name="type">ir.actions.act_window</field> + <field name="res_model">token.log</field> + <field name="view_mode">tree,form</field> + </record> + + <menuitem + action="token_log_action" + id="token_log" + parent="base.menu_users" + name="Token Log" + sequence="1" + /> +</odoo> |
