From d2bc3e1d20f196628edaf6956777ca332b8d5063 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 12 Jan 2026 15:02:52 +0700 Subject: create menu monitoring gudang service --- indoteknik_custom/models/__init__.py | 1 + indoteknik_custom/models/gudang_service.py | 37 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 indoteknik_custom/models/gudang_service.py (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index a14c766e..e6a59246 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -165,3 +165,4 @@ from . import partial_delivery from . import domain_apo from . import uom_uom from . import commission_internal +from . import gudang_service diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py new file mode 100644 index 00000000..a84f5c8a --- /dev/null +++ b/indoteknik_custom/models/gudang_service.py @@ -0,0 +1,37 @@ +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'] + + name = fields.Char('Name', readonly=True) + partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) + origin = fields.Char(string='Origin SO') + picking_id = fields.Many2one('stock.picking', string = 'Picking ID') + # origin_so = fields.Many2one('sale.order', string='Origin SO', compute='_compute_origin_so') + date = fields.Datetime('Date', default=fields.Datetime.now, required=True) + gudang_service_lines = fields.One2many('gudang.service.line', 'gudang_service_id', string='Gudang Service Lines') + + @api.model + def create(self, vals): + if not vals.get('name') or vals['name'] == 'New': + vals['name'] = self.env['ir.sequence'].next_by_code('gudang.service') + return super(GudangService, self).create(vals) + + +class GudangServiceLine(models.Model): + _name = "gudang.service.line" + _description = "Gudang Service Line" + _inherit = ['mail.thread', 'mail.activity.mixin'] + + product_id = fields.Many2one('product.product', string='Product') + quantity = fields.Float(string='Quantity') + picking_id = fields.Many2one('stock.picking', string = 'Nomor Picking') + # origin_so = fields.Many2one('sale.order', string='Origin SO', compute='_compute_origin_so') + gudang_service_id = fields.Many2one('gudang.service', string='Gudang Service ID') \ No newline at end of file -- cgit v1.2.3 From f77738e0c20c01a544dc233c12c1233793b45180 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 14 Jan 2026 17:01:56 +0700 Subject: done temp --- indoteknik_custom/models/gudang_service.py | 93 +++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 8 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index a84f5c8a..d9b32e91 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -12,26 +12,103 @@ class GudangService(models.Model): name = fields.Char('Name', readonly=True) partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) - origin = fields.Char(string='Origin SO') - picking_id = fields.Many2one('stock.picking', string = 'Picking ID') - # origin_so = fields.Many2one('sale.order', string='Origin SO', compute='_compute_origin_so') + origin = fields.Many2one('sale.order', string='Origin SO') + # picking_id = fields.Many2one('stock.picking', string = 'Picking ID') date = fields.Datetime('Date', default=fields.Datetime.now, required=True) gudang_service_lines = fields.One2many('gudang.service.line', 'gudang_service_id', string='Gudang Service Lines') + remaining_date = fields.Char('Remaining Date', compute='_compute_remaining_date') + state = fields.Selection([('draft', 'Draft'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft') + + def _compute_remaining_date(self): + if self.state in ['done', 'cancel', 'draft']: + return + for rec in self: + if rec.date: + rec.remaining_date = (datetime.now() - rec.date).days + + def send_odoo_notification(self): + return { + 'type': 'ir.actions.act_window', + 'res_model': 'mail.message', + 'view_mode': 'form', + 'view_type': 'form', + 'res_id': self.message_ids[-1].id, + } + + def action_submit(self): + self.state = 'onprogress' + # self.send_odoo_notification() + + def action_done(self): + self.state = 'done' + # self.send_odoo_notification() + + def action_draft(self): + """Reset to draft state""" + for record in self: + if record.state == 'cancel': + record.write({'state': 'draft'}) + else: + raise UserError("Hanya record yang di-cancel yang bisa dikembalikan ke draft") + def action_cancel(self): + self.state = 'cancel' + + # def write(vals, self): + # self.send_odoo_notification() + # return super(GudangService, self).write(vals) + + @api.depends('date') + def _compute_remaining_date(self): + for rec in self: + if rec.date: + rec.remaining_date = (datetime.now() - rec.date).days @api.model def create(self, vals): if not vals.get('name') or vals['name'] == 'New': vals['name'] = self.env['ir.sequence'].next_by_code('gudang.service') + + if vals.get('origin') and not vals.get('partner_id'): + so = self.env['sale.order'].browse(vals['origin']) + vals['partner_id'] = so.partner_id.id + return super(GudangService, self).create(vals) + + def write(self, vals): + if vals.get('origin'): + so = self.env['sale.order'].browse(vals['origin']) + vals['partner_id'] = so.partner_id.id + + 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" - _description = "Gudang Service Line" - _inherit = ['mail.thread', 'mail.activity.mixin'] product_id = fields.Many2one('product.product', string='Product') quantity = fields.Float(string='Quantity') - picking_id = fields.Many2one('stock.picking', string = 'Nomor Picking') - # origin_so = fields.Many2one('sale.order', string='Origin SO', compute='_compute_origin_so') - gudang_service_id = fields.Many2one('gudang.service', string='Gudang Service ID') \ No newline at end of file + # picking_id = fields.Many2one('stock.picking', string='Nomor Picking') + origin_so = fields.Many2one('sale.order', string='Origin SO') + gudang_service_id = fields.Many2one('gudang.service', string='Gudang Service ID') -- cgit v1.2.3 From e1c687df876eaa970cb40393cb4443abcfbd0b77 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 14 Jan 2026 18:55:22 +0700 Subject: add cron notification --- indoteknik_custom/models/gudang_service.py | 50 ++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 13 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index d9b32e91..6944f169 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -19,22 +19,45 @@ class GudangService(models.Model): remaining_date = fields.Char('Remaining Date', compute='_compute_remaining_date') state = fields.Selection([('draft', 'Draft'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft') - def _compute_remaining_date(self): - if self.state in ['done', 'cancel', 'draft']: + def _send_logistic_notification(self): + logistic_user = self.env.user.has_group('indoteknik_custom.group_role_logistic') + + if not logistic_user: return + for rec in self: - if rec.date: - rec.remaining_date = (datetime.now() - rec.date).days + for user in logistic_user: + 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': _( + 'Gudang Service %s masih On Progress sejak %s' + ) % (rec.name, rec.date), + 'date_deadline': fields.Date.today(), + }) + + @api.model + def cron_notify_onprogress_gudang_service(self): + records = self.search([ + ('state', '=', 'onprogress') + ]) + + if records: + records._send_logistic_notification() + + + @api.depends('date', 'state') + def _compute_remaining_date(self): + today = fields.Date.today() + for rec in self: + if rec.state in ['done', 'cancel', 'draft'] or not rec.date: + rec.remaining_date = 0 + continue + rec.remaining_date = (today - rec.date.date()).days - def send_odoo_notification(self): - return { - 'type': 'ir.actions.act_window', - 'res_model': 'mail.message', - 'view_mode': 'form', - 'view_type': 'form', - 'res_id': self.message_ids[-1].id, - } - def action_submit(self): self.state = 'onprogress' # self.send_odoo_notification() @@ -50,6 +73,7 @@ class GudangService(models.Model): record.write({'state': 'draft'}) else: raise UserError("Hanya record yang di-cancel yang bisa dikembalikan ke draft") + def action_cancel(self): self.state = 'cancel' -- cgit v1.2.3 From 4e1e2f93b0788c020bd3f1c1f802cf2f53997de5 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 14 Jan 2026 19:19:00 +0700 Subject: fix cron notif & add option to get product from picking --- indoteknik_custom/models/gudang_service.py | 36 +++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 10 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 6944f169..600febfd 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -13,20 +13,41 @@ class GudangService(models.Model): name = fields.Char('Name', readonly=True) partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) origin = fields.Many2one('sale.order', string='Origin SO') - # picking_id = fields.Many2one('stock.picking', string = 'Picking ID') + picking_id = fields.Many2one('stock.picking', string = 'Picking ID', domain="[('sale_id', '=', origin)]") date = fields.Datetime('Date', default=fields.Datetime.now, required=True) gudang_service_lines = fields.One2many('gudang.service.line', 'gudang_service_id', string='Gudang Service Lines') remaining_date = fields.Char('Remaining Date', compute='_compute_remaining_date') state = fields.Selection([('draft', 'Draft'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft') + @api.onchange('picking_id') + def _onchange_picking_id(self): + if not self.picking_id: + self.gudang_service_lines = [(5, 0, 0)] + return + + lines = [(5, 0, 0)] + for move in self.picking_id.move_ids_without_package: + if move.product_id: + lines.append((0, 0, { + 'product_id': move.product_id.id, + 'quantity': move.product_uom_qty, + 'origin_so': self.origin.id, + })) + + self.gudang_service_lines = lines + + def _send_logistic_notification(self): - logistic_user = self.env.user.has_group('indoteknik_custom.group_role_logistic') + group = self.env.ref('indoteknik_custom.group_role_logistic', raise_if_not_found=False) + if not group: + return - if not logistic_user: + users = group.users + if not users: return for rec in self: - for user in logistic_user: + for user in users: self.env['mail.activity'].create({ 'res_model_id': self.env['ir.model']._get_id('gudang.service'), 'res_id': rec.id, @@ -60,6 +81,7 @@ class GudangService(models.Model): def action_submit(self): self.state = 'onprogress' + self._send_logistic_notification # self.send_odoo_notification() def action_done(self): @@ -81,12 +103,6 @@ class GudangService(models.Model): # self.send_odoo_notification() # return super(GudangService, self).write(vals) - @api.depends('date') - def _compute_remaining_date(self): - for rec in self: - if rec.date: - rec.remaining_date = (datetime.now() - rec.date).days - @api.model def create(self, vals): if not vals.get('name') or vals['name'] == 'New': -- cgit v1.2.3 From 2e1e5b798cc5b859a0c10cbdb4e48cbbe0687112 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 15 Jan 2026 00:12:17 +0700 Subject: add cancel reason when trying to cancel and some decoration --- indoteknik_custom/models/gudang_service.py | 39 +++++++++++++++++------------- 1 file changed, 22 insertions(+), 17 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 600febfd..585563ca 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -12,29 +12,30 @@ class GudangService(models.Model): name = fields.Char('Name', readonly=True) partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) - origin = fields.Many2one('sale.order', string='Origin SO') - picking_id = fields.Many2one('stock.picking', string = 'Picking ID', domain="[('sale_id', '=', origin)]") + origin = fields.Many2one('sale.order', string='Origin SO', required=True) + # picking_id = fields.Many2one('stock.picking', string = 'Picking ID', domain="[('sale_id', '=', origin)]") date = fields.Datetime('Date', default=fields.Datetime.now, required=True) gudang_service_lines = fields.One2many('gudang.service.line', 'gudang_service_id', string='Gudang Service Lines') remaining_date = fields.Char('Remaining Date', compute='_compute_remaining_date') state = fields.Selection([('draft', 'Draft'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft') + cancel_reason = fields.Text('Cancel Reason') - @api.onchange('picking_id') - def _onchange_picking_id(self): - if not self.picking_id: - self.gudang_service_lines = [(5, 0, 0)] - return + # @api.onchange('picking_id') + # def _onchange_picking_id(self): + # if not self.picking_id: + # self.gudang_service_lines = [(5, 0, 0)] + # return - lines = [(5, 0, 0)] - for move in self.picking_id.move_ids_without_package: - if move.product_id: - lines.append((0, 0, { - 'product_id': move.product_id.id, - 'quantity': move.product_uom_qty, - 'origin_so': self.origin.id, - })) + # lines = [(5, 0, 0)] + # for move in self.picking_id.move_ids_without_package: + # if move.product_id: + # lines.append((0, 0, { + # 'product_id': move.product_id.id, + # 'quantity': move.product_uom_qty, + # 'origin_so': self.origin.id, + # })) - self.gudang_service_lines = lines + # self.gudang_service_lines = lines def _send_logistic_notification(self): @@ -94,9 +95,13 @@ class GudangService(models.Model): if record.state == 'cancel': record.write({'state': 'draft'}) else: - raise UserError("Hanya record yang di-cancel yang bisa dikembalikan ke draft") + raise UserError("Only Canceled Record Can Be Reset To Draft") def action_cancel(self): + if self.state == 'done': + raise UserError("You cannot cancel a done record") + if not self.cancel_reason: + raise UserError("Cancel Reason must be filled") self.state = 'cancel' # def write(vals, self): -- cgit v1.2.3 From b19788761f065be6593698d07f1e4743e151a8c6 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 15 Jan 2026 00:46:47 +0700 Subject: add done date & code improvement --- indoteknik_custom/models/gudang_service.py | 44 ++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 14 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 585563ca..2fe239ad 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -9,12 +9,14 @@ class GudangService(models.Model): _name = "gudang.service" _description = "Gudang Service" _inherit = ['mail.thread', 'mail.activity.mixin'] + _order = 'remaining_date desc, id asc' name = fields.Char('Name', readonly=True) partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) origin = fields.Many2one('sale.order', string='Origin SO', required=True) # picking_id = fields.Many2one('stock.picking', string = 'Picking ID', domain="[('sale_id', '=', origin)]") date = fields.Datetime('Date', default=fields.Datetime.now, required=True) + done_date = fields.Datetime(string='Date Done', copy=False) gudang_service_lines = fields.One2many('gudang.service.line', 'gudang_service_id', string='Gudang Service Lines') remaining_date = fields.Char('Remaining Date', compute='_compute_remaining_date') state = fields.Selection([('draft', 'Draft'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft') @@ -71,38 +73,52 @@ class GudangService(models.Model): records._send_logistic_notification() - @api.depends('date', 'state') + @api.depends('date', 'state', 'done_date') def _compute_remaining_date(self): today = fields.Date.today() for rec in self: - if rec.state in ['done', 'cancel', 'draft'] or not rec.date: + if not rec.date: rec.remaining_date = 0 continue + + if rec.state in ['draft', 'cancel']: + rec.remaining_date = 0 + continue + + if rec.state == 'done' and rec.done_date: + rec.remaining_date = (rec.done_date.date() - rec.date.date()).days + continue + rec.remaining_date = (today - rec.date.date()).days + def action_submit(self): - self.state = 'onprogress' + for rec in self: + rec.state = 'onprogress' + rec.date = fields.Datetime.now() self._send_logistic_notification - # self.send_odoo_notification() def action_done(self): - self.state = 'done' - # self.send_odoo_notification() + for rec in self: + rec.state = 'done' + if not rec.done_date: + rec.done_date = fields.Datetime.now() def action_draft(self): """Reset to draft state""" - for record in self: - if record.state == 'cancel': - record.write({'state': 'draft'}) + for rec in self: + if rec.state == 'cancel': + rec.write({'state': 'draft'}) else: raise UserError("Only Canceled Record Can Be Reset To Draft") def action_cancel(self): - if self.state == 'done': - raise UserError("You cannot cancel a done record") - if not self.cancel_reason: - raise UserError("Cancel Reason must be filled") - self.state = 'cancel' + for rec in self: + if rec.state == 'done': + raise UserError("You cannot cancel a done record") + if not rec.cancel_reason: + raise UserError("Cancel Reason must be filled") + rec.state = 'cancel' # def write(vals, self): # self.send_odoo_notification() -- cgit v1.2.3 From 4e372aad3010db13453b9e332c6f058fc8375a99 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 15 Jan 2026 00:52:34 +0700 Subject: add tracking --- indoteknik_custom/models/gudang_service.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 2fe239ad..06d08917 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -9,18 +9,18 @@ class GudangService(models.Model): _name = "gudang.service" _description = "Gudang Service" _inherit = ['mail.thread', 'mail.activity.mixin'] - _order = 'remaining_date desc, id asc' + _order = 'id asc' name = fields.Char('Name', readonly=True) partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) origin = fields.Many2one('sale.order', string='Origin SO', required=True) # picking_id = fields.Many2one('stock.picking', string = 'Picking ID', domain="[('sale_id', '=', origin)]") - date = fields.Datetime('Date', default=fields.Datetime.now, required=True) - done_date = fields.Datetime(string='Date Done', copy=False) + date = fields.Datetime('Date', default=fields.Datetime.now, tracking=True, copy=False) + 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') remaining_date = fields.Char('Remaining Date', compute='_compute_remaining_date') - state = fields.Selection([('draft', 'Draft'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft') - cancel_reason = fields.Text('Cancel Reason') + state = fields.Selection([('draft', 'Draft'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft', tracking=True) + cancel_reason = fields.Text('Cancel Reason', tracking=True) # @api.onchange('picking_id') # def _onchange_picking_id(self): -- cgit v1.2.3 From 3b400df6d9f630c4f20e518126afccf280ff62ac Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 15 Jan 2026 09:36:37 +0700 Subject: fix mail activity nyangkut --- indoteknik_custom/models/gudang_service.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 06d08917..2a8b589d 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -86,7 +86,8 @@ class GudangService(models.Model): continue if rec.state == 'done' and rec.done_date: - rec.remaining_date = (rec.done_date.date() - rec.date.date()).days + days = (rec.done_date.date() - rec.date.date()).days + rec.remaining_date = "Since %s days" % days continue rec.remaining_date = (today - rec.date.date()).days @@ -96,10 +97,16 @@ class GudangService(models.Model): for rec in self: rec.state = 'onprogress' rec.date = fields.Datetime.now() - self._send_logistic_notification + self._send_logistic_notification() def action_done(self): for rec in self: + activities = self.env['mail.activity'].search([ + ('res_id', '=', rec.id), + ('res_model', '=', 'gudang.service'), + ('state', '=', 'done') + ]) + activities.unlink() rec.state = 'done' if not rec.done_date: rec.done_date = fields.Datetime.now() @@ -167,9 +174,10 @@ class GudangService(models.Model): 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') # picking_id = fields.Many2one('stock.picking', string='Nomor Picking') origin_so = fields.Many2one('sale.order', string='Origin SO') - gudang_service_id = fields.Many2one('gudang.service', string='Gudang Service ID') + gudang_service_id = fields.Many2one('gudang.service', string='Gudang Service ID') \ No newline at end of file -- cgit v1.2.3 From 6a7c7c1ad694ba12f23b9a4cc5c5deca6ff52bd8 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 15 Jan 2026 13:21:44 +0700 Subject: push --- indoteknik_custom/models/stock_picking.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 2465fa96..c8f7bb5b 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1685,7 +1685,7 @@ class StockPicking(models.Model): ]) for line in po.order_sales_match_line: - if not line.bu_pick: + if not line.bu_pick and line.hold_outgoing_so: continue line.bu_pick.action_assign() -- cgit v1.2.3 From b2cbd45338fd26fb285f68e4c609284395ba9897 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 16 Jan 2026 15:38:15 +0700 Subject: remove from mail activity when docs canceled --- indoteknik_custom/models/gudang_service.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 2a8b589d..c65e599d 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -121,6 +121,11 @@ class GudangService(models.Model): 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 == 'done': raise UserError("You cannot cancel a done record") if not rec.cancel_reason: -- cgit v1.2.3 From 8de3619bc698551e7d23b463d4bcf55f9ad8b62c Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 19 Jan 2026 09:45:14 +0700 Subject: send private message --- indoteknik_custom/models/gudang_service.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 2a8b589d..5aeabb39 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -62,6 +62,11 @@ class GudangService(models.Model): ) % (rec.name, rec.date), 'date_deadline': fields.Date.today(), }) + 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.message_post(body=_('Gudang Service %s masih On Progress sejak %s') % (rec.name, rec.date), message_type='comment', subtype_xmlid='mail.mt_comment') @api.model def cron_notify_onprogress_gudang_service(self): -- cgit v1.2.3 From b9b6d10b518bd9bd80c85216de7ef33cf194f9ba Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 19 Jan 2026 10:37:24 +0700 Subject: add send with user --- indoteknik_custom/models/gudang_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 5aeabb39..8edf57d0 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -62,11 +62,13 @@ class GudangService(models.Model): ) % (rec.name, rec.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.message_post(body=_('Gudang Service %s masih On Progress sejak %s') % (rec.name, rec.date), message_type='comment', subtype_xmlid='mail.mt_comment') + res.with_user(self.env.user.browse(25)).message_post(body=_('Gudang Service %s masih On Progress sejak %s') % (rec.name, rec.date), message_type='comment', subtype_xmlid='mail.mt_comment') @api.model def cron_notify_onprogress_gudang_service(self): -- cgit v1.2.3 From 9633f55a90d8e70d754e4611c9bfc84d7643f89a Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 19 Jan 2026 11:10:25 +0700 Subject: add MD and exclude some users --- indoteknik_custom/models/gudang_service.py | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 1aadea70..93f8805a 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -46,11 +46,20 @@ class GudangService(models.Model): return users = group.users + # Safa + md = self.env['res.users'].browse([3425]) + # send to logistic and safa + users = users | md + if not users: return + 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, -- cgit v1.2.3 From b40989dc9dc36056f4e416aa5d2e040be595fd61 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 19 Jan 2026 15:22:22 +0700 Subject: done --- indoteknik_custom/models/gudang_service.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 93f8805a..0cdee5af 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -19,7 +19,7 @@ class GudangService(models.Model): 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') remaining_date = fields.Char('Remaining Date', compute='_compute_remaining_date') - state = fields.Selection([('draft', 'Draft'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft', tracking=True) + state = fields.Selection([('draft', 'Backlog'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft', tracking=True) cancel_reason = fields.Text('Cancel Reason', tracking=True) # @api.onchange('picking_id') @@ -113,7 +113,6 @@ class GudangService(models.Model): for rec in self: rec.state = 'onprogress' rec.date = fields.Datetime.now() - self._send_logistic_notification() def action_done(self): for rec in self: @@ -130,6 +129,7 @@ class GudangService(models.Model): 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: @@ -148,12 +148,11 @@ class GudangService(models.Model): raise UserError("Cancel Reason must be filled") rec.state = 'cancel' - # def write(vals, self): - # self.send_odoo_notification() - # return super(GudangService, self).write(vals) @api.model def create(self, vals): + # Send notification + self._send_logistic_notification() if not vals.get('name') or vals['name'] == 'New': vals['name'] = self.env['ir.sequence'].next_by_code('gudang.service') -- cgit v1.2.3 From bbffc7f7092ad28994d511b3b7022e55b07ec2a8 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 19 Jan 2026 22:25:00 +0700 Subject: revisi date --- indoteknik_custom/models/gudang_service.py | 55 +++++++++++++++++++----------- 1 file changed, 35 insertions(+), 20 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 0cdee5af..e401a65c 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -15,7 +15,18 @@ class GudangService(models.Model): partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) origin = fields.Many2one('sale.order', string='Origin SO', required=True) # picking_id = fields.Many2one('stock.picking', string = 'Picking ID', domain="[('sale_id', '=', origin)]") - date = fields.Datetime('Date', default=fields.Datetime.now, tracking=True, copy=False) + schedule_date = fields.Date( + string="Schedule Date", + required=True, + tracking=True + ) + + start_date = fields.Datetime( + string="Date Processed", + copy=False, + tracking=True + ) + 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') remaining_date = fields.Char('Remaining Date', compute='_compute_remaining_date') @@ -67,9 +78,9 @@ class GudangService(models.Model): 'user_id': user.id, 'summary': 'Gudang Service On Progress', 'note': _( - 'Gudang Service %s masih On Progress sejak %s' - ) % (rec.name, rec.date), - 'date_deadline': fields.Date.today(), + 'Ada Jadwal Service Barang di Document %s Jadwal Service 📅 %s' + ) % (rec.name, rec.schedule_date), + # 'date_deadline': fields.Date.today(), }) # kirim ke private message odoo @@ -77,7 +88,7 @@ class GudangService(models.Model): if not channel: continue res = self.env['mail.channel'].browse(channel) - res.with_user(self.env.user.browse(25)).message_post(body=_('Gudang Service %s masih On Progress sejak %s') % (rec.name, rec.date), message_type='comment', subtype_xmlid='mail.mt_comment') + res.with_user(self.env.user.browse(25)).message_post(body=_('Ada Jadwal Service Barang di Document %s Jadwal Service 📅 %s') % (rec.name, rec.schedule_date), message_type='comment', subtype_xmlid='mail.mt_comment') @api.model def cron_notify_onprogress_gudang_service(self): @@ -89,30 +100,32 @@ class GudangService(models.Model): records._send_logistic_notification() - @api.depends('date', 'state', 'done_date') + @api.depends('start_date', 'done_date', 'state') def _compute_remaining_date(self): today = fields.Date.today() + for rec in self: - if not rec.date: - rec.remaining_date = 0 + if not rec.start_date: + rec.remaining_date = "-" continue - if rec.state in ['draft', 'cancel']: - rec.remaining_date = 0 - continue + start = rec.start_date.date() if rec.state == 'done' and rec.done_date: - days = (rec.done_date.date() - rec.date.date()).days - rec.remaining_date = "Since %s days" % days - continue + end = rec.done_date.date() + else: + end = today + + days = (end - start).days + rec.remaining_date = _("Since %s days") % days - rec.remaining_date = (today - rec.date.date()).days def action_submit(self): for rec in self: rec.state = 'onprogress' - rec.date = fields.Datetime.now() + rec.start_date = fields.Datetime.now() + # rec.date = fields.Datetime.now() def action_done(self): for rec in self: @@ -146,13 +159,14 @@ class GudangService(models.Model): 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): # Send notification - self._send_logistic_notification() if not vals.get('name') or vals['name'] == 'New': vals['name'] = self.env['ir.sequence'].next_by_code('gudang.service') @@ -160,7 +174,10 @@ class GudangService(models.Model): so = self.env['sale.order'].browse(vals['origin']) vals['partner_id'] = so.partner_id.id - return super(GudangService, self).create(vals) + res = super(GudangService, self).create(vals) + # Send notification + res._send_logistic_notification() + return res def write(self, vals): if vals.get('origin'): @@ -169,8 +186,6 @@ class GudangService(models.Model): return super(GudangService, self).write(vals) - - @api.onchange('origin') def _onchange_origin(self): if not self.origin: -- cgit v1.2.3 From 7e52c025439a8614a1ec6ae77dfdda934989a09b Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 19 Jan 2026 22:26:55 +0700 Subject: change string --- indoteknik_custom/models/gudang_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index e401a65c..d4f7397c 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -29,7 +29,7 @@ class GudangService(models.Model): 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') - remaining_date = fields.Char('Remaining Date', compute='_compute_remaining_date') + remaining_date = fields.Char('Unprocessed Since', compute='_compute_remaining_date') state = fields.Selection([('draft', 'Backlog'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft', tracking=True) cancel_reason = fields.Text('Cancel Reason', tracking=True) @@ -117,7 +117,7 @@ class GudangService(models.Model): end = today days = (end - start).days - rec.remaining_date = _("Since %s days") % days + rec.remaining_date = _("%s days") % days -- cgit v1.2.3 From 2e708e7a98aca710b006fbc249366e7cea151b78 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 20 Jan 2026 08:52:18 +0700 Subject: cleanup code --- indoteknik_custom/models/gudang_service.py | 25 ------------------------- 1 file changed, 25 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index d4f7397c..73dcf0a4 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -14,7 +14,6 @@ class GudangService(models.Model): name = fields.Char('Name', readonly=True) partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) origin = fields.Many2one('sale.order', string='Origin SO', required=True) - # picking_id = fields.Many2one('stock.picking', string = 'Picking ID', domain="[('sale_id', '=', origin)]") schedule_date = fields.Date( string="Schedule Date", required=True, @@ -33,24 +32,6 @@ class GudangService(models.Model): state = fields.Selection([('draft', 'Backlog'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft', tracking=True) cancel_reason = fields.Text('Cancel Reason', tracking=True) - # @api.onchange('picking_id') - # def _onchange_picking_id(self): - # if not self.picking_id: - # self.gudang_service_lines = [(5, 0, 0)] - # return - - # lines = [(5, 0, 0)] - # for move in self.picking_id.move_ids_without_package: - # if move.product_id: - # lines.append((0, 0, { - # 'product_id': move.product_id.id, - # 'quantity': move.product_uom_qty, - # 'origin_so': self.origin.id, - # })) - - # self.gudang_service_lines = lines - - def _send_logistic_notification(self): group = self.env.ref('indoteknik_custom.group_role_logistic', raise_if_not_found=False) if not group: @@ -99,7 +80,6 @@ class GudangService(models.Model): if records: records._send_logistic_notification() - @api.depends('start_date', 'done_date', 'state') def _compute_remaining_date(self): today = fields.Date.today() @@ -119,8 +99,6 @@ class GudangService(models.Model): days = (end - start).days rec.remaining_date = _("%s days") % days - - def action_submit(self): for rec in self: rec.state = 'onprogress' @@ -163,7 +141,6 @@ class GudangService(models.Model): rec.done_date = False rec.state = 'cancel' - @api.model def create(self, vals): # Send notification @@ -206,13 +183,11 @@ class GudangService(models.Model): 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') - # picking_id = fields.Many2one('stock.picking', string='Nomor Picking') origin_so = fields.Many2one('sale.order', string='Origin SO') gudang_service_id = fields.Many2one('gudang.service', string='Gudang Service ID') \ No newline at end of file -- cgit v1.2.3 From 144c65b3bf3e0971c2f99413f321e65f0c2cbac1 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 20 Jan 2026 08:57:11 +0700 Subject: send message when backlog/draft --- indoteknik_custom/models/gudang_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 73dcf0a4..dc13a139 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -74,7 +74,7 @@ class GudangService(models.Model): @api.model def cron_notify_onprogress_gudang_service(self): records = self.search([ - ('state', '=', 'onprogress') + ('state', '=', 'draft') ]) if records: -- cgit v1.2.3 From ecd50c41bc9db2697865dfe1c55bb32b7c9410c0 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 22 Jan 2026 17:04:08 +0700 Subject: add vendor --- indoteknik_custom/models/gudang_service.py | 53 +++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 12 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index d4f7397c..72a0f00e 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -13,6 +13,7 @@ class GudangService(models.Model): name = fields.Char('Name', readonly=True) partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) + vendor_id = fields.Many2one('res.partner', string='Vendor', readonly=True, required=True) origin = fields.Many2one('sale.order', string='Origin SO', required=True) # picking_id = fields.Many2one('stock.picking', string = 'Picking ID', domain="[('sale_id', '=', origin)]") schedule_date = fields.Date( @@ -26,10 +27,17 @@ class GudangService(models.Model): 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') - remaining_date = fields.Char('Unprocessed Since', compute='_compute_remaining_date') + # unprocessed_date = fields.Char( + # string='Unprocessed Since', + # compute='_compute_unprocessed_date' + # ) + remaining_date = fields.Char( + compute='_compute_remaining_date', + string='Remaining Date' + ) state = fields.Selection([('draft', 'Backlog'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft', tracking=True) cancel_reason = fields.Text('Cancel Reason', tracking=True) @@ -99,26 +107,47 @@ class GudangService(models.Model): if records: records._send_logistic_notification() + # @api.depends('schedule_date', 'start_date', 'state') + # def _compute_unprocessed_date(self): + # today = fields.Date.today() + + # for rec in self: + # if not rec.schedule_date or rec.state == 'cancel': + # rec.unprocessed_date = "-" + # continue + + # schedule = rec.schedule_date + + # # BELUM DIPROSES + # if not rec.start_date: + # days = (today - schedule).days - @api.depends('start_date', 'done_date', 'state') + # # SUDAH DIPROSES + # else: + # days = (rec.start_date.date() - schedule).days + + # rec.unprocessed_date = _("Unprocessed %s days") % max(days, 0) + + @api.depends('schedule_date', 'create_date') def _compute_remaining_date(self): today = fields.Date.today() for rec in self: - if not rec.start_date: + if not rec.schedule_date: rec.remaining_date = "-" continue - start = rec.start_date.date() + base_date = rec.create_date.date() if rec.create_date else today - if rec.state == 'done' and rec.done_date: - end = rec.done_date.date() - else: - end = today - - days = (end - start).days - rec.remaining_date = _("%s days") % days + 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): -- cgit v1.2.3 From 24b33148858c9827ad0b14e716562c377e1644ca Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 30 Jan 2026 16:05:57 +0700 Subject: Initial integration to ccm --- indoteknik_custom/models/gudang_service.py | 48 ++++++++++++++++++++--- indoteknik_custom/models/tukar_guling.py | 61 ++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 6 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index ce3b2919..1056c4c8 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -20,7 +20,6 @@ class GudangService(models.Model): required=True, tracking=True ) - start_date = fields.Datetime( string="Date Processed", copy=False, @@ -54,6 +53,7 @@ class GudangService(models.Model): if not users: return + # Logistic users to be excluded excluded_users = [7, 17098, 216, 28, 15710] for rec in self: @@ -175,16 +175,52 @@ class GudangService(models.Model): @api.model def create(self, vals): - # Send notification + # sequence if not vals.get('name') or vals['name'] == 'New': vals['name'] = self.env['ir.sequence'].next_by_code('gudang.service') - if vals.get('origin') and not vals.get('partner_id'): - so = self.env['sale.order'].browse(vals['origin']) - vals['partner_id'] = so.partner_id.id + # partner dari SO + so = self.env['sale.order'].browse(vals['origin']) + vals['partner_id'] = so.partner_id.id + + # 1️⃣ Cari BU/OUT dari SO + picking = self.env['stock.picking'].search([ + ('origin', '=', so.name), + ('picking_type_id', '=', 29), # BU/OUT + ('state', '=', 'done'), + ], limit=1) + + if not picking: + raise UserError("BU/OUT (done) tidak ditemukan untuk SO ini.") + + # 2️⃣ Validasi product service ada di picking + picking_products = picking.move_lines.mapped('product_id') + service_lines = vals.get('gudang_service_lines', []) + + tg_lines = [] + for line in service_lines: + product = self.env['product.product'].browse(line[2]['product_id']) + if product not in picking_products: + raise UserError( + f"Produk {product.display_name} tidak ada di BU/OUT {picking.name}" + ) + + tg_lines.append((0, 0, { + 'product_id': product.id, + 'quantity': line[2]['quantity'], + })) + + # 3️⃣ Create Tukar Guling + self.env['tukar.guling'].create({ + 'origin_so': so.id, + 'operation_type': 'service', + 'partner_id': so.partner_id.id, + 'operations': picking.id, + 'line_ids': tg_lines, + }) res = super(GudangService, self).create(vals) - # Send notification + res._send_logistic_notification() return res diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 682c478a..03af984f 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"📦 {new_pick.name} created by {self.env.user.name} (state: {new_pick.state})") + + 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: -- cgit v1.2.3 From 12c5971f46f3527ed9a8e1d865430d2b9fa25b38 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 2 Feb 2026 11:13:40 +0700 Subject: Initial Commit --- indoteknik_custom/models/gudang_service.py | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index ce3b2919..55e27b65 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -35,7 +35,7 @@ class GudangService(models.Model): # ) remaining_date = fields.Char( compute='_compute_remaining_date', - string='Remaining Date' + string='Date Status' ) state = fields.Selection([('draft', 'Backlog'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft', tracking=True) cancel_reason = fields.Text('Cancel Reason', tracking=True) @@ -88,27 +88,6 @@ class GudangService(models.Model): if records: records._send_logistic_notification() - # @api.depends('schedule_date', 'start_date', 'state') - # def _compute_unprocessed_date(self): - # today = fields.Date.today() - - # for rec in self: - # if not rec.schedule_date or rec.state == 'cancel': - # rec.unprocessed_date = "-" - # continue - - # schedule = rec.schedule_date - - # # BELUM DIPROSES - # if not rec.start_date: - # days = (today - schedule).days - - # # SUDAH DIPROSES - # else: - # days = (rec.start_date.date() - schedule).days - - # rec.unprocessed_date = _("Unprocessed %s days") % max(days, 0) - @api.depends('schedule_date', 'create_date') def _compute_remaining_date(self): today = fields.Date.today() -- cgit v1.2.3 From b4c46fd025ce765acdf9efd3e44f475b6e83d747 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 10 Feb 2026 09:50:21 +0700 Subject: rev --- indoteknik_custom/models/gudang_service.py | 48 +++--------- indoteknik_custom/models/tukar_guling.py | 116 ++++++++++++++--------------- 2 files changed, 67 insertions(+), 97 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 70ff27c0..3f483a56 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -13,8 +13,8 @@ class GudangService(models.Model): name = fields.Char('Name', readonly=True) partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) - vendor_id = fields.Many2one('res.partner', string='Vendor', readonly=True, required=True) - origin = fields.Many2one('sale.order', string='Origin SO', required=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, @@ -36,7 +36,13 @@ class GudangService(models.Model): compute='_compute_remaining_date', string='Date Status' ) - state = fields.Selection([('draft', 'Backlog'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft', tracking=True) + 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'), + ('delived_to_cust', 'Delivered to Customer'), + ('cancel', 'Cancel')], default='draft', tracking=True) cancel_reason = fields.Text('Cancel Reason', tracking=True) def _send_logistic_notification(self): @@ -162,42 +168,6 @@ class GudangService(models.Model): so = self.env['sale.order'].browse(vals['origin']) vals['partner_id'] = so.partner_id.id - # 1️⃣ Cari BU/OUT dari SO - picking = self.env['stock.picking'].search([ - ('origin', '=', so.name), - ('picking_type_id', '=', 29), # BU/OUT - ('state', '=', 'done'), - ], limit=1) - - if not picking: - raise UserError("BU/OUT (done) tidak ditemukan untuk SO ini.") - - # 2️⃣ Validasi product service ada di picking - picking_products = picking.move_lines.mapped('product_id') - service_lines = vals.get('gudang_service_lines', []) - - tg_lines = [] - for line in service_lines: - product = self.env['product.product'].browse(line[2]['product_id']) - if product not in picking_products: - raise UserError( - f"Produk {product.display_name} tidak ada di BU/OUT {picking.name}" - ) - - tg_lines.append((0, 0, { - 'product_id': product.id, - 'quantity': line[2]['quantity'], - })) - - # 3️⃣ Create Tukar Guling - self.env['tukar.guling'].create({ - 'origin_so': so.id, - 'operation_type': 'service', - 'partner_id': so.partner_id.id, - 'operations': picking.id, - 'line_ids': tg_lines, - }) - res = super(GudangService, self).create(vals) res._send_logistic_notification() diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 03af984f..619e7c99 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -62,7 +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 + # ('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'), @@ -933,64 +933,64 @@ class TukarGuling(models.Model): record.message_post( body=f"📦 {new_pick.name} created by {self.env.user.name} (state: {new_pick.state})") - 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") + # 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") + # 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 -- cgit v1.2.3 From 7aadac577c7bdacf1fab54c23fe50b04fba08393 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 10 Feb 2026 15:06:17 +0700 Subject: fix state --- indoteknik_custom/models/gudang_service.py | 37 ++++++++++++++++++------------ 1 file changed, 22 insertions(+), 15 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 3f483a56..4e3b2c34 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -41,7 +41,7 @@ class GudangService(models.Model): ('received_from_cust', 'Received From Customer'), ('sent_to_vendor', 'Sent to Service Vendor'), ('received_from_vendor', 'Received From Service Vendor'), - ('delived_to_cust', 'Delivered to Customer'), + ('delivered_to_cust', 'Delivered to Customer'), ('cancel', 'Cancel')], default='draft', tracking=True) cancel_reason = fields.Text('Cancel Reason', tracking=True) @@ -88,7 +88,7 @@ class GudangService(models.Model): @api.model def cron_notify_onprogress_gudang_service(self): records = self.search([ - ('state', '=', 'draft') + ('state', 'in', ['draft', 'received_from_customer']), ]) if records: @@ -118,21 +118,28 @@ class GudangService(models.Model): def action_submit(self): for rec in self: - rec.state = 'onprogress' - rec.start_date = fields.Datetime.now() - # rec.date = fields.Datetime.now() + 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: - activities = self.env['mail.activity'].search([ - ('res_id', '=', rec.id), - ('res_model', '=', 'gudang.service'), - ('state', '=', 'done') - ]) - activities.unlink() - rec.state = 'done' - if not rec.done_date: - rec.done_date = fields.Datetime.now() + if self.state != 'received_from_vendor': + raise UserError("Only 'Received From Vendor' state can be set to Done") + else: + for rec in self: + activities = self.env['mail.activity'].search([ + ('res_id', '=', rec.id), + ('res_model', '=', 'gudang.service'), + ('state', '=', 'delivered_to_cust'), + ]) + activities.unlink() + rec.state = 'delivered_to_cust' + if not rec.done_date: + rec.done_date = fields.Datetime.now() def action_draft(self): """Reset to draft state""" -- cgit v1.2.3 From dc2baa117413f3140adf99c8a398da3d7637c3d3 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 10 Feb 2026 18:26:52 +0700 Subject: push --- indoteknik_custom/models/gudang_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 4e3b2c34..4900113f 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -157,7 +157,7 @@ class GudangService(models.Model): ('res_model', '=', 'gudang.service'), ]) activities.unlink() - if rec.state == 'done': + 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") -- cgit v1.2.3 From 30e7aae41d50528a4e61e6f55f38e3dd647e41c6 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Sat, 14 Feb 2026 11:14:22 +0700 Subject: merge --- indoteknik_custom/models/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index 2b1851ce..3b3dcd9d 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -165,8 +165,5 @@ from . import partial_delivery from . import domain_apo from . import uom_uom from . import commission_internal -<<<<<<< HEAD from . import gudang_service -======= -from . import update_depreciation_move_wizard ->>>>>>> 37e0beac646ee2e676ff935e8289cf3189b3c21b +from . import update_depreciation_move_wizard \ No newline at end of file -- cgit v1.2.3 From d951091bcb1bcab9d5aedf8060bbc1cd8905326c Mon Sep 17 00:00:00 2001 From: Mqdd Date: Sun, 15 Feb 2026 00:05:12 +0700 Subject: fix state typo & code improvement --- indoteknik_custom/models/gudang_service.py | 65 ++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 17 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 4900113f..5d89cfad 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -45,15 +45,49 @@ class GudangService(models.Model): ('cancel', 'Cancel')], default='draft', tracking=True) cancel_reason = fields.Text('Cancel Reason', tracking=True) + @api.constrains('gudang_service_lines') + def _check_qty(self): + for rec in self: + if not rec.origin: + continue + + so_qty_map = { + line.product_id.id: line.product_uom_qty + for line in rec.origin.order_line + } + + for line in rec.gudang_service_lines: + if line.quantity <= 0: + raise ValidationError( + f"Quantity for product {line.product_id.display_name} cannot be 0 or negative." + ) + + so_qty = so_qty_map.get(line.product_id.id, 0) + + if line.quantity > so_qty: + raise ValidationError( + f"Quantity for product {line.product_id.display_name} " + f"cannot exceed SO quantity ({so_qty})." + ) + + + @api.constrains('state', 'schedule_date') + def _check_edit_after_sent(self): + for rec in self: + if rec.state in ['sent_to_vendor', 'received_from_vendor', 'delivered_to_cust']: + if rec._origin.schedule_date != rec.schedule_date: + raise ValidationError("Schedule cannot be modified after sent to vendor") + + 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 - # Safa - md = self.env['res.users'].browse([3425]) - # send to logistic and safa + # MD + md = self.env['res.users'].browse([3425, 4801, 1036]) + # send to logistic and MD users = users | md if not users: @@ -88,7 +122,7 @@ class GudangService(models.Model): @api.model def cron_notify_onprogress_gudang_service(self): records = self.search([ - ('state', 'in', ['draft', 'received_from_customer']), + ('state', 'in', ['draft', 'received_from_cust']), ]) if records: @@ -127,19 +161,16 @@ class GudangService(models.Model): rec.state = 'received_from_vendor' def action_done(self): - if self.state != 'received_from_vendor': - raise UserError("Only 'Received From Vendor' state can be set to Done") - else: - for rec in self: - activities = self.env['mail.activity'].search([ - ('res_id', '=', rec.id), - ('res_model', '=', 'gudang.service'), - ('state', '=', 'delivered_to_cust'), - ]) - activities.unlink() - rec.state = 'delivered_to_cust' - if not rec.done_date: - rec.done_date = fields.Datetime.now() + 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""" -- cgit v1.2.3 From 2cf62d4c23e8fbd0770ba05cb4d1f65032bccdf0 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 16 Feb 2026 13:33:02 +0700 Subject: check duplicated mgs docs --- indoteknik_custom/models/gudang_service.py | 239 ++++++++++++++++++----------- 1 file changed, 150 insertions(+), 89 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 4900113f..b7ccc44f 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -8,51 +8,81 @@ 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", + _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, - tracking=True + domain=[("state", "in", ["done", "sale"])], ) - start_date = fields.Datetime( - string="Date Processed", - copy=False, - tracking=True + 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" ) - 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' + 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, ) - 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) + 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), + ( + "gudang_service_lines.product_id.name", + "=", + rec.gudang_service_lines.product_id.name, + ), + ( + "gudang_service_lines.quantity", + "=", + rec.gudang_service_lines.quantity, + ), + ("vendor_id.id", "=", rec.vendor_id.id), + ] + ) + 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) + group = self.env.ref( + "indoteknik_custom.group_role_logistic", raise_if_not_found=False + ) if not group: return users = group.users # Safa - md = self.env['res.users'].browse([3425]) + md = self.env["res.users"].browse([3425]) # send to logistic and safa users = users | md @@ -66,35 +96,53 @@ class GudangService(models.Model): 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 %s Jadwal Service 📅 %s' - ) % (rec.name, rec.schedule_date), - # 'date_deadline': fields.Date.today(), - }) + 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 %s Jadwal Service 📅 %s" + ) + % (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') + 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 %s Jadwal Service 📅 %s') % (rec.name, rec.schedule_date), message_type='comment', subtype_xmlid='mail.mt_comment') + res = self.env["mail.channel"].browse(channel) + res.with_user(self.env.user.browse(25)).message_post( + body=_( + "Ada Jadwal Service Barang di Document %s Jadwal Service 📅 %s" + ) + % (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']), - ]) + records = self.search( + [ + ("state", "in", ["draft", "received_from_customer"]), + ] + ) if records: records._send_logistic_notification() - @api.depends('schedule_date', 'create_date') + @api.depends("schedule_date", "create_date") def _compute_remaining_date(self): today = fields.Date.today() @@ -115,29 +163,32 @@ class GudangService(models.Model): 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' + 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' + elif rec.state == "sent_to_vendor": + rec.state = "received_from_vendor" def action_done(self): - if self.state != 'received_from_vendor': + if self.state != "received_from_vendor": raise UserError("Only 'Received From Vendor' state can be set to Done") else: for rec in self: - activities = self.env['mail.activity'].search([ - ('res_id', '=', rec.id), - ('res_model', '=', 'gudang.service'), - ('state', '=', 'delivered_to_cust'), - ]) + activities = self.env["mail.activity"].search( + [ + ("res_id", "=", rec.id), + ("res_model", "=", "gudang.service"), + ("state", "=", "delivered_to_cust"), + ] + ) activities.unlink() - rec.state = 'delivered_to_cust' + rec.state = "delivered_to_cust" if not rec.done_date: rec.done_date = fields.Datetime.now() @@ -145,63 +196,73 @@ class GudangService(models.Model): """Reset to draft state""" for rec in self: rec.cancel_reason = False - if rec.state == 'cancel': - rec.write({'state': 'draft'}) + 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 = self.env["mail.activity"].search( + [ + ("res_id", "=", rec.id), + ("res_model", "=", "gudang.service"), + ] + ) activities.unlink() - if rec.state == 'delivered_to_cust': + 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' + 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') + 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 + 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 + 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') + @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, - })) + 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 @@ -209,9 +270,9 @@ class GudangService(models.Model): class GudangServiceLine(models.Model): _name = "gudang.service.line" - _inherit = ['mail.thread', 'mail.activity.mixin'] + _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') \ No newline at end of file + 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") -- cgit v1.2.3 From 3b0686c3cd83dde114359e5a441d2d7b2c0ebc3f Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 16 Feb 2026 14:39:52 +0700 Subject: add more validation in duplicate docs --- indoteknik_custom/models/gudang_service.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index bd8ec8bd..d699ccf4 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -57,6 +57,8 @@ class GudangService(models.Model): ("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", "=", @@ -67,11 +69,10 @@ class GudangService(models.Model): "=", rec.gudang_service_lines.quantity, ), - ("vendor_id.id", "=", rec.vendor_id.id), ] ) if found: - raise UserError("This Document has duplicate with %s" % found.name) + raise UserError("This Document has duplicate with %s" % found.name) def _send_logistic_notification(self): group = self.env.ref( @@ -82,7 +83,7 @@ class GudangService(models.Model): users = group.users # MD - md = self.env['res.users'].browse([3425, 4801, 1036]) + md = self.env["res.users"].browse([3425, 4801, 1036]) # send to logistic and MD users = users | md @@ -177,15 +178,14 @@ class GudangService(models.Model): def action_done(self): for rec in self: - if rec.state != 'received_from_vendor': + 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() - }) + rec.write( + {"state": "delivered_to_cust", "done_date": fields.Datetime.now()} + ) def action_draft(self): """Reset to draft state""" -- cgit v1.2.3 From 7456c70b70749acbbe3f6545ee911a754a59f89b Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Thu, 19 Feb 2026 10:28:45 +0700 Subject: (andri) add estimate biaya pum --- .../models/advance_payment_request.py | 34 +++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/advance_payment_request.py b/indoteknik_custom/models/advance_payment_request.py index 8cadb1b6..18953c7c 100644 --- a/indoteknik_custom/models/advance_payment_request.py +++ b/indoteknik_custom/models/advance_payment_request.py @@ -155,6 +155,16 @@ 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' and rec.estimate_line_ids: + 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': @@ -1144,6 +1154,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 +1627,21 @@ 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='keTambahan', help='Deskripsi tambahan untuk estimasi, misalnya tujuan parkir atau rute tol.') + 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') -- cgit v1.2.3 From 10525d407bbc34d79146ff22955a874509f5b204 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Thu, 19 Feb 2026 15:55:27 +0700 Subject: (andri) compute estimate dengan realisasi --- .../models/advance_payment_request.py | 60 +++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/advance_payment_request.py b/indoteknik_custom/models/advance_payment_request.py index 18953c7c..ac9d7c2d 100644 --- a/indoteknik_custom/models/advance_payment_request.py +++ b/indoteknik_custom/models/advance_payment_request.py @@ -921,6 +921,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 @@ -1642,6 +1671,35 @@ class AdvancePaymentRequestEstimateLine(models.Model): ('konsumsi', 'Konsumsi'), ('lain_lain', 'Lain-lain'), ], string='Kategori Estimasi', required=True) - description = fields.Text(string='keTambahan', help='Deskripsi tambahan untuk estimasi, misalnya tujuan parkir atau rute tol.') + 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 + -- cgit v1.2.3 From 7394cd71c05d56ebfd5a5f95518a849289a782e0 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 20 Feb 2026 10:17:13 +0700 Subject: fix not sync to solr when there is no products --- indoteknik_custom/models/keywords.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 4ab649dc..9c8e2cfd 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 not record.skip and len(record.product_ids) > 0: record.solr_flag = 2 def solr_flag_to_queue(self, limit=500): @@ -164,6 +164,11 @@ class Keywords(models.Model): """Callback method executed by apache.solr.queue - syncs keyword data to Solr""" documents = [] for keyword in self: + # 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 = { @@ -194,6 +199,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 = { @@ -227,6 +237,8 @@ class Keywords(models.Model): neded_sync = any(field in vals for field in tracked_fields) if neded_sync and self.skip == False: 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 -- cgit v1.2.3 From 62c366e5cf58567b61700d2922b864e348e2a44b Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 20 Feb 2026 11:14:23 +0700 Subject: remove add to keywords when creating a product --- indoteknik_custom/models/product_template.py | 31 +--------------------------- 1 file changed, 1 insertion(+), 30 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/product_template.py b/indoteknik_custom/models/product_template.py index 397bd06d..e10b4de2 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): @@ -946,26 +940,7 @@ class ProductProduct(models.Model): qr_code_variant = fields.Binary("QR Code Variant", compute='_compute_qr_code_variant') qty_pcs_box = fields.Float("Pcs Box") barcode_box = fields.Char("Barcode Box") - keyword_id = fields.Many2one('keywords', string='Keyword') - - def _add_product_to_keywords(self,product): - keywords_model = self.env['keywords'] - if not product: - return False - - for product in self: - match_keywords = keywords_model.search([ - '|', - ('name', 'ilike', product.name), - ('keywords', 'ilike', product.website_description) - ]) - - for kw in match_keywords.filtered(lambda k: not k.skip): - if not self.unpublished and product.id not in kw.product_ids.ids: - kw.write({'product_ids': [(4, product.id)]}) - - return True - + # keyword_id = fields.Many2one('keywords', string='Keyword') has_magento = fields.Boolean(string='Has Magento?', default=False, readonly=True) def generate_product_sla(self): @@ -988,7 +963,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): @@ -1355,9 +1329,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) -- cgit v1.2.3 From a3dc6bc79215e6223c801f49d1997292b8f4c317 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Fri, 20 Feb 2026 15:36:56 +0700 Subject: (andri) fix --- indoteknik_custom/models/advance_payment_request.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/advance_payment_request.py b/indoteknik_custom/models/advance_payment_request.py index ac9d7c2d..039d18a5 100644 --- a/indoteknik_custom/models/advance_payment_request.py +++ b/indoteknik_custom/models/advance_payment_request.py @@ -160,7 +160,9 @@ class AdvancePaymentRequest(models.Model): @api.constrains('nominal', 'estimate_line_ids') def _check_nominal_vs_estimate_total(self): for rec in self: - if rec.type_request == 'pum' and rec.estimate_line_ids: + 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.") @@ -761,7 +763,7 @@ class AdvancePaymentRequest(models.Model): pum_ids = self.search([ ('user_id', '=', user.id), - ('status', '!=', 'reject'), + ('status', '!=', 'cancel'), ('type_request', '=', 'pum') ]) -- cgit v1.2.3 From 52e196b5aceb5b2cd91795a08f4be1517b123583 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Fri, 20 Feb 2026 15:57:34 +0700 Subject: hide pos --- indoteknik_custom/models/sale_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 90cd5fa2..e6fc4732 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -647,7 +647,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') -- cgit v1.2.3 From 2c89059ab4abc70f6e7f811b7995bc394d6514f8 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 20 Feb 2026 16:21:28 +0700 Subject: fix price not same as calculated on excel --- indoteknik_custom/models/price_group.py | 20 ++++++++++---------- indoteknik_custom/models/purchase_pricelist.py | 7 ++++--- 2 files changed, 14 insertions(+), 13 deletions(-) (limited to 'indoteknik_custom/models') 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/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 -- cgit v1.2.3 From abccff65f5eb91eafe08025ebc86d5e220243854 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Sat, 21 Feb 2026 08:15:45 +0700 Subject: fix logic sync solr --- indoteknik_custom/models/keywords.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 9c8e2cfd..487e58e5 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 and len(record.product_ids) > 0: + if len(record.product_ids) > 0: record.solr_flag = 2 def solr_flag_to_queue(self, limit=500): @@ -235,7 +235,7 @@ class Keywords(models.Model): tracked_fields = ['keywords', 'category_id', 'product_ids'] 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: # Only flag for sync if there are products if len(record.product_ids) > 0: -- cgit v1.2.3 From 345d45f5fae0f152ca39e9ba491513582a5c6791 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Sat, 21 Feb 2026 09:41:57 +0700 Subject: fix solr queue keywords --- indoteknik_custom/models/keywords.py | 64 +++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 12 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 487e58e5..2ee217f7 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -160,16 +160,50 @@ class Keywords(models.Model): return True + # Old + # def _sync_keywords_queue_callback(self): + # """Callback method executed by apache.solr.queue - syncs keyword data to Solr""" + # documents = [] + # for keyword in self: + # # 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 = { + # 'id': keyword.id, + # 'category_id_i': keyword.category_id.id, + # 'keywords_s': searchkey, + # 'url_s': keyword.url, + # 'product_ids_is': [p.product_tmpl_id.id for p in keyword.product_ids], + # } + # documents.append(doc) + # except Exception as e: + # _logger.error('failed %s', e) + # _logger.error('doc data: %s', doc) + + # if documents: + # solr.add(documents) + + # self.write({'solr_flag': 0}) + + # 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: - # 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) + 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, @@ -178,13 +212,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 @@ -233,7 +273,7 @@ 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: for record in self: -- cgit v1.2.3 From 18bdbf118d2f582ac0c520a9b4cc5b61fe88488f Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 23 Feb 2026 10:39:48 +0700 Subject: show date maturity in line & remove old keywords code --- indoteknik_custom/models/keywords.py | 31 ------------------------------- 1 file changed, 31 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 2ee217f7..3fa9dd72 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -160,37 +160,6 @@ class Keywords(models.Model): return True - # Old - # def _sync_keywords_queue_callback(self): - # """Callback method executed by apache.solr.queue - syncs keyword data to Solr""" - # documents = [] - # for keyword in self: - # # 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 = { - # 'id': keyword.id, - # 'category_id_i': keyword.category_id.id, - # 'keywords_s': searchkey, - # 'url_s': keyword.url, - # 'product_ids_is': [p.product_tmpl_id.id for p in keyword.product_ids], - # } - # documents.append(doc) - # except Exception as e: - # _logger.error('failed %s', e) - # _logger.error('doc data: %s', doc) - - # if documents: - # solr.add(documents) - - # self.write({'solr_flag': 0}) - - # return True - def _sync_keywords_queue_callback(self): success_keywords = self.browse() -- cgit v1.2.3 From 79471b69ffc683c3c860ecd6456f638750a8c81f Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 24 Feb 2026 09:56:19 +0700 Subject: kartu stock --- indoteknik_custom/models/__init__.py | 1 + indoteknik_custom/models/kartu_stock.py | 186 ++++++++++++++++++++++++++++++++ indoteknik_custom/models/keywords.py | 2 +- 3 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 indoteknik_custom/models/kartu_stock.py (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index a042750b..840796f8 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -167,3 +167,4 @@ from . import uom_uom from . import commission_internal from . import update_depreciation_move_wizard from . import keywords +from . import kartu_stock 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..5b7da705 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -225,7 +225,7 @@ class Keywords(models.Model): tracked_fields = ['keywords', 'category_id', 'product_ids'] neded_sync = any(field in vals for field in tracked_fields) - if neded_sync and self.skip == False: + if neded_sync and self.skip == False and len(self.product_ids) > 0: for record in self: record.solr_flag = 2 -- cgit v1.2.3 From 1112947139e69732589352359105e7602d6539b2 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Tue, 24 Feb 2026 12:40:04 +0700 Subject: (andri) fix error singleton create bill po karena payment term dp 30 multi line --- indoteknik_custom/models/purchase_order.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index b3ecca56..c3ab71aa 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -632,7 +632,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'))) payment_schedule = date_done + timedelta(days=day_extension) if payment_schedule.weekday() == 0: -- cgit v1.2.3 From 316b8257845d0df10153fa7e5e294a699ad17c56 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 24 Feb 2026 13:20:33 +0700 Subject: push --- indoteknik_custom/models/__init__.py | 1 + indoteknik_custom/models/automatic_purchase.py | 3 + indoteknik_custom/models/purchase_order.py | 299 ++++++++++++++++++++++++ indoteknik_custom/models/purchase_order_line.py | 17 ++ indoteknik_custom/models/stock_picking.py | 1 + indoteknik_custom/models/token_log.py | 17 ++ 6 files changed, 338 insertions(+) create mode 100644 indoteknik_custom/models/token_log.py (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index a042750b..faea989d 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -167,3 +167,4 @@ from . import uom_uom from . import commission_internal from . import update_depreciation_move_wizard from . import keywords +from . import token_log diff --git a/indoteknik_custom/models/automatic_purchase.py b/indoteknik_custom/models/automatic_purchase.py index 0b2f7d1b..3035ceab 100644 --- a/indoteknik_custom/models/automatic_purchase.py +++ b/indoteknik_custom/models/automatic_purchase.py @@ -744,7 +744,10 @@ class SaleNotInMatchPO(models.Model): 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 + join purchase_order_line pol on pol.order_id = posm.purchase_order_id and pol.product_id = posm.product_id + join stock_move sm on sm.purchase_line_id = pol.id where po.state not in ('cancel') + and sm.state not in ('cancel') ) ) """ % self._table) \ No newline at end of file diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index b3ecca56..dd0c5cd5 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,304 @@ 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:
{str(e)}
") + + + 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] + 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) + 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""" + + {s.get('no_soo')} + {s.get('totalprice')} + {s.get('diskon')} + {s.get('ppn')} + + """ + + soo_table = f""" + + + + + + + + + + + {soo_rows or ''} + +
SOO NumberTotal PriceDiskonPPN
Tidak ada data SOO
+ """ + + # ---- 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""" + + {l.get('item_code')} + {desc} + {l.get('qtyordered')} + {l.get('qtydelivered')} + {l.get('qtyinvoiced')} + {l.get('docstatus')} + + """ + + + po_table = f""" + + + + + + + + + + + + + {po_rows or ''} + +
Item CodeDescriptionOrderedDeliveredInvoicedStatus
Tidak ada item PO
+ """ + + # ---- POST TO CHATTER ---- + order.message_post( + body=f""" + 📦 Data SOO
{soo_table} +

+ 📦 Data Item PO
{po_table} + """ + ) + + except Exception as e: + order.message_post( + body=f"❌ Gagal ambil data dari Altama:
{str(e)}
" + ) @staticmethod def is_local_env(): hostname = socket.gethostname().lower() 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/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 -- cgit v1.2.3 From 09b1d9f475367b95759d6c7d466a48edf8ebac4e Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Tue, 24 Feb 2026 14:27:46 +0700 Subject: fix --- indoteknik_custom/models/purchase_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index c3ab71aa..0afa5155 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -633,7 +633,7 @@ class PurchaseOrder(models.Model): date_done = self.date_approve # day_extension = int(self.payment_term_id.line_ids.days) - day_extension = int(max(self.payment_term_id.line_ids.mapped('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: -- cgit v1.2.3 From 75972376c64d133e4dcf8cb0808a60b36db07825 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Tue, 24 Feb 2026 14:33:34 +0700 Subject: fix --- indoteknik_custom/models/purchase_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 0afa5155..6e09f3e9 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -633,7 +633,7 @@ class PurchaseOrder(models.Model): date_done = self.date_approve # day_extension = int(self.payment_term_id.line_ids.days) - day_extension = int(max(self.payment_term_id.line_ids.mapped('days')), default=0) + 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: -- cgit v1.2.3 From cbbf60e2fbf86dd50397342ce171796e1f983448 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 25 Feb 2026 11:41:20 +0700 Subject: fix format npwp from website --- indoteknik_custom/models/res_partner.py | 34 +++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) (limited to 'indoteknik_custom/models') 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): -- cgit v1.2.3 From 68040bdf60a35486ed1015421679310df742b74d Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Fri, 27 Feb 2026 09:36:15 +0700 Subject: push api altama odoo indo --- indoteknik_custom/models/automatic_purchase.py | 56 +++++++++++++++++++++----- indoteknik_custom/models/purchase_order.py | 12 +++++- 2 files changed, 57 insertions(+), 11 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/automatic_purchase.py b/indoteknik_custom/models/automatic_purchase.py index 3035ceab..f4ecdcd6 100644 --- a/indoteknik_custom/models/automatic_purchase.py +++ b/indoteknik_custom/models/automatic_purchase.py @@ -315,6 +315,53 @@ class AutomaticPurchase(models.Model): sale_ids_set = set() sale_ids_name = set() 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 + + 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: + # break + + incoming = 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), + ], limit=1) + + if incoming: + skip_line = True + break + + retur = self.env['stock.move'].search([ + ('reference', 'ilike', 'BU/INPUT'), + ('state', 'in', ['done']), + ('product_id', '=', existing.product_id.id), + ('purchase_line_id', '=', existing.purchase_line_id.id), + ], limit=1) + + if retur and existing.purchase_line_id.qty_received == existing.purchase_line_id.product_qty: + skip_line = True + break + + 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,14 +787,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 - join purchase_order_line pol on pol.order_id = posm.purchase_order_id and pol.product_id = posm.product_id - join stock_move sm on sm.purchase_line_id = pol.id - where po.state not in ('cancel') - and sm.state not in ('cancel') - ) ) """ % self._table) \ No newline at end of file diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index b0fea18b..a345b96b 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -304,11 +304,15 @@ class PurchaseOrder(models.Model): 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] + # 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) + # 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 @@ -1442,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.") @@ -1459,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) -- cgit v1.2.3 From 687e462aaf3112d3b8a7dcecfc8c50582c531798 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 27 Feb 2026 09:53:40 +0700 Subject: push --- indoteknik_custom/models/sale_order.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index e6fc4732..d42aefcc 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -2601,12 +2601,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)) -- cgit v1.2.3 From 40e70a21c1f97c7325fef6164ef098582eb39534 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Fri, 27 Feb 2026 11:26:25 +0700 Subject: approval SO 2 parameter dengan menambahkan pengecekan value --- indoteknik_custom/models/sale_order.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index e6fc4732..fd0d6bdd 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'), @@ -2384,7 +2385,8 @@ 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') - + value_trigger = order._requires_approval_by_value() + if order._requires_approval_margin_leader(): order.approval_status = 'pengajuan2' order.message_post(body="Mengajukan approval ke Pimpinan") @@ -2400,9 +2402,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 (Amount Total > 50jt)") + 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( @@ -2657,6 +2666,7 @@ class SaleOrder(models.Model): 'Warning', 'Hanya bisa konfirmasi SO tim Anda.' ) + value_trigger = order._requires_approval_by_value() if order._requires_approval_margin_leader(): order.approval_status = 'pengajuan2' return self._create_approval_notification('Pimpinan') @@ -2667,6 +2677,10 @@ class SaleOrder(models.Model): order.approval_status = 'pengajuan1' order.message_post(body="Mengajukan approval ke Team Sales") return self._create_approval_notification('Team Sales') + elif value_trigger: + order.approval_status = 'pengajuan1' + 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,6 +2792,9 @@ class SaleOrder(models.Model): and not self.env.user.is_leader ) + def _requires_approval_by_value(self): + LIMIT_VALUE = 50000000 + return self.amount_total > LIMIT_VALUE def _is_request_to_own_team_leader(self): user = self.env.user @@ -3397,7 +3414,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.") -- cgit v1.2.3 From 4d8b06efe10c33d44301b05d86bf96a62033820a Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Fri, 27 Feb 2026 14:42:29 +0700 Subject: push --- indoteknik_custom/models/automatic_purchase.py | 72 ++++++++++++++++---------- 1 file changed, 46 insertions(+), 26 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/automatic_purchase.py b/indoteknik_custom/models/automatic_purchase.py index f4ecdcd6..7e324061 100644 --- a/indoteknik_custom/models/automatic_purchase.py +++ b/indoteknik_custom/models/automatic_purchase.py @@ -314,6 +314,8 @@ 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), @@ -324,32 +326,50 @@ class AutomaticPurchase(models.Model): skip_line = False - 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: - # break - - incoming = 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), - ], limit=1) - - if incoming: - skip_line = True - break - - retur = self.env['stock.move'].search([ - ('reference', 'ilike', 'BU/INPUT'), - ('state', 'in', ['done']), - ('product_id', '=', existing.product_id.id), - ('purchase_line_id', '=', existing.purchase_line_id.id), - ], limit=1) - - if retur and existing.purchase_line_id.qty_received == existing.purchase_line_id.product_qty: - skip_line = True - break + 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]: + skip_line = True if skip_line: continue -- cgit v1.2.3 From deba773edb01799a13a047dcae080272145badc7 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Fri, 27 Feb 2026 14:59:16 +0700 Subject: push --- indoteknik_custom/models/automatic_purchase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/automatic_purchase.py b/indoteknik_custom/models/automatic_purchase.py index 7e324061..f7c0d75e 100644 --- a/indoteknik_custom/models/automatic_purchase.py +++ b/indoteknik_custom/models/automatic_purchase.py @@ -368,7 +368,7 @@ class AutomaticPurchase(models.Model): retur_cache[sale_line_id] = fully_received - if retur_cache[sale_line_id]: + if retur_cache[sale_line_id] and exist: skip_line = True if skip_line: -- cgit v1.2.3 From f1a0f0edb52a0d68bb711cf96ef6be8904e3faa3 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Fri, 27 Feb 2026 16:47:17 +0700 Subject: approve 2 layer dengan penambahan ketentuan cek amount baru cek margin --- indoteknik_custom/models/sale_order.py | 59 +++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 22 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 27bda20c..df72c9cb 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -2386,8 +2386,14 @@ class SaleOrder(models.Model): # return self._create_notification_action('Notification', # 'Terdapat invoice yang telah melewati batas waktu, mohon perbarui pada dokumen Due Extension') value_trigger = order._requires_approval_by_value() - - if order._requires_approval_margin_leader(): + 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') @@ -2405,13 +2411,13 @@ class SaleOrder(models.Model): 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 (Amount Total > 50jt)") - 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( @@ -2677,20 +2683,24 @@ class SaleOrder(models.Model): 'Hanya bisa konfirmasi SO tim Anda.' ) value_trigger = order._requires_approval_by_value() - if order._requires_approval_margin_leader(): + 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 = 'pengajuan1' - order.message_post(body="Mengajukan approval ke Team Sales (Total SO > 50jt)") - 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() @@ -2788,14 +2798,14 @@ class SaleOrder(models.Model): return False def _requires_approval_margin_leader(self): - return self.total_percent_margin <= 15 and not self.env.user.is_leader + return self.total_percent_margin <= 40 and not self.env.user.is_leader def _requires_approval_margin_manager(self): - return 15 < self.total_percent_margin < 18 and not self.env.user.is_sales_manager and not self.env.user.id == 375 and not self.env.user.is_leader + return 40 < self.total_percent_margin < 50 and not self.env.user.is_sales_manager and not self.env.user.id == 375 and not self.env.user.is_leader def _requires_approval_team_sales(self): return ( - 18 <= self.total_percent_margin <= 24 + 60 <= self.total_percent_margin <= 70 # self.total_percent_margin >= 18 and self.env.user.id not in [11, 9, 375] # Eko, Ade, Putra and not self.env.user.is_sales_manager @@ -2803,9 +2813,14 @@ class SaleOrder(models.Model): ) def _requires_approval_by_value(self): - LIMIT_VALUE = 50000000 - return self.amount_total > LIMIT_VALUE - + # 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 -- cgit v1.2.3 From 82e3d96181f5d68a9fb405114aa361a8709fd70f Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Fri, 27 Feb 2026 16:48:35 +0700 Subject: balikin margin, abis testing soalnya mwehehehehe --- indoteknik_custom/models/sale_order.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index df72c9cb..8eff613e 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -2798,14 +2798,14 @@ class SaleOrder(models.Model): return False def _requires_approval_margin_leader(self): - return self.total_percent_margin <= 40 and not self.env.user.is_leader + return self.total_percent_margin <= 15 and not self.env.user.is_leader def _requires_approval_margin_manager(self): - return 40 < self.total_percent_margin < 50 and not self.env.user.is_sales_manager and not self.env.user.id == 375 and not self.env.user.is_leader + return 15 < self.total_percent_margin < 18 and not self.env.user.is_sales_manager and not self.env.user.id == 375 and not self.env.user.is_leader def _requires_approval_team_sales(self): return ( - 60 <= self.total_percent_margin <= 70 + 18 <= self.total_percent_margin <= 24 # self.total_percent_margin >= 18 and self.env.user.id not in [11, 9, 375] # Eko, Ade, Putra and not self.env.user.is_sales_manager -- cgit v1.2.3 From 5af63f0a3c3a39cc52252c6a1235ef6dc3203c2a Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Fri, 27 Feb 2026 16:58:31 +0700 Subject: fix --- indoteknik_custom/models/sale_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 8eff613e..0cb6670e 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -2816,7 +2816,7 @@ class SaleOrder(models.Model): # 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 + 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 -- cgit v1.2.3