From 3ca53ea0afef07cb79040c9f3c5aa29fa2355c90 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Thu, 27 Mar 2025 08:48:27 +0700 Subject: sale order delay --- indoteknik_custom/models/__init__.py | 1 + indoteknik_custom/models/sale_order_delay.py | 25 +++++++++++++++++++++++++ indoteknik_custom/models/stock_picking.py | 6 +++--- 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 indoteknik_custom/models/sale_order_delay.py (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index 37a49332..d5cededa 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -147,3 +147,4 @@ from . import ir_actions_report from . import barcoding_product from . import account_payment_register from . import stock_inventory +from . import sale_order_delay diff --git a/indoteknik_custom/models/sale_order_delay.py b/indoteknik_custom/models/sale_order_delay.py new file mode 100644 index 00000000..7440cd2d --- /dev/null +++ b/indoteknik_custom/models/sale_order_delay.py @@ -0,0 +1,25 @@ +from odoo import api, fields, models + + +class SaleOrderDelay(models.Model): + _name = 'sale.order.delay' + _description = 'Sale Order Delay' + _rec_name = 'so_number' + + so_number = fields.Char(string="SO Number", required=True) + days_delayed = fields.Integer(string="Day ", required=True) + status = fields.Selection([ + ('delayed', 'Delayed'), + ('on track', 'On Track'), + ('early', 'Early') + ], string='Status', required=True) + + @api.model + def create(self, vals): + vals['updated_at'] = fields.Datetime.now() + return super(SaleOrderDelay, self).create(vals) + + def write(self, vals): + vals['updated_at'] = fields.Datetime.now() + return super(SaleOrderDelay, self).write(vals) + \ No newline at end of file diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 6c6cbaa1..a11bf29f 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -16,8 +16,8 @@ import re _logger = logging.getLogger(__name__) _biteship_url = "https://api.biteship.com/v1" -_biteship_api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA" -# _biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo" +# _biteship_api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA" +_biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo" @@ -1241,7 +1241,7 @@ class StockPicking(models.Model): 'confirmed' : 'Indoteknik telah melakukan permintaan pick-up', 'allocated' : 'Kurir akan melakukan pick-up pesanan', 'picking_up' : 'Kurir sedang dalam perjalanan menuju lokasi pick-up', - 'picked' : 'Pesanan sudah di pick-up kurir '+result.get("courier", {}).get("name", ""), + 'picked' : 'Pesanan sudah di pick-up kurir '+result.get("courier", {}).get("company", ""), 'on_hold' : 'Pesanan ditahan sementara karena masalah pengiriman', 'dropping_off' : 'Kurir sudah ditugaskan dan pesanan akan segera diantar ke pembeli', 'delivered' : 'Pesanan telah sampai dan diterima oleh '+result.get("destination", {}).get("contact_name", "") -- cgit v1.2.3 From 87dad63e8ee0ace13b2d87bae26a045b80409572 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Thu, 27 Mar 2025 10:21:06 +0700 Subject: schduled --- indoteknik_custom/models/sale_order_delay.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order_delay.py b/indoteknik_custom/models/sale_order_delay.py index 7440cd2d..e2735a3c 100644 --- a/indoteknik_custom/models/sale_order_delay.py +++ b/indoteknik_custom/models/sale_order_delay.py @@ -14,6 +14,10 @@ class SaleOrderDelay(models.Model): ('early', 'Early') ], string='Status', required=True) + def update_delay(self): + query = "SELECT check_so_delay();" + self.env.cr.execute(query) + @api.model def create(self, vals): vals['updated_at'] = fields.Datetime.now() -- cgit v1.2.3 From 337e86c31691544a49a04e3f8d3a4b259e6b126a Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Thu, 10 Apr 2025 08:51:12 +0700 Subject: testing biteship dinamis eta --- indoteknik_custom/models/sale_order_delay.py | 10 +-- indoteknik_custom/models/stock_picking.py | 91 ++++++++++------------------ 2 files changed, 39 insertions(+), 62 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order_delay.py b/indoteknik_custom/models/sale_order_delay.py index e2735a3c..dfd94650 100644 --- a/indoteknik_custom/models/sale_order_delay.py +++ b/indoteknik_custom/models/sale_order_delay.py @@ -4,26 +4,28 @@ from odoo import api, fields, models class SaleOrderDelay(models.Model): _name = 'sale.order.delay' _description = 'Sale Order Delay' - _rec_name = 'so_number' + _primary_key = 'so_number' so_number = fields.Char(string="SO Number", required=True) - days_delayed = fields.Integer(string="Day ", required=True) + days_delayed = fields.Integer(string="Day Delayed or Erly") status = fields.Selection([ ('delayed', 'Delayed'), ('on track', 'On Track'), ('early', 'Early') ], string='Status', required=True) + _sql_constraints = [ + ('unique_so_number', 'unique(so_number)', 'SO Number must be unique!') + ] + def update_delay(self): query = "SELECT check_so_delay();" self.env.cr.execute(query) @api.model def create(self, vals): - vals['updated_at'] = fields.Datetime.now() return super(SaleOrderDelay, self).create(vals) def write(self, vals): - vals['updated_at'] = fields.Datetime.now() return super(SaleOrderDelay, self).write(vals) \ No newline at end of file diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index a11bf29f..b741e94e 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -16,8 +16,8 @@ import re _logger = logging.getLogger(__name__) _biteship_url = "https://api.biteship.com/v1" -# _biteship_api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA" -_biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo" +_biteship_api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA" +# _biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo" @@ -189,63 +189,12 @@ class StockPicking(models.Model): biteship_id = fields.Char(string="Biteship Respon ID") biteship_tracking_id = fields.Char(string="Biteship Trackcking ID") biteship_waybill_id = fields.Char(string="Biteship Waybill ID") - # estimated_ready_ship_date = fields.Datetime(string='ET Ready to Ship', copy=False, related='sale_id.estimated_ready_ship_date') - # countdown_hours = fields.Float(string='Countdown in Hours', compute='_callculate_sequance', default=False, store=False, compute_sudo=False) - # countdown_ready_to_ship = fields.Char(string='Countdown Ready to Ship', compute='_callculate_sequance', store=False, compute_sudo=False) final_seq = fields.Float(string='Remaining Time') def schduled_update_sequance(self): query = "SELECT update_sequance_stock_picking();" self.env.cr.execute(query) - - - # @api.depends('estimated_ready_ship_date', 'state') - # def _callculate_sequance(self): - # for record in self: - # try : - # if record.estimated_ready_ship_date and record.state not in ('cancel', 'done'): - # rts = record.estimated_ready_ship_date - waktu.now() - # rts_days = rts.days - # rts_hours = divmod(rts.seconds, 3600) - - # estimated_by_erts = rts.total_seconds() / 3600 - - # record.countdown_ready_to_ship = f"{rts_days} days, {rts_hours} hours" - # record.countdown_hours = estimated_by_erts - # else: - # record.countdown_hours = 999999999999 - # record.countdown_ready_to_ship = False - # except Exception as e : - # _logger.error(f"Error calculating sequance {record.id}: {str(e)}") - # print(str(e)) - # return { 'error': str(e) } - - - # @api.depends('estimated_ready_ship_date', 'state') - # def _compute_countdown_hours(self): - # for record in self: - # if record.state in ('cancel', 'done') or not record.estimated_ready_ship_date: - # # Gunakan nilai yang sangat besar sebagai placeholder - # record.countdown_hours = 999999 - # else: - # delta = record.estimated_ready_ship_date - waktu.now() - # record.countdown_hours = delta.total_seconds() / 3600 - - # @api.depends('estimated_ready_ship_date', 'state') - # def _compute_countdown_ready_to_ship(self): - # for record in self: - # if record.state in ('cancel', 'done'): - # record.countdown_ready_to_ship = False - # else: - # if record.estimated_ready_ship_date: - # delta = record.estimated_ready_ship_date - waktu.now() - # days = delta.days - # hours, remainder = divmod(delta.seconds, 3600) - # record.countdown_ready_to_ship = f"{days} days, {hours} hours" - # record.countdown_hours = delta.total_seconds() / 3600 - # else: - # record.countdown_ready_to_ship = False def _compute_lalamove_image_html(self): for record in self: @@ -1182,6 +1131,8 @@ class StockPicking(models.Model): self.ensure_one() order = self.env['sale.order'].search([('name', '=', self.sale_id.name)], limit=1) + + sale_order_delay = self.env['sale.order.delay'].search([('so_number', '=', order.name)], limit=1) response = { 'delivery_order': { @@ -1197,13 +1148,24 @@ class StockPicking(models.Model): 'delivery_status': None, 'eta': self.generate_eta_delivery(), 'is_biteship': True if self.biteship_id else False, - 'manifests': self.get_manifests() + 'manifests': self.get_manifests(), + 'is_delay': True if sale_order_delay and sale_order_delay.status == 'delayed' else False } if self.biteship_id : histori = self.get_manifest_biteship() - eta_start = order.date_order + timedelta(days=order.estimated_arrival_days_start) - eta_end = order.date_order + timedelta(days=order.estimated_arrival_days) + day_start = order.estomated_arrival_days_start + day_end = order.estomated_arrival_days + if sale_order_delay: + if sale_order_delay.status == 'delayed': + day_start = day_start + sale_order_delay.days_delayed + day_end = day_end + sale_order_delay.days_delayed + elif sale_order_delay.status == 'early': + day_start = day_start - sale_order_delay.days_delayed + day_end = day_end - sale_order_delay.days_delayed + + eta_start = order.date_order + timedelta(days=day_start) + eta_end = order.date_order + timedelta(days=day_end) formatted_eta = f"{eta_start.strftime('%d %b')} - {eta_end.strftime('%d %b %Y')}" response['eta'] = formatted_eta response['manifests'] = histori.get("manifests", []) @@ -1297,18 +1259,31 @@ class StockPicking(models.Model): current_date = datetime.datetime.now() prepare_days = 3 start_date = self.driver_departure_date or self.create_date + + + add_day_start = 0 + add_day_end = 0 + sale_order_delay = self.env['sale.order.delay'].search([('so_number', '=', self.sale_id.name)], limit=1) + if sale_order_delay: + if sale_order_delay.status == 'delayed': + add_day_start = sale_order_delay.days_delayed + add_day_end = sale_order_delay.days_delayed + elif sale_order_delay.status == 'early': + add_day_start = -abs(sale_order_delay.days_delayed) + add_day_end = -abs(sale_order_delay.days_delayed) ead = self.sale_id.estimated_arrival_days or 0 if not self.driver_departure_date: ead += prepare_days ead_datetime = datetime.timedelta(days=ead) - fastest_eta = start_date + ead_datetime + fastest_eta = start_date + ead_datetime + datetime.timedelta(days=add_day_start) + if not self.driver_departure_date and fastest_eta < current_date: fastest_eta = current_date + ead_datetime longest_days = 3 - longest_eta = fastest_eta + datetime.timedelta(days=longest_days) + longest_eta = fastest_eta + datetime.timedelta(days=longest_days + add_day_end) format_time = '%d %b %Y' format_time_fastest = '%d %b' if fastest_eta.year == longest_eta.year else format_time -- cgit v1.2.3 From 62f9c93c02a1f8b12ecd7bf50f850c43dd7c2c49 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Fri, 11 Apr 2025 10:14:08 +0700 Subject: dirver departure date --- indoteknik_custom/models/stock_picking.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index b741e94e..d7e8e0e8 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -16,8 +16,8 @@ import re _logger = logging.getLogger(__name__) _biteship_url = "https://api.biteship.com/v1" -_biteship_api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA" -# _biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo" +# _biteship_api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA" +_biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo" @@ -1142,6 +1142,7 @@ class StockPicking(models.Model): 'receiver_name': '', 'receiver_city': '' }, + 'delivered_date': self.driver_departure_date or False, 'delivered': False, 'status': self.shipping_status, 'waybill_number': self.delivery_tracking_no or '', @@ -1154,8 +1155,8 @@ class StockPicking(models.Model): if self.biteship_id : histori = self.get_manifest_biteship() - day_start = order.estomated_arrival_days_start - day_end = order.estomated_arrival_days + day_start = order.estimated_arrival_days_start + day_end = order.estimated_arrival_days if sale_order_delay: if sale_order_delay.status == 'delayed': day_start = day_start + sale_order_delay.days_delayed @@ -1192,7 +1193,6 @@ class StockPicking(models.Model): "Content-Type": "application/json" } - manifests = [] try: @@ -1206,7 +1206,7 @@ class StockPicking(models.Model): 'picked' : 'Pesanan sudah di pick-up kurir '+result.get("courier", {}).get("company", ""), 'on_hold' : 'Pesanan ditahan sementara karena masalah pengiriman', 'dropping_off' : 'Kurir sudah ditugaskan dan pesanan akan segera diantar ke pembeli', - 'delivered' : 'Pesanan telah sampai dan diterima oleh '+result.get("destination", {}).get("contact_name", "") + 'delivered' : f'Pesanan telah sampai dan diterima oleh {result.get("destination", {}).get("contact_name", "")}' } if(result.get('success') == True): history = result.get("history", []) -- cgit v1.2.3 From ef00237c7b6b3aed4f6040d1f124199d3551561e Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Fri, 11 Apr 2025 14:50:53 +0700 Subject: expected delivery date manifest --- indoteknik_custom/models/stock_picking.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index d7e8e0e8..54256299 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1137,15 +1137,15 @@ class StockPicking(models.Model): response = { 'delivery_order': { 'name': self.name, - 'carrier': self.carrier_id.name or '', - 'service' : order.delivery_service_type or '', + 'carrier': self.carrier_id.name or '-', + 'service' : order.delivery_service_type or '-', 'receiver_name': '', 'receiver_city': '' }, - 'delivered_date': self.driver_departure_date or False, + 'delivered_date': self.driver_departure_date.strftime('%d %b %Y') if self.driver_departure_date != False else '-', 'delivered': False, 'status': self.shipping_status, - 'waybill_number': self.delivery_tracking_no or '', + 'waybill_number': self.delivery_tracking_no or '-', 'delivery_status': None, 'eta': self.generate_eta_delivery(), 'is_biteship': True if self.biteship_id else False, @@ -1205,7 +1205,7 @@ class StockPicking(models.Model): 'picking_up' : 'Kurir sedang dalam perjalanan menuju lokasi pick-up', 'picked' : 'Pesanan sudah di pick-up kurir '+result.get("courier", {}).get("company", ""), 'on_hold' : 'Pesanan ditahan sementara karena masalah pengiriman', - 'dropping_off' : 'Kurir sudah ditugaskan dan pesanan akan segera diantar ke pembeli', + 'dropping_off' : 'Kurir sudah ditugaskan dan pesanan akan segera diantar ke pembeli', 'delivered' : f'Pesanan telah sampai dan diterima oleh {result.get("destination", {}).get("contact_name", "")}' } if(result.get('success') == True): -- cgit v1.2.3 From 6eb0b48ad5c418f565efdf1a60d221a10465b0b8 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Mon, 14 Apr 2025 16:48:09 +0700 Subject: stock picking mapping --- indoteknik_custom/models/stock_picking.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 54256299..ba7a9452 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -16,8 +16,8 @@ import re _logger = logging.getLogger(__name__) _biteship_url = "https://api.biteship.com/v1" -# _biteship_api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA" -_biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo" +_biteship_api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA" +# _biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo" -- cgit v1.2.3 From fb50d10576f2e5d16faba612dfd1565f7168f655 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Wed, 16 Apr 2025 14:33:31 +0700 Subject: FEEDBACK --- indoteknik_custom/models/stock_picking.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 4a200ac5..f2b69b55 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1406,7 +1406,10 @@ class StockPicking(models.Model): "delivered": status } - return manifests + return { + "manifests": [], + "delivered": False + } except Exception as e : _logger.error(f"Error fetching Biteship order for picking {self.id}: {str(e)}") return { 'error': str(e) } -- cgit v1.2.3 From d9d8b9f3afc0ad60ca1199b08ab6e2836663a0de Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Thu, 24 Apr 2025 13:54:06 +0700 Subject: fixing revisi renca --- indoteknik_custom/models/sale_order.py | 22 +++++++++++++++++----- indoteknik_custom/models/stock_picking.py | 19 ++++++------------- 2 files changed, 23 insertions(+), 18 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index c83ffd61..a7ee9db8 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -496,7 +496,7 @@ class SaleOrder(models.Model): @api.depends('date_order', 'estimated_arrival_days', 'state', 'estimated_arrival_days_start') def _compute_eta_date(self): - current_date = datetime.now().date() + current_date = datetime.now() for rec in self: if rec.date_order and rec.state not in ['cancel'] and rec.estimated_arrival_days and rec.estimated_arrival_days_start: rec.eta_date = current_date + timedelta(days=rec.estimated_arrival_days) @@ -507,7 +507,19 @@ class SaleOrder(models.Model): def get_days_until_next_business_day(self,start_date=None, *args, **kwargs): - today = start_date or datetime.today().date() + now = start_date or datetime.now() + + # Jika hanya diberikan tanggal (tanpa jam), asumsikan jam 00:00 + if isinstance(now, datetime): + order_datetime = now + else: + order_datetime = datetime.combine(now, datetime.min.time()) + + today = order_datetime.date() + + if order_datetime.time() > datetime.strptime("15:00", "%H:%M").time(): + today += timedelta(days=1) + offset = 0 # Counter jumlah hari yang ditambahkan holiday = self.env['hr.public.holiday'] @@ -568,13 +580,13 @@ class SaleOrder(models.Model): rec.expected_ready_to_ship = False return - current_date = datetime.now().date() + current_date = datetime.now() max_slatime = 1 # Default SLA jika tidak ada slatime = self.calculate_sla_by_vendor(rec.order_line) max_slatime = max(max_slatime, slatime['slatime']) - sum_days = max_slatime + self.get_days_until_next_business_day(current_date) - 1 + sum_days = max_slatime + self.get_days_until_next_business_day(current_date) if not rec.estimated_arrival_days: rec.estimated_arrival_days = sum_days @@ -597,7 +609,7 @@ class SaleOrder(models.Model): max_slatime = 1 # Default SLA jika tidak ada slatime = self.calculate_sla_by_vendor(rec.order_line) max_slatime = max(max_slatime, slatime['slatime']) - sum_days = max_slatime + self.get_days_until_next_business_day(current_date) - 1 + sum_days = max_slatime + self.get_days_until_next_business_day(current_date) eta_minimum = current_date + timedelta(days=sum_days) if expected_date < eta_minimum: diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index f2b69b55..aa616e62 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1442,8 +1442,10 @@ class StockPicking(models.Model): def generate_eta_delivery(self): current_date = datetime.datetime.now() - prepare_days = 3 - start_date = self.driver_departure_date or self.create_date + days_start = self.sale_id.estimated_arrival_days_start or self.sale_id.estimated_arrival_days + days_end = self.sale_id.estimated_arrival_days or (self.sale_id.estimated_arrival_days + 3) + start_date = self.sale_id.create_date + datetime.timedelta(days=days_start) + end_date = self.sale_id.create_date + datetime.timedelta(days=days_end) add_day_start = 0 @@ -1456,19 +1458,10 @@ class StockPicking(models.Model): elif sale_order_delay.status == 'early': add_day_start = -abs(sale_order_delay.days_delayed) add_day_end = -abs(sale_order_delay.days_delayed) - - ead = self.sale_id.estimated_arrival_days or 0 - if not self.driver_departure_date: - ead += prepare_days - - ead_datetime = datetime.timedelta(days=ead) - fastest_eta = start_date + ead_datetime + datetime.timedelta(days=add_day_start) - if not self.driver_departure_date and fastest_eta < current_date: - fastest_eta = current_date + ead_datetime + fastest_eta = start_date +datetime.timedelta(days=add_day_start + add_day_start) - longest_days = 3 - longest_eta = fastest_eta + datetime.timedelta(days=longest_days + add_day_end) + longest_eta = end_date + datetime.timedelta(days=add_day_end) format_time = '%d %b %Y' format_time_fastest = '%d %b' if fastest_eta.year == longest_eta.year else format_time -- cgit v1.2.3 From 914705630f61f2e02f15ee24a479191e945a0f22 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Sat, 26 Apr 2025 08:39:32 +0700 Subject: handle bugs additional time when checkout > 15.00 --- indoteknik_custom/models/sale_order.py | 49 ++++++++++++++++++------------- indoteknik_custom/models/stock_picking.py | 8 ++--- 2 files changed, 33 insertions(+), 24 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 3117a330..1e40d15e 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta import logging, random, string, requests, math, json, re, qrcode, base64 from io import BytesIO from collections import defaultdict +import pytz _logger = logging.getLogger(__name__) @@ -540,34 +541,42 @@ class SaleOrder(models.Model): rec.eta_date_start = False - def get_days_until_next_business_day(self,start_date=None, *args, **kwargs): - now = start_date or datetime.now() - - # Jika hanya diberikan tanggal (tanpa jam), asumsikan jam 00:00 - if isinstance(now, datetime): - order_datetime = now - else: - order_datetime = datetime.combine(now, datetime.min.time()) - - today = order_datetime.date() - - if order_datetime.time() > datetime.strptime("15:00", "%H:%M").time(): - today += timedelta(days=1) + def get_days_until_next_business_day(self, start_date=None, *args, **kwargs): + jakarta = pytz.timezone("Asia/Jakarta") + current = datetime.now(jakarta) - offset = 0 # Counter jumlah hari yang ditambahkan + offset = 0 + + # Gunakan current time kalau start_date tidak diberikan + if start_date is None: + start_date = current + + # Pastikan start_date pakai timezone Jakarta + if start_date.tzinfo is None: + start_date = jakarta.localize(start_date) + + # Jika sudah lewat jam 15:00, mulai dari hari berikutnya + batas_waktu = datetime.strptime("15:00", "%H:%M").time() + if start_date.time() > batas_waktu: + offset += 1 + + current_day = start_date.date() holiday = self.env['hr.public.holiday'] - while True : - today += timedelta(days=1) + while True: + # Tambah satu hari, cek apakah hari kerja + current_day += timedelta(days=1) offset += 1 - - if today.weekday() >= 5: + + # Lewati weekend + if current_day.weekday() >= 5: continue - is_holiday = holiday.search([("start_date", "=", today)]) + # Lewati hari libur + is_holiday = holiday.search([("start_date", "=", current_day)]) if is_holiday: continue - + break return offset diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 36129f00..38a1173c 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -562,6 +562,10 @@ class StockPicking(models.Model): }) payload = { + "origin_coordinate" :{ + "latitude": -6.3031123, + "longitude" : 106.7794934999 + }, "reference_id " : self.sale_id.name, "shipper_contact_name": self.carrier_id.pic_name or '', "shipper_contact_phone": self.carrier_id.pic_phone or '', @@ -585,10 +589,6 @@ class StockPicking(models.Model): # Cek jika pengiriman instant atau same_day if self.sale_id.delivery_service_type and ("instant" in self.sale_id.delivery_service_type or "same_day" in self.sale_id.delivery_service_type): payload.update({ - "origin_coordinate" :{ - "latitude": -6.3031123, - "longitude" : 106.7794934999 - }, "destination_coordinate" : { "latitude": self.real_shipping_id.latitude, "longitude": self.real_shipping_id.longtitude, -- cgit v1.2.3 From 509eb9406e6c48caf3e6366c3d8ac60643b71546 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Tue, 29 Apr 2025 09:09:05 +0700 Subject: fixing jam 15 --- indoteknik_custom/models/sale_order.py | 24 +++++++++++++++--------- indoteknik_custom/models/stock_picking.py | 1 - 2 files changed, 15 insertions(+), 10 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 1e40d15e..91905037 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -2,7 +2,7 @@ from re import search from odoo import fields, models, api, _ from odoo.exceptions import UserError, ValidationError -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import logging, random, string, requests, math, json, re, qrcode, base64 from io import BytesIO from collections import defaultdict @@ -566,16 +566,20 @@ class SaleOrder(models.Model): while True: # Tambah satu hari, cek apakah hari kerja current_day += timedelta(days=1) - offset += 1 # Lewati weekend - if current_day.weekday() >= 5: - continue + is_weekend = current_day.weekday() >= 5 + + if is_weekend: + offset += 1 + continue # Lewati hari libur is_holiday = holiday.search([("start_date", "=", current_day)]) + if is_holiday: - continue + offset += 1 + continue break @@ -588,7 +592,7 @@ class SaleOrder(models.Model): # Cek apakah SEMUA produk memiliki qty_free_bandengan >= qty_needed all_fast_products = all(product.product_id.qty_free_bandengan >= product.product_uom_qty for product in products) if all_fast_products: - return {'slatime': 1, 'include_instant': include_instant} + return {'slatime': 0, 'include_instant': include_instant} # Cari semua vendor pemenang untuk produk yang diberikan vendors = self.env['purchase.pricelist'].search([ @@ -623,7 +627,8 @@ class SaleOrder(models.Model): rec.expected_ready_to_ship = False return - current_date = datetime.now() + jakarta = pytz.timezone("Asia/Jakarta") + current_date = datetime.now(jakarta) max_slatime = 1 # Default SLA jika tidak ada slatime = self.calculate_sla_by_vendor(rec.order_line) @@ -634,6 +639,7 @@ class SaleOrder(models.Model): rec.estimated_arrival_days = sum_days eta_date = current_date + timedelta(days=sum_days) + eta_date = eta_date.astimezone(timezone.utc).replace(tzinfo=None) rec.commitment_date = eta_date rec.expected_ready_to_ship = eta_date @@ -645,7 +651,7 @@ class SaleOrder(models.Model): def _validate_expected_ready_ship_date(self): for rec in self: if rec.expected_ready_to_ship and rec.commitment_date: - current_date = datetime.now().date() + current_date = datetime.now() # Hanya membandingkan tanggal saja, tanpa jam expected_date = rec.expected_ready_to_ship.date() @@ -655,7 +661,7 @@ class SaleOrder(models.Model): sum_days = max_slatime + self.get_days_until_next_business_day(current_date) eta_minimum = current_date + timedelta(days=sum_days) - if expected_date < eta_minimum: + if expected_date < eta_minimum.date(): rec.expected_ready_to_ship = eta_minimum raise ValidationError( "Tanggal 'Expected Ready to Ship' tidak boleh lebih kecil dari {}. Mohon pilih tanggal minimal {}." diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 38a1173c..39c74aa2 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -523,7 +523,6 @@ class StockPicking(models.Model): raise UserError(f"Kesalahan tidak terduga: {str(e)}") def action_send_to_biteship(self): - if self.biteship_tracking_id: raise UserError(f"Order ini sudah dikirim ke Biteship. Dengan Tracking Id: {self.biteship_tracking_id}") -- cgit v1.2.3 From c77d250353dbed0ba1ec5cd8abd940ba95a011fc Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Wed, 30 Apr 2025 13:55:26 +0700 Subject: handle 15am dan holidays --- indoteknik_custom/models/sale_order.py | 45 +++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 17 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 91905037..25c1b3a8 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -541,12 +541,13 @@ class SaleOrder(models.Model): rec.eta_date_start = False - def get_days_until_next_business_day(self, start_date=None, *args, **kwargs): + def handling_order_after_3pm(self, start_date=None, *args, **kwargs): jakarta = pytz.timezone("Asia/Jakarta") current = datetime.now(jakarta) offset = 0 - + + # Gunakan current time kalau start_date tidak diberikan if start_date is None: start_date = current @@ -559,28 +560,31 @@ class SaleOrder(models.Model): batas_waktu = datetime.strptime("15:00", "%H:%M").time() if start_date.time() > batas_waktu: offset += 1 + + return offset + + def get_days_until_next_business_day(self, start_date=None, *args, **kwargs): + jakarta = pytz.timezone("Asia/Jakarta") + current = datetime.now(jakarta) + + offset = 0 + i = 0 current_day = start_date.date() holiday = self.env['hr.public.holiday'] - + while True: - # Tambah satu hari, cek apakah hari kerja - current_day += timedelta(days=1) - + # # Tambah satu hari, cek apakah hari kerja + current_day += timedelta(days=i) + i = 1 + # Lewati weekend is_weekend = current_day.weekday() >= 5 - - if is_weekend: - offset += 1 - continue - - # Lewati hari libur is_holiday = holiday.search([("start_date", "=", current_day)]) - - if is_holiday: + if is_weekend or is_holiday: offset += 1 continue - + break return offset @@ -633,8 +637,11 @@ class SaleOrder(models.Model): max_slatime = 1 # Default SLA jika tidak ada slatime = self.calculate_sla_by_vendor(rec.order_line) max_slatime = max(max_slatime, slatime['slatime']) + + days_after_3pm = self.handling_order_after_3pm(current_date) + date_after_3pm = current_date + timedelta(days=days_after_3pm) - sum_days = max_slatime + self.get_days_until_next_business_day(current_date) + sum_days = max_slatime + self.get_days_until_next_business_day(date_after_3pm) if not rec.estimated_arrival_days: rec.estimated_arrival_days = sum_days @@ -658,7 +665,11 @@ class SaleOrder(models.Model): max_slatime = 1 # Default SLA jika tidak ada slatime = self.calculate_sla_by_vendor(rec.order_line) max_slatime = max(max_slatime, slatime['slatime']) - sum_days = max_slatime + self.get_days_until_next_business_day(current_date) + + days_after_3pm = self.handling_order_after_3pm(current_date) + date_after_3pm = current_date + timedelta(days=days_after_3pm) + + sum_days = max_slatime + self.get_days_until_next_business_day(date_after_3pm) eta_minimum = current_date + timedelta(days=sum_days) if expected_date < eta_minimum.date(): -- cgit v1.2.3 From 7897614cee8a347dfdd933df72db95859cb1a558 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Wed, 30 Apr 2025 16:43:38 +0700 Subject: fixing handle 15 pm, weekend, and holidays --- indoteknik_custom/models/product_template.py | 3 ++ indoteknik_custom/models/sale_order.py | 71 +++++++++++++++++++--------- 2 files changed, 51 insertions(+), 23 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/product_template.py b/indoteknik_custom/models/product_template.py index e6a01a04..56ae3087 100755 --- a/indoteknik_custom/models/product_template.py +++ b/indoteknik_custom/models/product_template.py @@ -341,6 +341,9 @@ class ProductTemplate(models.Model): 'search_key':[item_code], } response = requests.post(url, headers=headers, json=json_data) + if response.status_code != 200: + return 0 + datas = json.loads(response.text)['data'] qty = 0 for data in datas: diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 25c1b3a8..795bfa0b 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -565,29 +565,58 @@ class SaleOrder(models.Model): def get_days_until_next_business_day(self, start_date=None, *args, **kwargs): jakarta = pytz.timezone("Asia/Jakarta") - current = datetime.now(jakarta) - + now = datetime.now(jakarta) + + if start_date is None: + start_date = now + + if start_date.tzinfo is None: + start_date = jakarta.localize(start_date) + + holiday = self.env['hr.public.holiday'] + batas_waktu = datetime.strptime("15:00", "%H:%M").time() + current_day = start_date.date() offset = 0 + + # Step 1: Lewat jam 15 → Tambah 1 hari + if start_date.time() > batas_waktu: + offset += 1 + + # Step 2: Hitung hari libur selama offset itu + i = 0 + total_days = 0 + while i < offset: + current_day += timedelta(days=1) + total_days += 1 + is_weekend = current_day.weekday() >= 5 + is_holiday = holiday.search([("start_date", "=", current_day)]) + if not is_weekend and not is_holiday: + i += 1 # hanya hitung hari kerja + + # Step 3: Tambah 1 hari masa persiapan gudang i = 0 + while i < 1: + current_day += timedelta(days=1) + total_days += 1 + is_weekend = current_day.weekday() >= 5 + is_holiday = holiday.search([("start_date", "=", current_day)]) + if not is_weekend and not is_holiday: + i += 1 - current_day = start_date.date() - holiday = self.env['hr.public.holiday'] - + # Step 4: Kalau current_day ternyata weekend/libur, cari hari kerja berikutnya while True: - # # Tambah satu hari, cek apakah hari kerja - current_day += timedelta(days=i) - i = 1 - - # Lewati weekend is_weekend = current_day.weekday() >= 5 is_holiday = holiday.search([("start_date", "=", current_day)]) if is_weekend or is_holiday: - offset += 1 - continue - - break + current_day += timedelta(days=1) + total_days += 1 + else: + break + + final_offset = (current_day - start_date.date()).days + return final_offset + - return offset def calculate_sla_by_vendor(self, products): product_ids = products.mapped('product_id.id') # Kumpulkan semua ID produk @@ -637,11 +666,9 @@ class SaleOrder(models.Model): max_slatime = 1 # Default SLA jika tidak ada slatime = self.calculate_sla_by_vendor(rec.order_line) max_slatime = max(max_slatime, slatime['slatime']) - - days_after_3pm = self.handling_order_after_3pm(current_date) - date_after_3pm = current_date + timedelta(days=days_after_3pm) - sum_days = max_slatime + self.get_days_until_next_business_day(date_after_3pm) + sum_days = max_slatime + self.get_days_until_next_business_day(current_date) + sum_days -= 1 if not rec.estimated_arrival_days: rec.estimated_arrival_days = sum_days @@ -665,11 +692,9 @@ class SaleOrder(models.Model): max_slatime = 1 # Default SLA jika tidak ada slatime = self.calculate_sla_by_vendor(rec.order_line) max_slatime = max(max_slatime, slatime['slatime']) - - days_after_3pm = self.handling_order_after_3pm(current_date) - date_after_3pm = current_date + timedelta(days=days_after_3pm) - sum_days = max_slatime + self.get_days_until_next_business_day(date_after_3pm) + sum_days = max_slatime + self.get_days_until_next_business_day(current_date) + sum_days -= 1 eta_minimum = current_date + timedelta(days=sum_days) if expected_date < eta_minimum.date(): -- cgit v1.2.3 From 4688c123005c7c8038c4d56ef25307e309ef815e Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 13 May 2025 15:26:01 +0700 Subject: (andri) add field Selection Option dan menambahkan opsi biteship --- indoteknik_custom/models/sale_order.py | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 795bfa0b..90c32e2a 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -249,6 +249,30 @@ class SaleOrder(models.Model): nomor_so_pengganti = fields.Char(string='Nomor SO Pengganti', copy=False, tracking=3) shipping_option_id = fields.Many2one("shipping.option", string="Selected Shipping Option", domain="['|', ('sale_order_id', '=', False), ('sale_order_id', '=', id)]") + select_shipping_option = fields.Selection([ + ('biteship', 'Biteship'), + ('custom', 'Custom'), + ], string='Select Shipping Option', help="Select shipping option for delivery") + + @api.onchange('shipping_option_id') + def _onchange_shipping_option_id(self): + if self.shipping_option_id: + self.delivery_amt = self.shipping_option_id.price + + @api.onchange('select_shipping_option') + def _onchange_select_shipping_option(self): + # Reset shipping option when the shipping type changes + self.shipping_option_id = False + + if self.select_shipping_option == 'custom': + return {'domain': {'carrier_id': []}} + + elif self.select_shipping_option == 'biteship': + # Reset delivery amount as it will be calculated through Biteship API + self.delivery_amt = 0 + self.carrier_id = False + return {'domain': {'carrier_id': [('id', '=', -1)]}} + @api.constrains('fee_third_party', 'delivery_amt', 'biaya_lain_lain') def _check_total_margin_excl_third_party(self): for rec in self: @@ -404,6 +428,18 @@ class SaleOrder(models.Model): else: raise UserError("Gagal mendapatkan estimasi ongkir.") + def _call_biteship_api(self, total_weight, destination_zip): + + url = 'https://api.biteship.com/v1/rates/couriers' + api_key = 'biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiUFQuSW5kb3Rla25payBEb3Rjb20gR2VtaWxhbmciLCJ1c2VySWQiOiI2NTIxMTU5YmRkNGIzZTAwMTUzNWIzMmYiLCJlbWFpbCI6InNhbGVzQGluZG90ZWtuaWsuY29tIiwibWVyY2hhbnRJZCI6IjY1MjExNTliOWNkN2JkMDAxNTJhNDM1ZSIsImlhdCI6MTcwNTE0Njc0NywiZXhwIjoxNzczMjk5MTQ3fQ.yR20t00sRR3fj-5eHI7G_rJPt9gv4Bi5iOIgB9sZ67c' + + headers = { + 'Authorization': api_key, + 'Content-Type': 'application/json' + } + + + def _call_rajaongkir_api(self, total_weight, destination_subsdistrict_id): url = 'https://pro.rajaongkir.com/api/cost' -- cgit v1.2.3 From 847a025e889ae9dcfe9ff97c913c9310695fc2c5 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Fri, 16 May 2025 08:46:53 +0700 Subject: handle is 3 pm --- indoteknik_custom/models/sale_order.py | 45 ++++++++++++---------------------- 1 file changed, 16 insertions(+), 29 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 795bfa0b..1d318c46 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -2,7 +2,7 @@ from re import search from odoo import fields, models, api, _ from odoo.exceptions import UserError, ValidationError -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta, timezone, time import logging, random, string, requests, math, json, re, qrcode, base64 from io import BytesIO from collections import defaultdict @@ -541,28 +541,6 @@ class SaleOrder(models.Model): rec.eta_date_start = False - def handling_order_after_3pm(self, start_date=None, *args, **kwargs): - jakarta = pytz.timezone("Asia/Jakarta") - current = datetime.now(jakarta) - - offset = 0 - - - # Gunakan current time kalau start_date tidak diberikan - if start_date is None: - start_date = current - - # Pastikan start_date pakai timezone Jakarta - if start_date.tzinfo is None: - start_date = jakarta.localize(start_date) - - # Jika sudah lewat jam 15:00, mulai dari hari berikutnya - batas_waktu = datetime.strptime("15:00", "%H:%M").time() - if start_date.time() > batas_waktu: - offset += 1 - - return offset - def get_days_until_next_business_day(self, start_date=None, *args, **kwargs): jakarta = pytz.timezone("Asia/Jakarta") now = datetime.now(jakarta) @@ -577,9 +555,11 @@ class SaleOrder(models.Model): batas_waktu = datetime.strptime("15:00", "%H:%M").time() current_day = start_date.date() offset = 0 + is3pm = False # Step 1: Lewat jam 15 → Tambah 1 hari if start_date.time() > batas_waktu: + is3pm = True offset += 1 # Step 2: Hitung hari libur selama offset itu @@ -613,8 +593,8 @@ class SaleOrder(models.Model): else: break - final_offset = (current_day - start_date.date()).days - return final_offset + offset = (current_day - start_date.date()).days + return offset, is3pm @@ -666,13 +646,19 @@ class SaleOrder(models.Model): max_slatime = 1 # Default SLA jika tidak ada slatime = self.calculate_sla_by_vendor(rec.order_line) max_slatime = max(max_slatime, slatime['slatime']) - - sum_days = max_slatime + self.get_days_until_next_business_day(current_date) + + offset , is3pm = self.get_days_until_next_business_day(current_date) + sum_days = max_slatime + offset sum_days -= 1 if not rec.estimated_arrival_days: rec.estimated_arrival_days = sum_days eta_date = current_date + timedelta(days=sum_days) + if is3pm: + eta_date = datetime.combine(eta_date, time(10, 0)) # jam 10:00 + eta_date = jakarta.localize(eta_date).astimezone(timezone.utc) # ubah ke UTC + + eta_date = eta_date.astimezone(timezone.utc).replace(tzinfo=None) rec.commitment_date = eta_date rec.expected_ready_to_ship = eta_date @@ -692,8 +678,9 @@ class SaleOrder(models.Model): max_slatime = 1 # Default SLA jika tidak ada slatime = self.calculate_sla_by_vendor(rec.order_line) max_slatime = max(max_slatime, slatime['slatime']) - - sum_days = max_slatime + self.get_days_until_next_business_day(current_date) + + offset , is3pm = self.get_days_until_next_business_day(current_date) + sum_days = max_slatime + offset sum_days -= 1 eta_minimum = current_date + timedelta(days=sum_days) -- cgit v1.2.3 From 2c4ab23bdf0ab6073195144879639a0dae863fde Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Fri, 16 May 2025 14:45:54 +0700 Subject: (andri) revisi field shipping option --- indoteknik_custom/models/sale_order.py | 38 +++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 10 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 90c32e2a..902c6db1 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -259,19 +259,41 @@ class SaleOrder(models.Model): if self.shipping_option_id: self.delivery_amt = self.shipping_option_id.price + def _get_biteship_courier_codes(self): + return [ + 'jne', 'pos', 'tiki', 'rpx', 'wahana', 'sicepat', 'jnt', 'sap', + 'ninja', 'lion', 'anteraja', 'paxel', 'idexpress', 'rex', 'ide', + 'sentral', 'first', 'dse', 'ncs', 'jdl', 'slis', 'expedito' + ] + @api.onchange('select_shipping_option') def _onchange_select_shipping_option(self): - # Reset shipping option when the shipping type changes self.shipping_option_id = False + self.delivery_amt = 0 + + biteship_courier_codes = self._get_biteship_courier_codes() + + # Cari carrier yang namanya mengandung kode Biteship + biteship_carrier_ids = [] + for code in biteship_courier_codes: + carriers = self.env['delivery.carrier'].search([ + ('name', 'ilike', code) + ]) + if carriers: + biteship_carrier_ids.extend(carriers.ids) + + # Hapus duplikat + biteship_carrier_ids = list(set(biteship_carrier_ids)) if self.select_shipping_option == 'custom': - return {'domain': {'carrier_id': []}} + # Tampilkan carrier yang bukan dari Biteship + domain = [('id', 'not in', biteship_carrier_ids)] if biteship_carrier_ids else [] + return {'domain': {'carrier_id': domain}} elif self.select_shipping_option == 'biteship': - # Reset delivery amount as it will be calculated through Biteship API - self.delivery_amt = 0 - self.carrier_id = False - return {'domain': {'carrier_id': [('id', '=', -1)]}} + # Tampilkan hanya carrier dari Biteship + domain = [('id', 'in', biteship_carrier_ids)] if biteship_carrier_ids else [] + return {'domain': {'carrier_id': domain}} @api.constrains('fee_third_party', 'delivery_amt', 'biaya_lain_lain') def _check_total_margin_excl_third_party(self): @@ -429,7 +451,6 @@ class SaleOrder(models.Model): raise UserError("Gagal mendapatkan estimasi ongkir.") def _call_biteship_api(self, total_weight, destination_zip): - url = 'https://api.biteship.com/v1/rates/couriers' api_key = 'biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiUFQuSW5kb3Rla25payBEb3Rjb20gR2VtaWxhbmciLCJ1c2VySWQiOiI2NTIxMTU5YmRkNGIzZTAwMTUzNWIzMmYiLCJlbWFpbCI6InNhbGVzQGluZG90ZWtuaWsuY29tIiwibWVyY2hhbnRJZCI6IjY1MjExNTliOWNkN2JkMDAxNTJhNDM1ZSIsImlhdCI6MTcwNTE0Njc0NywiZXhwIjoxNzczMjk5MTQ3fQ.yR20t00sRR3fj-5eHI7G_rJPt9gv4Bi5iOIgB9sZ67c' @@ -438,9 +459,6 @@ class SaleOrder(models.Model): 'Content-Type': 'application/json' } - - - def _call_rajaongkir_api(self, total_weight, destination_subsdistrict_id): url = 'https://pro.rajaongkir.com/api/cost' headers = { -- cgit v1.2.3 From 0f7e05108336ea9de64348783cbec6e97edd1d64 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 19 May 2025 00:11:58 +0700 Subject: (andri) mendapatkan tarif pengiriman --- indoteknik_custom/models/sale_order.py | 383 ++++++++++++++++++++++++++++----- 1 file changed, 335 insertions(+), 48 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 902c6db1..69982a47 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -261,11 +261,9 @@ class SaleOrder(models.Model): def _get_biteship_courier_codes(self): return [ - 'jne', 'pos', 'tiki', 'rpx', 'wahana', 'sicepat', 'jnt', 'sap', - 'ninja', 'lion', 'anteraja', 'paxel', 'idexpress', 'rex', 'ide', - 'sentral', 'first', 'dse', 'ncs', 'jdl', 'slis', 'expedito' + 'gojek','grab','deliveree','lalamove','jne','tiki','ninja','lion','rara','sicepat','jnt','pos','idexpress','rpx','wahana','jdl','pos','anteraja','sap','paxel','borzo' ] - + @api.onchange('select_shipping_option') def _onchange_select_shipping_option(self): self.shipping_option_id = False @@ -385,10 +383,78 @@ class SaleOrder(models.Model): ) def action_estimate_shipping(self): - if self.carrier_id.id in [1, 151]: - self.action_indoteknik_estimate_shipping() - return + # if self.carrier_id.id in [1, 151]: + # self.action_indoteknik_estimate_shipping() + # return + + if self.select_shipping_option == 'biteship': + return self.action_estimate_shipping_biteship() + elif self.carrier_id.id in [1, 151]: # ID untuk Indoteknik Delivery + return self.action_indoteknik_estimate_shipping() + else: + total_weight = 0 + missing_weight_products = [] + + for line in self.order_line: + if line.weight > 0: + total_weight += line.weight * line.product_uom_qty + line.product_id.weight = line.weight + else: + missing_weight_products.append(line.product_id.name) + + if missing_weight_products: + product_names = '
'.join(missing_weight_products) + self.message_post(body=f"Produk berikut tidak memiliki berat:
{product_names}") + + if total_weight == 0: + raise UserError("Tidak dapat mengestimasi ongkir tanpa berat yang valid.") + + destination_subsdistrict_id = self.real_shipping_id.kecamatan_id.rajaongkir_id + if not destination_subsdistrict_id: + raise UserError("Gagal mendapatkan ID kota tujuan.") + + result = self._call_rajaongkir_api(total_weight, destination_subsdistrict_id) + if result: + shipping_options = [] + for courier in result['rajaongkir']['results']: + for cost_detail in courier['costs']: + service = cost_detail['service'] + description = cost_detail['description'] + etd = cost_detail['cost'][0]['etd'] + value = cost_detail['cost'][0]['value'] + shipping_options.append((service, description, etd, value, courier['code'])) + + self.env["shipping.option"].search([('sale_order_id', '=', self.id)]).unlink() + + _logger.info(f"Shipping options: {shipping_options}") + + for service, description, etd, value, provider in shipping_options: + self.env["shipping.option"].create({ + "name": service, + "price": value, + "provider": provider, + "etd": etd, + "sale_order_id": self.id, + }) + + + self.shipping_option_id = self.env["shipping.option"].search([('sale_order_id', '=', self.id)], limit=1).id + + _logger.info(f"Shipping option SO ID: {self.shipping_option_id}") + + self.message_post( + body=f"Estimasi Ongkos Kirim: Rp{self.delivery_amt}
Detail Lain:
" + f"{'
'.join([f'Service: {s[0]}, Description: {s[1]}, ETD: {s[2]} hari, Cost: Rp {s[3]}' for s in shipping_options])}", + message_type="comment" + ) + + # self.message_post(body=f"Estimasi Ongkos Kirim: Rp{self.delivery_amt}
Detail Lain:
{'
'.join([f'Service: {s[0]}, Description: {s[1]}, ETD: {s[2]} hari, Cost: Rp {s[3]}' for s in shipping_options])}", message_type="comment") + else: + raise UserError("Gagal mendapatkan estimasi ongkir.") + + def _validate_for_shipping_estimate(self): + # Cek berat produk total_weight = 0 missing_weight_products = [] @@ -405,59 +471,280 @@ class SaleOrder(models.Model): if total_weight == 0: raise UserError("Tidak dapat mengestimasi ongkir tanpa berat yang valid.") + + # Validasi alamat pengiriman + if not self.real_shipping_id: + raise UserError("Alamat pengiriman (Real Delivery Address) harus diisi.") + + if not self.real_shipping_id.kota_id: + raise UserError("Kota pada alamat pengiriman harus diisi.") + + if not self.real_shipping_id.zip: + raise UserError("Kode pos pada alamat pengiriman harus diisi.") + + if not self.real_shipping_id.state_id: + raise UserError("Provinsi pada alamat pengiriman harus diisi.") + + return total_weight - destination_subsdistrict_id = self.real_shipping_id.kecamatan_id.rajaongkir_id - if not destination_subsdistrict_id: - raise UserError("Gagal mendapatkan ID kota tujuan.") - - result = self._call_rajaongkir_api(total_weight, destination_subsdistrict_id) - if result: - shipping_options = [] - for courier in result['rajaongkir']['results']: - for cost_detail in courier['costs']: - service = cost_detail['service'] - description = cost_detail['description'] - etd = cost_detail['cost'][0]['etd'] - value = cost_detail['cost'][0]['value'] - shipping_options.append((service, description, etd, value, courier['code'])) - - self.env["shipping.option"].search([('sale_order_id', '=', self.id)]).unlink() + def action_estimate_shipping_biteship(self): + # Validasi data + total_weight = self._validate_for_shipping_estimate() + + # Konversi berat ke gram untuk Biteship + weight_gram = int(total_weight * 1000) + if weight_gram < 100: + weight_gram = 100 # Minimum weight untuk Biteship + + # Persiapkan data item + items = [{ + "name": "Paket Pesanan", + "description": f"Sale Order {self.name}", + "value": int(self.amount_untaxed), + "weight": weight_gram, + "quantity": 1, + "height": 10, + "width": 10, + "length": 10 + }] + + # Coba dapatkan data alamat tujuan + destination_data = {} + origin_data = {} + + # Persiapkan data alamat asal (Gudang) + # 1. Data tetap gudang Bandengan + origin_data = { + "origin_postal_code" : 14440, + # "origin_latitude": -6.1753924, + # "origin_longitude": 106.7794935, + } - _logger.info(f"Shipping options: {shipping_options}") + # Coba dapatkan data alamat tujuan + if not self.real_shipping_id: + raise UserError("Alamat pengiriman (Real Delivery Address) harus diisi.") + + shipping_address = self.real_shipping_id + _logger.info(f"Shipping Address: {shipping_address}") + _logger.info(f"Shipping Address: {shipping_address.zip}") + + # # Coba dapatkan koordinat dulu (jika tersedia) + # if hasattr(shipping_address, 'partner_latitude') and hasattr(shipping_address, 'partner_longitude'): + # if shipping_address.partner_latitude and shipping_address.partner_longitude: + # destination_data = { + # "destination_latitude": shipping_address.partner_latitude, + # "destination_longitude": shipping_address.partner_longitude + # } + + # Jika koordinat tidak tersedia, gunakan kode pos + if not destination_data and shipping_address.zip: + destination_data = { + "destination_postal_code": shipping_address.zip + } + + # Jika kode pos tidak tersedia, gunakan alamat lengkap + # if not destination_data: + # # Buat alamat lengkap + # full_address = f"{shipping_address.street or ''}" + # if shipping_address.street2: + # full_address += f", {shipping_address.street2}" + + # destination_data = { + # "address": full_address, + # "area": shipping_address.kota_id.name if shipping_address.kota_id else "", + # "suburb": shipping_address.kecamatan_id.name if shipping_address.kecamatan_id else "", + # "province": shipping_address.state_id.name if shipping_address.state_id else "", + # "postcode": shipping_address.zip or "" + # } + + # Siapkan daftar kurir yang valid untuk Biteship + couriers = ','.join(self._get_biteship_courier_codes()) + + # Jika tidak ada data tujuan yang valid + if not destination_data: + raise UserError("Tidak dapat mengestimasikan ongkir: Alamat pengiriman tidak lengkap.") + + # Panggil API Biteship dengan format yang benar + result = self._call_biteship_api(origin_data, destination_data, items, couriers) + + if not result: + raise UserError("Gagal mendapatkan estimasi ongkir dari Biteship.") + + # Hapus shipping_option lama + self.env["shipping.option"].search([('sale_order_id', '=', self.id)]).unlink() + + # Proses hasil API + shipping_options = [] + shipping_services = result.get('pricing', []) + + _logger.info(f"Ditemukan {len(shipping_services)} layanan pengiriman") + + for service in shipping_services: + courier_code = service.get('courier_code', '').lower() + courier_name = service.get('courier_name', '') + service_name = service.get('courier_service_name', '') + price = service.get('price', 0) + + _logger.info(f"Layanan: {courier_name} - {service_name}, Harga: {price}") + + # Lewati layanan dengan harga 0 + if not price: + _logger.warning(f"Melewati layanan dengan harga 0: {courier_name} - {service_name}") + continue - for service, description, etd, value, provider in shipping_options: - self.env["shipping.option"].create({ - "name": service, - "price": value, - "provider": provider, + # Format estimasi waktu + duration = service.get('duration', '') + shipment_range = service.get('shipment_duration_range', '') + shipment_unit = service.get('shipment_duration_unit', 'days') + + # Gunakan duration jika tersedia, jika tidak, buat dari range + if duration: + etd = duration + elif shipment_range: + etd = f"{shipment_range} {shipment_unit}" + else: + etd = "1-3 days" # Default fallback + + # Buat shipping option + try: + shipping_option = self.env["shipping.option"].create({ + "name": f"{courier_name} - {service_name}", + "price": price, + "provider": courier_code, "etd": etd, "sale_order_id": self.id, }) - - - self.shipping_option_id = self.env["shipping.option"].search([('sale_order_id', '=', self.id)], limit=1).id - - _logger.info(f"Shipping option SO ID: {self.shipping_option_id}") - - self.message_post( - body=f"Estimasi Ongkos Kirim: Rp{self.delivery_amt}
Detail Lain:
" - f"{'
'.join([f'Service: {s[0]}, Description: {s[1]}, ETD: {s[2]} hari, Cost: Rp {s[3]}' for s in shipping_options])}", - message_type="comment" - ) - - # self.message_post(body=f"Estimasi Ongkos Kirim: Rp{self.delivery_amt}
Detail Lain:
{'
'.join([f'Service: {s[0]}, Description: {s[1]}, ETD: {s[2]} hari, Cost: Rp {s[3]}' for s in shipping_options])}", message_type="comment") - - else: - raise UserError("Gagal mendapatkan estimasi ongkir.") + + shipping_options.append(shipping_option) + _logger.info(f"Berhasil membuat opsi pengiriman: {courier_name} - {service_name}") + except Exception as e: + _logger.error(f"Gagal membuat opsi pengiriman: {str(e)}") + + # Jika tidak ada opsi pengiriman + if not shipping_options: + raise UserError(f"Tidak ada layanan pengiriman ditemukan untuk kode pos {destination_data}. Mohon periksa kembali kode pos atau gunakan metode pengiriman lain.") + + # Set opsi pertama sebagai default + self.shipping_option_id = shipping_options[0].id + self.delivery_amt = shipping_options[0].price + + # Format pesan untuk log + option_list = '
'.join([ + f"{opt.name}: Rp {opt.price:,.0f} ({opt.etd})" + for opt in shipping_options + ]) + + # Log hasil estimasi + self.message_post( + body=f"Estimasi Ongkir Biteship (Kode Pos {origin_data} → {destination_data}):
{option_list}", + message_type="comment" + ) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Estimasi Ongkir Berhasil', + 'message': f'Mendapatkan {len(shipping_options)} opsi pengiriman', + 'type': 'success', + 'sticky': False, + } + } - def _call_biteship_api(self, total_weight, destination_zip): + def _call_biteship_api(self, origin_data, destination_data, items, couriers=None): + url = 'https://api.biteship.com/v1/rates/couriers' - api_key = 'biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiUFQuSW5kb3Rla25payBEb3Rjb20gR2VtaWxhbmciLCJ1c2VySWQiOiI2NTIxMTU5YmRkNGIzZTAwMTUzNWIzMmYiLCJlbWFpbCI6InNhbGVzQGluZG90ZWtuaWsuY29tIiwibWVyY2hhbnRJZCI6IjY1MjExNTliOWNkN2JkMDAxNTJhNDM1ZSIsImlhdCI6MTcwNTE0Njc0NywiZXhwIjoxNzczMjk5MTQ3fQ.yR20t00sRR3fj-5eHI7G_rJPt9gv4Bi5iOIgB9sZ67c' - + api_key = 'biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA' + headers = { 'Authorization': api_key, 'Content-Type': 'application/json' } + + if not couriers: + couriers = ','.join(self._get_biteship_courier_codes()) + + # # Cek apakah kita menggunakan koordinat (paling akurat) + # has_origin_coords = ('origin_latitude' in origin_data and 'origin_longitude' in origin_data) + # has_dest_coords = ('destination_latitude' in destination_data and 'destination_longitude' in destination_data) + + # Cek apakah kita menggunakan kode pos + has_origin_postal = ('origin_postal_code' in origin_data) + has_dest_postal = ('destination_postal_code' in destination_data) + _logger.info(f"Origin Data: {has_origin_postal}") + _logger.info(f"Destination Data: {has_dest_postal}") + # Tentukan payload berdasarkan data yang tersedia + # if has_origin_coords and has_dest_coords: + # # Mode koordinat - paling akurat + # payload = { + # "origin_latitude": origin_data.get('origin_latitude'), + # "origin_longitude": origin_data.get('origin_longitude'), + # "destination_latitude": destination_data.get('destination_latitude'), + # "destination_longitude": destination_data.get('destination_longitude'), + # "couriers": couriers, + # "items": items + # } + # api_mode = "koordinat" + # elif has_origin_postal and has_dest_postal: + if has_origin_postal and has_dest_postal: + # Mode kode pos - fallback 1 + payload = { + "origin_postal_code": origin_data.get('origin_postal_code'), + "destination_postal_code": destination_data.get('destination_postal_code'), + "couriers": couriers, + "items": items + } + api_mode = "kode_pos" + # else: + # # Mode alamat lengkap - fallback 2 + # payload = { + # "origin": { + # "address": origin_data.get('address', ''), + # "area": origin_data.get('area', ''), + # "suburb": origin_data.get('suburb', ''), + # "province": origin_data.get('province', ''), + # "postcode": origin_data.get('postcode', '') + # }, + # "destination": { + # "address": destination_data.get('address', ''), + # "area": destination_data.get('area', ''), + # "suburb": destination_data.get('suburb', ''), + # "province": destination_data.get('province', ''), + # "postcode": destination_data.get('postcode', '') + # }, + # "couriers": couriers, + # "items": items + # } + # api_mode = "alamat_lengkap" + + try: + _logger.info(f"Calling Biteship API with mode: {api_mode}") + _logger.info(f"Payload: {payload}") + + response = requests.post(url, headers=headers, json=payload, timeout=30) + + # Log response untuk debugging + _logger.info(f"Biteship API Status Code: {response.status_code}") + if response.status_code != 200: + _logger.error(f"Biteship API Error Response: {response.text}") + + if response.status_code == 200: + result = response.json() + result['api_mode'] = api_mode # Tambahkan info mode API untuk referensi + return result + else: + error_msg = response.text + _logger.error(f"Error calling Biteship API: {response.status_code} - {error_msg}") + return False + except requests.exceptions.Timeout: + _logger.error("Timeout connecting to Biteship API") + return False + except requests.exceptions.ConnectionError: + _logger.error("Connection error to Biteship API") + return False + except Exception as e: + _logger.error(f"Exception calling Biteship API: {str(e)}") + return False def _call_rajaongkir_api(self, total_weight, destination_subsdistrict_id): url = 'https://pro.rajaongkir.com/api/cost' -- cgit v1.2.3 From 60f1f3b3dbe988d8c0fda706fd25d3abe892238a Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 19 May 2025 11:08:11 +0700 Subject: (andri) penyesuaian selected shipping ketika estimate shipping --- indoteknik_custom/models/sale_order.py | 193 ++++++++++++--------------------- 1 file changed, 68 insertions(+), 125 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 12c04f19..4ee82100 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -252,12 +252,13 @@ class SaleOrder(models.Model): select_shipping_option = fields.Selection([ ('biteship', 'Biteship'), ('custom', 'Custom'), - ], string='Select Shipping Option', help="Select shipping option for delivery") + ], string='Select Shipping Option', help="Select shipping option for delivery", Tracking=True) @api.onchange('shipping_option_id') def _onchange_shipping_option_id(self): if self.shipping_option_id: self.delivery_amt = self.shipping_option_id.price + self.note_ekspedisi = f"Pengiriman: {self.shipping_option_id.name} - Rp {self.shipping_option_id.price:,.0f} ({self.shipping_option_id.etd})" def _get_biteship_courier_codes(self): return [ @@ -267,6 +268,7 @@ class SaleOrder(models.Model): @api.onchange('select_shipping_option') def _onchange_select_shipping_option(self): self.shipping_option_id = False + self.carrier_id = False self.delivery_amt = 0 biteship_courier_codes = self._get_biteship_courier_codes() @@ -488,7 +490,6 @@ class SaleOrder(models.Model): return total_weight def action_estimate_shipping_biteship(self): - # Validasi data total_weight = self._validate_for_shipping_estimate() # Konversi berat ke gram untuk Biteship @@ -508,63 +509,47 @@ class SaleOrder(models.Model): "length": 10 }] - # Coba dapatkan data alamat tujuan - destination_data = {} - origin_data = {} - - # Persiapkan data alamat asal (Gudang) - # 1. Data tetap gudang Bandengan + # Data asal (tetap gudang Bandengan) origin_data = { - "origin_postal_code" : 14440, - # "origin_latitude": -6.1753924, - # "origin_longitude": 106.7794935, + "origin_latitude": -6.3031123, + "origin_longitude": 106.7794934, } - - # Coba dapatkan data alamat tujuan - if not self.real_shipping_id: - raise UserError("Alamat pengiriman (Real Delivery Address) harus diisi.") + + # Prioritaskan penggunaan koordinat jika tersedia + destination_data = {} + use_coordinate = False shipping_address = self.real_shipping_id - _logger.info(f"Shipping Address: {shipping_address}") - _logger.info(f"Shipping Address: {shipping_address.zip}") - # # Coba dapatkan koordinat dulu (jika tersedia) - # if hasattr(shipping_address, 'partner_latitude') and hasattr(shipping_address, 'partner_longitude'): - # if shipping_address.partner_latitude and shipping_address.partner_longitude: - # destination_data = { - # "destination_latitude": shipping_address.partner_latitude, - # "destination_longitude": shipping_address.partner_longitude - # } + # Cek apakah latitude dan longitude tersedia dan valid + if hasattr(shipping_address, 'latitude') and hasattr(shipping_address, 'longitude'): + if shipping_address.latitude and shipping_address.longitude: + try: + lat = float(shipping_address.latitude) + lng = float(shipping_address.longitude) + destination_data = { + "destination_latitude": lat, + "destination_longitude": lng + } + use_coordinate = True + except (ValueError, TypeError): + use_coordinate = False # Jika koordinat tidak tersedia, gunakan kode pos - if not destination_data and shipping_address.zip: - destination_data = { - "destination_postal_code": shipping_address.zip - } + if not use_coordinate: + if shipping_address.zip: + origin_data = {"origin_postal_code": 14440} + destination_data = { + "destination_postal_code": shipping_address.zip + } + else: + raise UserError("Tidak dapat mengestimasikan ongkir: Kode pos tujuan tidak tersedia.") - # Jika kode pos tidak tersedia, gunakan alamat lengkap - # if not destination_data: - # # Buat alamat lengkap - # full_address = f"{shipping_address.street or ''}" - # if shipping_address.street2: - # full_address += f", {shipping_address.street2}" - - # destination_data = { - # "address": full_address, - # "area": shipping_address.kota_id.name if shipping_address.kota_id else "", - # "suburb": shipping_address.kecamatan_id.name if shipping_address.kecamatan_id else "", - # "province": shipping_address.state_id.name if shipping_address.state_id else "", - # "postcode": shipping_address.zip or "" - # } - - # Siapkan daftar kurir yang valid untuk Biteship - couriers = ','.join(self._get_biteship_courier_codes()) - - # Jika tidak ada data tujuan yang valid - if not destination_data: - raise UserError("Tidak dapat mengestimasikan ongkir: Alamat pengiriman tidak lengkap.") - - # Panggil API Biteship dengan format yang benar + # Filter kurir berdasarkan shipping method jika dipilih + couriers = self.carrier_id.name.lower() if self.carrier_id else ','.join(self._get_biteship_courier_codes()) + + # Panggil API Biteship + api_mode = "koordinat" if use_coordinate else "kode_pos" result = self._call_biteship_api(origin_data, destination_data, items, couriers) if not result: @@ -577,19 +562,14 @@ class SaleOrder(models.Model): shipping_options = [] shipping_services = result.get('pricing', []) - _logger.info(f"Ditemukan {len(shipping_services)} layanan pengiriman") - for service in shipping_services: courier_code = service.get('courier_code', '').lower() courier_name = service.get('courier_name', '') service_name = service.get('courier_service_name', '') price = service.get('price', 0) - _logger.info(f"Layanan: {courier_name} - {service_name}, Harga: {price}") - # Lewati layanan dengan harga 0 if not price: - _logger.warning(f"Melewati layanan dengan harga 0: {courier_name} - {service_name}") continue # Format estimasi waktu @@ -597,13 +577,12 @@ class SaleOrder(models.Model): shipment_range = service.get('shipment_duration_range', '') shipment_unit = service.get('shipment_duration_unit', 'days') - # Gunakan duration jika tersedia, jika tidak, buat dari range if duration: etd = duration elif shipment_range: etd = f"{shipment_range} {shipment_unit}" else: - etd = "1-3 days" # Default fallback + etd = "1-3 days" # Buat shipping option try: @@ -616,36 +595,50 @@ class SaleOrder(models.Model): }) shipping_options.append(shipping_option) - _logger.info(f"Berhasil membuat opsi pengiriman: {courier_name} - {service_name}") except Exception as e: _logger.error(f"Gagal membuat opsi pengiriman: {str(e)}") # Jika tidak ada opsi pengiriman if not shipping_options: - raise UserError(f"Tidak ada layanan pengiriman ditemukan untuk kode pos {destination_data}. Mohon periksa kembali kode pos atau gunakan metode pengiriman lain.") + raise UserError(f"Tidak ada layanan pengiriman ditemukan untuk kode pos {destination_data}.") # Set opsi pertama sebagai default self.shipping_option_id = shipping_options[0].id self.delivery_amt = shipping_options[0].price - # Format pesan untuk log + # Tampilkan lokasi pengiriman dengan format yang lebih baik + if use_coordinate: + origin_info = f"Koordinat ({origin_data.get('origin_latitude')}, {origin_data.get('origin_longitude')})" + destination_info = f"Koordinat ({destination_data.get('destination_latitude')}, {destination_data.get('destination_longitude')})" + else: + origin_info = f"Kode Pos {origin_data.get('origin_postal_code')}" + destination_info = f"Kode Pos {destination_data.get('destination_postal_code')}" + + # Format daftar opsi pengiriman option_list = '
'.join([ f"{opt.name}: Rp {opt.price:,.0f} ({opt.etd})" for opt in shipping_options ]) - # Log hasil estimasi + # Tampilkan informasi tentang kurir yang dipilih + selected_option = shipping_options[0] + shipping_method_info = f"Metode Pengiriman: {self.carrier_id.name}" if self.carrier_id else "" + + # Log hasil estimasi dengan format yang lebih baik dan informasi kurir self.message_post( - body=f"Estimasi Ongkir Biteship (Kode Pos {origin_data} → {destination_data}):
{option_list}", + body=f"Estimasi Ongkir Biteship ({origin_info} → {destination_info}):
{shipping_method_info}
{option_list}", message_type="comment" ) + # Simpan informasi untuk note ekspedisi + self.note_ekspedisi = f"Pengiriman: {selected_option.name} - Rp {selected_option.price:,.0f} ({selected_option.etd}) [via {api_mode}]" + return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'Estimasi Ongkir Berhasil', - 'message': f'Mendapatkan {len(shipping_options)} opsi pengiriman', + 'message': f'Mendapatkan {len(shipping_options)} opsi pengiriman menggunakan {api_mode}', 'type': 'success', 'sticky': False, } @@ -663,59 +656,16 @@ class SaleOrder(models.Model): if not couriers: couriers = ','.join(self._get_biteship_courier_codes()) - - # # Cek apakah kita menggunakan koordinat (paling akurat) - # has_origin_coords = ('origin_latitude' in origin_data and 'origin_longitude' in origin_data) - # has_dest_coords = ('destination_latitude' in destination_data and 'destination_longitude' in destination_data) - - # Cek apakah kita menggunakan kode pos - has_origin_postal = ('origin_postal_code' in origin_data) - has_dest_postal = ('destination_postal_code' in destination_data) - _logger.info(f"Origin Data: {has_origin_postal}") - _logger.info(f"Destination Data: {has_dest_postal}") - # Tentukan payload berdasarkan data yang tersedia - # if has_origin_coords and has_dest_coords: - # # Mode koordinat - paling akurat - # payload = { - # "origin_latitude": origin_data.get('origin_latitude'), - # "origin_longitude": origin_data.get('origin_longitude'), - # "destination_latitude": destination_data.get('destination_latitude'), - # "destination_longitude": destination_data.get('destination_longitude'), - # "couriers": couriers, - # "items": items - # } - # api_mode = "koordinat" - # elif has_origin_postal and has_dest_postal: - if has_origin_postal and has_dest_postal: - # Mode kode pos - fallback 1 - payload = { - "origin_postal_code": origin_data.get('origin_postal_code'), - "destination_postal_code": destination_data.get('destination_postal_code'), - "couriers": couriers, - "items": items - } - api_mode = "kode_pos" - # else: - # # Mode alamat lengkap - fallback 2 - # payload = { - # "origin": { - # "address": origin_data.get('address', ''), - # "area": origin_data.get('area', ''), - # "suburb": origin_data.get('suburb', ''), - # "province": origin_data.get('province', ''), - # "postcode": origin_data.get('postcode', '') - # }, - # "destination": { - # "address": destination_data.get('address', ''), - # "area": destination_data.get('area', ''), - # "suburb": destination_data.get('suburb', ''), - # "province": destination_data.get('province', ''), - # "postcode": destination_data.get('postcode', '') - # }, - # "couriers": couriers, - # "items": items - # } - # api_mode = "alamat_lengkap" + + # Persiapkan payload dengan menggabungkan origin, destination, dan items + payload = { + **origin_data, + **destination_data, + "couriers": couriers, + "items": items + } + + api_mode = "koordinat" if "destination_latitude" in destination_data else "kode_pos" try: _logger.info(f"Calling Biteship API with mode: {api_mode}") @@ -723,25 +673,18 @@ class SaleOrder(models.Model): response = requests.post(url, headers=headers, json=payload, timeout=30) - # Log response untuk debugging _logger.info(f"Biteship API Status Code: {response.status_code}") if response.status_code != 200: _logger.error(f"Biteship API Error Response: {response.text}") if response.status_code == 200: result = response.json() - result['api_mode'] = api_mode # Tambahkan info mode API untuk referensi + result['api_mode'] = api_mode # Tambahkan info mode API return result else: error_msg = response.text _logger.error(f"Error calling Biteship API: {response.status_code} - {error_msg}") return False - except requests.exceptions.Timeout: - _logger.error("Timeout connecting to Biteship API") - return False - except requests.exceptions.ConnectionError: - _logger.error("Connection error to Biteship API") - return False except Exception as e: _logger.error(f"Exception calling Biteship API: {str(e)}") return False -- cgit v1.2.3 From 63243a7b70292e9c48a21e2badbb07c398bc4166 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 19 May 2025 16:07:03 +0700 Subject: (andri) add field langitude & longitude pada customer & perbaikan biteship --- indoteknik_custom/models/sale_order.py | 90 ++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 41 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 4ee82100..f09869da 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -258,7 +258,6 @@ class SaleOrder(models.Model): def _onchange_shipping_option_id(self): if self.shipping_option_id: self.delivery_amt = self.shipping_option_id.price - self.note_ekspedisi = f"Pengiriman: {self.shipping_option_id.name} - Rp {self.shipping_option_id.price:,.0f} ({self.shipping_option_id.etd})" def _get_biteship_courier_codes(self): return [ @@ -271,29 +270,25 @@ class SaleOrder(models.Model): self.carrier_id = False self.delivery_amt = 0 - biteship_courier_codes = self._get_biteship_courier_codes() - - # Cari carrier yang namanya mengandung kode Biteship + # Dapatkan semua ID carrier untuk Biteship biteship_carrier_ids = [] - for code in biteship_courier_codes: - carriers = self.env['delivery.carrier'].search([ - ('name', 'ilike', code) - ]) - if carriers: - biteship_carrier_ids.extend(carriers.ids) - # Hapus duplikat - biteship_carrier_ids = list(set(biteship_carrier_ids)) + # Gunakan SQL langsung untuk menghindari masalah ORM + self.env.cr.execute(""" + SELECT delivery_carrier_id + FROM rajaongkir_kurir + WHERE name IN %s + """, (tuple(self._get_biteship_courier_codes()),)) - if self.select_shipping_option == 'custom': - # Tampilkan carrier yang bukan dari Biteship - domain = [('id', 'not in', biteship_carrier_ids)] if biteship_carrier_ids else [] - return {'domain': {'carrier_id': domain}} + # Ambil ID numerik hasil query + biteship_carrier_ids = [row[0] for row in self.env.cr.fetchall() if row[0]] - elif self.select_shipping_option == 'biteship': - # Tampilkan hanya carrier dari Biteship + if self.select_shipping_option == 'biteship': domain = [('id', 'in', biteship_carrier_ids)] if biteship_carrier_ids else [] - return {'domain': {'carrier_id': domain}} + else: # 'custom' + domain = [('id', 'not in', biteship_carrier_ids)] if biteship_carrier_ids else [] + + return {'domain': {'carrier_id': domain}} @api.constrains('fee_third_party', 'delivery_amt', 'biaya_lain_lain') def _check_total_margin_excl_third_party(self): @@ -490,6 +485,7 @@ class SaleOrder(models.Model): return total_weight def action_estimate_shipping_biteship(self): + total_weight = self._validate_for_shipping_estimate() # Konversi berat ke gram untuk Biteship @@ -509,6 +505,10 @@ class SaleOrder(models.Model): "length": 10 }] + # Coba dapatkan data koordinat dari alamat pengiriman + shipping_address = self.real_shipping_id + _logger.info(f"Shipping Address: {shipping_address}") + # Data asal (tetap gudang Bandengan) origin_data = { "origin_latitude": -6.3031123, @@ -519,37 +519,41 @@ class SaleOrder(models.Model): destination_data = {} use_coordinate = False - shipping_address = self.real_shipping_id - # Cek apakah latitude dan longitude tersedia dan valid - if hasattr(shipping_address, 'latitude') and hasattr(shipping_address, 'longitude'): - if shipping_address.latitude and shipping_address.longitude: + if hasattr(shipping_address, 'latitude') and hasattr(shipping_address, 'longtitude'): + if shipping_address.latitude and shipping_address.longtitude: try: + # Validasi format koordinat lat = float(shipping_address.latitude) - lng = float(shipping_address.longitude) + lng = float(shipping_address.longtitude) destination_data = { "destination_latitude": lat, "destination_longitude": lng } use_coordinate = True + _logger.info(f"Using coordinates: lat={lat}, lng={lng}") except (ValueError, TypeError): + _logger.warning(f"Invalid coordinates, falling back to postal code") use_coordinate = False - # Jika koordinat tidak tersedia, gunakan kode pos + # Jika koordinat tidak tersedia atau tidak valid, gunakan kode pos if not use_coordinate: if shipping_address.zip: - origin_data = {"origin_postal_code": 14440} + origin_data = {"origin_postal_code": 14440} # Reset origin untuk mode kode pos destination_data = { "destination_postal_code": shipping_address.zip } + _logger.info(f"Using postal code: {shipping_address.zip}") else: raise UserError("Tidak dapat mengestimasikan ongkir: Kode pos tujuan tidak tersedia.") - # Filter kurir berdasarkan shipping method jika dipilih - couriers = self.carrier_id.name.lower() if self.carrier_id else ','.join(self._get_biteship_courier_codes()) + # Siapkan daftar kurir + couriers = ','.join(self._get_biteship_courier_codes()) - # Panggil API Biteship + # Panggil API Biteship dengan format yang benar api_mode = "koordinat" if use_coordinate else "kode_pos" + _logger.info(f"Calling Biteship API with mode: {api_mode}") + result = self._call_biteship_api(origin_data, destination_data, items, couriers) if not result: @@ -562,14 +566,19 @@ class SaleOrder(models.Model): shipping_options = [] shipping_services = result.get('pricing', []) + _logger.info(f"Ditemukan {len(shipping_services)} layanan pengiriman") + for service in shipping_services: courier_code = service.get('courier_code', '').lower() courier_name = service.get('courier_name', '') service_name = service.get('courier_service_name', '') price = service.get('price', 0) + _logger.info(f"Layanan: {courier_name} - {service_name}, Harga: {price}") + # Lewati layanan dengan harga 0 if not price: + _logger.warning(f"Melewati layanan dengan harga 0: {courier_name} - {service_name}") continue # Format estimasi waktu @@ -577,12 +586,13 @@ class SaleOrder(models.Model): shipment_range = service.get('shipment_duration_range', '') shipment_unit = service.get('shipment_duration_unit', 'days') + # Gunakan duration jika tersedia, jika tidak, buat dari range if duration: etd = duration elif shipment_range: etd = f"{shipment_range} {shipment_unit}" else: - etd = "1-3 days" + etd = "1-3 days" # Default fallback # Buat shipping option try: @@ -595,18 +605,19 @@ class SaleOrder(models.Model): }) shipping_options.append(shipping_option) + _logger.info(f"Berhasil membuat opsi pengiriman: {courier_name} - {service_name}") except Exception as e: _logger.error(f"Gagal membuat opsi pengiriman: {str(e)}") # Jika tidak ada opsi pengiriman if not shipping_options: - raise UserError(f"Tidak ada layanan pengiriman ditemukan untuk kode pos {destination_data}.") + raise UserError(f"Tidak ada layanan pengiriman ditemukan untuk kode pos {destination_data}. Mohon periksa kembali kode pos atau gunakan metode pengiriman lain.") # Set opsi pertama sebagai default self.shipping_option_id = shipping_options[0].id self.delivery_amt = shipping_options[0].price - # Tampilkan lokasi pengiriman dengan format yang lebih baik + # Format pesan untuk log yang lebih informatif if use_coordinate: origin_info = f"Koordinat ({origin_data.get('origin_latitude')}, {origin_data.get('origin_longitude')})" destination_info = f"Koordinat ({destination_data.get('destination_latitude')}, {destination_data.get('destination_longitude')})" @@ -620,17 +631,14 @@ class SaleOrder(models.Model): for opt in shipping_options ]) - # Tampilkan informasi tentang kurir yang dipilih - selected_option = shipping_options[0] - shipping_method_info = f"Metode Pengiriman: {self.carrier_id.name}" if self.carrier_id else "" - - # Log hasil estimasi dengan format yang lebih baik dan informasi kurir + # Log hasil estimasi dengan format yang lebih baik self.message_post( - body=f"Estimasi Ongkir Biteship ({origin_info} → {destination_info}):
{shipping_method_info}
{option_list}", + body=f"Estimasi Ongkir Biteship ({origin_info} → {destination_info}):
{option_list}", message_type="comment" ) # Simpan informasi untuk note ekspedisi + selected_option = shipping_options[0] # Opsi pertama dipilih sebagai default self.note_ekspedisi = f"Pengiriman: {selected_option.name} - Rp {selected_option.price:,.0f} ({selected_option.etd}) [via {api_mode}]" return { @@ -1435,7 +1443,7 @@ class SaleOrder(models.Model): raise UserError("This order not yet approved by customer procurement or director") if not order.client_order_ref and order.create_date > datetime(2024, 6, 27): - raise UserError("Customer Reference kosong, di isi dengan NO PO jika PO tidak ada mohon ditulis Tanpa PO") + raise UserError("Customer Reference kosong, di isi dengan NO PO jika PO tidak ada mohon ditulis Tanpa PO") if not order.commitment_date and order.create_date > datetime(2024, 9, 12): raise UserError("Expected Delivery Date kosong, wajib diisi") @@ -1462,7 +1470,7 @@ class SaleOrder(models.Model): if (partner.customer_type == 'pkp' or order.customer_type == 'pkp') and order.sppkp != partner.sppkp: raise UserError("SPPKP berbeda pada Master Data Customer") if not order.client_order_ref and order.create_date > datetime(2024, 6, 27): - raise UserError("Customer Reference kosong, di isi dengan NO PO jika PO tidak ada mohon ditulis Tanpa PO") + raise UserError("Customer Reference kosong, di isi dengan NO PO jika PO tidak ada mohon ditulis Tanpa PO") if not order.user_id.active: raise UserError("Salesperson sudah tidak aktif, mohon diisi yang benar pada data SO dan Contact") @@ -1673,7 +1681,7 @@ class SaleOrder(models.Model): raise UserError("This order not yet approved by customer procurement or director") if not order.client_order_ref and order.create_date > datetime(2024, 6, 27): - raise UserError("Customer Reference kosong, di isi dengan NO PO jika PO tidak ada mohon ditulis Tanpa PO") + raise UserError("Customer Reference kosong, di isi dengan NO PO jika PO tidak ada mohon ditulis Tanpa PO") if not order.commitment_date and order.create_date > datetime(2024, 9, 12): raise UserError("Expected Delivery Date kosong, wajib diisi") -- cgit v1.2.3 From e987dc891e999ebd1d04fb4f8cdeb44134c67aed Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 19 May 2025 22:21:31 +0700 Subject: (andri) fix bug shipping method --- indoteknik_custom/models/sale_order.py | 40 +++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index f09869da..382272b9 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -7,6 +7,7 @@ import logging, random, string, requests, math, json, re, qrcode, base64 from io import BytesIO from collections import defaultdict import pytz +from lxml import etree _logger = logging.getLogger(__name__) @@ -252,7 +253,44 @@ class SaleOrder(models.Model): select_shipping_option = fields.Selection([ ('biteship', 'Biteship'), ('custom', 'Custom'), - ], string='Select Shipping Option', help="Select shipping option for delivery", Tracking=True) + ], string='Select Shipping Option', help="Select shipping option for delivery", tracking=True) + + def get_biteship_carrier_ids(self): + courier_codes = tuple(self._get_biteship_courier_codes() or []) + if not courier_codes: + return [] + + self.env.cr.execute(""" + SELECT delivery_carrier_id + FROM rajaongkir_kurir + WHERE name IN %s AND delivery_carrier_id IS NOT NULL + """, (courier_codes,)) + result = self.env.cr.fetchall() + carrier_ids = [row[0] for row in result if row[0]] + return carrier_ids + + @api.model + def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + res = super(SaleOrder, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) + + if view_type == 'form': + doc = etree.XML(res['arch']) + + carrier_ids = self.get_biteship_carrier_ids() + if carrier_ids: + carrier_ids_str = '(' + ','.join(str(x) for x in carrier_ids) + ')' + else: + carrier_ids_str = '(-1,)' # aman kalau kosong + + # ✅ Tambahkan log di sini + _logger.info("🛰️ Biteship Carrier IDs: %s", carrier_ids) + _logger.info("📦 Domain string to apply: [('id', 'in', %s)]", carrier_ids_str) + + for node in doc.xpath("//field[@name='carrier_id']"): + node.set('domain', "[('id', 'in', %s)]" % carrier_ids_str) + + res['arch'] = etree.tostring(doc, encoding='unicode') + return res @api.onchange('shipping_option_id') def _onchange_shipping_option_id(self): -- cgit v1.2.3 From 2acd9dadd20499ab93c8ece237518f2a9cc7e296 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 19 May 2025 23:02:46 +0700 Subject: (andri) merapihkan log note estimate shipping --- indoteknik_custom/models/sale_order.py | 59 ++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 25 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 382272b9..f21f35b4 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -523,7 +523,6 @@ class SaleOrder(models.Model): return total_weight def action_estimate_shipping_biteship(self): - total_weight = self._validate_for_shipping_estimate() # Konversi berat ke gram untuk Biteship @@ -602,6 +601,7 @@ class SaleOrder(models.Model): # Proses hasil API shipping_options = [] + courier_options = {} # Dictionary untuk mengelompokkan opsi per kurir shipping_services = result.get('pricing', []) _logger.info(f"Ditemukan {len(shipping_services)} layanan pengiriman") @@ -643,19 +643,30 @@ class SaleOrder(models.Model): }) shipping_options.append(shipping_option) + + # Kelompokkan opsi berdasarkan kurir + courier_upper = courier_code.upper() + if courier_upper not in courier_options: + courier_options[courier_upper] = [] + courier_options[courier_upper].append({ + "name": service_name, + "etd": etd, + "price": price + }) + _logger.info(f"Berhasil membuat opsi pengiriman: {courier_name} - {service_name}") except Exception as e: _logger.error(f"Gagal membuat opsi pengiriman: {str(e)}") # Jika tidak ada opsi pengiriman if not shipping_options: - raise UserError(f"Tidak ada layanan pengiriman ditemukan untuk kode pos {destination_data}. Mohon periksa kembali kode pos atau gunakan metode pengiriman lain.") + raise UserError(f"Tidak ada layanan pengiriman ditemukan untuk kode pos {destination_data.get('destination_postal_code', '')}. Mohon periksa kembali kode pos atau gunakan metode pengiriman lain.") # Set opsi pertama sebagai default - self.shipping_option_id = shipping_options[0].id - self.delivery_amt = shipping_options[0].price + # self.shipping_option_id = shipping_options[0].id + # self.delivery_amt = shipping_options[0].price - # Format pesan untuk log yang lebih informatif + # Format untuk pesan log if use_coordinate: origin_info = f"Koordinat ({origin_data.get('origin_latitude')}, {origin_data.get('origin_longitude')})" destination_info = f"Koordinat ({destination_data.get('destination_latitude')}, {destination_data.get('destination_longitude')})" @@ -663,32 +674,30 @@ class SaleOrder(models.Model): origin_info = f"Kode Pos {origin_data.get('origin_postal_code')}" destination_info = f"Kode Pos {destination_data.get('destination_postal_code')}" - # Format daftar opsi pengiriman - option_list = '
'.join([ - f"{opt.name}: Rp {opt.price:,.0f} ({opt.etd})" - for opt in shipping_options - ]) + # PENTING: Gunakan HTML untuk teks preformatted agar jarak baris terjaga + message_lines = [f"Estimasi Ongkos Kirim Biteship ({origin_info} → {destination_info}):
"] + + # Format setiap kurir dan layanannya + for courier, options in courier_options.items(): + message_lines.append(f"{courier}:
") + for opt in options: + message_lines.append(f"Service: {opt['name']}, ETD: {opt['etd']}, Cost: Rp {int(opt['price']):,}
") + # Tambahkan baris kosong setelah setiap kurir (kecuali yang terakhir) + if courier != list(courier_options.keys())[-1]: + message_lines.append("
") + + # Gabungkan baris pesan dengan HTML line breaks + message_body = "".join(message_lines) - # Log hasil estimasi dengan format yang lebih baik + # Log hasil estimasi dengan format yang diinginkan self.message_post( - body=f"Estimasi Ongkir Biteship ({origin_info} → {destination_info}):
{option_list}", + body=message_body, message_type="comment" ) # Simpan informasi untuk note ekspedisi - selected_option = shipping_options[0] # Opsi pertama dipilih sebagai default - self.note_ekspedisi = f"Pengiriman: {selected_option.name} - Rp {selected_option.price:,.0f} ({selected_option.etd}) [via {api_mode}]" - - return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'params': { - 'title': 'Estimasi Ongkir Berhasil', - 'message': f'Mendapatkan {len(shipping_options)} opsi pengiriman menggunakan {api_mode}', - 'type': 'success', - 'sticky': False, - } - } + # selected_option = shipping_options[0] # Opsi pertama dipilih sebagai default + # self.note_ekspedisi = f"Pengiriman: {selected_option.name} - Rp {selected_option.price:,.0f} ({selected_option.etd}) [via {api_mode}]" def _call_biteship_api(self, origin_data, destination_data, items, couriers=None): -- cgit v1.2.3 From d38635f75fe52cbc958d96e577a83d8b3e1e1272 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 20 May 2025 13:47:18 +0700 Subject: (andri) fix set shipping option sesuai dengan method yang dipilih --- indoteknik_custom/models/sale_order.py | 60 +++++++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 11 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index f21f35b4..2b2e518a 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -248,7 +248,7 @@ class SaleOrder(models.Model): string="Attachment Bukti Cancel", readonly=False, ) nomor_so_pengganti = fields.Char(string='Nomor SO Pengganti', copy=False, tracking=3) - shipping_option_id = fields.Many2one("shipping.option", string="Selected Shipping Option", domain="['|', ('sale_order_id', '=', False), ('sale_order_id', '=', id)]") + shipping_option_id = fields.Many2one("shipping.option", string="Selected Shipping Option", domain="['|', ('sale_order_id', '=', False), ('sale_order_id', '=', id)]") select_shipping_option = fields.Selection([ ('biteship', 'Biteship'), @@ -295,7 +295,7 @@ class SaleOrder(models.Model): @api.onchange('shipping_option_id') def _onchange_shipping_option_id(self): if self.shipping_option_id: - self.delivery_amt = self.shipping_option_id.price + self.delivery_amt = self.shipping_option_id.price def _get_biteship_courier_codes(self): return [ @@ -340,12 +340,6 @@ class SaleOrder(models.Model): """, (rec.total_percent_margin, rec.id)) self.invalidate_cache() - @api.constrains('shipping_option_id') - def _check_shipping_option(self): - for rec in self: - if rec.shipping_option_id: - rec.delivery_amt = rec.shipping_option_id.price - def _compute_shipping_method_picking(self): for order in self: if order.picking_ids: @@ -662,9 +656,53 @@ class SaleOrder(models.Model): if not shipping_options: raise UserError(f"Tidak ada layanan pengiriman ditemukan untuk kode pos {destination_data.get('destination_postal_code', '')}. Mohon periksa kembali kode pos atau gunakan metode pengiriman lain.") - # Set opsi pertama sebagai default - # self.shipping_option_id = shipping_options[0].id - # self.delivery_amt = shipping_options[0].price + # Set opsi sesuai dengan carrier yang sudah dipilih, atau set opsi pertama sebagai default + selected_option = None + + if self.carrier_id: + # Dapatkan kode kurir dari carrier + rajaongkir_kurir = self.env['rajaongkir.kurir'].search([ + ('delivery_carrier_id', '=', self.carrier_id.id) + ], limit=1) + + # Jika ditemukan rajaongkir_kurir, cari shipping option yang sesuai + if rajaongkir_kurir: + courier_code = rajaongkir_kurir.name.lower() + carrier_name = self.carrier_id.name.lower() + + # Mencoba beberapa kemungkinan format untuk pencocokan + possible_codes = [ + courier_code, + carrier_name, + carrier_name.split()[0] if ' ' in carrier_name else carrier_name + ] + + _logger.info(f"Mencari shipping option untuk kurir: {possible_codes}") + + # Coba temukan shipping option yang sesuai dengan carrier + for option in shipping_options: + option_provider = option.provider.lower() if option.provider else '' + option_name = option.name.lower() if option.name else '' + + # Cek pencocokan untuk provider atau nama + for code in possible_codes: + if code in option_provider or code in option_name: + selected_option = option + _logger.info(f"Menemukan shipping option yang cocok: {option.name}") + break + + if selected_option: + break + + # Jika tidak ada opsi yang cocok dengan carrier, gunakan opsi pertama + if not selected_option and shipping_options: + selected_option = shipping_options[0] + _logger.info(f"Menggunakan opsi pertama: {selected_option.name}") + + # Set shipping option yang terpilih + if selected_option: + self.shipping_option_id = selected_option.id + self.delivery_amt = selected_option.price # Format untuk pesan log if use_coordinate: -- cgit v1.2.3 From c109f6704106c59d37715b9e22f464e6f5b106db Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 20 May 2025 23:16:24 +0700 Subject: (andri) fix list shipping option sesuai dengan method yang dipilih --- indoteknik_custom/models/sale_order.py | 130 +++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 2b2e518a..b2faf9f3 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -302,6 +302,136 @@ class SaleOrder(models.Model): 'gojek','grab','deliveree','lalamove','jne','tiki','ninja','lion','rara','sicepat','jnt','pos','idexpress','rpx','wahana','jdl','pos','anteraja','sap','paxel','borzo' ] + @api.onchange('carrier_id') + def _onchange_carrier_id(self): + self.shipping_option_id = False + self.delivery_amt = 0 + + if not self.carrier_id: + return {'domain': {'shipping_option_id': [('id', '=', -1)]}} + + # Cari provider dari carrier yang dipilih langsung dari rajaongkir_kurir + self.env.cr.execute(""" + SELECT name FROM rajaongkir_kurir + WHERE delivery_carrier_id = %s + LIMIT 1 + """, (self.carrier_id.id,)) + + result = self.env.cr.fetchone() + provider = result[0].lower() if result and result[0] else False + + # Fallback jika tidak ditemukan di rajaongkir_kurir + if not provider: + # Gunakan nama carrier, ambil kata pertama + provider = self.carrier_id.name.lower().split()[0] if self.carrier_id.name else False + + # Log untuk debugging + _logger.info(f"Carrier changed to {self.carrier_id.name}, provider: {provider}") + + # PENTING: self.id mungkin False atau NewId pada saat onchange + # Perlu memeriksa apakah ini adalah record baru atau yang sudah ada + sale_order_id = False + if hasattr(self, '_origin') and self._origin: + sale_order_id = self._origin.id + + # Cek jika ada shipping options dengan provider ini (tanpa filter sale_order_id dulu) + self.env.cr.execute(""" + SELECT COUNT(*) FROM shipping_option + WHERE LOWER(provider) LIKE %s + """, (f'%{provider}%',)) + + count = self.env.cr.fetchone()[0] + _logger.info(f"Found {count} shipping options for provider {provider}") + + # Buat domain untuk shipping_option_id + if count > 0: + # Jika ada options yang tersedia, buat domain yang lebih permisif + domain = [ + '|', + ('provider', 'ilike', f'%{provider}%'), + ('provider', '=', provider) + ] + + # Jika ini record yang sudah ada, tambahkan filter sale_order_id + if sale_order_id: + domain = [ + '|', + '&', ('sale_order_id', '=', sale_order_id), ('provider', 'ilike', f'%{provider}%'), + '&', ('sale_order_id', '=', False), ('provider', 'ilike', f'%{provider}%') + ] + else: + domain = [('id', '=', -1)] # Tidak ada opsi + + _logger.info(f"Final domain for shipping_option_id: {domain}") + + # Masih menggunakan pendekatan mengembalikan domain karena ini yang paling efektif + # meskipun ada peringatan deprecated + return {'domain': {'shipping_option_id': domain}} + + @api.model + def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + res = super(SaleOrder, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) + + if view_type == 'form': + doc = etree.XML(res['arch']) + + # Filter carrier_id: hanya yang dari Biteship + carrier_ids = self.get_biteship_carrier_ids() + carrier_ids_str = '(' + ','.join(str(x) for x in carrier_ids) + ')' if carrier_ids else '(-1,)' + for node in doc.xpath("//field[@name='carrier_id']"): + node.set('domain', "[('id', 'in', %s)]" % carrier_ids_str) + + # PERBAIKAN UTAMA: Filter shipping_option_id saat form dibuka dalam mode edit + sale_id = self._context.get('active_id') + if sale_id: + # Ambil carrier_id dari database untuk record yang sedang diedit + self.env.cr.execute("SELECT carrier_id FROM sale_order WHERE id = %s", (sale_id,)) + carrier_result = self.env.cr.fetchone() + carrier_id = carrier_result[0] if carrier_result else None + + if carrier_id: + # Cari provider dari rajaongkir_kurir + self.env.cr.execute(""" + SELECT name FROM rajaongkir_kurir + WHERE delivery_carrier_id = %s + LIMIT 1 + """, (carrier_id,)) + row = self.env.cr.fetchone() + provider = None + + if row and row[0]: + provider = row[0].lower() + else: + # Fallback ke nama carrier + self.env.cr.execute("SELECT name FROM delivery_carrier WHERE id = %s", (carrier_id,)) + row = self.env.cr.fetchone() + if row and row[0]: + provider = row[0].lower().split()[0] + + if provider: + _logger.info(f"fields_view_get - Found provider: {provider} for carrier_id: {carrier_id}") + + # PENTING: Query untuk mendapatkan shipping options yang sesuai + # Ambil semua options yang memiliki provider yang cocok + # Dan prioritaskan yang terkait dengan sale_order ini + domain_str = f""" + [ + '|', + '&', ('sale_order_id', '=', {sale_id}), ('provider', 'ilike', '%{provider}%'), + '&', ('sale_order_id', '=', False), ('provider', 'ilike', '%{provider}%') + ] + """ + + # Set domain ke field shipping_option_id + for node in doc.xpath("//field[@name='shipping_option_id']"): + node.set('domain', domain_str) + # Tambahkan options untuk mencegah quick create + node.set('options', "{'no_create': True, 'no_quick_create': True}") + + _logger.info(f"Setting domain in fields_view_get: {domain_str}") + + res['arch'] = etree.tostring(doc, encoding='unicode') + return res @api.onchange('select_shipping_option') def _onchange_select_shipping_option(self): self.shipping_option_id = False -- cgit v1.2.3 From afb745b1e000f4d3c3dba1723ce4a19f44b8c510 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 21 May 2025 09:18:39 +0700 Subject: (andri) fix edit shipping option --- indoteknik_custom/models/sale_order.py | 95 +++++++++++++--------------------- 1 file changed, 35 insertions(+), 60 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index b2faf9f3..bcc4d5c4 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -363,75 +363,50 @@ class SaleOrder(models.Model): domain = [('id', '=', -1)] # Tidak ada opsi _logger.info(f"Final domain for shipping_option_id: {domain}") - - # Masih menggunakan pendekatan mengembalikan domain karena ini yang paling efektif - # meskipun ada peringatan deprecated return {'domain': {'shipping_option_id': domain}} @api.model - def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): - res = super(SaleOrder, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) + def fields_get(self, allfields=None, attributes=None): + res = super().fields_get(allfields=allfields, attributes=attributes) - if view_type == 'form': - doc = etree.XML(res['arch']) + # Aktifkan hanya kalau sedang buka form Sales Order (safety check) + if self.env.context.get('params', {}).get('model') == 'sale.order' and \ + self.env.context.get('params', {}).get('id'): - # Filter carrier_id: hanya yang dari Biteship - carrier_ids = self.get_biteship_carrier_ids() - carrier_ids_str = '(' + ','.join(str(x) for x in carrier_ids) + ')' if carrier_ids else '(-1,)' - for node in doc.xpath("//field[@name='carrier_id']"): - node.set('domain', "[('id', 'in', %s)]" % carrier_ids_str) + sale_id = self.env.context['params']['id'] - # PERBAIKAN UTAMA: Filter shipping_option_id saat form dibuka dalam mode edit - sale_id = self._context.get('active_id') - if sale_id: - # Ambil carrier_id dari database untuk record yang sedang diedit - self.env.cr.execute("SELECT carrier_id FROM sale_order WHERE id = %s", (sale_id,)) - carrier_result = self.env.cr.fetchone() - carrier_id = carrier_result[0] if carrier_result else None - - if carrier_id: - # Cari provider dari rajaongkir_kurir - self.env.cr.execute(""" - SELECT name FROM rajaongkir_kurir - WHERE delivery_carrier_id = %s - LIMIT 1 - """, (carrier_id,)) + # Ambil carrier_id dari SO yang sedang dibuka + self.env.cr.execute("SELECT carrier_id FROM sale_order WHERE id = %s", (sale_id,)) + row = self.env.cr.fetchone() + carrier_id = row[0] if row else None + + provider = None + if carrier_id: + self.env.cr.execute(""" + SELECT name FROM rajaongkir_kurir WHERE delivery_carrier_id = %s LIMIT 1 + """, (carrier_id,)) + row = self.env.cr.fetchone() + if row and row[0]: + provider = row[0].lower() + else: + self.env.cr.execute("SELECT name FROM delivery_carrier WHERE id = %s", (carrier_id,)) row = self.env.cr.fetchone() - provider = None - - if row and row[0]: - provider = row[0].lower() - else: - # Fallback ke nama carrier - self.env.cr.execute("SELECT name FROM delivery_carrier WHERE id = %s", (carrier_id,)) - row = self.env.cr.fetchone() - if row and row[0]: - provider = row[0].lower().split()[0] - - if provider: - _logger.info(f"fields_view_get - Found provider: {provider} for carrier_id: {carrier_id}") - - # PENTING: Query untuk mendapatkan shipping options yang sesuai - # Ambil semua options yang memiliki provider yang cocok - # Dan prioritaskan yang terkait dengan sale_order ini - domain_str = f""" - [ - '|', - '&', ('sale_order_id', '=', {sale_id}), ('provider', 'ilike', '%{provider}%'), - '&', ('sale_order_id', '=', False), ('provider', 'ilike', '%{provider}%') - ] - """ - - # Set domain ke field shipping_option_id - for node in doc.xpath("//field[@name='shipping_option_id']"): - node.set('domain', domain_str) - # Tambahkan options untuk mencegah quick create - node.set('options', "{'no_create': True, 'no_quick_create': True}") - - _logger.info(f"Setting domain in fields_view_get: {domain_str}") + provider = row[0].lower().split()[0] if row and row[0] else '' + + if provider: + domain = [ + '|', + '&', ('sale_order_id', '=', sale_id), ('provider', 'ilike', f'%{provider}%'), + '&', ('sale_order_id', '=', False), ('provider', 'ilike', f'%{provider}%') + ] + + if 'shipping_option_id' in res: + res['shipping_option_id']['domain'] = domain + _logger.info(f"fields_get - Injected domain for shipping_option_id: {domain}") - res['arch'] = etree.tostring(doc, encoding='unicode') return res + + @api.onchange('select_shipping_option') def _onchange_select_shipping_option(self): self.shipping_option_id = False -- cgit v1.2.3 From 34174e95638e1337169dfa5f3be56b9ef57021a1 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 22 May 2025 11:14:35 +0700 Subject: (andri) sync quotation web ke odoo terkait kurir, service, & tarif --- indoteknik_custom/models/sale_order.py | 173 ++++++++++++++++++--------------- 1 file changed, 97 insertions(+), 76 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index bcc4d5c4..5a5255b3 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -64,6 +64,7 @@ class ShippingOption(models.Model): price = fields.Float(string="Price", required=True) provider = fields.Char(string="Provider") etd = fields.Char(string="Estimated Delivery Time") + courier_service_code = fields.Char(string="Courier Service Code") sale_order_id = fields.Many2one('sale.order', string="Sale Order", ondelete="cascade") class SaleOrder(models.Model): @@ -253,7 +254,7 @@ class SaleOrder(models.Model): select_shipping_option = fields.Selection([ ('biteship', 'Biteship'), ('custom', 'Custom'), - ], string='Select Shipping Option', help="Select shipping option for delivery", tracking=True) + ], string='Shipping Option', help="Select shipping option for delivery", tracking=True) def get_biteship_carrier_ids(self): courier_codes = tuple(self._get_biteship_courier_codes() or []) @@ -603,8 +604,8 @@ class SaleOrder(models.Model): product_names = '
'.join(missing_weight_products) self.message_post(body=f"Produk berikut tidak memiliki berat:
{product_names}") - if total_weight == 0: - raise UserError("Tidak dapat mengestimasi ongkir tanpa berat yang valid.") + # if total_weight == 0: + # raise UserError("Tidak dapat mengestimasi ongkir tanpa berat yang valid.") # Validasi alamat pengiriman if not self.real_shipping_id: @@ -623,13 +624,11 @@ class SaleOrder(models.Model): def action_estimate_shipping_biteship(self): total_weight = self._validate_for_shipping_estimate() - - # Konversi berat ke gram untuk Biteship + weight_gram = int(total_weight * 1000) if weight_gram < 100: - weight_gram = 100 # Minimum weight untuk Biteship - - # Persiapkan data item + weight_gram = 100 + items = [{ "name": "Paket Pesanan", "description": f"Sale Order {self.name}", @@ -640,26 +639,21 @@ class SaleOrder(models.Model): "width": 10, "length": 10 }] - - # Coba dapatkan data koordinat dari alamat pengiriman + shipping_address = self.real_shipping_id _logger.info(f"Shipping Address: {shipping_address}") - - # Data asal (tetap gudang Bandengan) + origin_data = { "origin_latitude": -6.3031123, "origin_longitude": 106.7794934, } - - # Prioritaskan penggunaan koordinat jika tersedia + destination_data = {} use_coordinate = False - - # Cek apakah latitude dan longitude tersedia dan valid + if hasattr(shipping_address, 'latitude') and hasattr(shipping_address, 'longtitude'): if shipping_address.latitude and shipping_address.longtitude: try: - # Validasi format koordinat lat = float(shipping_address.latitude) lng = float(shipping_address.longtitude) destination_data = { @@ -671,79 +665,72 @@ class SaleOrder(models.Model): except (ValueError, TypeError): _logger.warning(f"Invalid coordinates, falling back to postal code") use_coordinate = False - - # Jika koordinat tidak tersedia atau tidak valid, gunakan kode pos + if not use_coordinate: if shipping_address.zip: - origin_data = {"origin_postal_code": 14440} # Reset origin untuk mode kode pos + origin_data = {"origin_postal_code": 14440} destination_data = { "destination_postal_code": shipping_address.zip } _logger.info(f"Using postal code: {shipping_address.zip}") else: raise UserError("Tidak dapat mengestimasikan ongkir: Kode pos tujuan tidak tersedia.") - - # Siapkan daftar kurir + couriers = ','.join(self._get_biteship_courier_codes()) - - # Panggil API Biteship dengan format yang benar + api_mode = "koordinat" if use_coordinate else "kode_pos" _logger.info(f"Calling Biteship API with mode: {api_mode}") - + result = self._call_biteship_api(origin_data, destination_data, items, couriers) - + if not result: raise UserError("Gagal mendapatkan estimasi ongkir dari Biteship.") - - # Hapus shipping_option lama + self.env["shipping.option"].search([('sale_order_id', '=', self.id)]).unlink() - - # Proses hasil API + shipping_options = [] - courier_options = {} # Dictionary untuk mengelompokkan opsi per kurir + courier_options = {} shipping_services = result.get('pricing', []) - + _logger.info(f"Ditemukan {len(shipping_services)} layanan pengiriman") - + for service in shipping_services: courier_code = service.get('courier_code', '').lower() courier_name = service.get('courier_name', '') service_name = service.get('courier_service_name', '') - price = service.get('price', 0) - + raw_price = service.get('price', 0) + markup_price = int(raw_price * 1.1) + price = round(markup_price / 1000) * 1000 + _logger.info(f"Layanan: {courier_name} - {service_name}, Harga: {price}") - - # Lewati layanan dengan harga 0 + if not price: _logger.warning(f"Melewati layanan dengan harga 0: {courier_name} - {service_name}") continue - - # Format estimasi waktu + duration = service.get('duration', '') shipment_range = service.get('shipment_duration_range', '') shipment_unit = service.get('shipment_duration_unit', 'days') - - # Gunakan duration jika tersedia, jika tidak, buat dari range + if duration: etd = duration elif shipment_range: etd = f"{shipment_range} {shipment_unit}" else: - etd = "1-3 days" # Default fallback - - # Buat shipping option + etd = "1-3 days" + try: shipping_option = self.env["shipping.option"].create({ "name": f"{courier_name} - {service_name}", "price": price, "provider": courier_code, "etd": etd, + "courier_service_code": service.get('courier_service_code'), "sale_order_id": self.id, }) - + shipping_options.append(shipping_option) - - # Kelompokkan opsi berdasarkan kurir + courier_upper = courier_code.upper() if courier_upper not in courier_options: courier_options[courier_upper] = [] @@ -752,87 +739,72 @@ class SaleOrder(models.Model): "etd": etd, "price": price }) - + _logger.info(f"Berhasil membuat opsi pengiriman: {courier_name} - {service_name}") except Exception as e: _logger.error(f"Gagal membuat opsi pengiriman: {str(e)}") - - # Jika tidak ada opsi pengiriman + if not shipping_options: raise UserError(f"Tidak ada layanan pengiriman ditemukan untuk kode pos {destination_data.get('destination_postal_code', '')}. Mohon periksa kembali kode pos atau gunakan metode pengiriman lain.") - - # Set opsi sesuai dengan carrier yang sudah dipilih, atau set opsi pertama sebagai default + selected_option = None if self.carrier_id: - # Dapatkan kode kurir dari carrier rajaongkir_kurir = self.env['rajaongkir.kurir'].search([ ('delivery_carrier_id', '=', self.carrier_id.id) ], limit=1) - - # Jika ditemukan rajaongkir_kurir, cari shipping option yang sesuai + if rajaongkir_kurir: courier_code = rajaongkir_kurir.name.lower() carrier_name = self.carrier_id.name.lower() - - # Mencoba beberapa kemungkinan format untuk pencocokan + possible_codes = [ courier_code, carrier_name, carrier_name.split()[0] if ' ' in carrier_name else carrier_name ] - + _logger.info(f"Mencari shipping option untuk kurir: {possible_codes}") - - # Coba temukan shipping option yang sesuai dengan carrier + for option in shipping_options: option_provider = option.provider.lower() if option.provider else '' option_name = option.name.lower() if option.name else '' - - # Cek pencocokan untuk provider atau nama + for code in possible_codes: if code in option_provider or code in option_name: selected_option = option _logger.info(f"Menemukan shipping option yang cocok: {option.name}") break - + if selected_option: break - # Jika tidak ada opsi yang cocok dengan carrier, gunakan opsi pertama if not selected_option and shipping_options: selected_option = shipping_options[0] _logger.info(f"Menggunakan opsi pertama: {selected_option.name}") - # Set shipping option yang terpilih if selected_option: self.shipping_option_id = selected_option.id self.delivery_amt = selected_option.price - - # Format untuk pesan log + if use_coordinate: origin_info = f"Koordinat ({origin_data.get('origin_latitude')}, {origin_data.get('origin_longitude')})" destination_info = f"Koordinat ({destination_data.get('destination_latitude')}, {destination_data.get('destination_longitude')})" else: origin_info = f"Kode Pos {origin_data.get('origin_postal_code')}" destination_info = f"Kode Pos {destination_data.get('destination_postal_code')}" - - # PENTING: Gunakan HTML untuk teks preformatted agar jarak baris terjaga + message_lines = [f"Estimasi Ongkos Kirim Biteship ({origin_info} → {destination_info}):
"] - - # Format setiap kurir dan layanannya + for courier, options in courier_options.items(): message_lines.append(f"{courier}:
") for opt in options: message_lines.append(f"Service: {opt['name']}, ETD: {opt['etd']}, Cost: Rp {int(opt['price']):,}
") - # Tambahkan baris kosong setelah setiap kurir (kecuali yang terakhir) if courier != list(courier_options.keys())[-1]: message_lines.append("
") - - # Gabungkan baris pesan dengan HTML line breaks + message_body = "".join(message_lines) - - # Log hasil estimasi dengan format yang diinginkan + self.message_post( body=message_body, message_type="comment" @@ -2290,10 +2262,59 @@ class SaleOrder(models.Model): order_line.discount = discount order_line.order_id.use_button = True + def _auto_set_shipping_from_website(self): + for order in self: + # Jalankan hanya untuk SO dari website (ID 59) + if not order.source_id or order.source_id.id != 59: + continue + + # Jika shipping method adalah Self Pick Up (ID 32), atur ke custom + if order.carrier_id and order.carrier_id.id == 32: + order.select_shipping_option = 'custom' + continue + + # Set shipping option ke biteship dan jalankan estimasi + order.select_shipping_option = 'biteship' + order.action_estimate_shipping() + + if not (order.delivery_service_type and order.carrier_id): + continue + + # Ambil provider dari rajaongkir_kurir + self.env.cr.execute(""" + SELECT name FROM rajaongkir_kurir + WHERE delivery_carrier_id = %s LIMIT 1 + """, (order.carrier_id.id,)) + result = self.env.cr.fetchone() + provider = result[0].lower() if result and result[0] else '' + + if not provider and order.carrier_id.name: + provider = order.carrier_id.name.lower().split()[0] + + if not provider: + _logger.warning(f"[AutoSetShipping] Provider tidak ditemukan untuk carrier_id: {order.carrier_id.id}") + continue + + # Cari shipping option berdasarkan provider dan courier_service_code + matched_option = self.env['shipping.option'].search([ + ('sale_order_id', '=', order.id), + ('courier_service_code', '=', order.delivery_service_type), + ('provider', 'ilike', provider), + ], limit=1) + + if matched_option: + order.shipping_option_id = matched_option.id + order.delivery_amt = matched_option.price + _logger.info(f"[AutoSetShipping] Matched via courier_service_code: {matched_option.name} | Provider: {provider}") + else: + _logger.warning(f"[AutoSetShipping] No match for service code '{order.delivery_service_type}' and provider '{provider}' in SO {order.name}") + + @api.model def create(self, vals): # Ensure partner details are updated when a sale order is created order = super(SaleOrder, self).create(vals) + order._auto_set_shipping_from_website() order._compute_etrts_date() order._validate_expected_ready_ship_date() order._validate_delivery_amt() -- cgit v1.2.3 From fad209db285b0a6204dc1fcbf2e2e0cb13f872b0 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Fri, 23 May 2025 09:05:27 +0700 Subject: (andri) penyesuaian data quotation SO ke delivery biteship --- indoteknik_custom/models/sale_order.py | 6 ++++-- indoteknik_custom/models/stock_picking.py | 5 ++++- 2 files changed, 8 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 5a5255b3..38d2505d 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -249,7 +249,7 @@ class SaleOrder(models.Model): string="Attachment Bukti Cancel", readonly=False, ) nomor_so_pengganti = fields.Char(string='Nomor SO Pengganti', copy=False, tracking=3) - shipping_option_id = fields.Many2one("shipping.option", string="Selected Shipping Option", domain="['|', ('sale_order_id', '=', False), ('sale_order_id', '=', id)]") + shipping_option_id = fields.Many2one("shipping.option", string="Selected Service Option", domain="['|', ('sale_order_id', '=', False), ('sale_order_id', '=', id)]") select_shipping_option = fields.Selection([ ('biteship', 'Biteship'), @@ -296,7 +296,8 @@ class SaleOrder(models.Model): @api.onchange('shipping_option_id') def _onchange_shipping_option_id(self): if self.shipping_option_id: - self.delivery_amt = self.shipping_option_id.price + self.delivery_amt = self.shipping_option_id.price + self.delivery_service_type = self.shipping_option_id.courier_service_code def _get_biteship_courier_codes(self): return [ @@ -786,6 +787,7 @@ class SaleOrder(models.Model): if selected_option: self.shipping_option_id = selected_option.id self.delivery_amt = selected_option.price + self.delivery_service_type = selected_option.courier_service_code if use_coordinate: origin_info = f"Koordinat ({origin_data.get('origin_latitude')}, {origin_data.get('origin_longitude')})" diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 39c74aa2..5548db75 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -565,7 +565,7 @@ class StockPicking(models.Model): "latitude": -6.3031123, "longitude" : 106.7794934999 }, - "reference_id " : self.sale_id.name, + "reference_id" : self.sale_id.name, "shipper_contact_name": self.carrier_id.pic_name or '', "shipper_contact_phone": self.carrier_id.pic_phone or '', "shipper_organization": self.carrier_id.name, @@ -585,6 +585,8 @@ class StockPicking(models.Model): "items": items_data_standard } + _logger.info(f"Payload untuk Biteship: {payload}") + # Cek jika pengiriman instant atau same_day if self.sale_id.delivery_service_type and ("instant" in self.sale_id.delivery_service_type or "same_day" in self.sale_id.delivery_service_type): payload.update({ @@ -603,6 +605,7 @@ class StockPicking(models.Model): # Kirim request ke Biteship response = requests.post(_biteship_url+'/orders', headers=headers, json=payload) + _logger.info(f"Response dari Biteship: {response.text}") if response.status_code == 200: data = response.json() -- cgit v1.2.3 From 48a2eae94b66f7bb8916dcd984bce17fbb36d45e Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Fri, 23 May 2025 09:44:34 +0700 Subject: (andri) add flag di API untuk mengetahui quotation SO dibuat dari website atau bukan --- indoteknik_custom/models/sale_order.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 38d2505d..a41e001b 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -2265,24 +2265,33 @@ class SaleOrder(models.Model): order_line.order_id.use_button = True def _auto_set_shipping_from_website(self): + # Jalankan hanya jika context menandakan ini proses checkout + if not self.env.context.get('from_website_checkout'): + _logger.info("[AutoSetShipping] Dilewati karena bukan dari website checkout (context tidak ada)") + return + for order in self: - # Jalankan hanya untuk SO dari website (ID 59) + _logger.info(f"[AutoSetShipping] Proses otomatis untuk SO: {order.name}") + + # Validasi: pastikan source_id = Website if not order.source_id or order.source_id.id != 59: + _logger.warning(f"[AutoSetShipping] SO {order.name} bukan dari Website (source_id.id != 59)") continue - # Jika shipping method adalah Self Pick Up (ID 32), atur ke custom + # Abaikan jika shipping method adalah Self Pick Up if order.carrier_id and order.carrier_id.id == 32: order.select_shipping_option = 'custom' + _logger.info(f"[AutoSetShipping] SO {order.name} menggunakan Self Pick Up. Set ke custom.") continue - # Set shipping option ke biteship dan jalankan estimasi order.select_shipping_option = 'biteship' order.action_estimate_shipping() if not (order.delivery_service_type and order.carrier_id): + _logger.warning(f"[AutoSetShipping] SO {order.name} tidak memiliki delivery_service_type atau carrier_id") continue - # Ambil provider dari rajaongkir_kurir + # Cari provider dari mapping rajaongkir_kurir self.env.cr.execute(""" SELECT name FROM rajaongkir_kurir WHERE delivery_carrier_id = %s LIMIT 1 @@ -2297,7 +2306,7 @@ class SaleOrder(models.Model): _logger.warning(f"[AutoSetShipping] Provider tidak ditemukan untuk carrier_id: {order.carrier_id.id}") continue - # Cari shipping option berdasarkan provider dan courier_service_code + # Cari shipping option berdasarkan courier_service_code + provider matched_option = self.env['shipping.option'].search([ ('sale_order_id', '=', order.id), ('courier_service_code', '=', order.delivery_service_type), @@ -2307,15 +2316,16 @@ class SaleOrder(models.Model): if matched_option: order.shipping_option_id = matched_option.id order.delivery_amt = matched_option.price - _logger.info(f"[AutoSetShipping] Matched via courier_service_code: {matched_option.name} | Provider: {provider}") + _logger.info(f"[AutoSetShipping] Match: {matched_option.name} | Provider: {provider} | Price: {matched_option.price}") else: - _logger.warning(f"[AutoSetShipping] No match for service code '{order.delivery_service_type}' and provider '{provider}' in SO {order.name}") + _logger.warning(f"[AutoSetShipping] Tidak ditemukan match untuk SO {order.name} dengan service_code '{order.delivery_service_type}' dan provider '{provider}'") @api.model def create(self, vals): # Ensure partner details are updated when a sale order is created order = super(SaleOrder, self).create(vals) + _logger.info(f"[CREATE CONTEXT] {self.env.context}") order._auto_set_shipping_from_website() order._compute_etrts_date() order._validate_expected_ready_ship_date() -- cgit v1.2.3 From 89b157500f517659bb931f6ec81d47f2390ebfd2 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 24 May 2025 10:15:37 +0700 Subject: (andri) validasi shipping option jika tidak sesuai dengan shipping method --- indoteknik_custom/models/sale_order.py | 100 +++++++++++++++++++++------------ 1 file changed, 64 insertions(+), 36 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index a41e001b..d61e3641 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -367,46 +367,74 @@ class SaleOrder(models.Model): _logger.info(f"Final domain for shipping_option_id: {domain}") return {'domain': {'shipping_option_id': domain}} - @api.model - def fields_get(self, allfields=None, attributes=None): - res = super().fields_get(allfields=allfields, attributes=attributes) - - # Aktifkan hanya kalau sedang buka form Sales Order (safety check) - if self.env.context.get('params', {}).get('model') == 'sale.order' and \ - self.env.context.get('params', {}).get('id'): - - sale_id = self.env.context['params']['id'] - - # Ambil carrier_id dari SO yang sedang dibuka - self.env.cr.execute("SELECT carrier_id FROM sale_order WHERE id = %s", (sale_id,)) - row = self.env.cr.fetchone() - carrier_id = row[0] if row else None + @api.onchange('shipping_option_id') + def _onchange_shipping_option_id(self): + if not self.shipping_option_id or not self.carrier_id: + return - provider = None - if carrier_id: - self.env.cr.execute(""" - SELECT name FROM rajaongkir_kurir WHERE delivery_carrier_id = %s LIMIT 1 - """, (carrier_id,)) - row = self.env.cr.fetchone() - if row and row[0]: - provider = row[0].lower() - else: - self.env.cr.execute("SELECT name FROM delivery_carrier WHERE id = %s", (carrier_id,)) - row = self.env.cr.fetchone() - provider = row[0].lower().split()[0] if row and row[0] else '' + # Ambil provider dari carrier + self.env.cr.execute(""" + SELECT name FROM rajaongkir_kurir + WHERE delivery_carrier_id = %s LIMIT 1 + """, (self.carrier_id.id,)) + row = self.env.cr.fetchone() + provider = row[0].lower() if row and row[0] else self.carrier_id.name.lower().split()[0] - if provider: - domain = [ - '|', - '&', ('sale_order_id', '=', sale_id), ('provider', 'ilike', f'%{provider}%'), - '&', ('sale_order_id', '=', False), ('provider', 'ilike', f'%{provider}%') - ] + selected_provider = (self.shipping_option_id.provider or '').lower() - if 'shipping_option_id' in res: - res['shipping_option_id']['domain'] = domain - _logger.info(f"fields_get - Injected domain for shipping_option_id: {domain}") + if provider not in selected_provider: + warning_msg = { + 'title': "Opsi Tidak Valid", + 'message': f"Opsi pengiriman '{self.shipping_option_id.name}' tidak cocok dengan metode '{self.carrier_id.name}'. Dikembalikan ke sebelumnya." + } - return res + # Kembalikan ke nilai lama (jika record sudah disimpan) + self.shipping_option_id = self._origin.shipping_option_id if self._origin else False + return {'warning': warning_msg} + + # Jika valid + self.delivery_amt = self.shipping_option_id.price + self.delivery_service_type = self.shipping_option_id.courier_service_code + + # @api.model + # def fields_get(self, allfields=None, attributes=None): + # res = super().fields_get(allfields=allfields, attributes=attributes) + + # # Aktifkan hanya kalau sedang buka form Sales Order (safety check) + # if self.env.context.get('params', {}).get('model') == 'sale.order' and \ + # self.env.context.get('params', {}).get('id'): + + # sale_id = self.env.context['params']['id'] + + # # Ambil carrier_id dari SO yang sedang dibuka + # self.env.cr.execute("SELECT carrier_id FROM sale_order WHERE id = %s", (sale_id,)) + # row = self.env.cr.fetchone() + # carrier_id = row[0] if row else None + + # provider = None + # if carrier_id: + # self.env.cr.execute(""" + # SELECT name FROM rajaongkir_kurir WHERE delivery_carrier_id = %s LIMIT 1 + # """, (carrier_id,)) + # row = self.env.cr.fetchone() + # if row and row[0]: + # provider = row[0].lower() + # else: + # self.env.cr.execute("SELECT name FROM delivery_carrier WHERE id = %s", (carrier_id,)) + # row = self.env.cr.fetchone() + # provider = row[0].lower().split()[0] if row and row[0] else '' + + # if provider: + # domain = [ + # '|', + # '&', ('sale_order_id', '=', sale_id), ('provider', 'ilike', f'%{provider}%'), + # '&', ('sale_order_id', '=', False), ('provider', 'ilike', f'%{provider}%') + # ] + + # if 'shipping_option_id' in res: + # res['shipping_option_id']['domain'] = domain + # _logger.info(f"fields_get - Injected domain for shipping_option_id: {domain}") + # return res @api.onchange('select_shipping_option') -- cgit v1.2.3 From efb7581b37cb6b007e249c201a4e48a3e5261dd1 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sun, 25 May 2025 17:47:13 +0700 Subject: (andri) fix bug value delivery service type yang kembali ke nilai awal ketika di save --- indoteknik_custom/models/sale_order.py | 40 +++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 6 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index d61e3641..8cf38040 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -293,11 +293,11 @@ class SaleOrder(models.Model): res['arch'] = etree.tostring(doc, encoding='unicode') return res - @api.onchange('shipping_option_id') - def _onchange_shipping_option_id(self): - if self.shipping_option_id: - self.delivery_amt = self.shipping_option_id.price - self.delivery_service_type = self.shipping_option_id.courier_service_code + # @api.onchange('shipping_option_id') + # def _onchange_shipping_option_id(self): + # if self.shipping_option_id: + # self.delivery_amt = self.shipping_option_id.price + # self.delivery_service_type = self.shipping_option_id.courier_service_code def _get_biteship_courier_codes(self): return [ @@ -369,7 +369,13 @@ class SaleOrder(models.Model): @api.onchange('shipping_option_id') def _onchange_shipping_option_id(self): - if not self.shipping_option_id or not self.carrier_id: + if not self.shipping_option_id: + return + + if not self.carrier_id: + # Jika belum pilih carrier, tetap update harga dan service type + self.delivery_amt = self.shipping_option_id.price + self.delivery_service_type = self.shipping_option_id.courier_service_code return # Ambil provider dari carrier @@ -396,6 +402,19 @@ class SaleOrder(models.Model): self.delivery_amt = self.shipping_option_id.price self.delivery_service_type = self.shipping_option_id.courier_service_code + def _update_delivery_service_type_from_shipping_option(self, vals): + shipping_option_id = vals.get('shipping_option_id') or self.shipping_option_id.id + if shipping_option_id: + shipping_option = self.env['shipping.option'].browse(shipping_option_id) + if shipping_option.exists(): + courier_service = shipping_option.courier_service_code + vals['delivery_service_type'] = courier_service + _logger.info("🛰️ Set delivery_service_type: %s from shipping_option_id: %s", courier_service, shipping_option_id) + else: + _logger.warning("⚠️ shipping_option_id %s not found or invalid.", shipping_option_id) + else: + _logger.info("ℹ️ shipping_option_id not found in vals or record.") + # @api.model # def fields_get(self, allfields=None, attributes=None): # res = super().fields_get(allfields=allfields, attributes=attributes) @@ -2390,6 +2409,13 @@ class SaleOrder(models.Model): 'customer_type': partner.customer_type, }) + def write(self, vals): + if 'shipping_option_id' in vals and vals['shipping_option_id']: + shipping_option = self.env['shipping.option'].browse(vals['shipping_option_id']) + if shipping_option: + vals['delivery_service_type'] = shipping_option.courier_service_code + return super(SaleOrder, self).write(vals) + def write(self, vals): for order in self: if order.state in ['sale', 'cancel']: @@ -2399,6 +2425,8 @@ class SaleOrder(models.Model): if command[0] == 0: # A new line is being added raise UserError( "SO tidak dapat ditambahkan produk baru karena SO sudah menjadi sale order.") + + order._update_delivery_service_type_from_shipping_option(vals) res = super(SaleOrder, self).write(vals) # self._check_total_margin_excl_third_party() -- cgit v1.2.3 From 85f9481cce4fbec278d2cde48f009d480b8a35ed Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 26 May 2025 17:40:50 +0700 Subject: (andri) match selected service type pada web dan juga odoo --- indoteknik_custom/models/sale_order.py | 91 +++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 41 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 8cf38040..e5297011 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -2312,61 +2312,70 @@ class SaleOrder(models.Model): order_line.order_id.use_button = True def _auto_set_shipping_from_website(self): - # Jalankan hanya jika context menandakan ini proses checkout if not self.env.context.get('from_website_checkout'): - _logger.info("[AutoSetShipping] Dilewati karena bukan dari website checkout (context tidak ada)") return for order in self: - _logger.info(f"[AutoSetShipping] Proses otomatis untuk SO: {order.name}") - - # Validasi: pastikan source_id = Website + # Validasi source website if not order.source_id or order.source_id.id != 59: - _logger.warning(f"[AutoSetShipping] SO {order.name} bukan dari Website (source_id.id != 59)") continue - # Abaikan jika shipping method adalah Self Pick Up + # Skip jika Self Pick Up if order.carrier_id and order.carrier_id.id == 32: order.select_shipping_option = 'custom' - _logger.info(f"[AutoSetShipping] SO {order.name} menggunakan Self Pick Up. Set ke custom.") continue + # Simpan pilihan user sebelum estimasi + user_carrier_id = order.carrier_id.id if order.carrier_id else None + user_service = order.delivery_service_type + user_amount = order.delivery_amt + + # Jalankan estimasi untuk refresh data order.select_shipping_option = 'biteship' order.action_estimate_shipping() - if not (order.delivery_service_type and order.carrier_id): - _logger.warning(f"[AutoSetShipping] SO {order.name} tidak memiliki delivery_service_type atau carrier_id") - continue - - # Cari provider dari mapping rajaongkir_kurir - self.env.cr.execute(""" - SELECT name FROM rajaongkir_kurir - WHERE delivery_carrier_id = %s LIMIT 1 - """, (order.carrier_id.id,)) - result = self.env.cr.fetchone() - provider = result[0].lower() if result and result[0] else '' - - if not provider and order.carrier_id.name: - provider = order.carrier_id.name.lower().split()[0] - - if not provider: - _logger.warning(f"[AutoSetShipping] Provider tidak ditemukan untuk carrier_id: {order.carrier_id.id}") - continue - - # Cari shipping option berdasarkan courier_service_code + provider - matched_option = self.env['shipping.option'].search([ - ('sale_order_id', '=', order.id), - ('courier_service_code', '=', order.delivery_service_type), - ('provider', 'ilike', provider), - ], limit=1) - - if matched_option: - order.shipping_option_id = matched_option.id - order.delivery_amt = matched_option.price - _logger.info(f"[AutoSetShipping] Match: {matched_option.name} | Provider: {provider} | Price: {matched_option.price}") - else: - _logger.warning(f"[AutoSetShipping] Tidak ditemukan match untuk SO {order.name} dengan service_code '{order.delivery_service_type}' dan provider '{provider}'") - + # Restore pilihan user setelah estimasi + if user_carrier_id and user_service: + # Dapatkan provider + self.env.cr.execute("SELECT name FROM rajaongkir_kurir WHERE delivery_carrier_id = %s LIMIT 1", (user_carrier_id,)) + result = self.env.cr.fetchone() + provider = result[0].lower() if result else order.env['delivery.carrier'].browse(user_carrier_id).name.lower().split()[0] + + # Cari opsi yang cocok (prioritas: service code > nama > harga > fallback) + domain_options = [ + [('courier_service_code', '=', user_service), ('provider', 'ilike', provider)], # exact service + [('name', 'ilike', user_service), ('provider', 'ilike', provider)], # nama service + [('price', '=', user_amount), ('provider', 'ilike', provider)] if user_amount > 0 else None, # harga sama + [('provider', 'ilike', provider)] # fallback + ] + + matched_option = None + for domain in domain_options: + if domain: + matched_option = self.env['shipping.option'].search([('sale_order_id', '=', order.id)] + domain, limit=1) + if matched_option: + break + + # Set opsi yang cocok atau buat manual + if matched_option: + order.shipping_option_id = matched_option.id + order.delivery_amt = matched_option.price + order.delivery_service_type = matched_option.courier_service_code + + # Notif jika harga berubah + if user_amount > 0 and abs(matched_option.price - user_amount) > 1000: + order.message_post(body=f"Harga shipping berubah dari Rp {user_amount:,} ke Rp {matched_option.price:,}") + + elif user_amount > 0: + # Buat opsi manual jika tidak ada yang cocok + manual_option = self.env['shipping.option'].create({ + 'name': f"{provider.upper()} - {user_service}", + 'price': user_amount, + 'provider': provider, + 'courier_service_code': user_service, + 'sale_order_id': order.id, + }) + order.shipping_option_id = manual_option.id @api.model def create(self, vals): -- cgit v1.2.3 From 76c8db3d444197af6e29a9b4071054383db9d4f7 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 26 May 2025 21:17:48 +0700 Subject: vendor_id return false/null instead of empty string --- indoteknik_custom/models/sale_order_line.py | 87 +++++++++++++++++------------ 1 file changed, 51 insertions(+), 36 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index 9247d1c1..c8066961 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -6,19 +6,22 @@ from datetime import datetime, timedelta class SaleOrderLine(models.Model): _inherit = 'sale.order.line' item_margin = fields.Float('Margin', compute='compute_item_margin', help="Total Margin in Sales Order Header") - item_before_margin = fields.Float('Before Margin', compute='compute_item_before_margin', help="Total Margin in Sales Order Header") - item_percent_margin = fields.Float('%Margin', compute='compute_item_margin', help="Total % Margin in Sales Order Header") + item_before_margin = fields.Float('Before Margin', compute='compute_item_before_margin', + help="Total Margin in Sales Order Header") + item_percent_margin = fields.Float('%Margin', compute='compute_item_margin', + help="Total % Margin in Sales Order Header") initial_discount = fields.Float('Initial Discount') vendor_id = fields.Many2one( 'res.partner', string='Vendor', readonly=True, change_default=True, index=True, tracking=1, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]" - ) - vendor_md_id = fields.Many2one('res.partner', string='MD Vendor') + ) + vendor_md_id = fields.Many2one('res.partner', string='MD Vendor') purchase_price = fields.Float('Purchase', required=True, digits='Product Price', default=0.0) purchase_price_md = fields.Float('MD Purchase') - purchase_tax_id = fields.Many2one('account.tax', string='Tax', domain=['|', ('active', '=', False), ('active', '=', True)]) + purchase_tax_id = fields.Many2one('account.tax', string='Tax', + domain=['|', ('active', '=', False), ('active', '=', True)]) delivery_amt_line = fields.Float('DeliveryAmtLine', compute='compute_delivery_amt_line') fee_third_party_line = fields.Float('FeeThirdPartyLine', compute='compute_fee_third_party_line', default=0) line_no = fields.Integer('No', default=0, copy=False) @@ -28,13 +31,15 @@ class SaleOrderLine(models.Model): ('info_vendor', 'Info Vendor'), ('penggabungan', 'Penggabungan'), ], string='Note', help="Harap diisi jika ada keterangan tambahan dari Procurement, agar dapat dimonitoring") - note_procurement = fields.Char(string='Note Detail', help="Harap diisi jika ada keterangan tambahan dari Procurement, agar dapat dimonitoring") + note_procurement = fields.Char(string='Note Detail', + help="Harap diisi jika ada keterangan tambahan dari Procurement, agar dapat dimonitoring") vendor_subtotal = fields.Float(string='Vendor Subtotal', compute="_compute_vendor_subtotal") amount_voucher_disc = fields.Float(string='Voucher Discount') qty_reserved = fields.Float(string='Qty Reserved', compute='_compute_qty_reserved') - product_available_quantity = fields.Float(string='Qty pickup by user',) + product_available_quantity = fields.Float(string='Qty pickup by user', ) reserved_from = fields.Char(string='Reserved From', copy=False) - item_percent_margin_without_deduction = fields.Float('Margin Without Deduction', compute='_compute_item_margin_without_deduction') + item_percent_margin_without_deduction = fields.Float('Margin Without Deduction', + compute='_compute_item_margin_without_deduction') weight = fields.Float(string='Weight') md_vendor_id = fields.Many2one('res.partner', string='MD Vendor', readonly=True) margin_md = fields.Float(string='Margin MD') @@ -45,7 +50,8 @@ class SaleOrderLine(models.Model): outgoing_moves = self.env['stock.move'] incoming_moves = self.env['stock.move'] - for move in self.move_ids.filtered(lambda r: r.state != 'cancel' and not r.scrapped and self.product_id == r.product_id): + for move in self.move_ids.filtered( + lambda r: r.state != 'cancel' and not r.scrapped and self.product_id == r.product_id): if move.location_dest_id.usage == "customer": if not move.origin_returned_move_id or (move.origin_returned_move_id and move.to_refund): outgoing_moves |= move @@ -80,7 +86,7 @@ class SaleOrderLine(models.Model): if not self.product_uom or not self.product_id: self.price_unit = 0.0 return - + self.price_unit = self.price_unit def _compute_qty_reserved(self): @@ -158,7 +164,7 @@ class SaleOrderLine(models.Model): line.item_percent_margin = 0 if not line.margin_md: - line.margin_md = line.item_percent_margin + line.margin_md = line.item_percent_margin def compute_item_before_margin(self): for line in self: @@ -169,7 +175,7 @@ class SaleOrderLine(models.Model): continue # calculate margin without tax sales_price = line.price_reduce_taxexcl * line.product_uom_qty - + purchase_price = line.purchase_price if line.purchase_tax_id.price_include: purchase_price = line.purchase_price / 1.11 @@ -183,7 +189,7 @@ class SaleOrderLine(models.Model): # TODO : need to change this logic @stephan if not self.product_id or self.product_id.type == 'service': return - elif self.product_id.categ_id.id == 34: # finish good / manufacturing only + elif self.product_id.categ_id.id == 34: # finish good / manufacturing only cost = self.product_id.standard_price self.purchase_price = cost elif self.product_id.x_manufacture.override_vendor_id: @@ -195,12 +201,12 @@ class SaleOrderLine(models.Model): self.purchase_price = price self.purchase_tax_id = taxes # else: - # purchase_price = self.env['purchase.pricelist'].search( - # [('vendor_id', '=', self.vendor_id.id), ('product_id', '=', self.product_id.id)], - # limit=1, order='count_trx_po desc, count_trx_po_vendor desc') - # price, taxes = self._get_valid_purchase_price(purchase_price) - # self.purchase_price = price - # self.purchase_tax_id = taxes + # purchase_price = self.env['purchase.pricelist'].search( + # [('vendor_id', '=', self.vendor_id.id), ('product_id', '=', self.product_id.id)], + # limit=1, order='count_trx_po desc, count_trx_po_vendor desc') + # price, taxes = self._get_valid_purchase_price(purchase_price) + # self.purchase_price = price + # self.purchase_tax_id = taxes # def _calculate_selling_price(self): # rec_purchase_price, rec_taxes, rec_vendor_id = self._get_purchase_price(self.product_id) @@ -260,7 +266,7 @@ class SaleOrderLine(models.Model): price = 0 taxes = 24 - vendor_id = '' + vendor_id = False human_last_update = purchase_price.human_last_update or datetime.min system_last_update = purchase_price.system_last_update or datetime.min @@ -271,18 +277,18 @@ class SaleOrderLine(models.Model): if delta_time > human_last_update: price = 0 taxes = 24 - vendor_id = '' + vendor_id = False if system_last_update > human_last_update: - #if purchase_price.taxes_system_id.type_tax_use == 'purchase': + # if purchase_price.taxes_system_id.type_tax_use == 'purchase': price = purchase_price.system_price taxes = purchase_price.taxes_system_id.id or 24 vendor_id = purchase_price.vendor_id.id if delta_time > system_last_update: price = 0 taxes = 24 - vendor_id = '' - + vendor_id = False + return price, taxes, vendor_id @api.onchange('product_id') @@ -302,11 +308,11 @@ class SaleOrderLine(models.Model): line.tax_id = line.order_id.sales_tax_id # price, taxes = line._get_valid_purchase_price(purchase_price) line.purchase_price = price - line.purchase_tax_id = taxes + line.purchase_tax_id = taxes attribute_values = line.product_id.product_template_attribute_value_ids.mapped('name') attribute_values_str = ', '.join(attribute_values) if attribute_values else '' - + line_name = ('[' + line.product_id.default_code + ']' if line.product_id.default_code else '') + ' ' + \ (line.product_id.name if line.product_id.name else '') + ' ' + \ ('(' + attribute_values_str + ')' if attribute_values_str else '') + ' ' + \ @@ -324,7 +330,7 @@ class SaleOrderLine(models.Model): price, taxes, vendor_id = self._get_purchase_price(line.product_id) line.vendor_md_id = vendor_id if vendor_id else None line.margin_md = line.item_percent_margin - line.purchase_price_md = price + line.purchase_price_md = price def compute_delivery_amt_line(self): for line in self: @@ -363,11 +369,15 @@ class SaleOrderLine(models.Model): fiscal_position=self.env.context.get('fiscal_position') ) - product_context = dict(self.env.context, partner_id=self.order_id.partner_id.id, date=self.order_id.date_order, uom=self.product_uom.id) + product_context = dict(self.env.context, partner_id=self.order_id.partner_id.id, date=self.order_id.date_order, + uom=self.product_uom.id) price, rule_id = self.order_id.pricelist_id.with_context(product_context).get_product_price_rule( self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) - new_list_price, currency = self.with_context(product_context)._get_real_price_currency(product, rule_id, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id) + new_list_price, currency = self.with_context(product_context)._get_real_price_currency(product, rule_id, + self.product_uom_qty, + self.product_uom, + self.order_id.pricelist_id.id) new_list_price = product.web_price if new_list_price != 0: @@ -390,8 +400,8 @@ class SaleOrderLine(models.Model): no_variant_attributes_price_extra = [ ptav.price_extra for ptav in self.product_no_variant_attribute_value_ids.filtered( lambda ptav: - ptav.price_extra and - ptav not in product.product_template_attribute_value_ids + ptav.price_extra and + ptav not in product.product_template_attribute_value_ids ) ] if no_variant_attributes_price_extra: @@ -401,10 +411,15 @@ class SaleOrderLine(models.Model): if self.order_id.pricelist_id.discount_policy == 'with_discount': return product.with_context(pricelist=self.order_id.pricelist_id.id, uom=self.product_uom.id).price - product_context = dict(self.env.context, partner_id=self.order_id.partner_id.id, date=self.order_id.date_order, uom=self.product_uom.id) - - final_price, rule_id = self.order_id.pricelist_id.with_context(product_context).get_product_price_rule(product or self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) - base_price, currency = self.with_context(product_context)._get_real_price_currency(product, rule_id, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id) + product_context = dict(self.env.context, partner_id=self.order_id.partner_id.id, date=self.order_id.date_order, + uom=self.product_uom.id) + + final_price, rule_id = self.order_id.pricelist_id.with_context(product_context).get_product_price_rule( + product or self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) + base_price, currency = self.with_context(product_context)._get_real_price_currency(product, rule_id, + self.product_uom_qty, + self.product_uom, + self.order_id.pricelist_id.id) base_price = product.web_price if currency != self.order_id.pricelist_id.currency_id: base_price = currency._convert( @@ -413,7 +428,7 @@ class SaleOrderLine(models.Model): # negative discounts (= surcharge) are included in the display price return max(base_price, final_price) - + def validate_line(self): for line in self: if line.product_id.id in [385544, 224484, 417724]: -- cgit v1.2.3 From 121f5f732fc837e1f8b801eaaed038962aa574fd Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 27 May 2025 09:54:41 +0700 Subject: notif diffrent vendor so and po --- indoteknik_custom/models/purchase_order.py | 6 +++++- 1 file changed, 5 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 cbfd4acd..36c4c7b9 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -817,7 +817,11 @@ class PurchaseOrder(models.Model): if not line.so_line_id: continue if line.so_line_id.vendor_id.id != vendor_po and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'): - raise UserError("Produk "+line.product_id.name+" memiliki vendor berbeda dengan SO (Vendor PO: "+str(self.partner_id.name)+", Vendor SO: "+str(line.so_line_id.vendor_id.name)+")") + self.env.user.notify_danger( + title='WARNING!!!', + message="Produk "+line.product_id.name+" memiliki vendor berbeda dengan SO (Vendor PO: "+str(self.partner_id.name)+", Vendor SO: "+str(line.so_line_id.vendor_id.name)+")", + sticky=True + ) def button_confirm(self): # self._check_payment_term() # check payment term -- cgit v1.2.3 From b7cb8418e18de4507bc81d04e8c150f78343b92e Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Tue, 27 May 2025 10:09:54 +0700 Subject: validation qty demand --- indoteknik_custom/models/stock_picking.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 0fcb7ca1..cbfcda22 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1269,6 +1269,20 @@ class StockPicking(models.Model): current_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') self.date_reserved = current_time + # Validate Qty Demand Can't higher than Qty Product + for move_line in self.move_line_ids_without_package: + purchase_line = move_line.move_id.purchase_line_id + if purchase_line: + if purchase_line.product_uom_qty < move_line.product_uom_qty: + raise UserError( + _("Quantity demand (%s) tidak bisa lebih besar dari qty product (%s) untuk produk %s") % ( + move_line.product_uom_qty, + purchase_line.product_uom_qty, + move_line.product_id.display_name + ) + ) + + self.validation_minus_onhand_quantity() self.responsible = self.env.user.id # self.send_koli_to_so() -- cgit v1.2.3 From 44dc882a2af128a523ad8c594056261ad294be34 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 27 May 2025 10:11:58 +0700 Subject: cr akses approval on rpo --- indoteknik_custom/models/requisition.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/requisition.py b/indoteknik_custom/models/requisition.py index 74236850..775acfb9 100644 --- a/indoteknik_custom/models/requisition.py +++ b/indoteknik_custom/models/requisition.py @@ -82,20 +82,20 @@ class Requisition(models.Model): state = ['done', 'sale'] if self.sale_order_id.state in state: raise UserError('SO sudah Confirm, akan berakibat double Purchase melalui PJ') - if self.env.user.id not in [377, 19, 28]: - raise UserError('Hanya Vita dan Darren Yang Bisa Approve') - if self.env.user.id == 377 or self.env.user.id == 28: + if self.env.user.id not in [21, 19, 28]: + raise UserError('Hanya Rafly dan Darren Yang Bisa Approve') + if self.env.user.id == 19 or self.env.user.id == 28: self.sales_approve = True - elif self.env.user.id == 19 or self.env.user.id == 28: + elif self.env.user.id == 21 or self.env.user.id == 28: if not self.sales_approve: raise UserError('Vita Belum Approve') self.merchandise_approve = True def create_po_from_requisition(self): if not self.sales_approve: - raise UserError('Harus Di Approve oleh Vita') - if not self.merchandise_approve: raise UserError('Harus Di Approve oleh Darren') + if not self.merchandise_approve: + raise UserError('Harus Di Approve oleh Rafly') if not self.requisition_lines: raise UserError('Tidak ada Lines, belum bisa create PO') if self.is_po: -- cgit v1.2.3 From b5bc5c5bc63ecbfa7efe9ff06a373bbedfcf7d42 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 27 May 2025 10:12:26 +0700 Subject: push --- indoteknik_custom/models/requisition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/requisition.py b/indoteknik_custom/models/requisition.py index 775acfb9..25133e72 100644 --- a/indoteknik_custom/models/requisition.py +++ b/indoteknik_custom/models/requisition.py @@ -88,7 +88,7 @@ class Requisition(models.Model): self.sales_approve = True elif self.env.user.id == 21 or self.env.user.id == 28: if not self.sales_approve: - raise UserError('Vita Belum Approve') + raise UserError('Darren Belum Approve') self.merchandise_approve = True def create_po_from_requisition(self): -- cgit v1.2.3 From a01f05f50dd852e76b44709259e8baaf33e4b462 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 27 May 2025 10:35:08 +0700 Subject: push --- indoteknik_custom/models/shipment_group.py | 4 ++-- indoteknik_custom/models/stock_picking.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/shipment_group.py b/indoteknik_custom/models/shipment_group.py index 87d222a6..2d84d54c 100644 --- a/indoteknik_custom/models/shipment_group.py +++ b/indoteknik_custom/models/shipment_group.py @@ -19,10 +19,10 @@ class ShipmentGroup(models.Model): def sync_api_shipping(self): for rec in self.shipment_line: - if rec.shipment_id.carrier_id == 173: + if rec.shipment_id.carrier_id.id == 173: rec.picking_id.action_get_kgx_pod() - if rec.shipment_id.carrier_id == 151: + if rec.shipment_id.carrier_id.id == 151: rec.picking_id.track_envio_shipment() @api.depends('shipment_line.total_colly') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 0fcb7ca1..05f946fd 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -288,7 +288,7 @@ class StockPicking(models.Model): self.ensure_one() if not self.name or not self.origin: return False - return f"{self.name} {self.origin}" + return f"{self.name}, {self.origin}" def _download_pod_photo(self, url): """Mengunduh foto POD dari URL""" -- cgit v1.2.3 From c4aebb9fc2cb875554b26e21231de0ad0bfd48ce Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Tue, 27 May 2025 10:38:38 +0700 Subject: add qty outgoing manufacturing order formula in free qty --- indoteknik_custom/models/product_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/product_template.py b/indoteknik_custom/models/product_template.py index 3bb54f44..2c07824a 100755 --- a/indoteknik_custom/models/product_template.py +++ b/indoteknik_custom/models/product_template.py @@ -1146,7 +1146,7 @@ class ProductProduct(models.Model): def _get_qty_free_bandengan(self): for product in self: - qty_free = product.qty_onhand_bandengan - product.qty_outgoing_bandengan + qty_free = product.qty_onhand_bandengan - product.qty_outgoing_bandengan - product.qty_outgoing_mo_bandengan product.qty_free_bandengan = qty_free def _get_max_qty_reordering_rule(self): -- cgit v1.2.3 From af127b94e7b597a3451f42199df56e27899272fd Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 27 May 2025 10:45:11 +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 05f946fd..887b2f92 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -625,7 +625,7 @@ class StockPicking(models.Model): ('state', '=', 'done'), ('carrier_id', '=', 151) ]) - for picking in pickings: + for picking in self: if not picking.name: raise UserError("Name pada stock.picking tidak ditemukan.") -- cgit v1.2.3 From 755f6efe380cbbdd05ba592f651ca87030a22143 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 27 May 2025 10:57:24 +0700 Subject: fix api shipment group --- indoteknik_custom/models/purchase_order.py | 4 ++-- indoteknik_custom/models/shipment_group.py | 10 ++++++---- indoteknik_custom/models/stock_picking.py | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 36c4c7b9..2513f8fd 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -849,8 +849,8 @@ class PurchaseOrder(models.Model): ) if not self.from_apo: - if not self.matches_so and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser') and not self.env.user.is_leader: - raise UserError("Tidak ada link dengan SO, harus approval Merchandise") + if not self.matches_so and not self.env.user.id == 21: + raise UserError("Tidak ada link dengan SO, harus approval Rafly") send_email = False if not self.not_update_purchasepricelist: diff --git a/indoteknik_custom/models/shipment_group.py b/indoteknik_custom/models/shipment_group.py index 2d84d54c..ea91238a 100644 --- a/indoteknik_custom/models/shipment_group.py +++ b/indoteknik_custom/models/shipment_group.py @@ -19,11 +19,13 @@ class ShipmentGroup(models.Model): def sync_api_shipping(self): for rec in self.shipment_line: + picking_names = [picking.name for picking in rec.picking_id] if rec.shipment_id.carrier_id.id == 173: - rec.picking_id.action_get_kgx_pod() - - if rec.shipment_id.carrier_id.id == 151: - rec.picking_id.track_envio_shipment() + rec.picking_id.action_get_kgx_pod( + shipment=f"{self.number},{','.join(picking_names)}" + ) + elif rec.shipment_id.carrier_id.id == 151: + rec.picking_id.track_envio_shipment() @api.depends('shipment_line.total_colly') def _compute_total_colly_line(self): diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 887b2f92..3223377c 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -310,10 +310,10 @@ class StockPicking(models.Model): except ValueError: return False - def action_get_kgx_pod(self): + def action_get_kgx_pod(self, shipment=False): self.ensure_one() - awb_number = self._get_kgx_awb_number() + awb_number = shipment or self._get_kgx_awb_number() if not awb_number: raise UserError("Nomor AWB tidak dapat dibuat, pastikan picking memiliki name dan origin") -- cgit v1.2.3 From 660d79ee317460e79a61287bda7d7a8e8b423b48 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 27 May 2025 11:00:46 +0700 Subject: push --- indoteknik_custom/models/purchase_order.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 2513f8fd..53037703 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -808,8 +808,8 @@ class PurchaseOrder(models.Model): # test = line.product_uom_qty # test2 = line.product_id.plafon_qty # test3 = test2 + line.product_uom_qty - if line.product_uom_qty > line.product_id.plafon_qty + line.product_uom_qty and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'): - raise UserError('Product '+line.product_id.name+' melebihi plafon, harus Approval MD') + if line.product_uom_qty > line.product_id.plafon_qty + line.product_uom_qty and not self.env.user.id == 21: + raise UserError('Product '+line.product_id.name+' melebihi plafon, harus Approval Rafly') def check_different_vendor_so_po(self): vendor_po = self.partner_id.id -- cgit v1.2.3 From 4ba7242388db36fa641ea2008af08319a3dc014c Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 27 May 2025 11:03:53 +0700 Subject: fix syntax get name picking --- indoteknik_custom/models/shipment_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/shipment_group.py b/indoteknik_custom/models/shipment_group.py index ea91238a..09a4fba9 100644 --- a/indoteknik_custom/models/shipment_group.py +++ b/indoteknik_custom/models/shipment_group.py @@ -19,7 +19,7 @@ class ShipmentGroup(models.Model): def sync_api_shipping(self): for rec in self.shipment_line: - picking_names = [picking.name for picking in rec.picking_id] + picking_names = [picking.picking_id.name for picking in rec] if rec.shipment_id.carrier_id.id == 173: rec.picking_id.action_get_kgx_pod( shipment=f"{self.number},{','.join(picking_names)}" -- cgit v1.2.3 From dad6bca8abdda9e5987a0adb8cae75540b6176eb Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 27 May 2025 11:10:09 +0700 Subject: push --- indoteknik_custom/models/shipment_group.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/shipment_group.py b/indoteknik_custom/models/shipment_group.py index 09a4fba9..9ebf36c0 100644 --- a/indoteknik_custom/models/shipment_group.py +++ b/indoteknik_custom/models/shipment_group.py @@ -19,10 +19,10 @@ class ShipmentGroup(models.Model): def sync_api_shipping(self): for rec in self.shipment_line: - picking_names = [picking.picking_id.name for picking in rec] + picking_names = [lines.picking_id.name for lines in self.shipment_line] if rec.shipment_id.carrier_id.id == 173: rec.picking_id.action_get_kgx_pod( - shipment=f"{self.number},{','.join(picking_names)}" + shipment=f"{self.number}, {', '.join(picking_names)}" ) elif rec.shipment_id.carrier_id.id == 151: rec.picking_id.track_envio_shipment() -- cgit v1.2.3 From ae9766498d304a8336737453310e4272929cea73 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 27 May 2025 11:20:09 +0700 Subject: push --- indoteknik_custom/models/shipment_group.py | 2 +- indoteknik_custom/models/stock_picking.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/shipment_group.py b/indoteknik_custom/models/shipment_group.py index 9ebf36c0..fcde39c9 100644 --- a/indoteknik_custom/models/shipment_group.py +++ b/indoteknik_custom/models/shipment_group.py @@ -25,7 +25,7 @@ class ShipmentGroup(models.Model): shipment=f"{self.number}, {', '.join(picking_names)}" ) elif rec.shipment_id.carrier_id.id == 151: - rec.picking_id.track_envio_shipment() + rec.picking_id.track_envio_shipment(shipment=f"{self.number}") @api.depends('shipment_line.total_colly') def _compute_total_colly_line(self): diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 3223377c..ea52450e 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -619,18 +619,19 @@ class StockPicking(models.Model): except ValueError: raise UserError(f"Format waktu tidak sesuai: {date_str}") - def track_envio_shipment(self): + def track_envio_shipment(self, shipment=False): pickings = self.env['stock.picking'].search([ ('picking_type_code', '=', 'outgoing'), ('state', '=', 'done'), ('carrier_id', '=', 151) ]) for picking in self: + name = shipment or picking.name if not picking.name: raise UserError("Name pada stock.picking tidak ditemukan.") # API URL dan headers - url = f"https://api.envio.co.id/v1/tracking/distribution?code={picking.name}" + url = f"https://api.envio.co.id/v1/tracking/distribution?code={name}" headers = { 'Authorization': 'Bearer JZ0Seh6qpYJAC3CJHdhF7sPqv8B/uSSfZe1VX5BL?vPYdo', 'Content-Type': 'application/json', -- cgit v1.2.3 From 4ed94e9e1de027aba326ff3dce954b765f752009 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 28 May 2025 09:00:30 +0700 Subject: (andri) penambahan log note ketika biteship berhasil & penyesuaian field required pada SO --- indoteknik_custom/models/sale_order.py | 11 ++++------- indoteknik_custom/models/stock_picking.py | 8 ++++++++ 2 files changed, 12 insertions(+), 7 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index e5297011..f2bb27ad 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -652,8 +652,8 @@ class SaleOrder(models.Model): product_names = '
'.join(missing_weight_products) self.message_post(body=f"Produk berikut tidak memiliki berat:
{product_names}") - # if total_weight == 0: - # raise UserError("Tidak dapat mengestimasi ongkir tanpa berat yang valid.") + if total_weight == 0: + raise UserError("Tidak dapat mengestimasi ongkir tanpa karena berat produk = 0 kg.") # Validasi alamat pengiriman if not self.real_shipping_id: @@ -674,8 +674,8 @@ class SaleOrder(models.Model): total_weight = self._validate_for_shipping_estimate() weight_gram = int(total_weight * 1000) - if weight_gram < 100: - weight_gram = 100 + # if weight_gram < 100: + # weight_gram = 100 items = [{ "name": "Paket Pesanan", @@ -683,9 +683,6 @@ class SaleOrder(models.Model): "value": int(self.amount_untaxed), "weight": weight_gram, "quantity": 1, - "height": 10, - "width": 10, - "length": 10 }] shipping_address = self.real_shipping_id diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 5548db75..4522dac0 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -617,6 +617,14 @@ class StockPicking(models.Model): waybill_id = data.get("courier", {}).get("waybill_id", "") + self.message_post( + body=f"📦 Biteship berhasil dilakukan.
" + f"Kurir: {self.carrier_id.name}
" + f"Tracking ID: {self.biteship_tracking_id or '-'}
" + f"Resi: {waybill_id or '-'}", + message_type="comment" + ) + message = f"✅ Berhasil Order ke Biteship! Resi: {waybill_id}" if waybill_id else "⚠️ Order berhasil, tetapi tidak ada nomor resi." return { -- cgit v1.2.3 From c2c8fff66b2aab1fa034fc2b6a12ebb9b4f9fbc6 Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Wed, 28 May 2025 11:26:42 +0700 Subject: fixed approval by purchasing manager if not linked with sales order request by rafly hanggara --- indoteknik_custom/models/purchase_order.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 36c4c7b9..b08dcf62 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -834,23 +834,23 @@ class PurchaseOrder(models.Model): if self.amount_untaxed >= 50000000 and not self.env.user.id == 21: raise UserError("Hanya Rafly Hanggara yang bisa approve") - if self.total_percent_margin < self.total_so_percent_margin and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser') and not self.env.user.is_leader: + if self.total_percent_margin < self.total_so_percent_margin and not self.env.user.is_leader: self.env.user.notify_danger( title='WARNING!!!', message='Beda Margin dengan Sale Order', sticky=True ) - if len(self.order_sales_match_line) == 0 and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser') and not self.env.user.is_leader: - self.env.user.notify_danger( - title='WARNING!!!', - message='Tidak ada matches SO', - sticky=True - ) + # if len(self.order_sales_match_line) == 0 and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser') and not self.env.user.is_leader: + # self.env.user.notify_danger( + # title='WARNING!!!', + # message='Tidak ada matches SO', + # sticky=True + # ) if not self.from_apo: - if not self.matches_so and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser') and not self.env.user.is_leader: - raise UserError("Tidak ada link dengan SO, harus approval Merchandise") + if not self.matches_so and not self.env.user.is_purchasing_manager and not self.env.user.is_leader: + raise UserError("Tidak ada link dengan SO, harus approval Purchasing Manager") send_email = False if not self.not_update_purchasepricelist: -- cgit v1.2.3 From cf4b57fe825ae3522b7d893344eb9649c7bd10b3 Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Wed, 28 May 2025 13:13:44 +0700 Subject: add purchasing manager as approval manufacturing order while confirm --- indoteknik_custom/models/manufacturing.py | 3 +++ indoteknik_custom/models/purchase_order.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/manufacturing.py b/indoteknik_custom/models/manufacturing.py index 715d8513..aea01362 100644 --- a/indoteknik_custom/models/manufacturing.py +++ b/indoteknik_custom/models/manufacturing.py @@ -11,6 +11,9 @@ class Manufacturing(models.Model): def action_confirm(self): if self._name != 'mrp.production': return super(Manufacturing, self).action_confirm() + + if not self.env.user.is_purchasing_manager: + raise UserError("Hanya bisa di confirm oleh Purchasing Manager") # if self.location_src_id.id != 75: # raise UserError('Component Location hanya bisa di AS/Stock') diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index d6b449fd..21ca55eb 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -850,7 +850,7 @@ class PurchaseOrder(models.Model): if not self.from_apo: if not self.matches_so and not self.env.user.is_purchasing_manager and not self.env.user.is_leader: - raise UserError("Tidak ada link dengan SO, harus approval Purchasing Manager") + raise UserError("Tidak ada link dengan SO, harus di confirm oleh Purchasing Manager") send_email = False if not self.not_update_purchasepricelist: -- cgit v1.2.3 From 104046bcaedf97dd97e604c24ceacf3797974713 Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Wed, 28 May 2025 14:27:01 +0700 Subject: error recursive method, must change to compute field --- indoteknik_custom/models/res_partner.py | 352 ++++++++++++++++---------------- 1 file changed, 176 insertions(+), 176 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index 191a44c9..f1e362e6 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -235,182 +235,182 @@ class ResPartner(models.Model): 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): - # Fungsi rekursif untuk meng-update semua child, termasuk child dari child - def update_children_recursively(partner, vals_for_child): - # Lakukan update pada partner saat ini hanya dengan field yang diizinkan - partner.write(vals_for_child) - - # Untuk setiap child dari partner ini, update juga child-nya - for child in partner.child_ids: - update_children_recursively(child, vals_for_child) - - # Jika self tidak memiliki parent_id, artinya self adalah parent - if not self.parent_id: - # Ambil semua child dari parent ini - children = self.child_ids - - # Perbarui vals dengan nilai dari parent jika tidak ada dalam vals - vals['customer_type'] = vals.get('customer_type', self.customer_type) - vals['nama_wajib_pajak'] = vals.get('nama_wajib_pajak', self.nama_wajib_pajak) - vals['npwp'] = vals.get('npwp', self.npwp) - vals['sppkp'] = vals.get('sppkp', self.sppkp) - vals['alamat_lengkap_text'] = vals.get('alamat_lengkap_text', self.alamat_lengkap_text) - vals['industry_id'] = vals.get('industry_id', self.industry_id.id if self.industry_id else None) - vals['company_type_id'] = vals.get('company_type_id', self.company_type_id.id if self.company_type_id else None) - - # Referensi - vals['supplier_ids'] = vals.get('supplier_ids', self.supplier_ids) - - # informasi perusahaan - vals['name_tempo'] = vals.get('name_tempo', self.name_tempo) - vals['industry_id_tempo'] = vals.get('industry_id_tempo', self.industry_id_tempo) - vals['street_tempo'] = vals.get('street_tempo', self.street_tempo) - vals['state_id_tempo'] = vals.get('state_id_tempo', self.state_id_tempo) - vals['city_id_tempo'] = vals.get('city_id_tempo', self.city_id_tempo) - vals['zip_tempo'] = vals.get('zip_tempo', self.zip_tempo) - vals['bank_name_tempo'] = vals.get('bank_name_tempo', self.bank_name_tempo) - vals['account_name_tempo'] = vals.get('account_name_tempo', self.account_name_tempo) - vals['account_number_tempo'] = vals.get('account_number_tempo', self.account_number_tempo) - vals['website_tempo'] = vals.get('website_tempo', self.website_tempo) - vals['portal'] = vals.get('portal', self.portal) - vals['estimasi_tempo'] = vals.get('estimasi_tempo', self.estimasi_tempo) - vals['tempo_duration'] = vals.get('tempo_duration', self.tempo_duration) - vals['tempo_limit'] = vals.get('tempo_limit', self.tempo_limit) - vals['category_produk_ids'] = vals.get('category_produk_ids', self.category_produk_ids) - - # Kontak Perusahaan - vals['direktur_name'] = vals.get('direktur_name', self.direktur_name) - vals['direktur_mobile'] = vals.get('direktur_mobile', self.direktur_mobile) - vals['direktur_email'] = vals.get('direktur_email', self.direktur_email) - vals['purchasing_name'] = vals.get('purchasing_name', self.purchasing_name) - vals['purchasing_mobile'] = vals.get('purchasing_mobile', self.purchasing_mobile) - vals['purchasing_email'] = vals.get('purchasing_email', self.purchasing_email) - vals['finance_name'] = vals.get('finance_name', self.finance_name) - vals['finance_mobile'] = vals.get('finance_mobile', self.finance_mobile) - vals['finance_email'] = vals.get('finance_email', self.finance_email) - - # Pengiriman - vals['pic_name'] = vals.get('pic_name', self.pic_name) - vals['pic_mobile'] = vals.get('pic_mobile', self.pic_mobile) - vals['street_pengiriman'] = vals.get('street_pengiriman', self.street_pengiriman) - vals['state_id_pengiriman'] = vals.get('state_id_pengiriman', self.state_id_pengiriman) - vals['city_id_pengiriman'] = vals.get('city_id_pengiriman', self.city_id_pengiriman) - vals['district_id_pengiriman'] = vals.get('district_id_pengiriman', self.district_id_pengiriman) - vals['subDistrict_id_pengiriman'] = vals.get('subDistrict_id_pengiriman', self.subDistrict_id_pengiriman) - vals['zip_pengiriman'] = vals.get('zip_pengiriman', self.zip_pengiriman) - vals['invoice_pic'] = vals.get('invoice_pic', self.invoice_pic) - vals['invoice_pic_mobile'] = vals.get('invoice_pic_mobile', self.invoice_pic_mobile) - vals['street_invoice'] = vals.get('street_invoice', self.street_invoice) - vals['state_id_invoice'] = vals.get('state_id_invoice', self.state_id_invoice) - vals['city_id_invoice'] = vals.get('city_id_invoice', self.city_id_invoice) - vals['district_id_invoice'] = vals.get('district_id_invoice', self.district_id_invoice) - vals['subDistrict_id_invoice'] = vals.get('subDistrict_id_invoice', self.subDistrict_id_invoice) - vals['zip_invoice'] = vals.get('zip_invoice', self.zip_invoice) - vals['tukar_invoice'] = vals.get('tukar_invoice', self.tukar_invoice) - vals['jadwal_bayar'] = vals.get('jadwal_bayar', self.jadwal_bayar) - vals['dokumen_prosedur'] = vals.get('dokumen_prosedur', self.dokumen_prosedur) - vals['dokumen_pengiriman'] = vals.get('dokumen_pengiriman', self.dokumen_pengiriman) - vals['dokumen_pengiriman_input'] = vals.get('dokumen_pengiriman_input', self.dokumen_pengiriman_input) - vals['dokumen_invoice'] = vals.get('dokumen_invoice', self.dokumen_invoice) - - # Dokumen - vals['dokumen_npwp'] = vals.get('dokumen_npwp', self.dokumen_npwp) - vals['dokumen_sppkp'] = vals.get('dokumen_sppkp', self.dokumen_sppkp) - vals['dokumen_nib'] = vals.get('dokumen_nib', self.dokumen_nib) - vals['dokumen_siup'] = vals.get('dokumen_siup', self.dokumen_siup) - vals['dokumen_tdp'] = vals.get('dokumen_tdp', self.dokumen_tdp) - vals['dokumen_skdp'] = vals.get('dokumen_skdp', self.dokumen_skdp) - vals['dokumen_skt'] = vals.get('dokumen_skt', self.dokumen_skt) - vals['dokumen_akta_perubahan'] = vals.get('dokumen_akta_perubahan', self.dokumen_akta_perubahan) - vals['dokumen_ktp_dirut'] = vals.get('dokumen_ktp_dirut', self.dokumen_ktp_dirut) - vals['dokumen_akta_pendirian'] = vals.get('dokumen_akta_pendirian', self.dokumen_akta_pendirian) - vals['dokumen_laporan_keuangan'] = vals.get('dokumen_laporan_keuangan', self.dokumen_laporan_keuangan) - vals['dokumen_foto_kantor'] = vals.get('dokumen_foto_kantor', self.dokumen_foto_kantor) - vals['dokumen_tempat_bekerja'] = vals.get('dokumen_tempat_bekerja', self.dokumen_tempat_bekerja) - - # Simpan hanya field yang perlu di-update pada child - vals_for_child = { - 'customer_type': vals.get('customer_type'), - 'nama_wajib_pajak': vals.get('nama_wajib_pajak'), - 'npwp': vals.get('npwp'), - 'sppkp': vals.get('sppkp'), - 'alamat_lengkap_text': vals.get('alamat_lengkap_text'), - 'industry_id': vals.get('industry_id'), - 'company_type_id': vals.get('company_type_id'), - 'supplier_ids': vals.get('supplier_ids'), - 'name_tempo': vals.get('name_tempo'), - 'industry_id_tempo': vals.get('industry_id_tempo'), - 'street_tempo': vals.get('street_tempo'), - 'state_id_tempo': vals.get('state_id_tempo'), - 'city_id_tempo': vals.get('city_id_tempo'), - 'zip_tempo': vals.get('zip_tempo'), - 'bank_name_tempo': vals.get('bank_name_tempo'), - 'account_name_tempo': vals.get('account_name_tempo'), - 'account_number_tempo': vals.get('account_number_tempo'), - 'website_tempo': vals.get('website_tempo'), - 'portal': vals.get('portal'), - 'estimasi_tempo': vals.get('estimasi_tempo'), - 'tempo_duration': vals.get('tempo_duration'), - 'tempo_limit': vals.get('tempo_limit'), - 'category_produk_ids': vals.get('category_produk_ids'), - 'direktur_name': vals.get('direktur_name'), - 'direktur_mobile': vals.get('direktur_mobile'), - 'direktur_email': vals.get('direktur_email'), - 'purchasing_name': vals.get('purchasing_name'), - 'purchasing_mobile': vals.get('purchasing_mobile'), - 'purchasing_email': vals.get('purchasing_email'), - 'finance_name': vals.get('finance_name'), - 'finance_mobile': vals.get('finance_mobile'), - 'finance_email': vals.get('finance_email'), - 'pic_name': vals.get('pic_name'), - 'pic_mobile': vals.get('pic_mobile'), - 'street_pengiriman': vals.get('street_pengiriman'), - 'state_id_pengiriman': vals.get('state_id_pengiriman'), - 'city_id_pengiriman': vals.get('city_id_pengiriman'), - 'district_id_pengiriman': vals.get('district_id_pengiriman'), - 'subDistrict_id_pengiriman': vals.get('subDistrict_id_pengiriman'), - 'zip_pengiriman': vals.get('zip_pengiriman'), - 'invoice_pic': vals.get('invoice_pic'), - 'invoice_pic_mobile': vals.get('invoice_pic_mobile'), - 'street_invoice': vals.get('street_invoice'), - 'state_id_invoice': vals.get('state_id_invoice'), - 'city_id_invoice': vals.get('city_id_invoice'), - 'district_id_invoice': vals.get('district_id_invoice'), - 'subDistrict_id_invoice': vals.get('subDistrict_id_invoice'), - 'zip_invoice': vals.get('zip_invoice'), - 'tukar_invoice': vals.get('tukar_invoice'), - 'jadwal_bayar': vals.get('jadwal_bayar'), - 'dokumen_prosedur': vals.get('dokumen_prosedur'), - 'dokumen_pengiriman': vals.get('dokumen_pengiriman'), - 'dokumen_pengiriman_input': vals.get('dokumen_pengiriman_input'), - 'dokumen_invoice': vals.get('dokumen_invoice'), - 'dokumen_npwp': vals.get('dokumen_npwp'), - 'dokumen_sppkp': vals.get('dokumen_sppkp'), - 'dokumen_nib': vals.get('dokumen_nib'), - 'dokumen_siup': vals.get('dokumen_siup'), - 'dokumen_tdp': vals.get('dokumen_tdp'), - 'dokumen_skdp': vals.get('dokumen_skdp'), - 'dokumen_skt': vals.get('dokumen_skt'), - 'dokumen_akta_perubahan': vals.get('dokumen_akta_perubahan'), - 'dokumen_ktp_dirut': vals.get('dokumen_ktp_dirut'), - 'dokumen_akta_pendirian': vals.get('dokumen_akta_pendirian'), - 'dokumen_laporan_keuangan': vals.get('dokumen_laporan_keuangan'), - 'dokumen_foto_kantor': vals.get('dokumen_foto_kantor'), - 'dokumen_tempat_bekerja': vals.get('dokumen_tempat_bekerja'), - - # internal_notes - 'comment': vals.get('comment') - } - - # Lakukan update pada semua child secara rekursif - for child in children: - update_children_recursively(child, vals_for_child) - - # Lakukan write untuk parent dengan vals asli - res = super(ResPartner, self).write(vals) - - return res + # def write(self, vals): + # # Fungsi rekursif untuk meng-update semua child, termasuk child dari child + # def update_children_recursively(partner, vals_for_child): + # # Lakukan update pada partner saat ini hanya dengan field yang diizinkan + # partner.write(vals_for_child) + # + # # Untuk setiap child dari partner ini, update juga child-nya + # for child in partner.child_ids: + # update_children_recursively(child, vals_for_child) + # + # # Jika self tidak memiliki parent_id, artinya self adalah parent + # if not self.parent_id: + # # Ambil semua child dari parent ini + # children = self.child_ids + # + # # Perbarui vals dengan nilai dari parent jika tidak ada dalam vals + # vals['customer_type'] = vals.get('customer_type', self.customer_type) + # vals['nama_wajib_pajak'] = vals.get('nama_wajib_pajak', self.nama_wajib_pajak) + # vals['npwp'] = vals.get('npwp', self.npwp) + # vals['sppkp'] = vals.get('sppkp', self.sppkp) + # vals['alamat_lengkap_text'] = vals.get('alamat_lengkap_text', self.alamat_lengkap_text) + # vals['industry_id'] = vals.get('industry_id', self.industry_id.id if self.industry_id else None) + # vals['company_type_id'] = vals.get('company_type_id', self.company_type_id.id if self.company_type_id else None) + # + # # Referensi + # vals['supplier_ids'] = vals.get('supplier_ids', self.supplier_ids) + # + # # informasi perusahaan + # vals['name_tempo'] = vals.get('name_tempo', self.name_tempo) + # vals['industry_id_tempo'] = vals.get('industry_id_tempo', self.industry_id_tempo) + # vals['street_tempo'] = vals.get('street_tempo', self.street_tempo) + # vals['state_id_tempo'] = vals.get('state_id_tempo', self.state_id_tempo) + # vals['city_id_tempo'] = vals.get('city_id_tempo', self.city_id_tempo) + # vals['zip_tempo'] = vals.get('zip_tempo', self.zip_tempo) + # vals['bank_name_tempo'] = vals.get('bank_name_tempo', self.bank_name_tempo) + # vals['account_name_tempo'] = vals.get('account_name_tempo', self.account_name_tempo) + # vals['account_number_tempo'] = vals.get('account_number_tempo', self.account_number_tempo) + # vals['website_tempo'] = vals.get('website_tempo', self.website_tempo) + # vals['portal'] = vals.get('portal', self.portal) + # vals['estimasi_tempo'] = vals.get('estimasi_tempo', self.estimasi_tempo) + # vals['tempo_duration'] = vals.get('tempo_duration', self.tempo_duration) + # vals['tempo_limit'] = vals.get('tempo_limit', self.tempo_limit) + # vals['category_produk_ids'] = vals.get('category_produk_ids', self.category_produk_ids) + # + # # Kontak Perusahaan + # vals['direktur_name'] = vals.get('direktur_name', self.direktur_name) + # vals['direktur_mobile'] = vals.get('direktur_mobile', self.direktur_mobile) + # vals['direktur_email'] = vals.get('direktur_email', self.direktur_email) + # vals['purchasing_name'] = vals.get('purchasing_name', self.purchasing_name) + # vals['purchasing_mobile'] = vals.get('purchasing_mobile', self.purchasing_mobile) + # vals['purchasing_email'] = vals.get('purchasing_email', self.purchasing_email) + # vals['finance_name'] = vals.get('finance_name', self.finance_name) + # vals['finance_mobile'] = vals.get('finance_mobile', self.finance_mobile) + # vals['finance_email'] = vals.get('finance_email', self.finance_email) + # + # # Pengiriman + # vals['pic_name'] = vals.get('pic_name', self.pic_name) + # vals['pic_mobile'] = vals.get('pic_mobile', self.pic_mobile) + # vals['street_pengiriman'] = vals.get('street_pengiriman', self.street_pengiriman) + # vals['state_id_pengiriman'] = vals.get('state_id_pengiriman', self.state_id_pengiriman) + # vals['city_id_pengiriman'] = vals.get('city_id_pengiriman', self.city_id_pengiriman) + # vals['district_id_pengiriman'] = vals.get('district_id_pengiriman', self.district_id_pengiriman) + # vals['subDistrict_id_pengiriman'] = vals.get('subDistrict_id_pengiriman', self.subDistrict_id_pengiriman) + # vals['zip_pengiriman'] = vals.get('zip_pengiriman', self.zip_pengiriman) + # vals['invoice_pic'] = vals.get('invoice_pic', self.invoice_pic) + # vals['invoice_pic_mobile'] = vals.get('invoice_pic_mobile', self.invoice_pic_mobile) + # vals['street_invoice'] = vals.get('street_invoice', self.street_invoice) + # vals['state_id_invoice'] = vals.get('state_id_invoice', self.state_id_invoice) + # vals['city_id_invoice'] = vals.get('city_id_invoice', self.city_id_invoice) + # vals['district_id_invoice'] = vals.get('district_id_invoice', self.district_id_invoice) + # vals['subDistrict_id_invoice'] = vals.get('subDistrict_id_invoice', self.subDistrict_id_invoice) + # vals['zip_invoice'] = vals.get('zip_invoice', self.zip_invoice) + # vals['tukar_invoice'] = vals.get('tukar_invoice', self.tukar_invoice) + # vals['jadwal_bayar'] = vals.get('jadwal_bayar', self.jadwal_bayar) + # vals['dokumen_prosedur'] = vals.get('dokumen_prosedur', self.dokumen_prosedur) + # vals['dokumen_pengiriman'] = vals.get('dokumen_pengiriman', self.dokumen_pengiriman) + # vals['dokumen_pengiriman_input'] = vals.get('dokumen_pengiriman_input', self.dokumen_pengiriman_input) + # vals['dokumen_invoice'] = vals.get('dokumen_invoice', self.dokumen_invoice) + # + # # Dokumen + # vals['dokumen_npwp'] = vals.get('dokumen_npwp', self.dokumen_npwp) + # vals['dokumen_sppkp'] = vals.get('dokumen_sppkp', self.dokumen_sppkp) + # vals['dokumen_nib'] = vals.get('dokumen_nib', self.dokumen_nib) + # vals['dokumen_siup'] = vals.get('dokumen_siup', self.dokumen_siup) + # vals['dokumen_tdp'] = vals.get('dokumen_tdp', self.dokumen_tdp) + # vals['dokumen_skdp'] = vals.get('dokumen_skdp', self.dokumen_skdp) + # vals['dokumen_skt'] = vals.get('dokumen_skt', self.dokumen_skt) + # vals['dokumen_akta_perubahan'] = vals.get('dokumen_akta_perubahan', self.dokumen_akta_perubahan) + # vals['dokumen_ktp_dirut'] = vals.get('dokumen_ktp_dirut', self.dokumen_ktp_dirut) + # vals['dokumen_akta_pendirian'] = vals.get('dokumen_akta_pendirian', self.dokumen_akta_pendirian) + # vals['dokumen_laporan_keuangan'] = vals.get('dokumen_laporan_keuangan', self.dokumen_laporan_keuangan) + # vals['dokumen_foto_kantor'] = vals.get('dokumen_foto_kantor', self.dokumen_foto_kantor) + # vals['dokumen_tempat_bekerja'] = vals.get('dokumen_tempat_bekerja', self.dokumen_tempat_bekerja) + # + # # Simpan hanya field yang perlu di-update pada child + # vals_for_child = { + # 'customer_type': vals.get('customer_type'), + # 'nama_wajib_pajak': vals.get('nama_wajib_pajak'), + # 'npwp': vals.get('npwp'), + # 'sppkp': vals.get('sppkp'), + # 'alamat_lengkap_text': vals.get('alamat_lengkap_text'), + # 'industry_id': vals.get('industry_id'), + # 'company_type_id': vals.get('company_type_id'), + # 'supplier_ids': vals.get('supplier_ids'), + # 'name_tempo': vals.get('name_tempo'), + # 'industry_id_tempo': vals.get('industry_id_tempo'), + # 'street_tempo': vals.get('street_tempo'), + # 'state_id_tempo': vals.get('state_id_tempo'), + # 'city_id_tempo': vals.get('city_id_tempo'), + # 'zip_tempo': vals.get('zip_tempo'), + # 'bank_name_tempo': vals.get('bank_name_tempo'), + # 'account_name_tempo': vals.get('account_name_tempo'), + # 'account_number_tempo': vals.get('account_number_tempo'), + # 'website_tempo': vals.get('website_tempo'), + # 'portal': vals.get('portal'), + # 'estimasi_tempo': vals.get('estimasi_tempo'), + # 'tempo_duration': vals.get('tempo_duration'), + # 'tempo_limit': vals.get('tempo_limit'), + # 'category_produk_ids': vals.get('category_produk_ids'), + # 'direktur_name': vals.get('direktur_name'), + # 'direktur_mobile': vals.get('direktur_mobile'), + # 'direktur_email': vals.get('direktur_email'), + # 'purchasing_name': vals.get('purchasing_name'), + # 'purchasing_mobile': vals.get('purchasing_mobile'), + # 'purchasing_email': vals.get('purchasing_email'), + # 'finance_name': vals.get('finance_name'), + # 'finance_mobile': vals.get('finance_mobile'), + # 'finance_email': vals.get('finance_email'), + # 'pic_name': vals.get('pic_name'), + # 'pic_mobile': vals.get('pic_mobile'), + # 'street_pengiriman': vals.get('street_pengiriman'), + # 'state_id_pengiriman': vals.get('state_id_pengiriman'), + # 'city_id_pengiriman': vals.get('city_id_pengiriman'), + # 'district_id_pengiriman': vals.get('district_id_pengiriman'), + # 'subDistrict_id_pengiriman': vals.get('subDistrict_id_pengiriman'), + # 'zip_pengiriman': vals.get('zip_pengiriman'), + # 'invoice_pic': vals.get('invoice_pic'), + # 'invoice_pic_mobile': vals.get('invoice_pic_mobile'), + # 'street_invoice': vals.get('street_invoice'), + # 'state_id_invoice': vals.get('state_id_invoice'), + # 'city_id_invoice': vals.get('city_id_invoice'), + # 'district_id_invoice': vals.get('district_id_invoice'), + # 'subDistrict_id_invoice': vals.get('subDistrict_id_invoice'), + # 'zip_invoice': vals.get('zip_invoice'), + # 'tukar_invoice': vals.get('tukar_invoice'), + # 'jadwal_bayar': vals.get('jadwal_bayar'), + # 'dokumen_prosedur': vals.get('dokumen_prosedur'), + # 'dokumen_pengiriman': vals.get('dokumen_pengiriman'), + # 'dokumen_pengiriman_input': vals.get('dokumen_pengiriman_input'), + # 'dokumen_invoice': vals.get('dokumen_invoice'), + # 'dokumen_npwp': vals.get('dokumen_npwp'), + # 'dokumen_sppkp': vals.get('dokumen_sppkp'), + # 'dokumen_nib': vals.get('dokumen_nib'), + # 'dokumen_siup': vals.get('dokumen_siup'), + # 'dokumen_tdp': vals.get('dokumen_tdp'), + # 'dokumen_skdp': vals.get('dokumen_skdp'), + # 'dokumen_skt': vals.get('dokumen_skt'), + # 'dokumen_akta_perubahan': vals.get('dokumen_akta_perubahan'), + # 'dokumen_ktp_dirut': vals.get('dokumen_ktp_dirut'), + # 'dokumen_akta_pendirian': vals.get('dokumen_akta_pendirian'), + # 'dokumen_laporan_keuangan': vals.get('dokumen_laporan_keuangan'), + # 'dokumen_foto_kantor': vals.get('dokumen_foto_kantor'), + # 'dokumen_tempat_bekerja': vals.get('dokumen_tempat_bekerja'), + # + # # internal_notes + # 'comment': vals.get('comment') + # } + # + # # Lakukan update pada semua child secara rekursif + # for child in children: + # update_children_recursively(child, vals_for_child) + # + # # Lakukan write untuk parent dengan vals asli + # res = super(ResPartner, self).write(vals) + # + # return res # if self.company_type == 'person' and not partner.parent_id: # if self.parent_id: -- cgit v1.2.3 From 9b1ff130cfcec243c698ff4a8b2a5412d3e79011 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Wed, 28 May 2025 14:47:16 +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 ea52450e..e83ab13f 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -288,7 +288,7 @@ class StockPicking(models.Model): self.ensure_one() if not self.name or not self.origin: return False - return f"{self.name}, {self.origin}" + return f"{self.name} {self.origin}" def _download_pod_photo(self, url): """Mengunduh foto POD dari URL""" -- cgit v1.2.3 From 6e56ffc2c5b5ee22bc97d2518274eeb8a959e80e Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 28 May 2025 15:16:40 +0700 Subject: (andri) penyesuaian request biteship di stock picking --- indoteknik_custom/models/sale_order.py | 4 +- indoteknik_custom/models/stock_picking.py | 73 ++++++++++++++++++++----------- 2 files changed, 49 insertions(+), 28 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index f2bb27ad..a4166016 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -862,8 +862,8 @@ class SaleOrder(models.Model): def _call_biteship_api(self, origin_data, destination_data, items, couriers=None): - url = 'https://api.biteship.com/v1/rates/couriers' - api_key = 'biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA' + url = "https://api.biteship.com/v1/rates/couriers" + api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA" headers = { 'Authorization': api_key, diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 4522dac0..54da700b 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -526,46 +526,55 @@ class StockPicking(models.Model): if self.biteship_tracking_id: raise UserError(f"Order ini sudah dikirim ke Biteship. Dengan Tracking Id: {self.biteship_tracking_id}") - # Mencari data sale.order.line berdasarkan sale_id + # Fungsi bantu: menentukan apakah kurir perlu koordinat + def is_courier_need_coordinates(service_code): + return service_code in [ + "instant", "same_day", "instant_car", + "instant_bike", "motorcycle", "mpv", "van", "truck", + "cdd_bak", "cdd_box", "engkel_box", "engkel_bak" + ] + + # Ambil order line products = self.env['sale.order.line'].search([('order_id', '=', self.sale_id.id)]) - - # Fungsi untuk membangun items_data dari order lines + + # Bangun data items untuk standard def build_items_data(lines): return [{ "name": line.product_id.name, "description": line.name, "value": line.price_unit, "quantity": line.product_uom_qty, - "weight": line.weight + "weight": line.weight*1000 } for line in lines] - # Items untuk pengiriman standard items_data_standard = build_items_data(products) - # Items untuk pengiriman instant, mengambil product_id dari move_line_ids_without_package + # Bangun data items untuk pengiriman instant items_data_instant = [] for move_line in self.move_line_ids_without_package: - # Mencari baris di sale.order.line berdasarkan product_id dari move_line order_line = self.env['sale.order.line'].search([ ('order_id', '=', self.sale_id.id), ('product_id', '=', move_line.product_id.id) ], limit=1) - + if order_line: items_data_instant.append({ "name": order_line.product_id.name, "description": order_line.name, "value": order_line.price_unit, "quantity": move_line.qty_done, - "weight": order_line.weight + "weight": order_line.weight*1000 }) + _logger.info(f"Items data standard: {items_data_standard}") + _logger.info(f"Items data instant: {items_data_instant}") + # Bangun payload dasar payload = { - "origin_coordinate" :{ + "origin_coordinate": { "latitude": -6.3031123, - "longitude" : 106.7794934999 + "longitude": 106.7794934999 }, - "reference_id" : self.sale_id.name, + "reference_id": self.sale_id.name, "shipper_contact_name": self.carrier_id.pic_name or '', "shipper_contact_phone": self.carrier_id.pic_phone or '', "shipper_organization": self.carrier_id.name, @@ -581,30 +590,40 @@ class StockPicking(models.Model): "courier_type": self.sale_id.delivery_service_type or "reg", "courier_company": self.carrier_id.name.lower(), "delivery_type": "now", - "destination_postal_code": self.real_shipping_id.zip, "items": items_data_standard } - _logger.info(f"Payload untuk Biteship: {payload}") + _logger.info(f"Delivery service type: {self.sale_id.delivery_service_type}") + _logger.info(f"Carrier: {self.carrier_id.name}") + _logger.info(f"Payload awal: {payload}") + + # Tambahkan destination_coordinate jika diperlukan + if is_courier_need_coordinates(self.sale_id.delivery_service_type): + if not self.real_shipping_id.latitude or not self.real_shipping_id.longtitude: + raise UserError("Alamat tujuan tidak memiliki koordinat (latitude/longitude).") + + # items_to_use = items_data_instant if items_data_instant else items_data_standard + if not items_data_instant: + raise UserError("Pengiriman instant membutuhkan produk yang sudah diproses (qty_done > 0). Harap lakukan validasi picking terlebih dahulu.") - # Cek jika pengiriman instant atau same_day - if self.sale_id.delivery_service_type and ("instant" in self.sale_id.delivery_service_type or "same_day" in self.sale_id.delivery_service_type): payload.update({ - "destination_coordinate" : { + "destination_coordinate": { "latitude": self.real_shipping_id.latitude, "longitude": self.real_shipping_id.longtitude, }, "items": items_data_instant }) - + + _logger.info(f"Payload untuk Biteship: {payload}") + + # Kirim ke Biteship api_key = _biteship_api_key headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } - # Kirim request ke Biteship - response = requests.post(_biteship_url+'/orders', headers=headers, json=payload) + response = requests.post(_biteship_url + '/orders', headers=headers, json=payload) _logger.info(f"Response dari Biteship: {response.text}") if response.status_code == 200: @@ -614,11 +633,11 @@ class StockPicking(models.Model): self.biteship_tracking_id = data.get("courier", {}).get("tracking_id", "") self.biteship_waybill_id = data.get("courier", {}).get("waybill_id", "") self.delivery_tracking_no = data.get("courier", {}).get("waybill_id", "") - - waybill_id = data.get("courier", {}).get("waybill_id", "") + + waybill_id = self.biteship_waybill_id self.message_post( - body=f"📦 Biteship berhasil dilakukan.
" + body=f"Biteship berhasil dilakukan.
" f"Kurir: {self.carrier_id.name}
" f"Tracking ID: {self.biteship_tracking_id or '-'}
" f"Resi: {waybill_id or '-'}", @@ -629,16 +648,18 @@ class StockPicking(models.Model): return { 'effect': { - 'fadeout': 'slow', # Efek menghilang perlahan - 'message': message, # Pesan sukses - 'type': 'rainbow_man', # Efek animasi lucu Odoo + 'fadeout': 'slow', + 'message': message, + 'type': 'rainbow_man', } } + else: error_data = response.json() error_message = error_data.get("error", "Unknown error") error_code = error_data.get("code", "No code provided") raise UserError(f"Error saat mengirim ke Biteship: {error_message} (Code: {error_code})") + @api.constrains('driver_departure_date') def constrains_driver_departure_date(self): -- cgit v1.2.3 From ea3d8ab967d6f02c81b92115f2c82a2a17dae99d Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Wed, 28 May 2025 15:52:19 +0700 Subject: remove validation on create, cause of error while duplicate --- indoteknik_custom/models/sale_order.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index f89dfb10..fa570819 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -1259,6 +1259,7 @@ class SaleOrder(models.Model): self._validate_order() for order in self: + order._validate_delivery_amt() order._validate_uniform_taxes() order.order_line.validate_line() order.check_data_real_delivery_address() @@ -1501,6 +1502,7 @@ class SaleOrder(models.Model): def action_confirm(self): for order in self: + order._validate_delivery_amt() order._validate_uniform_taxes() order.check_duplicate_product() order.check_product_bom() @@ -1962,7 +1964,7 @@ class SaleOrder(models.Model): order = super(SaleOrder, self).create(vals) order._compute_etrts_date() order._validate_expected_ready_ship_date() - order._validate_delivery_amt() + # order._validate_delivery_amt() # order._check_total_margin_excl_third_party() # order._update_partner_details() return order -- cgit v1.2.3 From b641951b811590231c060ac40ef633f59037bfbb Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 29 May 2025 20:26:55 +0700 Subject: (andri) revisi biteship terkait berat --- indoteknik_custom/models/sale_order.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index a4166016..94cbfb84 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -652,8 +652,8 @@ class SaleOrder(models.Model): product_names = '
'.join(missing_weight_products) self.message_post(body=f"Produk berikut tidak memiliki berat:
{product_names}") - if total_weight == 0: - raise UserError("Tidak dapat mengestimasi ongkir tanpa karena berat produk = 0 kg.") + # if total_weight == 0: + # raise UserError("Tidak dapat mengestimasi ongkir tanpa karena berat produk = 0 kg.") # Validasi alamat pengiriman if not self.real_shipping_id: @@ -674,8 +674,8 @@ class SaleOrder(models.Model): total_weight = self._validate_for_shipping_estimate() weight_gram = int(total_weight * 1000) - # if weight_gram < 100: - # weight_gram = 100 + if weight_gram < 100: + weight_gram = 100 items = [{ "name": "Paket Pesanan", -- cgit v1.2.3 From 711885733186a090be447099f1b7979e89ada85d Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 29 May 2025 21:58:53 +0700 Subject: (andri) pilihan shipping method akan terganti jika pilihan kurir tsb tidak ada pada hasil estimate shipping (otomatis akan keganti di opsi pertama) --- indoteknik_custom/models/sale_order.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 94cbfb84..e4564c7d 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -828,6 +828,19 @@ class SaleOrder(models.Model): selected_option = shipping_options[0] _logger.info(f"Menggunakan opsi pertama: {selected_option.name}") + # Ganti carrier_id otomatis sesuai provider dari shipping option + provider = selected_option.provider.lower() + self.env.cr.execute(""" + SELECT delivery_carrier_id FROM rajaongkir_kurir + WHERE LOWER(name) = %s AND delivery_carrier_id IS NOT NULL + LIMIT 1 + """, (provider,)) + row = self.env.cr.fetchone() + matched_carrier_id = row[0] if row else False + if matched_carrier_id: + self.carrier_id = matched_carrier_id + _logger.info(f"Carrier diganti otomatis ke ID {matched_carrier_id} berdasarkan provider {provider}") + if selected_option: self.shipping_option_id = selected_option.id self.delivery_amt = selected_option.price -- cgit v1.2.3 From d6c59069035919e270d4940a39242fe5d5291982 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 29 May 2025 22:13:46 +0700 Subject: (andri) tambah validasi pada shipping method --- indoteknik_custom/models/sale_order.py | 83 +++++++++++++++++----------------- 1 file changed, 41 insertions(+), 42 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index e4564c7d..946761ce 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -306,64 +306,63 @@ class SaleOrder(models.Model): @api.onchange('carrier_id') def _onchange_carrier_id(self): - self.shipping_option_id = False - self.delivery_amt = 0 - if not self.carrier_id: + self.shipping_option_id = False + self.delivery_amt = 0 return {'domain': {'shipping_option_id': [('id', '=', -1)]}} - - # Cari provider dari carrier yang dipilih langsung dari rajaongkir_kurir + + # Ambil provider dari rajaongkir_kurir self.env.cr.execute(""" SELECT name FROM rajaongkir_kurir WHERE delivery_carrier_id = %s LIMIT 1 """, (self.carrier_id.id,)) - result = self.env.cr.fetchone() provider = result[0].lower() if result and result[0] else False - - # Fallback jika tidak ditemukan di rajaongkir_kurir + + # Fallback: pakai nama carrier if not provider: - # Gunakan nama carrier, ambil kata pertama provider = self.carrier_id.name.lower().split()[0] if self.carrier_id.name else False - - # Log untuk debugging + _logger.info(f"Carrier changed to {self.carrier_id.name}, provider: {provider}") - - # PENTING: self.id mungkin False atau NewId pada saat onchange - # Perlu memeriksa apakah ini adalah record baru atau yang sudah ada - sale_order_id = False - if hasattr(self, '_origin') and self._origin: - sale_order_id = self._origin.id - - # Cek jika ada shipping options dengan provider ini (tanpa filter sale_order_id dulu) + + sale_order_id = self._origin.id if self._origin else False + + # Cek jumlah shipping option dengan provider tersebut self.env.cr.execute(""" SELECT COUNT(*) FROM shipping_option - WHERE LOWER(provider) LIKE %s - """, (f'%{provider}%',)) - + WHERE LOWER(provider) LIKE %s AND sale_order_id = %s + """, (f'%{provider}%', sale_order_id)) count = self.env.cr.fetchone()[0] + _logger.info(f"Found {count} shipping options for provider {provider}") - - # Buat domain untuk shipping_option_id - if count > 0: - # Jika ada options yang tersedia, buat domain yang lebih permisif - domain = [ - '|', - ('provider', 'ilike', f'%{provider}%'), - ('provider', '=', provider) - ] - - # Jika ini record yang sudah ada, tambahkan filter sale_order_id - if sale_order_id: - domain = [ - '|', - '&', ('sale_order_id', '=', sale_order_id), ('provider', 'ilike', f'%{provider}%'), - '&', ('sale_order_id', '=', False), ('provider', 'ilike', f'%{provider}%') - ] - else: - domain = [('id', '=', -1)] # Tidak ada opsi - + + # VALIDASI GAGAL + if count == 0: + previous_carrier = self._origin.carrier_id if self._origin else False + self.carrier_id = previous_carrier + + return { + 'warning': { + 'title': "Shipping Method Tidak Tersedia", + 'message': ( + f"Shipping method '{provider}' tidak tersedia pada pengiriman ini.\n" + f"Pilihan dikembalikan ke sebelumnya." + ) + }, + 'domain': {'shipping_option_id': [('id', '=', -1)]} + } + + # ✅ Valid, baru reset shipping_option dan delivery amount + self.shipping_option_id = False + self.delivery_amt = 0 + + domain = [ + '|', + '&', ('sale_order_id', '=', sale_order_id), ('provider', 'ilike', f'%{provider}%'), + '&', ('sale_order_id', '=', False), ('provider', 'ilike', f'%{provider}%') + ] + _logger.info(f"Final domain for shipping_option_id: {domain}") return {'domain': {'shipping_option_id': domain}} -- cgit v1.2.3 From 2f89da69c9ec8d78f187be5afd254cdd594ee24a Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Fri, 30 May 2025 09:19:22 +0700 Subject: (andri) fix bug shipping method custom --- indoteknik_custom/models/sale_order.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 946761ce..f1280b37 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -311,6 +311,15 @@ class SaleOrder(models.Model): self.delivery_amt = 0 return {'domain': {'shipping_option_id': [('id', '=', -1)]}} + # ✅ Lewati validasi jika carrier bukan Biteship + if self.carrier_id.delivery_type != 'biteship': + _logger.info(f"Carrier {self.carrier_id.name} bertipe custom ({self.carrier_id.delivery_type}), tidak divalidasi.") + self.shipping_option_id = False + self.delivery_amt = 0 + return { + 'domain': {'shipping_option_id': [('id', '=', -1)]} + } + # Ambil provider dari rajaongkir_kurir self.env.cr.execute(""" SELECT name FROM rajaongkir_kurir @@ -320,21 +329,27 @@ class SaleOrder(models.Model): result = self.env.cr.fetchone() provider = result[0].lower() if result and result[0] else False - # Fallback: pakai nama carrier + # Fallback ke nama carrier jika tidak ada di rajaongkir_kurir if not provider: provider = self.carrier_id.name.lower().split()[0] if self.carrier_id.name else False _logger.info(f"Carrier changed to {self.carrier_id.name}, provider: {provider}") - sale_order_id = self._origin.id if self._origin else False + sale_order_id = self._origin.id if self._origin and self._origin.id else None - # Cek jumlah shipping option dengan provider tersebut - self.env.cr.execute(""" - SELECT COUNT(*) FROM shipping_option - WHERE LOWER(provider) LIKE %s AND sale_order_id = %s - """, (f'%{provider}%', sale_order_id)) - count = self.env.cr.fetchone()[0] + # Cek shipping option untuk provider ini + if sale_order_id: + self.env.cr.execute(""" + SELECT COUNT(*) FROM shipping_option + WHERE LOWER(provider) LIKE %s AND sale_order_id = %s + """, (f'%{provider}%', sale_order_id)) + else: + self.env.cr.execute(""" + SELECT COUNT(*) FROM shipping_option + WHERE LOWER(provider) LIKE %s AND sale_order_id IS NULL + """, (f'%{provider}%',)) + count = self.env.cr.fetchone()[0] _logger.info(f"Found {count} shipping options for provider {provider}") # VALIDASI GAGAL @@ -346,7 +361,7 @@ class SaleOrder(models.Model): 'warning': { 'title': "Shipping Method Tidak Tersedia", 'message': ( - f"Shipping method '{provider}' tidak tersedia pada pengiriman ini.\n" + f"Shipping method '{self.carrier_id.name}' tidak tersedia pada pengiriman ini.\n" f"Pilihan dikembalikan ke sebelumnya." ) }, @@ -366,6 +381,7 @@ class SaleOrder(models.Model): _logger.info(f"Final domain for shipping_option_id: {domain}") return {'domain': {'shipping_option_id': domain}} + @api.onchange('shipping_option_id') def _onchange_shipping_option_id(self): if not self.shipping_option_id: -- cgit v1.2.3 From e3856970da63328c820833893c894f18dc6700bd Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 30 May 2025 15:11:42 +0700 Subject: fix bug total percent margin --- indoteknik_custom/models/sale_order.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index fa570819..d1dc3324 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -403,14 +403,14 @@ class SaleOrder(models.Model): if len(tax_sets) > 1: raise ValidationError("Semua produk dalam Sales Order harus memiliki kombinasi pajak yang sama.") - # @api.constrains('fee_third_party', 'delivery_amt', 'biaya_lain_lain') + # @api.constrains('fee_third_party', 'delivery_amt', 'biaya_lain_lain', 'ongkir_ke_xpdc') # def _check_total_margin_excl_third_party(self): # for rec in self: # if rec.fee_third_party == 0 and rec.total_margin_excl_third_party != rec.total_percent_margin: # # Gunakan direct SQL atau flag context untuk menghindari rekursi # self.env.cr.execute(""" - # UPDATE sale_order - # SET total_margin_excl_third_party = %s + # UPDATE sale_order + # SET total_margin_excl_third_party = %s # WHERE id = %s # """, (rec.total_percent_margin, rec.id)) # self.invalidate_cache() @@ -1703,9 +1703,14 @@ class SaleOrder(models.Model): else: delivery_amt = 0 - # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2) + net_margin = order.total_margin - order.biaya_lain_lain + order.total_percent_margin = round( - (order.total_margin / (order.amount_untaxed - order.fee_third_party - order.biaya_lain_lain)) * 100, 2) + (net_margin / (order.amount_untaxed - order.fee_third_party)) * 100, 2) + + # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2) + # order.total_percent_margin = round( + # (order.total_margin / (order.amount_untaxed - order.fee_third_party - order.biaya_lain_lain)) * 100, 2) # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed)) * 100, 2) @api.onchange('sales_tax_id') -- cgit v1.2.3 From b6962acb39ad373f2aded4bebfa1e7a2dbbb0a8a Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sat, 31 May 2025 09:17:01 +0700 Subject: before margin --- indoteknik_custom/models/sale_order.py | 228 ++++++++++++++++++++++++++------- 1 file changed, 182 insertions(+), 46 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index d1dc3324..3c69c3d1 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -148,8 +148,8 @@ class SaleOrder(models.Model): help="Total Margin in Sales Order Header") total_percent_margin = fields.Float('Total Percent Margin', compute='_compute_total_percent_margin', help="Total % Margin in Sales Order Header") - total_margin_excl_third_party = fields.Float('Before Margin', help="Before Margin in Sales Order Header", - compute='_compute_total_margin_excl_third_party') + total_margin_excl_third_party = fields.Float('Before Margin', help="Before Margin in Sales Order Header") + approval_status = fields.Selection([ ('pengajuan1', 'Approval Manager'), ('pengajuan2', 'Approval Pimpinan'), @@ -340,16 +340,16 @@ class SaleOrder(models.Model): date_unhold = fields.Datetime(string='Date Unhold', tracking=True, readonly=True, help='Waktu ketika SO di Unhold' ) - def _compute_total_margin_excl_third_party(self): - for order in self: - if order.amount_untaxed == 0: - order.total_margin_excl_third_party = 0 - continue - - # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2) - order.total_margin_excl_third_party = round((order.total_before_margin / (order.amount_untaxed)) * 100, 2) - # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed)) * 100, 2) - + # def _compute_total_margin_excl_third_party(self): + # for order in self: + # if order.amount_untaxed == 0: + # order.total_margin_excl_third_party = 0 + # continue + # + # # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2) + # order.total_margin_excl_third_party = round((order.total_before_margin / (order.amount_untaxed)) * 100, 2) + # # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed)) * 100, 2) + # def ask_retur_cancel_purchasing(self): for rec in self: if self.env.user.has_group('indoteknik_custom.group_role_purchasing'): @@ -1035,11 +1035,11 @@ class SaleOrder(models.Model): line_no += 1 line.line_no = line_no - def write(self, vals): - if 'carrier_id' in vals: - for picking in self.picking_ids: - if picking.state == 'assigned': - picking.carrier_id = self.carrier_id + # def write(self, vals): + # if 'carrier_id' in vals: + # for picking in self.picking_ids: + # if picking.state == 'assigned': + # picking.carrier_id = self.carrier_id def calculate_so_status(self): so_state = ['sale'] @@ -1157,12 +1157,12 @@ class SaleOrder(models.Model): helper_ids_str = self.env['ir.config_parameter'].sudo().get_param('sale.order.user_helper_ids') return helper_ids_str.split(', ') - def write(self, values): - helper_ids = self._get_helper_ids() - if str(self.env.user.id) in helper_ids: - values['helper_by_id'] = self.env.user.id - - return super(SaleOrder, self).write(values) + # def write(self, values): + # helper_ids = self._get_helper_ids() + # if str(self.env.user.id) in helper_ids: + # values['helper_by_id'] = self.env.user.id + # + # return super(SaleOrder, self).write(values) def check_due(self): """To show the due amount and warning stage""" @@ -1693,25 +1693,161 @@ class SaleOrder(models.Model): total_before_margin = sum(line.item_before_margin for line in order.order_line if line.product_id) order.total_before_margin = total_before_margin + # def _compute_total_percent_margin(self): + # for order in self: + # if order.amount_untaxed == 0: + # order.total_percent_margin = 0 + # continue + # if order.shipping_cost_covered == 'indoteknik': + # delivery_amt = order.delivery_amt + # else: + # delivery_amt = 0 + # + # net_margin = order.total_margin - order.biaya_lain_lain + # + # order.total_percent_margin = round( + # (net_margin / (order.amount_untaxed - order.fee_third_party)) * 100, 2) + + # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2) + # order.total_percent_margin = round( + # (order.total_margin / (order.amount_untaxed - order.fee_third_party - order.biaya_lain_lain)) * 100, 2) + # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed)) * 100, 2) + def _compute_total_percent_margin(self): for order in self: if order.amount_untaxed == 0: order.total_percent_margin = 0 continue + if order.shipping_cost_covered == 'indoteknik': delivery_amt = order.delivery_amt else: delivery_amt = 0 + # Net margin = total margin - biaya tambahan net_margin = order.total_margin - order.biaya_lain_lain + denominator = order.amount_untaxed - order.fee_third_party - order.total_percent_margin = round( - (net_margin / (order.amount_untaxed - order.fee_third_party)) * 100, 2) + if denominator > 0: + order.total_percent_margin = round((net_margin / denominator) * 100, 2) + else: + order.total_percent_margin = 0 - # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2) - # order.total_percent_margin = round( - # (order.total_margin / (order.amount_untaxed - order.fee_third_party - order.biaya_lain_lain)) * 100, 2) - # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed)) * 100, 2) + # @api.onchange('biaya_lain_lain') + # def _onchange_biaya_lain_lain(self): + # """Ketika biaya_lain_lain berubah, simpan nilai margin sebelumnya""" + # if hasattr(self, '_origin') and self._origin.id: + # # Hitung margin sebelum biaya_lain_lain ditambahkan + # if self.amount_untaxed > 0: + # original_net_margin = self.total_margin # tanpa biaya_lain_lain + # self.total_margin_excl_third_party = round( + # (original_net_margin / (self.amount_untaxed - self.fee_third_party)) * 100, 2) + + def write(self, vals): + import logging + _logger = logging.getLogger(__name__) + + # Simpan nilai margin sebelumnya untuk field yang mempengaruhi perhitungan + margin_sebelumnya = {} + + margin_affecting_fields = [ + 'biaya_lain_lain', 'fee_third_party', 'delivery_amt', + 'ongkir_ke_xpdc', 'shipping_cost_covered', 'order_line' + ] + + if any(field in vals for field in margin_affecting_fields): + for order in self: + if order.amount_untaxed > 0: + # LOGIC PENTING: Kapan Before Margin harus diupdate? + + current_before = order.total_margin_excl_third_party or 0 + + # CASE 1: Before margin masih kosong, simpan margin saat ini + if current_before == 0: + margin_sebelumnya[order.id] = order.total_percent_margin + _logger.info( + f"CASE 1 - SO {order.name}: Before margin kosong, simpan {order.total_percent_margin}%") + + # CASE 2: Ada perubahan biaya_lain_lain + elif 'biaya_lain_lain' in vals: + old_biaya = order.biaya_lain_lain or 0 + new_biaya = vals['biaya_lain_lain'] or 0 + + _logger.info(f"SO {order.name}: Biaya lain-lain berubah dari {old_biaya} ke {new_biaya}") + + # Jika sebelumnya tidak ada biaya_lain_lain, dan sekarang ada + if old_biaya == 0 and new_biaya > 0: + # Simpan margin saat ini sebagai "before margin" + margin_sebelumnya[order.id] = order.total_percent_margin + _logger.info(f"Menyimpan before margin: {order.total_percent_margin}%") + + # Jika biaya_lain_lain dihapus (dari ada jadi 0) + elif old_biaya > 0 and new_biaya == 0: + # Before margin tetap tidak berubah (sudah tersimpan sebelumnya) + _logger.info(f"Biaya dihapus, before margin tetap: {current_before}%") + # TIDAK mengubah before margin + + # CASE 3: Perubahan field lain (fee_third_party, dll) + elif any(field in vals for field in + ['fee_third_party', 'delivery_amt', 'ongkir_ke_xpdc', 'order_line']): + # Simpan margin saat ini sebelum perubahan + margin_sebelumnya[order.id] = order.total_percent_margin + _logger.info(f"CASE 3 - Field lain berubah, simpan {order.total_percent_margin}%") + + # Validasi dan proses lainnya... + for order in self: + if order.state in ['sale', 'cancel']: + if 'order_line' in vals: + new_lines = vals.get('order_line', []) + for command in new_lines: + if command[0] == 0: + raise UserError( + "SO tidak dapat ditambahkan produk baru karena SO sudah menjadi sale order.") + + if 'carrier_id' in vals: + for order in self: + for picking in order.picking_ids: + if picking.state == 'assigned': + picking.carrier_id = vals['carrier_id'] + + try: + helper_ids = self._get_helper_ids() + if str(self.env.user.id) in helper_ids: + vals['helper_by_id'] = self.env.user.id + except: + pass + + # Jalankan super write + res = super(SaleOrder, self).write(vals) + + # Update before margin jika diperlukan + if margin_sebelumnya: + for order_id, margin_value in margin_sebelumnya.items(): + _logger.info(f"Updating before margin untuk SO {order_id}: {margin_value}%") + + self.env.cr.execute(""" + UPDATE sale_order + SET total_margin_excl_third_party = %s + WHERE id = %s + """, (margin_value, order_id)) + + self.env.cr.commit() + self.invalidate_cache(['total_margin_excl_third_party']) + + # Validasi lainnya + if any(field in vals for field in ['delivery_amt', 'carrier_id', 'shipping_cost_covered']): + try: + self._validate_delivery_amt() + except: + pass + + if any(field in vals for field in ["order_line", "client_order_ref"]): + try: + self._calculate_etrts_date() + except: + pass + + return res @api.onchange('sales_tax_id') def onchange_sales_tax_id(self): @@ -2002,23 +2138,23 @@ class SaleOrder(models.Model): 'customer_type': partner.customer_type, }) - def write(self, vals): - for order in self: - if order.state in ['sale', 'cancel']: - if 'order_line' in vals: - new_lines = vals.get('order_line', []) - for command in new_lines: - if command[0] == 0: # A new line is being added - raise UserError( - "SO tidak dapat ditambahkan produk baru karena SO sudah menjadi sale order.") - - res = super(SaleOrder, self).write(vals) - # self._check_total_margin_excl_third_party() - if any(fields in vals for fields in ['delivery_amt', 'carrier_id', 'shipping_cost_covered']): - self._validate_delivery_amt() - if any(field in vals for field in ["order_line", "client_order_ref"]): - self._calculate_etrts_date() - return res + # def write(self, vals): + # for order in self: + # if order.state in ['sale', 'cancel']: + # if 'order_line' in vals: + # new_lines = vals.get('order_line', []) + # for command in new_lines: + # if command[0] == 0: # A new line is being added + # raise UserError( + # "SO tidak dapat ditambahkan produk baru karena SO sudah menjadi sale order.") + # + # res = super(SaleOrder, self).write(vals) + # # self._check_total_margin_excl_third_party() + # if any(fields in vals for fields in ['delivery_amt', 'carrier_id', 'shipping_cost_covered']): + # self._validate_delivery_amt() + # if any(field in vals for field in ["order_line", "client_order_ref"]): + # self._calculate_etrts_date() + # return res # @api.depends('commitment_date') def _compute_ready_to_ship_status_detail(self): -- cgit v1.2.3 From c652ee3e1f652d23e37833f01e3fdd7aa8e52021 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 31 May 2025 11:03:06 +0700 Subject: (andri) add tracking biteship pada BU OUT & fix delivery departure & arrival date --- indoteknik_custom/models/stock_picking.py | 55 ++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 54da700b..4d38e5b3 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1551,7 +1551,60 @@ class StockPicking(models.Model): except Exception as e : _logger.error(f"Error fetching Biteship order for picking {self.id}: {str(e)}") return { 'error': str(e) } - + + def action_sync_biteship_tracking(self): + for picking in self: + if not picking.biteship_id: + raise UserError("Tracking Biteship tidak tersedia.") + + histori = picking.get_manifest_biteship() + updated_fields = {} + seen_logs = set() + + manifests = sorted(histori.get("manifests", []), key=lambda m: m.get("datetime") or "") + + for manifest in manifests: + status = manifest.get("status", "").lower() + dt_str = manifest.get("datetime") + desc = manifest.get("description") + dt = False + + try: + local_dt_str = picking._convert_to_local_time(dt_str) + dt = fields.Datetime.from_string(local_dt_str) + except Exception as e: + _logger.warning(f"[Biteship Sync] Gagal parse datetime: {e}") + continue + + # Update tanggal ke field + if status == "picked" and dt and not picking.driver_departure_date: + updated_fields["driver_departure_date"] = dt + + if status == "delivered" and dt and not picking.driver_arrival_date: + updated_fields["driver_arrival_date"] = dt + + # Buat log unik + if dt and desc: + desc_clean = ' '.join(desc.strip().split()) + log_line = f"[TRACKING] {status} - {dt.strftime('%d %b %Y %H:%M')}: {desc_clean}" + if not picking._has_existing_log(log_line): + picking.message_post(body=log_line) + seen_logs.add(log_line) + + if updated_fields: + picking.write(updated_fields) + + def _has_existing_log(self, log_line): + self.ensure_one() + self.env.cr.execute(""" + SELECT 1 FROM mail_message + WHERE model = %s AND res_id = %s + AND subtype_id IS NOT NULL + AND body ILIKE %s + LIMIT 1 + """, (self._name, self.id, f"%{log_line}%")) + return self.env.cr.fetchone() is not None + def _convert_to_local_time(self, iso_date): try: dt_with_tz = waktu.fromisoformat(iso_date) -- cgit v1.2.3 From b6928c9bffc486b471d4c335c2550cbc1bf7d841 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 31 May 2025 12:32:45 +0700 Subject: (andri) fix bug datetime departure&arrival --- indoteknik_custom/models/stock_picking.py | 34 ++++++++++++++++++------------- 1 file changed, 20 insertions(+), 14 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 4d38e5b3..4517a941 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1570,23 +1570,23 @@ class StockPicking(models.Model): dt = False try: - local_dt_str = picking._convert_to_local_time(dt_str) - dt = fields.Datetime.from_string(local_dt_str) + dt = picking._convert_to_local_time(dt_str) except Exception as e: _logger.warning(f"[Biteship Sync] Gagal parse datetime: {e}") continue - # Update tanggal ke field + # Update tanggal ke field (pastikan naive datetime UTC) if status == "picked" and dt and not picking.driver_departure_date: - updated_fields["driver_departure_date"] = dt + updated_fields["driver_departure_date"] = fields.Datetime.to_string(dt) if status == "delivered" and dt and not picking.driver_arrival_date: - updated_fields["driver_arrival_date"] = dt + updated_fields["driver_arrival_date"] = fields.Datetime.to_string(dt) - # Buat log unik + # Buat log unik dengan waktu lokal Asia/Jakarta if dt and desc: + dt_local = pytz.utc.localize(dt).astimezone(pytz.timezone("Asia/Jakarta")) desc_clean = ' '.join(desc.strip().split()) - log_line = f"[TRACKING] {status} - {dt.strftime('%d %b %Y %H:%M')}: {desc_clean}" + log_line = f"[TRACKING] {status} - {dt_local.strftime('%d %b %Y %H:%M')}: {desc_clean}" if not picking._has_existing_log(log_line): picking.message_post(body=log_line) seen_logs.add(log_line) @@ -1607,15 +1607,21 @@ class StockPicking(models.Model): def _convert_to_local_time(self, iso_date): try: - dt_with_tz = waktu.fromisoformat(iso_date) - utc_dt = dt_with_tz.astimezone(pytz.utc) - + from dateutil import parser + import pytz + if isinstance(iso_date, str): + waktu = parser.parse(iso_date) + else: + waktu = iso_date + if waktu.tzinfo is None: + waktu = waktu.replace(tzinfo=pytz.utc) local_tz = pytz.timezone("Asia/Jakarta") - local_dt = utc_dt.astimezone(local_tz) - - return local_dt.strftime("%Y-%m-%d %H:%M:%S") + local_dt = waktu.astimezone(local_tz) + utc_dt = local_dt.astimezone(pytz.utc).replace(tzinfo=None) + return utc_dt except Exception as e: - return str(e) + _logger.warning(f"[Biteship] Gagal konversi waktu lokal: {e}") + return False def _map_status_biteship(self, status): status_mapping = { -- cgit v1.2.3 From 03d52ddceacea1939aef8ee4c571cacdb8b2c055 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sun, 1 Jun 2025 13:12:27 +0700 Subject: (andri) fix bug selected shipping service --- indoteknik_custom/models/sale_order.py | 78 ++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 37 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index f1280b37..490e4581 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -249,7 +249,7 @@ class SaleOrder(models.Model): string="Attachment Bukti Cancel", readonly=False, ) nomor_so_pengganti = fields.Char(string='Nomor SO Pengganti', copy=False, tracking=3) - shipping_option_id = fields.Many2one("shipping.option", string="Selected Service Option", domain="['|', ('sale_order_id', '=', False), ('sale_order_id', '=', id)]") + shipping_option_id = fields.Many2one("shipping.option", string="Selected Service Option", help="Selected shipping option for delivery", tracking=True, domain="['|', ('sale_order_id', '=', False), ('sale_order_id', '=', id)]") select_shipping_option = fields.Selection([ ('biteship', 'Biteship'), @@ -306,20 +306,13 @@ class SaleOrder(models.Model): @api.onchange('carrier_id') def _onchange_carrier_id(self): + self.shipping_option_id = False + # self.delivery_amt = 0 + # self.delivery_service_type = False + if not self.carrier_id: - self.shipping_option_id = False - self.delivery_amt = 0 return {'domain': {'shipping_option_id': [('id', '=', -1)]}} - # ✅ Lewati validasi jika carrier bukan Biteship - if self.carrier_id.delivery_type != 'biteship': - _logger.info(f"Carrier {self.carrier_id.name} bertipe custom ({self.carrier_id.delivery_type}), tidak divalidasi.") - self.shipping_option_id = False - self.delivery_amt = 0 - return { - 'domain': {'shipping_option_id': [('id', '=', -1)]} - } - # Ambil provider dari rajaongkir_kurir self.env.cr.execute(""" SELECT name FROM rajaongkir_kurir @@ -329,56 +322,67 @@ class SaleOrder(models.Model): result = self.env.cr.fetchone() provider = result[0].lower() if result and result[0] else False - # Fallback ke nama carrier jika tidak ada di rajaongkir_kurir + # Fallback dari nama carrier if not provider: provider = self.carrier_id.name.lower().split()[0] if self.carrier_id.name else False - _logger.info(f"Carrier changed to {self.carrier_id.name}, provider: {provider}") + _logger.info(f"[Carrier Changed] {self.carrier_id.name}, Detected Provider: {provider}") - sale_order_id = self._origin.id if self._origin and self._origin.id else None - - # Cek shipping option untuk provider ini - if sale_order_id: - self.env.cr.execute(""" - SELECT COUNT(*) FROM shipping_option - WHERE LOWER(provider) LIKE %s AND sale_order_id = %s - """, (f'%{provider}%', sale_order_id)) - else: - self.env.cr.execute(""" - SELECT COUNT(*) FROM shipping_option - WHERE LOWER(provider) LIKE %s AND sale_order_id IS NULL - """, (f'%{provider}%',)) + # Ambil ID SO + sale_order_id = self._origin.id if self._origin else False + # Hitung jumlah shipping_option yang cocok + self.env.cr.execute(""" + SELECT COUNT(*) FROM shipping_option + WHERE LOWER(provider) LIKE %s AND sale_order_id = %s + """, (f'%{provider}%', sale_order_id)) count = self.env.cr.fetchone()[0] - _logger.info(f"Found {count} shipping options for provider {provider}") - # VALIDASI GAGAL + _logger.info(f"[Shipping Option Count] Provider: {provider} | SO ID: {sale_order_id} | Count: {count}") + + # Jika tidak ditemukan shipping option if count == 0: previous_carrier = self._origin.carrier_id if self._origin else False + previous_provider = False + self.carrier_id = previous_carrier + self.shipping_option_id = self._origin.shipping_option_id if self._origin else False + + # Rehitung provider untuk domain fallback + if previous_carrier: + self.env.cr.execute(""" + SELECT name FROM rajaongkir_kurir + WHERE delivery_carrier_id = %s LIMIT 1 + """, (previous_carrier.id,)) + prev_row = self.env.cr.fetchone() + previous_provider = prev_row[0].lower() if prev_row and prev_row[0] else previous_carrier.name.lower().split()[0] + + fallback_domain = [('id', '=', -1)] + if sale_order_id and previous_provider: + fallback_domain = [ + '|', + '&', ('sale_order_id', '=', sale_order_id), ('provider', 'ilike', f'%{previous_provider}%'), + '&', ('sale_order_id', '=', False), ('provider', 'ilike', f'%{previous_provider}%') + ] return { 'warning': { - 'title': "Shipping Method Tidak Tersedia", + 'title': "Shipping Option Tidak Ditemukan", 'message': ( - f"Shipping method '{self.carrier_id.name}' tidak tersedia pada pengiriman ini.\n" + f"Layanan kurir tidak tersedia untuk pengiriman ini.\n" f"Pilihan dikembalikan ke sebelumnya." ) }, - 'domain': {'shipping_option_id': [('id', '=', -1)]} + 'domain': {'shipping_option_id': fallback_domain} } - # ✅ Valid, baru reset shipping_option dan delivery amount - self.shipping_option_id = False - self.delivery_amt = 0 - + # Jika ditemukan, set domain normal domain = [ '|', '&', ('sale_order_id', '=', sale_order_id), ('provider', 'ilike', f'%{provider}%'), '&', ('sale_order_id', '=', False), ('provider', 'ilike', f'%{provider}%') ] - _logger.info(f"Final domain for shipping_option_id: {domain}") return {'domain': {'shipping_option_id': domain}} -- cgit v1.2.3 From e5a3d37ca40127a6bd2a31f08d94704103a1ac11 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sun, 1 Jun 2025 17:23:56 +0700 Subject: (andri) fix bug shipping method --- indoteknik_custom/models/sale_order.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 490e4581..453406c4 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -306,6 +306,10 @@ class SaleOrder(models.Model): @api.onchange('carrier_id') def _onchange_carrier_id(self): + # Jika record masih baru (belum disimpan), jangan jalankan onchange + if not self._origin or not self._origin.id: + return + self.shipping_option_id = False # self.delivery_amt = 0 # self.delivery_service_type = False @@ -313,7 +317,7 @@ class SaleOrder(models.Model): if not self.carrier_id: return {'domain': {'shipping_option_id': [('id', '=', -1)]}} - # Ambil provider dari rajaongkir_kurir + # Ambil provider dari mapping self.env.cr.execute(""" SELECT name FROM rajaongkir_kurir WHERE delivery_carrier_id = %s @@ -322,16 +326,14 @@ class SaleOrder(models.Model): result = self.env.cr.fetchone() provider = result[0].lower() if result and result[0] else False - # Fallback dari nama carrier if not provider: provider = self.carrier_id.name.lower().split()[0] if self.carrier_id.name else False _logger.info(f"[Carrier Changed] {self.carrier_id.name}, Detected Provider: {provider}") - # Ambil ID SO - sale_order_id = self._origin.id if self._origin else False + sale_order_id = self._origin.id - # Hitung jumlah shipping_option yang cocok + # Cek apakah ada shipping_option yang cocok self.env.cr.execute(""" SELECT COUNT(*) FROM shipping_option WHERE LOWER(provider) LIKE %s AND sale_order_id = %s @@ -340,7 +342,6 @@ class SaleOrder(models.Model): _logger.info(f"[Shipping Option Count] Provider: {provider} | SO ID: {sale_order_id} | Count: {count}") - # Jika tidak ditemukan shipping option if count == 0: previous_carrier = self._origin.carrier_id if self._origin else False previous_provider = False @@ -348,7 +349,7 @@ class SaleOrder(models.Model): self.carrier_id = previous_carrier self.shipping_option_id = self._origin.shipping_option_id if self._origin else False - # Rehitung provider untuk domain fallback + # Ambil kembali domain provider sebelumnya if previous_carrier: self.env.cr.execute(""" SELECT name FROM rajaongkir_kurir @@ -358,7 +359,7 @@ class SaleOrder(models.Model): previous_provider = prev_row[0].lower() if prev_row and prev_row[0] else previous_carrier.name.lower().split()[0] fallback_domain = [('id', '=', -1)] - if sale_order_id and previous_provider: + if previous_provider: fallback_domain = [ '|', '&', ('sale_order_id', '=', sale_order_id), ('provider', 'ilike', f'%{previous_provider}%'), @@ -376,7 +377,7 @@ class SaleOrder(models.Model): 'domain': {'shipping_option_id': fallback_domain} } - # Jika ditemukan, set domain normal + # Jika data ada, kembalikan domain biasa domain = [ '|', '&', ('sale_order_id', '=', sale_order_id), ('provider', 'ilike', f'%{provider}%'), @@ -385,7 +386,6 @@ class SaleOrder(models.Model): return {'domain': {'shipping_option_id': domain}} - @api.onchange('shipping_option_id') def _onchange_shipping_option_id(self): if not self.shipping_option_id: -- cgit v1.2.3 From 1c0bec8dadc593348df9ca585dae13b8ff65c316 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sun, 1 Jun 2025 20:29:57 +0700 Subject: (andri) fix bug price estimate biteship --- indoteknik_custom/models/sale_order.py | 57 ++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 27 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 453406c4..ec4b55e7 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -696,10 +696,12 @@ class SaleOrder(models.Model): if weight_gram < 100: weight_gram = 100 + value = int(self.amount_untaxed or sum(line.price_subtotal for line in self.order_line)) + items = [{ "name": "Paket Pesanan", "description": f"Sale Order {self.name}", - "value": int(self.amount_untaxed), + "value": value, "weight": weight_gram, "quantity": 1, }] @@ -811,6 +813,7 @@ class SaleOrder(models.Model): if not shipping_options: raise UserError(f"Tidak ada layanan pengiriman ditemukan untuk kode pos {destination_data.get('destination_postal_code', '')}. Mohon periksa kembali kode pos atau gunakan metode pengiriman lain.") + # Temukan shipping option yang cocok berdasarkan carrier_id selected_option = None if self.carrier_id: @@ -822,44 +825,44 @@ class SaleOrder(models.Model): courier_code = rajaongkir_kurir.name.lower() carrier_name = self.carrier_id.name.lower() - possible_codes = [ + possible_codes = list({ courier_code, carrier_name, carrier_name.split()[0] if ' ' in carrier_name else carrier_name - ] + }) - _logger.info(f"Mencari shipping option untuk kurir: {possible_codes}") + _logger.info(f"[MATCHING] Mencari shipping option untuk kurir: {possible_codes}") for option in shipping_options: - option_provider = option.provider.lower() if option.provider else '' - option_name = option.name.lower() if option.name else '' - - for code in possible_codes: - if code in option_provider or code in option_name: - selected_option = option - _logger.info(f"Menemukan shipping option yang cocok: {option.name}") - break + option_provider = (option.provider or '').lower() + option_name = (option.name or '').lower() - if selected_option: + if any(code in option_provider or code in option_name for code in possible_codes): + selected_option = option + _logger.info(f"[MATCHED] Shipping option cocok: {option.name}") break if not selected_option and shipping_options: selected_option = shipping_options[0] - _logger.info(f"Menggunakan opsi pertama: {selected_option.name}") - - # Ganti carrier_id otomatis sesuai provider dari shipping option - provider = selected_option.provider.lower() - self.env.cr.execute(""" - SELECT delivery_carrier_id FROM rajaongkir_kurir - WHERE LOWER(name) = %s AND delivery_carrier_id IS NOT NULL - LIMIT 1 - """, (provider,)) - row = self.env.cr.fetchone() - matched_carrier_id = row[0] if row else False - if matched_carrier_id: - self.carrier_id = matched_carrier_id - _logger.info(f"Carrier diganti otomatis ke ID {matched_carrier_id} berdasarkan provider {provider}") + _logger.info(f"[DEFAULT] Tidak ada yang cocok, pakai opsi pertama: {selected_option.name}") + + # ❗ Ganti carrier_id hanya jika BELUM terisi sama sekali (contoh: user dari backend) + if not self.carrier_id: + provider = selected_option.provider.lower() + self.env.cr.execute(""" + SELECT delivery_carrier_id FROM rajaongkir_kurir + WHERE LOWER(name) = %s AND delivery_carrier_id IS NOT NULL + LIMIT 1 + """, (provider,)) + row = self.env.cr.fetchone() + matched_carrier_id = row[0] if row else False + if matched_carrier_id: + self.carrier_id = matched_carrier_id + _logger.info(f"[AUTO-SET] Carrier diisi otomatis ke ID {matched_carrier_id} (provider: {provider})") + else: + _logger.warning(f"[WARNING] Provider {provider} tidak ditemukan di rajaongkir_kurir") + # Set shipping option dan nilai ongkir if selected_option: self.shipping_option_id = selected_option.id self.delivery_amt = selected_option.price -- cgit v1.2.3 From fe458043667bb7f1cde757659fefe0174252002d Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Mon, 2 Jun 2025 01:09:39 +0700 Subject: estimasi barang sampai fix> --- indoteknik_custom/models/sale_order.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index f1280b37..aa1b4b49 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -1057,16 +1057,23 @@ class SaleOrder(models.Model): rec.compute_fullfillment = True - @api.depends('date_order', 'estimated_arrival_days', 'state', 'estimated_arrival_days_start') + @api.depends('expected_ready_to_ship', 'shipping_option_id.etd', 'state') def _compute_eta_date(self): - current_date = datetime.now() - for rec in self: - if rec.date_order and rec.state not in ['cancel'] and rec.estimated_arrival_days and rec.estimated_arrival_days_start: - rec.eta_date = current_date + timedelta(days=rec.estimated_arrival_days) - rec.eta_date_start = current_date + timedelta(days=rec.estimated_arrival_days_start) + for rec in self: + if rec.expected_ready_to_ship and rec.shipping_option_id and rec.shipping_option_id.etd and rec.state not in ['cancel']: + etd_text = rec.shipping_option_id.etd + match = re.match(r"(\d+)\s*-\s*(\d+)", etd_text) + if match: + start_days = int(match.group(1)) + end_days = int(match.group(2)) + rec.eta_date_start = rec.expected_ready_to_ship + timedelta(days=start_days) + rec.eta_date = rec.expected_ready_to_ship + timedelta(days=end_days) + else: + rec.eta_date_start = False + rec.eta_date = False else: - rec.eta_date = False rec.eta_date_start = False + rec.eta_date = False def get_days_until_next_business_day(self, start_date=None, *args, **kwargs): -- cgit v1.2.3 From 04e6162ba44784eb60b1f7122856f5f1a578d16f Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 2 Jun 2025 08:26:20 +0700 Subject: fix margin calculation formula --- indoteknik_custom/models/sale_order.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 3c69c3d1..096ffe3a 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -1724,12 +1724,11 @@ class SaleOrder(models.Model): else: delivery_amt = 0 - # Net margin = total margin - biaya tambahan - net_margin = order.total_margin - order.biaya_lain_lain - denominator = order.amount_untaxed - order.fee_third_party + net_margin = order.total_margin - order.fee_third_party - order.biaya_lain_lain - if denominator > 0: - order.total_percent_margin = round((net_margin / denominator) * 100, 2) + + if order.amount_untaxed > 0: + order.total_percent_margin = round((net_margin / order.amount_untaxed) * 100, 2) else: order.total_percent_margin = 0 -- cgit v1.2.3 From 570555a44d521ebc593490c35e5dfdb30790c201 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 2 Jun 2025 09:10:37 +0700 Subject: fix margin calculation formula --- indoteknik_custom/models/sale_order.py | 1 + 1 file changed, 1 insertion(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 096ffe3a..07e9a97e 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -1693,6 +1693,7 @@ class SaleOrder(models.Model): total_before_margin = sum(line.item_before_margin for line in order.order_line if line.product_id) order.total_before_margin = total_before_margin + # Perhitungan Lama # def _compute_total_percent_margin(self): # for order in self: # if order.amount_untaxed == 0: -- cgit v1.2.3 From c22cbb9f52be68baa089af59dc822cfe56554d70 Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Mon, 2 Jun 2025 09:33:59 +0700 Subject: remove merchandiser from po --- indoteknik_custom/models/purchase_order.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 21ca55eb..240289bf 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -816,7 +816,7 @@ class PurchaseOrder(models.Model): for line in self.order_line: if not line.so_line_id: continue - if line.so_line_id.vendor_id.id != vendor_po and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'): + if line.so_line_id.vendor_id.id != vendor_po: self.env.user.notify_danger( title='WARNING!!!', message="Produk "+line.product_id.name+" memiliki vendor berbeda dengan SO (Vendor PO: "+str(self.partner_id.name)+", Vendor SO: "+str(line.so_line_id.vendor_id.name)+")", @@ -834,7 +834,7 @@ class PurchaseOrder(models.Model): if self.amount_untaxed >= 50000000 and not self.env.user.id == 21: raise UserError("Hanya Rafly Hanggara yang bisa approve") - if self.total_percent_margin < self.total_so_percent_margin and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser') and not self.env.user.is_leader: + if self.total_percent_margin < self.total_so_percent_margin: self.env.user.notify_danger( title='WARNING!!!', message='Beda Margin dengan Sale Order', @@ -1002,7 +1002,7 @@ class PurchaseOrder(models.Model): self.approval_status_unlock = 'approvedFinance' else: raise UserError("Bisa langsung Confirm, menunggu persetujuan Finance jika ingin unlock PO") - elif self.env.user.is_leader or self.env.user.has_group('indoteknik_custom.group_role_merchandiser'): + elif self.env.user.is_leader or self.env.user.has_group('indoteknik_custom.group_role_purchasing'): raise UserError("Bisa langsung Confirm") elif self.total_percent_margin == self.total_so_percent_margin and self.matches_so and not greater_than_plafon and not different_vendor_message: raise UserError("Bisa langsung Confirm") -- cgit v1.2.3 From 9e71d7ac3e018e9d4415e1341231671e62fbdb45 Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Mon, 2 Jun 2025 11:19:21 +0700 Subject: fix hours shipping method --- indoteknik_custom/models/sale_order.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index a76d8011..a86d43cb 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -1068,13 +1068,33 @@ class SaleOrder(models.Model): def _compute_eta_date(self): for rec in self: if rec.expected_ready_to_ship and rec.shipping_option_id and rec.shipping_option_id.etd and rec.state not in ['cancel']: - etd_text = rec.shipping_option_id.etd - match = re.match(r"(\d+)\s*-\s*(\d+)", etd_text) + etd_text = rec.shipping_option_id.etd.strip().lower() + match = re.match(r"(\d+)\s*-\s*(\d+)\s*(days?|hours?)", etd_text) + single_match = re.match(r"(\d+)\s*(days?|hours?)", etd_text) + if match: - start_days = int(match.group(1)) - end_days = int(match.group(2)) - rec.eta_date_start = rec.expected_ready_to_ship + timedelta(days=start_days) - rec.eta_date = rec.expected_ready_to_ship + timedelta(days=end_days) + start_val = int(match.group(1)) + end_val = int(match.group(2)) + unit = match.group(3) + + if 'hour' in unit: + rec.eta_date_start = rec.expected_ready_to_ship + timedelta(hours=start_val) + rec.eta_date = rec.expected_ready_to_ship + timedelta(hours=end_val) + else: + rec.eta_date_start = rec.expected_ready_to_ship + timedelta(days=start_val) + rec.eta_date = rec.expected_ready_to_ship + timedelta(days=end_val) + + elif single_match: + val = int(single_match.group(1)) + unit = single_match.group(2) + + if 'hour' in unit: + rec.eta_date_start = rec.expected_ready_to_ship + timedelta(hours=val) + rec.eta_date = rec.expected_ready_to_ship + timedelta(hours=val) + else: + rec.eta_date_start = rec.expected_ready_to_ship + timedelta(days=val) + rec.eta_date = rec.expected_ready_to_ship + timedelta(days=val) + else: rec.eta_date_start = False rec.eta_date = False -- cgit v1.2.3 From 43d180117e90db9115f07ab4b5b2880c32594bea Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 2 Jun 2025 12:09:27 +0700 Subject: (andri) fix date tracking --- indoteknik_custom/models/stock_picking.py | 34 +++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 4517a941..a2935a07 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1570,7 +1570,8 @@ class StockPicking(models.Model): dt = False try: - dt = picking._convert_to_local_time(dt_str) + dt = picking._convert_to_utc_datetime(dt_str) + _logger.info(f"[Biteship Sync] Berhasil parse datetime: {dt_str} -> {dt}") except Exception as e: _logger.warning(f"[Biteship Sync] Gagal parse datetime: {e}") continue @@ -1584,7 +1585,12 @@ class StockPicking(models.Model): # Buat log unik dengan waktu lokal Asia/Jakarta if dt and desc: - dt_local = pytz.utc.localize(dt).astimezone(pytz.timezone("Asia/Jakarta")) + try: + dt_local = parser.parse(dt_str).replace(tzinfo=None) + except Exception as e: + _logger.warning(f"[Biteship Sync] Gagal parse dt_str untuk log: {e}") + dt_local = dt # fallback + desc_clean = ' '.join(desc.strip().split()) log_line = f"[TRACKING] {status} - {dt_local.strftime('%d %b %Y %H:%M')}: {desc_clean}" if not picking._has_existing_log(log_line): @@ -1605,10 +1611,24 @@ class StockPicking(models.Model): """, (self._name, self.id, f"%{log_line}%")) return self.env.cr.fetchone() is not None + # Untuk internal Odoo (mengembalikan naive UTC datetime untuk disimpan ke DB) + def _convert_to_utc_datetime(self, iso_date): + try: + if isinstance(iso_date, str): + waktu = parser.parse(iso_date) + else: + waktu = iso_date + if waktu.tzinfo is None: + waktu = waktu.replace(tzinfo=pytz.utc) + utc_dt = waktu.astimezone(pytz.utc).replace(tzinfo=None) + return utc_dt + except Exception as e: + _logger.warning(f"[Biteship] Gagal konversi waktu UTC: {e}") + return False + + # Untuk tampilan di API atau kebutuhan web (mengembalikan string waktu lokal) def _convert_to_local_time(self, iso_date): try: - from dateutil import parser - import pytz if isinstance(iso_date, str): waktu = parser.parse(iso_date) else: @@ -1617,11 +1637,9 @@ class StockPicking(models.Model): waktu = waktu.replace(tzinfo=pytz.utc) local_tz = pytz.timezone("Asia/Jakarta") local_dt = waktu.astimezone(local_tz) - utc_dt = local_dt.astimezone(pytz.utc).replace(tzinfo=None) - return utc_dt + return local_dt.strftime("%Y-%m-%d %H:%M:%S") except Exception as e: - _logger.warning(f"[Biteship] Gagal konversi waktu lokal: {e}") - return False + return str(e) def _map_status_biteship(self, status): status_mapping = { -- cgit v1.2.3 From 01029f94471df384decb606dbb3088f31a5a8108 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 3 Jun 2025 09:49:28 +0700 Subject: push --- indoteknik_custom/models/stock_picking.py | 1 + 1 file changed, 1 insertion(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index a215eb74..3921ed5a 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -584,6 +584,7 @@ class StockPicking(models.Model): self.lalamove_phone = phone self.lalamove_status = pod.get("status") self.lalamove_delivered_at = delivered_at_dt + self.driver_arrival_date = delivered_at_dt return data raise UserError("No delivered data found in Lalamove response.") -- cgit v1.2.3 From d6f3060b46582ddd78f596ce3527871cae1b2b46 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 3 Jun 2025 14:08:30 +0700 Subject: (andri) ganti referensi dari no SO ke no BU OUT --- indoteknik_custom/models/stock_picking.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index a2935a07..39872ecb 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -568,13 +568,13 @@ class StockPicking(models.Model): _logger.info(f"Items data standard: {items_data_standard}") _logger.info(f"Items data instant: {items_data_instant}") - # Bangun payload dasar + payload = { "origin_coordinate": { "latitude": -6.3031123, "longitude": 106.7794934999 }, - "reference_id": self.sale_id.name, + "reference_id": self.name, # PERUBAHAN: Gunakan nomor BU/OUT "shipper_contact_name": self.carrier_id.pic_name or '', "shipper_contact_phone": self.carrier_id.pic_phone or '', "shipper_organization": self.carrier_id.name, @@ -587,6 +587,7 @@ class StockPicking(models.Model): "destination_address": self.real_shipping_id.street, "destination_postal_code": self.real_shipping_id.zip, "origin_note": "BELAKANG INDOMARET", + "destination_note": f"SO: {self.sale_id.name}", # PERUBAHAN: Tambahkan SO ke note "courier_type": self.sale_id.delivery_service_type or "reg", "courier_company": self.carrier_id.name.lower(), "delivery_type": "now", @@ -640,7 +641,9 @@ class StockPicking(models.Model): body=f"Biteship berhasil dilakukan.
" f"Kurir: {self.carrier_id.name}
" f"Tracking ID: {self.biteship_tracking_id or '-'}
" - f"Resi: {waybill_id or '-'}", + f"Resi: {waybill_id or '-'}
" + f"Reference: {self.name}
" + f"SO: {self.sale_id.name}", message_type="comment" ) -- cgit v1.2.3 From b519e0fdac46a64ffef87d27ba824038147d831b Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 3 Jun 2025 15:36:08 +0700 Subject: (andri) memastikan order sesuai dengan informasi barang yang ingin dikirim --- indoteknik_custom/models/stock_picking.py | 98 ++++++++++++------------------- 1 file changed, 39 insertions(+), 59 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 39872ecb..cdff6e32 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -526,7 +526,6 @@ class StockPicking(models.Model): if self.biteship_tracking_id: raise UserError(f"Order ini sudah dikirim ke Biteship. Dengan Tracking Id: {self.biteship_tracking_id}") - # Fungsi bantu: menentukan apakah kurir perlu koordinat def is_courier_need_coordinates(service_code): return service_code in [ "instant", "same_day", "instant_car", @@ -534,47 +533,42 @@ class StockPicking(models.Model): "cdd_bak", "cdd_box", "engkel_box", "engkel_bak" ] - # Ambil order line - products = self.env['sale.order.line'].search([('order_id', '=', self.sale_id.id)]) - - # Bangun data items untuk standard - def build_items_data(lines): - return [{ - "name": line.product_id.name, - "description": line.name, - "value": line.price_unit, - "quantity": line.product_uom_qty, - "weight": line.weight*1000 - } for line in lines] - - items_data_standard = build_items_data(products) + # ✅ Ambil item dari move_line_ids_with_package (qty_done > 0) + items = [] + for ml in self.move_line_ids_without_package: + if ml.qty_done <= 0: + continue - # Bangun data items untuk pengiriman instant - items_data_instant = [] - for move_line in self.move_line_ids_without_package: - order_line = self.env['sale.order.line'].search([ + product = ml.product_id + weight = product.weight or 0.1 # default minimal + line = ml.sale_line_id or self.env['sale.order.line'].search([ ('order_id', '=', self.sale_id.id), - ('product_id', '=', move_line.product_id.id) + ('product_id', '=', product.id) ], limit=1) - if order_line: - items_data_instant.append({ - "name": order_line.product_id.name, - "description": order_line.name, - "value": order_line.price_unit, - "quantity": move_line.qty_done, - "weight": order_line.weight*1000 - }) + value = line.price_unit if line else 0 + description = line.name if line else product.name + + items.append({ + "name": product.name, + "description": description, + "value": value, + "quantity": ml.qty_done, + "weight": int(weight * 1000), + }) - _logger.info(f"Items data standard: {items_data_standard}") - _logger.info(f"Items data instant: {items_data_instant}") + if not items: + raise UserError("Pengiriman tidak dapat dilakukan karena tidak ada barang yang divalidasi (qty_done = 0).") + + shipping_partner = self.real_shipping_id + courier_service_code = self.sale_id.delivery_service_type or "reg" payload = { "origin_coordinate": { "latitude": -6.3031123, "longitude": 106.7794934999 }, - "reference_id": self.name, # PERUBAHAN: Gunakan nomor BU/OUT + "reference_id": self.name, "shipper_contact_name": self.carrier_id.pic_name or '', "shipper_contact_phone": self.carrier_id.pic_phone or '', "shipper_organization": self.carrier_id.name, @@ -582,38 +576,26 @@ class StockPicking(models.Model): "origin_contact_phone": "081717181922", "origin_address": "Jl. Bandengan Utara Komp A & BRT. Penjaringan, Kec. Penjaringan, Jakarta (BELAKANG INDOMARET) KOTA JAKARTA UTARA PENJARINGAN", "origin_postal_code": 14440, - "destination_contact_name": self.real_shipping_id.name, - "destination_contact_phone": self.real_shipping_id.phone or self.real_shipping_id.mobile, - "destination_address": self.real_shipping_id.street, - "destination_postal_code": self.real_shipping_id.zip, + "destination_contact_name": shipping_partner.name, + "destination_contact_phone": shipping_partner.phone or shipping_partner.mobile, + "destination_address": shipping_partner.street, + "destination_postal_code": shipping_partner.zip, "origin_note": "BELAKANG INDOMARET", - "destination_note": f"SO: {self.sale_id.name}", # PERUBAHAN: Tambahkan SO ke note - "courier_type": self.sale_id.delivery_service_type or "reg", + "destination_note": f"SO: {self.sale_id.name}", + "courier_type": courier_service_code, "courier_company": self.carrier_id.name.lower(), "delivery_type": "now", - "items": items_data_standard + "items": items } - _logger.info(f"Delivery service type: {self.sale_id.delivery_service_type}") - _logger.info(f"Carrier: {self.carrier_id.name}") - _logger.info(f"Payload awal: {payload}") - - # Tambahkan destination_coordinate jika diperlukan - if is_courier_need_coordinates(self.sale_id.delivery_service_type): - if not self.real_shipping_id.latitude or not self.real_shipping_id.longtitude: + if is_courier_need_coordinates(courier_service_code): + if not shipping_partner.latitude or not shipping_partner.longtitude: raise UserError("Alamat tujuan tidak memiliki koordinat (latitude/longitude).") - # items_to_use = items_data_instant if items_data_instant else items_data_standard - if not items_data_instant: - raise UserError("Pengiriman instant membutuhkan produk yang sudah diproses (qty_done > 0). Harap lakukan validasi picking terlebih dahulu.") - - payload.update({ - "destination_coordinate": { - "latitude": self.real_shipping_id.latitude, - "longitude": self.real_shipping_id.longtitude, - }, - "items": items_data_instant - }) + payload["destination_coordinate"] = { + "latitude": shipping_partner.latitude, + "longitude": shipping_partner.longtitude, + } _logger.info(f"Payload untuk Biteship: {payload}") @@ -633,7 +615,7 @@ class StockPicking(models.Model): self.biteship_id = data.get("id", "") self.biteship_tracking_id = data.get("courier", {}).get("tracking_id", "") self.biteship_waybill_id = data.get("courier", {}).get("waybill_id", "") - self.delivery_tracking_no = data.get("courier", {}).get("waybill_id", "") + self.delivery_tracking_no = self.biteship_waybill_id waybill_id = self.biteship_waybill_id @@ -656,13 +638,11 @@ class StockPicking(models.Model): 'type': 'rainbow_man', } } - else: error_data = response.json() error_message = error_data.get("error", "Unknown error") error_code = error_data.get("code", "No code provided") raise UserError(f"Error saat mengirim ke Biteship: {error_message} (Code: {error_code})") - @api.constrains('driver_departure_date') def constrains_driver_departure_date(self): -- cgit v1.2.3 From 8a9c08a21fd7d2ac63ef849d9417b11563092f0d Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 3 Jun 2025 21:10:12 +0700 Subject: (andri) perbaikan biteship pada stock picking --- indoteknik_custom/models/stock_picking.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index cdff6e32..7e001299 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -541,9 +541,9 @@ class StockPicking(models.Model): product = ml.product_id weight = product.weight or 0.1 # default minimal - line = ml.sale_line_id or self.env['sale.order.line'].search([ + line = ml.move_id.sale_line_id or self.env['sale.order.line'].search([ ('order_id', '=', self.sale_id.id), - ('product_id', '=', product.id) + ('product_id', '=', ml.product_id.id) ], limit=1) value = line.price_unit if line else 0 -- cgit v1.2.3 From e43e3d0da19e5cbbcbadec40c252c24d2921149c Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 4 Jun 2025 08:52:38 +0700 Subject: (andri) fix bug SLA pada quotation SO --- indoteknik_custom/models/sale_order.py | 172 ++++++++++++++++++++------------- 1 file changed, 106 insertions(+), 66 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index a86d43cb..3e340c60 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -306,78 +306,71 @@ class SaleOrder(models.Model): @api.onchange('carrier_id') def _onchange_carrier_id(self): - # Jika record masih baru (belum disimpan), jangan jalankan onchange + # ─────────────────────────────────────────────────────────────── + # 1. abaikan onchange kalau SO masih draft / belum tersimpan + # ─────────────────────────────────────────────────────────────── if not self._origin or not self._origin.id: return + sale_order_id = self._origin.id # id SO asli (sudah tersimpan) + + # ─────────────────────────────────────────────────────────────── + # 2. Jika SO BELUM mempunyai satupun shipping.option ⇒ + # jangan lakukan validasi apa-apa; cukup reset field & domain + # ─────────────────────────────────────────────────────────────── + total_so_options = self.env['shipping.option'].search_count( + [('sale_order_id', '=', sale_order_id)] + ) + if total_so_options == 0: + # belum pernah estimasi ongkir ⇒ biarkan user ganti carrier + self.shipping_option_id = False + return {'domain': {'shipping_option_id': [('id', '=', -1)]}} + + # ─────────────────────────────────────────────────────────────── + # 3. (kode lama) – mulai validasi hanya jika sudah ada option + # ─────────────────────────────────────────────────────────────── self.shipping_option_id = False - # self.delivery_amt = 0 - # self.delivery_service_type = False if not self.carrier_id: return {'domain': {'shipping_option_id': [('id', '=', -1)]}} - # Ambil provider dari mapping + # cari provider dari mapping rajaongkir_kurir self.env.cr.execute(""" - SELECT name FROM rajaongkir_kurir + SELECT name FROM rajaongkir_kurir WHERE delivery_carrier_id = %s LIMIT 1 """, (self.carrier_id.id,)) - result = self.env.cr.fetchone() - provider = result[0].lower() if result and result[0] else False - - if not provider: - provider = self.carrier_id.name.lower().split()[0] if self.carrier_id.name else False + row = self.env.cr.fetchone() + provider = row[0].lower() if row and row[0] else ( + self.carrier_id.name.lower().split()[0] if self.carrier_id.name else False + ) _logger.info(f"[Carrier Changed] {self.carrier_id.name}, Detected Provider: {provider}") - sale_order_id = self._origin.id - - # Cek apakah ada shipping_option yang cocok + # hitung berapa option yg match provider BARU self.env.cr.execute(""" SELECT COUNT(*) FROM shipping_option WHERE LOWER(provider) LIKE %s AND sale_order_id = %s """, (f'%{provider}%', sale_order_id)) - count = self.env.cr.fetchone()[0] - - _logger.info(f"[Shipping Option Count] Provider: {provider} | SO ID: {sale_order_id} | Count: {count}") - - if count == 0: - previous_carrier = self._origin.carrier_id if self._origin else False - previous_provider = False - - self.carrier_id = previous_carrier - self.shipping_option_id = self._origin.shipping_option_id if self._origin else False - - # Ambil kembali domain provider sebelumnya - if previous_carrier: - self.env.cr.execute(""" - SELECT name FROM rajaongkir_kurir - WHERE delivery_carrier_id = %s LIMIT 1 - """, (previous_carrier.id,)) - prev_row = self.env.cr.fetchone() - previous_provider = prev_row[0].lower() if prev_row and prev_row[0] else previous_carrier.name.lower().split()[0] - - fallback_domain = [('id', '=', -1)] - if previous_provider: - fallback_domain = [ - '|', - '&', ('sale_order_id', '=', sale_order_id), ('provider', 'ilike', f'%{previous_provider}%'), - '&', ('sale_order_id', '=', False), ('provider', 'ilike', f'%{previous_provider}%') - ] + matched = self.env.cr.fetchone()[0] or 0 + if matched == 0: + # provider baru tidak ada di option yang SUDAH dibuat → kembalikan ke carrier lama + prev_carrier = self._origin.carrier_id + self.carrier_id = prev_carrier + self.shipping_option_id = self._origin.shipping_option_id or False return { 'warning': { 'title': "Shipping Option Tidak Ditemukan", 'message': ( - f"Layanan kurir tidak tersedia untuk pengiriman ini.\n" - f"Pilihan dikembalikan ke sebelumnya." + "Layanan kurir tidak tersedia untuk pengiriman ini.\n" + "Pilihan dikembalikan ke sebelumnya." ) }, - 'domain': {'shipping_option_id': fallback_domain} + 'domain': {'shipping_option_id': [('id', '=', -1)]} } - # Jika data ada, kembalikan domain biasa + # kalau match ada → set domain normal (hanya option dengan provider itu) domain = [ '|', '&', ('sale_order_id', '=', sale_order_id), ('provider', 'ilike', f'%{provider}%'), @@ -1230,30 +1223,77 @@ class SaleOrder(models.Model): self._calculate_etrts_date() + # def _validate_expected_ready_ship_date(self): + # for rec in self: + # if not rec.order_line: + # _logger.info("⏩ Lewati validasi ERTS karena belum ada produk.") + # return # Lewati validasi jika belum ada produk + + # now = fields.Datetime.now() + # expected_date = rec.expected_ready_to_ship and rec.expected_ready_to_ship.date() or None + # if not expected_date: + # return # Tidak validasi jika tidak ada input sama sekali + + # sla = rec.calculate_sla_by_vendor() + # offset_day, lewat_jam_3 = rec.get_days_until_next_business_day() + # eta_minimum = now + timedelta(days=sla + offset_day) + + # if expected_date < eta_minimum.date(): + # rec.expected_ready_to_ship = eta_minimum + # raise ValidationError( + # "Tanggal 'Expected Ready to Ship' tidak boleh lebih kecil dari {}. Mohon pilih tanggal minimal {}." + # .format(eta_minimum.strftime('%d-%m-%Y'), eta_minimum.strftime('%d-%m-%Y')) + # ) + def _validate_expected_ready_ship_date(self): + """ + Pastikan expected_ready_to_ship tidak lebih awal dari SLA minimum. + Dipanggil setiap onchange / simpan SO. + """ for rec in self: - if rec.expected_ready_to_ship and rec.commitment_date: - current_date = datetime.now() - # Hanya membandingkan tanggal saja, tanpa jam - expected_date = rec.expected_ready_to_ship.date() - - max_slatime = 1 # Default SLA jika tidak ada - slatime = self.calculate_sla_by_vendor(rec.order_line) - max_slatime = max(max_slatime, slatime['slatime']) - - offset , is3pm = self.get_days_until_next_business_day(current_date) - sum_days = max_slatime + offset - sum_days -= 1 - eta_minimum = current_date + timedelta(days=sum_days) - - if expected_date < eta_minimum.date(): - rec.expected_ready_to_ship = eta_minimum - raise ValidationError( - "Tanggal 'Expected Ready to Ship' tidak boleh lebih kecil dari {}. Mohon pilih tanggal minimal {}." - .format(eta_minimum.strftime('%d-%m-%Y'), eta_minimum.strftime('%d-%m-%Y')) - ) - else: - rec.commitment_date = rec.expected_ready_to_ship + # ───────────────────────────────────────────────────── + # 1. Hanya validasi kalau field sudah terisi + # (quotation baru / belum ada tanggal → abaikan) + # ───────────────────────────────────────────────────── + if not rec.expected_ready_to_ship: + continue + + current_date = datetime.now() + + # ───────────────────────────────────────────────────── + # 2. Hitung SLA berdasarkan product lines (jika ada) + # ───────────────────────────────────────────────────── + products = rec.order_line + if products: + sla_data = rec.calculate_sla_by_vendor(products) + max_sla_time = sla_data.get('slatime', 1) + else: + # belum ada item → gunakan default 1 hari + max_sla_time = 1 + + # offset hari libur / weekend + offset, is3pm = rec.get_days_until_next_business_day(current_date) + min_days = max_sla_time + offset - 1 + eta_minimum = current_date + timedelta(days=min_days) + + # ───────────────────────────────────────────────────── + # 3. Validasi - raise error bila terlalu cepat + # ───────────────────────────────────────────────────── + if rec.expected_ready_to_ship.date() < eta_minimum.date(): + # set otomatis ke tanggal minimum supaya user tidak perlu + # menekan Save dua kali + rec.expected_ready_to_ship = eta_minimum + + raise ValidationError( + _("Tanggal 'Expected Ready to Ship' tidak boleh " + "lebih kecil dari %(tgl)s. Mohon pilih minimal %(tgl)s.") + % {'tgl': eta_minimum.strftime('%d-%m-%Y')} + ) + else: + # sinkronkan ke field commitment_date + rec.commitment_date = rec.expected_ready_to_ship + + @api.onchange('expected_ready_to_ship') #Hangle Onchange form Expected Ready to Ship -- cgit v1.2.3 From e846e6fef3274839e9f9b21984a753205d31970e Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 4 Jun 2025 15:21:27 +0700 Subject: unify write method --- indoteknik_custom/models/sale_order.py | 174 +++++++++++++++------------------ 1 file changed, 78 insertions(+), 96 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 07e9a97e..705d16ef 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -866,7 +866,6 @@ class SaleOrder(models.Model): def _validate_delivery_amt(self): is_indoteknik = self.carrier_id.id == 1 or self.shipping_cost_covered == 'indoteknik' is_active_id = not self.env.context.get('active_id', []) - if is_indoteknik and is_active_id: if self.delivery_amt == 0: if self.carrier_id.id == 1: @@ -1743,11 +1742,7 @@ class SaleOrder(models.Model): # self.total_margin_excl_third_party = round( # (original_net_margin / (self.amount_untaxed - self.fee_third_party)) * 100, 2) - def write(self, vals): - import logging - _logger = logging.getLogger(__name__) - - # Simpan nilai margin sebelumnya untuk field yang mempengaruhi perhitungan + def _prepare_before_margin_values(self, vals): margin_sebelumnya = {} margin_affecting_fields = [ @@ -1755,99 +1750,37 @@ class SaleOrder(models.Model): 'ongkir_ke_xpdc', 'shipping_cost_covered', 'order_line' ] - if any(field in vals for field in margin_affecting_fields): - for order in self: - if order.amount_untaxed > 0: - # LOGIC PENTING: Kapan Before Margin harus diupdate? - - current_before = order.total_margin_excl_third_party or 0 - - # CASE 1: Before margin masih kosong, simpan margin saat ini - if current_before == 0: - margin_sebelumnya[order.id] = order.total_percent_margin - _logger.info( - f"CASE 1 - SO {order.name}: Before margin kosong, simpan {order.total_percent_margin}%") + if not any(field in vals for field in margin_affecting_fields): + return {} - # CASE 2: Ada perubahan biaya_lain_lain - elif 'biaya_lain_lain' in vals: - old_biaya = order.biaya_lain_lain or 0 - new_biaya = vals['biaya_lain_lain'] or 0 - - _logger.info(f"SO {order.name}: Biaya lain-lain berubah dari {old_biaya} ke {new_biaya}") - - # Jika sebelumnya tidak ada biaya_lain_lain, dan sekarang ada - if old_biaya == 0 and new_biaya > 0: - # Simpan margin saat ini sebagai "before margin" - margin_sebelumnya[order.id] = order.total_percent_margin - _logger.info(f"Menyimpan before margin: {order.total_percent_margin}%") - - # Jika biaya_lain_lain dihapus (dari ada jadi 0) - elif old_biaya > 0 and new_biaya == 0: - # Before margin tetap tidak berubah (sudah tersimpan sebelumnya) - _logger.info(f"Biaya dihapus, before margin tetap: {current_before}%") - # TIDAK mengubah before margin - - # CASE 3: Perubahan field lain (fee_third_party, dll) - elif any(field in vals for field in - ['fee_third_party', 'delivery_amt', 'ongkir_ke_xpdc', 'order_line']): - # Simpan margin saat ini sebelum perubahan - margin_sebelumnya[order.id] = order.total_percent_margin - _logger.info(f"CASE 3 - Field lain berubah, simpan {order.total_percent_margin}%") - - # Validasi dan proses lainnya... for order in self: - if order.state in ['sale', 'cancel']: - if 'order_line' in vals: - new_lines = vals.get('order_line', []) - for command in new_lines: - if command[0] == 0: - raise UserError( - "SO tidak dapat ditambahkan produk baru karena SO sudah menjadi sale order.") - - if 'carrier_id' in vals: - for order in self: - for picking in order.picking_ids: - if picking.state == 'assigned': - picking.carrier_id = vals['carrier_id'] - - try: - helper_ids = self._get_helper_ids() - if str(self.env.user.id) in helper_ids: - vals['helper_by_id'] = self.env.user.id - except: - pass - - # Jalankan super write - res = super(SaleOrder, self).write(vals) - - # Update before margin jika diperlukan - if margin_sebelumnya: - for order_id, margin_value in margin_sebelumnya.items(): - _logger.info(f"Updating before margin untuk SO {order_id}: {margin_value}%") - - self.env.cr.execute(""" - UPDATE sale_order - SET total_margin_excl_third_party = %s - WHERE id = %s - """, (margin_value, order_id)) - - self.env.cr.commit() - self.invalidate_cache(['total_margin_excl_third_party']) - - # Validasi lainnya - if any(field in vals for field in ['delivery_amt', 'carrier_id', 'shipping_cost_covered']): - try: - self._validate_delivery_amt() - except: - pass + if order.amount_untaxed <= 0: + continue - if any(field in vals for field in ["order_line", "client_order_ref"]): - try: - self._calculate_etrts_date() - except: - pass + current_before = order.total_margin_excl_third_party or 0 + + # CASE 1: Before margin masih kosong → ambil dari item_percent_margin + if current_before == 0: + line_margin = 0 + for line in order.order_line: + if line.item_percent_margin is not None: + line_margin = line.item_percent_margin + break + margin_sebelumnya[order.id] = line_margin + _logger.info(f"[BEFORE] SO {order.name}: Before margin kosong, ambil dari order line: {line_margin}%") + else: + # CASE 2: Ada perubahan field yang mempengaruhi margin + for field in margin_affecting_fields: + if field in vals: + old_val = getattr(order, field, 0) or 0 + new_val = vals[field] or 0 + if old_val != new_val: + margin_sebelumnya[order.id] = order.total_percent_margin + _logger.info( + f"[BEFORE] SO {order.name}: {field} berubah dari {old_val} ke {new_val}, simpan {order.total_percent_margin}%") + break - return res + return margin_sebelumnya @api.onchange('sales_tax_id') def onchange_sales_tax_id(self): @@ -2184,4 +2117,53 @@ class SaleOrder(models.Model): else: order.ready_to_ship_status_detail = "On Track" else: - order.ready_to_ship_status_detail = 'On Track' \ No newline at end of file + order.ready_to_ship_status_detail = 'On Track' + + def write(self, vals): + + margin_sebelumnya = self._prepare_before_margin_values(vals) + + for order in self: + if order.state in ['sale', 'cancel']: + if 'order_line' in vals: + for command in vals.get('order_line', []): + if command[0] == 0: + raise UserError( + "SO tidak dapat ditambahkan produk baru karena SO sudah menjadi sale order.") + + if 'carrier_id' in vals: + for order in self: + for picking in order.picking_ids: + if picking.state == 'assigned': + picking.carrier_id = vals['carrier_id'] + + try: + helper_ids = self._get_helper_ids() + if str(self.env.user.id) in helper_ids: + vals['helper_by_id'] = self.env.user.id + except: + pass + + res = super(SaleOrder, self).write(vals) + + # Update before margin setelah write + if margin_sebelumnya: + for order_id, margin_value in margin_sebelumnya.items(): + _logger.info(f"[UPDATE] SO ID {order_id}: Set before margin ke {margin_value}%") + self.env.cr.execute(""" + UPDATE sale_order + SET total_margin_excl_third_party = %s + WHERE id = %s + """, (margin_value, order_id)) + + self.env.cr.commit() + self.invalidate_cache(['total_margin_excl_third_party']) + + # Validasi setelah write + if any(field in vals for field in ['delivery_amt', 'carrier_id', 'shipping_cost_covered']): + self._validate_delivery_amt() + + if any(field in vals for field in ["order_line", "client_order_ref"]): + self._calculate_etrts_date() + + return res \ No newline at end of file -- cgit v1.2.3 From fa2c8cc7b00e963e740307484c174961f61ffc84 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 5 Jun 2025 08:35:39 +0700 Subject: (andri) add field Journal Uang Muka di PO dan Purchase Order pada CAB --- indoteknik_custom/models/account_move.py | 11 +++++++++++ indoteknik_custom/models/purchase_order.py | 2 +- indoteknik_custom/models/uangmuka_pembelian.py | 2 ++ 3 files changed, 14 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 30de67be..24cedc34 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -68,6 +68,17 @@ class AccountMove(models.Model): purchase_order_id = fields.Many2one('purchase.order', string='Purchase Order') length_of_payment = fields.Integer(string="Length of Payment", compute='compute_length_of_payment') + def name_get(self): + result = [] + for move in self: + if move.move_type == 'entry': + # Tampilkan nomor CAB (name) + result.append((move.id, move.name)) + else: + # Gunakan default display + result.append((move.id, move.display_name)) + return result + def compute_length_of_payment(self): for rec in self: payment_term = rec.invoice_payment_term_id.line_ids[0].days diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 240289bf..0696dd9f 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -65,7 +65,7 @@ class PurchaseOrder(models.Model): sale_order = fields.Char(string='Sale Order') matches_so = fields.Many2many('sale.order', string='Matches SO', compute='_compute_matches_so') is_create_uangmuka = fields.Boolean(string='Uang Muka?') - move_id = fields.Many2one('account.move', string='Account Move') + move_id = fields.Many2one('account.move', string='Journal Entries Uang Muka', domain=[('move_type', '=', 'entry')]) logbook_bill_id = fields.Many2one('report.logbook.bill', string='Logbook Bill') status_printed = fields.Selection([ ('not_printed', 'Belum Print'), diff --git a/indoteknik_custom/models/uangmuka_pembelian.py b/indoteknik_custom/models/uangmuka_pembelian.py index ba41f814..13d51dcf 100644 --- a/indoteknik_custom/models/uangmuka_pembelian.py +++ b/indoteknik_custom/models/uangmuka_pembelian.py @@ -57,6 +57,8 @@ class UangmukaPembelian(models.TransientModel): account_move = request.env['account.move'].create([param_header]) _logger.info('Success Create Uang Muka Pembelian %s' % account_move.name) + account_move.purchase_order_id = order.id # isi field purchase_order_id + if order.partner_id.parent_id: partner_id = order.partner_id.parent_id.id else: -- cgit v1.2.3 From c43cba9dd205a3f08343f79233d3af6f5ab189ff Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 5 Jun 2025 08:47:31 +0700 Subject: (andri) penyesuaian value field CAB ketika masi dalam keadaan draft --- indoteknik_custom/models/account_move.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 24cedc34..29368089 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -72,10 +72,14 @@ class AccountMove(models.Model): result = [] for move in self: if move.move_type == 'entry': - # Tampilkan nomor CAB (name) - result.append((move.id, move.name)) + # Jika masih draft, tampilkan 'Draft CAB' + if move.state == 'draft': + label = 'Draft CAB' + else: + label = move.name + result.append((move.id, label)) else: - # Gunakan default display + # Untuk invoice dan lainnya, pakai default result.append((move.id, move.display_name)) return result -- cgit v1.2.3 From 2b4d72b6157a438f0a77be8aaae6875c2b415392 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Thu, 5 Jun 2025 09:53:57 +0700 Subject: change acccess approve rpo --- indoteknik_custom/models/requisition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/requisition.py b/indoteknik_custom/models/requisition.py index 25133e72..6d10f511 100644 --- a/indoteknik_custom/models/requisition.py +++ b/indoteknik_custom/models/requisition.py @@ -87,7 +87,7 @@ class Requisition(models.Model): if self.env.user.id == 19 or self.env.user.id == 28: self.sales_approve = True elif self.env.user.id == 21 or self.env.user.id == 28: - if not self.sales_approve: + if not self.sales_approve and not self.env.user.id == 21: raise UserError('Darren Belum Approve') self.merchandise_approve = True -- cgit v1.2.3 From 9b47b9b9c09e2a04bf3be0e81c0931e825500ea4 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Thu, 5 Jun 2025 10:07:27 +0700 Subject: cr change access approve rpo --- indoteknik_custom/models/requisition.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/requisition.py b/indoteknik_custom/models/requisition.py index 6d10f511..75529378 100644 --- a/indoteknik_custom/models/requisition.py +++ b/indoteknik_custom/models/requisition.py @@ -87,15 +87,11 @@ class Requisition(models.Model): if self.env.user.id == 19 or self.env.user.id == 28: self.sales_approve = True elif self.env.user.id == 21 or self.env.user.id == 28: - if not self.sales_approve and not self.env.user.id == 21: - raise UserError('Darren Belum Approve') self.merchandise_approve = True def create_po_from_requisition(self): - if not self.sales_approve: - raise UserError('Harus Di Approve oleh Darren') - if not self.merchandise_approve: - raise UserError('Harus Di Approve oleh Rafly') + if not self.sales_approve and not self.merchandise_approve: + raise UserError('Harus Di Approve oleh Darren atau Rafly') if not self.requisition_lines: raise UserError('Tidak ada Lines, belum bisa create PO') if self.is_po: -- cgit v1.2.3 From 9a3b6feba01b1d27524484f5e612e49412942414 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Thu, 5 Jun 2025 10:12:14 +0700 Subject: push --- indoteknik_custom/models/requisition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/requisition.py b/indoteknik_custom/models/requisition.py index 75529378..bcdafb12 100644 --- a/indoteknik_custom/models/requisition.py +++ b/indoteknik_custom/models/requisition.py @@ -48,8 +48,8 @@ class Requisition(models.Model): is_po = fields.Boolean(string='Is PO') requisition_match = fields.One2many('requisition.purchase.match', 'requisition_id', string='Matches', auto_join=True) sale_order_id = fields.Many2one('sale.order', string='SO', help='harus diisi nomor SO yang ingin digenerate') - sales_approve = fields.Boolean(string='Sales Approve', tracking=3, copy=False) - merchandise_approve = fields.Boolean(string='Merchandise Approve', tracking=3, copy=False) + sales_approve = fields.Boolean(string='Approval Status', tracking=3, copy=False) + merchandise_approve = fields.Boolean(string='Approval Status', tracking=3, copy=False) def generate_requisition_from_so(self): state = ['done', 'sale'] -- cgit v1.2.3 From 55fb073416a7f8504c3819178aa7b6883304335f Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 5 Jun 2025 10:53:30 +0700 Subject: (andri) merubah peletakan CAB menjadi berada di atas sebelah tombol receipt --- indoteknik_custom/models/purchase_order.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 0696dd9f..27f1aebd 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -89,6 +89,20 @@ class PurchaseOrder(models.Model): store_name = fields.Char(string='Nama Toko') purchase_order_count = fields.Integer('Purchase Order Count', related='partner_id.purchase_order_count') + def action_view_journal_uangmuka(self): + self.ensure_one() + if not self.move_id: + raise UserError("Journal Uang Muka belum tersedia.") + + return { + 'type': 'ir.actions.act_window', + 'name': 'Journal Entry', + 'res_model': 'account.move', + 'res_id': self.move_id.id, + 'view_mode': 'form', + 'target': 'current', + } + # cek payment term def _check_payment_term(self): _logger.info("Check Payment Term Terpanggil") -- cgit v1.2.3 From add53c91a49d43b20438b14cf48aaececbabf2cd Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 5 Jun 2025 11:43:02 +0700 Subject: add before margin in order line --- indoteknik_custom/models/sale_order_line.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index c8066961..da66465e 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -10,6 +10,8 @@ class SaleOrderLine(models.Model): help="Total Margin in Sales Order Header") item_percent_margin = fields.Float('%Margin', compute='compute_item_margin', help="Total % Margin in Sales Order Header") + item_percent_margin_before = fields.Float('%Margin Before', compute='_compute_item_percent_margin_before', + help="Total % Margin excluding third party in Sales Order Header") initial_discount = fields.Float('Initial Discount') vendor_id = fields.Many2one( 'res.partner', string='Vendor', readonly=True, @@ -134,6 +136,29 @@ class SaleOrderLine(models.Model): else: line.item_percent_margin_without_deduction = 0 + def _compute_item_percent_margin_before(self): + for line in self: + if not line.product_id or line.product_id.type == 'service' \ + or line.price_unit <= 0 or line.product_uom_qty <= 0 \ + or not line.vendor_id: + line.item_percent_margin_before = 0 + continue + + sales_price = line.price_reduce_taxexcl * line.product_uom_qty + + purchase_price = line.purchase_price + if line.purchase_tax_id and line.purchase_tax_id.price_include: + purchase_price = line.purchase_price / 1.11 + + purchase_price = purchase_price * line.product_uom_qty + + margin_before = sales_price - purchase_price + + if sales_price > 0: + line.item_percent_margin_before = round((margin_before / sales_price), 4) * 100 + else: + line.item_percent_margin_before = 0 + def compute_item_margin(self): for line in self: if not line.product_id or line.product_id.type == 'service' \ -- cgit v1.2.3 From 32c2899ccd24071adcee829b569b9d29bb3a4fe9 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 5 Jun 2025 11:59:27 +0700 Subject: fix round --- indoteknik_custom/models/sale_order_line.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index da66465e..291940ed 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -155,7 +155,7 @@ class SaleOrderLine(models.Model): margin_before = sales_price - purchase_price if sales_price > 0: - line.item_percent_margin_before = round((margin_before / sales_price), 4) * 100 + line.item_percent_margin_before = round((margin_before / sales_price), 2) * 100 else: line.item_percent_margin_before = 0 -- cgit v1.2.3 From b892a11ae2e28d1ad2d42c1fba0fec875fdbf163 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 7 Jun 2025 08:34:54 +0700 Subject: (andri) fix bug shipping method --- indoteknik_custom/models/sale_order.py | 53 ++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 9 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index a86d43cb..9faafb11 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -256,6 +256,20 @@ class SaleOrder(models.Model): ('custom', 'Custom'), ], string='Shipping Option', help="Select shipping option for delivery", tracking=True) + @api.onchange('shipping_cost_covered') + def _onchange_shipping_cost_covered(self): + if self.shipping_cost_covered == 'indoteknik' and self.select_shipping_option == 'biteship': + self.shipping_cost_covered = 'customer' + return { + 'warning': { + 'title': "Biteship Tidak Diizinkan", + 'message': ( + "Biaya pengiriman ditanggung Indoteknik, sehingga tidak diizinkan menggunakan metode Biteship. " + "Pilihan penanggung biaya akan dikembalikan sebelumnya" + ) + } + } + def get_biteship_carrier_ids(self): courier_codes = tuple(self._get_biteship_courier_codes() or []) if not courier_codes: @@ -277,20 +291,29 @@ class SaleOrder(models.Model): if view_type == 'form': doc = etree.XML(res['arch']) - carrier_ids = self.get_biteship_carrier_ids() - if carrier_ids: - carrier_ids_str = '(' + ','.join(str(x) for x in carrier_ids) + ')' - else: - carrier_ids_str = '(-1,)' # aman kalau kosong + # Ambil semua delivery_carrier_id dari mapping rajaongkir_kurir + biteship_ids = self.env['rajaongkir.kurir'].search([]).mapped('delivery_carrier_id.id') + biteship_ids = list(set(filter(None, biteship_ids))) # pastikan unik dan bukan None + + all_ids = self.env['delivery.carrier'].search([]).ids + custom_ids = list(set(all_ids) - set(biteship_ids)) - # ✅ Tambahkan log di sini - _logger.info("🛰️ Biteship Carrier IDs: %s", carrier_ids) - _logger.info("📦 Domain string to apply: [('id', 'in', %s)]", carrier_ids_str) + # Format sebagai string Python list + biteship_ids_str = ','.join(str(i) for i in biteship_ids) or '-1' + custom_ids_str = ','.join(str(i) for i in custom_ids) or '-1' + # Terapkan domain ke field carrier_id for node in doc.xpath("//field[@name='carrier_id']"): - node.set('domain', "[('id', 'in', %s)]" % carrier_ids_str) + # Domain tergantung select_shipping_option + node.set( + 'domain', + "[('id', 'in', [%s]) if select_shipping_option == 'biteship' else ('id', 'in', [%s])]" % + (biteship_ids_str, custom_ids_str) + ) + # Simpan kembali hasil XML ke arsitektur form res['arch'] = etree.tostring(doc, encoding='unicode') + return res # @api.onchange('shipping_option_id') @@ -477,6 +500,18 @@ class SaleOrder(models.Model): @api.onchange('select_shipping_option') def _onchange_select_shipping_option(self): + if self.select_shipping_option == 'biteship' and self.shipping_cost_covered == 'indoteknik': + self.select_shipping_option = self._origin.select_shipping_option if self._origin else 'custom' + return { + 'warning': { + 'title': "Biteship Tidak Diizinkan", + 'message': ( + "Biaya pengiriman ditanggung Indoteknik. Tidak diizinkan memilih metode Biteship. " + "Opsi pengiriman dikembalikan ke sebelumnya." + ) + } + } + self.shipping_option_id = False self.carrier_id = False self.delivery_amt = 0 -- cgit v1.2.3 From 288a7f333c1d37574dba76f3fb3f7216da259cae Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 7 Jun 2025 09:41:27 +0700 Subject: (andri) add field shipping option pada stock picking sebagai tambahan info --- indoteknik_custom/models/stock_picking.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 7e001299..d6413b87 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -244,6 +244,11 @@ class StockPicking(models.Model): biteship_waybill_id = fields.Char(string="Biteship Waybill ID") final_seq = fields.Float(string='Remaining Time') shipping_method_so_id = fields.Many2one('delivery.carrier', string='Shipping Method SO', related='sale_id.carrier_id') + shipping_option_so_id = fields.Many2one('shipping.option', string='Shipping Option SO', related='sale_id.shipping_option_id') + select_shipping_option_so = fields.Selection([ + ('biteship', 'Biteship'), + ('custom', 'Custom'), + ], string='Shipping Type SO', related='sale_id.select_shipping_option') state_packing = fields.Selection([('not_packing', 'Belum Packing'), ('packing_done', 'Sudah Packing')], string='Packing Status') approval_invoice_date_id = fields.Many2one('approval.invoice.date', string='Approval Invoice Date') last_update_date_doc_kirim = fields.Datetime(string='Last Update Tanggal Kirim') -- cgit v1.2.3 From d3538371691a43efbd5527b746c942bdef9fd1ba Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 7 Jun 2025 11:02:03 +0700 Subject: (andri) Pada PO, CAB tidak tampil jika statusnya bukan posted --- indoteknik_custom/models/purchase_order.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 27f1aebd..004a1fa4 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -89,6 +89,14 @@ class PurchaseOrder(models.Model): store_name = fields.Char(string='Nama Toko') purchase_order_count = fields.Integer('Purchase Order Count', related='partner_id.purchase_order_count') + is_cab_visible = fields.Boolean(string='Tampilkan Tombol CAB', compute='_compute_is_cab_visible') + + @api.depends('move_id.state') + def _compute_is_cab_visible(self): + for order in self: + move = order.move_id + order.is_cab_visible = bool(move and move.state == 'posted') + def action_view_journal_uangmuka(self): self.ensure_one() if not self.move_id: -- cgit v1.2.3 From 02511349a98e9488ed91795a062774f7d3ad26a6 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 7 Jun 2025 13:41:09 +0700 Subject: (andri) Nomor CAB serta nominal terisi otomatis ketika create reklas --- indoteknik_custom/models/invoice_reklas.py | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/invoice_reklas.py b/indoteknik_custom/models/invoice_reklas.py index d10d4c31..8641ca07 100644 --- a/indoteknik_custom/models/invoice_reklas.py +++ b/indoteknik_custom/models/invoice_reklas.py @@ -18,6 +18,17 @@ class InvoiceReklas(models.TransientModel): ('pembelian', 'Pembelian'), ], string='Reklas Tipe') + @api.model + def default_get(self, fields): + res = super().default_get(fields) + active_ids = self._context.get('active_ids', []) + if active_ids: + move = self.env['account.move'].browse(active_ids[0]) + if move.move_type == 'entry': + res['reklas_id'] = move.id + res['pay_amt'] = move.amount_total # atau amount_residual jika mau sisa + return res + @api.onchange('reklas_type') def _onchange_reklas_type(self): if self.reklas_type == 'penjualan': -- cgit v1.2.3 From 49dcd6e8111483cd8f64aa2401a41b3a8d57a1dc Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 7 Jun 2025 13:46:46 +0700 Subject: (andri) onchange Nomor CAB serta nominal terisi otomatis ketika create reklas --- indoteknik_custom/models/invoice_reklas.py | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/invoice_reklas.py b/indoteknik_custom/models/invoice_reklas.py index 8641ca07..59c78ce6 100644 --- a/indoteknik_custom/models/invoice_reklas.py +++ b/indoteknik_custom/models/invoice_reklas.py @@ -18,6 +18,15 @@ class InvoiceReklas(models.TransientModel): ('pembelian', 'Pembelian'), ], string='Reklas Tipe') + @api.onchange('reklas_type') + def _onchange_reklas_type(self): + if self.reklas_type == 'penjualan': + invoices = self.env['account.move'].browse(self._context.get('active_ids', [])) + self.pay_amt = invoices.amount_total + # Tambahan ini: + if len(invoices) == 1 and invoices.move_type == 'entry': + self.reklas_id = invoices.id + @api.model def default_get(self, fields): res = super().default_get(fields) -- cgit v1.2.3 From 984e128294f4c3d9d3d8eab8dacf03846f8336b2 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 7 Jun 2025 15:53:32 +0700 Subject: (andri) revisi peletakan CAB pada PO dan add field reklas misc --- indoteknik_custom/models/account_move.py | 1 + 1 file changed, 1 insertion(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 29368089..73dbefd6 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -67,6 +67,7 @@ class AccountMove(models.Model): is_hr = fields.Boolean(string="Is HR?", default=False) purchase_order_id = fields.Many2one('purchase.order', string='Purchase Order') length_of_payment = fields.Integer(string="Length of Payment", compute='compute_length_of_payment') + reklas_misc_id = fields.Many2one('account.move', string='No Jurnal Reklas (MISC)') def name_get(self): result = [] -- cgit v1.2.3 From 9200e74126c99410b79ef2c344915251bef6af19 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 7 Jun 2025 15:55:40 +0700 Subject: (andri) edit penamaan field Reklas --- indoteknik_custom/models/account_move.py | 2 +- indoteknik_custom/models/invoice_reklas.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 73dbefd6..3333af8f 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -67,7 +67,7 @@ class AccountMove(models.Model): is_hr = fields.Boolean(string="Is HR?", default=False) purchase_order_id = fields.Many2one('purchase.order', string='Purchase Order') length_of_payment = fields.Integer(string="Length of Payment", compute='compute_length_of_payment') - reklas_misc_id = fields.Many2one('account.move', string='No Jurnal Reklas (MISC)') + reklas_misc_id = fields.Many2one('account.move', string='Journal Entries Reklas') def name_get(self): result = [] diff --git a/indoteknik_custom/models/invoice_reklas.py b/indoteknik_custom/models/invoice_reklas.py index 59c78ce6..5e21a787 100644 --- a/indoteknik_custom/models/invoice_reklas.py +++ b/indoteknik_custom/models/invoice_reklas.py @@ -11,7 +11,7 @@ _logger = logging.getLogger(__name__) class InvoiceReklas(models.TransientModel): _name = 'invoice.reklas' _description = "digunakan untuk reklas Uang Muka Penjualan" - reklas_id = fields.Many2one('account.move', string='Nomor CAB') + reklas_id = fields.Many2one('account.move', string='Nomor CAB', domain="[('move_type','=','entry')]") pay_amt = fields.Float(string='Yang dibayarkan') reklas_type = fields.Selection([ ('penjualan', 'Penjualan'), -- cgit v1.2.3 From a7f3eb12ddc41a18df90a3d5519014c07c0e5d7b Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 9 Jun 2025 08:23:04 +0700 Subject: (andri) fix retry email marketing --- indoteknik_custom/models/mail_mail.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/mail_mail.py b/indoteknik_custom/models/mail_mail.py index 82b1fcca..7ebd9293 100644 --- a/indoteknik_custom/models/mail_mail.py +++ b/indoteknik_custom/models/mail_mail.py @@ -1,12 +1,21 @@ from odoo import fields, models, api, _ +from datetime import timedelta class MailMail(models.Model): _inherit = 'mail.mail' + @api.model def retry_send_mail(self): - mails = self.env['mail.mail'].search([ + now = fields.Datetime.now() + seven_days_ago = now - timedelta(days=7) + + # Filter hanya email gagal dalam 7 hari terakhir + mails = self.search([ ('state', 'in', ['exception', 'cancel']), + ('create_date', '>=', seven_days_ago), + ('create_date', '<=', now), ], limit=250) + for mail in mails: - mail.state = 'outgoing' + mail.state = 'outgoing' -- cgit v1.2.3 From 114d17b3d8e21801a4020956c8e8eb80d33b1be6 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 9 Jun 2025 09:28:58 +0700 Subject: (andri) fix retry --- indoteknik_custom/models/mail_mail.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/mail_mail.py b/indoteknik_custom/models/mail_mail.py index 7ebd9293..cbcd883a 100644 --- a/indoteknik_custom/models/mail_mail.py +++ b/indoteknik_custom/models/mail_mail.py @@ -1,6 +1,8 @@ from odoo import fields, models, api, _ from datetime import timedelta +import logging +_logger = logging.getLogger(__name__) class MailMail(models.Model): _inherit = 'mail.mail' @@ -12,10 +14,14 @@ class MailMail(models.Model): # Filter hanya email gagal dalam 7 hari terakhir mails = self.search([ - ('state', 'in', ['exception', 'cancel']), + ('state', 'in', 'exception'), ('create_date', '>=', seven_days_ago), ('create_date', '<=', now), ], limit=250) + _logger.info("Found %s failed emails in last 7 days to retry.", len(mails)) + for mail in mails: - mail.state = 'outgoing' + _logger.info("Retrying email ID %s - To: %s - Subject: %s", + mail.id, mail.email_to, mail.subject) + mail.state = 'outgoing' -- cgit v1.2.3 From de747091235c844d33bcf62b4d98af0d03251826 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 9 Jun 2025 11:37:19 +0700 Subject: (andri) fix note order biteship --- indoteknik_custom/models/stock_picking.py | 1 + 1 file changed, 1 insertion(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index d6413b87..d04b3d27 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -587,6 +587,7 @@ class StockPicking(models.Model): "destination_postal_code": shipping_partner.zip, "origin_note": "BELAKANG INDOMARET", "destination_note": f"SO: {self.sale_id.name}", + "order_note": f"SO: {self.sale_id.name}", "courier_type": courier_service_code, "courier_company": self.carrier_id.name.lower(), "delivery_type": "now", -- cgit v1.2.3 From 03bceb0f2641c1f7303dffcf0dcbc855c70cffb3 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 9 Jun 2025 12:59:54 +0700 Subject: fix commitment date kosong --- indoteknik_custom/models/sale_order.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 705d16ef..565e8a19 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -2109,11 +2109,15 @@ class SaleOrder(models.Model): ], limit=1) picking_in = stock_move.picking_id result_date = picking_in.date_done if picking_in else None - if result_date: + + if result_date and eta and isinstance(eta, (datetime.date, datetime.datetime)): status = "Early" if result_date < eta else "Delay" result_date_str = result_date.strftime('%m/%d/%Y') eta_str = eta.strftime('%m/%d/%Y') order.ready_to_ship_status_detail = f"Expected: {eta_str} | Realtime: {result_date_str} | {status}" + elif not eta: + # If eta is missing or False, treat as 'On Track' or you may choose different logic + order.ready_to_ship_status_detail = "On Track" else: order.ready_to_ship_status_detail = "On Track" else: -- cgit v1.2.3 From e577c31c748d66b102362af35a33984b9b28edd4 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Mon, 9 Jun 2025 15:51:28 +0700 Subject: fix bug cant open so --- indoteknik_custom/models/sale_order.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 565e8a19..eab4f2f3 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -2092,7 +2092,7 @@ class SaleOrder(models.Model): # @api.depends('commitment_date') def _compute_ready_to_ship_status_detail(self): for order in self: - eta = order.commitment_date + eta = order.commitment_date if order.commitment_date else None match_lines = self.env['purchase.order.sales.match'].search([ ('sale_id', '=', order.id) ]) @@ -2109,9 +2109,10 @@ class SaleOrder(models.Model): ], limit=1) picking_in = stock_move.picking_id result_date = picking_in.date_done if picking_in else None - - if result_date and eta and isinstance(eta, (datetime.date, datetime.datetime)): - status = "Early" if result_date < eta else "Delay" + if result_date: + status = "Delay" + if result_date and eta and result_date < eta: + status = "Early" result_date_str = result_date.strftime('%m/%d/%Y') eta_str = eta.strftime('%m/%d/%Y') order.ready_to_ship_status_detail = f"Expected: {eta_str} | Realtime: {result_date_str} | {status}" -- cgit v1.2.3 From 2760b81f8a650ea95d36c125d1ab4e2feb011e44 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 9 Jun 2025 17:12:48 +0700 Subject: (andri) fix berat produk jika transaksi dari website --- indoteknik_custom/models/sale_order.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index a0576ad8..f53d375b 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -844,9 +844,10 @@ class SaleOrder(models.Model): missing_weight_products = [] for line in self.order_line: - if line.weight > 0: - total_weight += line.weight * line.product_uom_qty - line.product_id.weight = line.weight + product_weight = line.product_id.weight or 0 + if product_weight > 0: + total_weight += product_weight * line.product_uom_qty + line.weight = product_weight else: missing_weight_products.append(line.product_id.name) @@ -2795,8 +2796,8 @@ class SaleOrder(models.Model): def create(self, vals): # Ensure partner details are updated when a sale order is created order = super(SaleOrder, self).create(vals) - _logger.info(f"[CREATE CONTEXT] {self.env.context}") - order._auto_set_shipping_from_website() + # _logger.info(f"[CREATE CONTEXT] {self.env.context}") + # order._auto_set_shipping_from_website() order._compute_etrts_date() order._validate_expected_ready_ship_date() # order._validate_delivery_amt() @@ -2893,7 +2894,7 @@ class SaleOrder(models.Model): "SO tidak dapat ditambahkan produk baru karena SO sudah menjadi sale order.") order._update_delivery_service_type_from_shipping_option(vals) - + if 'carrier_id' in vals: for order in self: for picking in order.picking_ids: -- cgit v1.2.3 From 5f5753dbec8af518dd34821820462cd6340a2a08 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 9 Jun 2025 17:35:22 +0700 Subject: wati leads --- indoteknik_custom/models/wati.py | 166 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 165 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/wati.py b/indoteknik_custom/models/wati.py index a0619f83..18517502 100644 --- a/indoteknik_custom/models/wati.py +++ b/indoteknik_custom/models/wati.py @@ -201,6 +201,170 @@ class WatiNotification(models.Model): wati.is_lead = True wati.lead_id = current_lead.id + # FINAL CODE - Sesuai dengan mapping table Anda + + def check_wati_tags_leads(self): + """Check tags 'leads' di WATI dan create leads di Odoo - Final Version""" + _logger.info('=== Starting WATI Tags Check (Final) ===') + + wati_api = self.env['wati.api'] + total_leads_created = 0 + + try: + # Get WATI contacts + wati_contacts = wati_api.http_get('/api/v1/getContacts', {'pageSize': 100, 'pageNumber': 1}) + + if isinstance(wati_contacts, dict) and wati_contacts.get('result') == 'success': + contact_list = wati_contacts.get('contact_list', []) + + for contact in contact_list: + if self._create_lead_if_tagged(contact): + total_leads_created += 1 + + _logger.info('WATI check completed: %s leads created' % total_leads_created) + return {'leads_created': total_leads_created} + + except Exception as e: + _logger.error('Error in WATI tags check: %s' % str(e)) + return {'leads_created': 0, 'error': str(e)} + + def _create_lead_if_tagged(self, contact): + """Create lead jika contact punya tags=leads - Sesuai Mapping Table""" + try: + # Check tags leads + if not self._has_tags_leads(contact): + return False + + phone = contact.get('phone', '') + if not phone: + return False + + # Check existing lead by phone + existing_lead = self.env['crm.lead'].search([('phone', '=', phone)], limit=1) + if existing_lead: + _logger.info('Lead already exists for phone %s' % phone) + return False + + # Extract data dari customParams sesuai mapping table + custom_params = contact.get('customParams', []) + contact_data = self._extract_contact_data(custom_params) + + # Create lead dengan field mapping yang sesuai + lead_vals = { + 'name': self._generate_lead_name(contact_data, contact), + 'phone': phone, # Phone Number → Mobile + 'contact_name': contact_data.get('name', ''), # Name → Contact Name + 'partner_name': contact_data.get('perusahaan', ''), # Perusahaan → Company Name + 'email_from': contact_data.get('email', ''), # Email → Email + 'description': contact_data.get('notes', ''), # Notes → Internal Notes + 'type': 'lead', + 'user_id': self._get_salesperson_id(contact_data.get('sales', '')), # Sales → Salesperson + } + + new_lead = self.env['crm.lead'].create(lead_vals) + _logger.info('Created WATI lead %s for %s (%s)' % (new_lead.id, contact_data.get('name', 'Unknown'), phone)) + return True + + except Exception as e: + _logger.error('Error creating lead: %s' % str(e)) + return False + + def _extract_contact_data(self, custom_params): + """Extract data dari customParams sesuai mapping table""" + contact_data = {} + + for param in custom_params: + param_name = param.get('name', '').lower() + param_value = param.get('value', '').strip() + + # Mapping sesuai table: + if param_name == 'perusahaan': # Perusahaan → Company Name + contact_data['perusahaan'] = param_value + elif param_name == 'name': # Name → Contact Name + contact_data['name'] = param_value + elif param_name == 'email': # Email → Email + contact_data['email'] = param_value + elif param_name == 'sales': # Sales → Salesperson + contact_data['sales'] = param_value + elif param_name == 'notes': # Notes → Internal Notes + contact_data['notes'] = param_value + # Phone Number sudah diambil dari contact.phone + + return contact_data + + def _generate_lead_name(self, contact_data, contact): + """Generate lead name sesuai mapping: Judul Leads berdasarkan company/contact""" + company_name = contact_data.get('perusahaan', '') + contact_name = contact_data.get('name', '') or contact.get('name', '') + + if company_name: + return 'WATI Lead - %s' % company_name + elif contact_name: + return 'WATI Lead - %s' % contact_name + else: + return 'WATI Lead - %s' % contact.get('phone', 'Unknown') + + def _get_salesperson_id(self, sales_name): + """Get salesperson ID dari nama - Sales → Salesperson""" + if not sales_name: + return 2 # Default Sales (ID 2) + + # Try find user by name + user = self.env['res.users'].search([ + ('name', 'ilike', sales_name) + ], limit=1) + + if user: + return user.id + else: + # Fallback ke default Sales + return 2 + + def _has_tags_leads(self, contact): + """Check apakah ada tags untuk tajik ke odoo => Leads""" + custom_params = contact.get('customParams', []) + + for param in custom_params: + param_name = param.get('name', '').lower() + param_value = param.get('value', '').lower() + + # Check: "Judul Tags untuk tajik ke odoo => Leads" + if param_name == 'tags' and param_value == 'leads': + return True + + return False + + def manual_check_tags(self): + """Manual trigger untuk testing""" + result = self.check_wati_tags_leads() + + message = 'WATI Tags Check Completed!\n\n' + message += 'Leads Created: %s\n\n' % result.get('leads_created', 0) + message += 'Field Mapping:\n' + message += '• Perusahaan → Company Name\n' + message += '• Name → Contact Name\n' + message += '• Email → Email\n' + message += '• Sales → Salesperson\n' + message += '• Phone Number → Mobile\n' + message += '• Notes → Internal Notes\n' + message += '• Tags=leads → Trigger Lead Creation' + + if result.get('error'): + message += '\n\nError: %s' % result['error'] + message_type = 'warning' + else: + message_type = 'success' + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'WATI Tags Check', + 'message': message, + 'type': message_type, + 'sticky': True, + } + } class WatiHistory(models.Model): _name = 'wati.history' @@ -319,4 +483,4 @@ class WatiHistoryLine(models.Model): ticket_id = fields.Char(string='Ticket ID') type = fields.Char(string='Type') wa_id = fields.Char(string='WA ID') - date_wati = fields.Datetime(string='Date WATI') + date_wati = fields.Datetime(string='Date WATI') \ No newline at end of file -- cgit v1.2.3 From 1a63fac5f7f4dbb2990e5b1eeb9d7f381f39e908 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 9 Jun 2025 20:35:35 +0700 Subject: add products in manifest --- indoteknik_custom/models/stock_picking.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 71eca020..ae3c8f1d 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1646,6 +1646,14 @@ class StockPicking(models.Model): sale_order_delay = self.env['sale.order.delay'].search([('so_number', '=', order.name)], limit=1) + product_shipped = [] + for move_line in self.move_line_ids_without_package: + if move_line.qty_done > 0: + product_shipped.append({ + 'name': move_line.product_id.name, + 'qty': move_line.qty_done + }) + response = { 'delivery_order': { 'name': self.name, @@ -1662,7 +1670,8 @@ class StockPicking(models.Model): 'eta': self.generate_eta_delivery(), 'is_biteship': True if self.biteship_id else False, 'manifests': self.get_manifests(), - 'is_delay': True if sale_order_delay and sale_order_delay.status == 'delayed' else False + 'is_delay': True if sale_order_delay and sale_order_delay.status == 'delayed' else False, + 'products': product_shipped } if self.biteship_id: -- cgit v1.2.3 From 05dafe1ab837cac8992d1dc6c012a26bce88c15c Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 10 Jun 2025 10:36:45 +0700 Subject: push code apit --- indoteknik_custom/models/sale_order.py | 49 +++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 16 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index eab4f2f3..baa8207f 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -2091,11 +2091,22 @@ class SaleOrder(models.Model): # @api.depends('commitment_date') def _compute_ready_to_ship_status_detail(self): + def is_empty(val): + """Helper untuk cek data kosong yang umum di Odoo.""" + return val is None or val == "" or val == [] or val == {} + for order in self: - eta = order.commitment_date if order.commitment_date else None + order.ready_to_ship_status_detail = 'On Track' # Default value + + # Skip if no commitment date + if is_empty(order.commitment_date): + continue + + eta = order.commitment_date match_lines = self.env['purchase.order.sales.match'].search([ ('sale_id', '=', order.id) ]) + if match_lines: for match in match_lines: po = match.purchase_order_id @@ -2104,25 +2115,31 @@ class SaleOrder(models.Model): ('order_id', '=', po.id), ('product_id', '=', product.id) ], limit=1) + + if is_empty(po_line): + continue + stock_move = self.env['stock.move'].search([ ('purchase_line_id', '=', po_line.id) ], limit=1) + + if is_empty(stock_move) or is_empty(stock_move.picking_id): + continue + picking_in = stock_move.picking_id - result_date = picking_in.date_done if picking_in else None - if result_date: - status = "Delay" - if result_date and eta and result_date < eta: - status = "Early" - result_date_str = result_date.strftime('%m/%d/%Y') - eta_str = eta.strftime('%m/%d/%Y') - order.ready_to_ship_status_detail = f"Expected: {eta_str} | Realtime: {result_date_str} | {status}" - elif not eta: - # If eta is missing or False, treat as 'On Track' or you may choose different logic - order.ready_to_ship_status_detail = "On Track" - else: - order.ready_to_ship_status_detail = "On Track" - else: - order.ready_to_ship_status_detail = 'On Track' + result_date = picking_in.date_done + + if is_empty(result_date): + continue + + try: + if result_date < eta: + order.ready_to_ship_status_detail = f"Early (Actual: {result_date.strftime('%m/%d/%Y')})" + else: + order.ready_to_ship_status_detail = f"Delay (Actual: {result_date.strftime('%m/%d/%Y')})" + except Exception as e: + _logger.error(f"Error computing ready to ship status: {str(e)}") + continue def write(self, vals): -- cgit v1.2.3 From 5ca33915f1e3d052cfa989163d43a15dbc9ddec9 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 10 Jun 2025 11:17:57 +0700 Subject: (andri) add button get koordinat pada contact --- indoteknik_custom/models/res_partner.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index f1e362e6..0f1edac2 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -3,6 +3,7 @@ from odoo.exceptions import UserError, ValidationError from datetime import datetime from odoo.http import request import re +import requests class GroupPartner(models.Model): _name = 'group.partner' @@ -521,4 +522,28 @@ class ResPartner(models.Model): @api.onchange('name') def _onchange_name(self): if self.company_type == 'person': - self.nama_wajib_pajak = self.name \ No newline at end of file + self.nama_wajib_pajak = self.name + + def geocode_address(self): + for rec in self: + address = ', '.join(filter(None, [ + rec.street, + rec.city, + rec.state_id.name if rec.state_id else '', + rec.zip, + rec.country_id.name if rec.country_id else '' + ])) + + if not address: + continue + + api_key = self.env['ir.config_parameter'].sudo().get_param('google.maps.api_key') + url = f'https://maps.googleapis.com/maps/api/geocode/json?address={address}&key={api_key}' + response = requests.get(url) + + if response.ok: + result = response.json() + if result.get('results'): + location = result['results'][0]['geometry']['location'] + rec.latitude = location['lat'] + rec.longtitude = location['lng'] \ No newline at end of file -- cgit v1.2.3 From c827294ee6dd613de089af846521cfdc76550e16 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 10 Jun 2025 13:10:35 +0700 Subject: (andri) revisi mengenai onchange create reklas dan yang lain --- indoteknik_custom/models/account_move.py | 3 + indoteknik_custom/models/invoice_reklas.py | 97 ++++++++++++++++------ .../models/invoice_reklas_penjualan.py | 60 +++++++++---- 3 files changed, 117 insertions(+), 43 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 3333af8f..4cd2b6b3 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -68,6 +68,9 @@ class AccountMove(models.Model): purchase_order_id = fields.Many2one('purchase.order', string='Purchase Order') length_of_payment = fields.Integer(string="Length of Payment", compute='compute_length_of_payment') reklas_misc_id = fields.Many2one('account.move', string='Journal Entries Reklas') + # Di model account.move + bill_id = fields.Many2one('account.move', string='Vendor Bill', domain=[('move_type', '=', 'in_invoice')], help='Bill asal dari proses reklas ini') + def name_get(self): result = [] diff --git a/indoteknik_custom/models/invoice_reklas.py b/indoteknik_custom/models/invoice_reklas.py index 5e21a787..b7d52371 100644 --- a/indoteknik_custom/models/invoice_reklas.py +++ b/indoteknik_custom/models/invoice_reklas.py @@ -20,12 +20,26 @@ class InvoiceReklas(models.TransientModel): @api.onchange('reklas_type') def _onchange_reklas_type(self): - if self.reklas_type == 'penjualan': - invoices = self.env['account.move'].browse(self._context.get('active_ids', [])) - self.pay_amt = invoices.amount_total - # Tambahan ini: - if len(invoices) == 1 and invoices.move_type == 'entry': - self.reklas_id = invoices.id + active_ids = self._context.get('active_ids', []) + if not active_ids: + return + + move = self.env['account.move'].browse(active_ids[0]) + cab = False + + if move.move_type == 'entry': + cab = move + elif move.move_type == 'in_invoice': + if move.reklas_misc_id: + cab = move.reklas_misc_id + elif move.purchase_order_id and move.purchase_order_id.move_id: + cab = move.purchase_order_id.move_id + + if cab: + self.reklas_id = cab.id + + # ✅ Selalu ambil nilai dari invoice yang direklas (bukan dari CAB) + self.pay_amt = move.amount_total @api.model def default_get(self, fields): @@ -33,11 +47,23 @@ class InvoiceReklas(models.TransientModel): active_ids = self._context.get('active_ids', []) if active_ids: move = self.env['account.move'].browse(active_ids[0]) + cab = False + if move.move_type == 'entry': - res['reklas_id'] = move.id - res['pay_amt'] = move.amount_total # atau amount_residual jika mau sisa + cab = move + elif move.move_type == 'in_invoice': + if move.reklas_misc_id: + cab = move.reklas_misc_id + elif move.purchase_order_id and move.purchase_order_id.move_id: + cab = move.purchase_order_id.move_id + + if cab: + res['reklas_id'] = cab.id + + res['pay_amt'] = move.amount_total return res + @api.onchange('reklas_type') def _onchange_reklas_type(self): if self.reklas_type == 'penjualan': @@ -49,33 +75,47 @@ class InvoiceReklas(models.TransientModel): raise UserError('Reklas Tipe harus diisi') if not self.reklas_id: raise UserError('Nomor CAB harus diisi') + invoices = self.env['account.move'].browse(self._context.get('active_ids', [])) current_time = datetime.now() + for invoice in invoices: - if self.reklas_type == 'penjualan': - ref_name = 'REKLAS '+self.reklas_id.name+" UANG MUKA PENJUALAN "+invoice.name+" "+invoice.partner_id.name - else: - ref_name = 'REKLAS '+self.reklas_id.name+" UANG MUKA PEMBELIAN "+invoice.name+" "+invoice.partner_id.name - if self.reklas_type == 'penjualan': - parameters_header = { - 'ref': ref_name, - 'date': current_time, - 'journal_id': 13 - } - else: - parameters_header = { - 'ref': ref_name, - 'date': current_time, - 'journal_id': 13 - } + # Ambil nama PO jika ada + po_name = invoice.purchase_order_id.name if invoice.purchase_order_id else '' + + # Susun nama referensi dengan aman + ref_name = 'REKLAS {} UANG MUKA {} {}{} {}'.format( + self.reklas_id.name or '', + 'PENJUALAN' if self.reklas_type == 'penjualan' else 'PEMBELIAN', + invoice.name or '', + f" - {po_name}" if po_name else '', + invoice.partner_id.name or '' + ) + + # Header jurnal reklas + parameters_header = { + 'ref': ref_name, + 'date': current_time, + 'journal_id': 13 + } account_move = request.env['account.move'].create([parameters_header]) _logger.info('Success Reklas with %s' % account_move.name) + # ✅ Set Bill asal sebagai source document + account_move.bill_id = invoice.id + + # Tambahkan info asal invoice ke jurnal (opsional) + account_move.invoice_origin = invoice.name + + # Simpan hubungan balik ke invoice + invoice.reklas_misc_id = account_move.id + + # Buat line debit dan kredit if self.reklas_type == 'penjualan': parameter_debit = { 'move_id': account_move.id, - 'account_id': 668, # penerimaan belum alokasi + 'account_id': 668, # penerimaan belum alokasi 'partner_id': invoice.partner_id.id, 'currency_id': 12, 'debit': self.pay_amt, @@ -91,7 +131,7 @@ class InvoiceReklas(models.TransientModel): 'credit': self.pay_amt, 'name': ref_name } - else: + else: # pembelian parameter_debit = { 'move_id': account_move.id, 'account_id': 438, @@ -110,7 +150,11 @@ class InvoiceReklas(models.TransientModel): 'credit': self.pay_amt, 'name': ref_name } + + # Simpan journal lines request.env['account.move.line'].create([parameter_debit, parameter_credit]) + + # Tampilkan hasil jurnal reklas return { 'name': _('Journal Entries'), 'view_mode': 'form', @@ -120,4 +164,3 @@ class InvoiceReklas(models.TransientModel): 'type': 'ir.actions.act_window', 'res_id': account_move.id } - \ No newline at end of file diff --git a/indoteknik_custom/models/invoice_reklas_penjualan.py b/indoteknik_custom/models/invoice_reklas_penjualan.py index 80c3ed43..2f5ee160 100644 --- a/indoteknik_custom/models/invoice_reklas_penjualan.py +++ b/indoteknik_custom/models/invoice_reklas_penjualan.py @@ -17,43 +17,70 @@ class InvoiceReklasPenjualan(models.TransientModel): def create_reklas_penjualan(self): invoices = self.invoice_reklas_line - current_time = datetime.now() account_move_ids = [] - for invoice in invoices: - ref_name = 'REKLAS ' + invoice.reklas_id.name + " UANG MUKA PENJUALAN " + invoice.name + " " + invoice.partner_id.name + + for line in invoices: + # Ambil nama SO jika ada + so_name = line.sale_id.name if line.sale_id else '' + + # Susun referensi nama jurnal + ref_name = 'REKLAS {} UANG MUKA PENJUALAN {}{} {}'.format( + line.reklas_id.name or '', + line.name or '', + f" - {so_name}" if so_name else '', + line.partner_id.name or '' + ) + + # Header jurnal parameters_header = { 'ref': ref_name, 'date': current_time, - 'journal_id': 13 + 'journal_id': 13, + # ⬇️ Tambahkan jika tahu invoice asal (name = ID Bill) + 'bill_id': int(line.name) if line.name and line.name.isdigit() else False, } account_move = self.env['account.move'].create([parameters_header]) _logger.info('Success Reklas with %s' % account_move.name) - parameter_debit = { + # Simpan info asal (optional) + account_move.invoice_origin = line.name + + # Simpan juga ke `reklas_misc_id` jika ditemukan invoice valid + if line.name and line.name.isdigit(): + invoice_id = self.env['account.move'].browse(int(line.name)) + if invoice_id.exists(): + invoice_id.reklas_misc_id = account_move.id + + # Buat debit kredit line + debit_line = { 'move_id': account_move.id, - 'account_id': 668, # uang muka penjualan - 'partner_id': invoice.partner_id.id, + 'account_id': 668, # akun penerimaan belum alokasi + 'partner_id': line.partner_id.id, 'currency_id': 12, - 'debit': invoice.pay_amt, + 'debit': line.pay_amt, 'credit': 0, 'name': ref_name } - parameter_credit = { + credit_line = { 'move_id': account_move.id, - 'account_id': 395, - 'partner_id': invoice.partner_id.id, + 'account_id': 395, # akun pengurang + 'partner_id': line.partner_id.id, 'currency_id': 12, 'debit': 0, - 'credit': invoice.pay_amt, + 'credit': line.pay_amt, 'name': ref_name } - self.env['account.move.line'].create([parameter_debit, parameter_credit]) + + self.env['account.move.line'].create([debit_line, credit_line]) account_move_ids.append(account_move.id) - invoice.unlink() - - self.unlink() + + line.unlink() # bersihkan line setelah selesai + + self.unlink() # hapus wizard utama setelah selesai + + # Tampilkan hasil jurnal reklas return { 'name': _('Journal Entries'), 'view_mode': 'tree,form', @@ -63,6 +90,7 @@ class InvoiceReklasPenjualan(models.TransientModel): 'domain': [('id', 'in', account_move_ids)], } + class InvoiceReklasPenjualanLine(models.TransientModel): _name = 'invoice.reklas.penjualan.line' _description = "digunakan untuk reklas Uang Muka Penjualan" -- cgit v1.2.3 From d89f6b21f0174d52b1efb7d54ec3443bbbe3fbcb Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 10 Jun 2025 14:00:00 +0700 Subject: (andri) penyesuaian munculnya field --- indoteknik_custom/models/invoice_reklas.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/invoice_reklas.py b/indoteknik_custom/models/invoice_reklas.py index b7d52371..5145e098 100644 --- a/indoteknik_custom/models/invoice_reklas.py +++ b/indoteknik_custom/models/invoice_reklas.py @@ -27,6 +27,7 @@ class InvoiceReklas(models.TransientModel): move = self.env['account.move'].browse(active_ids[0]) cab = False + # Deteksi dari mana asal CAB if move.move_type == 'entry': cab = move elif move.move_type == 'in_invoice': @@ -35,12 +36,14 @@ class InvoiceReklas(models.TransientModel): elif move.purchase_order_id and move.purchase_order_id.move_id: cab = move.purchase_order_id.move_id + # Isi field Nomor CAB jika ditemukan if cab: self.reklas_id = cab.id - # ✅ Selalu ambil nilai dari invoice yang direklas (bukan dari CAB) + # Nilai yang dibayarkan harus tetap ambil dari invoice/bill self.pay_amt = move.amount_total + @api.model def default_get(self, fields): res = super().default_get(fields) @@ -64,11 +67,11 @@ class InvoiceReklas(models.TransientModel): return res - @api.onchange('reklas_type') - def _onchange_reklas_type(self): - if self.reklas_type == 'penjualan': - invoices = self.env['account.move'].browse(self._context.get('active_ids', [])) - self.pay_amt = invoices.amount_total + # @api.onchange('reklas_type') + # def _onchange_reklas_type(self): + # if self.reklas_type == 'penjualan': + # invoices = self.env['account.move'].browse(self._context.get('active_ids', [])) + # self.pay_amt = invoices.amount_total def create_reklas(self): if not self.reklas_type: -- cgit v1.2.3 From df0467f8e493840f3013bc58ca26fc6d98793c95 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 10 Jun 2025 15:47:32 +0700 Subject: (andri) add openstreetmaps pada contact --- indoteknik_custom/models/res_partner.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index 0f1edac2..fee0e73b 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -146,6 +146,7 @@ class ResPartner(models.Model): date_payment_terms_purchase = fields.Datetime(string='Date Update Payment Terms') longtitude = fields.Char(string='Longtitude') latitude = fields.Char(string='Latitude') + map_view = fields.Char(string='Map') address_map = fields.Char(string='Address Map') company_type = fields.Selection(string='Company Type', selection=[('person', 'Individual'), ('company', 'Company')], @@ -526,12 +527,24 @@ class ResPartner(models.Model): def geocode_address(self): for rec in self: + # Ambil nama dari relasi (Many2one) atau gunakan nilai default + kelurahan = rec.kelurahan_id.name if rec.kelurahan_id else '' + kecamatan = rec.kecamatan_id.name if rec.kecamatan_id else '' + kota = rec.kota_id.name if rec.kota_id else '' + zip_code = rec.zip or '' + state = rec.state_id.name if rec.state_id else '' + country = rec.country_id.name if rec.country_id else '' + street = rec.street or '' + + # Susun alamat lengkap sesuai urutan lokal address = ', '.join(filter(None, [ - rec.street, - rec.city, - rec.state_id.name if rec.state_id else '', - rec.zip, - rec.country_id.name if rec.country_id else '' + street, + kelurahan, + kecamatan, + kota, + zip_code, + state, + country, ])) if not address: @@ -539,6 +552,7 @@ class ResPartner(models.Model): api_key = self.env['ir.config_parameter'].sudo().get_param('google.maps.api_key') url = f'https://maps.googleapis.com/maps/api/geocode/json?address={address}&key={api_key}' + response = requests.get(url) if response.ok: @@ -546,4 +560,4 @@ class ResPartner(models.Model): if result.get('results'): location = result['results'][0]['geometry']['location'] rec.latitude = location['lat'] - rec.longtitude = location['lng'] \ No newline at end of file + rec.longtitude = location['lng'] -- cgit v1.2.3 From a6629db53b6080bd2217e426b434c2ecc72588ab Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 11 Jun 2025 09:14:42 +0700 Subject: (andri) add button INFORMATION DETAIL pada popup detail contact & addresses --- indoteknik_custom/models/res_partner.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index fee0e73b..b8bdfe22 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -525,6 +525,16 @@ class ResPartner(models.Model): if self.company_type == 'person': self.nama_wajib_pajak = self.name + def action_open_full_form(self): + return { + 'type': 'ir.actions.act_window', + 'name': 'Partner', + 'res_model': 'res.partner', + 'res_id': self.id, + 'view_mode': 'form', + 'target': 'current', + } + def geocode_address(self): for rec in self: # Ambil nama dari relasi (Many2one) atau gunakan nilai default -- cgit v1.2.3 From 771cd3b9f5c0a6594b6e276bc47e3599d6c751e4 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 11 Jun 2025 09:50:05 +0700 Subject: (andri) add validasi tidak bisa pinpoint ketika alamat yang diisikan belum lengkap --- indoteknik_custom/models/res_partner.py | 62 ++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 24 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index b8bdfe22..a15398c7 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -537,32 +537,42 @@ class ResPartner(models.Model): def geocode_address(self): for rec in self: - # Ambil nama dari relasi (Many2one) atau gunakan nilai default - kelurahan = rec.kelurahan_id.name if rec.kelurahan_id else '' - kecamatan = rec.kecamatan_id.name if rec.kecamatan_id else '' - kota = rec.kota_id.name if rec.kota_id else '' - zip_code = rec.zip or '' - state = rec.state_id.name if rec.state_id else '' - country = rec.country_id.name if rec.country_id else '' - street = rec.street or '' - - # Susun alamat lengkap sesuai urutan lokal - address = ', '.join(filter(None, [ - street, - kelurahan, - kecamatan, - kota, - zip_code, - state, - country, - ])) - - if not address: - continue - + # Daftar field penting + required_fields = { + 'Alamat Jalan (street)': rec.street, + 'Kelurahan': rec.kelurahan_id.name if rec.kelurahan_id else '', + 'Kecamatan': rec.kecamatan_id.name if rec.kecamatan_id else '', + 'Kota': rec.kota_id.name if rec.kota_id else '', + 'Kode Pos': rec.zip, + 'Provinsi': rec.state_id.name if rec.state_id else '', + 'Negara': rec.country_id.name if rec.country_id else '', + } + + # Cek jika ada yang kosong + missing = [label for label, val in required_fields.items() if not val] + if missing: + raise UserError( + "Alamat tidak lengkap. Mohon lengkapi field berikut:\n- " + "\n- ".join(missing) + ) + + # Susun alamat lengkap + address = ', '.join([ + required_fields['Alamat Jalan (street)'], + required_fields['Kelurahan'], + required_fields['Kecamatan'], + required_fields['Kota'], + required_fields['Kode Pos'], + required_fields['Provinsi'], + required_fields['Negara'], + ]) + + # Ambil API Key api_key = self.env['ir.config_parameter'].sudo().get_param('google.maps.api_key') - url = f'https://maps.googleapis.com/maps/api/geocode/json?address={address}&key={api_key}' + if not api_key: + raise UserError("API Key Google Maps belum dikonfigurasi. Silakan isi melalui Settings.") + # Request ke Google Maps + url = f'https://maps.googleapis.com/maps/api/geocode/json?address={address}&key={api_key}' response = requests.get(url) if response.ok: @@ -571,3 +581,7 @@ class ResPartner(models.Model): location = result['results'][0]['geometry']['location'] rec.latitude = location['lat'] rec.longtitude = location['lng'] + else: + raise UserError("Tidak ditemukan hasil geocode untuk alamat tersebut.") + else: + raise UserError("Permintaan ke Google Maps gagal. Periksa koneksi internet atau API Key.") -- cgit v1.2.3 From acc284c5881675780faecd89ab5aaa85fd017914 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 11 Jun 2025 13:07:51 +0700 Subject: (andri) fix bug name invoice yang berdampak pada dunningrun line --- indoteknik_custom/models/account_move.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 4cd2b6b3..54eaabcf 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -72,20 +72,20 @@ class AccountMove(models.Model): bill_id = fields.Many2one('account.move', string='Vendor Bill', domain=[('move_type', '=', 'in_invoice')], help='Bill asal dari proses reklas ini') - def name_get(self): - result = [] - for move in self: - if move.move_type == 'entry': - # Jika masih draft, tampilkan 'Draft CAB' - if move.state == 'draft': - label = 'Draft CAB' - else: - label = move.name - result.append((move.id, label)) - else: - # Untuk invoice dan lainnya, pakai default - result.append((move.id, move.display_name)) - return result + # def name_get(self): + # result = [] + # for move in self: + # if move.move_type == 'entry': + # # Jika masih draft, tampilkan 'Draft CAB' + # if move.state == 'draft': + # label = 'Draft CAB' + # else: + # label = move.name + # result.append((move.id, label)) + # else: + # # Untuk invoice dan lainnya, pakai default + # result.append((move.id, move.display_name)) + # return result def compute_length_of_payment(self): for rec in self: -- cgit v1.2.3 From b6f63fcf96355bc11f9843a9f661cea0a458475e Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Thu, 12 Jun 2025 13:17:22 +0700 Subject: fix tracking api lalamove --- indoteknik_custom/models/stock_picking.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 3921ed5a..9dfc8b78 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -529,13 +529,7 @@ class StockPicking(models.Model): record.kgx_pod_photo = "No image available." def action_fetch_lalamove_order(self): - pickings = self.env['stock.picking'].search([ - ('picking_type_code', '=', 'outgoing'), - ('state', '=', 'done'), - ('carrier_id', '=', 9), - ('lalamove_order_id', '!=', False) - ]) - for picking in pickings: + for picking in self: try: order_id = picking.lalamove_order_id apikey = self.env['ir.config_parameter'].sudo().get_param('lalamove.apikey') -- cgit v1.2.3 From be7c601f44c3fab282dc91559a62a024d09e3f73 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 12 Jun 2025 15:07:21 +0700 Subject: (andri) hapus validasi negara pada pinpoint & hapus autofokus page pinpoin --- indoteknik_custom/models/res_partner.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index a15398c7..eeb8b67d 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -545,7 +545,6 @@ class ResPartner(models.Model): 'Kota': rec.kota_id.name if rec.kota_id else '', 'Kode Pos': rec.zip, 'Provinsi': rec.state_id.name if rec.state_id else '', - 'Negara': rec.country_id.name if rec.country_id else '', } # Cek jika ada yang kosong @@ -563,7 +562,6 @@ class ResPartner(models.Model): required_fields['Kota'], required_fields['Kode Pos'], required_fields['Provinsi'], - required_fields['Negara'], ]) # Ambil API Key -- cgit v1.2.3 From 8ac5d556a6686c6b81d5e9178bff5d308e8f176f Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 12 Jun 2025 15:11:06 +0700 Subject: (andri) add validasi tidak bisa kirim biteship (stock picking) jika shipping option SO adalah custom --- indoteknik_custom/models/stock_picking.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index ae3c8f1d..8b03e18d 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -690,6 +690,9 @@ class StockPicking(models.Model): def action_send_to_biteship(self): if self.biteship_tracking_id: raise UserError(f"Order ini sudah dikirim ke Biteship. Dengan Tracking Id: {self.biteship_tracking_id}") + + if self.sale_id.select_shipping_option == 'custom': + raise UserError("Shipping Option pada Sales Order ini adalah *Custom*. Tidak dapat dikirim melalui Biteship.") def is_courier_need_coordinates(service_code): return service_code in [ -- cgit v1.2.3 From 6616f73259c9e146449af3f2579263b42270bf8b Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Thu, 12 Jun 2025 16:18:32 +0700 Subject: Revert " commit" This reverts commit 95cef4d4d3936f8a612dadf00e83f9cddcccc0dc, reversing changes made to 626e93d3a0bfad2bc87002d9e0a7ae860ea9baba. --- indoteknik_custom/models/account_move.py | 19 -- indoteknik_custom/models/invoice_reklas.py | 112 ++------- .../models/invoice_reklas_penjualan.py | 60 ++--- indoteknik_custom/models/mail_mail.py | 19 +- indoteknik_custom/models/purchase_order.py | 30 +-- indoteknik_custom/models/requisition.py | 12 +- indoteknik_custom/models/sale_order.py | 261 +++++---------------- indoteknik_custom/models/sale_order_line.py | 25 -- indoteknik_custom/models/stock_picking.py | 9 +- indoteknik_custom/models/uangmuka_pembelian.py | 2 - 10 files changed, 118 insertions(+), 431 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 54eaabcf..30de67be 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -67,25 +67,6 @@ class AccountMove(models.Model): is_hr = fields.Boolean(string="Is HR?", default=False) purchase_order_id = fields.Many2one('purchase.order', string='Purchase Order') length_of_payment = fields.Integer(string="Length of Payment", compute='compute_length_of_payment') - reklas_misc_id = fields.Many2one('account.move', string='Journal Entries Reklas') - # Di model account.move - bill_id = fields.Many2one('account.move', string='Vendor Bill', domain=[('move_type', '=', 'in_invoice')], help='Bill asal dari proses reklas ini') - - - # def name_get(self): - # result = [] - # for move in self: - # if move.move_type == 'entry': - # # Jika masih draft, tampilkan 'Draft CAB' - # if move.state == 'draft': - # label = 'Draft CAB' - # else: - # label = move.name - # result.append((move.id, label)) - # else: - # # Untuk invoice dan lainnya, pakai default - # result.append((move.id, move.display_name)) - # return result def compute_length_of_payment(self): for rec in self: diff --git a/indoteknik_custom/models/invoice_reklas.py b/indoteknik_custom/models/invoice_reklas.py index 5145e098..d10d4c31 100644 --- a/indoteknik_custom/models/invoice_reklas.py +++ b/indoteknik_custom/models/invoice_reklas.py @@ -11,7 +11,7 @@ _logger = logging.getLogger(__name__) class InvoiceReklas(models.TransientModel): _name = 'invoice.reklas' _description = "digunakan untuk reklas Uang Muka Penjualan" - reklas_id = fields.Many2one('account.move', string='Nomor CAB', domain="[('move_type','=','entry')]") + reklas_id = fields.Many2one('account.move', string='Nomor CAB') pay_amt = fields.Float(string='Yang dibayarkan') reklas_type = fields.Selection([ ('penjualan', 'Penjualan'), @@ -20,105 +20,42 @@ class InvoiceReklas(models.TransientModel): @api.onchange('reklas_type') def _onchange_reklas_type(self): - active_ids = self._context.get('active_ids', []) - if not active_ids: - return - - move = self.env['account.move'].browse(active_ids[0]) - cab = False - - # Deteksi dari mana asal CAB - if move.move_type == 'entry': - cab = move - elif move.move_type == 'in_invoice': - if move.reklas_misc_id: - cab = move.reklas_misc_id - elif move.purchase_order_id and move.purchase_order_id.move_id: - cab = move.purchase_order_id.move_id - - # Isi field Nomor CAB jika ditemukan - if cab: - self.reklas_id = cab.id - - # Nilai yang dibayarkan harus tetap ambil dari invoice/bill - self.pay_amt = move.amount_total - - - @api.model - def default_get(self, fields): - res = super().default_get(fields) - active_ids = self._context.get('active_ids', []) - if active_ids: - move = self.env['account.move'].browse(active_ids[0]) - cab = False - - if move.move_type == 'entry': - cab = move - elif move.move_type == 'in_invoice': - if move.reklas_misc_id: - cab = move.reklas_misc_id - elif move.purchase_order_id and move.purchase_order_id.move_id: - cab = move.purchase_order_id.move_id - - if cab: - res['reklas_id'] = cab.id - - res['pay_amt'] = move.amount_total - return res - - - # @api.onchange('reklas_type') - # def _onchange_reklas_type(self): - # if self.reklas_type == 'penjualan': - # invoices = self.env['account.move'].browse(self._context.get('active_ids', [])) - # self.pay_amt = invoices.amount_total + if self.reklas_type == 'penjualan': + invoices = self.env['account.move'].browse(self._context.get('active_ids', [])) + self.pay_amt = invoices.amount_total def create_reklas(self): if not self.reklas_type: raise UserError('Reklas Tipe harus diisi') if not self.reklas_id: raise UserError('Nomor CAB harus diisi') - invoices = self.env['account.move'].browse(self._context.get('active_ids', [])) current_time = datetime.now() - for invoice in invoices: - # Ambil nama PO jika ada - po_name = invoice.purchase_order_id.name if invoice.purchase_order_id else '' - - # Susun nama referensi dengan aman - ref_name = 'REKLAS {} UANG MUKA {} {}{} {}'.format( - self.reklas_id.name or '', - 'PENJUALAN' if self.reklas_type == 'penjualan' else 'PEMBELIAN', - invoice.name or '', - f" - {po_name}" if po_name else '', - invoice.partner_id.name or '' - ) - - # Header jurnal reklas - parameters_header = { - 'ref': ref_name, - 'date': current_time, - 'journal_id': 13 - } + if self.reklas_type == 'penjualan': + ref_name = 'REKLAS '+self.reklas_id.name+" UANG MUKA PENJUALAN "+invoice.name+" "+invoice.partner_id.name + else: + ref_name = 'REKLAS '+self.reklas_id.name+" UANG MUKA PEMBELIAN "+invoice.name+" "+invoice.partner_id.name + if self.reklas_type == 'penjualan': + parameters_header = { + 'ref': ref_name, + 'date': current_time, + 'journal_id': 13 + } + else: + parameters_header = { + 'ref': ref_name, + 'date': current_time, + 'journal_id': 13 + } account_move = request.env['account.move'].create([parameters_header]) _logger.info('Success Reklas with %s' % account_move.name) - # ✅ Set Bill asal sebagai source document - account_move.bill_id = invoice.id - - # Tambahkan info asal invoice ke jurnal (opsional) - account_move.invoice_origin = invoice.name - - # Simpan hubungan balik ke invoice - invoice.reklas_misc_id = account_move.id - - # Buat line debit dan kredit if self.reklas_type == 'penjualan': parameter_debit = { 'move_id': account_move.id, - 'account_id': 668, # penerimaan belum alokasi + 'account_id': 668, # penerimaan belum alokasi 'partner_id': invoice.partner_id.id, 'currency_id': 12, 'debit': self.pay_amt, @@ -134,7 +71,7 @@ class InvoiceReklas(models.TransientModel): 'credit': self.pay_amt, 'name': ref_name } - else: # pembelian + else: parameter_debit = { 'move_id': account_move.id, 'account_id': 438, @@ -153,11 +90,7 @@ class InvoiceReklas(models.TransientModel): 'credit': self.pay_amt, 'name': ref_name } - - # Simpan journal lines request.env['account.move.line'].create([parameter_debit, parameter_credit]) - - # Tampilkan hasil jurnal reklas return { 'name': _('Journal Entries'), 'view_mode': 'form', @@ -167,3 +100,4 @@ class InvoiceReklas(models.TransientModel): 'type': 'ir.actions.act_window', 'res_id': account_move.id } + \ No newline at end of file diff --git a/indoteknik_custom/models/invoice_reklas_penjualan.py b/indoteknik_custom/models/invoice_reklas_penjualan.py index 2f5ee160..80c3ed43 100644 --- a/indoteknik_custom/models/invoice_reklas_penjualan.py +++ b/indoteknik_custom/models/invoice_reklas_penjualan.py @@ -17,70 +17,43 @@ class InvoiceReklasPenjualan(models.TransientModel): def create_reklas_penjualan(self): invoices = self.invoice_reklas_line + current_time = datetime.now() account_move_ids = [] - - for line in invoices: - # Ambil nama SO jika ada - so_name = line.sale_id.name if line.sale_id else '' - - # Susun referensi nama jurnal - ref_name = 'REKLAS {} UANG MUKA PENJUALAN {}{} {}'.format( - line.reklas_id.name or '', - line.name or '', - f" - {so_name}" if so_name else '', - line.partner_id.name or '' - ) - - # Header jurnal + for invoice in invoices: + ref_name = 'REKLAS ' + invoice.reklas_id.name + " UANG MUKA PENJUALAN " + invoice.name + " " + invoice.partner_id.name parameters_header = { 'ref': ref_name, 'date': current_time, - 'journal_id': 13, - # ⬇️ Tambahkan jika tahu invoice asal (name = ID Bill) - 'bill_id': int(line.name) if line.name and line.name.isdigit() else False, + 'journal_id': 13 } account_move = self.env['account.move'].create([parameters_header]) _logger.info('Success Reklas with %s' % account_move.name) - # Simpan info asal (optional) - account_move.invoice_origin = line.name - - # Simpan juga ke `reklas_misc_id` jika ditemukan invoice valid - if line.name and line.name.isdigit(): - invoice_id = self.env['account.move'].browse(int(line.name)) - if invoice_id.exists(): - invoice_id.reklas_misc_id = account_move.id - - # Buat debit kredit line - debit_line = { + parameter_debit = { 'move_id': account_move.id, - 'account_id': 668, # akun penerimaan belum alokasi - 'partner_id': line.partner_id.id, + 'account_id': 668, # uang muka penjualan + 'partner_id': invoice.partner_id.id, 'currency_id': 12, - 'debit': line.pay_amt, + 'debit': invoice.pay_amt, 'credit': 0, 'name': ref_name } - credit_line = { + parameter_credit = { 'move_id': account_move.id, - 'account_id': 395, # akun pengurang - 'partner_id': line.partner_id.id, + 'account_id': 395, + 'partner_id': invoice.partner_id.id, 'currency_id': 12, 'debit': 0, - 'credit': line.pay_amt, + 'credit': invoice.pay_amt, 'name': ref_name } - - self.env['account.move.line'].create([debit_line, credit_line]) + self.env['account.move.line'].create([parameter_debit, parameter_credit]) account_move_ids.append(account_move.id) - - line.unlink() # bersihkan line setelah selesai - - self.unlink() # hapus wizard utama setelah selesai - - # Tampilkan hasil jurnal reklas + invoice.unlink() + + self.unlink() return { 'name': _('Journal Entries'), 'view_mode': 'tree,form', @@ -90,7 +63,6 @@ class InvoiceReklasPenjualan(models.TransientModel): 'domain': [('id', 'in', account_move_ids)], } - class InvoiceReklasPenjualanLine(models.TransientModel): _name = 'invoice.reklas.penjualan.line' _description = "digunakan untuk reklas Uang Muka Penjualan" diff --git a/indoteknik_custom/models/mail_mail.py b/indoteknik_custom/models/mail_mail.py index cbcd883a..82b1fcca 100644 --- a/indoteknik_custom/models/mail_mail.py +++ b/indoteknik_custom/models/mail_mail.py @@ -1,27 +1,12 @@ from odoo import fields, models, api, _ -from datetime import timedelta -import logging -_logger = logging.getLogger(__name__) class MailMail(models.Model): _inherit = 'mail.mail' - @api.model def retry_send_mail(self): - now = fields.Datetime.now() - seven_days_ago = now - timedelta(days=7) - - # Filter hanya email gagal dalam 7 hari terakhir - mails = self.search([ - ('state', 'in', 'exception'), - ('create_date', '>=', seven_days_ago), - ('create_date', '<=', now), + mails = self.env['mail.mail'].search([ + ('state', 'in', ['exception', 'cancel']), ], limit=250) - - _logger.info("Found %s failed emails in last 7 days to retry.", len(mails)) - for mail in mails: - _logger.info("Retrying email ID %s - To: %s - Subject: %s", - mail.id, mail.email_to, mail.subject) mail.state = 'outgoing' diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 004a1fa4..21ca55eb 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -65,7 +65,7 @@ class PurchaseOrder(models.Model): sale_order = fields.Char(string='Sale Order') matches_so = fields.Many2many('sale.order', string='Matches SO', compute='_compute_matches_so') is_create_uangmuka = fields.Boolean(string='Uang Muka?') - move_id = fields.Many2one('account.move', string='Journal Entries Uang Muka', domain=[('move_type', '=', 'entry')]) + move_id = fields.Many2one('account.move', string='Account Move') logbook_bill_id = fields.Many2one('report.logbook.bill', string='Logbook Bill') status_printed = fields.Selection([ ('not_printed', 'Belum Print'), @@ -89,28 +89,6 @@ class PurchaseOrder(models.Model): store_name = fields.Char(string='Nama Toko') purchase_order_count = fields.Integer('Purchase Order Count', related='partner_id.purchase_order_count') - is_cab_visible = fields.Boolean(string='Tampilkan Tombol CAB', compute='_compute_is_cab_visible') - - @api.depends('move_id.state') - def _compute_is_cab_visible(self): - for order in self: - move = order.move_id - order.is_cab_visible = bool(move and move.state == 'posted') - - def action_view_journal_uangmuka(self): - self.ensure_one() - if not self.move_id: - raise UserError("Journal Uang Muka belum tersedia.") - - return { - 'type': 'ir.actions.act_window', - 'name': 'Journal Entry', - 'res_model': 'account.move', - 'res_id': self.move_id.id, - 'view_mode': 'form', - 'target': 'current', - } - # cek payment term def _check_payment_term(self): _logger.info("Check Payment Term Terpanggil") @@ -838,7 +816,7 @@ class PurchaseOrder(models.Model): for line in self.order_line: if not line.so_line_id: continue - if line.so_line_id.vendor_id.id != vendor_po: + if line.so_line_id.vendor_id.id != vendor_po and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'): self.env.user.notify_danger( title='WARNING!!!', message="Produk "+line.product_id.name+" memiliki vendor berbeda dengan SO (Vendor PO: "+str(self.partner_id.name)+", Vendor SO: "+str(line.so_line_id.vendor_id.name)+")", @@ -856,7 +834,7 @@ class PurchaseOrder(models.Model): if self.amount_untaxed >= 50000000 and not self.env.user.id == 21: raise UserError("Hanya Rafly Hanggara yang bisa approve") - if self.total_percent_margin < self.total_so_percent_margin: + if self.total_percent_margin < self.total_so_percent_margin and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser') and not self.env.user.is_leader: self.env.user.notify_danger( title='WARNING!!!', message='Beda Margin dengan Sale Order', @@ -1024,7 +1002,7 @@ class PurchaseOrder(models.Model): self.approval_status_unlock = 'approvedFinance' else: raise UserError("Bisa langsung Confirm, menunggu persetujuan Finance jika ingin unlock PO") - elif self.env.user.is_leader or self.env.user.has_group('indoteknik_custom.group_role_purchasing'): + elif self.env.user.is_leader or self.env.user.has_group('indoteknik_custom.group_role_merchandiser'): raise UserError("Bisa langsung Confirm") elif self.total_percent_margin == self.total_so_percent_margin and self.matches_so and not greater_than_plafon and not different_vendor_message: raise UserError("Bisa langsung Confirm") diff --git a/indoteknik_custom/models/requisition.py b/indoteknik_custom/models/requisition.py index bcdafb12..25133e72 100644 --- a/indoteknik_custom/models/requisition.py +++ b/indoteknik_custom/models/requisition.py @@ -48,8 +48,8 @@ class Requisition(models.Model): is_po = fields.Boolean(string='Is PO') requisition_match = fields.One2many('requisition.purchase.match', 'requisition_id', string='Matches', auto_join=True) sale_order_id = fields.Many2one('sale.order', string='SO', help='harus diisi nomor SO yang ingin digenerate') - sales_approve = fields.Boolean(string='Approval Status', tracking=3, copy=False) - merchandise_approve = fields.Boolean(string='Approval Status', tracking=3, copy=False) + sales_approve = fields.Boolean(string='Sales Approve', tracking=3, copy=False) + merchandise_approve = fields.Boolean(string='Merchandise Approve', tracking=3, copy=False) def generate_requisition_from_so(self): state = ['done', 'sale'] @@ -87,11 +87,15 @@ class Requisition(models.Model): if self.env.user.id == 19 or self.env.user.id == 28: self.sales_approve = True elif self.env.user.id == 21 or self.env.user.id == 28: + if not self.sales_approve: + raise UserError('Darren Belum Approve') self.merchandise_approve = True def create_po_from_requisition(self): - if not self.sales_approve and not self.merchandise_approve: - raise UserError('Harus Di Approve oleh Darren atau Rafly') + if not self.sales_approve: + raise UserError('Harus Di Approve oleh Darren') + if not self.merchandise_approve: + raise UserError('Harus Di Approve oleh Rafly') if not self.requisition_lines: raise UserError('Tidak ada Lines, belum bisa create PO') if self.is_po: diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index baa8207f..fa570819 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -148,8 +148,8 @@ class SaleOrder(models.Model): help="Total Margin in Sales Order Header") total_percent_margin = fields.Float('Total Percent Margin', compute='_compute_total_percent_margin', help="Total % Margin in Sales Order Header") - total_margin_excl_third_party = fields.Float('Before Margin', help="Before Margin in Sales Order Header") - + total_margin_excl_third_party = fields.Float('Before Margin', help="Before Margin in Sales Order Header", + compute='_compute_total_margin_excl_third_party') approval_status = fields.Selection([ ('pengajuan1', 'Approval Manager'), ('pengajuan2', 'Approval Pimpinan'), @@ -340,16 +340,16 @@ class SaleOrder(models.Model): date_unhold = fields.Datetime(string='Date Unhold', tracking=True, readonly=True, help='Waktu ketika SO di Unhold' ) - # def _compute_total_margin_excl_third_party(self): - # for order in self: - # if order.amount_untaxed == 0: - # order.total_margin_excl_third_party = 0 - # continue - # - # # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2) - # order.total_margin_excl_third_party = round((order.total_before_margin / (order.amount_untaxed)) * 100, 2) - # # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed)) * 100, 2) - # + def _compute_total_margin_excl_third_party(self): + for order in self: + if order.amount_untaxed == 0: + order.total_margin_excl_third_party = 0 + continue + + # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2) + order.total_margin_excl_third_party = round((order.total_before_margin / (order.amount_untaxed)) * 100, 2) + # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed)) * 100, 2) + def ask_retur_cancel_purchasing(self): for rec in self: if self.env.user.has_group('indoteknik_custom.group_role_purchasing'): @@ -403,14 +403,14 @@ class SaleOrder(models.Model): if len(tax_sets) > 1: raise ValidationError("Semua produk dalam Sales Order harus memiliki kombinasi pajak yang sama.") - # @api.constrains('fee_third_party', 'delivery_amt', 'biaya_lain_lain', 'ongkir_ke_xpdc') + # @api.constrains('fee_third_party', 'delivery_amt', 'biaya_lain_lain') # def _check_total_margin_excl_third_party(self): # for rec in self: # if rec.fee_third_party == 0 and rec.total_margin_excl_third_party != rec.total_percent_margin: # # Gunakan direct SQL atau flag context untuk menghindari rekursi # self.env.cr.execute(""" - # UPDATE sale_order - # SET total_margin_excl_third_party = %s + # UPDATE sale_order + # SET total_margin_excl_third_party = %s # WHERE id = %s # """, (rec.total_percent_margin, rec.id)) # self.invalidate_cache() @@ -866,6 +866,7 @@ class SaleOrder(models.Model): def _validate_delivery_amt(self): is_indoteknik = self.carrier_id.id == 1 or self.shipping_cost_covered == 'indoteknik' is_active_id = not self.env.context.get('active_id', []) + if is_indoteknik and is_active_id: if self.delivery_amt == 0: if self.carrier_id.id == 1: @@ -1034,11 +1035,11 @@ class SaleOrder(models.Model): line_no += 1 line.line_no = line_no - # def write(self, vals): - # if 'carrier_id' in vals: - # for picking in self.picking_ids: - # if picking.state == 'assigned': - # picking.carrier_id = self.carrier_id + def write(self, vals): + if 'carrier_id' in vals: + for picking in self.picking_ids: + if picking.state == 'assigned': + picking.carrier_id = self.carrier_id def calculate_so_status(self): so_state = ['sale'] @@ -1156,12 +1157,12 @@ class SaleOrder(models.Model): helper_ids_str = self.env['ir.config_parameter'].sudo().get_param('sale.order.user_helper_ids') return helper_ids_str.split(', ') - # def write(self, values): - # helper_ids = self._get_helper_ids() - # if str(self.env.user.id) in helper_ids: - # values['helper_by_id'] = self.env.user.id - # - # return super(SaleOrder, self).write(values) + def write(self, values): + helper_ids = self._get_helper_ids() + if str(self.env.user.id) in helper_ids: + values['helper_by_id'] = self.env.user.id + + return super(SaleOrder, self).write(values) def check_due(self): """To show the due amount and warning stage""" @@ -1692,95 +1693,20 @@ class SaleOrder(models.Model): total_before_margin = sum(line.item_before_margin for line in order.order_line if line.product_id) order.total_before_margin = total_before_margin - # Perhitungan Lama - # def _compute_total_percent_margin(self): - # for order in self: - # if order.amount_untaxed == 0: - # order.total_percent_margin = 0 - # continue - # if order.shipping_cost_covered == 'indoteknik': - # delivery_amt = order.delivery_amt - # else: - # delivery_amt = 0 - # - # net_margin = order.total_margin - order.biaya_lain_lain - # - # order.total_percent_margin = round( - # (net_margin / (order.amount_untaxed - order.fee_third_party)) * 100, 2) - - # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2) - # order.total_percent_margin = round( - # (order.total_margin / (order.amount_untaxed - order.fee_third_party - order.biaya_lain_lain)) * 100, 2) - # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed)) * 100, 2) - def _compute_total_percent_margin(self): for order in self: if order.amount_untaxed == 0: order.total_percent_margin = 0 continue - if order.shipping_cost_covered == 'indoteknik': delivery_amt = order.delivery_amt else: delivery_amt = 0 - net_margin = order.total_margin - order.fee_third_party - order.biaya_lain_lain - - - if order.amount_untaxed > 0: - order.total_percent_margin = round((net_margin / order.amount_untaxed) * 100, 2) - else: - order.total_percent_margin = 0 - - # @api.onchange('biaya_lain_lain') - # def _onchange_biaya_lain_lain(self): - # """Ketika biaya_lain_lain berubah, simpan nilai margin sebelumnya""" - # if hasattr(self, '_origin') and self._origin.id: - # # Hitung margin sebelum biaya_lain_lain ditambahkan - # if self.amount_untaxed > 0: - # original_net_margin = self.total_margin # tanpa biaya_lain_lain - # self.total_margin_excl_third_party = round( - # (original_net_margin / (self.amount_untaxed - self.fee_third_party)) * 100, 2) - - def _prepare_before_margin_values(self, vals): - margin_sebelumnya = {} - - margin_affecting_fields = [ - 'biaya_lain_lain', 'fee_third_party', 'delivery_amt', - 'ongkir_ke_xpdc', 'shipping_cost_covered', 'order_line' - ] - - if not any(field in vals for field in margin_affecting_fields): - return {} - - for order in self: - if order.amount_untaxed <= 0: - continue - - current_before = order.total_margin_excl_third_party or 0 - - # CASE 1: Before margin masih kosong → ambil dari item_percent_margin - if current_before == 0: - line_margin = 0 - for line in order.order_line: - if line.item_percent_margin is not None: - line_margin = line.item_percent_margin - break - margin_sebelumnya[order.id] = line_margin - _logger.info(f"[BEFORE] SO {order.name}: Before margin kosong, ambil dari order line: {line_margin}%") - else: - # CASE 2: Ada perubahan field yang mempengaruhi margin - for field in margin_affecting_fields: - if field in vals: - old_val = getattr(order, field, 0) or 0 - new_val = vals[field] or 0 - if old_val != new_val: - margin_sebelumnya[order.id] = order.total_percent_margin - _logger.info( - f"[BEFORE] SO {order.name}: {field} berubah dari {old_val} ke {new_val}, simpan {order.total_percent_margin}%") - break - - return margin_sebelumnya + # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2) + order.total_percent_margin = round( + (order.total_margin / (order.amount_untaxed - order.fee_third_party - order.biaya_lain_lain)) * 100, 2) + # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed)) * 100, 2) @api.onchange('sales_tax_id') def onchange_sales_tax_id(self): @@ -2071,42 +1997,31 @@ class SaleOrder(models.Model): 'customer_type': partner.customer_type, }) - # def write(self, vals): - # for order in self: - # if order.state in ['sale', 'cancel']: - # if 'order_line' in vals: - # new_lines = vals.get('order_line', []) - # for command in new_lines: - # if command[0] == 0: # A new line is being added - # raise UserError( - # "SO tidak dapat ditambahkan produk baru karena SO sudah menjadi sale order.") - # - # res = super(SaleOrder, self).write(vals) - # # self._check_total_margin_excl_third_party() - # if any(fields in vals for fields in ['delivery_amt', 'carrier_id', 'shipping_cost_covered']): - # self._validate_delivery_amt() - # if any(field in vals for field in ["order_line", "client_order_ref"]): - # self._calculate_etrts_date() - # return res + def write(self, vals): + for order in self: + if order.state in ['sale', 'cancel']: + if 'order_line' in vals: + new_lines = vals.get('order_line', []) + for command in new_lines: + if command[0] == 0: # A new line is being added + raise UserError( + "SO tidak dapat ditambahkan produk baru karena SO sudah menjadi sale order.") + + res = super(SaleOrder, self).write(vals) + # self._check_total_margin_excl_third_party() + if any(fields in vals for fields in ['delivery_amt', 'carrier_id', 'shipping_cost_covered']): + self._validate_delivery_amt() + if any(field in vals for field in ["order_line", "client_order_ref"]): + self._calculate_etrts_date() + return res # @api.depends('commitment_date') def _compute_ready_to_ship_status_detail(self): - def is_empty(val): - """Helper untuk cek data kosong yang umum di Odoo.""" - return val is None or val == "" or val == [] or val == {} - for order in self: - order.ready_to_ship_status_detail = 'On Track' # Default value - - # Skip if no commitment date - if is_empty(order.commitment_date): - continue - eta = order.commitment_date match_lines = self.env['purchase.order.sales.match'].search([ ('sale_id', '=', order.id) ]) - if match_lines: for match in match_lines: po = match.purchase_order_id @@ -2115,77 +2030,17 @@ class SaleOrder(models.Model): ('order_id', '=', po.id), ('product_id', '=', product.id) ], limit=1) - - if is_empty(po_line): - continue - stock_move = self.env['stock.move'].search([ ('purchase_line_id', '=', po_line.id) ], limit=1) - - if is_empty(stock_move) or is_empty(stock_move.picking_id): - continue - picking_in = stock_move.picking_id - result_date = picking_in.date_done - - if is_empty(result_date): - continue - - try: - if result_date < eta: - order.ready_to_ship_status_detail = f"Early (Actual: {result_date.strftime('%m/%d/%Y')})" - else: - order.ready_to_ship_status_detail = f"Delay (Actual: {result_date.strftime('%m/%d/%Y')})" - except Exception as e: - _logger.error(f"Error computing ready to ship status: {str(e)}") - continue - - def write(self, vals): - - margin_sebelumnya = self._prepare_before_margin_values(vals) - - for order in self: - if order.state in ['sale', 'cancel']: - if 'order_line' in vals: - for command in vals.get('order_line', []): - if command[0] == 0: - raise UserError( - "SO tidak dapat ditambahkan produk baru karena SO sudah menjadi sale order.") - - if 'carrier_id' in vals: - for order in self: - for picking in order.picking_ids: - if picking.state == 'assigned': - picking.carrier_id = vals['carrier_id'] - - try: - helper_ids = self._get_helper_ids() - if str(self.env.user.id) in helper_ids: - vals['helper_by_id'] = self.env.user.id - except: - pass - - res = super(SaleOrder, self).write(vals) - - # Update before margin setelah write - if margin_sebelumnya: - for order_id, margin_value in margin_sebelumnya.items(): - _logger.info(f"[UPDATE] SO ID {order_id}: Set before margin ke {margin_value}%") - self.env.cr.execute(""" - UPDATE sale_order - SET total_margin_excl_third_party = %s - WHERE id = %s - """, (margin_value, order_id)) - - self.env.cr.commit() - self.invalidate_cache(['total_margin_excl_third_party']) - - # Validasi setelah write - if any(field in vals for field in ['delivery_amt', 'carrier_id', 'shipping_cost_covered']): - self._validate_delivery_amt() - - if any(field in vals for field in ["order_line", "client_order_ref"]): - self._calculate_etrts_date() - - return res \ No newline at end of file + result_date = picking_in.date_done if picking_in else None + if result_date: + status = "Early" if result_date < eta else "Delay" + result_date_str = result_date.strftime('%m/%d/%Y') + eta_str = eta.strftime('%m/%d/%Y') + order.ready_to_ship_status_detail = f"Expected: {eta_str} | Realtime: {result_date_str} | {status}" + else: + order.ready_to_ship_status_detail = "On Track" + else: + order.ready_to_ship_status_detail = 'On Track' \ No newline at end of file diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index 291940ed..c8066961 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -10,8 +10,6 @@ class SaleOrderLine(models.Model): help="Total Margin in Sales Order Header") item_percent_margin = fields.Float('%Margin', compute='compute_item_margin', help="Total % Margin in Sales Order Header") - item_percent_margin_before = fields.Float('%Margin Before', compute='_compute_item_percent_margin_before', - help="Total % Margin excluding third party in Sales Order Header") initial_discount = fields.Float('Initial Discount') vendor_id = fields.Many2one( 'res.partner', string='Vendor', readonly=True, @@ -136,29 +134,6 @@ class SaleOrderLine(models.Model): else: line.item_percent_margin_without_deduction = 0 - def _compute_item_percent_margin_before(self): - for line in self: - if not line.product_id or line.product_id.type == 'service' \ - or line.price_unit <= 0 or line.product_uom_qty <= 0 \ - or not line.vendor_id: - line.item_percent_margin_before = 0 - continue - - sales_price = line.price_reduce_taxexcl * line.product_uom_qty - - purchase_price = line.purchase_price - if line.purchase_tax_id and line.purchase_tax_id.price_include: - purchase_price = line.purchase_price / 1.11 - - purchase_price = purchase_price * line.product_uom_qty - - margin_before = sales_price - purchase_price - - if sales_price > 0: - line.item_percent_margin_before = round((margin_before / sales_price), 2) * 100 - else: - line.item_percent_margin_before = 0 - def compute_item_margin(self): for line in self: if not line.product_id or line.product_id.type == 'service' \ diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 9dfc8b78..a215eb74 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -529,7 +529,13 @@ class StockPicking(models.Model): record.kgx_pod_photo = "No image available." def action_fetch_lalamove_order(self): - for picking in self: + pickings = self.env['stock.picking'].search([ + ('picking_type_code', '=', 'outgoing'), + ('state', '=', 'done'), + ('carrier_id', '=', 9), + ('lalamove_order_id', '!=', False) + ]) + for picking in pickings: try: order_id = picking.lalamove_order_id apikey = self.env['ir.config_parameter'].sudo().get_param('lalamove.apikey') @@ -578,7 +584,6 @@ class StockPicking(models.Model): self.lalamove_phone = phone self.lalamove_status = pod.get("status") self.lalamove_delivered_at = delivered_at_dt - self.driver_arrival_date = delivered_at_dt return data raise UserError("No delivered data found in Lalamove response.") diff --git a/indoteknik_custom/models/uangmuka_pembelian.py b/indoteknik_custom/models/uangmuka_pembelian.py index 13d51dcf..ba41f814 100644 --- a/indoteknik_custom/models/uangmuka_pembelian.py +++ b/indoteknik_custom/models/uangmuka_pembelian.py @@ -57,8 +57,6 @@ class UangmukaPembelian(models.TransientModel): account_move = request.env['account.move'].create([param_header]) _logger.info('Success Create Uang Muka Pembelian %s' % account_move.name) - account_move.purchase_order_id = order.id # isi field purchase_order_id - if order.partner_id.parent_id: partner_id = order.partner_id.parent_id.id else: -- cgit v1.2.3 From 741ccc28589388c1214f5da44b9a7d56ef32680c Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Thu, 12 Jun 2025 16:19:09 +0700 Subject: Reapply " commit" This reverts commit 6616f73259c9e146449af3f2579263b42270bf8b. --- indoteknik_custom/models/account_move.py | 19 ++ indoteknik_custom/models/invoice_reklas.py | 112 +++++++-- .../models/invoice_reklas_penjualan.py | 60 +++-- indoteknik_custom/models/mail_mail.py | 19 +- indoteknik_custom/models/purchase_order.py | 30 ++- indoteknik_custom/models/requisition.py | 12 +- indoteknik_custom/models/sale_order.py | 261 ++++++++++++++++----- indoteknik_custom/models/sale_order_line.py | 25 ++ indoteknik_custom/models/stock_picking.py | 9 +- indoteknik_custom/models/uangmuka_pembelian.py | 2 + 10 files changed, 431 insertions(+), 118 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 30de67be..54eaabcf 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -67,6 +67,25 @@ class AccountMove(models.Model): is_hr = fields.Boolean(string="Is HR?", default=False) purchase_order_id = fields.Many2one('purchase.order', string='Purchase Order') length_of_payment = fields.Integer(string="Length of Payment", compute='compute_length_of_payment') + reklas_misc_id = fields.Many2one('account.move', string='Journal Entries Reklas') + # Di model account.move + bill_id = fields.Many2one('account.move', string='Vendor Bill', domain=[('move_type', '=', 'in_invoice')], help='Bill asal dari proses reklas ini') + + + # def name_get(self): + # result = [] + # for move in self: + # if move.move_type == 'entry': + # # Jika masih draft, tampilkan 'Draft CAB' + # if move.state == 'draft': + # label = 'Draft CAB' + # else: + # label = move.name + # result.append((move.id, label)) + # else: + # # Untuk invoice dan lainnya, pakai default + # result.append((move.id, move.display_name)) + # return result def compute_length_of_payment(self): for rec in self: diff --git a/indoteknik_custom/models/invoice_reklas.py b/indoteknik_custom/models/invoice_reklas.py index d10d4c31..5145e098 100644 --- a/indoteknik_custom/models/invoice_reklas.py +++ b/indoteknik_custom/models/invoice_reklas.py @@ -11,7 +11,7 @@ _logger = logging.getLogger(__name__) class InvoiceReklas(models.TransientModel): _name = 'invoice.reklas' _description = "digunakan untuk reklas Uang Muka Penjualan" - reklas_id = fields.Many2one('account.move', string='Nomor CAB') + reklas_id = fields.Many2one('account.move', string='Nomor CAB', domain="[('move_type','=','entry')]") pay_amt = fields.Float(string='Yang dibayarkan') reklas_type = fields.Selection([ ('penjualan', 'Penjualan'), @@ -20,42 +20,105 @@ class InvoiceReklas(models.TransientModel): @api.onchange('reklas_type') def _onchange_reklas_type(self): - if self.reklas_type == 'penjualan': - invoices = self.env['account.move'].browse(self._context.get('active_ids', [])) - self.pay_amt = invoices.amount_total + active_ids = self._context.get('active_ids', []) + if not active_ids: + return + + move = self.env['account.move'].browse(active_ids[0]) + cab = False + + # Deteksi dari mana asal CAB + if move.move_type == 'entry': + cab = move + elif move.move_type == 'in_invoice': + if move.reklas_misc_id: + cab = move.reklas_misc_id + elif move.purchase_order_id and move.purchase_order_id.move_id: + cab = move.purchase_order_id.move_id + + # Isi field Nomor CAB jika ditemukan + if cab: + self.reklas_id = cab.id + + # Nilai yang dibayarkan harus tetap ambil dari invoice/bill + self.pay_amt = move.amount_total + + + @api.model + def default_get(self, fields): + res = super().default_get(fields) + active_ids = self._context.get('active_ids', []) + if active_ids: + move = self.env['account.move'].browse(active_ids[0]) + cab = False + + if move.move_type == 'entry': + cab = move + elif move.move_type == 'in_invoice': + if move.reklas_misc_id: + cab = move.reklas_misc_id + elif move.purchase_order_id and move.purchase_order_id.move_id: + cab = move.purchase_order_id.move_id + + if cab: + res['reklas_id'] = cab.id + + res['pay_amt'] = move.amount_total + return res + + + # @api.onchange('reklas_type') + # def _onchange_reklas_type(self): + # if self.reklas_type == 'penjualan': + # invoices = self.env['account.move'].browse(self._context.get('active_ids', [])) + # self.pay_amt = invoices.amount_total def create_reklas(self): if not self.reklas_type: raise UserError('Reklas Tipe harus diisi') if not self.reklas_id: raise UserError('Nomor CAB harus diisi') + invoices = self.env['account.move'].browse(self._context.get('active_ids', [])) current_time = datetime.now() + for invoice in invoices: - if self.reklas_type == 'penjualan': - ref_name = 'REKLAS '+self.reklas_id.name+" UANG MUKA PENJUALAN "+invoice.name+" "+invoice.partner_id.name - else: - ref_name = 'REKLAS '+self.reklas_id.name+" UANG MUKA PEMBELIAN "+invoice.name+" "+invoice.partner_id.name - if self.reklas_type == 'penjualan': - parameters_header = { - 'ref': ref_name, - 'date': current_time, - 'journal_id': 13 - } - else: - parameters_header = { - 'ref': ref_name, - 'date': current_time, - 'journal_id': 13 - } + # Ambil nama PO jika ada + po_name = invoice.purchase_order_id.name if invoice.purchase_order_id else '' + + # Susun nama referensi dengan aman + ref_name = 'REKLAS {} UANG MUKA {} {}{} {}'.format( + self.reklas_id.name or '', + 'PENJUALAN' if self.reklas_type == 'penjualan' else 'PEMBELIAN', + invoice.name or '', + f" - {po_name}" if po_name else '', + invoice.partner_id.name or '' + ) + + # Header jurnal reklas + parameters_header = { + 'ref': ref_name, + 'date': current_time, + 'journal_id': 13 + } account_move = request.env['account.move'].create([parameters_header]) _logger.info('Success Reklas with %s' % account_move.name) + # ✅ Set Bill asal sebagai source document + account_move.bill_id = invoice.id + + # Tambahkan info asal invoice ke jurnal (opsional) + account_move.invoice_origin = invoice.name + + # Simpan hubungan balik ke invoice + invoice.reklas_misc_id = account_move.id + + # Buat line debit dan kredit if self.reklas_type == 'penjualan': parameter_debit = { 'move_id': account_move.id, - 'account_id': 668, # penerimaan belum alokasi + 'account_id': 668, # penerimaan belum alokasi 'partner_id': invoice.partner_id.id, 'currency_id': 12, 'debit': self.pay_amt, @@ -71,7 +134,7 @@ class InvoiceReklas(models.TransientModel): 'credit': self.pay_amt, 'name': ref_name } - else: + else: # pembelian parameter_debit = { 'move_id': account_move.id, 'account_id': 438, @@ -90,7 +153,11 @@ class InvoiceReklas(models.TransientModel): 'credit': self.pay_amt, 'name': ref_name } + + # Simpan journal lines request.env['account.move.line'].create([parameter_debit, parameter_credit]) + + # Tampilkan hasil jurnal reklas return { 'name': _('Journal Entries'), 'view_mode': 'form', @@ -100,4 +167,3 @@ class InvoiceReklas(models.TransientModel): 'type': 'ir.actions.act_window', 'res_id': account_move.id } - \ No newline at end of file diff --git a/indoteknik_custom/models/invoice_reklas_penjualan.py b/indoteknik_custom/models/invoice_reklas_penjualan.py index 80c3ed43..2f5ee160 100644 --- a/indoteknik_custom/models/invoice_reklas_penjualan.py +++ b/indoteknik_custom/models/invoice_reklas_penjualan.py @@ -17,43 +17,70 @@ class InvoiceReklasPenjualan(models.TransientModel): def create_reklas_penjualan(self): invoices = self.invoice_reklas_line - current_time = datetime.now() account_move_ids = [] - for invoice in invoices: - ref_name = 'REKLAS ' + invoice.reklas_id.name + " UANG MUKA PENJUALAN " + invoice.name + " " + invoice.partner_id.name + + for line in invoices: + # Ambil nama SO jika ada + so_name = line.sale_id.name if line.sale_id else '' + + # Susun referensi nama jurnal + ref_name = 'REKLAS {} UANG MUKA PENJUALAN {}{} {}'.format( + line.reklas_id.name or '', + line.name or '', + f" - {so_name}" if so_name else '', + line.partner_id.name or '' + ) + + # Header jurnal parameters_header = { 'ref': ref_name, 'date': current_time, - 'journal_id': 13 + 'journal_id': 13, + # ⬇️ Tambahkan jika tahu invoice asal (name = ID Bill) + 'bill_id': int(line.name) if line.name and line.name.isdigit() else False, } account_move = self.env['account.move'].create([parameters_header]) _logger.info('Success Reklas with %s' % account_move.name) - parameter_debit = { + # Simpan info asal (optional) + account_move.invoice_origin = line.name + + # Simpan juga ke `reklas_misc_id` jika ditemukan invoice valid + if line.name and line.name.isdigit(): + invoice_id = self.env['account.move'].browse(int(line.name)) + if invoice_id.exists(): + invoice_id.reklas_misc_id = account_move.id + + # Buat debit kredit line + debit_line = { 'move_id': account_move.id, - 'account_id': 668, # uang muka penjualan - 'partner_id': invoice.partner_id.id, + 'account_id': 668, # akun penerimaan belum alokasi + 'partner_id': line.partner_id.id, 'currency_id': 12, - 'debit': invoice.pay_amt, + 'debit': line.pay_amt, 'credit': 0, 'name': ref_name } - parameter_credit = { + credit_line = { 'move_id': account_move.id, - 'account_id': 395, - 'partner_id': invoice.partner_id.id, + 'account_id': 395, # akun pengurang + 'partner_id': line.partner_id.id, 'currency_id': 12, 'debit': 0, - 'credit': invoice.pay_amt, + 'credit': line.pay_amt, 'name': ref_name } - self.env['account.move.line'].create([parameter_debit, parameter_credit]) + + self.env['account.move.line'].create([debit_line, credit_line]) account_move_ids.append(account_move.id) - invoice.unlink() - - self.unlink() + + line.unlink() # bersihkan line setelah selesai + + self.unlink() # hapus wizard utama setelah selesai + + # Tampilkan hasil jurnal reklas return { 'name': _('Journal Entries'), 'view_mode': 'tree,form', @@ -63,6 +90,7 @@ class InvoiceReklasPenjualan(models.TransientModel): 'domain': [('id', 'in', account_move_ids)], } + class InvoiceReklasPenjualanLine(models.TransientModel): _name = 'invoice.reklas.penjualan.line' _description = "digunakan untuk reklas Uang Muka Penjualan" diff --git a/indoteknik_custom/models/mail_mail.py b/indoteknik_custom/models/mail_mail.py index 82b1fcca..cbcd883a 100644 --- a/indoteknik_custom/models/mail_mail.py +++ b/indoteknik_custom/models/mail_mail.py @@ -1,12 +1,27 @@ from odoo import fields, models, api, _ +from datetime import timedelta +import logging +_logger = logging.getLogger(__name__) class MailMail(models.Model): _inherit = 'mail.mail' + @api.model def retry_send_mail(self): - mails = self.env['mail.mail'].search([ - ('state', 'in', ['exception', 'cancel']), + now = fields.Datetime.now() + seven_days_ago = now - timedelta(days=7) + + # Filter hanya email gagal dalam 7 hari terakhir + mails = self.search([ + ('state', 'in', 'exception'), + ('create_date', '>=', seven_days_ago), + ('create_date', '<=', now), ], limit=250) + + _logger.info("Found %s failed emails in last 7 days to retry.", len(mails)) + for mail in mails: + _logger.info("Retrying email ID %s - To: %s - Subject: %s", + mail.id, mail.email_to, mail.subject) mail.state = 'outgoing' diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 21ca55eb..004a1fa4 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -65,7 +65,7 @@ class PurchaseOrder(models.Model): sale_order = fields.Char(string='Sale Order') matches_so = fields.Many2many('sale.order', string='Matches SO', compute='_compute_matches_so') is_create_uangmuka = fields.Boolean(string='Uang Muka?') - move_id = fields.Many2one('account.move', string='Account Move') + move_id = fields.Many2one('account.move', string='Journal Entries Uang Muka', domain=[('move_type', '=', 'entry')]) logbook_bill_id = fields.Many2one('report.logbook.bill', string='Logbook Bill') status_printed = fields.Selection([ ('not_printed', 'Belum Print'), @@ -89,6 +89,28 @@ class PurchaseOrder(models.Model): store_name = fields.Char(string='Nama Toko') purchase_order_count = fields.Integer('Purchase Order Count', related='partner_id.purchase_order_count') + is_cab_visible = fields.Boolean(string='Tampilkan Tombol CAB', compute='_compute_is_cab_visible') + + @api.depends('move_id.state') + def _compute_is_cab_visible(self): + for order in self: + move = order.move_id + order.is_cab_visible = bool(move and move.state == 'posted') + + def action_view_journal_uangmuka(self): + self.ensure_one() + if not self.move_id: + raise UserError("Journal Uang Muka belum tersedia.") + + return { + 'type': 'ir.actions.act_window', + 'name': 'Journal Entry', + 'res_model': 'account.move', + 'res_id': self.move_id.id, + 'view_mode': 'form', + 'target': 'current', + } + # cek payment term def _check_payment_term(self): _logger.info("Check Payment Term Terpanggil") @@ -816,7 +838,7 @@ class PurchaseOrder(models.Model): for line in self.order_line: if not line.so_line_id: continue - if line.so_line_id.vendor_id.id != vendor_po and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'): + if line.so_line_id.vendor_id.id != vendor_po: self.env.user.notify_danger( title='WARNING!!!', message="Produk "+line.product_id.name+" memiliki vendor berbeda dengan SO (Vendor PO: "+str(self.partner_id.name)+", Vendor SO: "+str(line.so_line_id.vendor_id.name)+")", @@ -834,7 +856,7 @@ class PurchaseOrder(models.Model): if self.amount_untaxed >= 50000000 and not self.env.user.id == 21: raise UserError("Hanya Rafly Hanggara yang bisa approve") - if self.total_percent_margin < self.total_so_percent_margin and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser') and not self.env.user.is_leader: + if self.total_percent_margin < self.total_so_percent_margin: self.env.user.notify_danger( title='WARNING!!!', message='Beda Margin dengan Sale Order', @@ -1002,7 +1024,7 @@ class PurchaseOrder(models.Model): self.approval_status_unlock = 'approvedFinance' else: raise UserError("Bisa langsung Confirm, menunggu persetujuan Finance jika ingin unlock PO") - elif self.env.user.is_leader or self.env.user.has_group('indoteknik_custom.group_role_merchandiser'): + elif self.env.user.is_leader or self.env.user.has_group('indoteknik_custom.group_role_purchasing'): raise UserError("Bisa langsung Confirm") elif self.total_percent_margin == self.total_so_percent_margin and self.matches_so and not greater_than_plafon and not different_vendor_message: raise UserError("Bisa langsung Confirm") diff --git a/indoteknik_custom/models/requisition.py b/indoteknik_custom/models/requisition.py index 25133e72..bcdafb12 100644 --- a/indoteknik_custom/models/requisition.py +++ b/indoteknik_custom/models/requisition.py @@ -48,8 +48,8 @@ class Requisition(models.Model): is_po = fields.Boolean(string='Is PO') requisition_match = fields.One2many('requisition.purchase.match', 'requisition_id', string='Matches', auto_join=True) sale_order_id = fields.Many2one('sale.order', string='SO', help='harus diisi nomor SO yang ingin digenerate') - sales_approve = fields.Boolean(string='Sales Approve', tracking=3, copy=False) - merchandise_approve = fields.Boolean(string='Merchandise Approve', tracking=3, copy=False) + sales_approve = fields.Boolean(string='Approval Status', tracking=3, copy=False) + merchandise_approve = fields.Boolean(string='Approval Status', tracking=3, copy=False) def generate_requisition_from_so(self): state = ['done', 'sale'] @@ -87,15 +87,11 @@ class Requisition(models.Model): if self.env.user.id == 19 or self.env.user.id == 28: self.sales_approve = True elif self.env.user.id == 21 or self.env.user.id == 28: - if not self.sales_approve: - raise UserError('Darren Belum Approve') self.merchandise_approve = True def create_po_from_requisition(self): - if not self.sales_approve: - raise UserError('Harus Di Approve oleh Darren') - if not self.merchandise_approve: - raise UserError('Harus Di Approve oleh Rafly') + if not self.sales_approve and not self.merchandise_approve: + raise UserError('Harus Di Approve oleh Darren atau Rafly') if not self.requisition_lines: raise UserError('Tidak ada Lines, belum bisa create PO') if self.is_po: diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index fa570819..baa8207f 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -148,8 +148,8 @@ class SaleOrder(models.Model): help="Total Margin in Sales Order Header") total_percent_margin = fields.Float('Total Percent Margin', compute='_compute_total_percent_margin', help="Total % Margin in Sales Order Header") - total_margin_excl_third_party = fields.Float('Before Margin', help="Before Margin in Sales Order Header", - compute='_compute_total_margin_excl_third_party') + total_margin_excl_third_party = fields.Float('Before Margin', help="Before Margin in Sales Order Header") + approval_status = fields.Selection([ ('pengajuan1', 'Approval Manager'), ('pengajuan2', 'Approval Pimpinan'), @@ -340,16 +340,16 @@ class SaleOrder(models.Model): date_unhold = fields.Datetime(string='Date Unhold', tracking=True, readonly=True, help='Waktu ketika SO di Unhold' ) - def _compute_total_margin_excl_third_party(self): - for order in self: - if order.amount_untaxed == 0: - order.total_margin_excl_third_party = 0 - continue - - # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2) - order.total_margin_excl_third_party = round((order.total_before_margin / (order.amount_untaxed)) * 100, 2) - # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed)) * 100, 2) - + # def _compute_total_margin_excl_third_party(self): + # for order in self: + # if order.amount_untaxed == 0: + # order.total_margin_excl_third_party = 0 + # continue + # + # # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2) + # order.total_margin_excl_third_party = round((order.total_before_margin / (order.amount_untaxed)) * 100, 2) + # # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed)) * 100, 2) + # def ask_retur_cancel_purchasing(self): for rec in self: if self.env.user.has_group('indoteknik_custom.group_role_purchasing'): @@ -403,14 +403,14 @@ class SaleOrder(models.Model): if len(tax_sets) > 1: raise ValidationError("Semua produk dalam Sales Order harus memiliki kombinasi pajak yang sama.") - # @api.constrains('fee_third_party', 'delivery_amt', 'biaya_lain_lain') + # @api.constrains('fee_third_party', 'delivery_amt', 'biaya_lain_lain', 'ongkir_ke_xpdc') # def _check_total_margin_excl_third_party(self): # for rec in self: # if rec.fee_third_party == 0 and rec.total_margin_excl_third_party != rec.total_percent_margin: # # Gunakan direct SQL atau flag context untuk menghindari rekursi # self.env.cr.execute(""" - # UPDATE sale_order - # SET total_margin_excl_third_party = %s + # UPDATE sale_order + # SET total_margin_excl_third_party = %s # WHERE id = %s # """, (rec.total_percent_margin, rec.id)) # self.invalidate_cache() @@ -866,7 +866,6 @@ class SaleOrder(models.Model): def _validate_delivery_amt(self): is_indoteknik = self.carrier_id.id == 1 or self.shipping_cost_covered == 'indoteknik' is_active_id = not self.env.context.get('active_id', []) - if is_indoteknik and is_active_id: if self.delivery_amt == 0: if self.carrier_id.id == 1: @@ -1035,11 +1034,11 @@ class SaleOrder(models.Model): line_no += 1 line.line_no = line_no - def write(self, vals): - if 'carrier_id' in vals: - for picking in self.picking_ids: - if picking.state == 'assigned': - picking.carrier_id = self.carrier_id + # def write(self, vals): + # if 'carrier_id' in vals: + # for picking in self.picking_ids: + # if picking.state == 'assigned': + # picking.carrier_id = self.carrier_id def calculate_so_status(self): so_state = ['sale'] @@ -1157,12 +1156,12 @@ class SaleOrder(models.Model): helper_ids_str = self.env['ir.config_parameter'].sudo().get_param('sale.order.user_helper_ids') return helper_ids_str.split(', ') - def write(self, values): - helper_ids = self._get_helper_ids() - if str(self.env.user.id) in helper_ids: - values['helper_by_id'] = self.env.user.id - - return super(SaleOrder, self).write(values) + # def write(self, values): + # helper_ids = self._get_helper_ids() + # if str(self.env.user.id) in helper_ids: + # values['helper_by_id'] = self.env.user.id + # + # return super(SaleOrder, self).write(values) def check_due(self): """To show the due amount and warning stage""" @@ -1693,20 +1692,95 @@ class SaleOrder(models.Model): total_before_margin = sum(line.item_before_margin for line in order.order_line if line.product_id) order.total_before_margin = total_before_margin + # Perhitungan Lama + # def _compute_total_percent_margin(self): + # for order in self: + # if order.amount_untaxed == 0: + # order.total_percent_margin = 0 + # continue + # if order.shipping_cost_covered == 'indoteknik': + # delivery_amt = order.delivery_amt + # else: + # delivery_amt = 0 + # + # net_margin = order.total_margin - order.biaya_lain_lain + # + # order.total_percent_margin = round( + # (net_margin / (order.amount_untaxed - order.fee_third_party)) * 100, 2) + + # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2) + # order.total_percent_margin = round( + # (order.total_margin / (order.amount_untaxed - order.fee_third_party - order.biaya_lain_lain)) * 100, 2) + # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed)) * 100, 2) + def _compute_total_percent_margin(self): for order in self: if order.amount_untaxed == 0: order.total_percent_margin = 0 continue + if order.shipping_cost_covered == 'indoteknik': delivery_amt = order.delivery_amt else: delivery_amt = 0 - # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2) - order.total_percent_margin = round( - (order.total_margin / (order.amount_untaxed - order.fee_third_party - order.biaya_lain_lain)) * 100, 2) - # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed)) * 100, 2) + net_margin = order.total_margin - order.fee_third_party - order.biaya_lain_lain + + + if order.amount_untaxed > 0: + order.total_percent_margin = round((net_margin / order.amount_untaxed) * 100, 2) + else: + order.total_percent_margin = 0 + + # @api.onchange('biaya_lain_lain') + # def _onchange_biaya_lain_lain(self): + # """Ketika biaya_lain_lain berubah, simpan nilai margin sebelumnya""" + # if hasattr(self, '_origin') and self._origin.id: + # # Hitung margin sebelum biaya_lain_lain ditambahkan + # if self.amount_untaxed > 0: + # original_net_margin = self.total_margin # tanpa biaya_lain_lain + # self.total_margin_excl_third_party = round( + # (original_net_margin / (self.amount_untaxed - self.fee_third_party)) * 100, 2) + + def _prepare_before_margin_values(self, vals): + margin_sebelumnya = {} + + margin_affecting_fields = [ + 'biaya_lain_lain', 'fee_third_party', 'delivery_amt', + 'ongkir_ke_xpdc', 'shipping_cost_covered', 'order_line' + ] + + if not any(field in vals for field in margin_affecting_fields): + return {} + + for order in self: + if order.amount_untaxed <= 0: + continue + + current_before = order.total_margin_excl_third_party or 0 + + # CASE 1: Before margin masih kosong → ambil dari item_percent_margin + if current_before == 0: + line_margin = 0 + for line in order.order_line: + if line.item_percent_margin is not None: + line_margin = line.item_percent_margin + break + margin_sebelumnya[order.id] = line_margin + _logger.info(f"[BEFORE] SO {order.name}: Before margin kosong, ambil dari order line: {line_margin}%") + else: + # CASE 2: Ada perubahan field yang mempengaruhi margin + for field in margin_affecting_fields: + if field in vals: + old_val = getattr(order, field, 0) or 0 + new_val = vals[field] or 0 + if old_val != new_val: + margin_sebelumnya[order.id] = order.total_percent_margin + _logger.info( + f"[BEFORE] SO {order.name}: {field} berubah dari {old_val} ke {new_val}, simpan {order.total_percent_margin}%") + break + + return margin_sebelumnya @api.onchange('sales_tax_id') def onchange_sales_tax_id(self): @@ -1997,31 +2071,42 @@ class SaleOrder(models.Model): 'customer_type': partner.customer_type, }) - def write(self, vals): - for order in self: - if order.state in ['sale', 'cancel']: - if 'order_line' in vals: - new_lines = vals.get('order_line', []) - for command in new_lines: - if command[0] == 0: # A new line is being added - raise UserError( - "SO tidak dapat ditambahkan produk baru karena SO sudah menjadi sale order.") - - res = super(SaleOrder, self).write(vals) - # self._check_total_margin_excl_third_party() - if any(fields in vals for fields in ['delivery_amt', 'carrier_id', 'shipping_cost_covered']): - self._validate_delivery_amt() - if any(field in vals for field in ["order_line", "client_order_ref"]): - self._calculate_etrts_date() - return res + # def write(self, vals): + # for order in self: + # if order.state in ['sale', 'cancel']: + # if 'order_line' in vals: + # new_lines = vals.get('order_line', []) + # for command in new_lines: + # if command[0] == 0: # A new line is being added + # raise UserError( + # "SO tidak dapat ditambahkan produk baru karena SO sudah menjadi sale order.") + # + # res = super(SaleOrder, self).write(vals) + # # self._check_total_margin_excl_third_party() + # if any(fields in vals for fields in ['delivery_amt', 'carrier_id', 'shipping_cost_covered']): + # self._validate_delivery_amt() + # if any(field in vals for field in ["order_line", "client_order_ref"]): + # self._calculate_etrts_date() + # return res # @api.depends('commitment_date') def _compute_ready_to_ship_status_detail(self): + def is_empty(val): + """Helper untuk cek data kosong yang umum di Odoo.""" + return val is None or val == "" or val == [] or val == {} + for order in self: + order.ready_to_ship_status_detail = 'On Track' # Default value + + # Skip if no commitment date + if is_empty(order.commitment_date): + continue + eta = order.commitment_date match_lines = self.env['purchase.order.sales.match'].search([ ('sale_id', '=', order.id) ]) + if match_lines: for match in match_lines: po = match.purchase_order_id @@ -2030,17 +2115,77 @@ class SaleOrder(models.Model): ('order_id', '=', po.id), ('product_id', '=', product.id) ], limit=1) + + if is_empty(po_line): + continue + stock_move = self.env['stock.move'].search([ ('purchase_line_id', '=', po_line.id) ], limit=1) + + if is_empty(stock_move) or is_empty(stock_move.picking_id): + continue + picking_in = stock_move.picking_id - result_date = picking_in.date_done if picking_in else None - if result_date: - status = "Early" if result_date < eta else "Delay" - result_date_str = result_date.strftime('%m/%d/%Y') - eta_str = eta.strftime('%m/%d/%Y') - order.ready_to_ship_status_detail = f"Expected: {eta_str} | Realtime: {result_date_str} | {status}" - else: - order.ready_to_ship_status_detail = "On Track" - else: - order.ready_to_ship_status_detail = 'On Track' \ No newline at end of file + result_date = picking_in.date_done + + if is_empty(result_date): + continue + + try: + if result_date < eta: + order.ready_to_ship_status_detail = f"Early (Actual: {result_date.strftime('%m/%d/%Y')})" + else: + order.ready_to_ship_status_detail = f"Delay (Actual: {result_date.strftime('%m/%d/%Y')})" + except Exception as e: + _logger.error(f"Error computing ready to ship status: {str(e)}") + continue + + def write(self, vals): + + margin_sebelumnya = self._prepare_before_margin_values(vals) + + for order in self: + if order.state in ['sale', 'cancel']: + if 'order_line' in vals: + for command in vals.get('order_line', []): + if command[0] == 0: + raise UserError( + "SO tidak dapat ditambahkan produk baru karena SO sudah menjadi sale order.") + + if 'carrier_id' in vals: + for order in self: + for picking in order.picking_ids: + if picking.state == 'assigned': + picking.carrier_id = vals['carrier_id'] + + try: + helper_ids = self._get_helper_ids() + if str(self.env.user.id) in helper_ids: + vals['helper_by_id'] = self.env.user.id + except: + pass + + res = super(SaleOrder, self).write(vals) + + # Update before margin setelah write + if margin_sebelumnya: + for order_id, margin_value in margin_sebelumnya.items(): + _logger.info(f"[UPDATE] SO ID {order_id}: Set before margin ke {margin_value}%") + self.env.cr.execute(""" + UPDATE sale_order + SET total_margin_excl_third_party = %s + WHERE id = %s + """, (margin_value, order_id)) + + self.env.cr.commit() + self.invalidate_cache(['total_margin_excl_third_party']) + + # Validasi setelah write + if any(field in vals for field in ['delivery_amt', 'carrier_id', 'shipping_cost_covered']): + self._validate_delivery_amt() + + if any(field in vals for field in ["order_line", "client_order_ref"]): + self._calculate_etrts_date() + + return res \ No newline at end of file diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index c8066961..291940ed 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -10,6 +10,8 @@ class SaleOrderLine(models.Model): help="Total Margin in Sales Order Header") item_percent_margin = fields.Float('%Margin', compute='compute_item_margin', help="Total % Margin in Sales Order Header") + item_percent_margin_before = fields.Float('%Margin Before', compute='_compute_item_percent_margin_before', + help="Total % Margin excluding third party in Sales Order Header") initial_discount = fields.Float('Initial Discount') vendor_id = fields.Many2one( 'res.partner', string='Vendor', readonly=True, @@ -134,6 +136,29 @@ class SaleOrderLine(models.Model): else: line.item_percent_margin_without_deduction = 0 + def _compute_item_percent_margin_before(self): + for line in self: + if not line.product_id or line.product_id.type == 'service' \ + or line.price_unit <= 0 or line.product_uom_qty <= 0 \ + or not line.vendor_id: + line.item_percent_margin_before = 0 + continue + + sales_price = line.price_reduce_taxexcl * line.product_uom_qty + + purchase_price = line.purchase_price + if line.purchase_tax_id and line.purchase_tax_id.price_include: + purchase_price = line.purchase_price / 1.11 + + purchase_price = purchase_price * line.product_uom_qty + + margin_before = sales_price - purchase_price + + if sales_price > 0: + line.item_percent_margin_before = round((margin_before / sales_price), 2) * 100 + else: + line.item_percent_margin_before = 0 + def compute_item_margin(self): for line in self: if not line.product_id or line.product_id.type == 'service' \ diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index a215eb74..9dfc8b78 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -529,13 +529,7 @@ class StockPicking(models.Model): record.kgx_pod_photo = "No image available." def action_fetch_lalamove_order(self): - pickings = self.env['stock.picking'].search([ - ('picking_type_code', '=', 'outgoing'), - ('state', '=', 'done'), - ('carrier_id', '=', 9), - ('lalamove_order_id', '!=', False) - ]) - for picking in pickings: + for picking in self: try: order_id = picking.lalamove_order_id apikey = self.env['ir.config_parameter'].sudo().get_param('lalamove.apikey') @@ -584,6 +578,7 @@ class StockPicking(models.Model): self.lalamove_phone = phone self.lalamove_status = pod.get("status") self.lalamove_delivered_at = delivered_at_dt + self.driver_arrival_date = delivered_at_dt return data raise UserError("No delivered data found in Lalamove response.") diff --git a/indoteknik_custom/models/uangmuka_pembelian.py b/indoteknik_custom/models/uangmuka_pembelian.py index ba41f814..13d51dcf 100644 --- a/indoteknik_custom/models/uangmuka_pembelian.py +++ b/indoteknik_custom/models/uangmuka_pembelian.py @@ -57,6 +57,8 @@ class UangmukaPembelian(models.TransientModel): account_move = request.env['account.move'].create([param_header]) _logger.info('Success Create Uang Muka Pembelian %s' % account_move.name) + account_move.purchase_order_id = order.id # isi field purchase_order_id + if order.partner_id.parent_id: partner_id = order.partner_id.parent_id.id else: -- cgit v1.2.3 From a921017a829ebef8442740fac964260d98566e6a Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 12 Jun 2025 19:26:46 +0700 Subject: (andri) try gmaps sebagai pengganti openstreetmaps --- indoteknik_custom/models/res_partner.py | 77 +++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index eeb8b67d..82aa1134 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -4,6 +4,8 @@ from datetime import datetime from odoo.http import request import re import requests +import logging +_logger = logging.getLogger(__name__) class GroupPartner(models.Model): _name = 'group.partner' @@ -583,3 +585,78 @@ class ResPartner(models.Model): raise UserError("Tidak ditemukan hasil geocode untuk alamat tersebut.") else: raise UserError("Permintaan ke Google Maps gagal. Periksa koneksi internet atau API Key.") + + # def _update_address_from_coords(self): + # for rec in self: + # if rec.latitude and rec.longtitude: + # address = self.reverse_geocode(rec.latitude, rec.longtitude) + # if not address: + # continue + + # updates = { + # 'street': address.get('road') or '', + # 'zip': address.get('postcode') or '', + # 'city': address.get('city') or address.get('town') or address.get('village') or '', + # } + + # # Kelurahan (vit.kelurahan) + # if address.get('suburb'): + # kel = self.env['vit.kelurahan'].search([ + # ('name', 'ilike', address['suburb']) + # ], limit=1) + # if kel: + # updates['kelurahan_id'] = kel.id + + # # Kecamatan (vit.kecamatan) + # kec_nama = address.get('district') or address.get('village') + # if kec_nama: + # kec = self.env['vit.kecamatan'].search([ + # ('name', 'ilike', kec_nama) + # ], limit=1) + # if kec: + # updates['kecamatan_id'] = kec.id + + # # Kota (vit.kota) + # kota_nama = address.get('city') or address.get('town') + # if kota_nama: + # kota = self.env['vit.kota'].search([ + # ('name', 'ilike', kota_nama) + # ], limit=1) + # if kota: + # updates['kota_id'] = kota.id + + # # Provinsi (res.country.state) + # if address.get('state'): + # state = self.env['res.country.state'].search([ + # ('name', 'ilike', address['state']) + # ], limit=1) + # if state: + # updates['state_id'] = state.id + + # # Negara (res.country) + # if address.get('country_code'): + # country = self.env['res.country'].search([ + # ('code', '=', address['country_code'].upper()) + # ], limit=1) + # if country: + # updates['country_id'] = country.id + + # rec.write(updates) + + + + # def reverse_geocode(self, lat, lon): + # try: + # url = f'https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={lat}&lon={lon}' + # headers = { + # 'User-Agent': 'Odoo/1.0 (andrifebriyadiputra@gmail.com)', # WAJIB: ganti dengan email domain kamu + # } + # response = requests.get(url, headers=headers, timeout=5) + # if response.ok: + # data = response.json() + # return data.get('address', {}) + # else: + # _logger.warning("Reverse geocode failed with status %s: %s", response.status_code, response.text) + # except Exception as e: + # _logger.exception("Exception during reverse geocode: %s", e) + # return {} -- cgit v1.2.3 From 8a025fe63ea44b93d3978da7df3aa31533da5300 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 13 Jun 2025 09:13:08 +0700 Subject: add tukar guling model and view --- indoteknik_custom/models/tukar_guling.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 indoteknik_custom/models/tukar_guling.py (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py new file mode 100644 index 00000000..e69de29b -- cgit v1.2.3 From bb765c2cddb0f630c8cf2a69793aa95f7f8f6f08 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Fri, 13 Jun 2025 09:48:01 +0700 Subject: change request params to get data kgx --- indoteknik_custom/models/shipment_group.py | 2 +- indoteknik_custom/models/stock_picking.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/shipment_group.py b/indoteknik_custom/models/shipment_group.py index fcde39c9..4969c35a 100644 --- a/indoteknik_custom/models/shipment_group.py +++ b/indoteknik_custom/models/shipment_group.py @@ -22,7 +22,7 @@ class ShipmentGroup(models.Model): picking_names = [lines.picking_id.name for lines in self.shipment_line] if rec.shipment_id.carrier_id.id == 173: rec.picking_id.action_get_kgx_pod( - shipment=f"{self.number}, {', '.join(picking_names)}" + shipment=f"{self.number}" ) elif rec.shipment_id.carrier_id.id == 151: rec.picking_id.track_envio_shipment(shipment=f"{self.number}") diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 9dfc8b78..622c537e 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -288,7 +288,7 @@ class StockPicking(models.Model): self.ensure_one() if not self.name or not self.origin: return False - return f"{self.name} {self.origin}" + return f"{self.name}" def _download_pod_photo(self, url): """Mengunduh foto POD dari URL""" -- cgit v1.2.3 From 5c40bc011dd1ea27c1407709914ee9db039fa6fe Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Fri, 13 Jun 2025 10:22:14 +0700 Subject: change approval customer commission --- indoteknik_custom/models/commision.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py index eeaa8efc..dc7f5f46 100644 --- a/indoteknik_custom/models/commision.py +++ b/indoteknik_custom/models/commision.py @@ -354,12 +354,12 @@ class CustomerCommision(models.Model): if not self.status or self.status == 'draft': self.status = 'pengajuan1' - elif self.status == 'pengajuan1' and self.env.user.is_sales_manager: + elif self.status == 'pengajuan1' and self.env.user.id == 19: self.status = 'pengajuan2' self.approved_by = (self.approved_by + ', ' if self.approved_by else '') + self.env.user.name self.date_approved_sales = now_naive - self.position_sales = 'Sales Manager' - elif self.status == 'pengajuan2' and self.env.user.id == 19: + self.position_sales = 'Purchasing Manager' + elif self.status == 'pengajuan2' and self.env.user.id == 216: self.status = 'pengajuan3' self.approved_by = (self.approved_by + ', ' if self.approved_by else '') + self.env.user.name self.date_approved_marketing = now_naive -- cgit v1.2.3 From 98c757b960591eeb2ac6da272bfc8822663918a4 Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Fri, 13 Jun 2025 10:25:35 +0700 Subject: change approval customer commission --- indoteknik_custom/models/commision.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py index dc7f5f46..842e64bf 100644 --- a/indoteknik_custom/models/commision.py +++ b/indoteknik_custom/models/commision.py @@ -358,7 +358,7 @@ class CustomerCommision(models.Model): self.status = 'pengajuan2' self.approved_by = (self.approved_by + ', ' if self.approved_by else '') + self.env.user.name self.date_approved_sales = now_naive - self.position_sales = 'Purchasing Manager' + self.position_sales = 'Sales Manager' elif self.status == 'pengajuan2' and self.env.user.id == 216: self.status = 'pengajuan3' self.approved_by = (self.approved_by + ', ' if self.approved_by else '') + self.env.user.name -- cgit v1.2.3 From 0e7fdb8ea85c53de2c8ad5fa8674c5fbc489e45a Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Fri, 13 Jun 2025 10:58:27 +0700 Subject: (andri) bisa ubah alamat via ubah pinpoin langsung --- indoteknik_custom/models/res_partner.py | 164 ++++++++++++++++++-------------- 1 file changed, 90 insertions(+), 74 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index 82aa1134..6ef5698c 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -188,6 +188,10 @@ class ResPartner(models.Model): def write(self, vals): res = super(ResPartner, self).write(vals) + + for rec in self: + if 'latitude' in vals or 'longtitude' in vals: + rec._update_address_from_coords() # # # if 'property_payment_term_id' in vals: # # if not self.env.user.is_accounting and vals['property_payment_term_id'] != 26: @@ -199,6 +203,14 @@ class ResPartner(models.Model): # # raise UserError('You name it') # return res + + @api.model + def create(self, vals): + records = super().create(vals) + for rec in records: + if vals.get('latitude') and vals.get('longtitude'): + rec._update_address_from_coords() + return records @api.constrains('name') def _check_duplicate_name(self): @@ -579,84 +591,88 @@ class ResPartner(models.Model): result = response.json() if result.get('results'): location = result['results'][0]['geometry']['location'] + formatted_address = result['results'][0].get('formatted_address', '') + rec.latitude = location['lat'] rec.longtitude = location['lng'] + rec.address_map = formatted_address # ✅ Simpan alamat lengkap else: raise UserError("Tidak ditemukan hasil geocode untuk alamat tersebut.") else: raise UserError("Permintaan ke Google Maps gagal. Periksa koneksi internet atau API Key.") - # def _update_address_from_coords(self): - # for rec in self: - # if rec.latitude and rec.longtitude: - # address = self.reverse_geocode(rec.latitude, rec.longtitude) - # if not address: - # continue - - # updates = { - # 'street': address.get('road') or '', - # 'zip': address.get('postcode') or '', - # 'city': address.get('city') or address.get('town') or address.get('village') or '', - # } - - # # Kelurahan (vit.kelurahan) - # if address.get('suburb'): - # kel = self.env['vit.kelurahan'].search([ - # ('name', 'ilike', address['suburb']) - # ], limit=1) - # if kel: - # updates['kelurahan_id'] = kel.id - - # # Kecamatan (vit.kecamatan) - # kec_nama = address.get('district') or address.get('village') - # if kec_nama: - # kec = self.env['vit.kecamatan'].search([ - # ('name', 'ilike', kec_nama) - # ], limit=1) - # if kec: - # updates['kecamatan_id'] = kec.id - - # # Kota (vit.kota) - # kota_nama = address.get('city') or address.get('town') - # if kota_nama: - # kota = self.env['vit.kota'].search([ - # ('name', 'ilike', kota_nama) - # ], limit=1) - # if kota: - # updates['kota_id'] = kota.id - - # # Provinsi (res.country.state) - # if address.get('state'): - # state = self.env['res.country.state'].search([ - # ('name', 'ilike', address['state']) - # ], limit=1) - # if state: - # updates['state_id'] = state.id - - # # Negara (res.country) - # if address.get('country_code'): - # country = self.env['res.country'].search([ - # ('code', '=', address['country_code'].upper()) - # ], limit=1) - # if country: - # updates['country_id'] = country.id - - # rec.write(updates) - - - - # def reverse_geocode(self, lat, lon): - # try: - # url = f'https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={lat}&lon={lon}' - # headers = { - # 'User-Agent': 'Odoo/1.0 (andrifebriyadiputra@gmail.com)', # WAJIB: ganti dengan email domain kamu - # } - # response = requests.get(url, headers=headers, timeout=5) - # if response.ok: - # data = response.json() - # return data.get('address', {}) - # else: - # _logger.warning("Reverse geocode failed with status %s: %s", response.status_code, response.text) - # except Exception as e: - # _logger.exception("Exception during reverse geocode: %s", e) - # return {} + def _update_address_from_coords(self): + for rec in self: + if rec.latitude and rec.longtitude: + try: + components, formatted, parsed = rec._reverse_geocode(rec.latitude, rec.longtitude) + if not parsed: + continue + + updates = { + 'street': parsed.get('road') or '', + 'zip': parsed.get('postcode') or '', + 'address_map': formatted or '', + } + + state = self.env['res.country.state'].search([('name', 'ilike', parsed.get('state'))], limit=1) + if state: + updates['state_id'] = state.id + + kota = self.env['vit.kota'].search([('name', 'ilike', parsed.get('city'))], limit=1) + if kota: + updates['kota_id'] = kota.id + + kec = self.env['vit.kecamatan'].search([('name', 'ilike', parsed.get('district'))], limit=1) + if kec: + updates['kecamatan_id'] = kec.id + + kel = self.env['vit.kelurahan'].search([('name', 'ilike', parsed.get('suburb'))], limit=1) + if kel: + updates['kelurahan_id'] = kel.id + + rec.update(updates) + + except Exception as e: + raise UserError(f"Gagal update alamat dari koordinat: {str(e)}") + + + def _reverse_geocode(self, lat, lng): + api_key = self.env['ir.config_parameter'].sudo().get_param('google.maps.api_key') + if not api_key: + raise UserError("API Key Google Maps belum dikonfigurasi.") + + url = f'https://maps.googleapis.com/maps/api/geocode/json?latlng={lat},{lng}&key={api_key}' + response = requests.get(url) + if response.ok: + result = response.json() + if result.get('results'): + components = result['results'][0]['address_components'] + formatted = result['results'][0]['formatted_address'] + return components, formatted, self._parse_google_address(components) + return {}, '', {} + + def _parse_google_address(self, components): + def get(types): + for comp in components: + if types in comp['types']: + return comp['long_name'] + return '' + + street_number = get('street_number') + route = get('route') + neighborhood = get('neighborhood') # Bisa jadi nama RW + subpremise = get('subpremise') # Bisa jadi no kamar/ruko + + # Gabungkan informasi jalan + road = " ".join(filter(None, [route, street_number, subpremise, neighborhood])) + + return { + 'road': road.strip(), + 'postcode': get('postal_code'), + 'state': get('administrative_area_level_1'), + 'city': get('administrative_area_level_2') or get('locality'), + 'district': get('administrative_area_level_3'), + 'suburb': get('administrative_area_level_4'), + 'formatted': get('formatted_address'), + } -- cgit v1.2.3 From 860abd78b0474279f851378f0b6507fe71fd76be Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 13 Jun 2025 11:39:00 +0700 Subject: PTG document name sequence --- indoteknik_custom/models/__init__.py | 1 + indoteknik_custom/models/tukar_guling.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index 08fa9803..72bd7cee 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -150,3 +150,4 @@ from . import stock_backorder_confirmation from . import account_payment_register from . import stock_inventory from . import approval_invoice_date +from . import tukar_guling diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index e69de29b..ca246c7f 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -0,0 +1,21 @@ +from odoo import models, fields, api +from odoo.exceptions import UserError, ValidationError + + +class TukarGuling(models.Model): + _name = 'tukar.guling' + _description = 'Tukar Guling' + _order = 'date desc, id desc' + _rec_name = 'name' + + # Hanya 2 field seperti yang Anda inginkan + name = fields.Char('Reference', required=True, copy=False, readonly=True, default='New') + date = fields.Datetime('Date', default=fields.Datetime.now, required=True) + + # Sequence generator + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('name', 'New') == 'New': + vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling') or '0' + return super(TukarGuling, self).create(vals_list) \ No newline at end of file -- cgit v1.2.3 From 80355d9de0d079ec2f1004efac0377b8c4bfa0eb Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Fri, 13 Jun 2025 15:04:06 +0700 Subject: (andri) add validasi tidak bisa pilih kurir instan bila alamat pengiriman belum pinpoint --- indoteknik_custom/models/sale_order.py | 83 ++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 35 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index f53d375b..e54053ff 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -421,39 +421,19 @@ class SaleOrder(models.Model): @api.onchange('carrier_id') def _onchange_carrier_id(self): - # ─────────────────────────────────────────────────────────────── - # 1. abaikan onchange kalau SO masih draft / belum tersimpan - # ─────────────────────────────────────────────────────────────── if not self._origin or not self._origin.id: return - sale_order_id = self._origin.id # id SO asli (sudah tersimpan) - - # ─────────────────────────────────────────────────────────────── - # 2. Jika SO BELUM mempunyai satupun shipping.option ⇒ - # jangan lakukan validasi apa-apa; cukup reset field & domain - # ─────────────────────────────────────────────────────────────── - total_so_options = self.env['shipping.option'].search_count( - [('sale_order_id', '=', sale_order_id)] - ) - if total_so_options == 0: - # belum pernah estimasi ongkir ⇒ biarkan user ganti carrier - self.shipping_option_id = False - return {'domain': {'shipping_option_id': [('id', '=', -1)]}} - - # ─────────────────────────────────────────────────────────────── - # 3. (kode lama) – mulai validasi hanya jika sudah ada option - # ─────────────────────────────────────────────────────────────── + sale_order_id = self._origin.id self.shipping_option_id = False if not self.carrier_id: return {'domain': {'shipping_option_id': [('id', '=', -1)]}} - # cari provider dari mapping rajaongkir_kurir + # Ambil provider dari mapping self.env.cr.execute(""" SELECT name FROM rajaongkir_kurir - WHERE delivery_carrier_id = %s - LIMIT 1 + WHERE delivery_carrier_id = %s LIMIT 1 """, (self.carrier_id.id,)) row = self.env.cr.fetchone() provider = row[0].lower() if row and row[0] else ( @@ -462,36 +442,69 @@ class SaleOrder(models.Model): _logger.info(f"[Carrier Changed] {self.carrier_id.name}, Detected Provider: {provider}") - # hitung berapa option yg match provider BARU - self.env.cr.execute(""" - SELECT COUNT(*) FROM shipping_option - WHERE LOWER(provider) LIKE %s AND sale_order_id = %s - """, (f'%{provider}%', sale_order_id)) - matched = self.env.cr.fetchone()[0] or 0 + # ─────────────────────────────────────────────────────────────── + # Validasi koordinat untuk kurir instan + # ─────────────────────────────────────────────────────────────── + instan_kurir = ['gojek', 'grab', 'lalamove', 'borzo', 'rara', 'deliveree'] + if provider in instan_kurir: + lat = self.real_shipping_id.latitude + lng = self.real_shipping_id.longtitude + def is_invalid(val): + try: + return not val or float(val) == 0.0 + except (ValueError, TypeError): + return True + + if is_invalid(lat) or is_invalid(lng): + self.carrier_id = self._origin.carrier_id + self.shipping_option_id = self._origin.shipping_option_id or False + return { + 'warning': { + 'title': "Alamat Belum Pin Point", + 'message': ( + "Kurir instan seperti Gojek, Grab, Lalamove, Borzo, Rara, dan Deliveree " + "membutuhkan alamat pengiriman yang sudah Pin Point.\n\n" + "Silakan tentukan lokasi dengan tepat pada Pin Point Location yang tersedia di kontak." + ) + }, + 'domain': {'shipping_option_id': [('id', '=', -1)]} + } + + # ─────────────────────────────────────────────────────────────── + # Baru cek apakah shipping option sudah ada + # ─────────────────────────────────────────────────────────────── + total_so_options = self.env['shipping.option'].search_count([ + ('sale_order_id', '=', sale_order_id) + ]) + if total_so_options == 0: + return {'domain': {'shipping_option_id': [('id', '=', -1)]}} + + # Validasi: apakah shipping option ada untuk provider ini? + matched = self.env['shipping.option'].search_count([ + ('sale_order_id', '=', sale_order_id), + ('provider', 'ilike', provider), + ]) if matched == 0: - # provider baru tidak ada di option yang SUDAH dibuat → kembalikan ke carrier lama - prev_carrier = self._origin.carrier_id - self.carrier_id = prev_carrier + self.carrier_id = self._origin.carrier_id self.shipping_option_id = self._origin.shipping_option_id or False return { 'warning': { 'title': "Shipping Option Tidak Ditemukan", 'message': ( - "Layanan kurir tidak tersedia untuk pengiriman ini.\n" + "Layanan kurir ini tidak tersedia pada pengiriman ini. " "Pilihan dikembalikan ke sebelumnya." ) }, 'domain': {'shipping_option_id': [('id', '=', -1)]} } - # kalau match ada → set domain normal (hanya option dengan provider itu) + # Kalau semua valid, kembalikan domain normal domain = [ '|', '&', ('sale_order_id', '=', sale_order_id), ('provider', 'ilike', f'%{provider}%'), '&', ('sale_order_id', '=', False), ('provider', 'ilike', f'%{provider}%') ] - return {'domain': {'shipping_option_id': domain}} @api.onchange('shipping_option_id') -- cgit v1.2.3 From 3cb22a5059bfddc5f1d234e8c34e726debe9643d Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Fri, 13 Jun 2025 17:05:11 +0700 Subject: (andri) perapihan log note estimate shipping --- indoteknik_custom/models/sale_order.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index e54053ff..b9ca4a09 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -890,7 +890,7 @@ class SaleOrder(models.Model): total_weight = self._validate_for_shipping_estimate() weight_gram = int(total_weight * 1000) - if weight_gram < 100: + if weight_gram Estimasi Ongkos Kirim Biteship ({origin_info} → {destination_info}):
"] + message_lines = [f"Estimasi Ongkos Kirim Biteship:
"] for courier, options in courier_options.items(): message_lines.append(f"{courier}:
") @@ -1081,6 +1073,19 @@ class SaleOrder(models.Model): if courier != list(courier_options.keys())[-1]: message_lines.append("
") + origin_address = "Jl. Bandengan Utara Komp A & BRT. Penjaringan, Kec. Penjaringan, Jakarta (BELAKANG INDOMARET) KOTA JAKARTA UTARA PENJARINGAN" + destination_address = shipping_address.alamat_lengkap_text or shipping_address.street or shipping_address.name or '' + if use_coordinate: + origin_suffix = f"(Koordinat: {origin_data.get('origin_latitude')}, {origin_data.get('origin_longitude')})" + destination_suffix = f"(Koordinat: {destination_data.get('destination_latitude')}, {destination_data.get('destination_longitude')})" + else: + origin_suffix = f"(Kode Pos: {origin_data.get('origin_postal_code')})" + destination_suffix = f"(Kode Pos: {destination_data.get('destination_postal_code')})" + + message_lines.append("


Info Lokasi:
") + message_lines.append(f"Asal: {origin_address} {origin_suffix}
") + message_lines.append(f"Tujuan: {destination_address} {destination_suffix}
") + message_body = "".join(message_lines) self.message_post( -- cgit v1.2.3 From d2c05ac97f195e196605d91088d6f76d9312e528 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 13 Jun 2025 17:19:28 +0700 Subject: match with meet --- indoteknik_custom/models/tukar_guling.py | 141 +++++++++++++++++++++++++++++-- 1 file changed, 136 insertions(+), 5 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index ca246c7f..e214e268 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -8,14 +8,145 @@ class TukarGuling(models.Model): _order = 'date desc, id desc' _rec_name = 'name' - # Hanya 2 field seperti yang Anda inginkan - name = fields.Char('Reference', required=True, copy=False, readonly=True, default='New') + name = fields.Char('Number', required=True, copy=False, readonly=True, default='New') date = fields.Datetime('Date', default=fields.Datetime.now, required=True) + out_num = fields.Many2one('stock.picking', 'Nomor BU/Out', required=True, + domain=[('picking_type_id.code', '=', 'outgoing')]) + ba_num = fields.Text('Nomor BA') + notes = fields.Text('Notes') + return_type = fields.Selection(String='Return Type', selection=[ + ('tukar_guling', 'Tukar Guling'), + ('revisi_so', 'Revisi SO'), + ('revisi_po', 'Revisi PO'), + ('credit_memo', 'Credit Memo'), + ('debit_memo', 'Debit Memo'), + ('lain_lain', 'Lain-lain')]) + + # ✅ PERBAIKAN: Ganti 'states' dengan 'state' + state = fields.Selection(string='Status', selection=[ + ('draft', 'Draft'), + ('waiting', 'Waiting for Approval'), + ('done', 'Done'), + ('cancel', 'Canceled') + ], default='draft', tracking=True, required=True) + + # ✅ NEW: Line items + line_ids = fields.One2many('tukar.guling.line', 'tukar_guling_id', string='Product Lines') + + @api.constrains('line_ids', 'state') + def _check_product_lines(self): + """Constraint: Product lines harus ada jika state bukan draft""" + for record in self: + if record.state in ('waiting', 'done') and not record.line_ids: + raise ValidationError("Product lines harus diisi sebelum submit atau approve!") + + def _validate_product_lines(self): + """Helper method untuk validasi product lines""" + self.ensure_one() + + # Check ada product lines + if not self.line_ids: + raise UserError("Belum ada product lines yang ditambahkan!") + + # Check product sudah diisi + empty_lines = self.line_ids.filtered(lambda line: not line.product_id) + if empty_lines: + raise UserError("Ada product lines yang belum diisi productnya!") + + # Check quantity > 0 + zero_qty_lines = self.line_ids.filtered(lambda line: line.product_uom_qty <= 0) + if zero_qty_lines: + raise UserError("Quantity product tidak boleh kosong atau 0!") + + return True - # Sequence generator @api.model_create_multi def create(self, vals_list): for vals in vals_list: if vals.get('name', 'New') == 'New': - vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling') or '0' - return super(TukarGuling, self).create(vals_list) \ No newline at end of file + vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling') or 'PTG/00001' + return super(TukarGuling, self).create(vals_list) + + def copy(self, default=None): + """Override copy untuk custom behavior saat duplicate""" + if default is None: + default = {} + + # Reset fields penting saat duplicate + default.update({ + 'name': 'New', # Akan auto-generate sequence baru + 'state': 'draft', + 'date': fields.Datetime.now(), + # ba_num dan out_num tidak di-reset, user bisa edit manual + }) + + # Copy record dengan default values + new_record = super(TukarGuling, self).copy(default) + + # Re-sequence line items untuk record baru + if new_record.line_ids: + for i, line in enumerate(new_record.line_ids): + line.sequence = (i + 1) * 10 + + return new_record + + 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_submit(self): + self.ensure_one() + if self.state != 'draft': + raise UserError("Hanya status Draft saja yang bisa di submit") + self.state = 'waiting' + + def action_approve(self): + self.ensure_one() + if self.state != 'waiting': + raise UserError("Hanya status Waiting saja yang bisa di approve") + self.state = 'done' + + def action_cancel(self): + self.ensure_one() + if self.state == 'done': + raise UserError("Tidak bisa cancel jika sudah done") + self.state = 'cancel' + + +class TukarGulingLine(models.Model): + _name = 'tukar.guling.line' + _description = 'Tukar Guling Line' + _order = 'sequence, id' + + sequence = fields.Integer('Sequence', default=10, copy=False) + tukar_guling_id = fields.Many2one('tukar.guling', string='Tukar Guling', required=True, ondelete='cascade') + product_id = fields.Many2one('product.product', string='Product', required=True) + product_uom_qty = fields.Float('Quantity', digits='Product Unit of Measure', required=True, default=1.0) + product_uom = fields.Many2one('uom.uom', string='Unit of Measure') + name = fields.Text('Description') + + @api.model_create_multi + def create(self, vals_list): + """Override create to auto-assign sequence""" + for vals in vals_list: + if 'sequence' not in vals or vals.get('sequence', 0) <= 0: + # Get max sequence untuk tukar_guling yang sama + tukar_guling_id = vals.get('tukar_guling_id') + if tukar_guling_id: + max_seq = self.search([ + ('tukar_guling_id', '=', tukar_guling_id) + ], order='sequence desc', limit=1) + vals['sequence'] = (max_seq.sequence or 0) + 10 + else: + vals['sequence'] = 10 + return super(TukarGulingLine, self).create(vals_list) + + @api.onchange('product_id') + def _onchange_product_id(self): + if self.product_id: + self.name = self.product_id.display_name + self.product_uom = self.product_id.uom_id \ No newline at end of file -- cgit v1.2.3 From 136fc0acd87bceb21b89fc9f040bffc49c93e9f9 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sat, 14 Jun 2025 09:10:12 +0700 Subject: restore wati --- indoteknik_custom/models/wati.py | 166 +-------------------------------------- 1 file changed, 1 insertion(+), 165 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/wati.py b/indoteknik_custom/models/wati.py index 18517502..a0619f83 100644 --- a/indoteknik_custom/models/wati.py +++ b/indoteknik_custom/models/wati.py @@ -201,170 +201,6 @@ class WatiNotification(models.Model): wati.is_lead = True wati.lead_id = current_lead.id - # FINAL CODE - Sesuai dengan mapping table Anda - - def check_wati_tags_leads(self): - """Check tags 'leads' di WATI dan create leads di Odoo - Final Version""" - _logger.info('=== Starting WATI Tags Check (Final) ===') - - wati_api = self.env['wati.api'] - total_leads_created = 0 - - try: - # Get WATI contacts - wati_contacts = wati_api.http_get('/api/v1/getContacts', {'pageSize': 100, 'pageNumber': 1}) - - if isinstance(wati_contacts, dict) and wati_contacts.get('result') == 'success': - contact_list = wati_contacts.get('contact_list', []) - - for contact in contact_list: - if self._create_lead_if_tagged(contact): - total_leads_created += 1 - - _logger.info('WATI check completed: %s leads created' % total_leads_created) - return {'leads_created': total_leads_created} - - except Exception as e: - _logger.error('Error in WATI tags check: %s' % str(e)) - return {'leads_created': 0, 'error': str(e)} - - def _create_lead_if_tagged(self, contact): - """Create lead jika contact punya tags=leads - Sesuai Mapping Table""" - try: - # Check tags leads - if not self._has_tags_leads(contact): - return False - - phone = contact.get('phone', '') - if not phone: - return False - - # Check existing lead by phone - existing_lead = self.env['crm.lead'].search([('phone', '=', phone)], limit=1) - if existing_lead: - _logger.info('Lead already exists for phone %s' % phone) - return False - - # Extract data dari customParams sesuai mapping table - custom_params = contact.get('customParams', []) - contact_data = self._extract_contact_data(custom_params) - - # Create lead dengan field mapping yang sesuai - lead_vals = { - 'name': self._generate_lead_name(contact_data, contact), - 'phone': phone, # Phone Number → Mobile - 'contact_name': contact_data.get('name', ''), # Name → Contact Name - 'partner_name': contact_data.get('perusahaan', ''), # Perusahaan → Company Name - 'email_from': contact_data.get('email', ''), # Email → Email - 'description': contact_data.get('notes', ''), # Notes → Internal Notes - 'type': 'lead', - 'user_id': self._get_salesperson_id(contact_data.get('sales', '')), # Sales → Salesperson - } - - new_lead = self.env['crm.lead'].create(lead_vals) - _logger.info('Created WATI lead %s for %s (%s)' % (new_lead.id, contact_data.get('name', 'Unknown'), phone)) - return True - - except Exception as e: - _logger.error('Error creating lead: %s' % str(e)) - return False - - def _extract_contact_data(self, custom_params): - """Extract data dari customParams sesuai mapping table""" - contact_data = {} - - for param in custom_params: - param_name = param.get('name', '').lower() - param_value = param.get('value', '').strip() - - # Mapping sesuai table: - if param_name == 'perusahaan': # Perusahaan → Company Name - contact_data['perusahaan'] = param_value - elif param_name == 'name': # Name → Contact Name - contact_data['name'] = param_value - elif param_name == 'email': # Email → Email - contact_data['email'] = param_value - elif param_name == 'sales': # Sales → Salesperson - contact_data['sales'] = param_value - elif param_name == 'notes': # Notes → Internal Notes - contact_data['notes'] = param_value - # Phone Number sudah diambil dari contact.phone - - return contact_data - - def _generate_lead_name(self, contact_data, contact): - """Generate lead name sesuai mapping: Judul Leads berdasarkan company/contact""" - company_name = contact_data.get('perusahaan', '') - contact_name = contact_data.get('name', '') or contact.get('name', '') - - if company_name: - return 'WATI Lead - %s' % company_name - elif contact_name: - return 'WATI Lead - %s' % contact_name - else: - return 'WATI Lead - %s' % contact.get('phone', 'Unknown') - - def _get_salesperson_id(self, sales_name): - """Get salesperson ID dari nama - Sales → Salesperson""" - if not sales_name: - return 2 # Default Sales (ID 2) - - # Try find user by name - user = self.env['res.users'].search([ - ('name', 'ilike', sales_name) - ], limit=1) - - if user: - return user.id - else: - # Fallback ke default Sales - return 2 - - def _has_tags_leads(self, contact): - """Check apakah ada tags untuk tajik ke odoo => Leads""" - custom_params = contact.get('customParams', []) - - for param in custom_params: - param_name = param.get('name', '').lower() - param_value = param.get('value', '').lower() - - # Check: "Judul Tags untuk tajik ke odoo => Leads" - if param_name == 'tags' and param_value == 'leads': - return True - - return False - - def manual_check_tags(self): - """Manual trigger untuk testing""" - result = self.check_wati_tags_leads() - - message = 'WATI Tags Check Completed!\n\n' - message += 'Leads Created: %s\n\n' % result.get('leads_created', 0) - message += 'Field Mapping:\n' - message += '• Perusahaan → Company Name\n' - message += '• Name → Contact Name\n' - message += '• Email → Email\n' - message += '• Sales → Salesperson\n' - message += '• Phone Number → Mobile\n' - message += '• Notes → Internal Notes\n' - message += '• Tags=leads → Trigger Lead Creation' - - if result.get('error'): - message += '\n\nError: %s' % result['error'] - message_type = 'warning' - else: - message_type = 'success' - - return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'params': { - 'title': 'WATI Tags Check', - 'message': message, - 'type': message_type, - 'sticky': True, - } - } class WatiHistory(models.Model): _name = 'wati.history' @@ -483,4 +319,4 @@ class WatiHistoryLine(models.Model): ticket_id = fields.Char(string='Ticket ID') type = fields.Char(string='Type') wa_id = fields.Char(string='WA ID') - date_wati = fields.Datetime(string='Date WATI') \ No newline at end of file + date_wati = fields.Datetime(string='Date WATI') -- cgit v1.2.3 From f5fc52453c0f7e52b6c87fa57d2de5120c3041e0 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sat, 14 Jun 2025 10:05:15 +0700 Subject: check validation --- indoteknik_custom/models/tukar_guling.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index e214e268..36819ad4 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -22,7 +22,6 @@ class TukarGuling(models.Model): ('debit_memo', 'Debit Memo'), ('lain_lain', 'Lain-lain')]) - # ✅ PERBAIKAN: Ganti 'states' dengan 'state' state = fields.Selection(string='Status', selection=[ ('draft', 'Draft'), ('waiting', 'Waiting for Approval'), @@ -30,7 +29,6 @@ class TukarGuling(models.Model): ('cancel', 'Canceled') ], default='draft', tracking=True, required=True) - # ✅ NEW: Line items line_ids = fields.One2many('tukar.guling.line', 'tukar_guling_id', string='Product Lines') @api.constrains('line_ids', 'state') @@ -44,6 +42,7 @@ class TukarGuling(models.Model): """Helper method untuk validasi product lines""" self.ensure_one() + # Check ada product lines if not self.line_ids: raise UserError("Belum ada product lines yang ditambahkan!") @@ -74,16 +73,15 @@ class TukarGuling(models.Model): # Reset fields penting saat duplicate default.update({ - 'name': 'New', # Akan auto-generate sequence baru + 'name': 'New', 'state': 'draft', 'date': fields.Datetime.now(), - # ba_num dan out_num tidak di-reset, user bisa edit manual }) # Copy record dengan default values new_record = super(TukarGuling, self).copy(default) - # Re-sequence line items untuk record baru + # Re-sequence line items record baru if new_record.line_ids: for i, line in enumerate(new_record.line_ids): line.sequence = (i + 1) * 10 @@ -100,12 +98,29 @@ class TukarGuling(models.Model): def action_submit(self): self.ensure_one() + # cek bu out sudah diisi atau blm + if not self.out_num: + raise UserError("BU/Out harus diisi!") + + # cek return type + # if not self.return_type: + # raise UserError("Return Type harus diisi!") + if self.state != 'draft': raise UserError("Hanya status Draft saja yang bisa di submit") self.state = 'waiting' def action_approve(self): self.ensure_one() + + # cek bu out sudah diisi atau blm + if not self.out_num: + raise UserError("BU/Out harus diisi!") + + # cek return type + if not self.return_type: + raise UserError("Return Type harus diisi!") + if self.state != 'waiting': raise UserError("Hanya status Waiting saja yang bisa di approve") self.state = 'done' -- cgit v1.2.3 From 1cecf60302ea02e250a97ca2dbc6679332988bc0 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 14 Jun 2025 10:26:28 +0700 Subject: (andri) add search pada gmaps contact --- 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 b9ca4a09..492153c0 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -890,7 +890,7 @@ class SaleOrder(models.Model): total_weight = self._validate_for_shipping_estimate() weight_gram = int(total_weight * 1000) - if weight_gram Date: Sat, 14 Jun 2025 11:49:33 +0700 Subject: (andri) fix peletakan search autocomplete & tambahan info mengenai address map --- indoteknik_custom/models/res_partner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index 6ef5698c..380761b4 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -149,7 +149,7 @@ class ResPartner(models.Model): longtitude = fields.Char(string='Longtitude') latitude = fields.Char(string='Latitude') map_view = fields.Char(string='Map') - address_map = fields.Char(string='Address Map') + address_map = fields.Char(string='Address Map', help='Ini adalah alamat yang didapatkan dari pin point pada peta') company_type = fields.Selection(string='Company Type', selection=[('person', 'Individual'), ('company', 'Company')], compute='_compute_company_type', inverse='_write_company_type', tracking=3) -- cgit v1.2.3 From b87f89b8af2731e02d6ab095c3e92e48b39b4f6c Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 14 Jun 2025 12:27:25 +0700 Subject: (andri) fix retry --- indoteknik_custom/models/mail_mail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/mail_mail.py b/indoteknik_custom/models/mail_mail.py index cbcd883a..792b97b7 100644 --- a/indoteknik_custom/models/mail_mail.py +++ b/indoteknik_custom/models/mail_mail.py @@ -14,7 +14,7 @@ class MailMail(models.Model): # Filter hanya email gagal dalam 7 hari terakhir mails = self.search([ - ('state', 'in', 'exception'), + ('state', 'in', ['exception']), ('create_date', '>=', seven_days_ago), ('create_date', '<=', now), ], limit=250) -- cgit v1.2.3 From 819f7c2f0dfe0eaf857c44e80062dcc7e94c9828 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 14 Jun 2025 13:06:43 +0700 Subject: (andri) memindahkan key biteship (test&live) ke system parameter --- indoteknik_custom/models/sale_order.py | 4 ++-- indoteknik_custom/models/stock_picking.py | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 492153c0..8c8a3cc3 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -1101,8 +1101,8 @@ class SaleOrder(models.Model): def _call_biteship_api(self, origin_data, destination_data, items, couriers=None): url = "https://api.biteship.com/v1/rates/couriers" - api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA" - + # api_key = self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_live') + api_key = self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_test') headers = { 'Authorization': api_key, 'Content-Type': 'application/json' diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 8b03e18d..89e7fecc 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -19,11 +19,6 @@ import re _logger = logging.getLogger(__name__) _biteship_url = "https://api.biteship.com/v1" -_biteship_api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA" - - -# _biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo" - class StockPicking(models.Model): _inherit = 'stock.picking' @@ -170,6 +165,10 @@ class StockPicking(models.Model): area_name = fields.Char(string="Area", compute="_compute_area_name") + def _get_biteship_api_key(self): + return self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_test') + # return self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_live') + @api.depends('real_shipping_id.kecamatan_id', 'real_shipping_id.kota_id') def _compute_area_name(self): for record in self: @@ -769,7 +768,7 @@ class StockPicking(models.Model): _logger.info(f"Payload untuk Biteship: {payload}") # Kirim ke Biteship - api_key = _biteship_api_key + api_key = self._get_biteship_api_key() headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" @@ -1712,7 +1711,7 @@ class StockPicking(models.Model): return response def get_manifest_biteship(self): - api_key = _biteship_api_key + api_key = self._get_biteship_api_key() headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" -- cgit v1.2.3 From 8718c383e39440f83d6579ef536478053e623bc3 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 14 Jun 2025 13:14:24 +0700 Subject: (andri) live biteship --- indoteknik_custom/models/sale_order.py | 4 ++-- indoteknik_custom/models/stock_picking.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 8c8a3cc3..bfc4e5fd 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -1101,8 +1101,8 @@ class SaleOrder(models.Model): def _call_biteship_api(self, origin_data, destination_data, items, couriers=None): url = "https://api.biteship.com/v1/rates/couriers" - # api_key = self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_live') - api_key = self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_test') + api_key = self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_live') + # api_key = self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_test') headers = { 'Authorization': api_key, 'Content-Type': 'application/json' diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 89e7fecc..7775e991 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -166,8 +166,8 @@ class StockPicking(models.Model): area_name = fields.Char(string="Area", compute="_compute_area_name") def _get_biteship_api_key(self): - return self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_test') - # return self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_live') + # return self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_test') + return self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_live') @api.depends('real_shipping_id.kecamatan_id', 'real_shipping_id.kota_id') def _compute_area_name(self): -- cgit v1.2.3 From 50769e870756dd350421a205e6d49ab555023764 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sun, 15 Jun 2025 17:33:46 +0700 Subject: fix state view and add ingoing field --- indoteknik_custom/models/tukar_guling.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 36819ad4..b4a901d0 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -10,18 +10,17 @@ class TukarGuling(models.Model): name = fields.Char('Number', required=True, copy=False, readonly=True, default='New') date = fields.Datetime('Date', default=fields.Datetime.now, required=True) - out_num = fields.Many2one('stock.picking', 'Nomor BU/Out', required=True, + out_num = fields.Many2one('stock.picking', 'Nomor BU/Out', domain=[('picking_type_id.code', '=', 'outgoing')]) + in_num = fields.Many2one('stock.picking', 'Nomor BU/In', domain=[('picking_type_id.code', '=', 'incoming')]) ba_num = fields.Text('Nomor BA') notes = fields.Text('Notes') return_type = fields.Selection(String='Return Type', selection=[ - ('tukar_guling', 'Tukar Guling'), - ('revisi_so', 'Revisi SO'), + ('tukar_guling', 'Tukar Guling'), # -> barang yang sama + ('revisi_so', 'Revisi SO'), # -> ganti barang ? ('revisi_po', 'Revisi PO'), - ('credit_memo', 'Credit Memo'), - ('debit_memo', 'Debit Memo'), - ('lain_lain', 'Lain-lain')]) - + ('credit_memo', 'Credit Memo'), # -> dijadiin credit memo + ('debit_memo', 'Debit Memo')]) state = fields.Selection(string='Status', selection=[ ('draft', 'Draft'), ('waiting', 'Waiting for Approval'), @@ -31,6 +30,14 @@ class TukarGuling(models.Model): line_ids = fields.One2many('tukar.guling.line', 'tukar_guling_id', string='Product Lines') + @api.constrains('return_type', 'out_num', 'in_num') + def _check_return_type_fields(self): + for rec in self: + if rec.return_type in ['tukar_guling', 'credit_memo', 'revisi_so'] and not rec.out_num: + raise ValidationError("Field BU/Out wajib diisi untuk jenis return type tersebut.") + if rec.return_type in ['debit_memo', 'revisi_po'] and not rec.in_num: + raise ValidationError("Field BU/In wajib diisi untuk jenis return type tersebut.") + @api.constrains('line_ids', 'state') def _check_product_lines(self): """Constraint: Product lines harus ada jika state bukan draft""" -- cgit v1.2.3 From 32f33ae528f4f9883f38b8afba2ce79222eed4e5 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 16 Jun 2025 07:50:57 +0700 Subject: tukar guling fill bu/in or bu/out --- indoteknik_custom/models/tukar_guling.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index b4a901d0..f62206e8 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -30,13 +30,17 @@ class TukarGuling(models.Model): line_ids = fields.One2many('tukar.guling.line', 'tukar_guling_id', string='Product Lines') - @api.constrains('return_type', 'out_num', 'in_num') - def _check_return_type_fields(self): - for rec in self: - if rec.return_type in ['tukar_guling', 'credit_memo', 'revisi_so'] and not rec.out_num: - raise ValidationError("Field BU/Out wajib diisi untuk jenis return type tersebut.") - if rec.return_type in ['debit_memo', 'revisi_po'] and not rec.in_num: - raise ValidationError("Field BU/In wajib diisi untuk jenis return type tersebut.") + @api.onchange('return_type') + def _onchange_return_type(self): + domain = [] + if self.return_type in ['debit_memo', 'revisi_po']: + domain = [('picking_type_id.code', '=', 'incoming')] + elif self.return_type in ['revisi_so', 'credit_memo']: + domain = [('picking_type_id.code', '=', 'outgoing')] + elif self.return_type == 'tukar_guling': + domain = [('picking_type_id.code', 'in', ['incoming', 'outgoing'])] + + return {'domain': {'in_num': domain}} @api.constrains('line_ids', 'state') def _check_product_lines(self): -- cgit v1.2.3 From 0354e469c6761964ecfc68208f1ad9a521610d56 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 16 Jun 2025 07:54:40 +0700 Subject: tukar guling fill bu/in or bu/out --- indoteknik_custom/models/tukar_guling.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index f62206e8..4f27afde 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -32,15 +32,28 @@ class TukarGuling(models.Model): @api.onchange('return_type') def _onchange_return_type(self): - domain = [] + in_domain = [] + out_domain = [] + if self.return_type in ['debit_memo', 'revisi_po']: - domain = [('picking_type_id.code', '=', 'incoming')] + # Hanya tampilkan BU In + in_domain = [('picking_type_id.code', '=', 'incoming')] + out_domain = [('id', '=', False)] # Kosongkan BU Out elif self.return_type in ['revisi_so', 'credit_memo']: - domain = [('picking_type_id.code', '=', 'outgoing')] + # Hanya tampilkan BU Out + in_domain = [('id', '=', False)] # Kosongkan BU In + out_domain = [('picking_type_id.code', '=', 'outgoing')] elif self.return_type == 'tukar_guling': - domain = [('picking_type_id.code', 'in', ['incoming', 'outgoing'])] - - return {'domain': {'in_num': domain}} + # Boleh pilih keduanya + in_domain = [('picking_type_id.code', '=', 'incoming')] + out_domain = [('picking_type_id.code', '=', 'outgoing')] + + return { + 'domain': { + 'in_num': in_domain, + 'out_num': out_domain, + } + } @api.constrains('line_ids', 'state') def _check_product_lines(self): -- cgit v1.2.3 From 402085ff18942c21d1a61eb02f16497c845694b5 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 16 Jun 2025 07:56:51 +0700 Subject: validation --- indoteknik_custom/models/tukar_guling.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 4f27afde..27d4d954 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -55,6 +55,13 @@ class TukarGuling(models.Model): } } + @api.constrains('return_type', 'in_num', 'out_num') + def _check_bu_required_for_tukar_guling(self): + for record in self: + if record.return_type == 'tukar_guling': + if not record.in_num and not record.out_num: + raise ValidationError("Untuk Tukar Guling, isi salah satu: BU/In atau BU/Out.") + @api.constrains('line_ids', 'state') def _check_product_lines(self): """Constraint: Product lines harus ada jika state bukan draft""" -- cgit v1.2.3 From 5ed938e0386e64733b90d8a4b08b0a0a5b4bc00e Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 16 Jun 2025 07:58:56 +0700 Subject: validation --- indoteknik_custom/models/tukar_guling.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 27d4d954..f8cbec0a 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -56,11 +56,16 @@ class TukarGuling(models.Model): } @api.constrains('return_type', 'in_num', 'out_num') - def _check_bu_required_for_tukar_guling(self): + def _check_required_bu_fields(self): for record in self: - if record.return_type == 'tukar_guling': - if not record.in_num and not record.out_num: - raise ValidationError("Untuk Tukar Guling, isi salah satu: BU/In atau BU/Out.") + if record.return_type in ['debit_memo', 'revisi_po'] and not record.in_num: + raise ValidationError("BU/In harus diisi untuk return type Debit Memo atau Revisi PO.") + + if record.return_type in ['revisi_so', 'credit_memo'] and not record.out_num: + raise ValidationError("BU/Out harus diisi untuk return type Revisi SO atau Credit Memo.") + + if record.return_type == 'tukar_guling' and not (record.in_num or record.out_num): + raise ValidationError("Untuk Tukar Guling, minimal isi salah satu, BU/In atau BU/Out.") @api.constrains('line_ids', 'state') def _check_product_lines(self): -- cgit v1.2.3 From b20b6ada9b13ea40732de14e7ce356f28df40a6b Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 16 Jun 2025 08:07:03 +0700 Subject: (andri) revisi pesan help pada address_map & nonaktifkan button get pin point location sebab GMAPS sudah bisa --- indoteknik_custom/models/res_partner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index 380761b4..986ff786 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -149,7 +149,7 @@ class ResPartner(models.Model): longtitude = fields.Char(string='Longtitude') latitude = fields.Char(string='Latitude') map_view = fields.Char(string='Map') - address_map = fields.Char(string='Address Map', help='Ini adalah alamat yang didapatkan dari pin point pada peta') + address_map = fields.Char(string='Address Map', help='Ini adalah alamat yang didapatkan dari pin point pada peta, bila hasil alamat tidak sesuai, silahkan ubah alamat pada field Alamat Lengkap', tracking=3) company_type = fields.Selection(string='Company Type', selection=[('person', 'Individual'), ('company', 'Company')], compute='_compute_company_type', inverse='_write_company_type', tracking=3) -- cgit v1.2.3 From 1d26fc49f0e01cb972a8dbb60db600222389423d Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 16 Jun 2025 08:11:53 +0700 Subject: (andri) rev help address map --- indoteknik_custom/models/res_partner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index 986ff786..9668d79b 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -149,7 +149,7 @@ class ResPartner(models.Model): longtitude = fields.Char(string='Longtitude') latitude = fields.Char(string='Latitude') map_view = fields.Char(string='Map') - address_map = fields.Char(string='Address Map', help='Ini adalah alamat yang didapatkan dari pin point pada peta, bila hasil alamat tidak sesuai, silahkan ubah alamat pada field Alamat Lengkap', tracking=3) + address_map = fields.Char(string='Address Map', help='Alamat ini diisi otomatis berdasarkan koordinat pin pada peta. Silakan koreksi jika terdapat ketidaksesuaian', tracking=3) company_type = fields.Selection(string='Company Type', selection=[('person', 'Individual'), ('company', 'Company')], compute='_compute_company_type', inverse='_write_company_type', tracking=3) -- cgit v1.2.3 From df02e9c6f0db21b43ae25d77c7072a5dd15f9848 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 16 Jun 2025 08:12:35 +0700 Subject: (andri) rev help address map --- indoteknik_custom/models/res_partner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index 9668d79b..a8ce95d1 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -149,7 +149,7 @@ class ResPartner(models.Model): longtitude = fields.Char(string='Longtitude') latitude = fields.Char(string='Latitude') map_view = fields.Char(string='Map') - address_map = fields.Char(string='Address Map', help='Alamat ini diisi otomatis berdasarkan koordinat pin pada peta. Silakan koreksi jika terdapat ketidaksesuaian', tracking=3) + address_map = fields.Char(string='Address Map', help='Alamat ini diisi otomatis berdasarkan koordinat pin pada peta. Silakan koreksi dan ubah jika terdapat ketidaksesuaian', tracking=3) company_type = fields.Selection(string='Company Type', selection=[('person', 'Individual'), ('company', 'Company')], compute='_compute_company_type', inverse='_write_company_type', tracking=3) -- cgit v1.2.3 From 2ff882e9f591a25b6b9f5adbd4dd90e7402017a9 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 16 Jun 2025 08:48:39 +0700 Subject: (andri) comment map view --- indoteknik_custom/models/res_partner.py | 254 ++++++++++++++++---------------- indoteknik_custom/models/sale_order.py | 1 + 2 files changed, 128 insertions(+), 127 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index a8ce95d1..1786efa3 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -148,7 +148,7 @@ class ResPartner(models.Model): date_payment_terms_purchase = fields.Datetime(string='Date Update Payment Terms') longtitude = fields.Char(string='Longtitude') latitude = fields.Char(string='Latitude') - map_view = fields.Char(string='Map') + # map_view = fields.Char(string='Map') address_map = fields.Char(string='Address Map', help='Alamat ini diisi otomatis berdasarkan koordinat pin pada peta. Silakan koreksi dan ubah jika terdapat ketidaksesuaian', tracking=3) company_type = fields.Selection(string='Company Type', selection=[('person', 'Individual'), ('company', 'Company')], @@ -549,130 +549,130 @@ class ResPartner(models.Model): 'target': 'current', } - def geocode_address(self): - for rec in self: - # Daftar field penting - required_fields = { - 'Alamat Jalan (street)': rec.street, - 'Kelurahan': rec.kelurahan_id.name if rec.kelurahan_id else '', - 'Kecamatan': rec.kecamatan_id.name if rec.kecamatan_id else '', - 'Kota': rec.kota_id.name if rec.kota_id else '', - 'Kode Pos': rec.zip, - 'Provinsi': rec.state_id.name if rec.state_id else '', - } - - # Cek jika ada yang kosong - missing = [label for label, val in required_fields.items() if not val] - if missing: - raise UserError( - "Alamat tidak lengkap. Mohon lengkapi field berikut:\n- " + "\n- ".join(missing) - ) - - # Susun alamat lengkap - address = ', '.join([ - required_fields['Alamat Jalan (street)'], - required_fields['Kelurahan'], - required_fields['Kecamatan'], - required_fields['Kota'], - required_fields['Kode Pos'], - required_fields['Provinsi'], - ]) - - # Ambil API Key - api_key = self.env['ir.config_parameter'].sudo().get_param('google.maps.api_key') - if not api_key: - raise UserError("API Key Google Maps belum dikonfigurasi. Silakan isi melalui Settings.") - - # Request ke Google Maps - url = f'https://maps.googleapis.com/maps/api/geocode/json?address={address}&key={api_key}' - response = requests.get(url) - - if response.ok: - result = response.json() - if result.get('results'): - location = result['results'][0]['geometry']['location'] - formatted_address = result['results'][0].get('formatted_address', '') - - rec.latitude = location['lat'] - rec.longtitude = location['lng'] - rec.address_map = formatted_address # ✅ Simpan alamat lengkap - else: - raise UserError("Tidak ditemukan hasil geocode untuk alamat tersebut.") - else: - raise UserError("Permintaan ke Google Maps gagal. Periksa koneksi internet atau API Key.") - - def _update_address_from_coords(self): - for rec in self: - if rec.latitude and rec.longtitude: - try: - components, formatted, parsed = rec._reverse_geocode(rec.latitude, rec.longtitude) - if not parsed: - continue - - updates = { - 'street': parsed.get('road') or '', - 'zip': parsed.get('postcode') or '', - 'address_map': formatted or '', - } - - state = self.env['res.country.state'].search([('name', 'ilike', parsed.get('state'))], limit=1) - if state: - updates['state_id'] = state.id - - kota = self.env['vit.kota'].search([('name', 'ilike', parsed.get('city'))], limit=1) - if kota: - updates['kota_id'] = kota.id - - kec = self.env['vit.kecamatan'].search([('name', 'ilike', parsed.get('district'))], limit=1) - if kec: - updates['kecamatan_id'] = kec.id - - kel = self.env['vit.kelurahan'].search([('name', 'ilike', parsed.get('suburb'))], limit=1) - if kel: - updates['kelurahan_id'] = kel.id - - rec.update(updates) - - except Exception as e: - raise UserError(f"Gagal update alamat dari koordinat: {str(e)}") - - - def _reverse_geocode(self, lat, lng): - api_key = self.env['ir.config_parameter'].sudo().get_param('google.maps.api_key') - if not api_key: - raise UserError("API Key Google Maps belum dikonfigurasi.") - - url = f'https://maps.googleapis.com/maps/api/geocode/json?latlng={lat},{lng}&key={api_key}' - response = requests.get(url) - if response.ok: - result = response.json() - if result.get('results'): - components = result['results'][0]['address_components'] - formatted = result['results'][0]['formatted_address'] - return components, formatted, self._parse_google_address(components) - return {}, '', {} - - def _parse_google_address(self, components): - def get(types): - for comp in components: - if types in comp['types']: - return comp['long_name'] - return '' - - street_number = get('street_number') - route = get('route') - neighborhood = get('neighborhood') # Bisa jadi nama RW - subpremise = get('subpremise') # Bisa jadi no kamar/ruko - - # Gabungkan informasi jalan - road = " ".join(filter(None, [route, street_number, subpremise, neighborhood])) + # def geocode_address(self): + # for rec in self: + # # Daftar field penting + # required_fields = { + # 'Alamat Jalan (street)': rec.street, + # 'Kelurahan': rec.kelurahan_id.name if rec.kelurahan_id else '', + # 'Kecamatan': rec.kecamatan_id.name if rec.kecamatan_id else '', + # 'Kota': rec.kota_id.name if rec.kota_id else '', + # 'Kode Pos': rec.zip, + # 'Provinsi': rec.state_id.name if rec.state_id else '', + # } - return { - 'road': road.strip(), - 'postcode': get('postal_code'), - 'state': get('administrative_area_level_1'), - 'city': get('administrative_area_level_2') or get('locality'), - 'district': get('administrative_area_level_3'), - 'suburb': get('administrative_area_level_4'), - 'formatted': get('formatted_address'), - } + # # Cek jika ada yang kosong + # missing = [label for label, val in required_fields.items() if not val] + # if missing: + # raise UserError( + # "Alamat tidak lengkap. Mohon lengkapi field berikut:\n- " + "\n- ".join(missing) + # ) + + # # Susun alamat lengkap + # address = ', '.join([ + # required_fields['Alamat Jalan (street)'], + # required_fields['Kelurahan'], + # required_fields['Kecamatan'], + # required_fields['Kota'], + # required_fields['Kode Pos'], + # required_fields['Provinsi'], + # ]) + + # # Ambil API Key + # api_key = self.env['ir.config_parameter'].sudo().get_param('google.maps.api_key') + # if not api_key: + # raise UserError("API Key Google Maps belum dikonfigurasi. Silakan isi melalui Settings.") + + # # Request ke Google Maps + # url = f'https://maps.googleapis.com/maps/api/geocode/json?address={address}&key={api_key}' + # response = requests.get(url) + + # if response.ok: + # result = response.json() + # if result.get('results'): + # location = result['results'][0]['geometry']['location'] + # formatted_address = result['results'][0].get('formatted_address', '') + + # rec.latitude = location['lat'] + # rec.longtitude = location['lng'] + # rec.address_map = formatted_address # ✅ Simpan alamat lengkap + # else: + # raise UserError("Tidak ditemukan hasil geocode untuk alamat tersebut.") + # else: + # raise UserError("Permintaan ke Google Maps gagal. Periksa koneksi internet atau API Key.") + + # def _update_address_from_coords(self): + # for rec in self: + # if rec.latitude and rec.longtitude: + # try: + # components, formatted, parsed = rec._reverse_geocode(rec.latitude, rec.longtitude) + # if not parsed: + # continue + + # updates = { + # 'street': parsed.get('road') or '', + # 'zip': parsed.get('postcode') or '', + # 'address_map': formatted or '', + # } + + # state = self.env['res.country.state'].search([('name', 'ilike', parsed.get('state'))], limit=1) + # if state: + # updates['state_id'] = state.id + + # kota = self.env['vit.kota'].search([('name', 'ilike', parsed.get('city'))], limit=1) + # if kota: + # updates['kota_id'] = kota.id + + # kec = self.env['vit.kecamatan'].search([('name', 'ilike', parsed.get('district'))], limit=1) + # if kec: + # updates['kecamatan_id'] = kec.id + + # kel = self.env['vit.kelurahan'].search([('name', 'ilike', parsed.get('suburb'))], limit=1) + # if kel: + # updates['kelurahan_id'] = kel.id + + # rec.update(updates) + + # except Exception as e: + # raise UserError(f"Gagal update alamat dari koordinat: {str(e)}") + + + # def _reverse_geocode(self, lat, lng): + # api_key = self.env['ir.config_parameter'].sudo().get_param('google.maps.api_key') + # if not api_key: + # raise UserError("API Key Google Maps belum dikonfigurasi.") + + # url = f'https://maps.googleapis.com/maps/api/geocode/json?latlng={lat},{lng}&key={api_key}' + # response = requests.get(url) + # if response.ok: + # result = response.json() + # if result.get('results'): + # components = result['results'][0]['address_components'] + # formatted = result['results'][0]['formatted_address'] + # return components, formatted, self._parse_google_address(components) + # return {}, '', {} + + # def _parse_google_address(self, components): + # def get(types): + # for comp in components: + # if types in comp['types']: + # return comp['long_name'] + # return '' + + # street_number = get('street_number') + # route = get('route') + # neighborhood = get('neighborhood') # Bisa jadi nama RW + # subpremise = get('subpremise') # Bisa jadi no kamar/ruko + + # # Gabungkan informasi jalan + # road = " ".join(filter(None, [route, street_number, subpremise, neighborhood])) + + # return { + # 'road': road.strip(), + # 'postcode': get('postal_code'), + # 'state': get('administrative_area_level_1'), + # 'city': get('administrative_area_level_2') or get('locality'), + # 'district': get('administrative_area_level_3'), + # 'suburb': get('administrative_area_level_4'), + # 'formatted': get('formatted_address'), + # } diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index bfc4e5fd..d0017115 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -75,6 +75,7 @@ class SaleOrderLine(models.Model): _inherit = 'sale.order.line' def unlink(self): + lines_to_reject = [] for line in self: if line.order_id: -- cgit v1.2.3 From 6e2419c226eb74ea5c14d21f441d570829a20021 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 16 Jun 2025 08:54:56 +0700 Subject: (andri) on map view --- indoteknik_custom/models/res_partner.py | 254 ++++++++++++++++---------------- 1 file changed, 127 insertions(+), 127 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index 1786efa3..a8ce95d1 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -148,7 +148,7 @@ class ResPartner(models.Model): date_payment_terms_purchase = fields.Datetime(string='Date Update Payment Terms') longtitude = fields.Char(string='Longtitude') latitude = fields.Char(string='Latitude') - # map_view = fields.Char(string='Map') + map_view = fields.Char(string='Map') address_map = fields.Char(string='Address Map', help='Alamat ini diisi otomatis berdasarkan koordinat pin pada peta. Silakan koreksi dan ubah jika terdapat ketidaksesuaian', tracking=3) company_type = fields.Selection(string='Company Type', selection=[('person', 'Individual'), ('company', 'Company')], @@ -549,130 +549,130 @@ class ResPartner(models.Model): 'target': 'current', } - # def geocode_address(self): - # for rec in self: - # # Daftar field penting - # required_fields = { - # 'Alamat Jalan (street)': rec.street, - # 'Kelurahan': rec.kelurahan_id.name if rec.kelurahan_id else '', - # 'Kecamatan': rec.kecamatan_id.name if rec.kecamatan_id else '', - # 'Kota': rec.kota_id.name if rec.kota_id else '', - # 'Kode Pos': rec.zip, - # 'Provinsi': rec.state_id.name if rec.state_id else '', - # } + def geocode_address(self): + for rec in self: + # Daftar field penting + required_fields = { + 'Alamat Jalan (street)': rec.street, + 'Kelurahan': rec.kelurahan_id.name if rec.kelurahan_id else '', + 'Kecamatan': rec.kecamatan_id.name if rec.kecamatan_id else '', + 'Kota': rec.kota_id.name if rec.kota_id else '', + 'Kode Pos': rec.zip, + 'Provinsi': rec.state_id.name if rec.state_id else '', + } + + # Cek jika ada yang kosong + missing = [label for label, val in required_fields.items() if not val] + if missing: + raise UserError( + "Alamat tidak lengkap. Mohon lengkapi field berikut:\n- " + "\n- ".join(missing) + ) + + # Susun alamat lengkap + address = ', '.join([ + required_fields['Alamat Jalan (street)'], + required_fields['Kelurahan'], + required_fields['Kecamatan'], + required_fields['Kota'], + required_fields['Kode Pos'], + required_fields['Provinsi'], + ]) + + # Ambil API Key + api_key = self.env['ir.config_parameter'].sudo().get_param('google.maps.api_key') + if not api_key: + raise UserError("API Key Google Maps belum dikonfigurasi. Silakan isi melalui Settings.") + + # Request ke Google Maps + url = f'https://maps.googleapis.com/maps/api/geocode/json?address={address}&key={api_key}' + response = requests.get(url) + + if response.ok: + result = response.json() + if result.get('results'): + location = result['results'][0]['geometry']['location'] + formatted_address = result['results'][0].get('formatted_address', '') + + rec.latitude = location['lat'] + rec.longtitude = location['lng'] + rec.address_map = formatted_address # ✅ Simpan alamat lengkap + else: + raise UserError("Tidak ditemukan hasil geocode untuk alamat tersebut.") + else: + raise UserError("Permintaan ke Google Maps gagal. Periksa koneksi internet atau API Key.") - # # Cek jika ada yang kosong - # missing = [label for label, val in required_fields.items() if not val] - # if missing: - # raise UserError( - # "Alamat tidak lengkap. Mohon lengkapi field berikut:\n- " + "\n- ".join(missing) - # ) - - # # Susun alamat lengkap - # address = ', '.join([ - # required_fields['Alamat Jalan (street)'], - # required_fields['Kelurahan'], - # required_fields['Kecamatan'], - # required_fields['Kota'], - # required_fields['Kode Pos'], - # required_fields['Provinsi'], - # ]) - - # # Ambil API Key - # api_key = self.env['ir.config_parameter'].sudo().get_param('google.maps.api_key') - # if not api_key: - # raise UserError("API Key Google Maps belum dikonfigurasi. Silakan isi melalui Settings.") - - # # Request ke Google Maps - # url = f'https://maps.googleapis.com/maps/api/geocode/json?address={address}&key={api_key}' - # response = requests.get(url) - - # if response.ok: - # result = response.json() - # if result.get('results'): - # location = result['results'][0]['geometry']['location'] - # formatted_address = result['results'][0].get('formatted_address', '') - - # rec.latitude = location['lat'] - # rec.longtitude = location['lng'] - # rec.address_map = formatted_address # ✅ Simpan alamat lengkap - # else: - # raise UserError("Tidak ditemukan hasil geocode untuk alamat tersebut.") - # else: - # raise UserError("Permintaan ke Google Maps gagal. Periksa koneksi internet atau API Key.") - - # def _update_address_from_coords(self): - # for rec in self: - # if rec.latitude and rec.longtitude: - # try: - # components, formatted, parsed = rec._reverse_geocode(rec.latitude, rec.longtitude) - # if not parsed: - # continue - - # updates = { - # 'street': parsed.get('road') or '', - # 'zip': parsed.get('postcode') or '', - # 'address_map': formatted or '', - # } - - # state = self.env['res.country.state'].search([('name', 'ilike', parsed.get('state'))], limit=1) - # if state: - # updates['state_id'] = state.id - - # kota = self.env['vit.kota'].search([('name', 'ilike', parsed.get('city'))], limit=1) - # if kota: - # updates['kota_id'] = kota.id - - # kec = self.env['vit.kecamatan'].search([('name', 'ilike', parsed.get('district'))], limit=1) - # if kec: - # updates['kecamatan_id'] = kec.id - - # kel = self.env['vit.kelurahan'].search([('name', 'ilike', parsed.get('suburb'))], limit=1) - # if kel: - # updates['kelurahan_id'] = kel.id - - # rec.update(updates) - - # except Exception as e: - # raise UserError(f"Gagal update alamat dari koordinat: {str(e)}") - - - # def _reverse_geocode(self, lat, lng): - # api_key = self.env['ir.config_parameter'].sudo().get_param('google.maps.api_key') - # if not api_key: - # raise UserError("API Key Google Maps belum dikonfigurasi.") - - # url = f'https://maps.googleapis.com/maps/api/geocode/json?latlng={lat},{lng}&key={api_key}' - # response = requests.get(url) - # if response.ok: - # result = response.json() - # if result.get('results'): - # components = result['results'][0]['address_components'] - # formatted = result['results'][0]['formatted_address'] - # return components, formatted, self._parse_google_address(components) - # return {}, '', {} - - # def _parse_google_address(self, components): - # def get(types): - # for comp in components: - # if types in comp['types']: - # return comp['long_name'] - # return '' - - # street_number = get('street_number') - # route = get('route') - # neighborhood = get('neighborhood') # Bisa jadi nama RW - # subpremise = get('subpremise') # Bisa jadi no kamar/ruko - - # # Gabungkan informasi jalan - # road = " ".join(filter(None, [route, street_number, subpremise, neighborhood])) - - # return { - # 'road': road.strip(), - # 'postcode': get('postal_code'), - # 'state': get('administrative_area_level_1'), - # 'city': get('administrative_area_level_2') or get('locality'), - # 'district': get('administrative_area_level_3'), - # 'suburb': get('administrative_area_level_4'), - # 'formatted': get('formatted_address'), - # } + def _update_address_from_coords(self): + for rec in self: + if rec.latitude and rec.longtitude: + try: + components, formatted, parsed = rec._reverse_geocode(rec.latitude, rec.longtitude) + if not parsed: + continue + + updates = { + 'street': parsed.get('road') or '', + 'zip': parsed.get('postcode') or '', + 'address_map': formatted or '', + } + + state = self.env['res.country.state'].search([('name', 'ilike', parsed.get('state'))], limit=1) + if state: + updates['state_id'] = state.id + + kota = self.env['vit.kota'].search([('name', 'ilike', parsed.get('city'))], limit=1) + if kota: + updates['kota_id'] = kota.id + + kec = self.env['vit.kecamatan'].search([('name', 'ilike', parsed.get('district'))], limit=1) + if kec: + updates['kecamatan_id'] = kec.id + + kel = self.env['vit.kelurahan'].search([('name', 'ilike', parsed.get('suburb'))], limit=1) + if kel: + updates['kelurahan_id'] = kel.id + + rec.update(updates) + + except Exception as e: + raise UserError(f"Gagal update alamat dari koordinat: {str(e)}") + + + def _reverse_geocode(self, lat, lng): + api_key = self.env['ir.config_parameter'].sudo().get_param('google.maps.api_key') + if not api_key: + raise UserError("API Key Google Maps belum dikonfigurasi.") + + url = f'https://maps.googleapis.com/maps/api/geocode/json?latlng={lat},{lng}&key={api_key}' + response = requests.get(url) + if response.ok: + result = response.json() + if result.get('results'): + components = result['results'][0]['address_components'] + formatted = result['results'][0]['formatted_address'] + return components, formatted, self._parse_google_address(components) + return {}, '', {} + + def _parse_google_address(self, components): + def get(types): + for comp in components: + if types in comp['types']: + return comp['long_name'] + return '' + + street_number = get('street_number') + route = get('route') + neighborhood = get('neighborhood') # Bisa jadi nama RW + subpremise = get('subpremise') # Bisa jadi no kamar/ruko + + # Gabungkan informasi jalan + road = " ".join(filter(None, [route, street_number, subpremise, neighborhood])) + + return { + 'road': road.strip(), + 'postcode': get('postal_code'), + 'state': get('administrative_area_level_1'), + 'city': get('administrative_area_level_2') or get('locality'), + 'district': get('administrative_area_level_3'), + 'suburb': get('administrative_area_level_4'), + 'formatted': get('formatted_address'), + } -- cgit v1.2.3 From ac3e99d4d7e1fd21aa146d621a06b42df86e3f7b Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 16 Jun 2025 09:20:13 +0700 Subject: (andri) set default shipping option ke custom --- 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 7c3f8952..39b74069 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -333,7 +333,7 @@ class SaleOrder(models.Model): select_shipping_option = fields.Selection([ ('biteship', 'Biteship'), ('custom', 'Custom'), - ], string='Shipping Option', help="Select shipping option for delivery", tracking=True) + ], string='Shipping Option', help="Select shipping option for delivery", tracking=True, default='custom') hold_outgoing = fields.Boolean('Hold Outgoing SO', tracking=3) state_ask_cancel = fields.Selection([ -- cgit v1.2.3 From 9b6aa67f911a0db8d466d2f63cf1d8ce43ab8e14 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 16 Jun 2025 10:59:35 +0700 Subject: (andri) fix autoset checkout SO custom --- indoteknik_custom/models/sale_order.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 39b74069..0662522f 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -2755,7 +2755,8 @@ class SaleOrder(models.Model): continue # Skip jika Self Pick Up - if order.carrier_id and order.carrier_id.id == 32: + if int(order.carrier_id.id or 0) == 32: + _logger.info(f"[Checkout] Skip estimasi: Self Pickup untuk SO {order.name}") order.select_shipping_option = 'custom' continue -- cgit v1.2.3 From dba82e88291ac63473b8f45dd7a4da5b4d147973 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Mon, 16 Jun 2025 14:00:14 +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 6eac857c..92317334 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1362,7 +1362,7 @@ class StockPicking(models.Model): picking_date = fields.Date.to_date(picking.date_doc_kirim) invoice_date = fields.Date.to_date(invoice.invoice_date) - if picking_date != invoice_date and picking.update_date_doc_kirim_add: + if picking_date != invoice_date and picking.update_date_doc_kirim_add and not picking.so_lama: raise UserError("Tanggal Kirim (%s) tidak sesuai dengan Tanggal Invoice (%s)!" % ( picking_date.strftime('%d-%m-%Y'), invoice_date.strftime('%d-%m-%Y') -- cgit v1.2.3 From a0e90200638e26ad06d1caaf2d91d0aeea3ba19d Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 16 Jun 2025 14:38:40 +0700 Subject: add tukar guling PO and move from inventory --- indoteknik_custom/models/tukar_guling.py | 211 +++++++++++++++++++++---------- 1 file changed, 146 insertions(+), 65 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index f8cbec0a..95aa7cd6 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -12,66 +12,34 @@ class TukarGuling(models.Model): date = fields.Datetime('Date', default=fields.Datetime.now, required=True) out_num = fields.Many2one('stock.picking', 'Nomor BU/Out', domain=[('picking_type_id.code', '=', 'outgoing')]) - in_num = fields.Many2one('stock.picking', 'Nomor BU/In', domain=[('picking_type_id.code', '=', 'incoming')]) ba_num = fields.Text('Nomor BA') notes = fields.Text('Notes') return_type = fields.Selection(String='Return Type', selection=[ ('tukar_guling', 'Tukar Guling'), # -> barang yang sama ('revisi_so', 'Revisi SO'), # -> ganti barang ? - ('revisi_po', 'Revisi PO'), - ('credit_memo', 'Credit Memo'), # -> dijadiin credit memo - ('debit_memo', 'Debit Memo')]) + ('credit_memo', 'Credit Memo')]) # -> dijadiin credit memo state = fields.Selection(string='Status', selection=[ ('draft', 'Draft'), - ('waiting', 'Waiting for Approval'), + ('approval_sales', ' Approval Sales'), + ('approval_logistic', 'Approval Logistic'), + ('approval_finance', 'Approval Finance'), ('done', 'Done'), ('cancel', 'Canceled') ], default='draft', tracking=True, required=True) line_ids = fields.One2many('tukar.guling.line', 'tukar_guling_id', string='Product Lines') - @api.onchange('return_type') - def _onchange_return_type(self): - in_domain = [] - out_domain = [] - - if self.return_type in ['debit_memo', 'revisi_po']: - # Hanya tampilkan BU In - in_domain = [('picking_type_id.code', '=', 'incoming')] - out_domain = [('id', '=', False)] # Kosongkan BU Out - elif self.return_type in ['revisi_so', 'credit_memo']: - # Hanya tampilkan BU Out - in_domain = [('id', '=', False)] # Kosongkan BU In - out_domain = [('picking_type_id.code', '=', 'outgoing')] - elif self.return_type == 'tukar_guling': - # Boleh pilih keduanya - in_domain = [('picking_type_id.code', '=', 'incoming')] - out_domain = [('picking_type_id.code', '=', 'outgoing')] - - return { - 'domain': { - 'in_num': in_domain, - 'out_num': out_domain, - } - } - - @api.constrains('return_type', 'in_num', 'out_num') + @api.constrains('return_type', 'out_num') def _check_required_bu_fields(self): for record in self: - if record.return_type in ['debit_memo', 'revisi_po'] and not record.in_num: - raise ValidationError("BU/In harus diisi untuk return type Debit Memo atau Revisi PO.") - - if record.return_type in ['revisi_so', 'credit_memo'] and not record.out_num: - raise ValidationError("BU/Out harus diisi untuk return type Revisi SO atau Credit Memo.") - - if record.return_type == 'tukar_guling' and not (record.in_num or record.out_num): - raise ValidationError("Untuk Tukar Guling, minimal isi salah satu, BU/In atau BU/Out.") + if record.return_type in ['revisi_so', 'credit_memo', 'tukar_guling'] and not record.out_num: + raise ValidationError("BU/Out harus diisi!") @api.constrains('line_ids', 'state') def _check_product_lines(self): """Constraint: Product lines harus ada jika state bukan draft""" for record in self: - if record.state in ('waiting', 'done') and not record.line_ids: + if record.state in ('approval_sales', 'approval_logistic', 'approval_finance', 'done') and not record.line_ids: raise ValidationError("Product lines harus diisi sebelum submit atau approve!") def _validate_product_lines(self): @@ -95,29 +63,29 @@ class TukarGuling(models.Model): return True - @api.model_create_multi - def create(self, vals_list): - for vals in vals_list: - if vals.get('name', 'New') == 'New': - vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling') or 'PTG/00001' - return super(TukarGuling, self).create(vals_list) + @api.model + def create(self, vals): + if not vals.get('name') or vals['name'] == 'New': + vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling') or 'New' + return super(TukarGuling, self).create(vals) def copy(self, default=None): - """Override copy untuk custom behavior saat duplicate""" if default is None: default = {} - # Reset fields penting saat duplicate + if 'name' not in default: + default.update({ + 'name': self.env['ir.sequence'].next_by_code(self._name) or 'New', + }) + default.update({ - 'name': 'New', 'state': 'draft', 'date': fields.Datetime.now(), }) - # Copy record dengan default values new_record = super(TukarGuling, self).copy(default) - # Re-sequence line items record baru + # Re-sequence lines if new_record.line_ids: for i, line in enumerate(new_record.line_ids): line.sequence = (i + 1) * 10 @@ -134,40 +102,153 @@ class TukarGuling(models.Model): def action_submit(self): self.ensure_one() - # cek bu out sudah diisi atau blm + + if self.state != 'draft': + raise UserError("Submit hanya bisa dilakukan dari Draft.") + self.state = 'approval_sales' + + def action_approve(self): + self.ensure_one() + if not self.out_num: raise UserError("BU/Out harus diisi!") - # cek return type - # if not self.return_type: - # raise UserError("Return Type harus diisi!") + if not self.return_type: + raise UserError("Return Type harus diisi!") + + # Cek hak akses berdasarkan state + if self.state == 'approval_sales': + if not self.env.user.has_group('indoteknik_custom.group_sales_manager'): + raise UserError("Hanya Sales Manager yang boleh approve tahap ini.") + self.state = 'approval_logistic' + + elif self.state == 'approval_logistic': + if not self.env.user.has_group('indoteknik_custom.group_logistic'): + raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.") + self.state = 'approval_finance' + + elif self.state == 'approval_finance': + if not self.env.user.has_group('indoteknik_custom.group_finance'): + raise UserError("Hanya Finance Manager yang boleh approve tahap ini.") + self.state = 'done' + + else: + raise UserError("Status ini tidak bisa di-approve.") + def action_cancel(self): + self.ensure_one() + # if self.state == 'done': + # raise UserError("Tidak bisa cancel jika sudah done") + self.state = 'cancel' + +class TukarGulingPO(models.Model): + _name = 'tukar.guling.po' + _inherit = 'tukar.guling' + _description = 'Tukar Guling PO' + + # tukar_guling_id = fields.Many2one( + # 'tukar.guling', required=True, ondelete='cascade', string='Tukar Guling Ref' + # ) + + return_type = fields.Selection([ + ('tukar_guling', 'Tukar Guling'), + ('revisi_po', 'Revisi PO'), + ('debit_memo', 'Debit Memo'), + ], string='Return Type', required=True) + + @api.constrains('return_type', 'out_num') + def _check_required_bu_fields(self): + for record in self: + if record.return_type in ['tukar_guling', 'revisi_po', 'debit_memo'] and not record.out_num: + raise ValidationError("BU/Out harus diisi!") + + @api.constrains('line_ids', 'state') + def _check_product_lines(self): + """Constraint: Product lines harus ada jika state bukan draft""" + for record in self: + if record.state in ('approval_sales', 'approval_logistic', 'approval_finance', 'done') and not record.line_ids: + raise ValidationError("Product lines harus diisi sebelum submit atau approve!") + + def _validate_product_lines(self): + """Helper method untuk validasi product lines""" + self.ensure_one() + + # Check ada product lines + if not self.line_ids: + raise UserError("Belum ada product lines yang ditambahkan!") + + # Check product sudah diisi + empty_lines = self.line_ids.filtered(lambda line: not line.product_id) + if empty_lines: + raise UserError("Ada product lines yang belum diisi productnya!") + + # Check quantity > 0 + zero_qty_lines = self.line_ids.filtered(lambda line: line.product_uom_qty <= 0) + if zero_qty_lines: + raise UserError("Quantity product tidak boleh kosong atau 0!") + return True + + @api.model + def create(self, vals): + if not vals.get('name') or vals['name'] in ('New', False): + vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling.po') or 'New' + return super(TukarGulingPO, self).create(vals) + def copy(self, default=None): + if default is None: + default = {} + + # Generate sequence satu-satunya di sini + default['name'] = self.env['ir.sequence'].next_by_code('tukar.guling.po') or 'New' + default['state'] = 'draft' + default['date'] = fields.Datetime.now() + + new_record = super(TukarGulingPO, self).copy(default) + + # Re-sequence lines + if new_record.line_ids: + for i, line in enumerate(new_record.line_ids): + line.sequence = (i + 1) * 10 + + return new_record + + 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_submit(self): + self.ensure_one() if self.state != 'draft': - raise UserError("Hanya status Draft saja yang bisa di submit") - self.state = 'waiting' + raise UserError("Submit hanya bisa dilakukan dari Draft.") + self.state = 'approval_sales' def action_approve(self): self.ensure_one() - # cek bu out sudah diisi atau blm if not self.out_num: raise UserError("BU/Out harus diisi!") - # cek return type if not self.return_type: raise UserError("Return Type harus diisi!") - if self.state != 'waiting': - raise UserError("Hanya status Waiting saja yang bisa di approve") - self.state = 'done' + if self.state == 'approval_sales': + self.state = 'approval_logistic' + elif self.state == 'approval_logistic': + self.state = 'approval_finance' + elif self.state == 'approval_finance': + self.state = 'done' + else: + raise UserError("Status ini tidak bisa di-approve.") def action_cancel(self): self.ensure_one() - if self.state == 'done': - raise UserError("Tidak bisa cancel jika sudah done") + # if self.state == 'done': + # raise UserError("Tidak bisa cancel jika sudah done") self.state = 'cancel' - class TukarGulingLine(models.Model): _name = 'tukar.guling.line' _description = 'Tukar Guling Line' -- cgit v1.2.3 From 3c8ff8cb6a24dd1ddec7d34313722d7dee6f23a3 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 16 Jun 2025 16:15:57 +0700 Subject: (andri) taruh key hardcode --- indoteknik_custom/models/sale_order.py | 4 +++- indoteknik_custom/models/stock_picking.py | 13 ++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 0662522f..1771f210 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -1102,7 +1102,9 @@ class SaleOrder(models.Model): def _call_biteship_api(self, origin_data, destination_data, items, couriers=None): url = "https://api.biteship.com/v1/rates/couriers" - api_key = self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_live') + api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo" + # api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA" + # api_key = self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_live') # api_key = self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_test') headers = { 'Authorization': api_key, diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 6eac857c..f552ff3f 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -19,6 +19,9 @@ import re _logger = logging.getLogger(__name__) _biteship_url = "https://api.biteship.com/v1" +biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo" +# biteship_api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA" + class StockPicking(models.Model): _inherit = 'stock.picking' @@ -165,9 +168,9 @@ class StockPicking(models.Model): area_name = fields.Char(string="Area", compute="_compute_area_name") - def _get_biteship_api_key(self): - # return self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_test') - return self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_live') + # def _get_biteship_api_key(self): + # # return self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_test') + # return self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_live') @api.depends('real_shipping_id.kecamatan_id', 'real_shipping_id.kota_id') def _compute_area_name(self): @@ -762,7 +765,7 @@ class StockPicking(models.Model): _logger.info(f"Payload untuk Biteship: {payload}") # Kirim ke Biteship - api_key = self._get_biteship_api_key() + api_key = biteship_api_key headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" @@ -1705,7 +1708,7 @@ class StockPicking(models.Model): return response def get_manifest_biteship(self): - api_key = self._get_biteship_api_key() + api_key = biteship_api_key headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" -- cgit v1.2.3 From abd7da741c6eec02dbefa195b91dbedd70b3323e Mon Sep 17 00:00:00 2001 From: Miqdad Date: Tue, 17 Jun 2025 08:09:57 +0700 Subject: add tukar guling PO and move from inventory --- indoteknik_custom/models/tukar_guling.py | 56 +++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 8 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 95aa7cd6..aeb2c9e7 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -142,12 +142,25 @@ class TukarGuling(models.Model): class TukarGulingPO(models.Model): _name = 'tukar.guling.po' - _inherit = 'tukar.guling' _description = 'Tukar Guling PO' - # tukar_guling_id = fields.Many2one( - # 'tukar.guling', required=True, ondelete='cascade', string='Tukar Guling Ref' - # ) + name = fields.Char('Number', required=True, copy=False, readonly=True, default='New') + date = fields.Datetime('Date', default=fields.Datetime.now, required=True) + out_num = fields.Many2one('stock.picking', 'Nomor BU/Out', + domain=[('picking_type_id.code', '=', 'outgoing')]) + ba_num = fields.Text('Nomor BA') + notes = fields.Text('Notes') + state = fields.Selection(string='Status', selection=[ + ('draft', 'Draft'), + ('approval_purchase', ' Approval Purchase'), + ('approval_logistic', 'Approval Logistic'), + ('approval_finance', 'Approval Finance'), + ('done', 'Done'), + ('cancel', 'Canceled') + ], default='draft', tracking=True, required=True) + + line_ids = fields.One2many('tukar.guling.line.po', 'tukar_guling_po_id', string='Product Lines') + tukar_guling_po_id = fields.Many2one('tukar.guling.po', 'Tukar Guling PO') return_type = fields.Selection([ ('tukar_guling', 'Tukar Guling'), @@ -165,7 +178,7 @@ class TukarGulingPO(models.Model): def _check_product_lines(self): """Constraint: Product lines harus ada jika state bukan draft""" for record in self: - if record.state in ('approval_sales', 'approval_logistic', 'approval_finance', 'done') and not record.line_ids: + if record.state in ('approval_purchase', 'approval_logistic', 'approval_finance', 'done') and not record.line_ids: raise ValidationError("Product lines harus diisi sebelum submit atau approve!") def _validate_product_lines(self): @@ -223,7 +236,7 @@ class TukarGulingPO(models.Model): if self.state != 'draft': raise UserError("Submit hanya bisa dilakukan dari Draft.") - self.state = 'approval_sales' + self.state = 'approval_purchase' def action_approve(self): self.ensure_one() @@ -234,12 +247,21 @@ class TukarGulingPO(models.Model): if not self.return_type: raise UserError("Return Type harus diisi!") - if self.state == 'approval_sales': + if self.state == 'approval_purchase': + if not self.env.user.has_group('indoteknik_custom.group_role_purchasing'): + raise UserError("Hanya Purchasing yang boleh approve tahap ini.") self.state = 'approval_logistic' + elif self.state == 'approval_logistic': + if not self.env.user.has_group('indoteknik_custom.group_role_logistic'): + raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.") self.state = 'approval_finance' + elif self.state == 'approval_finance': + if not self.env.user.has_group('indoteknik_custom.group_role_fat'): + raise UserError("Hanya Finance Manager yang boleh approve tahap ini.") self.state = 'done' + else: raise UserError("Status ini tidak bisa di-approve.") @@ -281,4 +303,22 @@ class TukarGulingLine(models.Model): def _onchange_product_id(self): if self.product_id: self.name = self.product_id.display_name - self.product_uom = self.product_id.uom_id \ No newline at end of file + self.product_uom = self.product_id.uom_id + +class TukarGulingLinePO(models.Model): + _name = 'tukar.guling.line.po' + _description = 'Tukar Guling Line (PO)' + _order = 'sequence, id' + + tukar_guling_po_id = fields.Many2one('tukar.guling.po', string='Tukar Guling PO', required=True, ondelete='cascade') + sequence = fields.Integer('Sequence', default=10, copy=False) + product_id = fields.Many2one('product.product', string='Product', required=True) + product_uom_qty = fields.Float('Quantity', digits='Product Unit of Measure', required=True, default=1.0) + product_uom = fields.Many2one('uom.uom', string='Unit of Measure') + name = fields.Text('Description') + + @api.onchange('product_id') + def _onchange_product_id(self): + if self.product_id: + self.name = self.product_id.display_name + self.product_uom = self.product_id.uom_id -- cgit v1.2.3 From 3a76bd301734621831f291228deaa962c144be5e Mon Sep 17 00:00:00 2001 From: Miqdad Date: Tue, 17 Jun 2025 08:38:21 +0700 Subject: fix group role --- indoteknik_custom/models/tukar_guling.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index aeb2c9e7..5e814459 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -118,17 +118,17 @@ class TukarGuling(models.Model): # Cek hak akses berdasarkan state if self.state == 'approval_sales': - if not self.env.user.has_group('indoteknik_custom.group_sales_manager'): + if not self.env.user.has_group('indoteknik_custom.group_role_sales'): raise UserError("Hanya Sales Manager yang boleh approve tahap ini.") self.state = 'approval_logistic' elif self.state == 'approval_logistic': - if not self.env.user.has_group('indoteknik_custom.group_logistic'): + if not self.env.user.has_group('indoteknik_custom.group_role_logistic'): raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.") self.state = 'approval_finance' elif self.state == 'approval_finance': - if not self.env.user.has_group('indoteknik_custom.group_finance'): + if not self.env.user.has_group('indoteknik_custom.group_role_fat'): raise UserError("Hanya Finance Manager yang boleh approve tahap ini.") self.state = 'done' -- cgit v1.2.3 From a80565682063e718fc55c90e4243b9d5b2432285 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 17 Jun 2025 08:47:53 +0700 Subject: (andri) fix load gmaps pada view & edit --- indoteknik_custom/models/res_partner.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index a8ce95d1..9986b9c0 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -579,9 +579,10 @@ class ResPartner(models.Model): ]) # Ambil API Key - api_key = self.env['ir.config_parameter'].sudo().get_param('google.maps.api_key') - if not api_key: - raise UserError("API Key Google Maps belum dikonfigurasi. Silakan isi melalui Settings.") + api_key = "AIzaSyB7bG9aSNAJnSrj0Z7f1abFsqKVoiJfsPE" + # api_key = self.env['ir.config_parameter'].sudo().get_param('google.maps.api_key') + # if not api_key: + # raise UserError("API Key Google Maps belum dikonfigurasi. Silakan isi melalui Settings.") # Request ke Google Maps url = f'https://maps.googleapis.com/maps/api/geocode/json?address={address}&key={api_key}' @@ -638,9 +639,10 @@ class ResPartner(models.Model): def _reverse_geocode(self, lat, lng): - api_key = self.env['ir.config_parameter'].sudo().get_param('google.maps.api_key') - if not api_key: - raise UserError("API Key Google Maps belum dikonfigurasi.") + api_key = "AIzaSyB7bG9aSNAJnSrj0Z7f1abFsqKVoiJfsPE" + # api_key = self.env['ir.config_parameter'].sudo().get_param('google.maps.api_key') + # if not api_key: + # raise UserError("API Key Google Maps belum dikonfigurasi.") url = f'https://maps.googleapis.com/maps/api/geocode/json?latlng={lat},{lng}&key={api_key}' response = requests.get(url) -- cgit v1.2.3 From 4fb838700e7d971abb2b1a46d051bc460e532d9e Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 17 Jun 2025 17:55:12 +0700 Subject: (andri) fix onchange shipping option --- indoteknik_custom/models/sale_order.py | 99 ++++++++++++++++------------------ 1 file changed, 47 insertions(+), 52 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 1771f210..03e40145 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -377,37 +377,37 @@ class SaleOrder(models.Model): carrier_ids = [row[0] for row in result if row[0]] return carrier_ids - @api.model - def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): - res = super(SaleOrder, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) - - if view_type == 'form': - doc = etree.XML(res['arch']) - - # Ambil semua delivery_carrier_id dari mapping rajaongkir_kurir - biteship_ids = self.env['rajaongkir.kurir'].search([]).mapped('delivery_carrier_id.id') - biteship_ids = list(set(filter(None, biteship_ids))) # pastikan unik dan bukan None - - all_ids = self.env['delivery.carrier'].search([]).ids - custom_ids = list(set(all_ids) - set(biteship_ids)) - - # Format sebagai string Python list - biteship_ids_str = ','.join(str(i) for i in biteship_ids) or '-1' - custom_ids_str = ','.join(str(i) for i in custom_ids) or '-1' - - # Terapkan domain ke field carrier_id - for node in doc.xpath("//field[@name='carrier_id']"): - # Domain tergantung select_shipping_option - node.set( - 'domain', - "[('id', 'in', [%s]) if select_shipping_option == 'biteship' else ('id', 'in', [%s])]" % - (biteship_ids_str, custom_ids_str) - ) + # @api.model + # def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + # res = super(SaleOrder, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) + + # if view_type == 'form': + # doc = etree.XML(res['arch']) + + # # Ambil semua delivery_carrier_id dari mapping rajaongkir_kurir + # biteship_ids = self.env['rajaongkir.kurir'].search([]).mapped('delivery_carrier_id.id') + # biteship_ids = list(set(filter(None, biteship_ids))) # pastikan unik dan bukan None + + # all_ids = self.env['delivery.carrier'].search([]).ids + # custom_ids = list(set(all_ids) - set(biteship_ids)) + + # # Format sebagai string Python list + # biteship_ids_str = ','.join(str(i) for i in biteship_ids) or '-1' + # custom_ids_str = ','.join(str(i) for i in custom_ids) or '-1' + + # # Terapkan domain ke field carrier_id + # for node in doc.xpath("//field[@name='carrier_id']"): + # # Domain tergantung select_shipping_option + # node.set( + # 'domain', + # "[('id', 'in', [%s]) if select_shipping_option == 'biteship' else ('id', 'in', [%s])]" % + # (biteship_ids_str, custom_ids_str) + # ) - # Simpan kembali hasil XML ke arsitektur form - res['arch'] = etree.tostring(doc, encoding='unicode') + # # Simpan kembali hasil XML ke arsitektur form + # res['arch'] = etree.tostring(doc, encoding='unicode') - return res + # return res # @api.onchange('shipping_option_id') # def _onchange_shipping_option_id(self): @@ -599,40 +599,35 @@ class SaleOrder(models.Model): @api.onchange('select_shipping_option') def _onchange_select_shipping_option(self): - if self.select_shipping_option == 'biteship' and self.shipping_cost_covered == 'indoteknik': - self.select_shipping_option = self._origin.select_shipping_option if self._origin else 'custom' - return { - 'warning': { - 'title': "Biteship Tidak Diizinkan", - 'message': ( - "Biaya pengiriman ditanggung Indoteknik. Tidak diizinkan memilih metode Biteship. " - "Opsi pengiriman dikembalikan ke sebelumnya." - ) - } - } - self.shipping_option_id = False self.carrier_id = False self.delivery_amt = 0 - - # Dapatkan semua ID carrier untuk Biteship + biteship_carrier_ids = [] - - # Gunakan SQL langsung untuk menghindari masalah ORM self.env.cr.execute(""" SELECT delivery_carrier_id FROM rajaongkir_kurir WHERE name IN %s """, (tuple(self._get_biteship_courier_codes()),)) - - # Ambil ID numerik hasil query biteship_carrier_ids = [row[0] for row in self.env.cr.fetchall() if row[0]] - + if self.select_shipping_option == 'biteship': - domain = [('id', 'in', biteship_carrier_ids)] if biteship_carrier_ids else [] - else: # 'custom' - domain = [('id', 'not in', biteship_carrier_ids)] if biteship_carrier_ids else [] - + if self.shipping_cost_covered == 'indoteknik': + self.select_shipping_option = self._origin.select_shipping_option if self._origin else 'custom' + return { + 'warning': { + 'title': "Biteship Tidak Diizinkan", + 'message': ( + "Biaya pengiriman ditanggung Indoteknik. Tidak diizinkan memilih metode Biteship. " + "Opsi pengiriman dikembalikan ke sebelumnya." + ) + } + } + + domain = [('id', 'in', biteship_carrier_ids)] if biteship_carrier_ids else [('id', '=', -1)] + else: + domain = [] # tampilkan semua + return {'domain': {'carrier_id': domain}} # def _compute_total_margin_excl_third_party(self): -- cgit v1.2.3 From 34d276fe64a92c4a1c75f6fbf8fa961e84c8afc4 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Tue, 17 Jun 2025 22:52:39 +0700 Subject: push --- indoteknik_custom/models/stock_picking.py | 10 ++- indoteknik_custom/models/tukar_guling.py | 135 ++++++++++++++++++++++++++---- 2 files changed, 127 insertions(+), 18 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index eabef37c..e24bff02 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -26,6 +26,10 @@ biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lI class StockPicking(models.Model): _inherit = 'stock.picking' _order = 'final_seq ASC' + tukar_guling_id = fields.Many2one( + 'tukar.guling', + string='Tukar Guling Reference' + ) konfirm_koli_lines = fields.One2many('konfirm.koli', 'picking_id', string='Konfirm Koli', auto_join=True, copy=False) scan_koli_lines = fields.One2many('scan.koli', 'picking_id', string='Scan Koli', auto_join=True, copy=False) @@ -1196,6 +1200,10 @@ class StockPicking(models.Model): def button_validate(self): self.check_invoice_date() + _logger.info("Kode Picking: %s", self.picking_type_id.code) + _logger.info("Group ID: %s", self.group_id) + _logger.info("Group ID ID: %s", self.group_id.id if self.group_id else None) + _logger.info("Is Internal Use: %s", self.is_internal_use) threshold_datetime = waktu(2025, 4, 11, 6, 26) group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) @@ -2391,4 +2399,4 @@ class WarningModalWizard(models.TransientModel): def action_continue(self): if self.picking_id: return self.picking_id.with_context(skip_koli_check=True).button_validate() - return {'type': 'ir.actions.act_window_close'} + return {'type': 'ir.actions.act_window_close'} \ No newline at end of file diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 5e814459..29670f5c 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -8,6 +8,13 @@ class TukarGuling(models.Model): _order = 'date desc, id desc' _rec_name = 'name' + real_shipping_id = fields.Many2one('res.partner', string='Shipping Address') + + picking_ids = fields.One2many( + 'stock.picking', + 'tukar_guling_id', + string='Transfers' + ) name = fields.Char('Number', required=True, copy=False, readonly=True, default='New') date = fields.Datetime('Date', default=fields.Datetime.now, required=True) out_num = fields.Many2one('stock.picking', 'Nomor BU/Out', @@ -92,6 +99,17 @@ class TukarGuling(models.Model): return new_record + def action_view_picking(self): + self.ensure_one() + action = self.env.ref('stock.action_picking_tree_all').read()[0] + pickings = self.picking_ids + if len(pickings) > 1: + action['domain'] = [('id', 'in', pickings.ids)] + elif pickings: + action['views'] = [(self.env.ref('stock.view_picking_form').id, 'form')] + action['res_id'] = pickings.id + return action + def action_draft(self): """Reset to draft state""" for record in self: @@ -117,29 +135,107 @@ class TukarGuling(models.Model): raise UserError("Return Type harus diisi!") # Cek hak akses berdasarkan state - if self.state == 'approval_sales': - if not self.env.user.has_group('indoteknik_custom.group_role_sales'): - raise UserError("Hanya Sales Manager yang boleh approve tahap ini.") - self.state = 'approval_logistic' - - elif self.state == 'approval_logistic': - if not self.env.user.has_group('indoteknik_custom.group_role_logistic'): - raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.") - self.state = 'approval_finance' - - elif self.state == 'approval_finance': - if not self.env.user.has_group('indoteknik_custom.group_role_fat'): - raise UserError("Hanya Finance Manager yang boleh approve tahap ini.") - self.state = 'done' - - else: - raise UserError("Status ini tidak bisa di-approve.") + for rec in self: + if rec.state == 'approval_sales': + if not rec.env.user.has_group('indoteknik_custom.group_role_sales'): + raise UserError("Hanya Sales Manager yang boleh approve tahap ini.") + rec.state = 'approval_logistic' + + elif rec.state == 'approval_logistic': + if not rec.env.user.has_group('indoteknik_custom.group_role_logistic'): + raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.") + rec.state = 'approval_finance' + + elif rec.state == 'approval_finance': + if not rec.env.user.has_group('indoteknik_custom.group_role_fat'): + raise UserError("Hanya Finance Manager yang boleh approve tahap ini.") + rec.state = 'done' + rec._create_pickings() + else: + raise UserError("Status ini tidak bisa di-approve.") def action_cancel(self): self.ensure_one() # if self.state == 'done': # raise UserError("Tidak bisa cancel jika sudah done") self.state = 'cancel' + def _create_pickings(self): + if not self.out_num: + raise UserError("BU/Out harus diisi terlebih dahulu.") + + group_id = self.out_num.group_id.id if self.out_num.group_id else False + + Picking = self.env['stock.picking'] + srt_type = self.env['stock.picking.type'].search([ + ('sequence_code', '=', 'SRT') + ], limit=1) + + ort_type = self.env['stock.picking.type'].search([ + ('sequence_code', '=', 'ORT') + ], limit=1) + + # Lokasi + location_dest_id = srt_type.default_location_dest_id.id + location_customer = self.out_num.location_dest_id + + # 1. BU/SRT: retur dari out_num + srt_picking = Picking.create({ + 'partner_id': self.out_num.partner_id.id, + 'picking_type_id': srt_type.id, + 'location_id': location_customer.id, + 'location_dest_id': location_dest_id, + 'origin': f"Retur {self.out_num.name}", + 'tukar_guling_id': self.id, + 'group_id': group_id, + 'move_ids_without_package': [ + (0, 0, { + 'name': line.name or line.product_id.name, + 'product_id': line.product_id.id, + 'product_uom_qty': line.product_uom_qty, + 'product_uom': line.product_uom.id, + 'location_id': location_customer.id, + 'location_dest_id': location_dest_id, + }) for line in self.line_ids + ] + }) + srt_picking.action_confirm() + + # 2. Cari BU/PICK dari SO yang sama + origin_so = self.out_num.origin + if not origin_so: + raise UserError("BU/OUT tidak memiliki origin (SO), tidak bisa cari BU/PICK.") + + pick = Picking.search([ + ('origin', '=', origin_so), + ('picking_type_id.code', '=', 'internal') + ], limit=1) + + if not pick: + raise UserError(f"BU/PICK dengan origin {origin_so} tidak ditemukan.") + + # 3. BU/ORT: retur dari BU/PICK + ort_picking = Picking.create({ + 'partner_id': self.out_num.partner_id.id, + 'picking_type_id': ort_type.id, + 'location_id': location_dest_id, + 'location_dest_id': location_customer.id, + 'origin': f"Retur {pick.name}", + 'tukar_guling_id': self.id, + 'group_id': group_id, + 'move_ids_without_package': [ + (0, 0, { + 'name': line.name or line.product_id.name, + 'product_id': line.product_id.id, + 'product_uom_qty': line.product_uom_qty, + 'product_uom': line.product_uom.id, + 'location_id': location_dest_id, + 'location_dest_id': location_customer.id, + }) for line in self.line_ids + ] + }) + ort_picking.action_confirm() + + class TukarGulingPO(models.Model): _name = 'tukar.guling.po' _description = 'Tukar Guling PO' @@ -322,3 +418,8 @@ class TukarGulingLinePO(models.Model): if self.product_id: self.name = self.product_id.display_name self.product_uom = self.product_id.uom_id + +class StockPicking(models.Model): + _inherit = 'stock.picking' + + origin_tukar_guling_id = fields.Many2one('tukar.guling', string='Tukar Guling Ref') \ No newline at end of file -- cgit v1.2.3 From cca31a5f582e097518701a192d4cda88525fc979 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 18 Jun 2025 08:09:14 +0700 Subject: (andri) fix onchange shipping option --- indoteknik_custom/models/sale_order.py | 1 + 1 file changed, 1 insertion(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 03e40145..68beffbc 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -600,6 +600,7 @@ class SaleOrder(models.Model): @api.onchange('select_shipping_option') def _onchange_select_shipping_option(self): self.shipping_option_id = False + self.delivery_service_type = False self.carrier_id = False self.delivery_amt = 0 -- cgit v1.2.3 From 2a349bbe7d5317433e339d873bdaa46e9d37ae17 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 18 Jun 2025 08:15:56 +0700 Subject: Add origin SO --- indoteknik_custom/models/tukar_guling.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 29670f5c..c1672b73 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -8,6 +8,8 @@ class TukarGuling(models.Model): _order = 'date desc, id desc' _rec_name = 'name' + origin = fields.Char(string='Origin SO') + real_shipping_id = fields.Many2one('res.partner', string='Shipping Address') picking_ids = fields.One2many( @@ -74,6 +76,11 @@ class TukarGuling(models.Model): def create(self, vals): if not vals.get('name') or vals['name'] == 'New': vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling') or 'New' + # Auto-fill origin from out_num + if not vals.get('origin') and vals.get('out_num'): + picking = self.env['stock.picking'].browse(vals['out_num']) + if picking.origin: + vals['origin'] = picking.origin return super(TukarGuling, self).create(vals) def copy(self, default=None): @@ -99,6 +106,14 @@ class TukarGuling(models.Model): return new_record + def write(self, vals): + if 'out_num' in vals and not vals.get('origin'): + picking = self.env['stock.picking'].browse(vals['out_num']) + if picking.origin: + vals['origin'] = picking.origin + + return super(TukarGuling, self).write(vals) + def action_view_picking(self): self.ensure_one() action = self.env.ref('stock.action_picking_tree_all').read()[0] -- cgit v1.2.3 From 968d9987eb53670f0d96209e77debb1196f9b939 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 18 Jun 2025 09:33:37 +0700 Subject: tukar guling so done --- indoteknik_custom/models/tukar_guling.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index c1672b73..7ed6e10f 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -1,6 +1,8 @@ from odoo import models, fields, api from odoo.exceptions import UserError, ValidationError +import logging +_logger = logging.getLogger(__name__) class TukarGuling(models.Model): _name = 'tukar.guling' @@ -178,7 +180,19 @@ class TukarGuling(models.Model): if not self.out_num: raise UserError("BU/Out harus diisi terlebih dahulu.") - group_id = self.out_num.group_id.id if self.out_num.group_id else False + origin_so = self.out_num.origin + if not origin_so: + raise UserError("BU/OUT tidak memiliki origin (SO), tidak bisa cari BU/PICK.") + + # Cari DO dari SO + get_group_id = self.env['stock.picking'].search([ + ('origin', '=', origin_so), + ], limit=1) + + if not get_group_id: + raise UserError(f"Delivery Order dari SO {origin_so} tidak ditemukan.") + + group_id = get_group_id.group_id.id if get_group_id.group_id else False Picking = self.env['stock.picking'] srt_type = self.env['stock.picking.type'].search([ @@ -191,6 +205,7 @@ class TukarGuling(models.Model): # Lokasi location_dest_id = srt_type.default_location_dest_id.id + location_dest_id_ort = ort_type.default_location_dest_id.id location_customer = self.out_num.location_dest_id # 1. BU/SRT: retur dari out_num @@ -210,6 +225,7 @@ class TukarGuling(models.Model): 'product_uom': line.product_uom.id, 'location_id': location_customer.id, 'location_dest_id': location_dest_id, + 'group_id': group_id, }) for line in self.line_ids ] }) @@ -233,7 +249,7 @@ class TukarGuling(models.Model): 'partner_id': self.out_num.partner_id.id, 'picking_type_id': ort_type.id, 'location_id': location_dest_id, - 'location_dest_id': location_customer.id, + 'location_dest_id': location_dest_id_ort, 'origin': f"Retur {pick.name}", 'tukar_guling_id': self.id, 'group_id': group_id, @@ -244,7 +260,8 @@ class TukarGuling(models.Model): 'product_uom_qty': line.product_uom_qty, 'product_uom': line.product_uom.id, 'location_id': location_dest_id, - 'location_dest_id': location_customer.id, + 'location_dest_id': location_dest_id_ort, + 'group_id': group_id, }) for line in self.line_ids ] }) -- cgit v1.2.3 From 9d7d71f23de6335464d96d2a04ba8b36db620105 Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Wed, 18 Jun 2025 09:57:55 +0700 Subject: change tempo limit approval --- indoteknik_custom/models/user_pengajuan_tempo_request.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/user_pengajuan_tempo_request.py b/indoteknik_custom/models/user_pengajuan_tempo_request.py index 87227764..600381c0 100644 --- a/indoteknik_custom/models/user_pengajuan_tempo_request.py +++ b/indoteknik_custom/models/user_pengajuan_tempo_request.py @@ -371,7 +371,7 @@ class UserPengajuanTempoRequest(models.Model): @api.onchange('tempo_limit') def _onchange_tempo_limit(self): for tempo in self: - if tempo.env.user.id not in (7, 688, 28, 377, 12182, 375): + if tempo.env.user.id not in (7, 688, 28, 19, 375): raise UserError("Limit tempo hanya bisa diubah oleh Sales Manager atau Direktur") def button_approve(self): for tempo in self: @@ -381,7 +381,7 @@ class UserPengajuanTempoRequest(models.Model): if tempo.env.user.id in (688, 28, 7): raise UserError("Pengajuan tempo harus di approve oleh sales manager terlebih dahulu") else: - if tempo.env.user.id not in (377, 12182, 375): + if tempo.env.user.id not in (375, 19): # if tempo.env.user.id != 12182: raise UserError("Pengajuan tempo hanya bisa di approve oleh sales manager") else: @@ -400,7 +400,7 @@ class UserPengajuanTempoRequest(models.Model): if tempo.env.user.id == 7: raise UserError("Pengajuan tempo harus di approve oleh Finence terlebih dahulu") else: - if tempo.env.user.id not in (688, 28, 12182): + if tempo.env.user.id not in (688, 28): # if tempo.env.user.id not in (288,28,12182): raise UserError("Pengajuan tempo hanya bisa di approve oleh Finence") else: -- cgit v1.2.3 From f4486625017ee5edae369a6be283863a30066b3b Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Wed, 18 Jun 2025 10:30:02 +0700 Subject: cashback 15% customer commision --- indoteknik_custom/models/commision.py | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py index 842e64bf..d3392a0c 100644 --- a/indoteknik_custom/models/commision.py +++ b/indoteknik_custom/models/commision.py @@ -177,6 +177,8 @@ class CustomerCommision(models.Model): ], string='Status') commision_percent = fields.Float(string='Commision %', tracking=3) commision_amt = fields.Float(string='Commision Amount', tracking=3) + cashback = fields.Float(string='Cashback', tracking=3) + total_commision = fields.Float(string='Total Commision', tracking=3) commision_amt_text = fields.Char(string='Commision Amount Text', compute='compute_delivery_amt_text') total_dpp = fields.Float(string='Total DPP', compute='_compute_total_dpp') commision_type = fields.Selection([ @@ -316,6 +318,9 @@ class CustomerCommision(models.Model): if self.commision_amt == 0: self.commision_amt = self.commision_percent * self.total_dpp // 100 + if self.commision_type == 'cashback': + self.cashback = self.commision_amt * 0.15 + self.total_commision = self.commision_amt * 0.85 @api.constrains('commision_amt') def _onchange_commision_amt(self): @@ -328,6 +333,12 @@ class CustomerCommision(models.Model): if self.total_dpp > 0 and self.commision_percent == 0: self.commision_percent = (self.commision_amt / self.total_dpp) * 100 + @api.constrains('commision_type') + def _onchange_commision_amt(self): + if self.commision_type == 'cashback' and self.commision_amt > 0: + self.cashback = self.commision_amt * 0.15 + self.total_commision = self.commision_amt * 0.85 + def _compute_total_dpp(self): for data in self: total_dpp = 0 -- cgit v1.2.3 From e2678c3729217dc3f6d3fe9053b53c06c6890fbb Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 18 Jun 2025 10:48:42 +0700 Subject: (andri) fix address shipping biteship --- 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 68beffbc..7dc33133 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -1071,7 +1071,7 @@ class SaleOrder(models.Model): message_lines.append("
") origin_address = "Jl. Bandengan Utara Komp A & BRT. Penjaringan, Kec. Penjaringan, Jakarta (BELAKANG INDOMARET) KOTA JAKARTA UTARA PENJARINGAN" - destination_address = shipping_address.alamat_lengkap_text or shipping_address.street or shipping_address.name or '' + destination_address = shipping_address.street or shipping_address.name or '' if use_coordinate: origin_suffix = f"(Koordinat: {origin_data.get('origin_latitude')}, {origin_data.get('origin_longitude')})" destination_suffix = f"(Koordinat: {destination_data.get('destination_latitude')}, {destination_data.get('destination_longitude')})" -- cgit v1.2.3 From 587547a2689b4d089302dfb50feb2f876f2633d0 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Wed, 18 Jun 2025 10:51:56 +0700 Subject: push --- indoteknik_custom/models/commision.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py index d3392a0c..03d32d2d 100644 --- a/indoteknik_custom/models/commision.py +++ b/indoteknik_custom/models/commision.py @@ -177,8 +177,9 @@ class CustomerCommision(models.Model): ], string='Status') commision_percent = fields.Float(string='Commision %', tracking=3) commision_amt = fields.Float(string='Commision Amount', tracking=3) - cashback = fields.Float(string='Cashback', tracking=3) - total_commision = fields.Float(string='Total Commision', tracking=3) + cashback = fields.Float(string='Cashback', compute="compute_cashback") + total_commision = fields.Float(string='Total Commision', compute="compute_cashback") + total_cashback = fields.Float(string='Total Cashback') commision_amt_text = fields.Char(string='Commision Amount Text', compute='compute_delivery_amt_text') total_dpp = fields.Float(string='Total DPP', compute='_compute_total_dpp') commision_type = fields.Selection([ @@ -318,9 +319,6 @@ class CustomerCommision(models.Model): if self.commision_amt == 0: self.commision_amt = self.commision_percent * self.total_dpp // 100 - if self.commision_type == 'cashback': - self.cashback = self.commision_amt * 0.15 - self.total_commision = self.commision_amt * 0.85 @api.constrains('commision_amt') def _onchange_commision_amt(self): @@ -333,11 +331,13 @@ class CustomerCommision(models.Model): if self.total_dpp > 0 and self.commision_percent == 0: self.commision_percent = (self.commision_amt / self.total_dpp) * 100 - @api.constrains('commision_type') - def _onchange_commision_amt(self): + def compute_cashback(self): if self.commision_type == 'cashback' and self.commision_amt > 0: self.cashback = self.commision_amt * 0.15 self.total_commision = self.commision_amt * 0.85 + else: + self.cashback = 0 + self.total_commision = 0 def _compute_total_dpp(self): for data in self: -- cgit v1.2.3 From 80c9bc5e4590710a6b69b7d4728d21b2930b6d9b Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 18 Jun 2025 10:58:36 +0700 Subject: (andri) fix validate custom shipping method --- 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 7dc33133..4535ae0d 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -486,7 +486,7 @@ class SaleOrder(models.Model): ('sale_order_id', '=', sale_order_id), ('provider', 'ilike', provider), ]) - if matched == 0: + if self.select_shipping_option == 'biteship' and matched == 0: self.carrier_id = self._origin.carrier_id self.shipping_option_id = self._origin.shipping_option_id or False return { -- cgit v1.2.3 From df8995b1bca8cc55622621e73d46069507f09540 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 18 Jun 2025 11:06:57 +0700 Subject: (andri) fix address pada log note destination estimate shipping biteship --- indoteknik_custom/models/sale_order.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 4535ae0d..ee9d6f70 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -1071,7 +1071,13 @@ class SaleOrder(models.Model): message_lines.append("
") origin_address = "Jl. Bandengan Utara Komp A & BRT. Penjaringan, Kec. Penjaringan, Jakarta (BELAKANG INDOMARET) KOTA JAKARTA UTARA PENJARINGAN" - destination_address = shipping_address.street or shipping_address.name or '' + destination_address = ', '.join(filter(None, [ + shipping_address.street, + shipping_address.kelurahan_id.name if shipping_address.kelurahan_id else None, + shipping_address.kecamatan_id.name if shipping_address.kecamatan_id else None, + shipping_address.kota_id.name if shipping_address.kota_id else None, + shipping_address.state_id.name if shipping_address.state_id else None + ])) if use_coordinate: origin_suffix = f"(Koordinat: {origin_data.get('origin_latitude')}, {origin_data.get('origin_longitude')})" destination_suffix = f"(Koordinat: {destination_data.get('destination_latitude')}, {destination_data.get('destination_longitude')})" -- cgit v1.2.3 From d1ff4bd35deac6c17a17e97f0904f67e113c5add Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 18 Jun 2025 13:32:05 +0700 Subject: revisi, fetch item from bu out in tukar guling line --- indoteknik_custom/models/tukar_guling.py | 141 +++++++++++++++++++++++++------ 1 file changed, 113 insertions(+), 28 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 7ed6e10f..a5724104 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -4,6 +4,7 @@ import logging _logger = logging.getLogger(__name__) + class TukarGuling(models.Model): _name = 'tukar.guling' _description = 'Tukar Guling' @@ -21,14 +22,13 @@ class TukarGuling(models.Model): ) name = fields.Char('Number', required=True, copy=False, readonly=True, default='New') date = fields.Datetime('Date', default=fields.Datetime.now, required=True) - out_num = fields.Many2one('stock.picking', 'Nomor BU/Out', - domain=[('picking_type_id.code', '=', 'outgoing')]) + operations = fields.Many2one('stock.picking', 'Operations', + domain=[('picking_type_id.code', '=', 'outgoing')], help='Nomor BU/Out atau BU/Pick') ba_num = fields.Text('Nomor BA') notes = fields.Text('Notes') return_type = fields.Selection(String='Return Type', selection=[ - ('tukar_guling', 'Tukar Guling'), # -> barang yang sama - ('revisi_so', 'Revisi SO'), # -> ganti barang ? - ('credit_memo', 'Credit Memo')]) # -> dijadiin credit memo + ('tukar_guling', 'Tukar Guling'), # -> barang yang sama + ('revisi_so', 'Revisi SO')]) state = fields.Selection(string='Status', selection=[ ('draft', 'Draft'), ('approval_sales', ' Approval Sales'), @@ -40,24 +40,108 @@ class TukarGuling(models.Model): line_ids = fields.One2many('tukar.guling.line', 'tukar_guling_id', string='Product Lines') - @api.constrains('return_type', 'out_num') + @api.onchange('operations') + def _onchange_operations(self): + """Auto-populate lines ketika operations dipilih""" + if self.operations: + # Clear existing lines + self.line_ids = [(5, 0, 0)] + + # Set origin dari operations + if self.operations.origin: + self.origin = self.operations.origin + + # Auto-populate lines dari move_ids operations + lines_data = [] + sequence = 10 + + # Untuk Odoo 14, gunakan move_ids_without_package atau move_lines + moves_to_check = [] + + # 1. move_ids_without_package (standard di Odoo 14) + if hasattr(self.operations, 'move_ids_without_package') and self.operations.move_ids_without_package: + moves_to_check = self.operations.move_ids_without_package + # 2. move_lines (backup untuk versi lama) + elif hasattr(self.operations, 'move_lines') and self.operations.move_lines: + moves_to_check = self.operations.move_lines + + # Debug logging + _logger = logging.getLogger(__name__) + _logger.info(f"BU/OUT: {self.operations.name}, State: {self.operations.state}") + _logger.info(f"Total moves found: {len(moves_to_check)}") + + for move in moves_to_check: + _logger.info( + f"Move: {move.name}, Product: {move.product_id.name if move.product_id else 'No Product'}, Qty: {move.product_uom_qty}, State: {move.state}") + + # Ambil semua move yang ada quantity + if move.product_id and move.product_uom_qty > 0: + lines_data.append((0, 0, { + 'sequence': sequence, + 'product_id': move.product_id.id, + 'product_uom_qty': move.product_uom_qty, + 'product_uom': move.product_uom.id, + 'name': move.name or move.product_id.display_name, + })) + sequence += 10 + + if lines_data: + self.line_ids = lines_data + _logger.info(f"Created {len(lines_data)} lines") + else: + _logger.info("No lines created - no valid moves found") + else: + # Clear lines jika operations dikosongkan + self.line_ids = [(5, 0, 0)] + self.origin = False + + def action_populate_lines(self): + """Manual button untuk populate lines - sebagai alternatif""" + self.ensure_one() + if not self.operations: + raise UserError("Pilih BU/OUT terlebih dahulu!") + + # Clear existing lines + self.line_ids = [(5, 0, 0)] + + lines_data = [] + sequence = 10 + + # Ambil semua stock moves dari operations + for move in self.operations.move_ids: + if move.product_uom_qty > 0: + lines_data.append((0, 0, { + 'sequence': sequence, + 'product_id': move.product_id.id, + 'product_uom_qty': move.product_uom_qty, + 'product_uom': move.product_uom.id, + 'name': move.name or move.product_id.display_name, + })) + sequence += 10 + + if lines_data: + self.line_ids = lines_data + else: + raise UserError("Tidak ditemukan barang di BU/OUT yang dipilih!") + + @api.constrains('return_type', 'operations') def _check_required_bu_fields(self): for record in self: - if record.return_type in ['revisi_so', 'credit_memo', 'tukar_guling'] and not record.out_num: + if record.return_type in ['revisi_so', 'tukar_guling'] and not record.operations: raise ValidationError("BU/Out harus diisi!") @api.constrains('line_ids', 'state') def _check_product_lines(self): """Constraint: Product lines harus ada jika state bukan draft""" for record in self: - if record.state in ('approval_sales', 'approval_logistic', 'approval_finance', 'done') and not record.line_ids: + if record.state in ('approval_sales', 'approval_logistic', 'approval_finance', + 'done') and not record.line_ids: raise ValidationError("Product lines harus diisi sebelum submit atau approve!") def _validate_product_lines(self): """Helper method untuk validasi product lines""" self.ensure_one() - # Check ada product lines if not self.line_ids: raise UserError("Belum ada product lines yang ditambahkan!") @@ -78,9 +162,9 @@ class TukarGuling(models.Model): def create(self, vals): if not vals.get('name') or vals['name'] == 'New': vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling') or 'New' - # Auto-fill origin from out_num - if not vals.get('origin') and vals.get('out_num'): - picking = self.env['stock.picking'].browse(vals['out_num']) + # Auto-fill origin from operations + if not vals.get('origin') and vals.get('operations'): + picking = self.env['stock.picking'].browse(vals['operations']) if picking.origin: vals['origin'] = picking.origin return super(TukarGuling, self).create(vals) @@ -109,8 +193,8 @@ class TukarGuling(models.Model): return new_record def write(self, vals): - if 'out_num' in vals and not vals.get('origin'): - picking = self.env['stock.picking'].browse(vals['out_num']) + if 'operations' in vals and not vals.get('origin'): + picking = self.env['stock.picking'].browse(vals['operations']) if picking.origin: vals['origin'] = picking.origin @@ -145,7 +229,7 @@ class TukarGuling(models.Model): def action_approve(self): self.ensure_one() - if not self.out_num: + if not self.operations: raise UserError("BU/Out harus diisi!") if not self.return_type: @@ -170,6 +254,7 @@ class TukarGuling(models.Model): rec._create_pickings() else: raise UserError("Status ini tidak bisa di-approve.") + def action_cancel(self): self.ensure_one() # if self.state == 'done': @@ -177,10 +262,10 @@ class TukarGuling(models.Model): self.state = 'cancel' def _create_pickings(self): - if not self.out_num: + if not self.operations: raise UserError("BU/Out harus diisi terlebih dahulu.") - origin_so = self.out_num.origin + origin_so = self.operations.origin if not origin_so: raise UserError("BU/OUT tidak memiliki origin (SO), tidak bisa cari BU/PICK.") @@ -206,15 +291,15 @@ class TukarGuling(models.Model): # Lokasi location_dest_id = srt_type.default_location_dest_id.id location_dest_id_ort = ort_type.default_location_dest_id.id - location_customer = self.out_num.location_dest_id + location_customer = self.operations.location_dest_id - # 1. BU/SRT: retur dari out_num + # 1. BU/SRT: retur dari operations srt_picking = Picking.create({ - 'partner_id': self.out_num.partner_id.id, + 'partner_id': self.operations.partner_id.id, 'picking_type_id': srt_type.id, 'location_id': location_customer.id, 'location_dest_id': location_dest_id, - 'origin': f"Retur {self.out_num.name}", + 'origin': f"Retur {self.operations.name}", 'tukar_guling_id': self.id, 'group_id': group_id, 'move_ids_without_package': [ @@ -232,7 +317,7 @@ class TukarGuling(models.Model): srt_picking.action_confirm() # 2. Cari BU/PICK dari SO yang sama - origin_so = self.out_num.origin + origin_so = self.operations.origin if not origin_so: raise UserError("BU/OUT tidak memiliki origin (SO), tidak bisa cari BU/PICK.") @@ -246,7 +331,7 @@ class TukarGuling(models.Model): # 3. BU/ORT: retur dari BU/PICK ort_picking = Picking.create({ - 'partner_id': self.out_num.partner_id.id, + 'partner_id': self.operations.partner_id.id, 'picking_type_id': ort_type.id, 'location_id': location_dest_id, 'location_dest_id': location_dest_id_ort, @@ -266,7 +351,7 @@ class TukarGuling(models.Model): ] }) ort_picking.action_confirm() - + ort_picking.action_assign() class TukarGulingPO(models.Model): _name = 'tukar.guling.po' @@ -274,7 +359,7 @@ class TukarGulingPO(models.Model): name = fields.Char('Number', required=True, copy=False, readonly=True, default='New') date = fields.Datetime('Date', default=fields.Datetime.now, required=True) - out_num = fields.Many2one('stock.picking', 'Nomor BU/Out', + operations = fields.Many2one('stock.picking', 'Nomor BU/Out', domain=[('picking_type_id.code', '=', 'outgoing')]) ba_num = fields.Text('Nomor BA') notes = fields.Text('Notes') @@ -296,10 +381,10 @@ class TukarGulingPO(models.Model): ('debit_memo', 'Debit Memo'), ], string='Return Type', required=True) - @api.constrains('return_type', 'out_num') + @api.constrains('return_type', 'operations') def _check_required_bu_fields(self): for record in self: - if record.return_type in ['tukar_guling', 'revisi_po', 'debit_memo'] and not record.out_num: + if record.return_type in ['tukar_guling', 'revisi_po', 'debit_memo'] and not record.operations: raise ValidationError("BU/Out harus diisi!") @api.constrains('line_ids', 'state') @@ -369,7 +454,7 @@ class TukarGulingPO(models.Model): def action_approve(self): self.ensure_one() - if not self.out_num: + if not self.operations: raise UserError("BU/Out harus diisi!") if not self.return_type: -- cgit v1.2.3 From 4fc9dd9424b4b5665872a0386389278efada75f9 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 18 Jun 2025 15:24:23 +0700 Subject: (andri) fix date --- indoteknik_custom/models/account_move.py | 40 ++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 54eaabcf..66020a69 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -87,6 +87,17 @@ class AccountMove(models.Model): # result.append((move.id, move.display_name)) # return result + @api.onchange('invoice_date') + def _onchange_invoice_date(self): + if self.invoice_date: + self.date = self.invoice_date + + @api.onchange('date') + def _onchange_date(self): + if self.date: + self.invoice_date = self.date + + def compute_length_of_payment(self): for rec in self: payment_term = rec.invoice_payment_term_id.line_ids[0].days @@ -145,13 +156,38 @@ class AccountMove(models.Model): } template.send_mail(record.id, email_values=email_values, force_send=True) + # @api.model + # def create(self, vals): + # vals['nomor_kwitansi'] = self.env['ir.sequence'].next_by_code('nomor.kwitansi') or '0' + # result = super(AccountMove, self).create(vals) + # # result._update_line_name_from_ref() + # return result + @api.model def create(self, vals): - vals['nomor_kwitansi'] = self.env['ir.sequence'].next_by_code('nomor.kwitansi') or '0' + vals['nomor_kwitansi'] = self.env['ir.sequence'].next_by_code('nomor.kwitansi') or '0' result = super(AccountMove, self).create(vals) - # result._update_line_name_from_ref() + + # Tambahan: jika ini Vendor Bill dan tanggal belum diisi + if result.move_type == 'in_invoice' and not vals.get('invoice_date') and not vals.get('date'): + po = result.purchase_order_id + if po: + # Cari receipt dari PO + picking = self.env['stock.picking'].search([ + ('purchase_id', '=', po.id), + ('picking_type_code', '=', 'incoming'), + ('state', '=', 'done'), + ('date_done', '!=', False), + ], order='date_done desc', limit=1) + + if picking: + receipt_date = picking.date_done + result.invoice_date = receipt_date + result.date = receipt_date + return result + def compute_so_shipping_paid_by(self): for record in self: record.so_shipping_paid_by = record.sale_id.shipping_paid_by -- cgit v1.2.3 From 386d64204b1e75d7aaa28cf1c7413b69938ee397 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 18 Jun 2025 16:01:47 +0700 Subject: (andri) date bill & accounting menyesuaikan effective date BU PUT terkait --- indoteknik_custom/models/purchase_order.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 004a1fa4..505df735 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -452,6 +452,18 @@ class PurchaseOrder(models.Model): 'company_id': self.company_id.id, 'payment_schedule': payment_schedule } + + receipt = self.env['stock.picking'].search([ + ('purchase_id', '=', self.id), + ('state', '=', 'done'), + ('picking_type_code', '=', 'incoming'), + ('date_done', '!=', False) + ], order='date_done desc', limit=1) + + if receipt: + invoice_vals['invoice_date'] = receipt.date_done + invoice_vals['date'] = receipt.date_done + return invoice_vals def _compute_matches_so(self): -- cgit v1.2.3 From 594b0a4ac57480ac750b22a8361afb1045e7ad44 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 18 Jun 2025 16:13:57 +0700 Subject: cannot edit line product only delete --- indoteknik_custom/models/tukar_guling.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index a5724104..186cff97 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -296,6 +296,7 @@ class TukarGuling(models.Model): # 1. BU/SRT: retur dari operations srt_picking = Picking.create({ 'partner_id': self.operations.partner_id.id, + 'real_shipping_id': self.operations.real_shipping_id.id, 'picking_type_id': srt_type.id, 'location_id': location_customer.id, 'location_dest_id': location_dest_id, @@ -332,6 +333,7 @@ class TukarGuling(models.Model): # 3. BU/ORT: retur dari BU/PICK ort_picking = Picking.create({ 'partner_id': self.operations.partner_id.id, + 'real_shipping_id': self.operations.real_shipping_id.id, 'picking_type_id': ort_type.id, 'location_id': location_dest_id, 'location_dest_id': location_dest_id_ort, -- cgit v1.2.3 From 89a53c90bfeb735f3f256dfadfd24c5c5a5c6328 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 19 Jun 2025 09:24:39 +0700 Subject: (andri) penambahan ET product leadtime berdasarkan SLA vendor tanpa logistik & hari libur --- indoteknik_custom/models/sale_order.py | 41 ++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index ee9d6f70..7607d4ca 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -349,6 +349,47 @@ class SaleOrder(models.Model): date_unhold = fields.Datetime(string='Date Unhold', tracking=True, readonly=True, help='Waktu ketika SO di Unhold' ) + et_products = fields.Datetime(string='ET Products (SLA Vendor)', compute='_compute_et_products', help="Leadtime produk berdasarkan SLA vendor, tanpa logistik.") + + eta_date_reserved = fields.Datetime( + string="Date Reserved", + compute="_compute_eta_date_reserved", + help="Tanggal DO dengan prefix BU/PICK/ yang sudah 'assigned' dan memiliki reserved date." + ) + + @api.depends('order_line.product_id', 'date_order') + def _compute_et_products(self): + jakarta = pytz.timezone("Asia/Jakarta") + for order in self: + if not order.order_line or not order.date_order: + order.et_products = False + continue + + # Ambil tanggal order sebagai basis + base_date = order.date_order + if base_date.tzinfo is None: + base_date = jakarta.localize(base_date) + else: + base_date = base_date.astimezone(jakarta) + + # Ambil nilai SLA vendor dalam hari + sla_data = order.calculate_sla_by_vendor(order.order_line) + sla_days = sla_data.get('slatime', 1) + + # Hitung ETA produk (tanpa logistik) + eta_datetime = base_date + timedelta(days=sla_days) + + # Simpan ke field sebagai UTC-naive datetime (standar Odoo) + order.et_products = eta_datetime.astimezone(pytz.utc).replace(tzinfo=None) + + @api.depends('picking_ids.state', 'picking_ids.date_reserved') + def _compute_eta_date_reserved(self): + for order in self: + pickings = order.picking_ids.filtered( + lambda p: p.state == 'assigned' and p.date_reserved and 'BU/PICK/' in (p.name or '') + ) + order.eta_date_reserved = min(pickings.mapped('date_reserved')) if pickings else False + @api.onchange('shipping_cost_covered') def _onchange_shipping_cost_covered(self): if self.shipping_cost_covered == 'indoteknik' and self.select_shipping_option == 'biteship': -- cgit v1.2.3 From 130862f48ffebfcd30936b59628090f18e3b3a2a Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 19 Jun 2025 09:25:35 +0700 Subject: (andri) rev penamaan field --- 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 7607d4ca..4f8536ca 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -349,7 +349,7 @@ class SaleOrder(models.Model): date_unhold = fields.Datetime(string='Date Unhold', tracking=True, readonly=True, help='Waktu ketika SO di Unhold' ) - et_products = fields.Datetime(string='ET Products (SLA Vendor)', compute='_compute_et_products', help="Leadtime produk berdasarkan SLA vendor, tanpa logistik.") + et_products = fields.Datetime(string='ET Products', compute='_compute_et_products', help="Leadtime produk berdasarkan SLA vendor, tanpa logistik.") eta_date_reserved = fields.Datetime( string="Date Reserved", -- cgit v1.2.3 From c4640e2f846d853412c5f1cb0901e50208fa0216 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 19 Jun 2025 09:33:25 +0700 Subject: (andri) ganti keterangan eta date reserved & penyesuaian field ETA --- 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 4f8536ca..6da46398 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -354,7 +354,7 @@ class SaleOrder(models.Model): eta_date_reserved = fields.Datetime( string="Date Reserved", compute="_compute_eta_date_reserved", - help="Tanggal DO dengan prefix BU/PICK/ yang sudah 'assigned' dan memiliki reserved date." + help="Tanggal pertama kali barang berhasil di-reservasi pada DO (BU/PICK/) yang berstatus Siap Dikirim." ) @api.depends('order_line.product_id', 'date_order') -- cgit v1.2.3 From 1542b2373ef4cff98ded7c9bbf426e18b5524162 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 19 Jun 2025 10:33:02 +0700 Subject: push --- indoteknik_custom/models/stock_picking_return.py | 432 +++++++++++++++++++++-- indoteknik_custom/models/tukar_guling.py | 148 -------- 2 files changed, 398 insertions(+), 182 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking_return.py b/indoteknik_custom/models/stock_picking_return.py index a683d80e..341383e8 100644 --- a/indoteknik_custom/models/stock_picking_return.py +++ b/indoteknik_custom/models/stock_picking_return.py @@ -1,38 +1,402 @@ -from odoo import _, api, fields, models -from odoo.exceptions import UserError -from odoo.tools.float_utils import float_round +from odoo import models, fields, api +from odoo.exceptions import UserError, ValidationError +import logging +_logger = logging.getLogger(__name__) -class ReturnPicking(models.TransientModel): - _inherit = 'stock.return.picking' + +class TukarGuling(models.Model): + _name = 'tukar.guling' + _description = 'Tukar Guling' + _order = 'date desc, id desc' + _rec_name = 'name' + + origin = fields.Char(string='Origin SO') + real_shipping_id = fields.Many2one('res.partner', string='Shipping Address') + picking_ids = fields.One2many('stock.picking', 'tukar_guling_id', string='Transfers') + + name = fields.Char('Number', required=True, copy=False, readonly=True, default='New') + date = fields.Datetime('Date', default=fields.Datetime.now, required=True) + operations = fields.Many2one('stock.picking', 'Operations', + domain=[('picking_type_id.code', '=', 'outgoing')], + help='Nomor BU/Out atau BU/Pick') + ba_num = fields.Text('Nomor BA') + notes = fields.Text('Notes') + return_type = fields.Selection(String='Return Type', selection=[ + ('tukar_guling', 'Tukar Guling'), + ('revisi_so', 'Revisi SO')]) + + state = fields.Selection(string='Status', selection=[ + ('draft', 'Draft'), + ('approval_sales', 'Approval Sales'), + ('approval_logistic', 'Approval Logistic'), + ('approval_finance', 'Approval Finance'), + ('done', 'Done'), + ('cancel', 'Canceled') + ], default='draft', tracking=True, required=True) + + line_ids = fields.One2many('tukar.guling.line', 'tukar_guling_id', string='Product Lines') + + @api.onchange('operations') + def _onchange_operations(self): + """Auto-populate lines ketika operations dipilih""" + if self.operations: + # Clear existing lines + self.line_ids = [(5, 0, 0)] + + # Set origin dari operations + if self.operations.origin: + self.origin = self.operations.origin + + # Set shipping address + if self.operations.real_shipping_id: + self.real_shipping_id = self.operations.real_shipping_id.id + + # Auto-populate lines dari move_ids operations + lines_data = [] + sequence = 10 + + # Ambil moves yang sudah done/delivered + moves_to_check = self.operations.move_ids_without_package.filtered( + lambda m: m.state == 'done' and m.quantity_done > 0 + ) + + _logger.info(f"BU/OUT: {self.operations.name}, State: {self.operations.state}") + _logger.info(f"Total moves found: {len(moves_to_check)}") + + for move in moves_to_check: + _logger.info( + f"Move: {move.name}, Product: {move.product_id.name if move.product_id else 'No Product'}, " + f"Qty Done: {move.quantity_done}, State: {move.state}" + ) + + # Hanya ambil yang sudah done dengan quantity_done > 0 + if move.product_id and move.quantity_done > 0: + lines_data.append((0, 0, { + 'sequence': sequence, + 'product_id': move.product_id.id, + 'product_uom_qty': move.quantity_done, # Gunakan quantity_done + 'product_uom': move.product_uom.id, + 'name': move.name or move.product_id.display_name, + })) + sequence += 10 + + if lines_data: + self.line_ids = lines_data + _logger.info(f"Created {len(lines_data)} lines") + else: + _logger.info("No lines created - no valid moves found") + else: + # Clear lines jika operations dikosongkan + self.line_ids = [(5, 0, 0)] + self.origin = False + self.real_shipping_id = False + + def _create_pickings(self): + """Improved picking creation with proper move handling""" + if not self.operations: + raise UserError("BU/Out harus diisi terlebih dahulu.") + + origin_so = self.operations.origin + if not origin_so: + raise UserError("BU/OUT tidak memiliki origin (SO), tidak bisa cari BU/PICK.") + + # Cari BU/PICK dari SO yang sama + pick_picking = self.env['stock.picking'].search([ + ('origin', '=', origin_so), + ('picking_type_id.code', '=', 'internal') + ], limit=1) + + if not pick_picking: + raise UserError(f"BU/PICK dengan origin {origin_so} tidak ditemukan.") + + # Ambil group_id dari operations + group_id = self.operations.group_id.id if self.operations.group_id else False + + Picking = self.env['stock.picking'] + StockMove = self.env['stock.move'] + + # Cari picking types + srt_type = self.env['stock.picking.type'].search([ + ('sequence_code', '=', 'SRT') + ], limit=1) + + ort_type = self.env['stock.picking.type'].search([ + ('sequence_code', '=', 'ORT') + ], limit=1) + + if not srt_type or not ort_type: + raise UserError("Picking type SRT atau ORT tidak ditemukan!") + + # Lokasi + location_dest_id = srt_type.default_location_dest_id.id + location_dest_id_ort = ort_type.default_location_dest_id.id + location_customer = self.operations.location_dest_id + + # 1. Create BU/SRT: return dari customer ke gudang + srt_picking = Picking.create({ + 'partner_id': self.operations.partner_id.id, + 'real_shipping_id': self.operations.real_shipping_id.id, + 'picking_type_id': srt_type.id, + 'location_id': location_customer.id, + 'location_dest_id': location_dest_id, + 'origin': f"Retur {self.operations.name}", + 'tukar_guling_id': self.id, + 'group_id': group_id, + }) + + # Create moves untuk SRT + srt_moves = [] + for line in self.line_ids: + move_vals = { + 'name': line.name or line.product_id.name, + 'product_id': line.product_id.id, + 'product_uom_qty': line.product_uom_qty, + 'product_uom': line.product_uom.id, + 'location_id': location_customer.id, + 'location_dest_id': location_dest_id, + 'picking_id': srt_picking.id, + 'group_id': group_id, + 'state': 'draft', + } + move = StockMove.create(move_vals) + srt_moves.append(move) + + # Confirm SRT picking + srt_picking.action_confirm() + + # 2. Create BU/ORT: return dari gudang ke supplier/vendor + ort_picking = Picking.create({ + 'partner_id': self.operations.partner_id.id, + 'real_shipping_id': self.operations.real_shipping_id.id, + 'picking_type_id': ort_type.id, + 'location_id': location_dest_id, + 'location_dest_id': location_dest_id_ort, + 'origin': f"Retur {pick_picking.name}", + 'tukar_guling_id': self.id, + 'group_id': group_id, + }) + + # Create moves untuk ORT + ort_moves = [] + for line in self.line_ids: + move_vals = { + 'name': line.name or line.product_id.name, + 'product_id': line.product_id.id, + 'product_uom_qty': line.product_uom_qty, + 'product_uom': line.product_uom.id, + 'location_id': location_dest_id, + 'location_dest_id': location_dest_id_ort, + 'picking_id': ort_picking.id, + 'group_id': group_id, + 'state': 'draft', + } + move = StockMove.create(move_vals) + ort_moves.append(move) + + # Confirm ORT picking + ort_picking.action_confirm() + ort_picking.action_assign() + + # Log creation + _logger.info(f"Created SRT picking: {srt_picking.name} with {len(srt_moves)} moves") + _logger.info(f"Created ORT picking: {ort_picking.name} with {len(ort_moves)} moves") + + return { + 'srt_picking': srt_picking, + 'ort_picking': ort_picking + } + + def action_approve(self): + self.ensure_one() + + if not self.operations: + raise UserError("BU/Out harus diisi!") + + if not self.return_type: + raise UserError("Return Type harus diisi!") + + # Validasi product lines + self._validate_product_lines() + + # Cek hak akses berdasarkan state + if self.state == 'approval_sales': + if not self.env.user.has_group('indoteknik_custom.group_role_sales'): + raise UserError("Hanya Sales Manager yang boleh approve tahap ini.") + self.state = 'approval_logistic' + + elif self.state == 'approval_logistic': + if not self.env.user.has_group('indoteknik_custom.group_role_logistic'): + raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.") + self.state = 'approval_finance' + + elif self.state == 'approval_finance': + if not self.env.user.has_group('indoteknik_custom.group_role_fat'): + raise UserError("Hanya Finance Manager yang boleh approve tahap ini.") + self.state = 'done' + # Create pickings saat final approval + result = self._create_pickings() + if result: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Success', + 'message': f"Berhasil membuat BU/SRT: {result['srt_picking'].name} dan BU/ORT: {result['ort_picking'].name}", + 'type': 'success', + 'sticky': False, + } + } + else: + raise UserError("Status ini tidak bisa di-approve.") + + # ... (rest of the methods remain the same) + @api.constrains('return_type', 'operations') + def _check_required_bu_fields(self): + for record in self: + if record.return_type in ['revisi_so', 'tukar_guling'] and not record.operations: + raise ValidationError("BU/Out harus diisi!") + + @api.constrains('line_ids', 'state') + def _check_product_lines(self): + """Constraint: Product lines harus ada jika state bukan draft""" + for record in self: + if record.state in ('approval_sales', 'approval_logistic', 'approval_finance', + 'done') and not record.line_ids: + raise ValidationError("Product lines harus diisi sebelum submit atau approve!") + + def _validate_product_lines(self): + """Helper method untuk validasi product lines""" + self.ensure_one() + + # Check ada product lines + if not self.line_ids: + raise UserError("Belum ada product lines yang ditambahkan!") + + # Check product sudah diisi + empty_lines = self.line_ids.filtered(lambda line: not line.product_id) + if empty_lines: + raise UserError("Ada product lines yang belum diisi productnya!") + + # Check quantity > 0 + zero_qty_lines = self.line_ids.filtered(lambda line: line.product_uom_qty <= 0) + if zero_qty_lines: + raise UserError("Quantity product tidak boleh kosong atau 0!") + + return True @api.model - def default_get(self, fields): - res = super(ReturnPicking, self).default_get(fields) - - stock_picking = self.env['stock.picking'].search([ - ('id', '=', res['picking_id']), - ]) - - # sale_id = stock_picking.group_id.sale_id - if not stock_picking.approval_return_status == 'approved': - raise UserError('Harus Approval Accounting AR untuk melakukan Retur') - - # purchase = self.env['purchase.order'].search([ - # ('name', '=', stock_picking.group_id.name), - # ]) - # if not stock_picking.approval_return_status == 'approved' and purchase.invoice_ids: - # raise UserError('Harus Approval Accounting AP untuk melakukan Retur') - - return res - -class ReturnPickingLine(models.TransientModel): - _inherit = 'stock.return.picking.line' - - @api.onchange('quantity') - def _onchange_quantity(self): - for rec in self: - qty_done = rec.move_id.quantity_done - - if rec.quantity > qty_done: - raise UserError(f"Quantity yang Anda masukkan tidak boleh melebihi quantity done yaitu: {qty_done}") \ No newline at end of file + def create(self, vals): + if not vals.get('name') or vals['name'] == 'New': + vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling') or 'New' + # Auto-fill origin from operations + if not vals.get('origin') and vals.get('operations'): + picking = self.env['stock.picking'].browse(vals['operations']) + if picking.origin: + vals['origin'] = picking.origin + return super(TukarGuling, self).create(vals) + + def action_submit(self): + self.ensure_one() + if self.state != 'draft': + raise UserError("Submit hanya bisa dilakukan dari Draft.") + + # Validasi sebelum submit + self._validate_product_lines() + + self.state = 'approval_sales' + + def action_cancel(self): + self.ensure_one() + # Cek apakah ada picking yang sudah dibuat + if self.picking_ids: + done_pickings = self.picking_ids.filtered(lambda p: p.state == 'done') + if done_pickings: + raise UserError("Tidak bisa cancel karena ada transfer yang sudah selesai!") + + self.state = 'cancel' + + def action_view_picking(self): + self.ensure_one() + action = self.env.ref('stock.action_picking_tree_all').read()[0] + pickings = self.picking_ids + if len(pickings) > 1: + action['domain'] = [('id', 'in', pickings.ids)] + elif pickings: + action['views'] = [(self.env.ref('stock.view_picking_form').id, 'form')] + action['res_id'] = pickings.id + else: + raise UserError("Belum ada transfer yang dibuat!") + return action + + +class TukarGulingLine(models.Model): + _name = 'tukar.guling.line' + _description = 'Tukar Guling Line' + _order = 'sequence, id' + + sequence = fields.Integer('Sequence', default=10, copy=False) + tukar_guling_id = fields.Many2one('tukar.guling', string='Tukar Guling', required=True, ondelete='cascade') + product_id = fields.Many2one('product.product', string='Product', required=True) + product_uom_qty = fields.Float('Quantity', digits='Product Unit of Measure', required=True, default=1.0) + product_uom = fields.Many2one('uom.uom', string='Unit of Measure') + name = fields.Text('Description') + + @api.model_create_multi + def create(self, vals_list): + """Override create to auto-assign sequence""" + for vals in vals_list: + if 'sequence' not in vals or vals.get('sequence', 0) <= 0: + # Get max sequence untuk tukar_guling yang sama + tukar_guling_id = vals.get('tukar_guling_id') + if tukar_guling_id: + max_seq = self.search([ + ('tukar_guling_id', '=', tukar_guling_id) + ], order='sequence desc', limit=1) + vals['sequence'] = (max_seq.sequence or 0) + 10 + else: + vals['sequence'] = 10 + return super(TukarGulingLine, self).create(vals_list) + + @api.onchange('product_id') + def _onchange_product_id(self): + if self.product_id: + self.name = self.product_id.display_name + self.product_uom = self.product_id.uom_id + + +class StockPicking(models.Model): + _inherit = 'stock.picking' + + tukar_guling_id = fields.Many2one('tukar.guling', string='Tukar Guling Reference') + + def action_create_tukar_guling(self): + """Action untuk membuat Tukar Guling dari picking""" + self.ensure_one() + + # Cek apakah picking sudah done + if self.state != 'done': + raise UserError("Hanya bisa membuat Tukar Guling dari delivery yang sudah selesai!") + + # Cek apakah sudah ada tukar guling untuk picking ini + existing_tukar_guling = self.env['tukar.guling'].search([ + ('operations', '=', self.id) + ]) + + if existing_tukar_guling: + raise UserError(f"Sudah ada Tukar Guling untuk delivery ini: {existing_tukar_guling.name}") + + # Create tukar guling baru + tukar_guling = self.env['tukar.guling'].create({ + 'operations': self.id, + 'return_type': 'tukar_guling', # default value + }) + + return { + 'type': 'ir.actions.act_window', + 'name': 'Tukar Guling', + 'res_model': 'tukar.guling', + 'res_id': tukar_guling.id, + 'view_mode': 'form', + 'target': 'current', + } \ No newline at end of file diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 186cff97..08b862a7 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -355,137 +355,6 @@ class TukarGuling(models.Model): ort_picking.action_confirm() ort_picking.action_assign() -class TukarGulingPO(models.Model): - _name = 'tukar.guling.po' - _description = 'Tukar Guling PO' - - name = fields.Char('Number', required=True, copy=False, readonly=True, default='New') - date = fields.Datetime('Date', default=fields.Datetime.now, required=True) - operations = fields.Many2one('stock.picking', 'Nomor BU/Out', - domain=[('picking_type_id.code', '=', 'outgoing')]) - ba_num = fields.Text('Nomor BA') - notes = fields.Text('Notes') - state = fields.Selection(string='Status', selection=[ - ('draft', 'Draft'), - ('approval_purchase', ' Approval Purchase'), - ('approval_logistic', 'Approval Logistic'), - ('approval_finance', 'Approval Finance'), - ('done', 'Done'), - ('cancel', 'Canceled') - ], default='draft', tracking=True, required=True) - - line_ids = fields.One2many('tukar.guling.line.po', 'tukar_guling_po_id', string='Product Lines') - tukar_guling_po_id = fields.Many2one('tukar.guling.po', 'Tukar Guling PO') - - return_type = fields.Selection([ - ('tukar_guling', 'Tukar Guling'), - ('revisi_po', 'Revisi PO'), - ('debit_memo', 'Debit Memo'), - ], string='Return Type', required=True) - - @api.constrains('return_type', 'operations') - def _check_required_bu_fields(self): - for record in self: - if record.return_type in ['tukar_guling', 'revisi_po', 'debit_memo'] and not record.operations: - raise ValidationError("BU/Out harus diisi!") - - @api.constrains('line_ids', 'state') - def _check_product_lines(self): - """Constraint: Product lines harus ada jika state bukan draft""" - for record in self: - if record.state in ('approval_purchase', 'approval_logistic', 'approval_finance', 'done') and not record.line_ids: - raise ValidationError("Product lines harus diisi sebelum submit atau approve!") - - def _validate_product_lines(self): - """Helper method untuk validasi product lines""" - self.ensure_one() - - # Check ada product lines - if not self.line_ids: - raise UserError("Belum ada product lines yang ditambahkan!") - - # Check product sudah diisi - empty_lines = self.line_ids.filtered(lambda line: not line.product_id) - if empty_lines: - raise UserError("Ada product lines yang belum diisi productnya!") - - # Check quantity > 0 - zero_qty_lines = self.line_ids.filtered(lambda line: line.product_uom_qty <= 0) - if zero_qty_lines: - raise UserError("Quantity product tidak boleh kosong atau 0!") - return True - - @api.model - def create(self, vals): - if not vals.get('name') or vals['name'] in ('New', False): - vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling.po') or 'New' - return super(TukarGulingPO, self).create(vals) - def copy(self, default=None): - if default is None: - default = {} - - # Generate sequence satu-satunya di sini - default['name'] = self.env['ir.sequence'].next_by_code('tukar.guling.po') or 'New' - default['state'] = 'draft' - default['date'] = fields.Datetime.now() - - new_record = super(TukarGulingPO, self).copy(default) - - # Re-sequence lines - if new_record.line_ids: - for i, line in enumerate(new_record.line_ids): - line.sequence = (i + 1) * 10 - - return new_record - - 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_submit(self): - self.ensure_one() - - if self.state != 'draft': - raise UserError("Submit hanya bisa dilakukan dari Draft.") - self.state = 'approval_purchase' - - def action_approve(self): - self.ensure_one() - - if not self.operations: - raise UserError("BU/Out harus diisi!") - - if not self.return_type: - raise UserError("Return Type harus diisi!") - - if self.state == 'approval_purchase': - if not self.env.user.has_group('indoteknik_custom.group_role_purchasing'): - raise UserError("Hanya Purchasing yang boleh approve tahap ini.") - self.state = 'approval_logistic' - - elif self.state == 'approval_logistic': - if not self.env.user.has_group('indoteknik_custom.group_role_logistic'): - raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.") - self.state = 'approval_finance' - - elif self.state == 'approval_finance': - if not self.env.user.has_group('indoteknik_custom.group_role_fat'): - raise UserError("Hanya Finance Manager yang boleh approve tahap ini.") - self.state = 'done' - - else: - raise UserError("Status ini tidak bisa di-approve.") - - def action_cancel(self): - self.ensure_one() - # if self.state == 'done': - # raise UserError("Tidak bisa cancel jika sudah done") - self.state = 'cancel' - class TukarGulingLine(models.Model): _name = 'tukar.guling.line' _description = 'Tukar Guling Line' @@ -520,23 +389,6 @@ class TukarGulingLine(models.Model): self.name = self.product_id.display_name self.product_uom = self.product_id.uom_id -class TukarGulingLinePO(models.Model): - _name = 'tukar.guling.line.po' - _description = 'Tukar Guling Line (PO)' - _order = 'sequence, id' - - tukar_guling_po_id = fields.Many2one('tukar.guling.po', string='Tukar Guling PO', required=True, ondelete='cascade') - sequence = fields.Integer('Sequence', default=10, copy=False) - product_id = fields.Many2one('product.product', string='Product', required=True) - product_uom_qty = fields.Float('Quantity', digits='Product Unit of Measure', required=True, default=1.0) - product_uom = fields.Many2one('uom.uom', string='Unit of Measure') - name = fields.Text('Description') - - @api.onchange('product_id') - def _onchange_product_id(self): - if self.product_id: - self.name = self.product_id.display_name - self.product_uom = self.product_id.uom_id class StockPicking(models.Model): _inherit = 'stock.picking' -- cgit v1.2.3 From 32724232b991afaff527cf5ff9e58a2cad7ea824 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 19 Jun 2025 13:05:31 +0700 Subject: Fix sequence --- indoteknik_custom/models/stock_picking_return.py | 436 +++-------------------- indoteknik_custom/models/tukar_guling.py | 37 +- 2 files changed, 75 insertions(+), 398 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking_return.py b/indoteknik_custom/models/stock_picking_return.py index 341383e8..e50446d3 100644 --- a/indoteknik_custom/models/stock_picking_return.py +++ b/indoteknik_custom/models/stock_picking_return.py @@ -1,402 +1,54 @@ -from odoo import models, fields, api -from odoo.exceptions import UserError, ValidationError -import logging - -_logger = logging.getLogger(__name__) - - -class TukarGuling(models.Model): - _name = 'tukar.guling' - _description = 'Tukar Guling' - _order = 'date desc, id desc' - _rec_name = 'name' - - origin = fields.Char(string='Origin SO') - real_shipping_id = fields.Many2one('res.partner', string='Shipping Address') - picking_ids = fields.One2many('stock.picking', 'tukar_guling_id', string='Transfers') - - name = fields.Char('Number', required=True, copy=False, readonly=True, default='New') - date = fields.Datetime('Date', default=fields.Datetime.now, required=True) - operations = fields.Many2one('stock.picking', 'Operations', - domain=[('picking_type_id.code', '=', 'outgoing')], - help='Nomor BU/Out atau BU/Pick') - ba_num = fields.Text('Nomor BA') - notes = fields.Text('Notes') - return_type = fields.Selection(String='Return Type', selection=[ - ('tukar_guling', 'Tukar Guling'), - ('revisi_so', 'Revisi SO')]) - - state = fields.Selection(string='Status', selection=[ - ('draft', 'Draft'), - ('approval_sales', 'Approval Sales'), - ('approval_logistic', 'Approval Logistic'), - ('approval_finance', 'Approval Finance'), - ('done', 'Done'), - ('cancel', 'Canceled') - ], default='draft', tracking=True, required=True) - - line_ids = fields.One2many('tukar.guling.line', 'tukar_guling_id', string='Product Lines') - - @api.onchange('operations') - def _onchange_operations(self): - """Auto-populate lines ketika operations dipilih""" - if self.operations: - # Clear existing lines - self.line_ids = [(5, 0, 0)] - - # Set origin dari operations - if self.operations.origin: - self.origin = self.operations.origin - - # Set shipping address - if self.operations.real_shipping_id: - self.real_shipping_id = self.operations.real_shipping_id.id - - # Auto-populate lines dari move_ids operations - lines_data = [] - sequence = 10 - - # Ambil moves yang sudah done/delivered - moves_to_check = self.operations.move_ids_without_package.filtered( - lambda m: m.state == 'done' and m.quantity_done > 0 - ) - - _logger.info(f"BU/OUT: {self.operations.name}, State: {self.operations.state}") - _logger.info(f"Total moves found: {len(moves_to_check)}") - - for move in moves_to_check: - _logger.info( - f"Move: {move.name}, Product: {move.product_id.name if move.product_id else 'No Product'}, " - f"Qty Done: {move.quantity_done}, State: {move.state}" - ) - - # Hanya ambil yang sudah done dengan quantity_done > 0 - if move.product_id and move.quantity_done > 0: - lines_data.append((0, 0, { - 'sequence': sequence, - 'product_id': move.product_id.id, - 'product_uom_qty': move.quantity_done, # Gunakan quantity_done - 'product_uom': move.product_uom.id, - 'name': move.name or move.product_id.display_name, - })) - sequence += 10 - - if lines_data: - self.line_ids = lines_data - _logger.info(f"Created {len(lines_data)} lines") - else: - _logger.info("No lines created - no valid moves found") - else: - # Clear lines jika operations dikosongkan - self.line_ids = [(5, 0, 0)] - self.origin = False - self.real_shipping_id = False - - def _create_pickings(self): - """Improved picking creation with proper move handling""" - if not self.operations: - raise UserError("BU/Out harus diisi terlebih dahulu.") - - origin_so = self.operations.origin - if not origin_so: - raise UserError("BU/OUT tidak memiliki origin (SO), tidak bisa cari BU/PICK.") - - # Cari BU/PICK dari SO yang sama - pick_picking = self.env['stock.picking'].search([ - ('origin', '=', origin_so), - ('picking_type_id.code', '=', 'internal') - ], limit=1) - - if not pick_picking: - raise UserError(f"BU/PICK dengan origin {origin_so} tidak ditemukan.") - - # Ambil group_id dari operations - group_id = self.operations.group_id.id if self.operations.group_id else False - - Picking = self.env['stock.picking'] - StockMove = self.env['stock.move'] - - # Cari picking types - srt_type = self.env['stock.picking.type'].search([ - ('sequence_code', '=', 'SRT') - ], limit=1) - - ort_type = self.env['stock.picking.type'].search([ - ('sequence_code', '=', 'ORT') - ], limit=1) - - if not srt_type or not ort_type: - raise UserError("Picking type SRT atau ORT tidak ditemukan!") - - # Lokasi - location_dest_id = srt_type.default_location_dest_id.id - location_dest_id_ort = ort_type.default_location_dest_id.id - location_customer = self.operations.location_dest_id - - # 1. Create BU/SRT: return dari customer ke gudang - srt_picking = Picking.create({ - 'partner_id': self.operations.partner_id.id, - 'real_shipping_id': self.operations.real_shipping_id.id, - 'picking_type_id': srt_type.id, - 'location_id': location_customer.id, - 'location_dest_id': location_dest_id, - 'origin': f"Retur {self.operations.name}", - 'tukar_guling_id': self.id, - 'group_id': group_id, - }) - - # Create moves untuk SRT - srt_moves = [] - for line in self.line_ids: - move_vals = { - 'name': line.name or line.product_id.name, - 'product_id': line.product_id.id, - 'product_uom_qty': line.product_uom_qty, - 'product_uom': line.product_uom.id, - 'location_id': location_customer.id, - 'location_dest_id': location_dest_id, - 'picking_id': srt_picking.id, - 'group_id': group_id, - 'state': 'draft', - } - move = StockMove.create(move_vals) - srt_moves.append(move) - - # Confirm SRT picking - srt_picking.action_confirm() - - # 2. Create BU/ORT: return dari gudang ke supplier/vendor - ort_picking = Picking.create({ - 'partner_id': self.operations.partner_id.id, - 'real_shipping_id': self.operations.real_shipping_id.id, - 'picking_type_id': ort_type.id, - 'location_id': location_dest_id, - 'location_dest_id': location_dest_id_ort, - 'origin': f"Retur {pick_picking.name}", - 'tukar_guling_id': self.id, - 'group_id': group_id, - }) - - # Create moves untuk ORT - ort_moves = [] - for line in self.line_ids: - move_vals = { - 'name': line.name or line.product_id.name, - 'product_id': line.product_id.id, - 'product_uom_qty': line.product_uom_qty, - 'product_uom': line.product_uom.id, - 'location_id': location_dest_id, - 'location_dest_id': location_dest_id_ort, - 'picking_id': ort_picking.id, - 'group_id': group_id, - 'state': 'draft', - } - move = StockMove.create(move_vals) - ort_moves.append(move) - - # Confirm ORT picking - ort_picking.action_confirm() - ort_picking.action_assign() - - # Log creation - _logger.info(f"Created SRT picking: {srt_picking.name} with {len(srt_moves)} moves") - _logger.info(f"Created ORT picking: {ort_picking.name} with {len(ort_moves)} moves") - - return { - 'srt_picking': srt_picking, - 'ort_picking': ort_picking - } - - def action_approve(self): - self.ensure_one() - - if not self.operations: - raise UserError("BU/Out harus diisi!") - - if not self.return_type: - raise UserError("Return Type harus diisi!") - - # Validasi product lines - self._validate_product_lines() - - # Cek hak akses berdasarkan state - if self.state == 'approval_sales': - if not self.env.user.has_group('indoteknik_custom.group_role_sales'): - raise UserError("Hanya Sales Manager yang boleh approve tahap ini.") - self.state = 'approval_logistic' - - elif self.state == 'approval_logistic': - if not self.env.user.has_group('indoteknik_custom.group_role_logistic'): - raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.") - self.state = 'approval_finance' - - elif self.state == 'approval_finance': - if not self.env.user.has_group('indoteknik_custom.group_role_fat'): - raise UserError("Hanya Finance Manager yang boleh approve tahap ini.") - self.state = 'done' - # Create pickings saat final approval - result = self._create_pickings() - if result: - return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'params': { - 'title': 'Success', - 'message': f"Berhasil membuat BU/SRT: {result['srt_picking'].name} dan BU/ORT: {result['ort_picking'].name}", - 'type': 'success', - 'sticky': False, - } - } - else: - raise UserError("Status ini tidak bisa di-approve.") - - # ... (rest of the methods remain the same) - @api.constrains('return_type', 'operations') - def _check_required_bu_fields(self): - for record in self: - if record.return_type in ['revisi_so', 'tukar_guling'] and not record.operations: - raise ValidationError("BU/Out harus diisi!") - - @api.constrains('line_ids', 'state') - def _check_product_lines(self): - """Constraint: Product lines harus ada jika state bukan draft""" - for record in self: - if record.state in ('approval_sales', 'approval_logistic', 'approval_finance', - 'done') and not record.line_ids: - raise ValidationError("Product lines harus diisi sebelum submit atau approve!") - - def _validate_product_lines(self): - """Helper method untuk validasi product lines""" - self.ensure_one() - - # Check ada product lines - if not self.line_ids: - raise UserError("Belum ada product lines yang ditambahkan!") - - # Check product sudah diisi - empty_lines = self.line_ids.filtered(lambda line: not line.product_id) - if empty_lines: - raise UserError("Ada product lines yang belum diisi productnya!") - - # Check quantity > 0 - zero_qty_lines = self.line_ids.filtered(lambda line: line.product_uom_qty <= 0) - if zero_qty_lines: - raise UserError("Quantity product tidak boleh kosong atau 0!") - - return True +from odoo import _, api, fields, models +from odoo.exceptions import UserError +from odoo.tools.float_utils import float_round + + +class ReturnPicking(models.TransientModel): + _inherit = 'stock.return.picking' + + # @api.model + # def default_get(self, fields): + # res = super(ReturnPicking, self).default_get(fields) + # + # stock_picking = self.env['stock.picking'].search([ + # ('id', '=', res['picking_id']), + # ]) + # + # # sale_id = stock_picking.group_id.sale_id + # if not stock_picking.approval_return_status == 'approved': + # raise UserError('Harus Approval Accounting AR untuk melakukan Retur') + # + # # purchase = self.env['purchase.order'].search([ + # # ('name', '=', stock_picking.group_id.name), + # # ]) + # # if not stock_picking.approval_return_status == 'approved' and purchase.invoice_ids: + # # raise UserError('Harus Approval Accounting AP untuk melakukan Retur') + # + # return res @api.model - def create(self, vals): - if not vals.get('name') or vals['name'] == 'New': - vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling') or 'New' - # Auto-fill origin from operations - if not vals.get('origin') and vals.get('operations'): - picking = self.env['stock.picking'].browse(vals['operations']) - if picking.origin: - vals['origin'] = picking.origin - return super(TukarGuling, self).create(vals) - - def action_submit(self): - self.ensure_one() - if self.state != 'draft': - raise UserError("Submit hanya bisa dilakukan dari Draft.") - - # Validasi sebelum submit - self._validate_product_lines() - - self.state = 'approval_sales' - - def action_cancel(self): - self.ensure_one() - # Cek apakah ada picking yang sudah dibuat - if self.picking_ids: - done_pickings = self.picking_ids.filtered(lambda p: p.state == 'done') - if done_pickings: - raise UserError("Tidak bisa cancel karena ada transfer yang sudah selesai!") - - self.state = 'cancel' - - def action_view_picking(self): - self.ensure_one() - action = self.env.ref('stock.action_picking_tree_all').read()[0] - pickings = self.picking_ids - if len(pickings) > 1: - action['domain'] = [('id', 'in', pickings.ids)] - elif pickings: - action['views'] = [(self.env.ref('stock.view_picking_form').id, 'form')] - action['res_id'] = pickings.id - else: - raise UserError("Belum ada transfer yang dibuat!") - return action - - -class TukarGulingLine(models.Model): - _name = 'tukar.guling.line' - _description = 'Tukar Guling Line' - _order = 'sequence, id' - - sequence = fields.Integer('Sequence', default=10, copy=False) - tukar_guling_id = fields.Many2one('tukar.guling', string='Tukar Guling', required=True, ondelete='cascade') - product_id = fields.Many2one('product.product', string='Product', required=True) - product_uom_qty = fields.Float('Quantity', digits='Product Unit of Measure', required=True, default=1.0) - product_uom = fields.Many2one('uom.uom', string='Unit of Measure') - name = fields.Text('Description') - - @api.model_create_multi - def create(self, vals_list): - """Override create to auto-assign sequence""" - for vals in vals_list: - if 'sequence' not in vals or vals.get('sequence', 0) <= 0: - # Get max sequence untuk tukar_guling yang sama - tukar_guling_id = vals.get('tukar_guling_id') - if tukar_guling_id: - max_seq = self.search([ - ('tukar_guling_id', '=', tukar_guling_id) - ], order='sequence desc', limit=1) - vals['sequence'] = (max_seq.sequence or 0) + 10 - else: - vals['sequence'] = 10 - return super(TukarGulingLine, self).create(vals_list) - - @api.onchange('product_id') - def _onchange_product_id(self): - if self.product_id: - self.name = self.product_id.display_name - self.product_uom = self.product_id.uom_id - + def default_get(self, fields): + res = super(ReturnPicking, self).default_get(fields) -class StockPicking(models.Model): - _inherit = 'stock.picking' + picking_id = res.get('picking_id') + if not picking_id: + return res - tukar_guling_id = fields.Many2one('tukar.guling', string='Tukar Guling Reference') + stock_picking = self.env['stock.picking'].browse(picking_id) - def action_create_tukar_guling(self): - """Action untuk membuat Tukar Guling dari picking""" - self.ensure_one() + if not stock_picking.approval_return_status == 'approved': + raise UserError('Harus Approval Accounting AR untuk melakukan Retur') - # Cek apakah picking sudah done - if self.state != 'done': - raise UserError("Hanya bisa membuat Tukar Guling dari delivery yang sudah selesai!") + return res - # Cek apakah sudah ada tukar guling untuk picking ini - existing_tukar_guling = self.env['tukar.guling'].search([ - ('operations', '=', self.id) - ]) - if existing_tukar_guling: - raise UserError(f"Sudah ada Tukar Guling untuk delivery ini: {existing_tukar_guling.name}") +class ReturnPickingLine(models.TransientModel): + _inherit = 'stock.return.picking.line' - # Create tukar guling baru - tukar_guling = self.env['tukar.guling'].create({ - 'operations': self.id, - 'return_type': 'tukar_guling', # default value - }) + @api.onchange('quantity') + def _onchange_quantity(self): + for rec in self: + qty_done = rec.move_id.quantity_done - return { - 'type': 'ir.actions.act_window', - 'name': 'Tukar Guling', - 'res_model': 'tukar.guling', - 'res_id': tukar_guling.id, - 'view_mode': 'form', - 'target': 'current', - } \ No newline at end of file + if rec.quantity > qty_done: + raise UserError(f"Quantity yang Anda masukkan tidak boleh melebihi quantity done yaitu: {qty_done}") \ No newline at end of file diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 08b862a7..7bcf5e80 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -160,23 +160,34 @@ class TukarGuling(models.Model): @api.model def create(self, vals): + # Generate sequence number if not vals.get('name') or vals['name'] == 'New': - vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling') or 'New' + # Pastikan sequence code 'tukar.guling' ada + sequence = self.env['ir.sequence'].search([('code', '=', 'tukar.guling')], limit=1) + if sequence: + vals['name'] = sequence.next_by_id() + else: + # Fallback jika sequence belum dibuat + vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling') or 'PTG-00001' + # Auto-fill origin from operations if not vals.get('origin') and vals.get('operations'): picking = self.env['stock.picking'].browse(vals['operations']) if picking.origin: vals['origin'] = picking.origin + return super(TukarGuling, self).create(vals) def copy(self, default=None): if default is None: default = {} - if 'name' not in default: - default.update({ - 'name': self.env['ir.sequence'].next_by_code(self._name) or 'New', - }) + # Generate new sequence untuk duplicate + sequence = self.env['ir.sequence'].search([('code', '=', 'tukar.guling')], limit=1) + if sequence: + default['name'] = sequence.next_by_id() + else: + default['name'] = self.env['ir.sequence'].next_by_code('tukar.guling') or 'PTG-COPY' default.update({ 'state': 'draft', @@ -191,7 +202,6 @@ class TukarGuling(models.Model): line.sequence = (i + 1) * 10 return new_record - def write(self, vals): if 'operations' in vals and not vals.get('origin'): picking = self.env['stock.picking'].browse(vals['operations']) @@ -352,6 +362,21 @@ class TukarGuling(models.Model): }) for line in self.line_ids ] }) + for line in self.line_ids: + move = ort_picking.move_ids_without_package.filtered( + lambda m: m.product_id == line.product_id + )[:1] + + if move: + self.env['stock.move.line'].create({ + 'move_id': move.id, + 'picking_id': ort_picking.id, + 'product_id': line.product_id.id, + 'product_uom_id': line.product_uom.id, + 'qty_done': line.product_uom_qty, # Ambil dari return.picking.line.quantity + 'location_id': location_customer.id, + 'location_dest_id': location_dest_id, + }) ort_picking.action_confirm() ort_picking.action_assign() -- cgit v1.2.3 From 9afff443c6d1f489c30f3306cd60e4c97e3cebd1 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 19 Jun 2025 14:00:29 +0700 Subject: (andri) add button related BU di PO yang hasilnya mengarah ke list related BU --- indoteknik_custom/models/purchase_order.py | 37 ++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 505df735..59d8c789 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -91,6 +91,37 @@ class PurchaseOrder(models.Model): is_cab_visible = fields.Boolean(string='Tampilkan Tombol CAB', compute='_compute_is_cab_visible') + picking_ids = fields.One2many('stock.picking', 'purchase_id', string='Pickings') + + def action_view_related_bu(self): + self.ensure_one() + + # Step 1: cari semua BU pertama (PUT, INT) yang berasal dari PO ini + base_bu = self.env['stock.picking'].search([ + ('name', 'ilike', 'BU/'), + ('origin', 'ilike', self.name) + ]) + base_bu_names = base_bu.mapped('name') + + # Step 2: cari BU turunan (seperti BU/VRT) yang origin-nya mengandung nama BU tersebut + domain = [ + '|', + '&', + ('name', 'ilike', 'BU/'), + ('origin', 'ilike', self.name), + ('origin', 'in', [f"Return of {name}" for name in base_bu_names]) + ] + + return { + 'name': 'Related BU (INT/PRT/PUT/VRT)', + 'type': 'ir.actions.act_window', + 'res_model': 'stock.picking', + 'view_mode': 'tree,form', + 'target': 'current', + 'domain': domain, + } + + @api.depends('move_id.state') def _compute_is_cab_visible(self): for order in self: @@ -929,6 +960,12 @@ class PurchaseOrder(models.Model): if self.product_bom_id: self._remove_product_bom() + # Tambahan: redirect ke BU hanya untuk single PO yang berhasil dikonfirmasi + _logger.info("Jumlah PO: %s | State: %s", len(self), self.state) + if len(self) == 1: + _logger.info("Redirecting ke BU") + return self.action_view_related_bu() + return res def _remove_product_bom(self): -- cgit v1.2.3 From bb8d3f325228936acaad5f8cbdac555fc289b854 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 19 Jun 2025 14:49:03 +0700 Subject: push --- indoteknik_custom/models/tukar_guling.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 7bcf5e80..fe0d6ab0 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -202,6 +202,7 @@ class TukarGuling(models.Model): line.sequence = (i + 1) * 10 return new_record + def write(self, vals): if 'operations' in vals and not vals.get('origin'): picking = self.env['stock.picking'].browse(vals['operations']) @@ -279,7 +280,7 @@ class TukarGuling(models.Model): if not origin_so: raise UserError("BU/OUT tidak memiliki origin (SO), tidak bisa cari BU/PICK.") - # Cari DO dari SO + # Cari DO (deliv order (bu/out)) dari SO get_group_id = self.env['stock.picking'].search([ ('origin', '=', origin_so), ], limit=1) @@ -373,7 +374,7 @@ class TukarGuling(models.Model): 'picking_id': ort_picking.id, 'product_id': line.product_id.id, 'product_uom_id': line.product_uom.id, - 'qty_done': line.product_uom_qty, # Ambil dari return.picking.line.quantity + 'qty_done': line.product_uom_qty, 'location_id': location_customer.id, 'location_dest_id': location_dest_id, }) -- cgit v1.2.3 From 1c637ef3fa5f1a0cb39ba0b32353320485622901 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 19 Jun 2025 15:00:29 +0700 Subject: Don, create 4 document --- indoteknik_custom/models/tukar_guling.py | 175 ++++++++++++------------------- 1 file changed, 69 insertions(+), 106 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index fe0d6ab0..6c5d74ec 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -273,113 +273,76 @@ class TukarGuling(models.Model): self.state = 'cancel' def _create_pickings(self): - if not self.operations: - raise UserError("BU/Out harus diisi terlebih dahulu.") - - origin_so = self.operations.origin - if not origin_so: - raise UserError("BU/OUT tidak memiliki origin (SO), tidak bisa cari BU/PICK.") - - # Cari DO (deliv order (bu/out)) dari SO - get_group_id = self.env['stock.picking'].search([ - ('origin', '=', origin_so), - ], limit=1) - - if not get_group_id: - raise UserError(f"Delivery Order dari SO {origin_so} tidak ditemukan.") - - group_id = get_group_id.group_id.id if get_group_id.group_id else False - Picking = self.env['stock.picking'] - srt_type = self.env['stock.picking.type'].search([ - ('sequence_code', '=', 'SRT') - ], limit=1) - - ort_type = self.env['stock.picking.type'].search([ - ('sequence_code', '=', 'ORT') - ], limit=1) - - # Lokasi - location_dest_id = srt_type.default_location_dest_id.id - location_dest_id_ort = ort_type.default_location_dest_id.id - location_customer = self.operations.location_dest_id - - # 1. BU/SRT: retur dari operations - srt_picking = Picking.create({ - 'partner_id': self.operations.partner_id.id, - 'real_shipping_id': self.operations.real_shipping_id.id, - 'picking_type_id': srt_type.id, - 'location_id': location_customer.id, - 'location_dest_id': location_dest_id, - 'origin': f"Retur {self.operations.name}", - 'tukar_guling_id': self.id, - 'group_id': group_id, - 'move_ids_without_package': [ - (0, 0, { - 'name': line.name or line.product_id.name, - 'product_id': line.product_id.id, - 'product_uom_qty': line.product_uom_qty, - 'product_uom': line.product_uom.id, - 'location_id': location_customer.id, - 'location_dest_id': location_dest_id, - 'group_id': group_id, - }) for line in self.line_ids - ] - }) - srt_picking.action_confirm() - - # 2. Cari BU/PICK dari SO yang sama - origin_so = self.operations.origin - if not origin_so: - raise UserError("BU/OUT tidak memiliki origin (SO), tidak bisa cari BU/PICK.") - - pick = Picking.search([ - ('origin', '=', origin_so), - ('picking_type_id.code', '=', 'internal') - ], limit=1) - - if not pick: - raise UserError(f"BU/PICK dengan origin {origin_so} tidak ditemukan.") - - # 3. BU/ORT: retur dari BU/PICK - ort_picking = Picking.create({ - 'partner_id': self.operations.partner_id.id, - 'real_shipping_id': self.operations.real_shipping_id.id, - 'picking_type_id': ort_type.id, - 'location_id': location_dest_id, - 'location_dest_id': location_dest_id_ort, - 'origin': f"Retur {pick.name}", - 'tukar_guling_id': self.id, - 'group_id': group_id, - 'move_ids_without_package': [ - (0, 0, { - 'name': line.name or line.product_id.name, - 'product_id': line.product_id.id, - 'product_uom_qty': line.product_uom_qty, - 'product_uom': line.product_uom.id, - 'location_id': location_dest_id, - 'location_dest_id': location_dest_id_ort, - 'group_id': group_id, - }) for line in self.line_ids - ] - }) - for line in self.line_ids: - move = ort_picking.move_ids_without_package.filtered( - lambda m: m.product_id == line.product_id - )[:1] - - if move: - self.env['stock.move.line'].create({ - 'move_id': move.id, - 'picking_id': ort_picking.id, - 'product_id': line.product_id.id, - 'product_uom_id': line.product_uom.id, - 'qty_done': line.product_uom_qty, - 'location_id': location_customer.id, - 'location_dest_id': location_dest_id, - }) - ort_picking.action_confirm() - ort_picking.action_assign() + group_id = self.env['procurement.group'].create({'name': self.name}).id + + def create_moves(picking_type, origin_suffix, location_id, location_dest_id): + return Picking.create({ + 'partner_id': self.operations.partner_id.id, + 'real_shipping_id': self.operations.real_shipping_id.id, + 'picking_type_id': picking_type.id, + 'location_id': location_id, + 'location_dest_id': location_dest_id, + 'origin': f"Tukar Guling {self.name} - {origin_suffix}", + 'tukar_guling_id': self.id, + 'group_id': group_id, + 'move_ids_without_package': [ + (0, 0, { + 'name': line.name or line.product_id.name, + 'product_id': line.product_id.id, + 'product_uom_qty': line.product_uom_qty, + 'product_uom': line.product_uom.id, + 'location_id': location_id, + 'location_dest_id': location_dest_id, + 'group_id': group_id, + }) for line in self.line_ids + ] + }) + + # BU/SRT + srt_type = self.env['stock.picking.type'].search([('sequence_code', '=', 'SRT')], limit=1) + if not srt_type: + raise UserError("Picking Type dengan sequence_code 'SRT' tidak ditemukan.") + bu_srt = create_moves( + srt_type, 'SRT', + self.operations.location_dest_id.id, + srt_type.default_location_dest_id.id + ) + bu_srt.action_confirm() + + # BU/ORT + ort_type = self.env['stock.picking.type'].search([('sequence_code', '=', 'ORT')], limit=1) + if not ort_type: + raise UserError("Picking Type dengan sequence_code 'ORT' tidak ditemukan.") + bu_ort = create_moves( + ort_type, 'ORT', + ort_type.default_location_src_id.id, + ort_type.default_location_dest_id.id + ) + bu_ort.action_confirm() + + # Jika return_type tukar_guling → lanjut buat PICK dan OUT + if self.return_type == 'tukar_guling': + pick_type = self.env['stock.picking.type'].search([('sequence_code', '=', 'PICK')], limit=1) + if not pick_type: + raise UserError("Picking Type dengan sequence_code 'PICK' tidak ditemukan.") + bu_pick = create_moves( + pick_type, 'PICK', + pick_type.default_location_src_id.id, + pick_type.default_location_dest_id.id + ) + bu_pick.action_confirm() + + out_type = self.env['stock.picking.type'].search([('sequence_code', '=', 'OUT')], limit=1) + if not out_type: + raise UserError("Picking Type dengan sequence_code 'OUT' tidak ditemukan.") + bu_out = create_moves( + out_type, 'OUT', + out_type.default_location_src_id.id, + self.operations.location_dest_id.id + ) + bu_out.action_confirm() + class TukarGulingLine(models.Model): _name = 'tukar.guling.line' -- cgit v1.2.3 From 6be435f0321929f2fb696e9bf91b64737f387769 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 19 Jun 2025 15:21:18 +0700 Subject: (andri) add count bu related --- indoteknik_custom/models/purchase_order.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 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 59d8c789..1a7e50f8 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -91,7 +91,33 @@ class PurchaseOrder(models.Model): is_cab_visible = fields.Boolean(string='Tampilkan Tombol CAB', compute='_compute_is_cab_visible') - picking_ids = fields.One2many('stock.picking', 'purchase_id', string='Pickings') + # picking_ids = fields.One2many('stock.picking', 'purchase_id', string='Pickings') + + bu_related_count = fields.Integer( + string="BU Related Count", + compute='_compute_bu_related_count' + ) + + @api.depends('name') + def _compute_bu_related_count(self): + for order in self: + if not order.name: + order.bu_related_count = 0 + continue + + # BU langsung dari PO + base_bu = self.env['stock.picking'].search([ + ('name', 'ilike', 'BU/'), + ('origin', 'ilike', order.name) + ]) + base_names = base_bu.mapped('name') + + # Return dari BU di atas + return_bu = self.env['stock.picking'].search([ + ('origin', 'in', [f"Return of {name}" for name in base_names]) + ]) + + order.bu_related_count = len(base_bu) + len(return_bu) def action_view_related_bu(self): self.ensure_one() -- cgit v1.2.3 From 3be1e1707547d40f618f70bd18e54519842e95a1 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 19 Jun 2025 16:59:09 +0700 Subject: (andri) fix value PO pada CAB dan Vendor Bill pada MISC --- indoteknik_custom/models/invoice_reklas.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/invoice_reklas.py b/indoteknik_custom/models/invoice_reklas.py index 5145e098..58299d3e 100644 --- a/indoteknik_custom/models/invoice_reklas.py +++ b/indoteknik_custom/models/invoice_reklas.py @@ -102,6 +102,9 @@ class InvoiceReklas(models.TransientModel): 'journal_id': 13 } + if invoice.purchase_order_id: + parameters_header['purchase_order_id'] = invoice.purchase_order_id.id + account_move = request.env['account.move'].create([parameters_header]) _logger.info('Success Reklas with %s' % account_move.name) -- cgit v1.2.3 From 498e5c757cdc08691956f2cad6ff5bd40755e7c1 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 19 Jun 2025 17:22:29 +0700 Subject: (andri) fix value reference setelah reklas --- indoteknik_custom/models/invoice_reklas.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/invoice_reklas.py b/indoteknik_custom/models/invoice_reklas.py index 58299d3e..4ed7f0bf 100644 --- a/indoteknik_custom/models/invoice_reklas.py +++ b/indoteknik_custom/models/invoice_reklas.py @@ -87,10 +87,10 @@ class InvoiceReklas(models.TransientModel): po_name = invoice.purchase_order_id.name if invoice.purchase_order_id else '' # Susun nama referensi dengan aman - ref_name = 'REKLAS {} UANG MUKA {} {}{} {}'.format( + ref_name = 'REKLAS {} UANG MUKA {}{}{} {}'.format( self.reklas_id.name or '', 'PENJUALAN' if self.reklas_type == 'penjualan' else 'PEMBELIAN', - invoice.name or '', + f" {invoice.name}" if invoice.name != self.reklas_id.name else '', f" - {po_name}" if po_name else '', invoice.partner_id.name or '' ) @@ -109,7 +109,8 @@ class InvoiceReklas(models.TransientModel): _logger.info('Success Reklas with %s' % account_move.name) # ✅ Set Bill asal sebagai source document - account_move.bill_id = invoice.id + if invoice.move_type == 'in_invoice': + account_move.bill_id = invoice.id # Tambahkan info asal invoice ke jurnal (opsional) account_move.invoice_origin = invoice.name -- cgit v1.2.3 From f1bd55eed208cc351e7486c556cbdf7b6c94a341 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 20 Jun 2025 08:28:02 +0700 Subject: redirect to pengajuan retur --- indoteknik_custom/models/stock_picking_return.py | 113 +++++++++++++++-------- 1 file changed, 77 insertions(+), 36 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking_return.py b/indoteknik_custom/models/stock_picking_return.py index e50446d3..d6225f1a 100644 --- a/indoteknik_custom/models/stock_picking_return.py +++ b/indoteknik_custom/models/stock_picking_return.py @@ -1,46 +1,87 @@ -from odoo import _, api, fields, models from odoo.exceptions import UserError from odoo.tools.float_utils import float_round +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + +from odoo import models, fields, api, _ +from odoo.exceptions import UserError -class ReturnPicking(models.TransientModel): +class StockReturnPicking(models.TransientModel): _inherit = 'stock.return.picking' - # @api.model - # def default_get(self, fields): - # res = super(ReturnPicking, self).default_get(fields) - # - # stock_picking = self.env['stock.picking'].search([ - # ('id', '=', res['picking_id']), - # ]) - # - # # sale_id = stock_picking.group_id.sale_id - # if not stock_picking.approval_return_status == 'approved': - # raise UserError('Harus Approval Accounting AR untuk melakukan Retur') - # - # # purchase = self.env['purchase.order'].search([ - # # ('name', '=', stock_picking.group_id.name), - # # ]) - # # if not stock_picking.approval_return_status == 'approved' and purchase.invoice_ids: - # # raise UserError('Harus Approval Accounting AP untuk melakukan Retur') - # - # return res - - @api.model - def default_get(self, fields): - res = super(ReturnPicking, self).default_get(fields) - - picking_id = res.get('picking_id') - if not picking_id: - return res - - stock_picking = self.env['stock.picking'].browse(picking_id) - - if not stock_picking.approval_return_status == 'approved': - raise UserError('Harus Approval Accounting AR untuk melakukan Retur') - - return res + return_type = fields.Selection([ + ('revisi_so', 'Revisi SO'), + ('tukar_guling', 'Tukar Guling') + ], string='Jenis Retur', default='revisi_so') + + def create_returns(self): + """Override method to handle Tukar Guling redirection""" + if self.return_type == 'tukar_guling': + return self._redirect_to_tukar_guling() + return super(StockReturnPicking, self).create_returns() + + def _redirect_to_tukar_guling(self): + """Redirect to Tukar Guling form with pre-filled data""" + self.ensure_one() + picking = self.picking_id + + # Gunakan pendekatan yang lebih kompatibel untuk Odoo 14 + # Cari hanya baris yang masih ada dan memiliki quantity > 0 + valid_lines = [] + for line in self.product_return_moves: + # Periksa apakah baris masih ada di database atau baru dibuat + if line.id: + # Untuk baris yang sudah ada di database, pastikan masih ada + if not self.env['stock.return.picking.line'].browse(line.id).exists(): + continue + # Baris baru yang belum disimpan tidak memiliki id + + if line.quantity > 0: + valid_lines.append(line) + + if not valid_lines: + raise UserError(_("Please specify at least one product to return with positive quantity.")) + + # Prepare context for Tukar Guling form + context = { + 'default_operations': picking.id, + 'default_partner_id': picking.partner_id.id, + 'default_origin': picking.origin or picking.name, + 'default_return_type': 'tukar_guling', + 'default_date': fields.Datetime.now(), + 'default_state': 'draft', + 'default_ba_num': _('Retur dari %s') % picking.name, + } + + # Prepare product lines + line_vals = [] + for line in valid_lines: + line_vals.append((0, 0, { + 'product_id': line.product_id.id, + 'product_uom_qty': line.quantity, + 'product_uom': line.product_id.uom_id.id, + 'name': line.product_id.display_name, + })) + + context['default_line_ids'] = line_vals + + # Set SO if available + if picking.sale_id: + context['default_so_id'] = picking.sale_id.id + + # Set shipping address + if picking.partner_id: + context['default_real_shipping_id'] = picking.partner_id.id + return { + 'name': _('Tukar Guling'), + 'type': 'ir.actions.act_window', + 'res_model': 'tukar.guling', + 'view_mode': 'form', + 'target': 'current', + 'context': context, + } class ReturnPickingLine(models.TransientModel): _inherit = 'stock.return.picking.line' -- cgit v1.2.3 From 06afce2162894ef23163062092daf00882de8a85 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Fri, 20 Jun 2025 09:04:18 +0700 Subject: (andri) revisi penamaan field pada jika commision type adalah cashback --- indoteknik_custom/models/commision.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py index 03d32d2d..215e2ded 100644 --- a/indoteknik_custom/models/commision.py +++ b/indoteknik_custom/models/commision.py @@ -175,10 +175,19 @@ class CustomerCommision(models.Model): ('approved', 'Approved'), ('reject', 'Rejected'), ], string='Status') - commision_percent = fields.Float(string='Commision %', tracking=3) - commision_amt = fields.Float(string='Commision Amount', tracking=3) - cashback = fields.Float(string='Cashback', compute="compute_cashback") - total_commision = fields.Float(string='Total Commision', compute="compute_cashback") + + # commision_percent = fields.Float(string='Commision %', tracking=3) + commision_percent = fields.Float(string='Cashback %', tracking=3) + + # commision_amt = fields.Float(string='Commision Amount', tracking=3) + commision_amt = fields.Float(string='Cashback', tracking=3) + + # cashback = fields.Float(string='Cashback', compute="compute_cashback") + cashback = fields.Float(string='PPh Cashback', compute="compute_cashback") + + # total_commision = fields.Float(string='Total Commision', compute="compute_cashback") + total_commision = fields.Float(string='Cashback yang dibayarkan', compute="compute_cashback") + total_cashback = fields.Float(string='Total Cashback') commision_amt_text = fields.Char(string='Commision Amount Text', compute='compute_delivery_amt_text') total_dpp = fields.Float(string='Total DPP', compute='_compute_total_dpp') -- cgit v1.2.3 From bce4d940dc90bf50e045a8fde3fd1c7bb53e8562 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 20 Jun 2025 09:21:19 +0700 Subject: revert --- indoteknik_custom/models/tukar_guling.py | 176 +++++++++++++++++++------------ 1 file changed, 106 insertions(+), 70 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 6c5d74ec..7bcf5e80 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -202,7 +202,6 @@ class TukarGuling(models.Model): line.sequence = (i + 1) * 10 return new_record - def write(self, vals): if 'operations' in vals and not vals.get('origin'): picking = self.env['stock.picking'].browse(vals['operations']) @@ -273,76 +272,113 @@ class TukarGuling(models.Model): self.state = 'cancel' def _create_pickings(self): - Picking = self.env['stock.picking'] - group_id = self.env['procurement.group'].create({'name': self.name}).id - - def create_moves(picking_type, origin_suffix, location_id, location_dest_id): - return Picking.create({ - 'partner_id': self.operations.partner_id.id, - 'real_shipping_id': self.operations.real_shipping_id.id, - 'picking_type_id': picking_type.id, - 'location_id': location_id, - 'location_dest_id': location_dest_id, - 'origin': f"Tukar Guling {self.name} - {origin_suffix}", - 'tukar_guling_id': self.id, - 'group_id': group_id, - 'move_ids_without_package': [ - (0, 0, { - 'name': line.name or line.product_id.name, - 'product_id': line.product_id.id, - 'product_uom_qty': line.product_uom_qty, - 'product_uom': line.product_uom.id, - 'location_id': location_id, - 'location_dest_id': location_dest_id, - 'group_id': group_id, - }) for line in self.line_ids - ] - }) - - # BU/SRT - srt_type = self.env['stock.picking.type'].search([('sequence_code', '=', 'SRT')], limit=1) - if not srt_type: - raise UserError("Picking Type dengan sequence_code 'SRT' tidak ditemukan.") - bu_srt = create_moves( - srt_type, 'SRT', - self.operations.location_dest_id.id, - srt_type.default_location_dest_id.id - ) - bu_srt.action_confirm() - - # BU/ORT - ort_type = self.env['stock.picking.type'].search([('sequence_code', '=', 'ORT')], limit=1) - if not ort_type: - raise UserError("Picking Type dengan sequence_code 'ORT' tidak ditemukan.") - bu_ort = create_moves( - ort_type, 'ORT', - ort_type.default_location_src_id.id, - ort_type.default_location_dest_id.id - ) - bu_ort.action_confirm() - - # Jika return_type tukar_guling → lanjut buat PICK dan OUT - if self.return_type == 'tukar_guling': - pick_type = self.env['stock.picking.type'].search([('sequence_code', '=', 'PICK')], limit=1) - if not pick_type: - raise UserError("Picking Type dengan sequence_code 'PICK' tidak ditemukan.") - bu_pick = create_moves( - pick_type, 'PICK', - pick_type.default_location_src_id.id, - pick_type.default_location_dest_id.id - ) - bu_pick.action_confirm() - - out_type = self.env['stock.picking.type'].search([('sequence_code', '=', 'OUT')], limit=1) - if not out_type: - raise UserError("Picking Type dengan sequence_code 'OUT' tidak ditemukan.") - bu_out = create_moves( - out_type, 'OUT', - out_type.default_location_src_id.id, - self.operations.location_dest_id.id - ) - bu_out.action_confirm() + if not self.operations: + raise UserError("BU/Out harus diisi terlebih dahulu.") + + origin_so = self.operations.origin + if not origin_so: + raise UserError("BU/OUT tidak memiliki origin (SO), tidak bisa cari BU/PICK.") + + # Cari DO dari SO + get_group_id = self.env['stock.picking'].search([ + ('origin', '=', origin_so), + ], limit=1) + if not get_group_id: + raise UserError(f"Delivery Order dari SO {origin_so} tidak ditemukan.") + + group_id = get_group_id.group_id.id if get_group_id.group_id else False + + Picking = self.env['stock.picking'] + srt_type = self.env['stock.picking.type'].search([ + ('sequence_code', '=', 'SRT') + ], limit=1) + + ort_type = self.env['stock.picking.type'].search([ + ('sequence_code', '=', 'ORT') + ], limit=1) + + # Lokasi + location_dest_id = srt_type.default_location_dest_id.id + location_dest_id_ort = ort_type.default_location_dest_id.id + location_customer = self.operations.location_dest_id + + # 1. BU/SRT: retur dari operations + srt_picking = Picking.create({ + 'partner_id': self.operations.partner_id.id, + 'real_shipping_id': self.operations.real_shipping_id.id, + 'picking_type_id': srt_type.id, + 'location_id': location_customer.id, + 'location_dest_id': location_dest_id, + 'origin': f"Retur {self.operations.name}", + 'tukar_guling_id': self.id, + 'group_id': group_id, + 'move_ids_without_package': [ + (0, 0, { + 'name': line.name or line.product_id.name, + 'product_id': line.product_id.id, + 'product_uom_qty': line.product_uom_qty, + 'product_uom': line.product_uom.id, + 'location_id': location_customer.id, + 'location_dest_id': location_dest_id, + 'group_id': group_id, + }) for line in self.line_ids + ] + }) + srt_picking.action_confirm() + + # 2. Cari BU/PICK dari SO yang sama + origin_so = self.operations.origin + if not origin_so: + raise UserError("BU/OUT tidak memiliki origin (SO), tidak bisa cari BU/PICK.") + + pick = Picking.search([ + ('origin', '=', origin_so), + ('picking_type_id.code', '=', 'internal') + ], limit=1) + + if not pick: + raise UserError(f"BU/PICK dengan origin {origin_so} tidak ditemukan.") + + # 3. BU/ORT: retur dari BU/PICK + ort_picking = Picking.create({ + 'partner_id': self.operations.partner_id.id, + 'real_shipping_id': self.operations.real_shipping_id.id, + 'picking_type_id': ort_type.id, + 'location_id': location_dest_id, + 'location_dest_id': location_dest_id_ort, + 'origin': f"Retur {pick.name}", + 'tukar_guling_id': self.id, + 'group_id': group_id, + 'move_ids_without_package': [ + (0, 0, { + 'name': line.name or line.product_id.name, + 'product_id': line.product_id.id, + 'product_uom_qty': line.product_uom_qty, + 'product_uom': line.product_uom.id, + 'location_id': location_dest_id, + 'location_dest_id': location_dest_id_ort, + 'group_id': group_id, + }) for line in self.line_ids + ] + }) + for line in self.line_ids: + move = ort_picking.move_ids_without_package.filtered( + lambda m: m.product_id == line.product_id + )[:1] + + if move: + self.env['stock.move.line'].create({ + 'move_id': move.id, + 'picking_id': ort_picking.id, + 'product_id': line.product_id.id, + 'product_uom_id': line.product_uom.id, + 'qty_done': line.product_uom_qty, # Ambil dari return.picking.line.quantity + 'location_id': location_customer.id, + 'location_dest_id': location_dest_id, + }) + ort_picking.action_confirm() + ort_picking.action_assign() class TukarGulingLine(models.Model): _name = 'tukar.guling.line' -- cgit v1.2.3 From 8a3717c34e0968f3f6ddd7d0cc4fb18aeb218bfe Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Fri, 20 Jun 2025 10:04:01 +0700 Subject: (andri) ganti no dokumen yang lama (CC) sesuai dengan commision type yang dipilih (RB/FE/CB) --- indoteknik_custom/models/commision.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py index 215e2ded..46718397 100644 --- a/indoteknik_custom/models/commision.py +++ b/indoteknik_custom/models/commision.py @@ -175,7 +175,7 @@ class CustomerCommision(models.Model): ('approved', 'Approved'), ('reject', 'Rejected'), ], string='Status') - + # commision_percent = fields.Float(string='Commision %', tracking=3) commision_percent = fields.Float(string='Cashback %', tracking=3) @@ -357,14 +357,31 @@ class CustomerCommision(models.Model): @api.model def create(self, vals): - vals['number'] = self.env['ir.sequence'].next_by_code('customer.commision') or '0' - # if vals['commision_amt'] > 0: - # commision_amt = vals['commision_amt'] - # total_dpp = vals['total_dpp'] - # commision_percent = commision_amt / total_dpp * 100 - # vals['commision_percent'] = commision_percent - result = super(CustomerCommision, self).create(vals) - return result + commision_type = vals.get('commision_type') + + if commision_type == 'cashback': + sequence_code = 'customer.commision.cashback' + elif commision_type == 'fee': + sequence_code = 'customer.commision.fee' + elif commision_type == 'rebate': + sequence_code = 'customer.commision.rebate' + else: + raise UserError('Tipe komisi tidak dikenal!') + + vals['number'] = self.env['ir.sequence'].next_by_code(sequence_code) or '0' + + return super(CustomerCommision, self).create(vals) + + # @api.model + # def create(self, vals): + # vals['number'] = self.env['ir.sequence'].next_by_code('customer.commision') or '0' + # # if vals['commision_amt'] > 0: + # # commision_amt = vals['commision_amt'] + # # total_dpp = vals['total_dpp'] + # # commision_percent = commision_amt / total_dpp * 100 + # # vals['commision_percent'] = commision_percent + # result = super(CustomerCommision, self).create(vals) + # return result def action_confirm_customer_commision(self): jakarta_tz = pytz.timezone('Asia/Jakarta') -- cgit v1.2.3 From 6d222cdfb56df09e61cd3add3c3fb328bd9adc7b Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Fri, 20 Jun 2025 14:20:57 +0700 Subject: (andri) patch json agar odoo menerima response kosong dari biteship --- indoteknik_custom/models/__init__.py | 1 + indoteknik_custom/models/patch/__init__.py | 1 + indoteknik_custom/models/patch/http_override.py | 46 +++++++++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 indoteknik_custom/models/patch/__init__.py create mode 100644 indoteknik_custom/models/patch/http_override.py (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index 605d1016..094ac69e 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -151,3 +151,4 @@ from . import account_payment_register from . import stock_inventory from . import sale_order_delay from . import approval_invoice_date +from . import patch diff --git a/indoteknik_custom/models/patch/__init__.py b/indoteknik_custom/models/patch/__init__.py new file mode 100644 index 00000000..051b6537 --- /dev/null +++ b/indoteknik_custom/models/patch/__init__.py @@ -0,0 +1 @@ +from . import http_override \ No newline at end of file diff --git a/indoteknik_custom/models/patch/http_override.py b/indoteknik_custom/models/patch/http_override.py new file mode 100644 index 00000000..e1978edb --- /dev/null +++ b/indoteknik_custom/models/patch/http_override.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +import odoo.http +import json +import logging +from werkzeug.exceptions import BadRequest +import functools + +_logger = logging.getLogger(__name__) + +class CustomJsonRequest(odoo.http.JsonRequest): + def __init__(self, httprequest): + super(odoo.http.JsonRequest, self).__init__(httprequest) + + self.params = {} + request_data_raw = self.httprequest.get_data().decode(self.httprequest.charset) + + self.jsonrequest = {} + if request_data_raw.strip(): + try: + self.jsonrequest = json.loads(request_data_raw) + except ValueError: + msg = 'Invalid JSON data: %r' % (request_data_raw,) + _logger.info('%s: %s (Handled by CustomJsonRequest)', self.httprequest.path, msg) + raise BadRequest(msg) + else: + _logger.info("CustomJsonRequest: Received empty or whitespace-only JSON body. Treating as empty JSON for webhook.") + + self.params = dict(self.jsonrequest.get("params", {})) + self.context = self.params.pop('context', dict(self.session.context)) + + +_original_get_request = odoo.http.Root.get_request + +@functools.wraps(_original_get_request) +def _get_request_override(self, httprequest): + _logger.info("--- DEBUG: !!! _get_request_override IS CALLED !!! ---") + _logger.info(f"--- DEBUG: Request Mimetype: {httprequest.mimetype}, Path: {httprequest.path} ---") + + if httprequest.mimetype in ("application/json", "application/json-rpc"): + _logger.debug("Odoo HTTP: Using CustomJsonRequest for mimetype: %s", httprequest.mimetype) + return CustomJsonRequest(httprequest) + else: + _logger.debug("Odoo HTTP: Using original get_request for mimetype: %s", httprequest.mimetype) + return _original_get_request(self, httprequest) + +odoo.http.Root.get_request = _get_request_override \ No newline at end of file -- cgit v1.2.3 From 6e8591a6bd28c4faafc08eb9c539fe24bdecf419 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Fri, 20 Jun 2025 16:06:00 +0700 Subject: (andri) tracking webhook aktif dan menggantikan peran button sebelumnya --- indoteknik_custom/models/__init__.py | 2 +- indoteknik_custom/models/stock_picking.py | 167 ++++++++++++++++++++---------- 2 files changed, 112 insertions(+), 57 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index 094ac69e..3f538e25 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -151,4 +151,4 @@ from . import account_payment_register from . import stock_inventory from . import sale_order_delay from . import approval_invoice_date -from . import patch +# from . import patch diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index eabef37c..7bb881c2 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1721,15 +1721,15 @@ class StockPicking(models.Model): response = requests.get(_biteship_url + '/trackings/' + self.biteship_tracking_id, headers=headers, json=manifests) result = response.json() - description = { - 'confirmed' : 'Indoteknik telah melakukan permintaan pick-up', - 'allocated' : 'Kurir akan melakukan pick-up pesanan', - 'picking_up' : 'Kurir sedang dalam perjalanan menuju lokasi pick-up', - 'picked' : 'Pesanan sudah di pick-up kurir '+result.get("courier", {}).get("company", ""), - 'on_hold' : 'Pesanan ditahan sementara karena masalah pengiriman', - 'dropping_off' : 'Kurir sudah ditugaskan dan pesanan akan segera diantar ke pembeli', - 'delivered' : f'Pesanan telah sampai dan diterima oleh {result.get("destination", {}).get("contact_name", "")}' - } + # description = { + # 'confirmed' : 'Indoteknik telah melakukan permintaan pick-up', + # 'allocated' : 'Kurir akan melakukan pick-up pesanan', + # 'picking_up' : 'Kurir sedang dalam perjalanan menuju lokasi pick-up', + # 'picked' : 'Pesanan sudah di pick-up kurir '+result.get("courier", {}).get("company", ""), + # 'on_hold' : 'Pesanan ditahan sementara karena masalah pengiriman', + # 'dropping_off' : 'Kurir sudah ditugaskan dan pesanan akan segera diantar ke pembeli', + # 'delivered' : f'Pesanan telah sampai dan diterima oleh {result.get("destination", {}).get("contact_name", "")}' + # } if (result.get('success') == True): history = result.get("history", []) status = result.get("status", "") @@ -1738,7 +1738,7 @@ class StockPicking(models.Model): manifests.append({ "status": re.sub(r'[^a-zA-Z0-9\s]', ' ', entry["status"]).lower().capitalize(), "datetime": self._convert_to_local_time(entry["updated_at"]), - "description": description[entry["status"]], + "description": self._get_biteship_status_description(entry["status"], result), }) return { @@ -1754,53 +1754,108 @@ class StockPicking(models.Model): _logger.error(f"Error fetching Biteship order for picking {self.id}: {str(e)}") return { 'error': str(e) } - def action_sync_biteship_tracking(self): - for picking in self: - if not picking.biteship_id: - raise UserError("Tracking Biteship tidak tersedia.") - - histori = picking.get_manifest_biteship() - updated_fields = {} - seen_logs = set() - - manifests = sorted(histori.get("manifests", []), key=lambda m: m.get("datetime") or "") - - for manifest in manifests: - status = manifest.get("status", "").lower() - dt_str = manifest.get("datetime") - desc = manifest.get("description") - dt = False - - try: - dt = picking._convert_to_utc_datetime(dt_str) - _logger.info(f"[Biteship Sync] Berhasil parse datetime: {dt_str} -> {dt}") - except Exception as e: - _logger.warning(f"[Biteship Sync] Gagal parse datetime: {e}") - continue + # def action_sync_biteship_tracking(self): + # for picking in self: + # if not picking.biteship_id: + # raise UserError("Tracking Biteship tidak tersedia.") + + # histori = picking.get_manifest_biteship() + # updated_fields = {} + # seen_logs = set() + + # manifests = sorted(histori.get("manifests", []), key=lambda m: m.get("datetime") or "") + + # for manifest in manifests: + # status = manifest.get("status", "").lower() + # dt_str = manifest.get("datetime") + # desc = manifest.get("description") + # dt = False + + # try: + # dt = picking._convert_to_utc_datetime(dt_str) + # _logger.info(f"[Biteship Sync] Berhasil parse datetime: {dt_str} -> {dt}") + # except Exception as e: + # _logger.warning(f"[Biteship Sync] Gagal parse datetime: {e}") + # continue + + # # Update tanggal ke field (pastikan naive datetime UTC) + # if status == "picked" and dt and not picking.driver_departure_date: + # updated_fields["driver_departure_date"] = fields.Datetime.to_string(dt) + + # if status == "delivered" and dt and not picking.driver_arrival_date: + # updated_fields["driver_arrival_date"] = fields.Datetime.to_string(dt) + + # # Buat log unik dengan waktu lokal Asia/Jakarta + # if dt and desc: + # try: + # dt_local = parser.parse(dt_str).replace(tzinfo=None) + # except Exception as e: + # _logger.warning(f"[Biteship Sync] Gagal parse dt_str untuk log: {e}") + # dt_local = dt # fallback + + # desc_clean = ' '.join(desc.strip().split()) + # log_line = f"[TRACKING] {status} - {dt_local.strftime('%d %b %Y %H:%M')}: {desc_clean}" + # if not picking._has_existing_log(log_line): + # picking.message_post(body=log_line) + # seen_logs.add(log_line) + + # if updated_fields: + # picking.write(updated_fields) + + def _get_biteship_status_description(self, status, data=None): + + data = data or {} + + courier = data.get("courier", {}).get("company", "") + contact_name = data.get("destination", {}).get("contact_name", "") + + description_map = { + 'confirmed': 'Indoteknik telah melakukan permintaan pick-up', + 'allocated': 'Kurir akan melakukan pick-up pesanan', + 'picking_up': 'Kurir sedang dalam perjalanan menuju lokasi pick-up', + 'picked': f'Pesanan sudah di pick-up kurir {courier}', + 'on_hold': 'Pesanan ditahan sementara karena masalah pengiriman', + 'dropping_off': 'Kurir sudah ditugaskan dan pesanan akan segera diantar ke pembeli', + 'delivered': f'Pesanan telah sampai dan diterima oleh {contact_name}', + 'cancelled': 'Pesanan dibatalkan oleh sistem atau pengguna', + } + + return description_map.get(status, f"Status '{status}' diterima dari Biteship") + + + def log_biteship_event_from_webhook(self, status, timestamp, description): + self.ensure_one() + updated_fields = {} + + try: + dt = self._convert_to_utc_datetime(timestamp) + except Exception as e: + _logger.warning(f"[Webhook] Gagal konversi waktu: {e}") + dt = datetime.utcnow() + + if status == "picked" and not self.driver_departure_date: + updated_fields["driver_departure_date"] = fields.Datetime.to_string(dt) + if status == "delivered" and not self.driver_arrival_date: + updated_fields["driver_arrival_date"] = fields.Datetime.to_string(dt) + + # Update shipping_status + shipping_status = self._map_status_biteship(status) + if shipping_status and self.shipping_status != shipping_status: + updated_fields["shipping_status"] = shipping_status + + # Log ke chatter + try: + dt_local = parser.parse(timestamp).replace(tzinfo=None) + except Exception: + dt_local = dt + + log_line = f"[TRACKING] {status} - {dt_local.strftime('%d %b %Y %H:%M')}: {description.strip()}" + if not self._has_existing_log(log_line): + self.message_post(body=log_line) + + if updated_fields: + self.write(updated_fields) - # Update tanggal ke field (pastikan naive datetime UTC) - if status == "picked" and dt and not picking.driver_departure_date: - updated_fields["driver_departure_date"] = fields.Datetime.to_string(dt) - - if status == "delivered" and dt and not picking.driver_arrival_date: - updated_fields["driver_arrival_date"] = fields.Datetime.to_string(dt) - - # Buat log unik dengan waktu lokal Asia/Jakarta - if dt and desc: - try: - dt_local = parser.parse(dt_str).replace(tzinfo=None) - except Exception as e: - _logger.warning(f"[Biteship Sync] Gagal parse dt_str untuk log: {e}") - dt_local = dt # fallback - - desc_clean = ' '.join(desc.strip().split()) - log_line = f"[TRACKING] {status} - {dt_local.strftime('%d %b %Y %H:%M')}: {desc_clean}" - if not picking._has_existing_log(log_line): - picking.message_post(body=log_line) - seen_logs.add(log_line) - - if updated_fields: - picking.write(updated_fields) def _has_existing_log(self, log_line): self.ensure_one() -- cgit v1.2.3 From 41b26b7fca60533fe30240d19b972cbe7022f333 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sat, 21 Jun 2025 09:07:05 +0700 Subject: return oke --- indoteknik_custom/models/stock_picking_return.py | 127 ++++++++---- indoteknik_custom/models/tukar_guling.py | 242 ++++++++++++----------- 2 files changed, 221 insertions(+), 148 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking_return.py b/indoteknik_custom/models/stock_picking_return.py index d6225f1a..74bf6407 100644 --- a/indoteknik_custom/models/stock_picking_return.py +++ b/indoteknik_custom/models/stock_picking_return.py @@ -1,10 +1,6 @@ from odoo.exceptions import UserError from odoo.tools.float_utils import float_round from odoo import models, fields, api, _ -from odoo.exceptions import UserError - -from odoo import models, fields, api, _ -from odoo.exceptions import UserError class StockReturnPicking(models.TransientModel): @@ -16,8 +12,7 @@ class StockReturnPicking(models.TransientModel): ], string='Jenis Retur', default='revisi_so') def create_returns(self): - """Override method to handle Tukar Guling redirection""" - if self.return_type == 'tukar_guling': + if self._context.get('from_ui', True) and self.return_type == 'tukar_guling': return self._redirect_to_tukar_guling() return super(StockReturnPicking, self).create_returns() @@ -26,53 +21,100 @@ class StockReturnPicking(models.TransientModel): self.ensure_one() picking = self.picking_id - # Gunakan pendekatan yang lebih kompatibel untuk Odoo 14 - # Cari hanya baris yang masih ada dan memiliki quantity > 0 + # Get valid return lines with better error handling valid_lines = [] - for line in self.product_return_moves: - # Periksa apakah baris masih ada di database atau baru dibuat - if line.id: - # Untuk baris yang sudah ada di database, pastikan masih ada - if not self.env['stock.return.picking.line'].browse(line.id).exists(): - continue - # Baris baru yang belum disimpan tidak memiliki id - if line.quantity > 0: - valid_lines.append(line) + try: + # Refresh the recordset to ensure we have the latest data + self.env.cr.execute("SELECT id FROM stock_return_picking_line WHERE wizard_id = %s", (self.id,)) + line_ids = [row[0] for row in self.env.cr.fetchall()] + + if line_ids: + # Use sudo to avoid access rights issues and browse existing lines + existing_lines = self.env['stock.return.picking.line'].sudo().browse(line_ids) + for line in existing_lines: + if line.exists() and line.quantity > 0: + valid_lines.append(line) + + # If no lines found via direct query, try the original approach + if not valid_lines: + for line in self.product_return_moves: + if hasattr(line, 'quantity') and line.quantity > 0: + # Additional check to ensure the line is valid + if line.product_id and line.move_id: + valid_lines.append(line) + + except Exception as e: + # Fallback: create lines based on picking moves + valid_lines = [] + for move in picking.move_ids_without_package: + if move.product_uom_qty > 0 and move.state == 'done': + # Create a temporary line object for data extraction + temp_line = type('TempLine', (), { + 'product_id': move.product_id, + 'quantity': move.quantity_done or move.product_uom_qty, + 'move_id': move + })() + valid_lines.append(temp_line) if not valid_lines: - raise UserError(_("Please specify at least one product to return with positive quantity.")) + raise UserError(_("Tidak ada produk yang bisa diretur. Pastikan ada produk dengan quantity > 0.")) # Prepare context for Tukar Guling form context = { 'default_operations': picking.id, - 'default_partner_id': picking.partner_id.id, - 'default_origin': picking.origin or picking.name, 'default_return_type': 'tukar_guling', 'default_date': fields.Datetime.now(), 'default_state': 'draft', 'default_ba_num': _('Retur dari %s') % picking.name, + 'from_return_picking': True, # Flag to prevent onchange from overriding lines } + # Set origin + if picking.origin: + context['default_origin'] = picking.origin + + # Set partner + if picking.partner_id: + context['default_partner_id'] = picking.partner_id.id + + # Set shipping address + if hasattr(picking, 'real_shipping_id') and picking.real_shipping_id: + context['default_real_shipping_id'] = picking.real_shipping_id.id + elif picking.partner_id: + context['default_real_shipping_id'] = picking.partner_id.id + # Prepare product lines line_vals = [] + sequence = 10 + for line in valid_lines: - line_vals.append((0, 0, { - 'product_id': line.product_id.id, - 'product_uom_qty': line.quantity, - 'product_uom': line.product_id.uom_id.id, - 'name': line.product_id.display_name, - })) + try: + # Get quantity - handle both real lines and temp objects + quantity = getattr(line, 'quantity', 0) + if quantity <= 0: + continue - context['default_line_ids'] = line_vals + # Get product + product = getattr(line, 'product_id', None) + if not product: + continue - # Set SO if available - if picking.sale_id: - context['default_so_id'] = picking.sale_id.id + line_vals.append((0, 0, { + 'sequence': sequence, + 'product_id': product.id, + 'product_uom_qty': quantity, + 'product_uom': product.uom_id.id, + 'name': product.display_name, + })) + sequence += 10 - # Set shipping address - if picking.partner_id: - context['default_real_shipping_id'] = picking.partner_id.id + except Exception as e: + # Skip problematic lines + continue + + if line_vals: + context['default_line_ids'] = line_vals return { 'name': _('Tukar Guling'), @@ -83,13 +125,24 @@ class StockReturnPicking(models.TransientModel): 'context': context, } + class ReturnPickingLine(models.TransientModel): _inherit = 'stock.return.picking.line' @api.onchange('quantity') def _onchange_quantity(self): + """Validate quantity against done quantity""" for rec in self: - qty_done = rec.move_id.quantity_done - - if rec.quantity > qty_done: - raise UserError(f"Quantity yang Anda masukkan tidak boleh melebihi quantity done yaitu: {qty_done}") \ No newline at end of file + if rec.move_id and rec.quantity > 0: + # Get quantity done from the move + qty_done = rec.move_id.quantity_done + + # If quantity_done is 0, use product_uom_qty as fallback + if qty_done == 0: + qty_done = rec.move_id.product_uom_qty + + if rec.quantity > qty_done: + raise UserError( + _("Quantity yang Anda masukkan (%.2f) tidak boleh melebihi quantity done yaitu: %.2f untuk produk %s") + % (rec.quantity, qty_done, rec.product_id.name) + ) \ No newline at end of file diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 7bcf5e80..bdd2a2f5 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -11,6 +11,11 @@ class TukarGuling(models.Model): _order = 'date desc, id desc' _rec_name = 'name' + picking_ids = fields.One2many( + 'stock.picking', + 'tukar_guling_id', + string='Transfers') + origin = fields.Char(string='Origin SO') real_shipping_id = fields.Many2one('res.partner', string='Shipping Address') @@ -44,7 +49,16 @@ class TukarGuling(models.Model): def _onchange_operations(self): """Auto-populate lines ketika operations dipilih""" if self.operations: - # Clear existing lines + from_return_picking = self.env.context.get('from_return_picking', False) or \ + self.env.context.get('default_line_ids', False) + + if self.line_ids and from_return_picking: + # Hanya update origin, jangan ubah lines + if self.operations.origin: + self.origin = self.operations.origin + return + + # Clear existing lines hanya jika tidak dari return picking self.line_ids = [(5, 0, 0)] # Set origin dari operations @@ -91,10 +105,14 @@ class TukarGuling(models.Model): else: _logger.info("No lines created - no valid moves found") else: - # Clear lines jika operations dikosongkan - self.line_ids = [(5, 0, 0)] - self.origin = False + # Clear lines jika operations dikosongkan, kecuali dari return picking + from_return_picking = self.env.context.get('from_return_picking', False) or \ + self.env.context.get('default_line_ids', False) + if not from_return_picking: + self.line_ids = [(5, 0, 0)] + + self.origin = False def action_populate_lines(self): """Manual button untuk populate lines - sebagai alternatif""" self.ensure_one() @@ -272,113 +290,115 @@ class TukarGuling(models.Model): self.state = 'cancel' def _create_pickings(self): - if not self.operations: - raise UserError("BU/Out harus diisi terlebih dahulu.") - - origin_so = self.operations.origin - if not origin_so: - raise UserError("BU/OUT tidak memiliki origin (SO), tidak bisa cari BU/PICK.") - - # Cari DO dari SO - get_group_id = self.env['stock.picking'].search([ - ('origin', '=', origin_so), - ], limit=1) - - if not get_group_id: - raise UserError(f"Delivery Order dari SO {origin_so} tidak ditemukan.") - - group_id = get_group_id.group_id.id if get_group_id.group_id else False - - Picking = self.env['stock.picking'] - srt_type = self.env['stock.picking.type'].search([ - ('sequence_code', '=', 'SRT') - ], limit=1) - - ort_type = self.env['stock.picking.type'].search([ - ('sequence_code', '=', 'ORT') - ], limit=1) - - # Lokasi - location_dest_id = srt_type.default_location_dest_id.id - location_dest_id_ort = ort_type.default_location_dest_id.id - location_customer = self.operations.location_dest_id - - # 1. BU/SRT: retur dari operations - srt_picking = Picking.create({ - 'partner_id': self.operations.partner_id.id, - 'real_shipping_id': self.operations.real_shipping_id.id, - 'picking_type_id': srt_type.id, - 'location_id': location_customer.id, - 'location_dest_id': location_dest_id, - 'origin': f"Retur {self.operations.name}", - 'tukar_guling_id': self.id, - 'group_id': group_id, - 'move_ids_without_package': [ - (0, 0, { - 'name': line.name or line.product_id.name, - 'product_id': line.product_id.id, - 'product_uom_qty': line.product_uom_qty, - 'product_uom': line.product_uom.id, - 'location_id': location_customer.id, - 'location_dest_id': location_dest_id, - 'group_id': group_id, - }) for line in self.line_ids - ] - }) - srt_picking.action_confirm() - - # 2. Cari BU/PICK dari SO yang sama - origin_so = self.operations.origin - if not origin_so: - raise UserError("BU/OUT tidak memiliki origin (SO), tidak bisa cari BU/PICK.") - - pick = Picking.search([ - ('origin', '=', origin_so), - ('picking_type_id.code', '=', 'internal') - ], limit=1) - - if not pick: - raise UserError(f"BU/PICK dengan origin {origin_so} tidak ditemukan.") - - # 3. BU/ORT: retur dari BU/PICK - ort_picking = Picking.create({ - 'partner_id': self.operations.partner_id.id, - 'real_shipping_id': self.operations.real_shipping_id.id, - 'picking_type_id': ort_type.id, - 'location_id': location_dest_id, - 'location_dest_id': location_dest_id_ort, - 'origin': f"Retur {pick.name}", - 'tukar_guling_id': self.id, - 'group_id': group_id, - 'move_ids_without_package': [ - (0, 0, { - 'name': line.name or line.product_id.name, - 'product_id': line.product_id.id, - 'product_uom_qty': line.product_uom_qty, - 'product_uom': line.product_uom.id, - 'location_id': location_dest_id, - 'location_dest_id': location_dest_id_ort, - 'group_id': group_id, - }) for line in self.line_ids - ] - }) - for line in self.line_ids: - move = ort_picking.move_ids_without_package.filtered( - lambda m: m.product_id == line.product_id - )[:1] - - if move: - self.env['stock.move.line'].create({ - 'move_id': move.id, - 'picking_id': ort_picking.id, - 'product_id': line.product_id.id, - 'product_uom_id': line.product_uom.id, - 'qty_done': line.product_uom_qty, # Ambil dari return.picking.line.quantity - 'location_id': location_customer.id, - 'location_dest_id': location_dest_id, + for record in self: + if not record.operations: + raise UserError("BU/OUT dari field operations tidak ditemukan.") + + operation_picking = record.operations + + # 1. Cari semua picking DONE berdasarkan origin SO + related_pickings = self.env['stock.picking'].search([ + ('origin', '=', record.origin), + ('state', '=', 'done'), + ]) + if not related_pickings: + raise UserError("Tidak ditemukan BU/PICK atau BU/OUT dari SO: %s" % record.origin + "Atau masih belum Done") + + # 2. Filter berdasarkan tipe picking + bu_pick_to_return = related_pickings.filtered(lambda p: p.picking_type_id.id == 30) # BU/PICK + bu_out_to_return = related_pickings.filtered(lambda p: p.picking_type_id.id == 29) # BU/OUT + + if not bu_pick_to_return and not bu_out_to_return: + raise UserError("Tidak ada BU/PICK atau BU/OUT yang selesai untuk diretur.") + + created_returns = [] + + # Lokasi default untuk retur + bu_out_type = self.env['stock.picking.type'].browse(73) + bu_stock_type = self.env['stock.picking.type'].browse(74) + + bu_out = bu_out_type.default_location_src_id.id + bu_stock = bu_out_type.default_location_dest_id.id + + if not bu_out or not bu_stock: + raise UserError("salahwoi") + + partner_location = self.env['stock.location'].browse(2) + if not partner_location: + raise UserError("Lokasi partner (real_shipping_id) tidak ditemukan pada BU/OUT utama.") + + # Fungsi membuat retur dari picking tertentu + def _create_return_from_picking(picking): + grup = self.env['stock.picking'].search([('origin', '=', self.operations.origin)]) + # Tentukan lokasi berdasarkan jenis picking + if picking.picking_type_id.id == 29: # BU/OUT → BU/SRT + default_location_id = partner_location.id + default_location_dest_id = bu_out + elif picking.picking_type_id.id == 30: # BU/PICK → BU/ORT + default_location_id = bu_out + default_location_dest_id = bu_stock + else: + return None + + return_context = dict(self.env.context) + return_context.update({ + 'active_id': picking.id, + 'default_location_id': default_location_id, + 'default_location_dest_id': default_location_dest_id, + 'from_ui': False, }) - ort_picking.action_confirm() - ort_picking.action_assign() + + return_wizard = self.env['stock.return.picking'].with_context(return_context).create({ + 'picking_id': picking.id, + 'location_id': default_location_id, + }) + + # Buat return lines + return_lines = [] + for move in picking.move_lines: + if move.quantity_done > 0: + return_lines.append((0, 0, { + 'product_id': move.product_id.id, + 'quantity': move.quantity_done, + 'move_id': move.id, + })) + if not return_lines: + return None + + return_wizard.product_return_moves = return_lines + + _logger.info("Creating return for picking %s", picking.name) + _logger.info("Default location src: %s", default_location_id) + _logger.info("Default location dest: %s", default_location_dest_id) + _logger.info("Move lines: %s", picking.move_lines) + return_vals = return_wizard.create_returns() + return_id = return_vals.get('res_id') + + if not return_id: + raise UserError("Retur gagal dibuat. Hasil create_returns: %s" % str(return_vals)) + + picking_obj = self.env['stock.picking'].browse(return_id) + for p in picking_obj: + p.group_id = self.operations.group_id.id + p.origin_tukar_guling_id = record.id + + return picking_obj.name + + # Buat return dari BU/PICK + for picking in bu_pick_to_return: + name = _create_return_from_picking(picking) + if name: + created_returns.append(name) + + # Buat return dari BU/OUT + for picking in bu_out_to_return: + name = _create_return_from_picking(picking) + if name: + created_returns.append(name) + + if not created_returns: + raise UserError("wkwkwk") + class TukarGulingLine(models.Model): _name = 'tukar.guling.line' @@ -418,4 +438,4 @@ class TukarGulingLine(models.Model): class StockPicking(models.Model): _inherit = 'stock.picking' - origin_tukar_guling_id = fields.Many2one('tukar.guling', string='Tukar Guling Ref') \ No newline at end of file + tukar_guling_id = fields.Many2one('tukar.guling', string='Tukar Guling Ref') \ No newline at end of file -- cgit v1.2.3 From 2dcea6aa1c0aa57de8bac126f041ca547bc73cad Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sat, 21 Jun 2025 09:18:25 +0700 Subject: fix cannot validate srt --- indoteknik_custom/models/tukar_guling.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index bdd2a2f5..81ef6b1a 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -11,11 +11,6 @@ class TukarGuling(models.Model): _order = 'date desc, id desc' _rec_name = 'name' - picking_ids = fields.One2many( - 'stock.picking', - 'tukar_guling_id', - string='Transfers') - origin = fields.Char(string='Origin SO') real_shipping_id = fields.Many2one('res.partner', string='Shipping Address') @@ -323,7 +318,7 @@ class TukarGuling(models.Model): if not bu_out or not bu_stock: raise UserError("salahwoi") - partner_location = self.env['stock.location'].browse(2) + partner_location = self.env['stock.location'].browse(5) if not partner_location: raise UserError("Lokasi partner (real_shipping_id) tidak ditemukan pada BU/OUT utama.") @@ -380,7 +375,7 @@ class TukarGuling(models.Model): picking_obj = self.env['stock.picking'].browse(return_id) for p in picking_obj: p.group_id = self.operations.group_id.id - p.origin_tukar_guling_id = record.id + p.tukar_guling_id = record.id return picking_obj.name -- cgit v1.2.3 From 87bd344baa9b40cde21256bd1e3680d0d2396e2e Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sat, 21 Jun 2025 09:32:38 +0700 Subject: fix cannot validate srt --- indoteknik_custom/models/tukar_guling.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 81ef6b1a..d77ea8d4 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -108,6 +108,7 @@ class TukarGuling(models.Model): self.line_ids = [(5, 0, 0)] self.origin = False + def action_populate_lines(self): """Manual button untuk populate lines - sebagai alternatif""" self.ensure_one() @@ -215,6 +216,7 @@ class TukarGuling(models.Model): line.sequence = (i + 1) * 10 return new_record + def write(self, vals): if 'operations' in vals and not vals.get('origin'): picking = self.env['stock.picking'].browse(vals['operations']) @@ -297,7 +299,8 @@ class TukarGuling(models.Model): ('state', '=', 'done'), ]) if not related_pickings: - raise UserError("Tidak ditemukan BU/PICK atau BU/OUT dari SO: %s" % record.origin + "Atau masih belum Done") + raise UserError( + "Tidak ditemukan BU/PICK atau BU/OUT dari SO: %s" % record.origin + "Atau masih belum Done") # 2. Filter berdasarkan tipe picking bu_pick_to_return = related_pickings.filtered(lambda p: p.picking_type_id.id == 30) # BU/PICK @@ -318,9 +321,11 @@ class TukarGuling(models.Model): if not bu_out or not bu_stock: raise UserError("salahwoi") - partner_location = self.env['stock.location'].browse(5) + partner_location = self.env['stock.location'].search( + [('complete_name', 'ilike', 'Partner Locations/Customers'), + ('id', '=', '5')]) if not partner_location: - raise UserError("Lokasi partner (real_shipping_id) tidak ditemukan pada BU/OUT utama.") + raise UserError("Lokasi partner salah atau tidak ditemukan pada BU/OUT.") # Fungsi membuat retur dari picking tertentu def _create_return_from_picking(picking): @@ -433,4 +438,4 @@ class TukarGulingLine(models.Model): class StockPicking(models.Model): _inherit = 'stock.picking' - tukar_guling_id = fields.Many2one('tukar.guling', string='Tukar Guling Ref') \ No newline at end of file + tukar_guling_id = fields.Many2one('tukar.guling', string='Tukar Guling Ref') -- cgit v1.2.3 From 47f83eefa28e7902c4f91c03ac6cd2f71a56e67d Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 21 Jun 2025 11:02:58 +0700 Subject: (andri) penyesuaian informasi delivery (biteship) pada BU OUT --- indoteknik_custom/models/stock_picking.py | 57 ++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 8 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 7bb881c2..f171c5d0 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -271,15 +271,22 @@ class StockPicking(models.Model): # Biteship Section biteship_id = fields.Char(string="Biteship Respon ID") - biteship_tracking_id = fields.Char(string="Biteship Trackcking ID") + biteship_tracking_id = fields.Char(string="Biteship Tracking ID") biteship_waybill_id = fields.Char(string="Biteship Waybill ID") + biteship_driver_name = fields.Char('Biteship Driver Name') + biteship_driver_phone = fields.Char('Biteship Driver Phone') + biteship_driver_plate_number = fields.Char('Biteship Driver Plate Number') + biteship_courier_link = fields.Char('Biteship Courier Link') + biteship_shipping_status = fields.Char('Biteship Shipping Status', help="Status pengiriman dari Biteship") + biteship_shipping_price = fields.Monetary('Biteship Shipping Price', currency_field='currency_id', help="Harga pengiriman dari Biteship") + currency_id = fields.Many2one('res.currency', related='sale_id.currency_id', string='Currency', readonly=True) final_seq = fields.Float(string='Remaining Time') - shipping_method_so_id = fields.Many2one('delivery.carrier', string='Shipping Method SO', related='sale_id.carrier_id') - shipping_option_so_id = fields.Many2one('shipping.option', string='Shipping Option SO', related='sale_id.shipping_option_id') + shipping_method_so_id = fields.Many2one('delivery.carrier', string='Shipping Method', related='sale_id.carrier_id', help="Shipping Method yang digunakan di SO") + shipping_option_so_id = fields.Many2one('shipping.option', string='Shipping Option', related='sale_id.shipping_option_id' , help="Shipping Option yang digunakan di SO") select_shipping_option_so = fields.Selection([ ('biteship', 'Biteship'), ('custom', 'Custom'), - ], string='Shipping Type SO', related='sale_id.select_shipping_option') + ], string='Shipping Type', related='sale_id.select_shipping_option', help="Shipping Type yang digunakan di SO") state_packing = fields.Selection([('not_packing', 'Belum Packing'), ('packing_done', 'Sudah Packing')], string='Packing Status') approval_invoice_date_id = fields.Many2one('approval.invoice.date', string='Approval Invoice Date') last_update_date_doc_kirim = fields.Datetime(string='Last Update Tanggal Kirim', copy=False) @@ -1802,6 +1809,16 @@ class StockPicking(models.Model): # if updated_fields: # picking.write(updated_fields) + def action_open_biteship_tracking(self): + self.ensure_one() + if not self.biteship_courier_link: + raise UserError("Biteship tracking link tidak tersedia.") + return { + 'type': 'ir.actions.act_url', + 'url': self.biteship_courier_link, + 'target': 'new', + } + def _get_biteship_status_description(self, status, data=None): data = data or {} @@ -1823,16 +1840,21 @@ class StockPicking(models.Model): return description_map.get(status, f"Status '{status}' diterima dari Biteship") - def log_biteship_event_from_webhook(self, status, timestamp, description): + def log_biteship_event_from_webhook(self, status, timestamp, description, extra_data=None): + """ + extra_data: dict opsional dari webhook (driver name, phone, plate, link, price, dsb) + """ self.ensure_one() updated_fields = {} + # Konversi timestamp ke UTC datetime try: dt = self._convert_to_utc_datetime(timestamp) except Exception as e: _logger.warning(f"[Webhook] Gagal konversi waktu: {e}") dt = datetime.utcnow() + # Update tanggal driver if status == "picked" and not self.driver_departure_date: updated_fields["driver_departure_date"] = fields.Datetime.to_string(dt) if status == "delivered" and not self.driver_arrival_date: @@ -1843,18 +1865,37 @@ class StockPicking(models.Model): if shipping_status and self.shipping_status != shipping_status: updated_fields["shipping_status"] = shipping_status - # Log ke chatter + # Update field tambahan Biteship jika ada + if extra_data: + if extra_data.get("courier_driver_name"): + updated_fields["biteship_driver_name"] = extra_data["courier_driver_name"] + if extra_data.get("courier_driver_phone"): + updated_fields["biteship_driver_phone"] = extra_data["courier_driver_phone"] + if extra_data.get("courier_driver_plate_number"): + updated_fields["biteship_driver_plate_number"] = extra_data["courier_driver_plate_number"] + if extra_data.get("courier_link"): + updated_fields["biteship_courier_link"] = extra_data["courier_link"] + if extra_data.get("order_price"): + updated_fields["biteship_shipping_price"] = extra_data["order_price"] + if extra_data.get("status"): + updated_fields["biteship_shipping_status"] = extra_data["status"] + + # Format log untuk chatter try: dt_local = parser.parse(timestamp).replace(tzinfo=None) except Exception: dt_local = dt - log_line = f"[TRACKING] {status} - {dt_local.strftime('%d %b %Y %H:%M')}: {description.strip()}" + desc_clean = ' '.join(description.strip().split()) + log_line = f"[TRACKING] {status} - {dt_local.strftime('%d %b %Y %H:%M')}: {desc_clean}" + if not self._has_existing_log(log_line): - self.message_post(body=log_line) + self.with_user(15172).message_post(body=log_line) + # Apply update if updated_fields: self.write(updated_fields) + _logger.info(f"[Webhook] Updated fields on picking {self.name}: {updated_fields}") def _has_existing_log(self, log_line): -- cgit v1.2.3 From 6b780e717b8fe0b7959bd1a1f6d59b183d9845d9 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 21 Jun 2025 11:21:13 +0700 Subject: (andri) fix datetime log tracking --- indoteknik_custom/models/stock_picking.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index f171c5d0..87363fd2 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1841,31 +1841,24 @@ class StockPicking(models.Model): def log_biteship_event_from_webhook(self, status, timestamp, description, extra_data=None): - """ - extra_data: dict opsional dari webhook (driver name, phone, plate, link, price, dsb) - """ self.ensure_one() updated_fields = {} - # Konversi timestamp ke UTC datetime try: dt = self._convert_to_utc_datetime(timestamp) except Exception as e: _logger.warning(f"[Webhook] Gagal konversi waktu: {e}") dt = datetime.utcnow() - # Update tanggal driver if status == "picked" and not self.driver_departure_date: updated_fields["driver_departure_date"] = fields.Datetime.to_string(dt) if status == "delivered" and not self.driver_arrival_date: updated_fields["driver_arrival_date"] = fields.Datetime.to_string(dt) - # Update shipping_status shipping_status = self._map_status_biteship(status) if shipping_status and self.shipping_status != shipping_status: updated_fields["shipping_status"] = shipping_status - # Update field tambahan Biteship jika ada if extra_data: if extra_data.get("courier_driver_name"): updated_fields["biteship_driver_name"] = extra_data["courier_driver_name"] @@ -1880,19 +1873,20 @@ class StockPicking(models.Model): if extra_data.get("status"): updated_fields["biteship_shipping_status"] = extra_data["status"] - # Format log untuk chatter try: - dt_local = parser.parse(timestamp).replace(tzinfo=None) + dt_parsed = parser.parse(timestamp) + if dt_parsed.tzinfo is None: + dt_parsed = dt_parsed.replace(tzinfo=pytz.utc) + dt_local = dt_parsed.astimezone(pytz.timezone("Asia/Jakarta")) except Exception: - dt_local = dt + dt_local = dt.astimezone(pytz.timezone("Asia/Jakarta")) desc_clean = ' '.join(description.strip().split()) - log_line = f"[TRACKING] {status} - {dt_local.strftime('%d %b %Y %H:%M')}: {desc_clean}" + log_line = f"[TRACKING] {status} - {dt_local.strftime('%d %b %Y %H:%M')}:
{desc_clean}" if not self._has_existing_log(log_line): self.with_user(15172).message_post(body=log_line) - # Apply update if updated_fields: self.write(updated_fields) _logger.info(f"[Webhook] Updated fields on picking {self.name}: {updated_fields}") -- cgit v1.2.3 From c1fa178a6afb9ef4a914dc617d2fb69da50af673 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sat, 21 Jun 2025 11:21:52 +0700 Subject: fix from and to return --- indoteknik_custom/models/tukar_guling.py | 56 +++++++++++++++++++------------- 1 file changed, 34 insertions(+), 22 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index d77ea8d4..1f5e786f 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -300,11 +300,11 @@ class TukarGuling(models.Model): ]) if not related_pickings: raise UserError( - "Tidak ditemukan BU/PICK atau BU/OUT dari SO: %s" % record.origin + "Atau masih belum Done") + "Tidak ditemukan BU/PICK atau BU/OUT dari SO: %s" % record.origin) # 2. Filter berdasarkan tipe picking - bu_pick_to_return = related_pickings.filtered(lambda p: p.picking_type_id.id == 30) # BU/PICK - bu_out_to_return = related_pickings.filtered(lambda p: p.picking_type_id.id == 29) # BU/OUT + bu_pick_to_return = related_pickings.filtered(lambda ktl: ktl.picking_type_id.id == 30) # BU/PICK + bu_out_to_return = related_pickings.filtered(lambda ktl: ktl.picking_type_id.id == 29) # BU/OUT if not bu_pick_to_return and not bu_out_to_return: raise UserError("Tidak ada BU/PICK atau BU/OUT yang selesai untuk diretur.") @@ -312,13 +312,18 @@ class TukarGuling(models.Model): created_returns = [] # Lokasi default untuk retur - bu_out_type = self.env['stock.picking.type'].browse(73) - bu_stock_type = self.env['stock.picking.type'].browse(74) + srt_type = self.env['stock.picking.type'].browse(73) + ort_type = self.env['stock.picking.type'].browse(74) - bu_out = bu_out_type.default_location_src_id.id - bu_stock = bu_out_type.default_location_dest_id.id + stock_location = self.env['stock.location'] - if not bu_out or not bu_stock: + srt_src = stock_location.browse(5) + srt_dest = stock_location.browse(60) + + ort_src = stock_location.browse(60) + ort_dest = stock_location.browse(57) + + if not ort_src or not ort_dest or not srt_src or not srt_dest: raise UserError("salahwoi") partner_location = self.env['stock.location'].search( @@ -329,14 +334,19 @@ class TukarGuling(models.Model): # Fungsi membuat retur dari picking tertentu def _create_return_from_picking(picking): - grup = self.env['stock.picking'].search([('origin', '=', self.operations.origin)]) - # Tentukan lokasi berdasarkan jenis picking + grup = self.operations.group_id + + PARTNER_LOCATION_ID = 5 # Partner Locations/Customers + BU_OUTPUT_LOCATION_ID = 60 # BU/Output (from your logs) + BU_STOCK_LOCATION_ID = 57 # BU/Stock (adjust to your actual ID) + + # Determine locations based on picking type if picking.picking_type_id.id == 29: # BU/OUT → BU/SRT - default_location_id = partner_location.id - default_location_dest_id = bu_out + default_location_id = PARTNER_LOCATION_ID # From: Partner Locations + default_location_dest_id = BU_OUTPUT_LOCATION_ID # To: BU/Output elif picking.picking_type_id.id == 30: # BU/PICK → BU/ORT - default_location_id = bu_out - default_location_dest_id = bu_stock + default_location_id = BU_OUTPUT_LOCATION_ID # From: BU/Output + default_location_dest_id = BU_STOCK_LOCATION_ID # To: BU/Stock (FIXED) else: return None @@ -353,7 +363,7 @@ class TukarGuling(models.Model): 'location_id': default_location_id, }) - # Buat return lines + # Create return lines return_lines = [] for move in picking.move_lines: if move.quantity_done > 0: @@ -370,19 +380,21 @@ class TukarGuling(models.Model): _logger.info("Creating return for picking %s", picking.name) _logger.info("Default location src: %s", default_location_id) _logger.info("Default location dest: %s", default_location_dest_id) - _logger.info("Move lines: %s", picking.move_lines) return_vals = return_wizard.create_returns() return_id = return_vals.get('res_id') + return_picking = self.env['stock.picking'].browse(return_id) - if not return_id: + if not return_picking: raise UserError("Retur gagal dibuat. Hasil create_returns: %s" % str(return_vals)) - picking_obj = self.env['stock.picking'].browse(return_id) - for p in picking_obj: - p.group_id = self.operations.group_id.id - p.tukar_guling_id = record.id + # Force the destination location (extra safeguard) + return_picking.write({ + 'location_dest_id': default_location_dest_id, + 'group_id': grup.id, + 'tukar_guling_id': record.id, + }) - return picking_obj.name + return return_picking.name # Buat return dari BU/PICK for picking in bu_pick_to_return: -- cgit v1.2.3 From 19c7a29333bd2c196a4aec2b173293da4d25e3ab Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sat, 21 Jun 2025 11:38:17 +0700 Subject: fix product not showing in detailed operations --- indoteknik_custom/models/tukar_guling.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 1f5e786f..740bb7d7 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -366,10 +366,11 @@ class TukarGuling(models.Model): # Create return lines return_lines = [] for move in picking.move_lines: - if move.quantity_done > 0: + qty = move.quantity_done or move.product_uom_qty + if qty > 0: return_lines.append((0, 0, { 'product_id': move.product_id.id, - 'quantity': move.quantity_done, + 'quantity': qty, 'move_id': move.id, })) if not return_lines: -- cgit v1.2.3 From 126774bd23276b80dd67dfb3bcdf3be633f6da86 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 21 Jun 2025 14:18:20 +0700 Subject: (andri) add user biteship live --- indoteknik_custom/models/stock_picking.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 87363fd2..7b5d98a2 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1885,7 +1885,8 @@ class StockPicking(models.Model): log_line = f"[TRACKING] {status} - {dt_local.strftime('%d %b %Y %H:%M')}:
{desc_clean}" if not self._has_existing_log(log_line): - self.with_user(15172).message_post(body=log_line) + self.with_user(15172).message_post(body=log_line) # user biteship test + # self.with_user(15710).message_post(body=log_line) # user biteship live if updated_fields: self.write(updated_fields) -- cgit v1.2.3 From f3cb0f0114b0e43bb6276e93e3636f0bf55c66b8 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 21 Jun 2025 14:20:24 +0700 Subject: (andri) Mark as Completed pada SO akan terekam di chatter --- indoteknik_custom/models/sale_orders_multi_update.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_orders_multi_update.py b/indoteknik_custom/models/sale_orders_multi_update.py index 95cfde21..962f60b5 100644 --- a/indoteknik_custom/models/sale_orders_multi_update.py +++ b/indoteknik_custom/models/sale_orders_multi_update.py @@ -11,6 +11,13 @@ class SaleOrdersMultiUpdate(models.TransientModel): sale_ids = self._context['sale_ids'] sales = self.env['sale.order'].browse(sale_ids) sales.action_multi_update_invoice_status() + + for sale in sales: + sale.message_post( + body="Sales Order has been marked as Completed", + message_type="comment" + ) + return { 'type': 'ir.actions.client', 'tag': 'display_notification', -- cgit v1.2.3 From eeb72c4ed24c33403bb733a51198b9cc0f356e6a Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 21 Jun 2025 14:57:10 +0700 Subject: (andri) rev log --- 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 6da46398..c8d4a712 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -591,11 +591,11 @@ class SaleOrder(models.Model): if shipping_option.exists(): courier_service = shipping_option.courier_service_code vals['delivery_service_type'] = courier_service - _logger.info("🛰️ Set delivery_service_type: %s from shipping_option_id: %s", courier_service, shipping_option_id) + _logger.info("Set delivery_service_type: %s from shipping_option_id: %s", courier_service, shipping_option_id) else: - _logger.warning("⚠️ shipping_option_id %s not found or invalid.", shipping_option_id) + _logger.warning("shipping_option_id %s not found or invalid.", shipping_option_id) else: - _logger.info("ℹ️ shipping_option_id not found in vals or record.") + _logger.info("shipping_option_id not found in vals or record.") # @api.model # def fields_get(self, allfields=None, attributes=None): -- cgit v1.2.3 From cbf88d93e082f30305123deb467c4c15916d9519 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sat, 21 Jun 2025 15:05:43 +0700 Subject: Revert " fix from and to return" wkwkwk This reverts commit c1fa178a6afb9ef4a914dc617d2fb69da50af673. --- indoteknik_custom/models/tukar_guling.py | 56 +++++++++++++------------------- 1 file changed, 22 insertions(+), 34 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 740bb7d7..ff52bab5 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -300,11 +300,11 @@ class TukarGuling(models.Model): ]) if not related_pickings: raise UserError( - "Tidak ditemukan BU/PICK atau BU/OUT dari SO: %s" % record.origin) + "Tidak ditemukan BU/PICK atau BU/OUT dari SO: %s" % record.origin + "Atau masih belum Done") # 2. Filter berdasarkan tipe picking - bu_pick_to_return = related_pickings.filtered(lambda ktl: ktl.picking_type_id.id == 30) # BU/PICK - bu_out_to_return = related_pickings.filtered(lambda ktl: ktl.picking_type_id.id == 29) # BU/OUT + bu_pick_to_return = related_pickings.filtered(lambda p: p.picking_type_id.id == 30) # BU/PICK + bu_out_to_return = related_pickings.filtered(lambda p: p.picking_type_id.id == 29) # BU/OUT if not bu_pick_to_return and not bu_out_to_return: raise UserError("Tidak ada BU/PICK atau BU/OUT yang selesai untuk diretur.") @@ -312,18 +312,13 @@ class TukarGuling(models.Model): created_returns = [] # Lokasi default untuk retur - srt_type = self.env['stock.picking.type'].browse(73) - ort_type = self.env['stock.picking.type'].browse(74) + bu_out_type = self.env['stock.picking.type'].browse(73) + bu_stock_type = self.env['stock.picking.type'].browse(74) - stock_location = self.env['stock.location'] + bu_out = bu_out_type.default_location_src_id.id + bu_stock = bu_out_type.default_location_dest_id.id - srt_src = stock_location.browse(5) - srt_dest = stock_location.browse(60) - - ort_src = stock_location.browse(60) - ort_dest = stock_location.browse(57) - - if not ort_src or not ort_dest or not srt_src or not srt_dest: + if not bu_out or not bu_stock: raise UserError("salahwoi") partner_location = self.env['stock.location'].search( @@ -334,19 +329,14 @@ class TukarGuling(models.Model): # Fungsi membuat retur dari picking tertentu def _create_return_from_picking(picking): - grup = self.operations.group_id - - PARTNER_LOCATION_ID = 5 # Partner Locations/Customers - BU_OUTPUT_LOCATION_ID = 60 # BU/Output (from your logs) - BU_STOCK_LOCATION_ID = 57 # BU/Stock (adjust to your actual ID) - - # Determine locations based on picking type + grup = self.env['stock.picking'].search([('origin', '=', self.operations.origin)]) + # Tentukan lokasi berdasarkan jenis picking if picking.picking_type_id.id == 29: # BU/OUT → BU/SRT - default_location_id = PARTNER_LOCATION_ID # From: Partner Locations - default_location_dest_id = BU_OUTPUT_LOCATION_ID # To: BU/Output + default_location_id = partner_location.id + default_location_dest_id = bu_out elif picking.picking_type_id.id == 30: # BU/PICK → BU/ORT - default_location_id = BU_OUTPUT_LOCATION_ID # From: BU/Output - default_location_dest_id = BU_STOCK_LOCATION_ID # To: BU/Stock (FIXED) + default_location_id = bu_out + default_location_dest_id = bu_stock else: return None @@ -363,7 +353,7 @@ class TukarGuling(models.Model): 'location_id': default_location_id, }) - # Create return lines + # Buat return lines return_lines = [] for move in picking.move_lines: qty = move.quantity_done or move.product_uom_qty @@ -381,21 +371,19 @@ class TukarGuling(models.Model): _logger.info("Creating return for picking %s", picking.name) _logger.info("Default location src: %s", default_location_id) _logger.info("Default location dest: %s", default_location_dest_id) + _logger.info("Move lines: %s", picking.move_lines) return_vals = return_wizard.create_returns() return_id = return_vals.get('res_id') - return_picking = self.env['stock.picking'].browse(return_id) - if not return_picking: + if not return_id: raise UserError("Retur gagal dibuat. Hasil create_returns: %s" % str(return_vals)) - # Force the destination location (extra safeguard) - return_picking.write({ - 'location_dest_id': default_location_dest_id, - 'group_id': grup.id, - 'tukar_guling_id': record.id, - }) + picking_obj = self.env['stock.picking'].browse(return_id) + for p in picking_obj: + p.group_id = self.operations.group_id.id + p.tukar_guling_id = record.id - return return_picking.name + return picking_obj.name # Buat return dari BU/PICK for picking in bu_pick_to_return: -- cgit v1.2.3 From ba4b74c9dc301dec73217bb6c35bb78ab7f41fb3 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sat, 21 Jun 2025 15:29:50 +0700 Subject: re fix location --- indoteknik_custom/models/tukar_guling.py | 61 +++++++++++++++++++------------- 1 file changed, 37 insertions(+), 24 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index ff52bab5..da1cfcf4 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -1,4 +1,4 @@ -from odoo import models, fields, api +from odoo import models, fields, api, _ from odoo.exceptions import UserError, ValidationError import logging @@ -20,6 +20,7 @@ class TukarGuling(models.Model): 'tukar_guling_id', string='Transfers' ) + # origin_so = fields.Many2one('sale.order', string='Origin SO') name = fields.Char('Number', required=True, copy=False, readonly=True, default='New') date = fields.Datetime('Date', default=fields.Datetime.now, required=True) operations = fields.Many2one('stock.picking', 'Operations', @@ -300,11 +301,11 @@ class TukarGuling(models.Model): ]) if not related_pickings: raise UserError( - "Tidak ditemukan BU/PICK atau BU/OUT dari SO: %s" % record.origin + "Atau masih belum Done") + "Tidak ditemukan BU/PICK atau BU/OUT dari SO: %s" % record.origin) # 2. Filter berdasarkan tipe picking - bu_pick_to_return = related_pickings.filtered(lambda p: p.picking_type_id.id == 30) # BU/PICK - bu_out_to_return = related_pickings.filtered(lambda p: p.picking_type_id.id == 29) # BU/OUT + bu_pick_to_return = related_pickings.filtered(lambda ktl: ktl.picking_type_id.id == 30) # BU/PICK + bu_out_to_return = related_pickings.filtered(lambda ktl: ktl.picking_type_id.id == 29) # BU/OUT if not bu_pick_to_return and not bu_out_to_return: raise UserError("Tidak ada BU/PICK atau BU/OUT yang selesai untuk diretur.") @@ -312,13 +313,18 @@ class TukarGuling(models.Model): created_returns = [] # Lokasi default untuk retur - bu_out_type = self.env['stock.picking.type'].browse(73) - bu_stock_type = self.env['stock.picking.type'].browse(74) + srt_type = self.env['stock.picking.type'].browse(73) + ort_type = self.env['stock.picking.type'].browse(74) - bu_out = bu_out_type.default_location_src_id.id - bu_stock = bu_out_type.default_location_dest_id.id + stock_location = self.env['stock.location'] - if not bu_out or not bu_stock: + srt_src = stock_location.browse(5) + srt_dest = stock_location.browse(60) + + ort_src = stock_location.browse(60) + ort_dest = stock_location.browse(57) + + if not ort_src or not ort_dest or not srt_src or not srt_dest: raise UserError("salahwoi") partner_location = self.env['stock.location'].search( @@ -329,14 +335,19 @@ class TukarGuling(models.Model): # Fungsi membuat retur dari picking tertentu def _create_return_from_picking(picking): - grup = self.env['stock.picking'].search([('origin', '=', self.operations.origin)]) - # Tentukan lokasi berdasarkan jenis picking + grup = self.operations.group_id + + PARTNER_LOCATION_ID = 5 # Partner Locations/Customers + BU_OUTPUT_LOCATION_ID = 60 # BU/Output (from your logs) + BU_STOCK_LOCATION_ID = 57 # BU/Stock (adjust to your actual ID) + + # Determine locations based on picking type if picking.picking_type_id.id == 29: # BU/OUT → BU/SRT - default_location_id = partner_location.id - default_location_dest_id = bu_out + default_location_id = PARTNER_LOCATION_ID # From: Partner Locations + default_location_dest_id = BU_OUTPUT_LOCATION_ID # To: BU/Output elif picking.picking_type_id.id == 30: # BU/PICK → BU/ORT - default_location_id = bu_out - default_location_dest_id = bu_stock + default_location_id = BU_OUTPUT_LOCATION_ID # From: BU/Output + default_location_dest_id = BU_STOCK_LOCATION_ID # To: BU/Stock (FIXED) else: return None @@ -353,7 +364,7 @@ class TukarGuling(models.Model): 'location_id': default_location_id, }) - # Buat return lines + # Create return lines return_lines = [] for move in picking.move_lines: qty = move.quantity_done or move.product_uom_qty @@ -371,19 +382,21 @@ class TukarGuling(models.Model): _logger.info("Creating return for picking %s", picking.name) _logger.info("Default location src: %s", default_location_id) _logger.info("Default location dest: %s", default_location_dest_id) - _logger.info("Move lines: %s", picking.move_lines) return_vals = return_wizard.create_returns() return_id = return_vals.get('res_id') + return_picking = self.env['stock.picking'].browse(return_id) - if not return_id: + if not return_picking: raise UserError("Retur gagal dibuat. Hasil create_returns: %s" % str(return_vals)) - picking_obj = self.env['stock.picking'].browse(return_id) - for p in picking_obj: - p.group_id = self.operations.group_id.id - p.tukar_guling_id = record.id + # Force the destination location (extra safeguard) + return_picking.write({ + 'location_dest_id': default_location_dest_id, + 'group_id': grup.id, + 'tukar_guling_id': record.id, + }) - return picking_obj.name + return return_picking.name # Buat return dari BU/PICK for picking in bu_pick_to_return: @@ -439,4 +452,4 @@ class TukarGulingLine(models.Model): class StockPicking(models.Model): _inherit = 'stock.picking' - tukar_guling_id = fields.Many2one('tukar.guling', string='Tukar Guling Ref') + tukar_guling_id = fields.Many2one('tukar.guling', string='Tukar Guling Ref') \ No newline at end of file -- cgit v1.2.3 From 20f206f3d9b798fee50a06d4a462cf256a71d58e Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 21 Jun 2025 15:49:35 +0700 Subject: (andri) penambahan status tracking --- indoteknik_custom/models/stock_picking.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 7b5d98a2..d4167609 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1820,9 +1820,7 @@ class StockPicking(models.Model): } def _get_biteship_status_description(self, status, data=None): - data = data or {} - courier = data.get("courier", {}).get("company", "") contact_name = data.get("destination", {}).get("contact_name", "") @@ -1831,9 +1829,14 @@ class StockPicking(models.Model): 'allocated': 'Kurir akan melakukan pick-up pesanan', 'picking_up': 'Kurir sedang dalam perjalanan menuju lokasi pick-up', 'picked': f'Pesanan sudah di pick-up kurir {courier}', - 'on_hold': 'Pesanan ditahan sementara karena masalah pengiriman', 'dropping_off': 'Kurir sudah ditugaskan dan pesanan akan segera diantar ke pembeli', 'delivered': f'Pesanan telah sampai dan diterima oleh {contact_name}', + 'return_in_transit': 'Pesanan dalam perjalanan kembali ke pengirim', + 'on_hold': 'Pesanan ditahan sementara karena masalah pengiriman', + 'rejected': 'Pesanan ditolak, silakan hubungi Biteship', + 'courier_not_found': 'Pesanan dibatalkan karena tidak ada kurir tersedia', + 'returned': 'Pesanan berhasil dikembalikan', + 'disposed': 'Pesanan sudah dimusnahkan', 'cancelled': 'Pesanan dibatalkan oleh sistem atau pengguna', } @@ -1941,10 +1944,15 @@ class StockPicking(models.Model): "allocated": "pending", "picking_up": "pending", "picked": "shipment", - "cancelled": "cancelled", - "on_hold": "on_hold", "dropping_off": "shipment", - "delivered": "completed" + "delivered": "completed", + "return_in_transit": "returning", + "on_hold": "on_hold", + "rejected": "cancelled", + "courier_not_found": "cancelled", + "returned": "returned", + "disposed": "disposed", + "cancelled": "cancelled" } return status_mapping.get(status, "Hubungi Admin") -- cgit v1.2.3 From 1be3cacacce54b6fe71eb3786d152c1d18707724 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sat, 21 Jun 2025 16:07:48 +0700 Subject: re fix location --- indoteknik_custom/models/tukar_guling.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index da1cfcf4..5c99bc18 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -337,17 +337,17 @@ class TukarGuling(models.Model): def _create_return_from_picking(picking): grup = self.operations.group_id - PARTNER_LOCATION_ID = 5 # Partner Locations/Customers - BU_OUTPUT_LOCATION_ID = 60 # BU/Output (from your logs) - BU_STOCK_LOCATION_ID = 57 # BU/Stock (adjust to your actual ID) + PARTNER_LOCATION_ID = 5 + BU_OUTPUT_LOCATION_ID = 60 + BU_STOCK_LOCATION_ID = 57 # Determine locations based on picking type - if picking.picking_type_id.id == 29: # BU/OUT → BU/SRT - default_location_id = PARTNER_LOCATION_ID # From: Partner Locations - default_location_dest_id = BU_OUTPUT_LOCATION_ID # To: BU/Output - elif picking.picking_type_id.id == 30: # BU/PICK → BU/ORT - default_location_id = BU_OUTPUT_LOCATION_ID # From: BU/Output - default_location_dest_id = BU_STOCK_LOCATION_ID # To: BU/Stock (FIXED) + if picking.picking_type_id.id == 29: + default_location_id = PARTNER_LOCATION_ID + default_location_dest_id = BU_OUTPUT_LOCATION_ID + elif picking.picking_type_id.id == 30: + default_location_id = BU_OUTPUT_LOCATION_ID + default_location_dest_id = BU_STOCK_LOCATION_ID else: return None @@ -389,7 +389,7 @@ class TukarGuling(models.Model): if not return_picking: raise UserError("Retur gagal dibuat. Hasil create_returns: %s" % str(return_vals)) - # Force the destination location (extra safeguard) + # Force the destination location return_picking.write({ 'location_dest_id': default_location_dest_id, 'group_id': grup.id, -- cgit v1.2.3 From 680748ae128a90b9999acff60c770e2472c7fcbe Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sat, 21 Jun 2025 20:52:32 +0700 Subject: it should be done --- indoteknik_custom/models/tukar_guling.py | 37 +++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 8 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 5c99bc18..a7b6e07e 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -315,6 +315,8 @@ class TukarGuling(models.Model): # Lokasi default untuk retur srt_type = self.env['stock.picking.type'].browse(73) ort_type = self.env['stock.picking.type'].browse(74) + bu_pick_type = self.env['stock.picking.type'].browse(30) + bu_out_type = self.env['stock.picking.type'].browse(29) stock_location = self.env['stock.location'] @@ -342,12 +344,22 @@ class TukarGuling(models.Model): BU_STOCK_LOCATION_ID = 57 # Determine locations based on picking type - if picking.picking_type_id.id == 29: + if picking.picking_type_id.id == 30: # -> ngeretur bu pick + return_type = srt_type + default_location_id = BU_OUTPUT_LOCATION_ID + default_location_dest_id = BU_STOCK_LOCATION_ID + elif picking.picking_type_id.id == 29: # -> ngeretur bu out + return_type = ort_type default_location_id = PARTNER_LOCATION_ID default_location_dest_id = BU_OUTPUT_LOCATION_ID - elif picking.picking_type_id.id == 30: + elif picking.picking_type_id.id == 74: # -> ngeretur srt + return_type = bu_pick_type + default_location_id = BU_STOCK_LOCATION_ID + default_location_dest_id = BU_OUTPUT_LOCATION_ID + elif picking.picking_type_id.id == 73: # -> ngeretur ort + return_type = bu_out_type default_location_id = BU_OUTPUT_LOCATION_ID - default_location_dest_id = BU_STOCK_LOCATION_ID + default_location_dest_id = PARTNER_LOCATION_ID else: return None @@ -396,20 +408,29 @@ class TukarGuling(models.Model): 'tukar_guling_id': record.id, }) - return return_picking.name + return return_picking - # Buat return dari BU/PICK - for picking in bu_pick_to_return: + # Buat return dari BU/OUT + for picking in bu_out_to_return: name = _create_return_from_picking(picking) if name: created_returns.append(name) - # Buat return dari BU/OUT - for picking in bu_out_to_return: + # Buat return dari BU/PICK + for picking in bu_pick_to_return: name = _create_return_from_picking(picking) if name: created_returns.append(name) + # Buat return dari SRT + if record.return_type == 'tukar_guling': + target = [woi for woi in created_returns if woi.picking_type_id.id in (73, 74)] + for picking in target: + retur = _create_return_from_picking(picking) + if retur: + created_returns.append(retur) + + if not created_returns: raise UserError("wkwkwk") -- cgit v1.2.3 From 39e570854fa72d673bd37fc6582861bf1fc49aa7 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sun, 22 Jun 2025 21:09:36 +0700 Subject: should be done --- indoteknik_custom/models/tukar_guling.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index a7b6e07e..456a2111 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -303,7 +303,7 @@ class TukarGuling(models.Model): raise UserError( "Tidak ditemukan BU/PICK atau BU/OUT dari SO: %s" % record.origin) - # 2. Filter berdasarkan tipe picking + # filter based on stockin.picking picking type bu_pick_to_return = related_pickings.filtered(lambda ktl: ktl.picking_type_id.id == 30) # BU/PICK bu_out_to_return = related_pickings.filtered(lambda ktl: ktl.picking_type_id.id == 29) # BU/OUT @@ -345,11 +345,11 @@ class TukarGuling(models.Model): # Determine locations based on picking type if picking.picking_type_id.id == 30: # -> ngeretur bu pick - return_type = srt_type + return_type = ort_type default_location_id = BU_OUTPUT_LOCATION_ID default_location_dest_id = BU_STOCK_LOCATION_ID elif picking.picking_type_id.id == 29: # -> ngeretur bu out - return_type = ort_type + return_type = srt_type default_location_id = PARTNER_LOCATION_ID default_location_dest_id = BU_OUTPUT_LOCATION_ID elif picking.picking_type_id.id == 74: # -> ngeretur srt @@ -373,7 +373,8 @@ class TukarGuling(models.Model): return_wizard = self.env['stock.return.picking'].with_context(return_context).create({ 'picking_id': picking.id, - 'location_id': default_location_id, + 'location_id': default_location_dest_id, + 'original_location_id': default_location_id }) # Create return lines @@ -404,6 +405,7 @@ class TukarGuling(models.Model): # Force the destination location return_picking.write({ 'location_dest_id': default_location_dest_id, + 'location_id': default_location_id, 'group_id': grup.id, 'tukar_guling_id': record.id, }) @@ -422,9 +424,9 @@ class TukarGuling(models.Model): if name: created_returns.append(name) - # Buat return dari SRT + # Buat return dari SRT dan ort if record.return_type == 'tukar_guling': - target = [woi for woi in created_returns if woi.picking_type_id.id in (73, 74)] + target = [woi for woi in created_returns if woi.picking_type_id.id in (74, 73)] for picking in target: retur = _create_return_from_picking(picking) if retur: -- cgit v1.2.3 From 6ae8ac2d0a560d850e7e8dc2ce87157f3e5a9669 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 23 Jun 2025 10:10:51 +0700 Subject: (andri) menu commision customer menjadi commision benefits --- indoteknik_custom/models/commision.py | 1 + 1 file changed, 1 insertion(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py index 46718397..199aa106 100644 --- a/indoteknik_custom/models/commision.py +++ b/indoteknik_custom/models/commision.py @@ -148,6 +148,7 @@ class CustomerCommision(models.Model): _order = 'id desc' _inherit = ['mail.thread'] _rec_name = 'number' + _description = 'Customer Benefits' number = fields.Char(string='Document No', index=True, copy=False, readonly=True) date_from = fields.Date(string='Date From', required=True) -- cgit v1.2.3 From f0f3e9d142dd435c9740f6d6c439e1b400a36c45 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 23 Jun 2025 15:28:01 +0700 Subject: fix qty --- indoteknik_custom/models/tukar_guling.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 456a2111..dfa62dc8 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -202,7 +202,7 @@ class TukarGuling(models.Model): if sequence: default['name'] = sequence.next_by_id() else: - default['name'] = self.env['ir.sequence'].next_by_code('tukar.guling') or 'PTG-COPY' + default['name'] = self.env['ir.sequence'].next_by_code('tukar.guling') or 'copy' default.update({ 'state': 'draft', @@ -312,6 +312,7 @@ class TukarGuling(models.Model): created_returns = [] + # Lokasi default untuk retur srt_type = self.env['stock.picking.type'].browse(73) ort_type = self.env['stock.picking.type'].browse(74) @@ -329,12 +330,6 @@ class TukarGuling(models.Model): if not ort_src or not ort_dest or not srt_src or not srt_dest: raise UserError("salahwoi") - partner_location = self.env['stock.location'].search( - [('complete_name', 'ilike', 'Partner Locations/Customers'), - ('id', '=', '5')]) - if not partner_location: - raise UserError("Lokasi partner salah atau tidak ditemukan pada BU/OUT.") - # Fungsi membuat retur dari picking tertentu def _create_return_from_picking(picking): grup = self.operations.group_id @@ -348,19 +343,28 @@ class TukarGuling(models.Model): return_type = ort_type default_location_id = BU_OUTPUT_LOCATION_ID default_location_dest_id = BU_STOCK_LOCATION_ID + if not default_location_id or not default_location_dest_id: + raise UserError("Lokasi Origin atau Destination salah.") elif picking.picking_type_id.id == 29: # -> ngeretur bu out return_type = srt_type default_location_id = PARTNER_LOCATION_ID default_location_dest_id = BU_OUTPUT_LOCATION_ID + if not default_location_id or not default_location_dest_id: + raise UserError("Lokasi Origin atau Destination salah.") elif picking.picking_type_id.id == 74: # -> ngeretur srt return_type = bu_pick_type default_location_id = BU_STOCK_LOCATION_ID default_location_dest_id = BU_OUTPUT_LOCATION_ID + if not default_location_id or not default_location_dest_id: + raise UserError("Lokasi Origin atau Destination salah.") elif picking.picking_type_id.id == 73: # -> ngeretur ort return_type = bu_out_type default_location_id = BU_OUTPUT_LOCATION_ID default_location_dest_id = PARTNER_LOCATION_ID + if not default_location_id or not default_location_dest_id: + raise UserError("Lokasi Origin atau Destination salah.") else: + raise UserError("Hayo") return None return_context = dict(self.env.context) @@ -379,12 +383,12 @@ class TukarGuling(models.Model): # Create return lines return_lines = [] - for move in picking.move_lines: - qty = move.quantity_done or move.product_uom_qty - if qty > 0: + for line in record.line_ids: + move = picking.move_lines.filtered(lambda wkwk: wkwk.product_id == line.product_id) + if move: return_lines.append((0, 0, { - 'product_id': move.product_id.id, - 'quantity': qty, + 'product_id': line.product_id.id, + 'quantity': line.product_uom_qty, 'move_id': move.id, })) if not return_lines: -- cgit v1.2.3 From af4ee6eef06a440207c6d16809d06e08057f66f0 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Fri, 20 Jun 2025 09:57:29 +0700 Subject: bu pick in purchase order sales match --- indoteknik_custom/models/purchase_order_sales_match.py | 14 ++++++++++++++ indoteknik_custom/models/stock_picking.py | 4 ++++ 2 files changed, 18 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/purchase_order_sales_match.py b/indoteknik_custom/models/purchase_order_sales_match.py index 0bd0092b..2ea89dab 100644 --- a/indoteknik_custom/models/purchase_order_sales_match.py +++ b/indoteknik_custom/models/purchase_order_sales_match.py @@ -28,6 +28,20 @@ class PurchaseOrderSalesMatch(models.Model): purchase_price_po = fields.Float('Purchase Price PO', compute='_compute_purchase_price_po') purchase_line_id = fields.Many2one('purchase.order.line', string='Purchase Line', compute='_compute_purchase_line_id') hold_outgoing_so = fields.Boolean(string='Hold Outgoing SO', related='sale_id.hold_outgoing') + bu_pick = fields.Many2one('stock.picking', string='BU Pick', compute='compute_bu_pick') + + def compute_bu_pick(self): + for rec in self: + stock_move = self.env['stock.move'].search([ + ('reference', 'ilike', 'BU/PICK'), + ('state', 'in', ['confirmed','waiting','partially_available']), + ('product_id', '=', rec.product_id.id), + ('sale_line_id', '=', rec.sale_line_id.id), + ]) + if stock_move: + rec.bu_pick = stock_move.picking_id.id + else: + rec.bu_pick = '' def _compute_purchase_line_id(self): for line in self: diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index eabef37c..7c4e6bc0 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1309,6 +1309,7 @@ class StockPicking(models.Model): self.final_seq = 0 self.set_picking_code_out() self.send_koli_to_so() + self.automatic_reserve_product() if (self.state_reserve == 'done' and self.picking_type_code == 'internal' and 'BU/PICK/' in self.name and self.linked_manual_bu_out): @@ -1347,6 +1348,9 @@ class StockPicking(models.Model): } self.send_mail_bills() return res + + # def automatic_reserve_product(self): + # if self.name def check_invoice_date(self): for picking in self: -- cgit v1.2.3 From a6aa700b5016c98d579a52125e3686acc615ce88 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Mon, 23 Jun 2025 16:29:02 +0700 Subject: trial automatic reserve and change qty purchase stock --- indoteknik_custom/models/automatic_purchase.py | 2 +- indoteknik_custom/models/stock_picking.py | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/automatic_purchase.py b/indoteknik_custom/models/automatic_purchase.py index c9edf07c..83a7cb3c 100644 --- a/indoteknik_custom/models/automatic_purchase.py +++ b/indoteknik_custom/models/automatic_purchase.py @@ -486,7 +486,7 @@ class AutomaticPurchase(models.Model): # _logger.info('test %s' % point.product_id.name) if point.product_id.qty_available_bandengan > point.product_min_qty: continue - qty_purchase = point.product_max_qty - point.product_id.qty_incoming_bandengan - point.product_id.qty_onhand_bandengan + qty_purchase = point.product_max_qty - point.product_id.qty_incoming_bandengan - point.product_id.qty_available_bandengan po_line = self.env['purchase.order.line'].search([('product_id', '=', point.product_id.id), ('order_id.state', '=', 'done')], order='id desc', limit=1) if self.vendor_id: diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 7c4e6bc0..27046063 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1309,8 +1309,6 @@ class StockPicking(models.Model): self.final_seq = 0 self.set_picking_code_out() self.send_koli_to_so() - self.automatic_reserve_product() - if (self.state_reserve == 'done' and self.picking_type_code == 'internal' and 'BU/PICK/' in self.name and self.linked_manual_bu_out): if not self.linked_manual_bu_out.date_reserved: @@ -1347,10 +1345,18 @@ class StockPicking(models.Model): 'target': 'new', } self.send_mail_bills() + if 'BU/PUT' in self.name: + self.automatic_reserve_product() return res - # def automatic_reserve_product(self): - # if self.name + def automatic_reserve_product(self): + if self.state == 'done': + po = self.env['purchase.order'].search([ + ('name', '=', self.group_id.name) + ]) + + for line in po.order_sales_match_line: + line.bu_pick.action_assign() def check_invoice_date(self): for picking in self: -- cgit v1.2.3 From 0d95dabc04a3f5334168e989705e9a7568130df4 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Tue, 24 Jun 2025 08:41:47 +0700 Subject: spesific tf --- indoteknik_custom/models/tukar_guling.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index dfa62dc8..d75dad41 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -25,6 +25,7 @@ class TukarGuling(models.Model): date = fields.Datetime('Date', default=fields.Datetime.now, required=True) operations = fields.Many2one('stock.picking', 'Operations', domain=[('picking_type_id.code', '=', 'outgoing')], help='Nomor BU/Out atau BU/Pick') + spesific_operations = fields.Many2one('stock.picking', 'Spesific Operations', domain = [('origin', '=', origin), ('state', '=', 'done')]) ba_num = fields.Text('Nomor BA') notes = fields.Text('Notes') return_type = fields.Selection(String='Return Type', selection=[ @@ -44,6 +45,10 @@ class TukarGuling(models.Model): @api.onchange('operations') def _onchange_operations(self): """Auto-populate lines ketika operations dipilih""" + if self.origin: + return {'domain': { + 'specific_transfers': [('origin', '=', self.origin), ('state', '=', 'done')] + }} if self.operations: from_return_picking = self.env.context.get('from_return_picking', False) or \ self.env.context.get('default_line_ids', False) @@ -294,7 +299,6 @@ class TukarGuling(models.Model): operation_picking = record.operations - # 1. Cari semua picking DONE berdasarkan origin SO related_pickings = self.env['stock.picking'].search([ ('origin', '=', record.origin), ('state', '=', 'done'), @@ -391,6 +395,8 @@ class TukarGuling(models.Model): 'quantity': line.product_uom_qty, 'move_id': move.id, })) + if not move: + raise UserError("eror woi") if not return_lines: return None -- cgit v1.2.3 From 05283d3ec0c49449e2ed7b14b2d824739db19174 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 24 Jun 2025 09:43:15 +0700 Subject: (andri) fix date reserved di SO --- 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 c8d4a712..c1cdf2ed 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -382,13 +382,13 @@ class SaleOrder(models.Model): # Simpan ke field sebagai UTC-naive datetime (standar Odoo) order.et_products = eta_datetime.astimezone(pytz.utc).replace(tzinfo=None) - @api.depends('picking_ids.state', 'picking_ids.date_reserved') + @api.depends('picking_ids.state', 'picking_ids.date_done') def _compute_eta_date_reserved(self): for order in self: pickings = order.picking_ids.filtered( - lambda p: p.state == 'assigned' and p.date_reserved and 'BU/PICK/' in (p.name or '') + lambda p: p.state in ('assigned', 'done') and p.date_reserved and 'BU/PICK/' in (p.name or '') ) - order.eta_date_reserved = min(pickings.mapped('date_reserved')) if pickings else False + order.eta_date_reserved = min(pickings.mapped('date_done')) if pickings else False @api.onchange('shipping_cost_covered') def _onchange_shipping_cost_covered(self): -- cgit v1.2.3 From 6a913c0025c64903536fa6c9aeda526b609d27e6 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 24 Jun 2025 11:23:36 +0700 Subject: change request coretax down payment --- indoteknik_custom/models/account_move.py | 3 +- indoteknik_custom/models/coretax_fatur.py | 83 +++++++++++++++++++++---------- indoteknik_custom/models/sale_order.py | 1 + 3 files changed, 61 insertions(+), 26 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 66020a69..ece47236 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -70,6 +70,7 @@ class AccountMove(models.Model): reklas_misc_id = fields.Many2one('account.move', string='Journal Entries Reklas') # Di model account.move bill_id = fields.Many2one('account.move', string='Vendor Bill', domain=[('move_type', '=', 'in_invoice')], help='Bill asal dari proses reklas ini') + down_payment = fields.Boolean('Down Payments?') # def name_get(self): @@ -441,7 +442,7 @@ class AccountMove(models.Model): # Panggil model coretax.faktur untuk menghasilkan XML coretax_faktur = self.env['coretax.faktur'].create({}) - response = coretax_faktur.export_to_download(invoices=valid_invoices) + response = coretax_faktur.export_to_download(invoices=valid_invoices, down_payments=valid_invoices.down_payment) current_time = datetime.utcnow() # Tandai faktur sebagai sudah diekspor diff --git a/indoteknik_custom/models/coretax_fatur.py b/indoteknik_custom/models/coretax_fatur.py index 92ff1a72..54eb0f8e 100644 --- a/indoteknik_custom/models/coretax_fatur.py +++ b/indoteknik_custom/models/coretax_fatur.py @@ -32,7 +32,7 @@ class CoretaxFaktur(models.Model): return cleaned_number - def generate_xml(self, invoices=None): + def generate_xml(self, invoices=None, down_payments=False): # Buat root XML root = ET.Element('TaxInvoiceBulk', { 'xmlns:xsi': "http://www.w3.org/2001/XMLSchema-instance", @@ -72,42 +72,76 @@ class CoretaxFaktur(models.Model): ET.SubElement(tax_invoice, 'BuyerEmail').text = invoice.partner_id.email or '' ET.SubElement(tax_invoice, 'BuyerIDTKU').text = buyerIDTKU - # Filter product - product_lines = invoice.invoice_line_ids.filtered( - lambda l: not l.display_type and hasattr(l, 'account_id') and - l.account_id and l.product_id and - l.account_id.id != self.DISCOUNT_ACCOUNT_ID and - l.quantity != -1 - ) + # Handle product lines based on down_payments flag + if down_payments and invoice.invoice_origin: + # Get from sale.order.line for down payment + sale_order = invoice.sale_id + if sale_order: + product_lines = sale_order.order_line.filtered( + lambda l: l.product_id and not l.is_downpayment and not l.display_type and not l.product_id.id == 229625 + ) + # Convert sale order lines to invoice-like format + converted_lines = [] + for line in product_lines: + converted_lines.append({ + 'name': line.name, + 'product_id': line.product_id, + 'price_subtotal': line.price_subtotal, + 'quantity': line.product_uom_qty, + 'price_unit': line.price_unit, + 'account_id': line.order_id.analytic_account_id or False, + }) + product_lines = converted_lines + else: + product_lines = [] + else: + # Normal case - get from invoice lines + product_lines = invoice.invoice_line_ids.filtered( + lambda l: not l.display_type and hasattr(l, 'account_id') and + l.account_id and l.product_id and + l.account_id.id != self.DISCOUNT_ACCOUNT_ID and + l.quantity != -1 + ) - # Filter discount + # Filter discount (always from invoice) discount_lines = invoice.invoice_line_ids.filtered( lambda l: not l.display_type and ( - (hasattr(l, 'account_id') and l.account_id and - l.account_id.id == self.DISCOUNT_ACCOUNT_ID) or - (l.quantity == -1) + (hasattr(l, 'account_id') and l.account_id and + l.account_id.id == self.DISCOUNT_ACCOUNT_ID) or + (l.quantity == -1) ) ) - # Calculate total product amount (before discount) - total_product_amount = sum(line.price_subtotal for line in product_lines) + # Calculate totals + total_product_amount = sum(line.get('price_subtotal', 0) if isinstance(line, dict) + else line.price_subtotal for line in product_lines) if total_product_amount == 0: total_product_amount = 1 # Avoid division by zero - # Calculate total discount amount total_discount_amount = abs(sum(line.price_subtotal for line in discount_lines)) # Tambahkan elemen ListOfGoodService list_of_good_service = ET.SubElement(tax_invoice, 'ListOfGoodService') for line in product_lines: + # Handle both dict (converted sale lines) and normal invoice lines + if isinstance(line, dict): + line_price_subtotal = line['price_subtotal'] + line_quantity = line['quantity'] + line_name = line['name'] + line_price_unit = line['price_unit'] + else: + line_price_subtotal = line.price_subtotal + line_quantity = line.quantity + line_name = line.name + line_price_unit = line.price_unit + # Calculate prorated discount - line_proportion = line.price_subtotal / total_product_amount + line_proportion = line_price_subtotal / total_product_amount line_discount = total_discount_amount * line_proportion - # unit_price = line.price_unit - subtotal = line.price_subtotal - quantity = line.quantity + subtotal = line_price_subtotal + quantity = line_quantity total_discount = round(line_discount, 2) # Calculate other tax values @@ -118,13 +152,12 @@ class CoretaxFaktur(models.Model): good_service = ET.SubElement(list_of_good_service, 'GoodService') ET.SubElement(good_service, 'Opt').text = 'A' ET.SubElement(good_service, 'Code').text = '000000' - ET.SubElement(good_service, 'Name').text = line.name + ET.SubElement(good_service, 'Name').text = line_name ET.SubElement(good_service, 'Unit').text = 'UM.0018' - ET.SubElement(good_service, 'Price').text = str(round(subtotal / quantity, 2)) if subtotal else '0' + ET.SubElement(good_service, 'Price').text = str(round(line_price_unit, 2)) if line_price_unit else '0' ET.SubElement(good_service, 'Qty').text = str(quantity) ET.SubElement(good_service, 'TotalDiscount').text = str(total_discount) - ET.SubElement(good_service, 'TaxBase').text = str( - round(subtotal)) if subtotal else '0' + ET.SubElement(good_service, 'TaxBase').text = str(round(subtotal)) if subtotal else '0' ET.SubElement(good_service, 'OtherTaxBase').text = str(otherTaxBase) ET.SubElement(good_service, 'VATRate').text = '12' ET.SubElement(good_service, 'VAT').text = str(vat_amount) @@ -136,9 +169,9 @@ class CoretaxFaktur(models.Model): xml_pretty = minidom.parseString(xml_str).toprettyxml(indent=" ") return xml_pretty - def export_to_download(self, invoices): + def export_to_download(self, invoices, down_payments): # Generate XML content - xml_content = self.generate_xml(invoices) + xml_content = self.generate_xml(invoices, down_payments) # Encode content to Base64 xml_encoded = base64.b64encode(xml_content.encode('utf-8')) diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index c1cdf2ed..85228901 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -1592,6 +1592,7 @@ class SaleOrder(models.Model): 'campaign_id': self.campaign_id.id, 'medium_id': self.medium_id.id, 'source_id': self.source_id.id, + 'down_payment': 229625 in [line.product_id.id for line in self.order_line], 'user_id': self.user_id.id, 'sale_id': self.id, 'invoice_user_id': self.user_id.id, -- cgit v1.2.3 From e1e281f6f43b9ba22443845484422cd0c5b1fb30 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 24 Jun 2025 12:08:57 +0700 Subject: (andri) penambahan field pada respartner --- indoteknik_custom/models/res_partner.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index 9986b9c0..98aac7eb 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -165,6 +165,23 @@ class ResPartner(models.Model): "this feature", tracking=3) telegram_id = fields.Char(string="Telegram") + avg_aging= fields.Float(string='Average Aging') + payment_difficulty = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', tracking=3, compute='_compute_payment_difficulty', inverse='_inverse_payment_difficulty', store=True) + payment_history_url = fields.Text(string='Payment History URL') + + @api.depends('parent_id.payment_difficulty') + def _compute_payment_difficulty(self): + for partner in self: + if partner.parent_id: + partner.payment_difficulty = partner.parent_id.payment_difficulty + + def _inverse_payment_difficulty(self): + for partner in self: + if not partner.parent_id: + partner.child_ids.write({ + 'payment_difficulty': partner.payment_difficulty + }) + @api.model def _default_payment_term(self): return self.env.ref('__export__.account_payment_term_26_484409e2').id -- cgit v1.2.3 From e921762879216dbe8df4e36dfa294ae4ccf293ee Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 24 Jun 2025 13:39:12 +0700 Subject: (andri) fix --- indoteknik_custom/models/res_partner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index 98aac7eb..d23ab824 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -166,7 +166,7 @@ class ResPartner(models.Model): telegram_id = fields.Char(string="Telegram") avg_aging= fields.Float(string='Average Aging') - payment_difficulty = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', tracking=3, compute='_compute_payment_difficulty', inverse='_inverse_payment_difficulty', store=True) + payment_difficulty = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', tracking=3, compute='_compute_payment_difficulty', inverse='_inverse_payment_difficulty') payment_history_url = fields.Text(string='Payment History URL') @api.depends('parent_id.payment_difficulty') -- cgit v1.2.3 From a7139584557cc8c0d881bafc25b4cbbdb1c93ffc Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 24 Jun 2025 13:59:07 +0700 Subject: (andri) ganti compute field menjadi field biasa --- indoteknik_custom/models/res_partner.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index d23ab824..d439c7a8 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -164,23 +164,22 @@ class ResPartner(models.Model): "Set its value to 0.00 to disable " "this feature", tracking=3) telegram_id = fields.Char(string="Telegram") - avg_aging= fields.Float(string='Average Aging') - payment_difficulty = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', tracking=3, compute='_compute_payment_difficulty', inverse='_inverse_payment_difficulty') + payment_difficulty = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', tracking=3) payment_history_url = fields.Text(string='Payment History URL') - @api.depends('parent_id.payment_difficulty') - def _compute_payment_difficulty(self): - for partner in self: - if partner.parent_id: - partner.payment_difficulty = partner.parent_id.payment_difficulty + # @api.depends('parent_id.payment_difficulty') + # def _compute_payment_difficulty(self): + # for partner in self: + # if partner.parent_id: + # partner.payment_difficulty = partner.parent_id.payment_difficulty - def _inverse_payment_difficulty(self): - for partner in self: - if not partner.parent_id: - partner.child_ids.write({ - 'payment_difficulty': partner.payment_difficulty - }) + # def _inverse_payment_difficulty(self): + # for partner in self: + # if not partner.parent_id: + # partner.child_ids.write({ + # 'payment_difficulty': partner.payment_difficulty + # }) @api.model def _default_payment_term(self): @@ -209,6 +208,10 @@ class ResPartner(models.Model): for rec in self: if 'latitude' in vals or 'longtitude' in vals: rec._update_address_from_coords() + + # Sinkronisasi payment_difficulty ke semua anak jika partner ini adalah parent + if not rec.parent_id and 'payment_difficulty' in vals: + rec.child_ids.write({'payment_difficulty': vals['payment_difficulty']}) # # # if 'property_payment_term_id' in vals: # # if not self.env.user.is_accounting and vals['property_payment_term_id'] != 26: @@ -227,6 +230,8 @@ class ResPartner(models.Model): for rec in records: if vals.get('latitude') and vals.get('longtitude'): rec._update_address_from_coords() + if rec.parent_id and not vals.get('payment_difficulty'): + rec.payment_difficulty = rec.parent_id.payment_difficulty return records @api.constrains('name') -- cgit v1.2.3 From 7e42b0fdd1735df03e249f1362e58c169236465d Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 24 Jun 2025 15:08:37 +0700 Subject: (andri) fix download xml invoice --- indoteknik_custom/models/account_move.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index ece47236..af24f93e 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -437,18 +437,18 @@ class AccountMove(models.Model): return invoices def export_faktur_to_xml(self): - valid_invoices = self - # Panggil model coretax.faktur untuk menghasilkan XML coretax_faktur = self.env['coretax.faktur'].create({}) - response = coretax_faktur.export_to_download(invoices=valid_invoices, down_payments=valid_invoices.down_payment) - current_time = datetime.utcnow() - # Tandai faktur sebagai sudah diekspor + response = coretax_faktur.export_to_download( + invoices=valid_invoices, + down_payments=[inv.down_payment for inv in valid_invoices], + ) + valid_invoices.write({ 'is_efaktur_exported': True, - 'date_efaktur_exported': current_time, # Set tanggal ekspor + 'date_efaktur_exported': datetime.utcnow(), }) - return response + return response \ No newline at end of file -- cgit v1.2.3 From 8fc5488d18ac3005df9e8e259b2fbc0b67664841 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 25 Jun 2025 04:58:10 +0700 Subject: additional changes --- indoteknik_custom/models/tukar_guling.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index d75dad41..0c6e5eca 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -302,14 +302,15 @@ class TukarGuling(models.Model): related_pickings = self.env['stock.picking'].search([ ('origin', '=', record.origin), ('state', '=', 'done'), + ('picking_type_id', 'in', [29, 30]) ]) if not related_pickings: raise UserError( "Tidak ditemukan BU/PICK atau BU/OUT dari SO: %s" % record.origin) # filter based on stockin.picking picking type - bu_pick_to_return = related_pickings.filtered(lambda ktl: ktl.picking_type_id.id == 30) # BU/PICK - bu_out_to_return = related_pickings.filtered(lambda ktl: ktl.picking_type_id.id == 29) # BU/OUT + bu_pick_to_return = self.env['stock.picking'] or related_pickings.filtered(lambda ktl: ktl.picking_type_id.id == 30) # BU/PICK + bu_out_to_return = record.operations or related_pickings.filtered(lambda ktl: ktl.picking_type_id.id == 29) # BU/OUT if not bu_pick_to_return and not bu_out_to_return: raise UserError("Tidak ada BU/PICK atau BU/OUT yang selesai untuk diretur.") -- cgit v1.2.3 From f8765bb7a9b6095c3b79d71f65ce8ae4a041da6d Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 25 Jun 2025 08:04:39 +0700 Subject: don --- indoteknik_custom/models/tukar_guling.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 0c6e5eca..e00c21f8 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -309,8 +309,8 @@ class TukarGuling(models.Model): "Tidak ditemukan BU/PICK atau BU/OUT dari SO: %s" % record.origin) # filter based on stockin.picking picking type - bu_pick_to_return = self.env['stock.picking'] or related_pickings.filtered(lambda ktl: ktl.picking_type_id.id == 30) # BU/PICK - bu_out_to_return = record.operations or related_pickings.filtered(lambda ktl: ktl.picking_type_id.id == 29) # BU/OUT + bu_pick_to_return = record.operations.konfirm_koli_lines.pick_id + bu_out_to_return = record.operations if not bu_pick_to_return and not bu_out_to_return: raise UserError("Tidak ada BU/PICK atau BU/OUT yang selesai untuk diretur.") -- cgit v1.2.3 From 96959a512908b6cdec226ada836607db2d525042 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 25 Jun 2025 08:38:52 +0700 Subject: (andri)fix --- indoteknik_custom/models/res_partner.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index d439c7a8..f5347bea 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -165,21 +165,21 @@ class ResPartner(models.Model): "this feature", tracking=3) telegram_id = fields.Char(string="Telegram") avg_aging= fields.Float(string='Average Aging') - payment_difficulty = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', tracking=3) + payment_difficulty = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', compute="_compute_payment_difficulty", inverse = "_inverse_payment_difficulty", tracking=3) payment_history_url = fields.Text(string='Payment History URL') - # @api.depends('parent_id.payment_difficulty') - # def _compute_payment_difficulty(self): - # for partner in self: - # if partner.parent_id: - # partner.payment_difficulty = partner.parent_id.payment_difficulty - - # def _inverse_payment_difficulty(self): - # for partner in self: - # if not partner.parent_id: - # partner.child_ids.write({ - # 'payment_difficulty': partner.payment_difficulty - # }) + @api.depends('parent_id.payment_difficulty') + def _compute_payment_difficulty(self): + for partner in self: + if partner.parent_id: + partner.payment_difficulty = partner.parent_id.payment_difficulty + + def _inverse_payment_difficulty(self): + for partner in self: + if not partner.parent_id: + partner.child_ids.write({ + 'payment_difficulty': partner.payment_difficulty + }) @api.model def _default_payment_term(self): -- cgit v1.2.3 From 92bf91488eca67adabb6bfe8394385a9cee1032d Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Wed, 25 Jun 2025 09:58:26 +0700 Subject: fix bu pick automatic reserve --- indoteknik_custom/models/stock_picking.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 27046063..c884f97e 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1356,6 +1356,8 @@ class StockPicking(models.Model): ]) for line in po.order_sales_match_line: + if not line.bu_pick: + continue line.bu_pick.action_assign() def check_invoice_date(self): -- cgit v1.2.3 From 74b49469064387219474ddf8e8f38e5d676079c2 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Wed, 25 Jun 2025 10:03:06 +0700 Subject: push --- indoteknik_custom/models/purchase_order_sales_match.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/purchase_order_sales_match.py b/indoteknik_custom/models/purchase_order_sales_match.py index 2ea89dab..b18864f3 100644 --- a/indoteknik_custom/models/purchase_order_sales_match.py +++ b/indoteknik_custom/models/purchase_order_sales_match.py @@ -41,7 +41,7 @@ class PurchaseOrderSalesMatch(models.Model): if stock_move: rec.bu_pick = stock_move.picking_id.id else: - rec.bu_pick = '' + rec.bu_pick = None def _compute_purchase_line_id(self): for line in self: -- cgit v1.2.3 From 8ca3394604d088a683fc4a3c69321ad65f093b7f Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Wed, 25 Jun 2025 11:18:33 +0700 Subject: add terbilang total cashback customer commision --- indoteknik_custom/models/commision.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py index 199aa106..997d4470 100644 --- a/indoteknik_custom/models/commision.py +++ b/indoteknik_custom/models/commision.py @@ -191,6 +191,7 @@ class CustomerCommision(models.Model): total_cashback = fields.Float(string='Total Cashback') commision_amt_text = fields.Char(string='Commision Amount Text', compute='compute_delivery_amt_text') + total_cashback_text = fields.Char(string='Commision Amount Text', compute='compute_total_cashback_text') total_dpp = fields.Float(string='Total DPP', compute='_compute_total_dpp') commision_type = fields.Selection([ ('fee', 'Fee'), @@ -281,6 +282,20 @@ class CustomerCommision(models.Model): except: record.commision_amt_text = res + def compute_total_cashback_text(self): + tb = Terbilang() + + for record in self: + res = '' + + try: + if record.total_commision > 0: + tb.parse(int(record.total_commision)) + res = tb.getresult().title() + record.commision_amt_text = res + ' Rupiah' + except: + record.commision_amt_text = res + def _compute_grouped_numbers(self): for rec in self: so_numbers = set() -- cgit v1.2.3 From f87442f721ea925bd2763492f990bbe68e1627ac Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Wed, 25 Jun 2025 13:09:24 +0700 Subject: fix terbilang cashback customer benefits --- indoteknik_custom/models/commision.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py index 997d4470..b685f6e1 100644 --- a/indoteknik_custom/models/commision.py +++ b/indoteknik_custom/models/commision.py @@ -191,7 +191,7 @@ class CustomerCommision(models.Model): total_cashback = fields.Float(string='Total Cashback') commision_amt_text = fields.Char(string='Commision Amount Text', compute='compute_delivery_amt_text') - total_cashback_text = fields.Char(string='Commision Amount Text', compute='compute_total_cashback_text') + total_cashback_text = fields.Char(string='Cashback Text', compute='compute_total_cashback_text') total_dpp = fields.Float(string='Total DPP', compute='_compute_total_dpp') commision_type = fields.Selection([ ('fee', 'Fee'), @@ -287,14 +287,14 @@ class CustomerCommision(models.Model): for record in self: res = '' - try: if record.total_commision > 0: tb.parse(int(record.total_commision)) res = tb.getresult().title() - record.commision_amt_text = res + ' Rupiah' - except: - record.commision_amt_text = res + record.total_cashback_text = f"{res} Rupiah" if res else "" + except Exception as e: + record.total_cashback_text = "" + _logger.error("Error computing cashback text: %s", str(e)) def _compute_grouped_numbers(self): for rec in self: -- cgit v1.2.3 From 0e02e21f5b8943a25e21c377303f6552494e6cc7 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 25 Jun 2025 14:45:32 +0700 Subject: (andri) fix xml coretax --- indoteknik_custom/models/coretax_fatur.py | 105 +++++++----------------------- 1 file changed, 25 insertions(+), 80 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/coretax_fatur.py b/indoteknik_custom/models/coretax_fatur.py index 54eb0f8e..4397d7a1 100644 --- a/indoteknik_custom/models/coretax_fatur.py +++ b/indoteknik_custom/models/coretax_fatur.py @@ -51,8 +51,7 @@ class CoretaxFaktur(models.Model): buyerIDTKU = formula if sum(int(char) for char in buyerTIN) > 0 else '000000' # Tambahkan elemen faktur - ET.SubElement(tax_invoice, 'TaxInvoiceDate').text = invoice.invoice_date.strftime( - '%Y-%m-%d') if invoice.invoice_date else '' + ET.SubElement(tax_invoice, 'TaxInvoiceDate').text = invoice.invoice_date.strftime('%Y-%m-%d') if invoice.invoice_date else '' ET.SubElement(tax_invoice, 'TaxInvoiceOpt').text = 'Normal' ET.SubElement(tax_invoice, 'TrxCode').text = '04' ET.SubElement(tax_invoice, 'AddInfo') @@ -62,103 +61,49 @@ class CoretaxFaktur(models.Model): ET.SubElement(tax_invoice, 'FacilityStamp') ET.SubElement(tax_invoice, 'SellerIDTKU').text = '0742260227086000000000' ET.SubElement(tax_invoice, 'BuyerTin').text = buyerTIN - ET.SubElement(tax_invoice, 'BuyerDocument').text = 'TIN' if sum( - int(char) for char in buyerTIN) > 0 else 'Other ID' + ET.SubElement(tax_invoice, 'BuyerDocument').text = 'TIN' if sum(int(char) for char in buyerTIN) > 0 else 'Other ID' ET.SubElement(tax_invoice, 'BuyerCountry').text = 'IDN' - ET.SubElement(tax_invoice, 'BuyerDocumentNumber').text = '-' if sum( - int(char) for char in buyerTIN) > 0 else str(invoice.partner_id.id) + ET.SubElement(tax_invoice, 'BuyerDocumentNumber').text = '-' if sum(int(char) for char in buyerTIN) > 0 else str(invoice.partner_id.id) ET.SubElement(tax_invoice, 'BuyerName').text = invoice.partner_id.nama_wajib_pajak or '' ET.SubElement(tax_invoice, 'BuyerAdress').text = invoice.partner_id.alamat_lengkap_text or '' ET.SubElement(tax_invoice, 'BuyerEmail').text = invoice.partner_id.email or '' ET.SubElement(tax_invoice, 'BuyerIDTKU').text = buyerIDTKU - # Handle product lines based on down_payments flag - if down_payments and invoice.invoice_origin: - # Get from sale.order.line for down payment - sale_order = invoice.sale_id - if sale_order: - product_lines = sale_order.order_line.filtered( - lambda l: l.product_id and not l.is_downpayment and not l.display_type and not l.product_id.id == 229625 - ) - # Convert sale order lines to invoice-like format - converted_lines = [] - for line in product_lines: - converted_lines.append({ - 'name': line.name, - 'product_id': line.product_id, - 'price_subtotal': line.price_subtotal, - 'quantity': line.product_uom_qty, - 'price_unit': line.price_unit, - 'account_id': line.order_id.analytic_account_id or False, - }) - product_lines = converted_lines - else: - product_lines = [] - else: - # Normal case - get from invoice lines - product_lines = invoice.invoice_line_ids.filtered( - lambda l: not l.display_type and hasattr(l, 'account_id') and - l.account_id and l.product_id and - l.account_id.id != self.DISCOUNT_ACCOUNT_ID and - l.quantity != -1 - ) - - # Filter discount (always from invoice) - discount_lines = invoice.invoice_line_ids.filtered( - lambda l: not l.display_type and ( - (hasattr(l, 'account_id') and l.account_id and - l.account_id.id == self.DISCOUNT_ACCOUNT_ID) or - (l.quantity == -1) - ) + # Ambil lines + product_lines = invoice.invoice_line_ids.filtered( + lambda l: not l.display_type and l.product_id and l.account_id and l.account_id.id != self.DISCOUNT_ACCOUNT_ID and l.quantity != -1 ) - # Calculate totals - total_product_amount = sum(line.get('price_subtotal', 0) if isinstance(line, dict) - else line.price_subtotal for line in product_lines) - if total_product_amount == 0: - total_product_amount = 1 # Avoid division by zero - - total_discount_amount = abs(sum(line.price_subtotal for line in discount_lines)) + invoice_untaxed = invoice.amount_untaxed + if invoice_untaxed == 0: + invoice_untaxed = 1 # Hindari div zero # Tambahkan elemen ListOfGoodService list_of_good_service = ET.SubElement(tax_invoice, 'ListOfGoodService') for line in product_lines: - # Handle both dict (converted sale lines) and normal invoice lines - if isinstance(line, dict): - line_price_subtotal = line['price_subtotal'] - line_quantity = line['quantity'] - line_name = line['name'] - line_price_unit = line['price_unit'] - else: - line_price_subtotal = line.price_subtotal - line_quantity = line.quantity - line_name = line.name - line_price_unit = line.price_unit - - # Calculate prorated discount - line_proportion = line_price_subtotal / total_product_amount - line_discount = total_discount_amount * line_proportion - - subtotal = line_price_subtotal - quantity = line_quantity - total_discount = round(line_discount, 2) - - # Calculate other tax values - otherTaxBase = round(subtotal * (11 / 12), 2) if subtotal else 0 - vat_amount = round(otherTaxBase * 0.12, 2) - - # Create the line in XML + line_price_subtotal = line.price_subtotal + line_quantity = line.quantity + line_name = line.name + line_price_unit = line.price_unit + + # Hitung proporsi & tax base per line + line_proportion = line_price_subtotal / invoice_untaxed + line_tax_base = round(invoice.amount_untaxed * line_proportion, 2) + other_tax_base = round(line_tax_base * (11 / 12), 2) + vat_amount = round(other_tax_base * 0.12, 2) + + # Isi XML good_service = ET.SubElement(list_of_good_service, 'GoodService') ET.SubElement(good_service, 'Opt').text = 'A' ET.SubElement(good_service, 'Code').text = '000000' ET.SubElement(good_service, 'Name').text = line_name ET.SubElement(good_service, 'Unit').text = 'UM.0018' ET.SubElement(good_service, 'Price').text = str(round(line_price_unit, 2)) if line_price_unit else '0' - ET.SubElement(good_service, 'Qty').text = str(quantity) - ET.SubElement(good_service, 'TotalDiscount').text = str(total_discount) - ET.SubElement(good_service, 'TaxBase').text = str(round(subtotal)) if subtotal else '0' - ET.SubElement(good_service, 'OtherTaxBase').text = str(otherTaxBase) + ET.SubElement(good_service, 'Qty').text = str(line_quantity) + ET.SubElement(good_service, 'TotalDiscount').text = '0' + ET.SubElement(good_service, 'TaxBase').text = str(line_tax_base) + ET.SubElement(good_service, 'OtherTaxBase').text = str(other_tax_base) ET.SubElement(good_service, 'VATRate').text = '12' ET.SubElement(good_service, 'VAT').text = str(vat_amount) ET.SubElement(good_service, 'STLGRate').text = '0' -- cgit v1.2.3 From e483cf1634b16df7c9ecc4a89007dbc253b68da1 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 25 Jun 2025 14:50:32 +0700 Subject: (andri) ganti penamaan field commision --- indoteknik_custom/models/commision.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py index b685f6e1..4db6c009 100644 --- a/indoteknik_custom/models/commision.py +++ b/indoteknik_custom/models/commision.py @@ -178,10 +178,10 @@ class CustomerCommision(models.Model): ], string='Status') # commision_percent = fields.Float(string='Commision %', tracking=3) - commision_percent = fields.Float(string='Cashback %', tracking=3) + commision_percent = fields.Float(string='Persentase', tracking=3) # commision_amt = fields.Float(string='Commision Amount', tracking=3) - commision_amt = fields.Float(string='Cashback', tracking=3) + commision_amt = fields.Float(string='Amount', tracking=3) # cashback = fields.Float(string='Cashback', compute="compute_cashback") cashback = fields.Float(string='PPh Cashback', compute="compute_cashback") @@ -190,7 +190,7 @@ class CustomerCommision(models.Model): total_commision = fields.Float(string='Cashback yang dibayarkan', compute="compute_cashback") total_cashback = fields.Float(string='Total Cashback') - commision_amt_text = fields.Char(string='Commision Amount Text', compute='compute_delivery_amt_text') + commision_amt_text = fields.Char(string='Amount Text', compute='compute_delivery_amt_text') total_cashback_text = fields.Char(string='Cashback Text', compute='compute_total_cashback_text') total_dpp = fields.Float(string='Total DPP', compute='_compute_total_dpp') commision_type = fields.Selection([ -- cgit v1.2.3 From e30c678ed5eb53f67d95d02645f6662462ec07d0 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 25 Jun 2025 14:54:16 +0700 Subject: (andri) kembalikan perhitungan coretax --- indoteknik_custom/models/coretax_fatur.py | 105 +++++++++++++++++++++++------- 1 file changed, 80 insertions(+), 25 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/coretax_fatur.py b/indoteknik_custom/models/coretax_fatur.py index 4397d7a1..54eb0f8e 100644 --- a/indoteknik_custom/models/coretax_fatur.py +++ b/indoteknik_custom/models/coretax_fatur.py @@ -51,7 +51,8 @@ class CoretaxFaktur(models.Model): buyerIDTKU = formula if sum(int(char) for char in buyerTIN) > 0 else '000000' # Tambahkan elemen faktur - ET.SubElement(tax_invoice, 'TaxInvoiceDate').text = invoice.invoice_date.strftime('%Y-%m-%d') if invoice.invoice_date else '' + ET.SubElement(tax_invoice, 'TaxInvoiceDate').text = invoice.invoice_date.strftime( + '%Y-%m-%d') if invoice.invoice_date else '' ET.SubElement(tax_invoice, 'TaxInvoiceOpt').text = 'Normal' ET.SubElement(tax_invoice, 'TrxCode').text = '04' ET.SubElement(tax_invoice, 'AddInfo') @@ -61,49 +62,103 @@ class CoretaxFaktur(models.Model): ET.SubElement(tax_invoice, 'FacilityStamp') ET.SubElement(tax_invoice, 'SellerIDTKU').text = '0742260227086000000000' ET.SubElement(tax_invoice, 'BuyerTin').text = buyerTIN - ET.SubElement(tax_invoice, 'BuyerDocument').text = 'TIN' if sum(int(char) for char in buyerTIN) > 0 else 'Other ID' + ET.SubElement(tax_invoice, 'BuyerDocument').text = 'TIN' if sum( + int(char) for char in buyerTIN) > 0 else 'Other ID' ET.SubElement(tax_invoice, 'BuyerCountry').text = 'IDN' - ET.SubElement(tax_invoice, 'BuyerDocumentNumber').text = '-' if sum(int(char) for char in buyerTIN) > 0 else str(invoice.partner_id.id) + ET.SubElement(tax_invoice, 'BuyerDocumentNumber').text = '-' if sum( + int(char) for char in buyerTIN) > 0 else str(invoice.partner_id.id) ET.SubElement(tax_invoice, 'BuyerName').text = invoice.partner_id.nama_wajib_pajak or '' ET.SubElement(tax_invoice, 'BuyerAdress').text = invoice.partner_id.alamat_lengkap_text or '' ET.SubElement(tax_invoice, 'BuyerEmail').text = invoice.partner_id.email or '' ET.SubElement(tax_invoice, 'BuyerIDTKU').text = buyerIDTKU - # Ambil lines - product_lines = invoice.invoice_line_ids.filtered( - lambda l: not l.display_type and l.product_id and l.account_id and l.account_id.id != self.DISCOUNT_ACCOUNT_ID and l.quantity != -1 + # Handle product lines based on down_payments flag + if down_payments and invoice.invoice_origin: + # Get from sale.order.line for down payment + sale_order = invoice.sale_id + if sale_order: + product_lines = sale_order.order_line.filtered( + lambda l: l.product_id and not l.is_downpayment and not l.display_type and not l.product_id.id == 229625 + ) + # Convert sale order lines to invoice-like format + converted_lines = [] + for line in product_lines: + converted_lines.append({ + 'name': line.name, + 'product_id': line.product_id, + 'price_subtotal': line.price_subtotal, + 'quantity': line.product_uom_qty, + 'price_unit': line.price_unit, + 'account_id': line.order_id.analytic_account_id or False, + }) + product_lines = converted_lines + else: + product_lines = [] + else: + # Normal case - get from invoice lines + product_lines = invoice.invoice_line_ids.filtered( + lambda l: not l.display_type and hasattr(l, 'account_id') and + l.account_id and l.product_id and + l.account_id.id != self.DISCOUNT_ACCOUNT_ID and + l.quantity != -1 + ) + + # Filter discount (always from invoice) + discount_lines = invoice.invoice_line_ids.filtered( + lambda l: not l.display_type and ( + (hasattr(l, 'account_id') and l.account_id and + l.account_id.id == self.DISCOUNT_ACCOUNT_ID) or + (l.quantity == -1) + ) ) - invoice_untaxed = invoice.amount_untaxed - if invoice_untaxed == 0: - invoice_untaxed = 1 # Hindari div zero + # Calculate totals + total_product_amount = sum(line.get('price_subtotal', 0) if isinstance(line, dict) + else line.price_subtotal for line in product_lines) + if total_product_amount == 0: + total_product_amount = 1 # Avoid division by zero + + total_discount_amount = abs(sum(line.price_subtotal for line in discount_lines)) # Tambahkan elemen ListOfGoodService list_of_good_service = ET.SubElement(tax_invoice, 'ListOfGoodService') for line in product_lines: - line_price_subtotal = line.price_subtotal - line_quantity = line.quantity - line_name = line.name - line_price_unit = line.price_unit - - # Hitung proporsi & tax base per line - line_proportion = line_price_subtotal / invoice_untaxed - line_tax_base = round(invoice.amount_untaxed * line_proportion, 2) - other_tax_base = round(line_tax_base * (11 / 12), 2) - vat_amount = round(other_tax_base * 0.12, 2) - - # Isi XML + # Handle both dict (converted sale lines) and normal invoice lines + if isinstance(line, dict): + line_price_subtotal = line['price_subtotal'] + line_quantity = line['quantity'] + line_name = line['name'] + line_price_unit = line['price_unit'] + else: + line_price_subtotal = line.price_subtotal + line_quantity = line.quantity + line_name = line.name + line_price_unit = line.price_unit + + # Calculate prorated discount + line_proportion = line_price_subtotal / total_product_amount + line_discount = total_discount_amount * line_proportion + + subtotal = line_price_subtotal + quantity = line_quantity + total_discount = round(line_discount, 2) + + # Calculate other tax values + otherTaxBase = round(subtotal * (11 / 12), 2) if subtotal else 0 + vat_amount = round(otherTaxBase * 0.12, 2) + + # Create the line in XML good_service = ET.SubElement(list_of_good_service, 'GoodService') ET.SubElement(good_service, 'Opt').text = 'A' ET.SubElement(good_service, 'Code').text = '000000' ET.SubElement(good_service, 'Name').text = line_name ET.SubElement(good_service, 'Unit').text = 'UM.0018' ET.SubElement(good_service, 'Price').text = str(round(line_price_unit, 2)) if line_price_unit else '0' - ET.SubElement(good_service, 'Qty').text = str(line_quantity) - ET.SubElement(good_service, 'TotalDiscount').text = '0' - ET.SubElement(good_service, 'TaxBase').text = str(line_tax_base) - ET.SubElement(good_service, 'OtherTaxBase').text = str(other_tax_base) + ET.SubElement(good_service, 'Qty').text = str(quantity) + ET.SubElement(good_service, 'TotalDiscount').text = str(total_discount) + ET.SubElement(good_service, 'TaxBase').text = str(round(subtotal)) if subtotal else '0' + ET.SubElement(good_service, 'OtherTaxBase').text = str(otherTaxBase) ET.SubElement(good_service, 'VATRate').text = '12' ET.SubElement(good_service, 'VAT').text = str(vat_amount) ET.SubElement(good_service, 'STLGRate').text = '0' -- cgit v1.2.3 From 27926566ef6fee5a5a4be9c4cfcaddc6edfa23a9 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 26 Jun 2025 08:44:24 +0700 Subject: (andri) fix price coretax & penamaan field cust benefits --- indoteknik_custom/models/commision.py | 2 +- indoteknik_custom/models/coretax_fatur.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py index 4db6c009..97184cdb 100644 --- a/indoteknik_custom/models/commision.py +++ b/indoteknik_custom/models/commision.py @@ -178,7 +178,7 @@ class CustomerCommision(models.Model): ], string='Status') # commision_percent = fields.Float(string='Commision %', tracking=3) - commision_percent = fields.Float(string='Persentase', tracking=3) + commision_percent = fields.Float(string='Persentase (%)', tracking=3) # commision_amt = fields.Float(string='Commision Amount', tracking=3) commision_amt = fields.Float(string='Amount', tracking=3) diff --git a/indoteknik_custom/models/coretax_fatur.py b/indoteknik_custom/models/coretax_fatur.py index 54eb0f8e..9b1544d3 100644 --- a/indoteknik_custom/models/coretax_fatur.py +++ b/indoteknik_custom/models/coretax_fatur.py @@ -148,13 +148,16 @@ class CoretaxFaktur(models.Model): otherTaxBase = round(subtotal * (11 / 12), 2) if subtotal else 0 vat_amount = round(otherTaxBase * 0.12, 2) + price_per_unit = round(subtotal / quantity, 2) if quantity else 0 + # Create the line in XML good_service = ET.SubElement(list_of_good_service, 'GoodService') ET.SubElement(good_service, 'Opt').text = 'A' ET.SubElement(good_service, 'Code').text = '000000' ET.SubElement(good_service, 'Name').text = line_name ET.SubElement(good_service, 'Unit').text = 'UM.0018' - ET.SubElement(good_service, 'Price').text = str(round(line_price_unit, 2)) if line_price_unit else '0' + # ET.SubElement(good_service, 'Price').text = str(round(line_price_unit, 2)) if line_price_unit else '0' + ET.SubElement(good_service, 'Price').text = str(price_per_unit) ET.SubElement(good_service, 'Qty').text = str(quantity) ET.SubElement(good_service, 'TotalDiscount').text = str(total_discount) ET.SubElement(good_service, 'TaxBase').text = str(round(subtotal)) if subtotal else '0' -- cgit v1.2.3 From 4a628d1644da21870706951c4601a5b007993cd8 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Thu, 26 Jun 2025 08:44:01 +0700 Subject: change command error --- indoteknik_custom/models/sale_order.py | 4 ++-- 1 file changed, 2 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 85228901..109771e9 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -1994,9 +1994,9 @@ class SaleOrder(models.Model): confirmed_bom = search_bom.filtered(lambda x: x.state == 'confirmed' or x.state == 'done') if not confirmed_bom: raise UserError( - "Product BOM belum dikonfirmasi di Manufacturing Orders. Silakan hubungi MD.") + "Product BOM belum dikonfirmasi di Manufacturing Orders. Silakan hubungi Purchasing.") else: - raise UserError("Product BOM tidak di temukan di manufacturing orders, silahkan hubungi MD") + raise UserError("Product BOM tidak di temukan di manufacturing orders, silahkan hubungi Purchasing") def check_duplicate_product(self): for order in self: -- cgit v1.2.3 From 3b6b3a2f7a161a08fa3995906f92019cc5ea4d17 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 26 Jun 2025 10:32:15 +0700 Subject: don --- indoteknik_custom/models/tukar_guling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index e00c21f8..681090f0 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -308,7 +308,7 @@ class TukarGuling(models.Model): raise UserError( "Tidak ditemukan BU/PICK atau BU/OUT dari SO: %s" % record.origin) - # filter based on stockin.picking picking type + # filter based on stock.picking picking type bu_pick_to_return = record.operations.konfirm_koli_lines.pick_id bu_out_to_return = record.operations -- cgit v1.2.3 From 8767ca7aed495a70114fdbaaa61b0772c497b4d0 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 26 Jun 2025 13:56:20 +0700 Subject: (andri) fix downpayment --- indoteknik_custom/models/coretax_fatur.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/coretax_fatur.py b/indoteknik_custom/models/coretax_fatur.py index 9b1544d3..ce94306f 100644 --- a/indoteknik_custom/models/coretax_fatur.py +++ b/indoteknik_custom/models/coretax_fatur.py @@ -3,6 +3,9 @@ import xml.etree.ElementTree as ET from xml.dom import minidom import base64 import re +import logging + +_logger = logging.getLogger(__name__) class CoretaxFaktur(models.Model): @@ -72,8 +75,9 @@ class CoretaxFaktur(models.Model): ET.SubElement(tax_invoice, 'BuyerEmail').text = invoice.partner_id.email or '' ET.SubElement(tax_invoice, 'BuyerIDTKU').text = buyerIDTKU + _logger.info(" invoice down_payments: %s", invoice.down_payment) # Handle product lines based on down_payments flag - if down_payments and invoice.invoice_origin: + if invoice.down_payment and invoice.invoice_origin: # Get from sale.order.line for down payment sale_order = invoice.sale_id if sale_order: -- cgit v1.2.3 From 7ac0e952f1ce4be4bbe3c374a646a7b106746581 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 26 Jun 2025 15:06:59 +0700 Subject: jago --- indoteknik_custom/models/tukar_guling.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 681090f0..77538c91 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -24,8 +24,8 @@ class TukarGuling(models.Model): name = fields.Char('Number', required=True, copy=False, readonly=True, default='New') date = fields.Datetime('Date', default=fields.Datetime.now, required=True) operations = fields.Many2one('stock.picking', 'Operations', - domain=[('picking_type_id.code', '=', 'outgoing')], help='Nomor BU/Out atau BU/Pick') - spesific_operations = fields.Many2one('stock.picking', 'Spesific Operations', domain = [('origin', '=', origin), ('state', '=', 'done')]) + domain=[('picking_type_id.sequence_code', 'in', ['OUT', 'PICK']), + ('state', '=', 'done')], help='Nomor BU/Out atau BU/Pick') ba_num = fields.Text('Nomor BA') notes = fields.Text('Notes') return_type = fields.Selection(String='Return Type', selection=[ @@ -45,10 +45,6 @@ class TukarGuling(models.Model): @api.onchange('operations') def _onchange_operations(self): """Auto-populate lines ketika operations dipilih""" - if self.origin: - return {'domain': { - 'specific_transfers': [('origin', '=', self.origin), ('state', '=', 'done')] - }} if self.operations: from_return_picking = self.env.context.get('from_return_picking', False) or \ self.env.context.get('default_line_ids', False) -- cgit v1.2.3 From 1e01a6845781821fcf65a42c4528b3efe144e28c Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 26 Jun 2025 15:23:24 +0700 Subject: show bu pick where the bu out is not done --- indoteknik_custom/models/tukar_guling.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 77538c91..6fbca20c 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -24,8 +24,10 @@ class TukarGuling(models.Model): name = fields.Char('Number', required=True, copy=False, readonly=True, default='New') date = fields.Datetime('Date', default=fields.Datetime.now, required=True) operations = fields.Many2one('stock.picking', 'Operations', - domain=[('picking_type_id.sequence_code', 'in', ['OUT', 'PICK']), - ('state', '=', 'done')], help='Nomor BU/Out atau BU/Pick') + domain=[('picking_type_id.sequence_code', '=', 'OUT'), + ('state', '=', 'done'), '&', + ('picking_type_id.sequence_code', '=', 'PICK'), + ('state', '!=', 'done')], help='Nomor BU/Out atau BU/Pick') ba_num = fields.Text('Nomor BA') notes = fields.Text('Notes') return_type = fields.Selection(String='Return Type', selection=[ @@ -76,11 +78,6 @@ class TukarGuling(models.Model): elif hasattr(self.operations, 'move_lines') and self.operations.move_lines: moves_to_check = self.operations.move_lines - # Debug logging - _logger = logging.getLogger(__name__) - _logger.info(f"BU/OUT: {self.operations.name}, State: {self.operations.state}") - _logger.info(f"Total moves found: {len(moves_to_check)}") - for move in moves_to_check: _logger.info( f"Move: {move.name}, Product: {move.product_id.name if move.product_id else 'No Product'}, Qty: {move.product_uom_qty}, State: {move.state}") @@ -115,7 +112,7 @@ class TukarGuling(models.Model): """Manual button untuk populate lines - sebagai alternatif""" self.ensure_one() if not self.operations: - raise UserError("Pilih BU/OUT terlebih dahulu!") + raise UserError("Pilih BU/OUT atau BU/PICK terlebih dahulu!") # Clear existing lines self.line_ids = [(5, 0, 0)] @@ -144,7 +141,7 @@ class TukarGuling(models.Model): def _check_required_bu_fields(self): for record in self: if record.return_type in ['revisi_so', 'tukar_guling'] and not record.operations: - raise ValidationError("BU/Out harus diisi!") + raise ValidationError("Operations harus diisi") @api.constrains('line_ids', 'state') def _check_product_lines(self): @@ -178,7 +175,6 @@ class TukarGuling(models.Model): def create(self, vals): # Generate sequence number if not vals.get('name') or vals['name'] == 'New': - # Pastikan sequence code 'tukar.guling' ada sequence = self.env['ir.sequence'].search([('code', '=', 'tukar.guling')], limit=1) if sequence: vals['name'] = sequence.next_by_id() @@ -257,7 +253,7 @@ class TukarGuling(models.Model): self.ensure_one() if not self.operations: - raise UserError("BU/Out harus diisi!") + raise UserError("Operations harus diisi!") if not self.return_type: raise UserError("Return Type harus diisi!") -- cgit v1.2.3 From b30cbc06bb4075b6c1fdab056cb5f5ea8d077d6c Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 26 Jun 2025 15:28:21 +0700 Subject: show bu pick where the bu out is not done --- indoteknik_custom/models/tukar_guling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 6fbca20c..6b111029 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -27,7 +27,7 @@ class TukarGuling(models.Model): domain=[('picking_type_id.sequence_code', '=', 'OUT'), ('state', '=', 'done'), '&', ('picking_type_id.sequence_code', '=', 'PICK'), - ('state', '!=', 'done')], help='Nomor BU/Out atau BU/Pick') + ('linked_manual_bu_out.state', '!=', 'done')], help='Nomor BU/Out atau BU/Pick') ba_num = fields.Text('Nomor BA') notes = fields.Text('Notes') return_type = fields.Selection(String='Return Type', selection=[ -- cgit v1.2.3 From 69eed6936f7749809ee57bc4fa4f408fc93da6d3 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 26 Jun 2025 15:30:17 +0700 Subject: show bu pick --- indoteknik_custom/models/tukar_guling.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 6b111029..c1bd82bb 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -24,10 +24,8 @@ class TukarGuling(models.Model): name = fields.Char('Number', required=True, copy=False, readonly=True, default='New') date = fields.Datetime('Date', default=fields.Datetime.now, required=True) operations = fields.Many2one('stock.picking', 'Operations', - domain=[('picking_type_id.sequence_code', '=', 'OUT'), - ('state', '=', 'done'), '&', - ('picking_type_id.sequence_code', '=', 'PICK'), - ('linked_manual_bu_out.state', '!=', 'done')], help='Nomor BU/Out atau BU/Pick') + domain=[('picking_type_id.sequence_code', 'in', 'OUT'), + ('state', '=', 'done')], help='Nomor BU/Out atau BU/Pick') ba_num = fields.Text('Nomor BA') notes = fields.Text('Notes') return_type = fields.Selection(String='Return Type', selection=[ -- cgit v1.2.3 From bc99ea265c50e22d0a6cd74e1e5a4d5f27988701 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 26 Jun 2025 16:59:47 +0700 Subject: create ort only when retur from bu pick --- indoteknik_custom/models/tukar_guling.py | 40 ++++++++++++++++---------------- 1 file changed, 20 insertions(+), 20 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index c1bd82bb..601603d7 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -24,8 +24,8 @@ class TukarGuling(models.Model): name = fields.Char('Number', required=True, copy=False, readonly=True, default='New') date = fields.Datetime('Date', default=fields.Datetime.now, required=True) operations = fields.Many2one('stock.picking', 'Operations', - domain=[('picking_type_id.sequence_code', 'in', 'OUT'), - ('state', '=', 'done')], help='Nomor BU/Out atau BU/Pick') + domain=[('picking_type_id.sequence_code', 'in', ['OUT', 'PICK']), + ('state', '=', 'done'), ('linked_manual_bu_out', '!=', 'done')], help='Nomor BU/Out atau BU/Pick') ba_num = fields.Text('Nomor BA') notes = fields.Text('Notes') return_type = fields.Selection(String='Return Type', selection=[ @@ -287,8 +287,6 @@ class TukarGuling(models.Model): if not record.operations: raise UserError("BU/OUT dari field operations tidak ditemukan.") - operation_picking = record.operations - related_pickings = self.env['stock.picking'].search([ ('origin', '=', record.origin), ('state', '=', 'done'), @@ -414,24 +412,26 @@ class TukarGuling(models.Model): return return_picking # Buat return dari BU/OUT - for picking in bu_out_to_return: - name = _create_return_from_picking(picking) - if name: - created_returns.append(name) + if record.operations.picking_type_id == 30: + for picking in [bu_pick_to_return]: + name = _create_return_from_picking(picking) + if name: + created_returns.append(name) # Buat return dari BU/PICK - for picking in bu_pick_to_return: - name = _create_return_from_picking(picking) - if name: - created_returns.append(name) - - # Buat return dari SRT dan ort - if record.return_type == 'tukar_guling': - target = [woi for woi in created_returns if woi.picking_type_id.id in (74, 73)] - for picking in target: - retur = _create_return_from_picking(picking) - if retur: - created_returns.append(retur) + else: + for picking in [bu_out_to_return]: + name = _create_return_from_picking(picking) + if name: + created_returns.append(name) + + # Buat return dari SRT dan ort + if record.return_type == 'tukar_guling' and record.operations.picking_type_id.id != 30: + target = [woi for woi in created_returns if woi.picking_type_id.id in (74, 73)] + for picking in target: + retur = _create_return_from_picking(picking) + if retur: + created_returns.append(retur) if not created_returns: -- cgit v1.2.3 From 2c3358216c4a2b09ef0df3a2f8998889198ad310 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 26 Jun 2025 21:33:44 +0700 Subject: add validations --- indoteknik_custom/models/stock_picking_return.py | 9 ++++++++- indoteknik_custom/models/tukar_guling.py | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking_return.py b/indoteknik_custom/models/stock_picking_return.py index 74bf6407..982f3ebb 100644 --- a/indoteknik_custom/models/stock_picking_return.py +++ b/indoteknik_custom/models/stock_picking_return.py @@ -12,6 +12,10 @@ class StockReturnPicking(models.TransientModel): ], string='Jenis Retur', default='revisi_so') def create_returns(self): + picking = self.picking_id + if picking.picking_type_id.sequence_code == 'PICK' or picking.picking_type_id.id == 30 and picking.linked_manual_bu_out.state == 'done': + raise UserError("❌ BU/PICK tidak dapat di retur karena BU/OUT Sudah Done") + if self._context.get('from_ui', True) and self.return_type == 'tukar_guling': return self._redirect_to_tukar_guling() return super(StockReturnPicking, self).create_returns() @@ -21,6 +25,9 @@ class StockReturnPicking(models.TransientModel): self.ensure_one() picking = self.picking_id + if picking.picking_type_id.sequence_code == 'PICK' or picking.picking_type_id.id == 30 and picking.linked_manual_bu_out.state == 'done': + raise UserError("❌ BU/PICK tidak dapat di retur karena BU/OUT Sudah Done") + # Get valid return lines with better error handling valid_lines = [] @@ -145,4 +152,4 @@ class ReturnPickingLine(models.TransientModel): raise UserError( _("Quantity yang Anda masukkan (%.2f) tidak boleh melebihi quantity done yaitu: %.2f untuk produk %s") % (rec.quantity, qty_done, rec.product_id.name) - ) \ No newline at end of file + ) diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 601603d7..e1513c2e 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -243,8 +243,30 @@ class TukarGuling(models.Model): def action_submit(self): self.ensure_one() + picking = self.operations + if self.state != 'draft': raise UserError("Submit hanya bisa dilakukan dari Draft.") + + if not self.operations: + raise UserError("Operations harus diisi!") + + if not self.return_type: + raise UserError("Return Type harus diisi!") + + self._validate_product_lines() + + if picking.picking_type_id.sequence_code == 'OUT' or picking.picking_type_id.id == 29: + if picking.state != 'done': + raise UserError("BU/OUT Harus Sudah DONE") + + elif picking.picking_type_id.sequence_code == 'PICK' or picking.picking_type_id.id == 30: + linked_bu_out = picking.linked_manual_bu_out + if linked_bu_out.state == 'done': + raise UserError("BU/PICK yang bisa diretur ketika BU/OUT yang belum DONE") + else: + raise UserError("Picking Type harus BU/OUT atau BU/PICK") + self.state = 'approval_sales' def action_approve(self): -- cgit v1.2.3 From 605385c9c0bf3ed95ada1628c02f00b53dc19eb6 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 27 Jun 2025 15:45:45 +0700 Subject: validation rev --- indoteknik_custom/models/stock_picking_return.py | 16 ++-- indoteknik_custom/models/tukar_guling.py | 112 +++++++++++++---------- 2 files changed, 71 insertions(+), 57 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking_return.py b/indoteknik_custom/models/stock_picking_return.py index 982f3ebb..97b622b4 100644 --- a/indoteknik_custom/models/stock_picking_return.py +++ b/indoteknik_custom/models/stock_picking_return.py @@ -6,17 +6,17 @@ from odoo import models, fields, api, _ class StockReturnPicking(models.TransientModel): _inherit = 'stock.return.picking' - return_type = fields.Selection([ - ('revisi_so', 'Revisi SO'), - ('tukar_guling', 'Tukar Guling') - ], string='Jenis Retur', default='revisi_so') + # return_type = fields.Selection([ + # ('revisi_so', 'Revisi SO'), + # ('tukar_guling', 'Tukar Guling') + # ], string='Jenis Retur', default='revisi_so') def create_returns(self): picking = self.picking_id - if picking.picking_type_id.sequence_code == 'PICK' or picking.picking_type_id.id == 30 and picking.linked_manual_bu_out.state == 'done': + if picking.picking_type_id.id == 30 and picking.linked_manual_bu_out.state == 'done': raise UserError("❌ BU/PICK tidak dapat di retur karena BU/OUT Sudah Done") - if self._context.get('from_ui', True) and self.return_type == 'tukar_guling': + if self._context.get('from_ui', True): return self._redirect_to_tukar_guling() return super(StockReturnPicking, self).create_returns() @@ -25,7 +25,7 @@ class StockReturnPicking(models.TransientModel): self.ensure_one() picking = self.picking_id - if picking.picking_type_id.sequence_code == 'PICK' or picking.picking_type_id.id == 30 and picking.linked_manual_bu_out.state == 'done': + if picking.picking_type_id.id == 30 and picking.linked_manual_bu_out.state == 'done': raise UserError("❌ BU/PICK tidak dapat di retur karena BU/OUT Sudah Done") # Get valid return lines with better error handling @@ -70,7 +70,7 @@ class StockReturnPicking(models.TransientModel): # Prepare context for Tukar Guling form context = { 'default_operations': picking.id, - 'default_return_type': 'tukar_guling', + # 'default_return_type': 'tukar_guling', 'default_date': fields.Datetime.now(), 'default_state': 'draft', 'default_ba_num': _('Retur dari %s') % picking.name, diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index e1513c2e..df501261 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -23,9 +23,23 @@ class TukarGuling(models.Model): # origin_so = fields.Many2one('sale.order', string='Origin SO') name = fields.Char('Number', required=True, copy=False, readonly=True, default='New') date = fields.Datetime('Date', default=fields.Datetime.now, required=True) - operations = fields.Many2one('stock.picking', 'Operations', - domain=[('picking_type_id.sequence_code', 'in', ['OUT', 'PICK']), - ('state', '=', 'done'), ('linked_manual_bu_out', '!=', 'done')], help='Nomor BU/Out atau BU/Pick') + operations = fields.Many2one( + 'stock.picking', + string='Operations', + domain=[ + '|', + # BU/OUT + '&', + ('picking_type_id.id', '=', 29), + ('state', '=', 'done'), + '&', + '&', + ('picking_type_id.id', '=', 30), + ('state', '=', 'done'), + ('linked_manual_bu_out', '!=', 'done'), + ], + help='Nomor BU/OUT atau BU/PICK' + ) ba_num = fields.Text('Nomor BA') notes = fields.Text('Notes') return_type = fields.Selection(String='Return Type', selection=[ @@ -169,6 +183,12 @@ class TukarGuling(models.Model): return True + def _is_already_returned(self, picking): + return self.env['stock.picking'].search_count([ + ('origin', '=', 'Return of %s' % picking.name), + ('state', '!=', 'cancel') + ]) > 0 + @api.model def create(self, vals): # Generate sequence number @@ -214,6 +234,8 @@ class TukarGuling(models.Model): return new_record def write(self, vals): + if self._is_already_returned(self.operations): + raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") if 'operations' in vals and not vals.get('origin'): picking = self.env['stock.picking'].browse(vals['operations']) if picking.origin: @@ -242,35 +264,29 @@ class TukarGuling(models.Model): def action_submit(self): self.ensure_one() + picking = self.env['stock.picking'] + if picking.picking_type_id.id == 29: + if picking.picking_type_id.state != 'done': + raise UserError("BU/OUT belum Done!") + elif picking.picking_type_id.id == 30: + linked_bu_out = picking.linked_manual_bu_out + if linked_bu_out and linked_bu_out.state == 'done': + raise UserError("Tidak bisa retur BU/PICK karena BU/OUT suda Done!") + else: + raise UserError("Dokumen Mungkin sudah done") - picking = self.operations - - if self.state != 'draft': - raise UserError("Submit hanya bisa dilakukan dari Draft.") - - if not self.operations: - raise UserError("Operations harus diisi!") - - if not self.return_type: - raise UserError("Return Type harus diisi!") - + if self._is_already_returned(self.operations): + raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") self._validate_product_lines() - if picking.picking_type_id.sequence_code == 'OUT' or picking.picking_type_id.id == 29: - if picking.state != 'done': - raise UserError("BU/OUT Harus Sudah DONE") - - elif picking.picking_type_id.sequence_code == 'PICK' or picking.picking_type_id.id == 30: - linked_bu_out = picking.linked_manual_bu_out - if linked_bu_out.state == 'done': - raise UserError("BU/PICK yang bisa diretur ketika BU/OUT yang belum DONE") - else: - raise UserError("Picking Type harus BU/OUT atau BU/PICK") + if self.state != 'draft': + raise UserError("Submit hanya bisa dilakukan dari Draft.") self.state = 'approval_sales' def action_approve(self): self.ensure_one() + self._validate_product_lines() if not self.operations: raise UserError("Operations harus diisi!") @@ -309,6 +325,8 @@ class TukarGuling(models.Model): if not record.operations: raise UserError("BU/OUT dari field operations tidak ditemukan.") + operation_picking = record.operations + related_pickings = self.env['stock.picking'].search([ ('origin', '=', record.origin), ('state', '=', 'done'), @@ -327,7 +345,6 @@ class TukarGuling(models.Model): created_returns = [] - # Lokasi default untuk retur srt_type = self.env['stock.picking.type'].browse(73) ort_type = self.env['stock.picking.type'].browse(74) @@ -354,25 +371,25 @@ class TukarGuling(models.Model): BU_STOCK_LOCATION_ID = 57 # Determine locations based on picking type - if picking.picking_type_id.id == 30: # -> ngeretur bu pick + if picking.picking_type_id.id == 30: # -> ngeretur bu pick return_type = ort_type default_location_id = BU_OUTPUT_LOCATION_ID default_location_dest_id = BU_STOCK_LOCATION_ID if not default_location_id or not default_location_dest_id: raise UserError("Lokasi Origin atau Destination salah.") - elif picking.picking_type_id.id == 29: # -> ngeretur bu out + elif picking.picking_type_id.id == 29: # -> ngeretur bu out return_type = srt_type default_location_id = PARTNER_LOCATION_ID default_location_dest_id = BU_OUTPUT_LOCATION_ID if not default_location_id or not default_location_dest_id: raise UserError("Lokasi Origin atau Destination salah.") - elif picking.picking_type_id.id == 74: # -> ngeretur srt + elif picking.picking_type_id.id == 74: # -> ngeretur srt return_type = bu_pick_type default_location_id = BU_STOCK_LOCATION_ID default_location_dest_id = BU_OUTPUT_LOCATION_ID if not default_location_id or not default_location_dest_id: raise UserError("Lokasi Origin atau Destination salah.") - elif picking.picking_type_id.id == 73: # -> ngeretur ort + elif picking.picking_type_id.id == 73: # -> ngeretur ort return_type = bu_out_type default_location_id = BU_OUTPUT_LOCATION_ID default_location_dest_id = PARTNER_LOCATION_ID @@ -434,27 +451,24 @@ class TukarGuling(models.Model): return return_picking # Buat return dari BU/OUT - if record.operations.picking_type_id == 30: - for picking in [bu_pick_to_return]: - name = _create_return_from_picking(picking) - if name: - created_returns.append(name) + for picking in bu_out_to_return: + name = _create_return_from_picking(picking) + if name: + created_returns.append(name) # Buat return dari BU/PICK - else: - for picking in [bu_out_to_return]: - name = _create_return_from_picking(picking) - if name: - created_returns.append(name) - - # Buat return dari SRT dan ort - if record.return_type == 'tukar_guling' and record.operations.picking_type_id.id != 30: - target = [woi for woi in created_returns if woi.picking_type_id.id in (74, 73)] - for picking in target: - retur = _create_return_from_picking(picking) - if retur: - created_returns.append(retur) - + for picking in bu_pick_to_return: + name = _create_return_from_picking(picking) + if name: + created_returns.append(name) + + # Buat return dari SRT dan ort + if record.return_type == 'tukar_guling': + target = [woi for woi in created_returns if woi.picking_type_id.id in (74, 73)] + for picking in target: + retur = _create_return_from_picking(picking) + if retur: + created_returns.append(retur) if not created_returns: raise UserError("wkwkwk") @@ -498,4 +512,4 @@ class TukarGulingLine(models.Model): class StockPicking(models.Model): _inherit = 'stock.picking' - tukar_guling_id = fields.Many2one('tukar.guling', string='Tukar Guling Ref') \ No newline at end of file + tukar_guling_id = fields.Many2one('tukar.guling', string='Tukar Guling Ref') -- cgit v1.2.3 From 0e2fe03295f96560500c53ee61104a0a9f563576 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 27 Jun 2025 15:53:11 +0700 Subject: remove ask return in stock picking --- indoteknik_custom/models/stock_picking.py | 66 ++++++++++++------------ indoteknik_custom/models/stock_picking_return.py | 6 +++ 2 files changed, 40 insertions(+), 32 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 6d868db4..1827c489 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1074,38 +1074,40 @@ class StockPicking(models.Model): self.approval_receipt_status = 'pengajuan1' def ask_return_approval(self): - for pick in self: - if self.env.user.is_accounting: - pick.approval_return_status = 'approved' - continue - else: - pick.approval_return_status = 'pengajuan1' - - action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_stock_return_note_wizard') - - if self.picking_type_code == 'outgoing': - if self.env.user.id in [3988, 3401, 20] or ( - self.env.user.has_group( - 'indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin - ): - action['context'] = {'picking_ids': [x.id for x in self]} - return action - elif not self.env.user.has_group( - 'indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin: - raise UserError('Harus Purchasing yang Ask Return') - else: - raise UserError('Harus Sales Admin yang Ask Return') - - elif self.picking_type_code == 'incoming': - if self.env.user.has_group('indoteknik_custom.group_role_purchasing') or ( - self.env.user.id in [3988, 3401, 20] and 'Return of' in self.origin - ): - action['context'] = {'picking_ids': [x.id for x in self]} - return action - elif not self.env.user.id in [3988, 3401, 20] and 'Return of' in self.origin: - raise UserError('Harus Sales Admin yang Ask Return') - else: - raise UserError('Harus Purchasing yang Ask Return') + pass + raise UserError("Bisa langsung Validate") + # for pick in self: + # if self.env.user.is_accounting: + # pick.approval_return_status = 'approved' + # continue + # else: + # pick.approval_return_status = 'pengajuan1' + # + # action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_stock_return_note_wizard') + # + # if self.picking_type_code == 'outgoing': + # if self.env.user.id in [3988, 3401, 20] or ( + # self.env.user.has_group( + # 'indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin + # ): + # action['context'] = {'picking_ids': [x.id for x in self]} + # return action + # elif not self.env.user.has_group( + # 'indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin: + # raise UserError('Harus Purchasing yang Ask Return') + # else: + # raise UserError('Harus Sales Admin yang Ask Return') + # + # elif self.picking_type_code == 'incoming': + # if self.env.user.has_group('indoteknik_custom.group_role_purchasing') or ( + # self.env.user.id in [3988, 3401, 20] and 'Return of' in self.origin + # ): + # action['context'] = {'picking_ids': [x.id for x in self]} + # return action + # elif not self.env.user.id in [3988, 3401, 20] and 'Return of' in self.origin: + # raise UserError('Harus Sales Admin yang Ask Return') + # else: + # raise UserError('Harus Purchasing yang Ask Return') def calculate_line_no(self): diff --git a/indoteknik_custom/models/stock_picking_return.py b/indoteknik_custom/models/stock_picking_return.py index 97b622b4..f7900a27 100644 --- a/indoteknik_custom/models/stock_picking_return.py +++ b/indoteknik_custom/models/stock_picking_return.py @@ -11,8 +11,14 @@ class StockReturnPicking(models.TransientModel): # ('tukar_guling', 'Tukar Guling') # ], string='Jenis Retur', default='revisi_so') + def create_returns(self): picking = self.picking_id + # guling = self.env['tukar.guling'] + # if guling._is_already_returned(picking): + # raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") + # if self._is_already_returned(picking): + # raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") if picking.picking_type_id.id == 30 and picking.linked_manual_bu_out.state == 'done': raise UserError("❌ BU/PICK tidak dapat di retur karena BU/OUT Sudah Done") -- cgit v1.2.3 From 3c4f2eff3035f423d3bf0dc19d5aad457854e43a Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 27 Jun 2025 22:03:37 +0700 Subject: done aman --- indoteknik_custom/models/stock_picking_return.py | 10 +-- indoteknik_custom/models/tukar_guling.py | 84 +++++++++++------------- 2 files changed, 45 insertions(+), 49 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking_return.py b/indoteknik_custom/models/stock_picking_return.py index f7900a27..3442496d 100644 --- a/indoteknik_custom/models/stock_picking_return.py +++ b/indoteknik_custom/models/stock_picking_return.py @@ -19,8 +19,8 @@ class StockReturnPicking(models.TransientModel): # raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") # if self._is_already_returned(picking): # raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") - if picking.picking_type_id.id == 30 and picking.linked_manual_bu_out.state == 'done': - raise UserError("❌ BU/PICK tidak dapat di retur karena BU/OUT Sudah Done") + # if picking.picking_type_id.id == 30 and picking.linked_manual_bu_out.state == 'done': + # raise UserError("❌ BU/PICK tidak dapat di retur karena BU/OUT Sudah Done") if self._context.get('from_ui', True): return self._redirect_to_tukar_guling() @@ -31,8 +31,8 @@ class StockReturnPicking(models.TransientModel): self.ensure_one() picking = self.picking_id - if picking.picking_type_id.id == 30 and picking.linked_manual_bu_out.state == 'done': - raise UserError("❌ BU/PICK tidak dapat di retur karena BU/OUT Sudah Done") + # if picking.picking_type_id.id == 30 and picking.linked_manual_bu_out.state == 'done': + # raise UserError("❌ BU/PICK tidak dapat di retur karena BU/OUT Sudah Done") # Get valid return lines with better error handling valid_lines = [] @@ -79,7 +79,7 @@ class StockReturnPicking(models.TransientModel): # 'default_return_type': 'tukar_guling', 'default_date': fields.Datetime.now(), 'default_state': 'draft', - 'default_ba_num': _('Retur dari %s') % picking.name, + 'default_notes': _('Retur dari %s') % picking.name, 'from_return_picking': True, # Flag to prevent onchange from overriding lines } diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index df501261..42646fbf 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -234,8 +234,9 @@ class TukarGuling(models.Model): return new_record def write(self, vals): - if self._is_already_returned(self.operations): - raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") + if self.operations.picking_type_id.id != 30: + if self._is_already_returned(self.operations): + raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") if 'operations' in vals and not vals.get('origin'): picking = self.env['stock.picking'].browse(vals['operations']) if picking.origin: @@ -271,12 +272,10 @@ class TukarGuling(models.Model): elif picking.picking_type_id.id == 30: linked_bu_out = picking.linked_manual_bu_out if linked_bu_out and linked_bu_out.state == 'done': - raise UserError("Tidak bisa retur BU/PICK karena BU/OUT suda Done!") - else: - raise UserError("Dokumen Mungkin sudah done") - - if self._is_already_returned(self.operations): - raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") + raise UserError("❌ Tidak bisa retur BU/PICK karena BU/OUT suda Done!") + if self.operations.picking_type_id.id != 30: + if self._is_already_returned(self.operations): + raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") self._validate_product_lines() @@ -371,34 +370,24 @@ class TukarGuling(models.Model): BU_STOCK_LOCATION_ID = 57 # Determine locations based on picking type - if picking.picking_type_id.id == 30: # -> ngeretur bu pick + if picking.picking_type_id.id == 30: return_type = ort_type default_location_id = BU_OUTPUT_LOCATION_ID default_location_dest_id = BU_STOCK_LOCATION_ID - if not default_location_id or not default_location_dest_id: - raise UserError("Lokasi Origin atau Destination salah.") - elif picking.picking_type_id.id == 29: # -> ngeretur bu out + elif picking.picking_type_id.id == 29: return_type = srt_type default_location_id = PARTNER_LOCATION_ID default_location_dest_id = BU_OUTPUT_LOCATION_ID - if not default_location_id or not default_location_dest_id: - raise UserError("Lokasi Origin atau Destination salah.") - elif picking.picking_type_id.id == 74: # -> ngeretur srt - return_type = bu_pick_type - default_location_id = BU_STOCK_LOCATION_ID - default_location_dest_id = BU_OUTPUT_LOCATION_ID - if not default_location_id or not default_location_dest_id: - raise UserError("Lokasi Origin atau Destination salah.") - elif picking.picking_type_id.id == 73: # -> ngeretur ort + elif picking.picking_type_id.id == 74: return_type = bu_out_type default_location_id = BU_OUTPUT_LOCATION_ID default_location_dest_id = PARTNER_LOCATION_ID - if not default_location_id or not default_location_dest_id: - raise UserError("Lokasi Origin atau Destination salah.") + elif picking.picking_type_id.id == 73: + return_type = bu_pick_type + default_location_id = BU_STOCK_LOCATION_ID + default_location_dest_id = BU_OUTPUT_LOCATION_ID else: - raise UserError("Hayo") return None - return_context = dict(self.env.context) return_context.update({ 'active_id': picking.id, @@ -450,25 +439,32 @@ class TukarGuling(models.Model): return return_picking - # Buat return dari BU/OUT - for picking in bu_out_to_return: - name = _create_return_from_picking(picking) - if name: - created_returns.append(name) - - # Buat return dari BU/PICK - for picking in bu_pick_to_return: - name = _create_return_from_picking(picking) - if name: - created_returns.append(name) - - # Buat return dari SRT dan ort - if record.return_type == 'tukar_guling': - target = [woi for woi in created_returns if woi.picking_type_id.id in (74, 73)] - for picking in target: - retur = _create_return_from_picking(picking) - if retur: - created_returns.append(retur) + if record.operations.picking_type_id.id == 30: + ort = _create_return_from_picking(record.operations) + if ort: + created_returns.append(ort) + else: + # CASE: Retur dari BU/OUT + srt = _create_return_from_picking(bu_out_to_return) + if srt: + created_returns.append(srt) + + ort = None + if bu_pick_to_return: + ort = _create_return_from_picking(bu_pick_to_return) + if ort: + created_returns.append(ort) + + if record.return_type == 'tukar_guling': + if srt: + bu_pick = _create_return_from_picking(srt) + if bu_pick: + created_returns.append(bu_pick) + + if ort: + bu_out = _create_return_from_picking(ort) + if bu_out: + created_returns.append(bu_out) if not created_returns: raise UserError("wkwkwk") -- cgit v1.2.3 From 498b7f73857189d1b22204c3f71f35d03ec4afb7 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 1 Jul 2025 09:36:21 +0700 Subject: add sale order on po bom and add bom on po bom --- indoteknik_custom/models/mrp_production.py | 5 +++-- indoteknik_custom/models/purchase_order.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/mrp_production.py b/indoteknik_custom/models/mrp_production.py index 14821f27..85b8405f 100644 --- a/indoteknik_custom/models/mrp_production.py +++ b/indoteknik_custom/models/mrp_production.py @@ -110,8 +110,9 @@ class MrpProduction(models.Model): 'picking_type_id': 28, # indoteknik bandengan receipts 'date_order': current_time, 'product_bom_id': self.product_id.id, - # 'sale_order_id': self.sale_order_id.id, - 'note_description': 'from Manufacturing Order' + 'sale_order_id': self.sale_order.id, + 'manufacturing_id': self.id, + 'note_description': 'from Manufacturing Order', } domain = [ diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 1a7e50f8..a3941b3b 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -97,6 +97,7 @@ class PurchaseOrder(models.Model): string="BU Related Count", compute='_compute_bu_related_count' ) + manufacturing_id = fields.Many2one('mrp.production', string='Manufacturing Orders') @api.depends('name') def _compute_bu_related_count(self): -- cgit v1.2.3 From 9677d61248b4399239c6e0eccac57a6f945ec58c Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 1 Jul 2025 16:32:45 +0700 Subject: (andri) rajaongkir V2 --- indoteknik_custom/models/sale_order.py | 138 +++++++++++++++++++++------------ 1 file changed, 90 insertions(+), 48 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 109771e9..bc830e2b 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -845,25 +845,32 @@ class SaleOrder(models.Model): if total_weight == 0: raise UserError("Tidak dapat mengestimasi ongkir tanpa berat yang valid.") - destination_subsdistrict_id = self.real_shipping_id.kecamatan_id.rajaongkir_id + kecamatan_name = self.real_shipping_id.kecamatan_id.name + kota_name = self.real_shipping_id.kota_id.name + + destination_subsdistrict_id = self._get_subdistrict_id_from_komerce(kecamatan_name, kota_name) + + # destination_subsdistrict_id = self.real_shipping_id.kecamatan_id.rajaongkir_id if not destination_subsdistrict_id: raise UserError("Gagal mendapatkan ID kota tujuan.") result = self._call_rajaongkir_api(total_weight, destination_subsdistrict_id) if result: shipping_options = [] - for courier in result['rajaongkir']['results']: - for cost_detail in courier['costs']: - service = cost_detail['service'] - description = cost_detail['description'] - etd = cost_detail['cost'][0]['etd'] - value = cost_detail['cost'][0]['value'] - shipping_options.append((service, description, etd, value, courier['code'])) - + + for cost in result.get('data', []): + service = cost.get('service') + description = cost.get('description') + etd = cost.get('etd', '') + value = cost.get('cost', 0) + provider = cost.get('code') + + shipping_options.append((service, description, etd, value, provider)) + self.env["shipping.option"].search([('sale_order_id', '=', self.id)]).unlink() _logger.info(f"Shipping options: {shipping_options}") - + for service, description, etd, value, provider in shipping_options: self.env["shipping.option"].create({ "name": service, @@ -873,19 +880,15 @@ class SaleOrder(models.Model): "sale_order_id": self.id, }) - self.shipping_option_id = self.env["shipping.option"].search([('sale_order_id', '=', self.id)], limit=1).id _logger.info(f"Shipping option SO ID: {self.shipping_option_id}") self.message_post( body=f"Estimasi Ongkos Kirim: Rp{self.delivery_amt}
Detail Lain:
" - f"{'
'.join([f'Service: {s[0]}, Description: {s[1]}, ETD: {s[2]} hari, Cost: Rp {s[3]}' for s in shipping_options])}", + f"{'
'.join([f'Service: {s[0]}, Description: {s[1]}, ETD: {s[2]}, Cost: Rp {s[3]}' for s in shipping_options])}", message_type="comment" ) - - # self.message_post(body=f"Estimasi Ongkos Kirim: Rp{self.delivery_amt}
Detail Lain:
{'
'.join([f'Service: {s[0]}, Description: {s[1]}, ETD: {s[2]} hari, Cost: Rp {s[3]}' for s in shipping_options])}", message_type="comment") - else: raise UserError("Gagal mendapatkan estimasi ongkir.") @@ -1191,25 +1194,30 @@ class SaleOrder(models.Model): def _call_rajaongkir_api(self, total_weight, destination_subsdistrict_id): - url = 'https://pro.rajaongkir.com/api/cost' + url = 'https://rajaongkir.komerce.id/api/v1/calculate/domestic-cost' headers = { 'key': '9b1310f644056d84d60b0af6bb21611a', } courier = self.carrier_id.name.lower() data = { - 'origin': 2127, - 'originType': 'subdistrict', + 'origin': 17656, + # 'originType': 'subdistrict', 'destination': int(destination_subsdistrict_id), - 'destinationType': 'subdistrict', + # 'destinationType': 'subdistrict', 'weight': int(total_weight * 1000), 'courier': courier, } - response = requests.post(url, headers=headers, data=data) - if response.status_code == 200: - return response.json() - return None + try: + _logger.info(f"Calling RajaOngkir API with data: {data}") + response = requests.post(url, headers=headers, data=data) + _logger.info(f"RajaOngkir response: {response.status_code} - {response.text}") + + if response.status_code == 200: + return response.json() + except Exception as e: + _logger.error(f"Exception while calling RajaOngkir: {str(e)}") def _normalize_city_name(self, city_name): city_name = city_name.lower() @@ -1223,37 +1231,71 @@ class SaleOrder(models.Model): return city_name - def _get_city_id_by_name(self, city_name): - url = 'https://pro.rajaongkir.com/api/city' + # def _get_city_id_by_name(self, city_name): + # url = 'https://pro.rajaongkir.com/api/city' + # headers = { + # 'key': '9b1310f644056d84d60b0af6bb21611a', + # } + + # normalized_city_name = self._normalize_city_name(city_name) + + # response = requests.get(url, headers=headers) + # if response.status_code == 200: + # city_data = response.json() + # for city in city_data['rajaongkir']['results']: + # if city['city_name'].lower() == normalized_city_name: + # return city['city_id'] + # return None + + # def _get_subdistrict_id_by_name(self, city_id, subdistrict_name): + # url = f'https://pro.rajaongkir.com/api/subdistrict?city={city_id}' + # headers = { + # 'key': '9b1310f644056d84d60b0af6bb21611a', + # } + + # response = requests.get(url, headers=headers) + # if response.status_code == 200: + # subdistrict_data = response.json() + # for subdistrict in subdistrict_data['rajaongkir']['results']: + # subsdistrict_1 = subdistrict['subdistrict_name'].lower() + # subsdistrict_2 = subdistrict_name.lower() + + # if subsdistrict_1 == subsdistrict_2: + # return subdistrict['subdistrict_id'] + # return None + + def _get_subdistrict_id_from_komerce(self, kecamatan_name, kota_name): + url = 'https://rajaongkir.komerce.id/api/v1/destination/domestic-destination' headers = { 'key': '9b1310f644056d84d60b0af6bb21611a', } - - normalized_city_name = self._normalize_city_name(city_name) - - response = requests.get(url, headers=headers) - if response.status_code == 200: - city_data = response.json() - for city in city_data['rajaongkir']['results']: - if city['city_name'].lower() == normalized_city_name: - return city['city_id'] - return None - - def _get_subdistrict_id_by_name(self, city_id, subdistrict_name): - url = f'https://pro.rajaongkir.com/api/subdistrict?city={city_id}' - headers = { - 'key': '9b1310f644056d84d60b0af6bb21611a', + search = f"{kecamatan_name} {kota_name}" + params = { + 'search': search, + 'limit': 5 } - response = requests.get(url, headers=headers) - if response.status_code == 200: - subdistrict_data = response.json() - for subdistrict in subdistrict_data['rajaongkir']['results']: - subsdistrict_1 = subdistrict['subdistrict_name'].lower() - subsdistrict_2 = subdistrict_name.lower() + try: + response = requests.get(url, headers=headers, params=params, timeout=10) + if response.status_code == 200: + data = response.json().get('data', []) + _logger.info(f"[Komerce] Fetched {len(data)} subdistricts for search '{search}'") + _logger.info(f"[Komerce] Response: {data}") + + normalized_kota = self._normalize_city_name(kota_name) + for item in data: + if ( + item.get('subdistrict_name', '').lower() == kecamatan_name.lower() and + item.get('city_name', '').lower() == normalized_kota + ): + return item.get('id') + + _logger.warning(f"[Komerce] No match for '{kecamatan_name}' in city '{kota_name}'") + else: + _logger.error(f"[Komerce] HTTP Error {response.status_code}: {response.text}") + except Exception as e: + _logger.error(f"[Komerce] Exception: {e}") - if subsdistrict_1 == subsdistrict_2: - return subdistrict['subdistrict_id'] return None def _compute_type_promotion(self): -- cgit v1.2.3 From 84a038cff26c28bd714fd6744f48c2b0e91cf347 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 1 Jul 2025 17:32:02 +0700 Subject: (andri) fix API destination --- indoteknik_custom/models/sale_order.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index bc830e2b..4b4e06cd 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -847,8 +847,9 @@ class SaleOrder(models.Model): kecamatan_name = self.real_shipping_id.kecamatan_id.name kota_name = self.real_shipping_id.kota_id.name + kelurahan_name = self.real_shipping_id.kelurahan_id.name - destination_subsdistrict_id = self._get_subdistrict_id_from_komerce(kecamatan_name, kota_name) + destination_subsdistrict_id = self._get_subdistrict_id_from_komerce(kecamatan_name, kota_name, kelurahan_name) # destination_subsdistrict_id = self.real_shipping_id.kecamatan_id.rajaongkir_id if not destination_subsdistrict_id: @@ -1264,15 +1265,20 @@ class SaleOrder(models.Model): # return subdistrict['subdistrict_id'] # return None - def _get_subdistrict_id_from_komerce(self, kecamatan_name, kota_name): + def _get_subdistrict_id_from_komerce(self, kecamatan_name, kota_name, kelurahan_name=None): url = 'https://rajaongkir.komerce.id/api/v1/destination/domestic-destination' headers = { 'key': '9b1310f644056d84d60b0af6bb21611a', } - search = f"{kecamatan_name} {kota_name}" + + if kelurahan_name: + search = f"{kelurahan_name} {kecamatan_name} {kota_name}" + else: + search = f"{kecamatan_name} {kota_name}" + params = { 'search': search, - 'limit': 5 + 'limit': 10 } try: @@ -1283,14 +1289,20 @@ class SaleOrder(models.Model): _logger.info(f"[Komerce] Response: {data}") normalized_kota = self._normalize_city_name(kota_name) + for item in data: + match_kelurahan = ( + not kelurahan_name or + item.get('subdistrict_name', '').lower() == kelurahan_name.lower() + ) if ( - item.get('subdistrict_name', '').lower() == kecamatan_name.lower() and + match_kelurahan and + item.get('district_name', '').lower() == kecamatan_name.lower() and item.get('city_name', '').lower() == normalized_kota ): return item.get('id') - _logger.warning(f"[Komerce] No match for '{kecamatan_name}' in city '{kota_name}'") + _logger.warning(f"[Komerce] No match for '{kecamatan_name}' in city '{kota_name}' with kelurahan '{kelurahan_name}'") else: _logger.error(f"[Komerce] HTTP Error {response.status_code}: {response.text}") except Exception as e: -- cgit v1.2.3 From f1f2b012ed156213a623858cb3fb816e6a795f3c Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 2 Jul 2025 10:20:27 +0700 Subject: (andri) fix pay diff --- indoteknik_custom/models/res_partner.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index f5347bea..1e5cfd62 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -165,21 +165,21 @@ class ResPartner(models.Model): "this feature", tracking=3) telegram_id = fields.Char(string="Telegram") avg_aging= fields.Float(string='Average Aging') - payment_difficulty = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', compute="_compute_payment_difficulty", inverse = "_inverse_payment_difficulty", tracking=3) + payment_difficulty = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', compute="", inverse = "", tracking=3) payment_history_url = fields.Text(string='Payment History URL') - @api.depends('parent_id.payment_difficulty') - def _compute_payment_difficulty(self): - for partner in self: - if partner.parent_id: - partner.payment_difficulty = partner.parent_id.payment_difficulty - - def _inverse_payment_difficulty(self): - for partner in self: - if not partner.parent_id: - partner.child_ids.write({ - 'payment_difficulty': partner.payment_difficulty - }) + # @api.depends('parent_id.payment_difficulty') + # def _compute_payment_difficulty(self): + # for partner in self: + # if partner.parent_id: + # partner.payment_difficulty = partner.parent_id.payment_difficulty + + # def _inverse_payment_difficulty(self): + # for partner in self: + # if not partner.parent_id: + # partner.child_ids.write({ + # 'payment_difficulty': partner.payment_difficulty + # }) @api.model def _default_payment_term(self): -- cgit v1.2.3 From b74109805a2ec65cb4a4b7811fdc34403d2505b2 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 2 Jul 2025 11:38:18 +0700 Subject: (andri) tambah validasi jika kurir tidak mendukung --- indoteknik_custom/models/sale_order.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 4b4e06cd..74d96314 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -856,6 +856,9 @@ class SaleOrder(models.Model): raise UserError("Gagal mendapatkan ID kota tujuan.") result = self._call_rajaongkir_api(total_weight, destination_subsdistrict_id) + if not result or not result.get('data'): + raise UserError(_("Kurir %s tidak tersedia untuk tujuan ini. Silakan pilih kurir lain.") % self.carrier_id.name) + if result: shipping_options = [] @@ -1278,7 +1281,7 @@ class SaleOrder(models.Model): params = { 'search': search, - 'limit': 10 + 'limit': 5 } try: -- cgit v1.2.3 From 5a3c3d327dd04b3ec4f3c272e4bc50ab9c594058 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 3 Jul 2025 09:20:44 +0700 Subject: (andri) fix --- indoteknik_custom/models/res_partner.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index 1e5cfd62..f5347bea 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -165,21 +165,21 @@ class ResPartner(models.Model): "this feature", tracking=3) telegram_id = fields.Char(string="Telegram") avg_aging= fields.Float(string='Average Aging') - payment_difficulty = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', compute="", inverse = "", tracking=3) + payment_difficulty = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', compute="_compute_payment_difficulty", inverse = "_inverse_payment_difficulty", tracking=3) payment_history_url = fields.Text(string='Payment History URL') - # @api.depends('parent_id.payment_difficulty') - # def _compute_payment_difficulty(self): - # for partner in self: - # if partner.parent_id: - # partner.payment_difficulty = partner.parent_id.payment_difficulty - - # def _inverse_payment_difficulty(self): - # for partner in self: - # if not partner.parent_id: - # partner.child_ids.write({ - # 'payment_difficulty': partner.payment_difficulty - # }) + @api.depends('parent_id.payment_difficulty') + def _compute_payment_difficulty(self): + for partner in self: + if partner.parent_id: + partner.payment_difficulty = partner.parent_id.payment_difficulty + + def _inverse_payment_difficulty(self): + for partner in self: + if not partner.parent_id: + partner.child_ids.write({ + 'payment_difficulty': partner.payment_difficulty + }) @api.model def _default_payment_term(self): -- cgit v1.2.3 From eac293a01a1cdf5e7d2be15575f96e17ebe33a4d Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 3 Jul 2025 09:49:37 +0700 Subject: (Andri) fix related BU pada PO --- indoteknik_custom/models/purchase_order.py | 60 ++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 19 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index a3941b3b..4dc26d74 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -101,43 +101,65 @@ class PurchaseOrder(models.Model): @api.depends('name') def _compute_bu_related_count(self): + StockPicking = self.env['stock.picking'] for order in self: if not order.name: order.bu_related_count = 0 continue - # BU langsung dari PO - base_bu = self.env['stock.picking'].search([ + # Ambil semua BU awal dari PO + base_bu = StockPicking.search([ ('name', 'ilike', 'BU/'), ('origin', 'ilike', order.name) ]) - base_names = base_bu.mapped('name') - # Return dari BU di atas - return_bu = self.env['stock.picking'].search([ - ('origin', 'in', [f"Return of {name}" for name in base_names]) - ]) + all_bu = base_bu + seen_names = set(base_bu.mapped('name')) + + # Loop rekursif untuk mencari seluruh return BU + while True: + next_bu = StockPicking.search([ + ('name', 'ilike', 'BU/'), + ('origin', 'in', ['Return of %s' % name for name in seen_names]) + ]) + next_names = set(next_bu.mapped('name')) + + if not next_names - seen_names: + break + + all_bu |= next_bu + seen_names |= next_names + + order.bu_related_count = len(all_bu) - order.bu_related_count = len(base_bu) + len(return_bu) def action_view_related_bu(self): self.ensure_one() + StockPicking = self.env['stock.picking'] + # Step 1: cari semua BU pertama (PUT, INT) yang berasal dari PO ini - base_bu = self.env['stock.picking'].search([ + base_bu = StockPicking.search([ ('name', 'ilike', 'BU/'), ('origin', 'ilike', self.name) ]) - base_bu_names = base_bu.mapped('name') - # Step 2: cari BU turunan (seperti BU/VRT) yang origin-nya mengandung nama BU tersebut - domain = [ - '|', - '&', - ('name', 'ilike', 'BU/'), - ('origin', 'ilike', self.name), - ('origin', 'in', [f"Return of {name}" for name in base_bu_names]) - ] + all_bu = base_bu + seen_names = set(base_bu.mapped('name')) + + # Step 2: Loop rekursif cari BU dengan origin 'Return of {name}' + while True: + next_bu = StockPicking.search([ + ('name', 'ilike', 'BU/'), + ('origin', 'in', ['Return of %s' % name for name in seen_names]) + ]) + next_names = set(next_bu.mapped('name')) + + if not next_names - seen_names: + break + + all_bu |= next_bu + seen_names |= next_names return { 'name': 'Related BU (INT/PRT/PUT/VRT)', @@ -145,7 +167,7 @@ class PurchaseOrder(models.Model): 'res_model': 'stock.picking', 'view_mode': 'tree,form', 'target': 'current', - 'domain': domain, + 'domain': [('id', 'in', list(all_bu.ids))], } -- cgit v1.2.3 From 90e26bd8d666503c2bb02608e3e5c26f15ab777b Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 3 Jul 2025 11:44:57 +0700 Subject: rev validation bu pick and prepare for tukar guling po --- indoteknik_custom/models/tukar_guling.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 42646fbf..20eb598d 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -12,9 +12,9 @@ class TukarGuling(models.Model): _rec_name = 'name' origin = fields.Char(string='Origin SO') - + if_so = fields.Boolean('Is SO', default=True) + if_po = fields.Boolean('Is PO', default=False) real_shipping_id = fields.Many2one('res.partner', string='Shipping Address') - picking_ids = fields.One2many( 'stock.picking', 'tukar_guling_id', @@ -265,9 +265,9 @@ class TukarGuling(models.Model): def action_submit(self): self.ensure_one() - picking = self.env['stock.picking'] + picking = self.operations if picking.picking_type_id.id == 29: - if picking.picking_type_id.state != 'done': + if picking.state != 'done': raise UserError("BU/OUT belum Done!") elif picking.picking_type_id.id == 30: linked_bu_out = picking.linked_manual_bu_out -- cgit v1.2.3 From db60e29b2f599ac21e96ffdfb5be94e3c0ba6a2f Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 3 Jul 2025 16:16:32 +0700 Subject: (andri) fix eta date reserved computed --- indoteknik_custom/models/sale_order.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 74d96314..591951ca 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -388,7 +388,9 @@ class SaleOrder(models.Model): pickings = order.picking_ids.filtered( lambda p: p.state in ('assigned', 'done') and p.date_reserved and 'BU/PICK/' in (p.name or '') ) - order.eta_date_reserved = min(pickings.mapped('date_done')) if pickings else False + done_dates = [d for d in pickings.mapped('date_done') if d] + order.eta_date_reserved = min(done_dates) if done_dates else False + # order.eta_date_reserved = min(pickings.mapped('date_done')) if pickings else False @api.onchange('shipping_cost_covered') def _onchange_shipping_cost_covered(self): -- cgit v1.2.3 From de50bcf16b21abf7b8e45fb59b366c594bc00038 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 3 Jul 2025 19:55:49 +0700 Subject: start tukar guling po --- indoteknik_custom/models/__init__.py | 1 + indoteknik_custom/models/stock_picking_return.py | 130 +++--- indoteknik_custom/models/tukar_guling.py | 14 +- indoteknik_custom/models/tukar_guling_po.py | 485 +++++++++++++++++++++++ 4 files changed, 544 insertions(+), 86 deletions(-) create mode 100644 indoteknik_custom/models/tukar_guling_po.py (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index 8f08828b..903c0745 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -152,3 +152,4 @@ from . import stock_inventory from . import sale_order_delay from . import approval_invoice_date from . import tukar_guling +from . import tukar_guling_po diff --git a/indoteknik_custom/models/stock_picking_return.py b/indoteknik_custom/models/stock_picking_return.py index 3442496d..a9781d3c 100644 --- a/indoteknik_custom/models/stock_picking_return.py +++ b/indoteknik_custom/models/stock_picking_return.py @@ -1,6 +1,8 @@ from odoo.exceptions import UserError from odoo.tools.float_utils import float_round from odoo import models, fields, api, _ +import logging +_logger = logging.getLogger(__name__) class StockReturnPicking(models.TransientModel): @@ -27,116 +29,86 @@ class StockReturnPicking(models.TransientModel): return super(StockReturnPicking, self).create_returns() def _redirect_to_tukar_guling(self): - """Redirect to Tukar Guling form with pre-filled data""" + """Redirect ke Tukar Guling SO atau PO form dengan pre-filled data""" self.ensure_one() picking = self.picking_id - # if picking.picking_type_id.id == 30 and picking.linked_manual_bu_out.state == 'done': - # raise UserError("❌ BU/PICK tidak dapat di retur karena BU/OUT Sudah Done") - - # Get valid return lines with better error handling + # Ambil lines valid valid_lines = [] + self.env.cr.execute("SELECT id FROM stock_return_picking_line WHERE wizard_id = %s", (self.id,)) + line_ids = [row[0] for row in self.env.cr.fetchall()] + if line_ids: + existing_lines = self.env['stock.return.picking.line'].sudo().browse(line_ids) + for line in existing_lines: + if line.exists() and line.quantity > 0: + valid_lines.append(line) - try: - # Refresh the recordset to ensure we have the latest data - self.env.cr.execute("SELECT id FROM stock_return_picking_line WHERE wizard_id = %s", (self.id,)) - line_ids = [row[0] for row in self.env.cr.fetchall()] - - if line_ids: - # Use sudo to avoid access rights issues and browse existing lines - existing_lines = self.env['stock.return.picking.line'].sudo().browse(line_ids) - for line in existing_lines: - if line.exists() and line.quantity > 0: - valid_lines.append(line) - - # If no lines found via direct query, try the original approach - if not valid_lines: - for line in self.product_return_moves: - if hasattr(line, 'quantity') and line.quantity > 0: - # Additional check to ensure the line is valid - if line.product_id and line.move_id: - valid_lines.append(line) - - except Exception as e: - # Fallback: create lines based on picking moves - valid_lines = [] - for move in picking.move_ids_without_package: - if move.product_uom_qty > 0 and move.state == 'done': - # Create a temporary line object for data extraction - temp_line = type('TempLine', (), { - 'product_id': move.product_id, - 'quantity': move.quantity_done or move.product_uom_qty, - 'move_id': move - })() - valid_lines.append(temp_line) + if not valid_lines: + for line in self.product_return_moves: + if hasattr(line, 'quantity') and line.quantity > 0: + valid_lines.append(line) if not valid_lines: raise UserError(_("Tidak ada produk yang bisa diretur. Pastikan ada produk dengan quantity > 0.")) - # Prepare context for Tukar Guling form + # Siapkan context context = { 'default_operations': picking.id, - # 'default_return_type': 'tukar_guling', 'default_date': fields.Datetime.now(), 'default_state': 'draft', 'default_notes': _('Retur dari %s') % picking.name, - 'from_return_picking': True, # Flag to prevent onchange from overriding lines + 'from_return_picking': True, } - - # Set origin if picking.origin: context['default_origin'] = picking.origin - - # Set partner if picking.partner_id: context['default_partner_id'] = picking.partner_id.id - - # Set shipping address if hasattr(picking, 'real_shipping_id') and picking.real_shipping_id: context['default_real_shipping_id'] = picking.real_shipping_id.id elif picking.partner_id: context['default_real_shipping_id'] = picking.partner_id.id - # Prepare product lines + # Siapkan product lines line_vals = [] sequence = 10 - for line in valid_lines: - try: - # Get quantity - handle both real lines and temp objects - quantity = getattr(line, 'quantity', 0) - if quantity <= 0: - continue - - # Get product - product = getattr(line, 'product_id', None) - if not product: - continue - - line_vals.append((0, 0, { - 'sequence': sequence, - 'product_id': product.id, - 'product_uom_qty': quantity, - 'product_uom': product.uom_id.id, - 'name': product.display_name, - })) - sequence += 10 - - except Exception as e: - # Skip problematic lines + quantity = getattr(line, 'quantity', 0) + if quantity <= 0: continue - + product = getattr(line, 'product_id', None) + if not product: + continue + line_vals.append((0, 0, { + 'sequence': sequence, + 'product_id': product.id, + 'product_uom_qty': quantity, + 'product_uom': product.uom_id.id, + 'name': product.display_name, + })) + sequence += 10 if line_vals: context['default_line_ids'] = line_vals - return { - 'name': _('Tukar Guling'), - 'type': 'ir.actions.act_window', - 'res_model': 'tukar.guling', - 'view_mode': 'form', - 'target': 'current', - 'context': context, - } + if picking.purchase_id or 'PO' in picking.origin: + _logger.info("Redirect ke Tukar Guling PO via purchase_id / origin") + return { + 'name': _('Tukar Guling PO'), + 'type': 'ir.actions.act_window', + 'res_model': 'tukar.guling.po', + 'view_mode': 'form', + 'target': 'current', + 'context': context, + } + else: + _logger.info("This picking is NOT from a PO, fallback to SO.") + return { + 'name': _('Tukar Guling SO'), + 'type': 'ir.actions.act_window', + 'res_model': 'tukar.guling', + 'view_mode': 'form', + 'target': 'current', + 'context': context, + } class ReturnPickingLine(models.TransientModel): diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 20eb598d..eeec2d80 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -44,7 +44,7 @@ class TukarGuling(models.Model): notes = fields.Text('Notes') return_type = fields.Selection(String='Return Type', selection=[ ('tukar_guling', 'Tukar Guling'), # -> barang yang sama - ('revisi_so', 'Revisi SO')]) + ('revisi_so', 'Revisi SO')], required=True) state = fields.Selection(string='Status', selection=[ ('draft', 'Draft'), ('approval_sales', ' Approval Sales'), @@ -457,15 +457,15 @@ class TukarGuling(models.Model): if record.return_type == 'tukar_guling': if srt: - bu_pick = _create_return_from_picking(srt) - if bu_pick: - created_returns.append(bu_pick) - - if ort: - bu_out = _create_return_from_picking(ort) + bu_out = _create_return_from_picking(srt) if bu_out: created_returns.append(bu_out) + if ort: + bu_pick = _create_return_from_picking(ort) + if bu_pick: + created_returns.append(bu_pick) + if not created_returns: raise UserError("wkwkwk") diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py new file mode 100644 index 00000000..4ed363cf --- /dev/null +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -0,0 +1,485 @@ +from email.policy import default + +from odoo import models, fields, api, _ +from odoo.exceptions import UserError, ValidationError +import logging + +_logger = logging.getLogger(__name__) + +class TukarGulingPO(models.Model): + _name = 'tukar.guling.po' + _description = 'Tukar Guling PO' + + origin = fields.Char(string='Origin PO') + is_po = fields.Boolean('Is PO', default=True) + is_so = fields.Boolean('Is SO', default=False) + name = fields.Char(string='Name', required=True) + po_picking_ids = fields.One2many( + 'stock.picking', + 'tukar_guling_po_id', + string='Picking Reference', + ) + name = fields.Char('Number', required=True, copy=False, readonly=True, default='New') + date = fields.Datetime('Date', default=fields.Datetime.now, required=True) + operations = fields.Many2one( + 'stock.picking', + string='Operations', + domain=[ + ('picking_type_id.id', 'in', [75, 32]), + ('state', '=', 'done') + ],help='Nomor BU INPUT atau BU PUT' + ) + ba_num = fields.Char('Nomor BA') + return_type = fields.Selection([ + ('revisi_po', 'Revisi PO'), + ('tukar_guling', 'Tukar Guling'), + ], string='Return Type', required=True) + notes = fields.Text('Notes') + tukar_guling_po_id = fields.Many2one('tukar.guling.po', string='Tukar Guling PO', ondelete='cascade') + line_ids = fields.One2many('tukar.guling.line.po', 'tukar_guling_po_id', string='Product Lines') + state = fields.Selection([ + ('draft', 'Draft'), + ('approval_purchase', 'Approval Purchasing'), + ('approval_logistic', 'Approval Logistic'), + ('approval_finance', 'Approval Finance'), + ('done', 'Done'), + ('cancel', 'Cancel'), + ], string='Status', default='draft') + + @api.model + def create(self, vals): + # Generate sequence number + if not vals.get('name') or vals['name'] == 'New': + sequence = self.env['ir.sequence'].search([('code', '=', 'tukar.guling.po')], limit=1) + if sequence: + vals['name'] = sequence.next_by_id() + else: + # Fallback jika sequence belum dibuat + vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling.po') or 'embo==' + + # Auto-fill origin from operations + if not vals.get('origin') and vals.get('operations'): + picking = self.env['stock.picking'].browse(vals['operations']) + if picking.origin: + vals['origin'] = picking.origin + + return super(TukarGulingPO, self).create(vals) + + @api.onchange('operations') + def _onchange_operations(self): + """Auto-populate lines ketika operations dipilih""" + if self.operations: + from_return_picking = self.env.context.get('from_return_picking', False) or \ + self.env.context.get('default_line_ids', False) + + if self.line_ids and from_return_picking: + # Hanya update origin, jangan ubah lines + if self.operations.origin: + self.origin = self.operations.origin + return + + # Clear existing lines hanya jika tidak dari return picking + self.line_ids = [(5, 0, 0)] + + # Set origin dari operations + if self.operations.origin: + self.origin = self.operations.origin + + # Auto-populate lines dari move_ids operations + lines_data = [] + sequence = 10 + + # Untuk Odoo 14, gunakan move_ids_without_package atau move_lines + moves_to_check = [] + + # 1. move_ids_without_package (standard di Odoo 14) + if hasattr(self.operations, 'move_ids_without_package') and self.operations.move_ids_without_package: + moves_to_check = self.operations.move_ids_without_package + # 2. move_lines (backup untuk versi lama) + elif hasattr(self.operations, 'move_lines') and self.operations.move_lines: + moves_to_check = self.operations.move_lines + + for move in moves_to_check: + _logger.info( + f"Move: {move.name}, Product: {move.product_id.name if move.product_id else 'No Product'}, Qty: {move.product_uom_qty}, State: {move.state}") + + # Ambil semua move yang ada quantity + if move.product_id and move.product_uom_qty > 0: + lines_data.append((0, 0, { + 'sequence': sequence, + 'product_id': move.product_id.id, + 'product_uom_qty': move.product_uom_qty, + 'product_uom': move.product_uom.id, + 'name': move.name or move.product_id.display_name, + })) + sequence += 10 + + if lines_data: + self.line_ids = lines_data + _logger.info(f"Created {len(lines_data)} lines") + else: + _logger.info("No lines created - no valid moves found") + else: + # Clear lines jika operations dikosongkan, kecuali dari return picking + from_return_picking = self.env.context.get('from_return_picking', False) or \ + self.env.context.get('default_line_ids', False) + + if not from_return_picking: + self.line_ids = [(5, 0, 0)] + + self.origin = False + + def action_populate_lines(self): + """Manual button untuk populate lines - sebagai alternatif""" + self.ensure_one() + if not self.operations: + raise UserError("Pilih BU/OUT atau BU/PICK terlebih dahulu!") + + # Clear existing lines + self.line_ids = [(5, 0, 0)] + + lines_data = [] + sequence = 10 + + # Ambil semua stock moves dari operations + for move in self.operations.move_ids: + if move.product_uom_qty > 0: + lines_data.append((0, 0, { + 'sequence': sequence, + 'product_id': move.product_id.id, + 'product_uom_qty': move.product_uom_qty, + 'product_uom': move.product_uom.id, + 'name': move.name or move.product_id.display_name, + })) + sequence += 10 + + if lines_data: + self.line_ids = lines_data + else: + raise UserError("Tidak ditemukan barang di BU/OUT yang dipilih!") + + @api.constrains('return_type', 'operations') + def _check_required_bu_fields(self): + for record in self: + if record.return_type in ['revisi_po', 'tukar_guling'] and not record.operations: + raise ValidationError("Operations harus diisi") + + @api.constrains('line_ids', 'state') + def _check_product_lines(self): + """Constraint: Product lines harus ada jika state bukan draft""" + for record in self: + if record.state in ('approval_purchase', 'approval_logistic', 'approval_finance', + 'done') and not record.line_ids: + raise ValidationError("Product lines harus diisi sebelum submit atau approve!") + + def _validate_product_lines(self): + """Helper method untuk validasi product lines""" + self.ensure_one() + + # Check ada product lines + if not self.line_ids: + raise UserError("Belum ada product lines yang ditambahkan!") + + # Check product sudah diisi + empty_lines = self.line_ids.filtered(lambda line: not line.product_id) + if empty_lines: + raise UserError("Ada product lines yang belum diisi productnya!") + + # Check quantity > 0 + zero_qty_lines = self.line_ids.filtered(lambda line: line.product_uom_qty <= 0) + if zero_qty_lines: + raise UserError("Quantity product tidak boleh kosong atau 0!") + + return True + + def _is_already_returned(self, picking): + return self.env['stock.picking'].search_count([ + ('origin', '=', 'Return of %s' % picking.name), + ('state', '!=', 'cancel') + ]) > 0 + + def copy(self, default=None): + if default is None: + default = {} + + # Generate new sequence untuk duplicate + sequence = self.env['ir.sequence'].search([('code', '=', 'tukar.guling.po')], limit=1) + if sequence: + default['name'] = sequence.next_by_id() + else: + default['name'] = self.env['ir.sequence'].next_by_code('tukar.guling.po') or 'copy' + + default.update({ + 'state': 'draft', + 'date': fields.Datetime.now(), + }) + + new_record = super(TukarGulingPO, self).copy(default) + + # Re-sequence lines + if new_record.line_ids: + for i, line in enumerate(new_record.line_ids): + line.sequence = (i + 1) * 10 + + return new_record + + def write(self, vals): + if self.operations.picking_type_id.id != 32: + if self._is_already_returned(self.operations): + raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") + if 'operations' in vals and not vals.get('origin'): + picking = self.env['stock.picking'].browse(vals['operations']) + if picking.origin: + vals['origin'] = picking.origin + + return super(TukarGulingPO, self).write(vals) + + def action_view_picking(self): + self.ensure_one() + action = self.env.ref('stock.action_picking_tree_all').read()[0] + pickings = self.po_picking_ids + if len(pickings) > 1: + action['domain'] = [('id', 'in', pickings.ids)] + elif pickings: + action['views'] = [(self.env.ref('stock.view_picking_form').id, 'form')] + action['res_id'] = pickings.id + return action + + 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_submit(self): + self.ensure_one() + picking = self.operations + if picking.picking_type_id.id == 75: + if picking.state != 'done': + raise UserError("BU/PUT belum Done!") + elif picking.picking_type_id.id == 32: + linked_bu_out = picking.linked_manual_bu_out + if linked_bu_out and linked_bu_out.state == 'done': + raise UserError("❌ Tidak bisa retur BU/INPUT karena BU/PUT suda Done!") + if picking.picking_type_id.id != 75 or picking.picking_type_id.id != 32: + if self._is_already_returned(self.operations): + raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") + self._validate_product_lines() + + + if self.state != 'draft': + raise UserError("Submit hanya bisa dilakukan dari Draft.") + self.state = 'approval_purchase' + + def action_approve(self): + self.ensure_one() + self._validate_product_lines() + + if not self.operations: + raise UserError("Operations harus diisi!") + + if not self.return_type: + raise UserError("Return Type harus diisi!") + + # Cek hak akses berdasarkan state + for rec in self: + if rec.state == 'approval_purchase': + if not rec.env.user.has_group('indoteknik_custom.group_role_sales'): + raise UserError("Hanya Sales Manager yang boleh approve tahap ini.") + rec.state = 'approval_logistic' + + elif rec.state == 'approval_logistic': + if not rec.env.user.has_group('indoteknik_custom.group_role_logistic'): + raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.") + rec.state = 'approval_finance' + + elif rec.state == 'approval_finance': + if not rec.env.user.has_group('indoteknik_custom.group_role_fat'): + raise UserError("Hanya Finance Manager yang boleh approve tahap ini.") + rec.state = 'done' + rec._create_pickings() + else: + raise UserError("Status ini tidak bisa di-approve.") + + def action_cancel(self): + self.ensure_one() + # if self.state == 'done': + # raise UserError("Tidak bisa cancel jika sudah done") + self.state = 'cancel' + + def _create_pickings(self): + for record in self: + if not record.operations: + raise UserError("BU/OUT dari field operations tidak ditemukan.") + + related_pickings = self.env['stock.picking'].search([ + ('origin', '=', record.origin), + ('state', '=', 'done'), + ('picking_type_id', 'in', [75, 32]) + ]) + if not related_pickings: + raise UserError( + "Tidak ditemukan BU/PICK atau BU/OUT dari SO: %s" % record.origin) + + # filter based on stock.picking picking type + bu_input_to_return = False + if record.operations.purchase_id: + bu_input_to_return = record.operations.purchase_id.picking_ids.filtered( + lambda p: p.picking_type_id.id == 75 and p.state == 'done' + ) + if bu_input_to_return: + bu_input_to_return = bu_input_to_return[0] + + # BU PUT = operations + bu_put_to_return = record.operations + + if not bu_input_to_return and not bu_put_to_return: + raise UserError("Tidak ada BU INPUT atau BU PUT yang siap diretur.") + + created_returns = [] + + # Lokasi default untuk retur + vrt_type = self.env['stock.picking.type'].browse(77) + prt_type = self.env['stock.picking.type'].browse(76) + bu_input_type = self.env['stock.picking.type'].browse(32) + bu_put_type = self.env['stock.picking.type'].browse(75) + + stock_location = self.env['stock.location'] + + # srt_src = stock_location.browse(5) + # srt_dest = stock_location.browse(60) + # + # ort_src = stock_location.browse(60) + # ort_dest = stock_location.browse(57) + # + # if not ort_src or not ort_dest or not srt_src or not srt_dest: + # raise UserError("salahwoi") + + # Fungsi membuat retur dari picking tertentu + def _create_return_from_picking(picking): + grup = self.operations.group_id + + PARTNER_LOCATION_ID = 5 + # BU_OUTPUT_LOCATION_ID = 60 + BU_INPUT_LOCATION_ID = 60 + BU_STOCK_LOCATION_ID = 57 + + # Determine locations based on picking type + if picking.picking_type_id.id == 77: + return_type = vrt_type + default_location_id = BU_STOCK_LOCATION_ID + default_location_dest_id = BU_INPUT_LOCATION_ID + elif picking.picking_type_id.id == 76: + return_type = prt_type + default_location_id = BU_INPUT_LOCATION_ID + default_location_dest_id = PARTNER_LOCATION_ID + elif picking.picking_type_id.id == 75: + return_type = bu_put_type + default_location_id = BU_INPUT_LOCATION_ID + default_location_dest_id = BU_STOCK_LOCATION_ID + elif picking.picking_type_id.id == 32: + return_type = bu_input_type + default_location_id = PARTNER_LOCATION_ID + default_location_dest_id = BU_INPUT_LOCATION_ID + else: + return None + return_context = dict(self.env.context) + return_context.update({ + 'active_id': picking.id, + 'default_location_id': default_location_id, + 'default_location_dest_id': default_location_dest_id, + 'from_ui': False, + }) + + return_wizard = self.env['stock.return.picking'].with_context(return_context).create({ + 'picking_id': picking.id, + 'location_id': default_location_dest_id, + 'original_location_id': default_location_id + }) + + # Create return lines + return_lines = [] + for line in record.line_ids: + move = picking.move_lines.filtered(lambda wkwk: wkwk.product_id == line.product_id) + if move: + return_lines.append((0, 0, { + 'product_id': line.product_id.id, + 'quantity': line.product_uom_qty, + 'move_id': move.id, + })) + if not move: + raise UserError("eror woi") + if not return_lines: + return None + + return_wizard.product_return_moves = return_lines + + _logger.info("Creating return for picking %s", picking.name) + _logger.info("Default location src: %s", default_location_id) + _logger.info("Default location dest: %s", default_location_dest_id) + return_vals = return_wizard.create_returns() + return_id = return_vals.get('res_id') + return_picking = self.env['stock.picking'].browse(return_id) + + if not return_picking: + raise UserError("Retur gagal dibuat. Hasil create_returns: %s" % str(return_vals)) + + # Force the destination location + return_picking.write({ + 'location_dest_id': default_location_dest_id, + 'location_id': default_location_id, + 'group_id': grup.id, + 'tukar_guling_po_id': record.id, + }) + + return return_picking + + if record.operations.picking_type_id.id == 76: + prt = _create_return_from_picking(record.operations) + if prt: + created_returns.append(prt) + else: + # CASE: Retur dari BU/OUT + vrt = _create_return_from_picking(bu_put_to_return) + if vrt: + created_returns.append(vrt) + + prt = None + if bu_input_to_return: + prt = _create_return_from_picking(bu_input_to_return) + if prt: + created_returns.append(prt) + + if record.return_type == 'tukar_guling': + if vrt: + bu_put = _create_return_from_picking(vrt) + if bu_put: + created_returns.append(bu_put) + + if prt: + bu_input = _create_return_from_picking(prt) + if bu_input: + created_returns.append(bu_input) + + if not created_returns: + raise UserError("wkwkwk") + + + +class TukarGulingLinePO(models.Model): + _name = 'tukar.guling.line.po' + _description = 'Tukar Guling PO Line' + + sequence = fields.Integer('Sequence', default=10, copy=False) + product_id = fields.Many2one('product.product', string='Product', required=True) + tukar_guling_po_id = fields.Many2one('tukar.guling.po', string='Tukar Guling PO', ondelete='cascade') + product_uom_qty = fields.Float('Quantity', digits='Product Unit of Measure', required=True, default=1.0) + product_uom = fields.Many2one('uom.uom', string='Unit of Measure') + name = fields.Text('Description') + + +class StockPicking(models.Model): + _inherit = 'stock.picking' + tukar_guling_po_id = fields.Many2one('tukar.guling.po', string='Tukar Guling PO Ref') \ No newline at end of file -- cgit v1.2.3 From 38ba3d7f5b59a4444d9eb953a6c83e4ab6015ba6 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Fri, 4 Jul 2025 09:18:50 +0700 Subject: approval payment term --- indoteknik_custom/models/__init__.py | 1 + indoteknik_custom/models/approval_payment_term.py | 82 +++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 indoteknik_custom/models/approval_payment_term.py (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index 605d1016..83392d42 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -151,3 +151,4 @@ from . import account_payment_register from . import stock_inventory from . import sale_order_delay from . import approval_invoice_date +from . import approval_payment_term diff --git a/indoteknik_custom/models/approval_payment_term.py b/indoteknik_custom/models/approval_payment_term.py new file mode 100644 index 00000000..81eb1908 --- /dev/null +++ b/indoteknik_custom/models/approval_payment_term.py @@ -0,0 +1,82 @@ +from odoo import models, api, fields +from odoo.exceptions import AccessError, UserError, ValidationError +from datetime import timedelta, date, datetime +import logging + +_logger = logging.getLogger(__name__) + +class ApprovalPaymentTerm(models.Model): + _name = "approval.payment.term" + _description = "Approval Payment Term" + _inherit = ['mail.thread'] + _rec_name = 'number' + + number = fields.Char(string='Document No', index=True, copy=False, readonly=True, tracking=True) + partner_id = fields.Many2one('res.partner', string='Partner', copy=False) + property_payment_term_id = fields.Many2one('account.payment.term', string='Payment Term', copy=False, tracking=True) + parent_id = fields.Many2one('res.partner', string='Related Company', copy=False) + blocking_stage = fields.Float(string='Blocking Amount', + help="Cannot make sales once the selected " + "customer is crossed blocking amount." + "Set its value to 0.00 to disable " + "this feature", tracking=True, copy=False) + warning_stage = fields.Float(string='Warning Amount', + help="A warning message will appear once the " + "selected customer is crossed warning " + "amount. Set its value to 0.00 to" + " disable this feature", tracking=True, copy=False) + active_limit = fields.Boolean('Active Credit Limit', copy=False, tracking=True) + approve_sales_manager = fields.Boolean('Approve Sales Manager', tracking=True, copy=False) + approve_finance = fields.Boolean('Approve Finance', tracking=True, copy=False) + approve_leader = fields.Boolean('Approve Pimpinan', tracking=True, copy=False) + reason = fields.Text('Reason', tracking=True) + approve_date = fields.Datetime('Approve Date') + + + @api.constrains('partner_id') + def constrains_partner_id(self): + if self.partner_id: + self.parent_id = self.partner_id.parent_id.id if self.partner_id.parent_id else None + self.blocking_stage = self.partner_id.blocking_stage + self.warning_stage = self.partner_id.warning_stage + self.active_limit = self.partner_id.active_limit + self.property_payment_term_id = self.partner_id.property_payment_term_id.id + + def button_approve(self): + user = self.env.user + is_it = user.has_group('indoteknik_custom.group_role_it') + + if is_it or user.id == 19: + self.approve_sales_manager = True + return + + if is_it or user.id == 688 and self.approve_sales_manager: + self.approve_finance = True + return + + if is_it or user.id == 7 and self.approve_sales_manager and self.approve_finance: + self.approve_leader = True + + if not is_it or not self.approve_finance: + raise UserError('Harus Approval Finance!!') + if not is_it or not self.approve_leader: + raise UserError('Harus Approval Pimpinan!!') + + if user.id == 7: + if not self.approve_finance: + raise UserError('Belum Di Approve Oleh Finance') + + if self.approve_leader == True: + self.partner_id.write({ + 'blocking_stage': self.blocking_stage, + 'warning_stage': self.warning_stage, + 'active_limit': self.active_limit, + 'property_payment_term_id': self.property_payment_term_id.id + }) + self.approve_date = datetime.utcnow() + + @api.model + def create(self, vals): + vals['number'] = self.env['ir.sequence'].next_by_code('approval.payment.term') or '0' + result = super(ApprovalPaymentTerm, self).create(vals) + return result -- cgit v1.2.3 From c1bde5a538b04186a5c83aea2817cd4f05f2acd7 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 4 Jul 2025 09:26:26 +0700 Subject: fix orig id and dest id --- indoteknik_custom/models/tukar_guling.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index eeec2d80..db82ce1b 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -371,21 +371,29 @@ class TukarGuling(models.Model): # Determine locations based on picking type if picking.picking_type_id.id == 30: + # BU/PICK → ORT return_type = ort_type default_location_id = BU_OUTPUT_LOCATION_ID default_location_dest_id = BU_STOCK_LOCATION_ID + + elif picking.picking_type_id.id == 74: + # ORT → BU/PICK + return_type = bu_pick_type + default_location_id = BU_STOCK_LOCATION_ID + default_location_dest_id = BU_OUTPUT_LOCATION_ID + elif picking.picking_type_id.id == 29: + # BU/OUT → SRT return_type = srt_type default_location_id = PARTNER_LOCATION_ID default_location_dest_id = BU_OUTPUT_LOCATION_ID - elif picking.picking_type_id.id == 74: + + elif picking.picking_type_id.id == 73: + # SRT → BU/OUT return_type = bu_out_type default_location_id = BU_OUTPUT_LOCATION_ID default_location_dest_id = PARTNER_LOCATION_ID - elif picking.picking_type_id.id == 73: - return_type = bu_pick_type - default_location_id = BU_STOCK_LOCATION_ID - default_location_dest_id = BU_OUTPUT_LOCATION_ID + else: return None return_context = dict(self.env.context) @@ -456,16 +464,16 @@ class TukarGuling(models.Model): created_returns.append(ort) if record.return_type == 'tukar_guling': - if srt: - bu_out = _create_return_from_picking(srt) - if bu_out: - created_returns.append(bu_out) - if ort: bu_pick = _create_return_from_picking(ort) if bu_pick: created_returns.append(bu_pick) + if srt: + bu_out = _create_return_from_picking(srt) + if bu_out: + created_returns.append(bu_out) + if not created_returns: raise UserError("wkwkwk") -- cgit v1.2.3 From e5b1c4117bd887b1e77c0aa8117b79646397855b Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Fri, 4 Jul 2025 10:02:31 +0700 Subject: (andri) fix open jurnal entries --- indoteknik_custom/models/account_move.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index af24f93e..b6627867 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -99,12 +99,27 @@ class AccountMove(models.Model): self.invoice_date = self.date + # def compute_length_of_payment(self): + # for rec in self: + # payment_term = rec.invoice_payment_term_id.line_ids[0].days + # terima_faktur = rec.date_terima_tukar_faktur + # payment = self.search([('ref', '=', rec.name), ('move_type', '=', 'entry')], limit=1) + + # if payment and terima_faktur: + # date_diff = terima_faktur - payment.date + # rec.length_of_payment = date_diff.days + payment_term + # else: + # rec.length_of_payment = 0 + def compute_length_of_payment(self): for rec in self: - payment_term = rec.invoice_payment_term_id.line_ids[0].days + payment_term = 0 + if rec.invoice_payment_term_id and rec.invoice_payment_term_id.line_ids: + payment_term = rec.invoice_payment_term_id.line_ids[0].days + terima_faktur = rec.date_terima_tukar_faktur payment = self.search([('ref', '=', rec.name), ('move_type', '=', 'entry')], limit=1) - + if payment and terima_faktur: date_diff = terima_faktur - payment.date rec.length_of_payment = date_diff.days + payment_term -- cgit v1.2.3 From 43b88a8d7814281e4e20c3a22c0c1780e4caf54e Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 4 Jul 2025 17:14:41 +0700 Subject: tukar guling po (-) sequence --- indoteknik_custom/models/tukar_guling_po.py | 173 +++++++++++++--------------- 1 file changed, 81 insertions(+), 92 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index 4ed363cf..92a58d21 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -25,7 +25,7 @@ class TukarGulingPO(models.Model): 'stock.picking', string='Operations', domain=[ - ('picking_type_id.id', 'in', [75, 32]), + ('picking_type_id.id', 'in', [75, 28]), ('state', '=', 'done') ],help='Nomor BU INPUT atau BU PUT' ) @@ -224,7 +224,7 @@ class TukarGulingPO(models.Model): return new_record def write(self, vals): - if self.operations.picking_type_id.id != 32: + if self.operations.picking_type_id.id != 28: if self._is_already_returned(self.operations): raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") if 'operations' in vals and not vals.get('origin'): @@ -259,11 +259,11 @@ class TukarGulingPO(models.Model): if picking.picking_type_id.id == 75: if picking.state != 'done': raise UserError("BU/PUT belum Done!") - elif picking.picking_type_id.id == 32: + elif picking.picking_type_id.id == 28: linked_bu_out = picking.linked_manual_bu_out if linked_bu_out and linked_bu_out.state == 'done': raise UserError("❌ Tidak bisa retur BU/INPUT karena BU/PUT suda Done!") - if picking.picking_type_id.id != 75 or picking.picking_type_id.id != 32: + if picking.picking_type_id.id != 75 or picking.picking_type_id.id != 28: if self._is_already_returned(self.operations): raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") self._validate_product_lines() @@ -312,79 +312,58 @@ class TukarGulingPO(models.Model): def _create_pickings(self): for record in self: if not record.operations: - raise UserError("BU/OUT dari field operations tidak ditemukan.") - - related_pickings = self.env['stock.picking'].search([ - ('origin', '=', record.origin), - ('state', '=', 'done'), - ('picking_type_id', 'in', [75, 32]) - ]) - if not related_pickings: - raise UserError( - "Tidak ditemukan BU/PICK atau BU/OUT dari SO: %s" % record.origin) - - # filter based on stock.picking picking type - bu_input_to_return = False - if record.operations.purchase_id: - bu_input_to_return = record.operations.purchase_id.picking_ids.filtered( - lambda p: p.picking_type_id.id == 75 and p.state == 'done' - ) - if bu_input_to_return: - bu_input_to_return = bu_input_to_return[0] - - # BU PUT = operations - bu_put_to_return = record.operations - - if not bu_input_to_return and not bu_put_to_return: - raise UserError("Tidak ada BU INPUT atau BU PUT yang siap diretur.") + raise UserError("BU Operations belum dipilih.") created_returns = [] - # Lokasi default untuk retur - vrt_type = self.env['stock.picking.type'].browse(77) - prt_type = self.env['stock.picking.type'].browse(76) - bu_input_type = self.env['stock.picking.type'].browse(32) - bu_put_type = self.env['stock.picking.type'].browse(75) - - stock_location = self.env['stock.location'] - - # srt_src = stock_location.browse(5) - # srt_dest = stock_location.browse(60) - # - # ort_src = stock_location.browse(60) - # ort_dest = stock_location.browse(57) - # - # if not ort_src or not ort_dest or not srt_src or not srt_dest: - # raise UserError("salahwoi") + # Ambil BU INPUT & BU PUT dari group yang sama + group = record.operations.group_id + bu_input_to_return = bu_put_to_return = False + + if group: + po_pickings = self.env['stock.picking'].search([ + ('group_id', '=', group.id), + ('state', '=', 'done') + ]) + bu_input_to_return = po_pickings.filtered(lambda p: p.picking_type_id.id == 28) + bu_put_to_return = po_pickings.filtered(lambda p: p.picking_type_id.id == 75) + bu_input_to_return = bu_input_to_return[0] if bu_input_to_return else False + bu_put_to_return = bu_put_to_return[0] if bu_put_to_return else False + else: + raise UserError("Group ID tidak ditemukan pada BU Operations.") - # Fungsi membuat retur dari picking tertentu + # Fungsi buat return picking def _create_return_from_picking(picking): - grup = self.operations.group_id + grup = record.operations.group_id - PARTNER_LOCATION_ID = 5 - # BU_OUTPUT_LOCATION_ID = 60 - BU_INPUT_LOCATION_ID = 60 + PARTNER_LOCATION_ID = 4 + BU_INPUT_LOCATION_ID = 58 BU_STOCK_LOCATION_ID = 57 - # Determine locations based on picking type - if picking.picking_type_id.id == 77: - return_type = vrt_type - default_location_id = BU_STOCK_LOCATION_ID - default_location_dest_id = BU_INPUT_LOCATION_ID - elif picking.picking_type_id.id == 76: - return_type = prt_type + # Lokasi sesuai type + if picking.picking_type_id.id == 28: + # Retur dari BU INPUT → hasilnya jadi PRT (BU Input → Partner) + # tapi wizard tetap diinput sebagai picking_id=28, dari input ke partner default_location_id = BU_INPUT_LOCATION_ID default_location_dest_id = PARTNER_LOCATION_ID + elif picking.picking_type_id.id == 75: - return_type = bu_put_type + # Retur dari BU PUT → hasilnya jadi VRT (BU Stock → BU Input) + default_location_id = BU_STOCK_LOCATION_ID + default_location_dest_id = BU_INPUT_LOCATION_ID + + elif picking.picking_type_id.id == 77: + # Retur dari VRT → hasilnya jadi PUT lagi (BU Input → BU Stock) default_location_id = BU_INPUT_LOCATION_ID default_location_dest_id = BU_STOCK_LOCATION_ID - elif picking.picking_type_id.id == 32: - return_type = bu_input_type + + elif picking.picking_type_id.id == 76: + # Retur dari PRT → hasilnya jadi INPUT lagi (Partner → BU Input) default_location_id = PARTNER_LOCATION_ID default_location_dest_id = BU_INPUT_LOCATION_ID else: return None + return_context = dict(self.env.context) return_context.update({ 'active_id': picking.id, @@ -399,72 +378,82 @@ class TukarGulingPO(models.Model): 'original_location_id': default_location_id }) - # Create return lines + # Buat lines return_lines = [] for line in record.line_ids: - move = picking.move_lines.filtered(lambda wkwk: wkwk.product_id == line.product_id) + move = picking.move_lines.filtered(lambda m: m.product_id == line.product_id) if move: return_lines.append((0, 0, { 'product_id': line.product_id.id, 'quantity': line.product_uom_qty, - 'move_id': move.id, + 'move_id': move[0].id, })) - if not move: - raise UserError("eror woi") + if not return_lines: return None return_wizard.product_return_moves = return_lines - - _logger.info("Creating return for picking %s", picking.name) - _logger.info("Default location src: %s", default_location_id) - _logger.info("Default location dest: %s", default_location_dest_id) return_vals = return_wizard.create_returns() - return_id = return_vals.get('res_id') - return_picking = self.env['stock.picking'].browse(return_id) + return_picking = self.env['stock.picking'].browse(return_vals.get('res_id')) - if not return_picking: - raise UserError("Retur gagal dibuat. Hasil create_returns: %s" % str(return_vals)) - - # Force the destination location + # Paksa locations di picking & moves return_picking.write({ - 'location_dest_id': default_location_dest_id, 'location_id': default_location_id, + 'location_dest_id': default_location_dest_id, 'group_id': grup.id, 'tukar_guling_po_id': record.id, }) + for move in return_picking.move_lines: + move.write({ + 'location_id': default_location_id, + 'location_dest_id': default_location_dest_id, + }) + + # Rename BU INPUT jadi BU/INPUT/ + if return_picking.picking_type_id.id == 28 and return_picking.name.startswith('BU/IN/'): + old_name = return_picking.name + return_picking.name = return_picking.name.replace('BU/IN/', 'BU/INPUT/', 1) + _logger.info("Rename %s -> %s", old_name, return_picking.name) + return return_picking + if record.operations.picking_typ_id.id == 28: + bu_prt = _create_return_from_picking(record.operations) + if bu_prt: + created_returns.append(bu_prt) + # Eksekusi sesuai kondisi operations if record.operations.picking_type_id.id == 76: - prt = _create_return_from_picking(record.operations) + # Kalau operations = PRT → return jadi BU INPUT + bu_input = _create_return_from_picking(record.operations) + if bu_input: + created_returns.append(bu_input) + elif record.operations.picking_type_id.id == 77: + # Kalau operations = VRT → return jadi BU PUT + bu_put = _create_return_from_picking(record.operations) + if bu_put: + created_returns.append(bu_put) + else: + # Standard: retur bu_input & bu_put + prt = _create_return_from_picking(bu_input_to_return) if prt: created_returns.append(prt) - else: - # CASE: Retur dari BU/OUT + vrt = _create_return_from_picking(bu_put_to_return) if vrt: created_returns.append(vrt) - prt = None - if bu_input_to_return: - prt = _create_return_from_picking(bu_input_to_return) - if prt: - created_returns.append(prt) - if record.return_type == 'tukar_guling': + vrt = _create_return_from_picking(bu_put_to_return) if vrt: - bu_put = _create_return_from_picking(vrt) - if bu_put: - created_returns.append(bu_put) + created_returns.append(vrt) + prt = _create_return_from_picking(bu_input_to_return) if prt: - bu_input = _create_return_from_picking(prt) - if bu_input: - created_returns.append(bu_input) + created_returns.append(prt) if not created_returns: - raise UserError("wkwkwk") + raise UserError("Tidak ada dokumen retur yang berhasil dibuat.") -- cgit v1.2.3 From f316cfaaf269cf57488bbfae027df95f83a6ec28 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sat, 5 Jul 2025 08:40:34 +0700 Subject: fix sequence and return --- indoteknik_custom/models/tukar_guling_po.py | 39 +++++++++++------------------ 1 file changed, 15 insertions(+), 24 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index 92a58d21..f885017c 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -418,40 +418,31 @@ class TukarGulingPO(models.Model): return return_picking - if record.operations.picking_typ_id.id == 28: - bu_prt = _create_return_from_picking(record.operations) - if bu_prt: - created_returns.append(bu_prt) - # Eksekusi sesuai kondisi operations - if record.operations.picking_type_id.id == 76: - # Kalau operations = PRT → return jadi BU INPUT - bu_input = _create_return_from_picking(record.operations) - if bu_input: - created_returns.append(bu_input) - elif record.operations.picking_type_id.id == 77: - # Kalau operations = VRT → return jadi BU PUT - bu_put = _create_return_from_picking(record.operations) - if bu_put: - created_returns.append(bu_put) - else: - # Standard: retur bu_input & bu_put - prt = _create_return_from_picking(bu_input_to_return) + if record.operations.picking_type_id.id == 28: + prt = _create_return_from_picking(record.operations) if prt: created_returns.append(prt) - + else: vrt = _create_return_from_picking(bu_put_to_return) if vrt: created_returns.append(vrt) - if record.return_type == 'tukar_guling': - vrt = _create_return_from_picking(bu_put_to_return) - if vrt: - created_returns.append(vrt) - + prt = None + if bu_input_to_return: prt = _create_return_from_picking(bu_input_to_return) if prt: created_returns.append(prt) + if record.return_type == 'tukar_guling': + if prt: + bu_input = _create_return_from_picking(prt) + if bu_input: + created_returns.append(bu_input) + if vrt: + bu_put = _create_return_from_picking(vrt) + if bu_put: + created_returns.append(bu_put) + if not created_returns: raise UserError("Tidak ada dokumen retur yang berhasil dibuat.") -- cgit v1.2.3 From d9ddc88ea00a5c86c7cf82552970ab0c917d8544 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 5 Jul 2025 10:36:28 +0700 Subject: (andri) add patch untuk webhook biteship --- indoteknik_custom/models/__init__.py | 1 + .../models/patch/__pycache__/__init__.py | 1 + .../models/patch/__pycache__/http_override.py | 46 ++++++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 indoteknik_custom/models/patch/__pycache__/__init__.py create mode 100644 indoteknik_custom/models/patch/__pycache__/http_override.py (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index 83392d42..cc406c13 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -152,3 +152,4 @@ from . import stock_inventory from . import sale_order_delay from . import approval_invoice_date from . import approval_payment_term +from . import patch diff --git a/indoteknik_custom/models/patch/__pycache__/__init__.py b/indoteknik_custom/models/patch/__pycache__/__init__.py new file mode 100644 index 00000000..051b6537 --- /dev/null +++ b/indoteknik_custom/models/patch/__pycache__/__init__.py @@ -0,0 +1 @@ +from . import http_override \ No newline at end of file diff --git a/indoteknik_custom/models/patch/__pycache__/http_override.py b/indoteknik_custom/models/patch/__pycache__/http_override.py new file mode 100644 index 00000000..e1978edb --- /dev/null +++ b/indoteknik_custom/models/patch/__pycache__/http_override.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +import odoo.http +import json +import logging +from werkzeug.exceptions import BadRequest +import functools + +_logger = logging.getLogger(__name__) + +class CustomJsonRequest(odoo.http.JsonRequest): + def __init__(self, httprequest): + super(odoo.http.JsonRequest, self).__init__(httprequest) + + self.params = {} + request_data_raw = self.httprequest.get_data().decode(self.httprequest.charset) + + self.jsonrequest = {} + if request_data_raw.strip(): + try: + self.jsonrequest = json.loads(request_data_raw) + except ValueError: + msg = 'Invalid JSON data: %r' % (request_data_raw,) + _logger.info('%s: %s (Handled by CustomJsonRequest)', self.httprequest.path, msg) + raise BadRequest(msg) + else: + _logger.info("CustomJsonRequest: Received empty or whitespace-only JSON body. Treating as empty JSON for webhook.") + + self.params = dict(self.jsonrequest.get("params", {})) + self.context = self.params.pop('context', dict(self.session.context)) + + +_original_get_request = odoo.http.Root.get_request + +@functools.wraps(_original_get_request) +def _get_request_override(self, httprequest): + _logger.info("--- DEBUG: !!! _get_request_override IS CALLED !!! ---") + _logger.info(f"--- DEBUG: Request Mimetype: {httprequest.mimetype}, Path: {httprequest.path} ---") + + if httprequest.mimetype in ("application/json", "application/json-rpc"): + _logger.debug("Odoo HTTP: Using CustomJsonRequest for mimetype: %s", httprequest.mimetype) + return CustomJsonRequest(httprequest) + else: + _logger.debug("Odoo HTTP: Using original get_request for mimetype: %s", httprequest.mimetype) + return _original_get_request(self, httprequest) + +odoo.http.Root.get_request = _get_request_override \ No newline at end of file -- cgit v1.2.3 From fc6d38599b405820b3c266a31ef21a3a0f3f0a73 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 5 Jul 2025 10:51:52 +0700 Subject: (andri) fix --- indoteknik_custom/models/patch/__init__.py | 1 + .../models/patch/__pycache__/__init__.py | 1 - .../models/patch/__pycache__/http_override.py | 46 ---------------------- indoteknik_custom/models/patch/http_override.py | 45 +++++++++++++++++++++ 4 files changed, 46 insertions(+), 47 deletions(-) create mode 100644 indoteknik_custom/models/patch/__init__.py delete mode 100644 indoteknik_custom/models/patch/__pycache__/__init__.py delete mode 100644 indoteknik_custom/models/patch/__pycache__/http_override.py create mode 100644 indoteknik_custom/models/patch/http_override.py (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/patch/__init__.py b/indoteknik_custom/models/patch/__init__.py new file mode 100644 index 00000000..051b6537 --- /dev/null +++ b/indoteknik_custom/models/patch/__init__.py @@ -0,0 +1 @@ +from . import http_override \ No newline at end of file diff --git a/indoteknik_custom/models/patch/__pycache__/__init__.py b/indoteknik_custom/models/patch/__pycache__/__init__.py deleted file mode 100644 index 051b6537..00000000 --- a/indoteknik_custom/models/patch/__pycache__/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import http_override \ No newline at end of file diff --git a/indoteknik_custom/models/patch/__pycache__/http_override.py b/indoteknik_custom/models/patch/__pycache__/http_override.py deleted file mode 100644 index e1978edb..00000000 --- a/indoteknik_custom/models/patch/__pycache__/http_override.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -import odoo.http -import json -import logging -from werkzeug.exceptions import BadRequest -import functools - -_logger = logging.getLogger(__name__) - -class CustomJsonRequest(odoo.http.JsonRequest): - def __init__(self, httprequest): - super(odoo.http.JsonRequest, self).__init__(httprequest) - - self.params = {} - request_data_raw = self.httprequest.get_data().decode(self.httprequest.charset) - - self.jsonrequest = {} - if request_data_raw.strip(): - try: - self.jsonrequest = json.loads(request_data_raw) - except ValueError: - msg = 'Invalid JSON data: %r' % (request_data_raw,) - _logger.info('%s: %s (Handled by CustomJsonRequest)', self.httprequest.path, msg) - raise BadRequest(msg) - else: - _logger.info("CustomJsonRequest: Received empty or whitespace-only JSON body. Treating as empty JSON for webhook.") - - self.params = dict(self.jsonrequest.get("params", {})) - self.context = self.params.pop('context', dict(self.session.context)) - - -_original_get_request = odoo.http.Root.get_request - -@functools.wraps(_original_get_request) -def _get_request_override(self, httprequest): - _logger.info("--- DEBUG: !!! _get_request_override IS CALLED !!! ---") - _logger.info(f"--- DEBUG: Request Mimetype: {httprequest.mimetype}, Path: {httprequest.path} ---") - - if httprequest.mimetype in ("application/json", "application/json-rpc"): - _logger.debug("Odoo HTTP: Using CustomJsonRequest for mimetype: %s", httprequest.mimetype) - return CustomJsonRequest(httprequest) - else: - _logger.debug("Odoo HTTP: Using original get_request for mimetype: %s", httprequest.mimetype) - return _original_get_request(self, httprequest) - -odoo.http.Root.get_request = _get_request_override \ No newline at end of file diff --git a/indoteknik_custom/models/patch/http_override.py b/indoteknik_custom/models/patch/http_override.py new file mode 100644 index 00000000..6bec1343 --- /dev/null +++ b/indoteknik_custom/models/patch/http_override.py @@ -0,0 +1,45 @@ +import odoo.http +import json +import logging +from werkzeug.exceptions import BadRequest +import functools + +_logger = logging.getLogger(__name__) + +class CustomJsonRequest(odoo.http.JsonRequest): + def __init__(self, httprequest): + super(odoo.http.JsonRequest, self).__init__(httprequest) + + self.params = {} + request_data_raw = self.httprequest.get_data().decode(self.httprequest.charset) + + self.jsonrequest = {} + if request_data_raw.strip(): + try: + self.jsonrequest = json.loads(request_data_raw) + except ValueError: + msg = 'Invalid JSON data: %r' % (request_data_raw,) + _logger.info('%s: %s (Handled by CustomJsonRequest)', self.httprequest.path, msg) + raise BadRequest(msg) + else: + _logger.info("CustomJsonRequest: Received empty or whitespace-only JSON body. Treating as empty JSON for webhook.") + + self.params = dict(self.jsonrequest.get("params", {})) + self.context = self.params.pop('context', dict(self.session.context)) + + +_original_get_request = odoo.http.Root.get_request + +@functools.wraps(_original_get_request) +def _get_request_override(self, httprequest): + _logger.info("--- DEBUG: !!! _get_request_override IS CALLED !!! ---") + _logger.info(f"--- DEBUG: Request Mimetype: {httprequest.mimetype}, Path: {httprequest.path} ---") + + if httprequest.mimetype in ("application/json", "application/json-rpc"): + _logger.debug("Odoo HTTP: Using CustomJsonRequest for mimetype: %s", httprequest.mimetype) + return CustomJsonRequest(httprequest) + else: + _logger.debug("Odoo HTTP: Using original get_request for mimetype: %s", httprequest.mimetype) + return _original_get_request(self, httprequest) + +odoo.http.Root.get_request = _get_request_override \ No newline at end of file -- cgit v1.2.3 From d51133889b2bfbc7fa2848382f233f5ce268d4d0 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sat, 5 Jul 2025 11:39:08 +0700 Subject: push --- indoteknik_custom/models/stock_picking_return.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking_return.py b/indoteknik_custom/models/stock_picking_return.py index a9781d3c..40cc30c9 100644 --- a/indoteknik_custom/models/stock_picking_return.py +++ b/indoteknik_custom/models/stock_picking_return.py @@ -5,7 +5,7 @@ import logging _logger = logging.getLogger(__name__) -class StockReturnPicking(models.TransientModel): +class ReturnPicking(models.TransientModel): _inherit = 'stock.return.picking' # return_type = fields.Selection([ -- cgit v1.2.3 From 5b1b45d46e34c6724572b9b3182813e0bfdea0a3 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 5 Jul 2025 13:41:38 +0700 Subject: (andri) off patch karena webhook berhasil didaftarkan --- indoteknik_custom/models/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index cc406c13..b815b472 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -152,4 +152,4 @@ from . import stock_inventory from . import sale_order_delay from . import approval_invoice_date from . import approval_payment_term -from . import patch +# from . import patch -- cgit v1.2.3 From 75fd0f87c6d1f8c3b92450f9826daa74550a5577 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 5 Jul 2025 15:03:29 +0700 Subject: (andri) info tambahan --- indoteknik_custom/models/stock_picking.py | 1 + 1 file changed, 1 insertion(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index d25208d1..e411aee6 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1773,6 +1773,7 @@ class StockPicking(models.Model): _logger.error(f"Error fetching Biteship order for picking {self.id}: {str(e)}") return { 'error': str(e) } + # ACTION GET TRACKING MANUAL BITESHIP # def action_sync_biteship_tracking(self): # for picking in self: # if not picking.biteship_id: -- cgit v1.2.3 From a78f8184c2e7d45a65315eff0ea354996adb9cce Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 5 Jul 2025 15:54:17 +0700 Subject: (andri) info tambahan webhook --- indoteknik_custom/models/stock_picking.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index e411aee6..6e8c1067 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1866,6 +1866,7 @@ class StockPicking(models.Model): _logger.warning(f"[Webhook] Gagal konversi waktu: {e}") dt = datetime.utcnow() + # Penanganan status pengiriman if status == "picked" and not self.driver_departure_date: updated_fields["driver_departure_date"] = fields.Datetime.to_string(dt) if status == "delivered" and not self.driver_arrival_date: @@ -1875,7 +1876,9 @@ class StockPicking(models.Model): if shipping_status and self.shipping_status != shipping_status: updated_fields["shipping_status"] = shipping_status + # Penanganan extra data dari webhook if extra_data: + # Informasi kurir if extra_data.get("courier_driver_name"): updated_fields["biteship_driver_name"] = extra_data["courier_driver_name"] if extra_data.get("courier_driver_phone"): @@ -1884,11 +1887,21 @@ class StockPicking(models.Model): updated_fields["biteship_driver_plate_number"] = extra_data["courier_driver_plate_number"] if extra_data.get("courier_link"): updated_fields["biteship_courier_link"] = extra_data["courier_link"] + # Informasi harga if extra_data.get("order_price"): updated_fields["biteship_shipping_price"] = extra_data["order_price"] + # Status mentah dari Biteship if extra_data.get("status"): updated_fields["biteship_shipping_status"] = extra_data["status"] + # Tambahan untuk handle order.waybill_id + if extra_data.get("tracking_id"): + updated_fields["biteship_tracking_id"] = extra_data["tracking_id"] + updated_fields["delivery_tracking_no"] = extra_data["tracking_id"] + if extra_data.get("waybill_id"): + updated_fields["biteship_waybill_id"] = extra_data["waybill_id"] + + # Konversi waktu lokal untuk log try: dt_parsed = parser.parse(timestamp) if dt_parsed.tzinfo is None: @@ -1897,13 +1910,16 @@ class StockPicking(models.Model): except Exception: dt_local = dt.astimezone(pytz.timezone("Asia/Jakarta")) + # Format pesan log desc_clean = ' '.join(description.strip().split()) log_line = f"[TRACKING] {status} - {dt_local.strftime('%d %b %Y %H:%M')}:
{desc_clean}" + # Hindari log duplikat if not self._has_existing_log(log_line): - self.with_user(15172).message_post(body=log_line) # user biteship test - # self.with_user(15710).message_post(body=log_line) # user biteship live + self.with_user(15172).message_post(body=log_line) # Biteship user + # self.with_user(15710).message_post(body=log_line) # Biteship user live + # Update field-field terkait if updated_fields: self.write(updated_fields) _logger.info(f"[Webhook] Updated fields on picking {self.name}: {updated_fields}") -- cgit v1.2.3 From 6ff554e2dd9efb329b4d828881967413f8c641fd Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 5 Jul 2025 15:56:14 +0700 Subject: (andri) user biteship --- indoteknik_custom/models/stock_picking.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 6e8c1067..33d21c8c 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1916,8 +1916,8 @@ class StockPicking(models.Model): # Hindari log duplikat if not self._has_existing_log(log_line): - self.with_user(15172).message_post(body=log_line) # Biteship user - # self.with_user(15710).message_post(body=log_line) # Biteship user live + # self.with_user(15172).message_post(body=log_line) # Biteship user + self.with_user(15710).message_post(body=log_line) # Biteship user live # Update field-field terkait if updated_fields: -- cgit v1.2.3 From 8837ae450af891898efd790e908d87664d2dd910 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 5 Jul 2025 16:47:26 +0700 Subject: (andri) fix user chatter biteship --- indoteknik_custom/models/stock_picking.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 33d21c8c..69718c7e 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1916,8 +1916,12 @@ class StockPicking(models.Model): # Hindari log duplikat if not self._has_existing_log(log_line): - # self.with_user(15172).message_post(body=log_line) # Biteship user - self.with_user(15710).message_post(body=log_line) # Biteship user live + biteship_user = self.env['res.users'].sudo().browse(15710) # ID live + # biteship_user = self.env['res.users'].sudo().browse(15710) # ID user (cek di db) + self.sudo().message_post( + body=log_line, + author_id=biteship_user.partner_id.id + ) # Update field-field terkait if updated_fields: -- cgit v1.2.3 From 54633b0db570e5811874f78a9515065b9cb41ad8 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 7 Jul 2025 08:26:38 +0700 Subject: (andri) payment diff res partner no compute --- indoteknik_custom/models/res_partner.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index f5347bea..33c01f37 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -168,12 +168,17 @@ class ResPartner(models.Model): payment_difficulty = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', compute="_compute_payment_difficulty", inverse = "_inverse_payment_difficulty", tracking=3) payment_history_url = fields.Text(string='Payment History URL') + # no compute + payment_diff = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', tracking=3) + + # tidak terpakai @api.depends('parent_id.payment_difficulty') def _compute_payment_difficulty(self): for partner in self: if partner.parent_id: partner.payment_difficulty = partner.parent_id.payment_difficulty + # tidak terpakai def _inverse_payment_difficulty(self): for partner in self: if not partner.parent_id: @@ -210,8 +215,8 @@ class ResPartner(models.Model): rec._update_address_from_coords() # Sinkronisasi payment_difficulty ke semua anak jika partner ini adalah parent - if not rec.parent_id and 'payment_difficulty' in vals: - rec.child_ids.write({'payment_difficulty': vals['payment_difficulty']}) + if not rec.parent_id and 'payment_diff' in vals: + rec.child_ids.write({'payment_diff': vals['payment_diff']}) # # # if 'property_payment_term_id' in vals: # # if not self.env.user.is_accounting and vals['property_payment_term_id'] != 26: @@ -230,8 +235,8 @@ class ResPartner(models.Model): for rec in records: if vals.get('latitude') and vals.get('longtitude'): rec._update_address_from_coords() - if rec.parent_id and not vals.get('payment_difficulty'): - rec.payment_difficulty = rec.parent_id.payment_difficulty + if rec.parent_id and not vals.get('payment_diff'): + rec.payment_diff = rec.parent_id.payment_diff return records @api.constrains('name') -- cgit v1.2.3 From 05879e52666f02c117aee569621556db97ef345c Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 8 Jul 2025 09:11:11 +0700 Subject: add statusbar and fix bug --- indoteknik_custom/models/approval_payment_term.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/approval_payment_term.py b/indoteknik_custom/models/approval_payment_term.py index 81eb1908..da71b7e4 100644 --- a/indoteknik_custom/models/approval_payment_term.py +++ b/indoteknik_custom/models/approval_payment_term.py @@ -31,6 +31,8 @@ class ApprovalPaymentTerm(models.Model): approve_leader = fields.Boolean('Approve Pimpinan', tracking=True, copy=False) reason = fields.Text('Reason', tracking=True) approve_date = fields.Datetime('Approve Date') + state = fields.Selection([('waiting_approval', 'Waiting Approval'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='waiting_approval', tracking=True) + reason_reject = fields.Selection([('reason1', 'Reason 1'), ('reason2', 'Reason 2'), ('reason3', 'Reason 3')], string='Reason Reject', tracking=True) @api.constrains('partner_id') @@ -46,20 +48,20 @@ class ApprovalPaymentTerm(models.Model): user = self.env.user is_it = user.has_group('indoteknik_custom.group_role_it') - if is_it or user.id == 19: + if user.id == 19 or is_it: self.approve_sales_manager = True return - if is_it or user.id == 688 and self.approve_sales_manager: + if user.id == 688 or is_it: self.approve_finance = True return - if is_it or user.id == 7 and self.approve_sales_manager and self.approve_finance: + if (user.id == 7 and self.approve_finance) or is_it: self.approve_leader = True - if not is_it or not self.approve_finance: + if not self.approve_finance or not is_it: raise UserError('Harus Approval Finance!!') - if not is_it or not self.approve_leader: + if not self.approve_leader or not is_it: raise UserError('Harus Approval Pimpinan!!') if user.id == 7: @@ -74,6 +76,12 @@ class ApprovalPaymentTerm(models.Model): 'property_payment_term_id': self.property_payment_term_id.id }) self.approve_date = datetime.utcnow() + self.state = 'approved' + + def button_reject(self): + if self.env.user.id not in [688, 7]: + raise UserError("Hanya Finance atau Pimpinan Yang Bisa Reject") + self.state = 'rejected' @api.model def create(self, vals): -- cgit v1.2.3 From 347e5e070592e7517b90160d666ce41ddff10347 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 8 Jul 2025 09:14:45 +0700 Subject: (andri) fix --- indoteknik_custom/models/res_partner.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index 33c01f37..78fd98ae 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -215,8 +215,8 @@ class ResPartner(models.Model): rec._update_address_from_coords() # Sinkronisasi payment_difficulty ke semua anak jika partner ini adalah parent - if not rec.parent_id and 'payment_diff' in vals: - rec.child_ids.write({'payment_diff': vals['payment_diff']}) + if not rec.parent_id and 'payment_difficulty' in vals: + rec.child_ids.write({'payment_difficulty': vals['payment_difficulty']}) # # # if 'property_payment_term_id' in vals: # # if not self.env.user.is_accounting and vals['property_payment_term_id'] != 26: @@ -235,8 +235,8 @@ class ResPartner(models.Model): for rec in records: if vals.get('latitude') and vals.get('longtitude'): rec._update_address_from_coords() - if rec.parent_id and not vals.get('payment_diff'): - rec.payment_diff = rec.parent_id.payment_diff + if rec.parent_id and not vals.get('payment_difficulty'): + rec.payment_difficulty = rec.parent_id.payment_difficulty return records @api.constrains('name') -- cgit v1.2.3 From c667a8699762057c9e6191466a182ebb69cb66c7 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 8 Jul 2025 09:16:50 +0700 Subject: (andri) fix --- indoteknik_custom/models/res_partner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index 78fd98ae..52947128 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -169,7 +169,7 @@ class ResPartner(models.Model): payment_history_url = fields.Text(string='Payment History URL') # no compute - payment_diff = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', tracking=3) + # payment_diff = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', tracking=3) # tidak terpakai @api.depends('parent_id.payment_difficulty') -- cgit v1.2.3 From 1ed9ce9ab59c12fa378cfab02f8919e08f424853 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Tue, 8 Jul 2025 09:31:01 +0700 Subject: rev 77 --- indoteknik_custom/models/stock_picking_return.py | 2 +- indoteknik_custom/models/tukar_guling.py | 34 +++++++++++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking_return.py b/indoteknik_custom/models/stock_picking_return.py index 40cc30c9..a9781d3c 100644 --- a/indoteknik_custom/models/stock_picking_return.py +++ b/indoteknik_custom/models/stock_picking_return.py @@ -5,7 +5,7 @@ import logging _logger = logging.getLogger(__name__) -class ReturnPicking(models.TransientModel): +class StockReturnPicking(models.TransientModel): _inherit = 'stock.return.picking' # return_type = fields.Selection([ diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index db82ce1b..339a1ff1 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -59,6 +59,10 @@ class TukarGuling(models.Model): @api.onchange('operations') def _onchange_operations(self): """Auto-populate lines ketika operations dipilih""" + for rec in self: + if rec.operations and rec.operations.picking_type_id.id == 30: + rec.return_type = 'revisi_so' + if self.operations: from_return_picking = self.env.context.get('from_return_picking', False) or \ self.env.context.get('default_line_ids', False) @@ -234,9 +238,11 @@ class TukarGuling(models.Model): return new_record def write(self, vals): - if self.operations.picking_type_id.id != 30: - if self._is_already_returned(self.operations): - raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") + if self.operations.picking_type_id.id == 30 and self.return_type == 'tukar_guling': + raise UserError ("BU/PICK tidak boleh retur tukar guling") + # if self.operations.picking_type_id.id != 30: + # if self._is_already_returned(self.operations): + # raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") if 'operations' in vals and not vals.get('origin'): picking = self.env['stock.picking'].browse(vals['operations']) if picking.origin: @@ -244,6 +250,12 @@ class TukarGuling(models.Model): return super(TukarGuling, self).write(vals) + def unlink(self): + for record in self: + if record.state == 'done': + raise UserError("Tidak bisa hapus pengajuan jika sudah done, set ke draft terlebih dahulu") + return super(TukarGuling, self).unlink() + def action_view_picking(self): self.ensure_one() action = self.env.ref('stock.action_picking_tree_all').read()[0] @@ -266,6 +278,8 @@ class TukarGuling(models.Model): def action_submit(self): self.ensure_one() picking = self.operations + if picking.picking_type_id.id == 30 and self.return_type == 'tukar_guling': + raise UserError("❌ BU/PICK tidak boleh di retur tukar guling") if picking.picking_type_id.id == 29: if picking.state != 'done': raise UserError("BU/OUT belum Done!") @@ -287,6 +301,9 @@ class TukarGuling(models.Model): self.ensure_one() self._validate_product_lines() + if self.operations.picking_type_id.id == 30 and self.return_type == 'tukar_guling': + raise UserError ("BU/PICK tidak boleh retur tukar guling") + if not self.operations: raise UserError("Operations harus diisi!") @@ -315,6 +332,14 @@ class TukarGuling(models.Model): def action_cancel(self): self.ensure_one() + # picking = self.env['stock.picking'] + bu_done = self.picking_ids.filtered(lambda p: p.state == 'done') + if bu_done: + raise UserError("Dokuemn BU sudah Done, tidak bisa di cancel") + ongoing_bu = self.picking_ids.filtered(lambda p: p.state != 'done') + for picking in ongoing_bu: + picking.action_cancel() + # if self.state == 'done': # raise UserError("Tidak bisa cancel jika sudah done") self.state = 'cancel' @@ -339,6 +364,9 @@ class TukarGuling(models.Model): bu_pick_to_return = record.operations.konfirm_koli_lines.pick_id bu_out_to_return = record.operations + # if bu_pick_to_return and self.return_type == 'tukar_guling': + # raise UserError("BU/PICK tidak boleh di retur tukar guling") + if not bu_pick_to_return and not bu_out_to_return: raise UserError("Tidak ada BU/PICK atau BU/OUT yang selesai untuk diretur.") -- cgit v1.2.3 From 52ed14a408f63bc7d93b7bc393a8a6eb4cd1ac08 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Tue, 8 Jul 2025 09:39:48 +0700 Subject: rev 77 check invoice --- indoteknik_custom/models/tukar_guling.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 339a1ff1..1aa5af5c 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -193,6 +193,20 @@ class TukarGuling(models.Model): ('state', '!=', 'cancel') ]) > 0 + @api.onchange('return_type', 'operations') + def _onchange_check_invoice(self): + for record in self: + if record.return_type == 'revisi_so' and record.origin: + # Cek invoice yang berhubungan dengan origin SO + invoices = self.env['account.move'].search([ + ('invoice_origin', 'ilike', record.origin), + ('state', 'not in', ['draft', 'cancel']) + ]) + if invoices: + raise UserError( + _("Tidak bisa memilih Return Type 'Revisi SO' karena dokumen SO %s sudah dibuat invoice.") % record.origin + ) + @api.model def create(self, vals): # Generate sequence number @@ -290,6 +304,7 @@ class TukarGuling(models.Model): if self.operations.picking_type_id.id != 30: if self._is_already_returned(self.operations): raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") + self._onchange_check_invoice() self._validate_product_lines() @@ -300,6 +315,7 @@ class TukarGuling(models.Model): def action_approve(self): self.ensure_one() self._validate_product_lines() + self._onchange_check_invoice() if self.operations.picking_type_id.id == 30 and self.return_type == 'tukar_guling': raise UserError ("BU/PICK tidak boleh retur tukar guling") -- cgit v1.2.3 From a8886eb056d62ace630f15fb3eaa5d55a360b277 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Tue, 8 Jul 2025 10:16:46 +0700 Subject: rev 77 check invoice --- indoteknik_custom/models/tukar_guling.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 1aa5af5c..9fe7527c 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -193,18 +193,17 @@ class TukarGuling(models.Model): ('state', '!=', 'cancel') ]) > 0 - @api.onchange('return_type', 'operations') - def _onchange_check_invoice(self): + @api.constrains('return_type', 'operations') + def _check_invoice_on_revisi_so(self): for record in self: if record.return_type == 'revisi_so' and record.origin: - # Cek invoice yang berhubungan dengan origin SO invoices = self.env['account.move'].search([ ('invoice_origin', 'ilike', record.origin), ('state', 'not in', ['draft', 'cancel']) ]) if invoices: - raise UserError( - _("Tidak bisa memilih Return Type 'Revisi SO' karena dokumen SO %s sudah dibuat invoice.") % record.origin + raise ValidationError( + _("Tidak bisa memilih Return Type 'Revisi SO' karena dokumen %s sudah dibuat invoice.") % record.origin ) @api.model @@ -252,6 +251,7 @@ class TukarGuling(models.Model): return new_record def write(self, vals): + self._check_invoice_on_revisi_so() if self.operations.picking_type_id.id == 30 and self.return_type == 'tukar_guling': raise UserError ("BU/PICK tidak boleh retur tukar guling") # if self.operations.picking_type_id.id != 30: @@ -304,7 +304,7 @@ class TukarGuling(models.Model): if self.operations.picking_type_id.id != 30: if self._is_already_returned(self.operations): raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") - self._onchange_check_invoice() + self._check_invoice_on_revisi_so() self._validate_product_lines() @@ -315,7 +315,7 @@ class TukarGuling(models.Model): def action_approve(self): self.ensure_one() self._validate_product_lines() - self._onchange_check_invoice() + self._check_invoice_on_revisi_so() if self.operations.picking_type_id.id == 30 and self.return_type == 'tukar_guling': raise UserError ("BU/PICK tidak boleh retur tukar guling") -- cgit v1.2.3 From aa217ff1809015908d7aa16683de9b9ca34e1910 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Tue, 8 Jul 2025 10:54:37 +0700 Subject: rev 77 po fix sequence --- indoteknik_custom/models/tukar_guling_po.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index f885017c..2e0ab604 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -55,7 +55,7 @@ class TukarGulingPO(models.Model): vals['name'] = sequence.next_by_id() else: # Fallback jika sequence belum dibuat - vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling.po') or 'embo==' + vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling.po') or 'new' # Auto-fill origin from operations if not vals.get('origin') and vals.get('operations'): -- cgit v1.2.3 From fbc29fcf20ef2571be30a7c06ad60f193282fa4b Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 8 Jul 2025 12:58:12 +0700 Subject: fix bug --- indoteknik_custom/models/approval_payment_term.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/approval_payment_term.py b/indoteknik_custom/models/approval_payment_term.py index da71b7e4..4cd9ea36 100644 --- a/indoteknik_custom/models/approval_payment_term.py +++ b/indoteknik_custom/models/approval_payment_term.py @@ -48,20 +48,20 @@ class ApprovalPaymentTerm(models.Model): user = self.env.user is_it = user.has_group('indoteknik_custom.group_role_it') - if user.id == 19 or is_it: + if (not user.id ==7 and user.id == 19) or is_it: self.approve_sales_manager = True return - if user.id == 688 or is_it: + if (not user.id ==7 and user.id == 688) or is_it: self.approve_finance = True return if (user.id == 7 and self.approve_finance) or is_it: self.approve_leader = True - if not self.approve_finance or not is_it: + if not self.approve_finance and not is_it: raise UserError('Harus Approval Finance!!') - if not self.approve_leader or not is_it: + if not self.approve_leader and not is_it: raise UserError('Harus Approval Pimpinan!!') if user.id == 7: -- cgit v1.2.3 From 3df2346732b38eb47accfb07d3dfb0feaab65854 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Wed, 9 Jul 2025 08:58:47 +0700 Subject: cr approval payment terms --- indoteknik_custom/models/approval_payment_term.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/approval_payment_term.py b/indoteknik_custom/models/approval_payment_term.py index 4cd9ea36..291e8e37 100644 --- a/indoteknik_custom/models/approval_payment_term.py +++ b/indoteknik_custom/models/approval_payment_term.py @@ -33,6 +33,13 @@ class ApprovalPaymentTerm(models.Model): approve_date = fields.Datetime('Approve Date') state = fields.Selection([('waiting_approval', 'Waiting Approval'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='waiting_approval', tracking=True) reason_reject = fields.Selection([('reason1', 'Reason 1'), ('reason2', 'Reason 2'), ('reason3', 'Reason 3')], string='Reason Reject', tracking=True) + sale_order_id = fields.Many2one('sale.order', string='Sale Order', copy=False, tracking=True) + total = fields.Float(string='Total', compute='_compute_total') + + @api.depends('sale_order_id') + def _compute_total(self): + for rec in self: + rec.total = rec.sale_order_id.amount_total @api.constrains('partner_id') @@ -48,11 +55,11 @@ class ApprovalPaymentTerm(models.Model): user = self.env.user is_it = user.has_group('indoteknik_custom.group_role_it') - if (not user.id ==7 and user.id == 19) or is_it: + if (not user.id ==7 and user.id == 19 and not self.approve_sales_manager) or is_it: self.approve_sales_manager = True return - if (not user.id ==7 and user.id == 688) or is_it: + if (not user.id ==7 and user.id == 688 and not self.approve_finance) or is_it: self.approve_finance = True return -- cgit v1.2.3 From 21128e0f165045558c2c8ef6faf199d4379614b1 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 9 Jul 2025 09:54:13 +0700 Subject: rev 77 vals --- indoteknik_custom/models/tukar_guling.py | 50 +++++++++++++++++++++++++---- indoteknik_custom/models/tukar_guling_po.py | 32 ++++++++++++++++++ 2 files changed, 75 insertions(+), 7 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 9fe7527c..e2f68e6c 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -251,9 +251,25 @@ class TukarGuling(models.Model): return new_record def write(self, vals): + self.ensure_one() self._check_invoice_on_revisi_so() - if self.operations.picking_type_id.id == 30 and self.return_type == 'tukar_guling': - raise UserError ("BU/PICK tidak boleh retur tukar guling") + operasi = self.operations.picking_type_id.id + tipe = self.return_type + pp = vals.get('return_type', tipe) + + if not self.operations: + raise UserError("Operations harus diisi!") + + if not self.return_type: + raise UserError("Return Type harus diisi!") + + if operasi == 30 and self.operations.linked_manual_bu_out.state == 'done': + raise UserError("❌ Tidak bisa retur BU/PICK karena BU/OUT sudah done") + if operasi == 30 and pp == 'tukar_guling': + raise UserError("❌ BU/PICK tidak boleh di retur tukar guling") + else: + _logger.info("hehhe") + # if self.operations.picking_type_id.id != 30: # if self._is_already_returned(self.operations): # raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") @@ -267,7 +283,11 @@ class TukarGuling(models.Model): def unlink(self): for record in self: if record.state == 'done': - raise UserError("Tidak bisa hapus pengajuan jika sudah done, set ke draft terlebih dahulu") + raise UserError( + "Tidak bisa hapus pengajuan jika sudah done, set ke draft terlebih dahulu jika ingin menghapus") + ongoing_bu = self.picking_ids.filtered(lambda p: p.state != 'done') + for picking in ongoing_bu: + picking.action_cancel() return super(TukarGuling, self).unlink() def action_view_picking(self): @@ -291,6 +311,15 @@ class TukarGuling(models.Model): def action_submit(self): self.ensure_one() + + existing_tukar_guling = self.env['tukar.guling'].search([ + ('operations', '=', self.operations.id), + ('id', '!=', self.id), + ('state', '!=', 'cancel'), + ], limit=1) + + if existing_tukar_guling: + raise UserError("BU ini sudah pernah diretur oleh dokumen %s." % existing_tukar_guling.name) picking = self.operations if picking.picking_type_id.id == 30 and self.return_type == 'tukar_guling': raise UserError("❌ BU/PICK tidak boleh di retur tukar guling") @@ -307,7 +336,6 @@ class TukarGuling(models.Model): self._check_invoice_on_revisi_so() self._validate_product_lines() - if self.state != 'draft': raise UserError("Submit hanya bisa dilakukan dari Draft.") self.state = 'approval_sales' @@ -317,8 +345,16 @@ class TukarGuling(models.Model): self._validate_product_lines() self._check_invoice_on_revisi_so() - if self.operations.picking_type_id.id == 30 and self.return_type == 'tukar_guling': - raise UserError ("BU/PICK tidak boleh retur tukar guling") + operasi = self.operations.picking_type_id.id + tipe = self.return_type + pp = vals.get('return_type', tipe) + + if operasi == 30 and self.operations.linked_manual_bu_out.state == 'done': + raise UserError("❌ Tidak bisa retur BU/PICK karena BU/OUT sudah done") + if operasi == 30 and pp == 'tukar_guling': + raise UserError("❌ BU/PICK tidak boleh di retur tukar guling") + else: + _logger.info("hehhe") if not self.operations: raise UserError("Operations harus diisi!") @@ -351,7 +387,7 @@ class TukarGuling(models.Model): # picking = self.env['stock.picking'] bu_done = self.picking_ids.filtered(lambda p: p.state == 'done') if bu_done: - raise UserError("Dokuemn BU sudah Done, tidak bisa di cancel") + raise UserError("Dokuemen BU sudah Done, tidak bisa di cancel") ongoing_bu = self.picking_ids.filtered(lambda p: p.state != 'done') for picking in ongoing_bu: picking.action_cancel() diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index 2e0ab604..e9dfda33 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -65,6 +65,20 @@ class TukarGulingPO(models.Model): return super(TukarGulingPO, self).create(vals) + @api.constrains('return_type', 'operations') + def _check_bill_on_revisi_po(self): + for record in self: + if record.return_type == 'revisi_po' and record.origin: + bills = self.env['account.move'].search([ + ('invoice_origin', 'ilike', record.origin), + ('move_type', '=', 'in_invoice'), # hanya vendor bill + ('state', 'not in', ['draft', 'cancel']) + ]) + if bills: + raise ValidationError( + _("Tidak bisa memilih Return Type 'Revisi PO' karena PO %s sudah dibuat vendor bill.") % record.origin + ) + @api.onchange('operations') def _onchange_operations(self): """Auto-populate lines ketika operations dipilih""" @@ -224,6 +238,7 @@ class TukarGulingPO(models.Model): return new_record def write(self, vals): + self._check_bill_on_revisi_po() if self.operations.picking_type_id.id != 28: if self._is_already_returned(self.operations): raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") @@ -234,6 +249,15 @@ class TukarGulingPO(models.Model): return super(TukarGulingPO, self).write(vals) + def unlink(self): + for record in self: + if record.state == 'done': + raise UserError ("Tidak bisa hapus pengajuan jika sudah done, set ke draft terlebih dahulu") + ongoing_bu = self.po_picking_ids.filtered(lambda p: p.state != 'done') + for picking in ongoing_bu: + picking.action_cancel() + return super(TukarGulingPO, self).unlink() + def action_view_picking(self): self.ensure_one() action = self.env.ref('stock.action_picking_tree_all').read()[0] @@ -255,6 +279,7 @@ class TukarGulingPO(models.Model): def action_submit(self): self.ensure_one() + self._check_bill_on_revisi_po() picking = self.operations if picking.picking_type_id.id == 75: if picking.state != 'done': @@ -276,6 +301,7 @@ class TukarGulingPO(models.Model): def action_approve(self): self.ensure_one() self._validate_product_lines() + self._check_bill_on_revisi_po() if not self.operations: raise UserError("Operations harus diisi!") @@ -307,6 +333,12 @@ class TukarGulingPO(models.Model): self.ensure_one() # if self.state == 'done': # raise UserError("Tidak bisa cancel jika sudah done") + bu_done = self.po_picking_ids.filtered(lambda p: p.state == 'done') + if bu_done: + raise UserError("Dokuemn BU sudah Done, tidak bisa di cancel") + ongoing_bu = self.po_picking_ids.filtered(lambda p: p.state != 'done') + for picking in ongoing_bu: + picking.action_cancel() self.state = 'cancel' def _create_pickings(self): -- cgit v1.2.3 From aedf25e194d4ddddab9b158b473164151109edaf Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 9 Jul 2025 10:16:19 +0700 Subject: rev 77 vals bu input --- indoteknik_custom/models/tukar_guling_po.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index e9dfda33..aa8fd1f3 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -239,6 +239,13 @@ class TukarGulingPO(models.Model): def write(self, vals): self._check_bill_on_revisi_po() + tipe = vals.get('return_type', self.return_type) + + + if self.operations.picking_type_id.id == 28 and tipe == 'tukar_guling': + raise UserError("❌ BU/INPUT tidak boleh di retur tukar guling") + + if self.operations.picking_type_id.id != 28: if self._is_already_returned(self.operations): raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") -- cgit v1.2.3 From 7cf4a10aa25c1f358b57d01ebf4efdbbcdd7b6a9 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Wed, 9 Jul 2025 10:57:41 +0700 Subject: change request approval payment terms --- indoteknik_custom/models/approval_payment_term.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/approval_payment_term.py b/indoteknik_custom/models/approval_payment_term.py index 291e8e37..00d990de 100644 --- a/indoteknik_custom/models/approval_payment_term.py +++ b/indoteknik_custom/models/approval_payment_term.py @@ -33,13 +33,26 @@ class ApprovalPaymentTerm(models.Model): approve_date = fields.Datetime('Approve Date') state = fields.Selection([('waiting_approval', 'Waiting Approval'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='waiting_approval', tracking=True) reason_reject = fields.Selection([('reason1', 'Reason 1'), ('reason2', 'Reason 2'), ('reason3', 'Reason 3')], string='Reason Reject', tracking=True) - sale_order_id = fields.Many2one('sale.order', string='Sale Order', copy=False, tracking=True) - total = fields.Float(string='Total', compute='_compute_total') + sale_order_ids = fields.Many2many( + 'sale.order', + string='Sale Orders', + copy=False, + tracking=True + ) + + total = fields.Char( + string='Sale Order Totals', + compute='_compute_total' + ) - @api.depends('sale_order_id') def _compute_total(self): for rec in self: - rec.total = rec.sale_order_id.amount_total + totals_list = [] + for order in rec.sale_order_ids: + formatted_total = "{:,.2f}".format(order.amount_total) + totals_list.append(f"{order.name}: {formatted_total}") + + rec.total = "\n".join(totals_list) if totals_list else "No Sale Orders" @api.constrains('partner_id') -- cgit v1.2.3 From 0604dbc3a2789c139ea66dd561726f796ad92cd6 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Wed, 9 Jul 2025 11:15:10 +0700 Subject: add grand total on approval payment term --- indoteknik_custom/models/approval_payment_term.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/approval_payment_term.py b/indoteknik_custom/models/approval_payment_term.py index 00d990de..6e1c8103 100644 --- a/indoteknik_custom/models/approval_payment_term.py +++ b/indoteknik_custom/models/approval_payment_term.py @@ -44,6 +44,13 @@ class ApprovalPaymentTerm(models.Model): string='Sale Order Totals', compute='_compute_total' ) + + grand_total = fields.Float(string='Grand Total', compute="_compute_grand_total") + + def _compute_grand_total(self): + for rec in self: + grand_total = sum(order.amount_total for order in rec.sale_order_ids) + rec.grand_total = grand_total def _compute_total(self): for rec in self: -- cgit v1.2.3 From db98db3e34ac47eeea0fc53f215cb483d6c5d5f9 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 9 Jul 2025 11:32:19 +0700 Subject: (andri) scheduler reminder due inv --- indoteknik_custom/models/account_move.py | 54 ++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index b6627867..df79b9f6 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -8,12 +8,14 @@ import PyPDF2 import os import re from terbilang import Terbilang +from collections import defaultdict _logger = logging.getLogger(__name__) class AccountMove(models.Model): _inherit = 'account.move' + _description = 'Account Move' invoice_day_to_due = fields.Integer(string="Day to Due", compute="_compute_invoice_day_to_due") bill_day_to_due = fields.Date(string="Day to Due", compute="_compute_bill_day_to_due") date_send_fp = fields.Datetime(string="Tanggal Kirim Faktur Pajak") @@ -72,6 +74,58 @@ class AccountMove(models.Model): bill_id = fields.Many2one('account.move', string='Vendor Bill', domain=[('move_type', '=', 'in_invoice')], help='Bill asal dari proses reklas ini') down_payment = fields.Boolean('Down Payments?') + def send_due_invoice_reminder(self): + today = fields.Date.today() + reminder_days = [-7, -3, 0, 3, 7] + target_dates = [today + timedelta(days=delta) for delta in reminder_days] + target_dates_str = [d.isoformat() for d in target_dates] + + # Ganti nama partner untuk test jika perlu + partner = self.env['res.partner'].search([('name', 'ilike', 'PRIMA SEJAHTERA MARITIM')], limit=1) + if not partner: + _logger.info("Partner tidak ditemukan.") + return + + invoices = self.env['account.move'].search([ + ('move_type', '=', 'out_invoice'), + ('state', '=', 'posted'), + ('payment_state', 'not in', ['paid','in_payment', 'reversed']), + ('invoice_date_due', '>=', today - timedelta(days=7)), + ('invoice_date_due', '>=', today - timedelta(days=3)), + ('invoice_date_due', '<=', today + timedelta(days=3)), + ('invoice_date_due', '<=', today + timedelta(days=7)), + ('partner_id', '=', partner.id), + ]) + + _logger.info(f"Invoices tahap 1: {invoices}") + + # Filter berdasarkan term mengandung "tempo" + invoices = invoices.filtered( + lambda inv: inv.invoice_payment_term_id and 'tempo' in (inv.invoice_payment_term_id.name or '').lower() + ) + _logger.info(f"Invoices tahap 2: {invoices}") + + if not invoices: + _logger.info(f"Tidak ada invoice yang due untuk partner: {partner.name}") + return + + # Pastikan field compute jalan + invoices._compute_invoice_day_to_due() + + # Ambil template + template = self.env.ref('indoteknik_custom.mail_template_invoice_due_reminder') + + for inv in invoices: + try: + # Untuk test: override ke email pribadi Anda + email_values = { + 'email_to': 'andrifebriyadiputra@gmail.com', + 'email_from': 'finance@indoteknik.co.id', + } + template.send_mail(inv.id, force_send=True, email_values=email_values) + _logger.info(f"Reminder terkirim: {inv.name} → {email_values['email_to']}") + except Exception as e: + _logger.error(f"Gagal kirim email untuk {inv.name}: {str(e)}") # def name_get(self): # result = [] -- cgit v1.2.3 From 0d43c8987d05543c20b1ea26e6645afcf153691b Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 9 Jul 2025 13:04:08 +0700 Subject: (andri) test --- indoteknik_custom/models/account_move.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index df79b9f6..059e8330 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -81,7 +81,7 @@ class AccountMove(models.Model): target_dates_str = [d.isoformat() for d in target_dates] # Ganti nama partner untuk test jika perlu - partner = self.env['res.partner'].search([('name', 'ilike', 'PRIMA SEJAHTERA MARITIM')], limit=1) + partner = self.env['res.partner'].search([('name', 'ilike', 'ROYALTAMA MULIA KONTRAKTORINDO')], limit=1) if not partner: _logger.info("Partner tidak ditemukan.") return -- cgit v1.2.3 From 5e905a9af7f6bb928c44cad2d47f8c6e69662bd2 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 9 Jul 2025 13:08:41 +0700 Subject: (andri) test --- indoteknik_custom/models/account_move.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 059e8330..8ef3d273 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -81,7 +81,7 @@ class AccountMove(models.Model): target_dates_str = [d.isoformat() for d in target_dates] # Ganti nama partner untuk test jika perlu - partner = self.env['res.partner'].search([('name', 'ilike', 'ROYALTAMA MULIA KONTRAKTORINDO')], limit=1) + partner = self.env['res.partner'].search([('name', 'ilike', 'DAYA ANUGRAH MULYA')], limit=1) if not partner: _logger.info("Partner tidak ditemukan.") return -- cgit v1.2.3 From 717dbfa7070e94c0af2bb39e2cebb4dc71d123b9 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 9 Jul 2025 13:14:59 +0700 Subject: rev 77 vals vals --- indoteknik_custom/models/tukar_guling.py | 11 ++++---- indoteknik_custom/models/tukar_guling_po.py | 41 +++++++++++++++++++++++------ 2 files changed, 38 insertions(+), 14 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index e2f68e6c..5762abbb 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -270,9 +270,7 @@ class TukarGuling(models.Model): else: _logger.info("hehhe") - # if self.operations.picking_type_id.id != 30: - # if self._is_already_returned(self.operations): - # raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") + if 'operations' in vals and not vals.get('origin'): picking = self.env['stock.picking'].browse(vals['operations']) if picking.origin: @@ -330,9 +328,8 @@ class TukarGuling(models.Model): linked_bu_out = picking.linked_manual_bu_out if linked_bu_out and linked_bu_out.state == 'done': raise UserError("❌ Tidak bisa retur BU/PICK karena BU/OUT suda Done!") - if self.operations.picking_type_id.id != 30: - if self._is_already_returned(self.operations): - raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") + if self._is_already_returned(self.operations): + raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") self._check_invoice_on_revisi_so() self._validate_product_lines() @@ -527,10 +524,12 @@ class TukarGuling(models.Model): return return_picking + # CASE: Kalau retur BU/PICK jadi ort saja if record.operations.picking_type_id.id == 30: ort = _create_return_from_picking(record.operations) if ort: created_returns.append(ort) + # Kalau retur BU/OUT else: # CASE: Retur dari BU/OUT srt = _create_return_from_picking(bu_out_to_return) diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index aa8fd1f3..9bcced0d 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -241,11 +241,22 @@ class TukarGulingPO(models.Model): self._check_bill_on_revisi_po() tipe = vals.get('return_type', self.return_type) + if self.operations and self.operations.picking_type_id.id == 28 and tipe == 'tukar_guling': + group = self.operations.group_id + if group: + # Cari BU/PUT dalam group yang sama + bu_put = self.env['stock.picking'].search([ + ('group_id', '=', group.id), + ('picking_type_id.id', '=', 75), # 75 = ID BU/PUT + ('state', '=', 'done') + ], limit=1) + + if bu_put: + raise UserError("❌ Tidak bisa retur BU/INPUT karena BU/PUT sudah Done!") if self.operations.picking_type_id.id == 28 and tipe == 'tukar_guling': raise UserError("❌ BU/INPUT tidak boleh di retur tukar guling") - if self.operations.picking_type_id.id != 28: if self._is_already_returned(self.operations): raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") @@ -287,19 +298,31 @@ class TukarGulingPO(models.Model): def action_submit(self): self.ensure_one() self._check_bill_on_revisi_po() + self._validate_product_lines() + + if self.operations and self.operations.picking_type_id.id == 28 and self.return_type == 'tukar_guling': + group = self.operations.group_id + if group: + # Cari BU/PUT dalam group yang sama + bu_put = self.env['stock.picking'].search([ + ('group_id', '=', group.id), + ('picking_type_id.id', '=', 75), + ('state', '=', 'done') + ], limit=1) + + if bu_put: + raise UserError("❌ Tidak bisa retur BU/INPUT karena BU/PUT sudah Done!") + picking = self.operations if picking.picking_type_id.id == 75: if picking.state != 'done': raise UserError("BU/PUT belum Done!") - elif picking.picking_type_id.id == 28: - linked_bu_out = picking.linked_manual_bu_out - if linked_bu_out and linked_bu_out.state == 'done': - raise UserError("❌ Tidak bisa retur BU/INPUT karena BU/PUT suda Done!") + if picking.picking_type_id.id != 75 or picking.picking_type_id.id != 28: - if self._is_already_returned(self.operations): - raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") - self._validate_product_lines() + raise UserError("❌ Tidak bisa retur bukan BU/INPUT atau BU/PUT!") + if self._is_already_returned(self.operations): + raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") if self.state != 'draft': raise UserError("Submit hanya bisa dilakukan dari Draft.") @@ -457,10 +480,12 @@ class TukarGulingPO(models.Model): return return_picking + # Kalau retur BU/INPUT Buat prt saja if record.operations.picking_type_id.id == 28: prt = _create_return_from_picking(record.operations) if prt: created_returns.append(prt) + # Kalau retur BU/PUT else: vrt = _create_return_from_picking(bu_put_to_return) if vrt: -- cgit v1.2.3 From c72db0d0fa214e6691fa9a293020e7091a9c82c2 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 9 Jul 2025 13:42:33 +0700 Subject: (andri) fix invoices --- indoteknik_custom/models/account_move.py | 36 +++++++++++++++++++------------- 1 file changed, 21 insertions(+), 15 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 8ef3d273..e63f4cb2 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -76,12 +76,9 @@ class AccountMove(models.Model): def send_due_invoice_reminder(self): today = fields.Date.today() - reminder_days = [-7, -3, 0, 3, 7] - target_dates = [today + timedelta(days=delta) for delta in reminder_days] - target_dates_str = [d.isoformat() for d in target_dates] # Ganti nama partner untuk test jika perlu - partner = self.env['res.partner'].search([('name', 'ilike', 'DAYA ANUGRAH MULYA')], limit=1) + partner = self.env['res.partner'].search([('name', 'ilike', 'TIRTA FRESINDO JAYA')], limit=1) if not partner: _logger.info("Partner tidak ditemukan.") return @@ -92,6 +89,7 @@ class AccountMove(models.Model): ('payment_state', 'not in', ['paid','in_payment', 'reversed']), ('invoice_date_due', '>=', today - timedelta(days=7)), ('invoice_date_due', '>=', today - timedelta(days=3)), + ('invoice_date_due', '>=', today - timedelta(days=0)), ('invoice_date_due', '<=', today + timedelta(days=3)), ('invoice_date_due', '<=', today + timedelta(days=7)), ('partner_id', '=', partner.id), @@ -115,17 +113,25 @@ class AccountMove(models.Model): # Ambil template template = self.env.ref('indoteknik_custom.mail_template_invoice_due_reminder') - for inv in invoices: - try: - # Untuk test: override ke email pribadi Anda - email_values = { - 'email_to': 'andrifebriyadiputra@gmail.com', - 'email_from': 'finance@indoteknik.co.id', - } - template.send_mail(inv.id, force_send=True, email_values=email_values) - _logger.info(f"Reminder terkirim: {inv.name} → {email_values['email_to']}") - except Exception as e: - _logger.error(f"Gagal kirim email untuk {inv.name}: {str(e)}") + try: + template.with_context(invoices=invoices).send_mail(partner.id, force_send=True, email_values={ + 'email_to': 'andrifebriyadiputra@gmail.com', # test override + }) + _logger.info(f"Reminder terkirim ke {partner.name} → {len(invoices)} invoice") + except Exception as e: + _logger.error(f"Gagal kirim email ke {partner.name}: {str(e)}") + + # for inv in invoices: + # try: + # # Untuk test: override ke email pribadi Anda + # email_values = { + # 'email_to': 'andrifebriyadiputra@gmail.com', + # 'email_from': 'finance@indoteknik.co.id', + # } + # template.send_mail(inv.id, force_send=True, email_values=email_values) + # _logger.info(f"Reminder terkirim: {inv.name} → {email_values['email_to']}") + # except Exception as e: + # _logger.error(f"Gagal kirim email untuk {inv.name}: {str(e)}") # def name_get(self): # result = [] -- cgit v1.2.3 From 028480352e86ac8cbfef5ea4834caf111ebfb3d4 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 9 Jul 2025 14:04:27 +0700 Subject: (andri) try --- indoteknik_custom/models/account_move.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index e63f4cb2..436dfcaa 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -114,12 +114,15 @@ class AccountMove(models.Model): template = self.env.ref('indoteknik_custom.mail_template_invoice_due_reminder') try: - template.with_context(invoices=invoices).send_mail(partner.id, force_send=True, email_values={ - 'email_to': 'andrifebriyadiputra@gmail.com', # test override - }) - _logger.info(f"Reminder terkirim ke {partner.name} → {len(invoices)} invoice") + email_values = template.with_context(invoices=invoices).generate_email(partner.id) + email_values['email_to'] = 'andrifebriyadiputra@gmail.com' # override untuk test + email_values['email_from'] = 'finance@indoteknik.co.id' + + self.env['mail.mail'].create(email_values).send() + _logger.info(f"[Reminder Terkirim] ke {partner.name} → {len(invoices)} invoice") except Exception as e: - _logger.error(f"Gagal kirim email ke {partner.name}: {str(e)}") + _logger.error(f"[Reminder Gagal] ke {partner.name} → {str(e)}") + # for inv in invoices: # try: -- cgit v1.2.3 From 00b357fa35ff809c153a5aeaf67f97a00715e463 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 9 Jul 2025 14:33:52 +0700 Subject: (andr) try2 --- indoteknik_custom/models/account_move.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 436dfcaa..c3908daf 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -114,7 +114,9 @@ class AccountMove(models.Model): template = self.env.ref('indoteknik_custom.mail_template_invoice_due_reminder') try: - email_values = template.with_context(invoices=invoices).generate_email(partner.id) + email_values = template.with_context(invoices=invoices).generate_email( + partner.id, + ['subject', 'body_html', 'email_to', 'email_from']) email_values['email_to'] = 'andrifebriyadiputra@gmail.com' # override untuk test email_values['email_from'] = 'finance@indoteknik.co.id' -- cgit v1.2.3 From c7781579662dbf2b66dcaff5d1b3737e9e32c3c5 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 10 Jul 2025 14:08:38 +0700 Subject: sort dunning run based on invoice num --- indoteknik_custom/models/dunning_run.py | 4 +++- indoteknik_custom/models/sale_order_line.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/dunning_run.py b/indoteknik_custom/models/dunning_run.py index bb53fc0c..682c7b0b 100644 --- a/indoteknik_custom/models/dunning_run.py +++ b/indoteknik_custom/models/dunning_run.py @@ -1,3 +1,4 @@ +from Tools.scripts.dutree import store from odoo import models, api, fields from odoo.exceptions import AccessError, UserError, ValidationError from datetime import timedelta @@ -123,8 +124,9 @@ class DunningRunLine(models.Model): _name = 'dunning.run.line' _description = 'Dunning Run Line' # _order = 'dunning_id, id' - _order = 'invoice_id desc, id' + _order = 'invoice_number asc, id' + invoice_number = fields.Char('Invoice Number', related='invoice_id.name', store=True) dunning_id = fields.Many2one('dunning.run', string='Dunning Ref', required=True, ondelete='cascade', index=True, copy=False) partner_id = fields.Many2one('res.partner', string='Customer') invoice_id = fields.Many2one('account.move', string='Invoice') diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index 291940ed..c4b5381a 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -5,6 +5,9 @@ from datetime import datetime, timedelta class SaleOrderLine(models.Model): _inherit = 'sale.order.line' + + hold_item = fields.Boolean('Hold?', default=False) + item_margin = fields.Float('Margin', compute='compute_item_margin', help="Total Margin in Sales Order Header") item_before_margin = fields.Float('Before Margin', compute='compute_item_before_margin', help="Total Margin in Sales Order Header") -- cgit v1.2.3 From d5a9aa70794de3604a1db9fdcb5f6952afa4a52b Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 10 Jul 2025 14:12:52 +0700 Subject: sort dunning run based on invoice num --- indoteknik_custom/models/dunning_run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/dunning_run.py b/indoteknik_custom/models/dunning_run.py index 682c7b0b..fdc730de 100644 --- a/indoteknik_custom/models/dunning_run.py +++ b/indoteknik_custom/models/dunning_run.py @@ -126,7 +126,7 @@ class DunningRunLine(models.Model): # _order = 'dunning_id, id' _order = 'invoice_number asc, id' - invoice_number = fields.Char('Invoice Number', related='invoice_id.name', store=True) + invoice_number = fields.Char('Invoice Number', related='invoice_id.name') dunning_id = fields.Many2one('dunning.run', string='Dunning Ref', required=True, ondelete='cascade', index=True, copy=False) partner_id = fields.Many2one('res.partner', string='Customer') invoice_id = fields.Many2one('account.move', string='Invoice') -- cgit v1.2.3 From d42597543c17a72173d50aa66939c0f3ab776363 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 10 Jul 2025 14:14:06 +0700 Subject: sort dunning run based on invoice num --- indoteknik_custom/models/sale_order_line.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index c4b5381a..2a0160e8 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -6,8 +6,6 @@ from datetime import datetime, timedelta class SaleOrderLine(models.Model): _inherit = 'sale.order.line' - hold_item = fields.Boolean('Hold?', default=False) - item_margin = fields.Float('Margin', compute='compute_item_margin', help="Total Margin in Sales Order Header") item_before_margin = fields.Float('Before Margin', compute='compute_item_before_margin', help="Total Margin in Sales Order Header") -- cgit v1.2.3 From 6ab4b390aec6ae68e7c3a43fae6bfd730ce54230 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 10 Jul 2025 14:18:37 +0700 Subject: sort dunning run based on invoice num --- indoteknik_custom/models/dunning_run.py | 1 - 1 file changed, 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/dunning_run.py b/indoteknik_custom/models/dunning_run.py index fdc730de..d7178cb4 100644 --- a/indoteknik_custom/models/dunning_run.py +++ b/indoteknik_custom/models/dunning_run.py @@ -1,4 +1,3 @@ -from Tools.scripts.dutree import store from odoo import models, api, fields from odoo.exceptions import AccessError, UserError, ValidationError from datetime import timedelta -- cgit v1.2.3 From 710c5100c5ba4f0a02210418e96a14b66ca03698 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 10 Jul 2025 14:56:54 +0700 Subject: sort dunning run based on invoice num --- indoteknik_custom/models/dunning_run.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/dunning_run.py b/indoteknik_custom/models/dunning_run.py index d7178cb4..341b206d 100644 --- a/indoteknik_custom/models/dunning_run.py +++ b/indoteknik_custom/models/dunning_run.py @@ -92,10 +92,19 @@ class DunningRun(models.Model): ('move_type', '=', 'out_invoice'), ('state', '=', 'posted'), ('partner_id', '=', partner.id), - # ('amount_residual_signed', '>', 0), ('date_kirim_tukar_faktur', '=', False), ] - invoices = self.env['account.move'].search(query, order='invoice_date') + invoices = self.env['account.move'].search(query) + + # sort by last number in invoice name + try: + invoices = sorted( + invoices, + key=lambda x: int((x.name or '0').split('/')[-1]) + ) + except Exception as e: + _logger.error('Gagal sort invoice number: %s', e) + count = 0 for invoice in invoices: self.env['dunning.run.line'].create([{ -- cgit v1.2.3 From 8e80bb240aa74c8b2942d983e73ff501f5b8defc Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 10 Jul 2025 15:12:36 +0700 Subject: (andri) rev mutiple invoices reminder --- indoteknik_custom/models/account_move.py | 78 ++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 20 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index df79b9f6..fd72d566 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -9,6 +9,7 @@ import os import re from terbilang import Terbilang from collections import defaultdict +from odoo.tools.misc import formatLang _logger = logging.getLogger(__name__) @@ -76,12 +77,8 @@ class AccountMove(models.Model): def send_due_invoice_reminder(self): today = fields.Date.today() - reminder_days = [-7, -3, 0, 3, 7] - target_dates = [today + timedelta(days=delta) for delta in reminder_days] - target_dates_str = [d.isoformat() for d in target_dates] - # Ganti nama partner untuk test jika perlu - partner = self.env['res.partner'].search([('name', 'ilike', 'PRIMA SEJAHTERA MARITIM')], limit=1) + partner = self.env['res.partner'].search([('name', 'ilike', 'TIRTA FRESINDO JAYA')], limit=1) if not partner: _logger.info("Partner tidak ditemukan.") return @@ -92,6 +89,7 @@ class AccountMove(models.Model): ('payment_state', 'not in', ['paid','in_payment', 'reversed']), ('invoice_date_due', '>=', today - timedelta(days=7)), ('invoice_date_due', '>=', today - timedelta(days=3)), + ('invoice_date_due', '=', today), ('invoice_date_due', '<=', today + timedelta(days=3)), ('invoice_date_due', '<=', today + timedelta(days=7)), ('partner_id', '=', partner.id), @@ -99,7 +97,6 @@ class AccountMove(models.Model): _logger.info(f"Invoices tahap 1: {invoices}") - # Filter berdasarkan term mengandung "tempo" invoices = invoices.filtered( lambda inv: inv.invoice_payment_term_id and 'tempo' in (inv.invoice_payment_term_id.name or '').lower() ) @@ -109,23 +106,64 @@ class AccountMove(models.Model): _logger.info(f"Tidak ada invoice yang due untuk partner: {partner.name}") return - # Pastikan field compute jalan - invoices._compute_invoice_day_to_due() + grouped = {} + for inv in invoices: + grouped.setdefault(inv.partner_id, []).append(inv) - # Ambil template template = self.env.ref('indoteknik_custom.mail_template_invoice_due_reminder') - for inv in invoices: - try: - # Untuk test: override ke email pribadi Anda - email_values = { - 'email_to': 'andrifebriyadiputra@gmail.com', - 'email_from': 'finance@indoteknik.co.id', - } - template.send_mail(inv.id, force_send=True, email_values=email_values) - _logger.info(f"Reminder terkirim: {inv.name} → {email_values['email_to']}") - except Exception as e: - _logger.error(f"Gagal kirim email untuk {inv.name}: {str(e)}") + for partner, invs in grouped.items(): + if not partner.email: + _logger.info(f"Partner {partner.name} tidak memiliki email") + continue + + invoice_table_rows = "" + for inv in invs: + days_to_due = (inv.invoice_date_due - today).days if inv.invoice_date_due else 0 + invoice_table_rows += f""" + + {inv.name} + {fields.Date.to_string(inv.invoice_date) or '-'} + {fields.Date.to_string(inv.invoice_date_due) or '-'} + {days_to_due} + {formatLang(self.env, inv.amount_total, currency_obj=inv.currency_id)} + {inv.ref or '-'} + + """ + + subject = f"Reminder Invoice Due - {partner.name}" + body_html = re.sub( + r"]*>.*?", + f"{invoice_table_rows}", + template.body_html, + flags=re.DOTALL + ).replace('${object.name}', partner.name) \ + .replace('${object.partner_id.name}', partner.name) \ + .replace('${object.email}', partner.email or '') + + values = { + 'subject': subject, + 'email_to': 'andrifebriyadiputra@gmail.com', # Ubah ke partner.email untuk produksi + 'email_from': 'finance@indoteknik.co.id', + 'body_html': body_html, + } + + _logger.info(f"VALUES: {values}") + + # self.env['mail.mail'].create(values).send() + # _logger.info(f"Reminder terkirim ke {partner.name} ({values['email_to']}) → {len(invs)} invoice") + + # for inv in invoices: + # try: + # # Untuk test: override ke email pribadi Anda + # email_values = { + # 'email_to': 'andrifebriyadiputra@gmail.com', + # 'email_from': 'finance@indoteknik.co.id', + # } + # template.send_mail(inv.id, force_send=True, email_values=email_values) + # _logger.info(f"Reminder terkirim: {inv.name} → {email_values['email_to']}") + # except Exception as e: + # _logger.error(f"Gagal kirim email untuk {inv.name}: {str(e)}") # def name_get(self): # result = [] -- cgit v1.2.3 From b2c6b57b7c621379aea029d2c716282cc65db6e0 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 10 Jul 2025 15:12:56 +0700 Subject: sort dunning run based on invoice num --- indoteknik_custom/models/dunning_run.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/dunning_run.py b/indoteknik_custom/models/dunning_run.py index 341b206d..5a6aebac 100644 --- a/indoteknik_custom/models/dunning_run.py +++ b/indoteknik_custom/models/dunning_run.py @@ -96,14 +96,18 @@ class DunningRun(models.Model): ] invoices = self.env['account.move'].search(query) - # sort by last number in invoice name - try: - invoices = sorted( - invoices, - key=lambda x: int((x.name or '0').split('/')[-1]) - ) - except Exception as e: - _logger.error('Gagal sort invoice number: %s', e) + # sort full berdasarkan tahun, bulan, nomor + def invoice_key(x): + try: + parts = x.name.split('/') + tahun = int(parts[1]) + bulan = int(parts[2]) + nomor = int(parts[3]) + return (tahun, bulan, nomor) + except Exception: + return (0, 0, 0) + + invoices = sorted(invoices, key=invoice_key) count = 0 for invoice in invoices: -- cgit v1.2.3 From 6aee89eff0e1511c257c60fac9fa84172729063c Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 10 Jul 2025 15:13:18 +0700 Subject: (andri) apus bagian yang tak perlu --- indoteknik_custom/models/account_move.py | 26 -------------------------- 1 file changed, 26 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index fd72d566..eb39a1ac 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -153,32 +153,6 @@ class AccountMove(models.Model): # self.env['mail.mail'].create(values).send() # _logger.info(f"Reminder terkirim ke {partner.name} ({values['email_to']}) → {len(invs)} invoice") - # for inv in invoices: - # try: - # # Untuk test: override ke email pribadi Anda - # email_values = { - # 'email_to': 'andrifebriyadiputra@gmail.com', - # 'email_from': 'finance@indoteknik.co.id', - # } - # template.send_mail(inv.id, force_send=True, email_values=email_values) - # _logger.info(f"Reminder terkirim: {inv.name} → {email_values['email_to']}") - # except Exception as e: - # _logger.error(f"Gagal kirim email untuk {inv.name}: {str(e)}") - - # def name_get(self): - # result = [] - # for move in self: - # if move.move_type == 'entry': - # # Jika masih draft, tampilkan 'Draft CAB' - # if move.state == 'draft': - # label = 'Draft CAB' - # else: - # label = move.name - # result.append((move.id, label)) - # else: - # # Untuk invoice dan lainnya, pakai default - # result.append((move.id, move.display_name)) - # return result @api.onchange('invoice_date') def _onchange_invoice_date(self): -- cgit v1.2.3 From 839474c5f411b8c6c2476d8dcda9a6068d9848e5 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 10 Jul 2025 15:28:10 +0700 Subject: (andri) try test --- indoteknik_custom/models/account_move.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 822c54f7..6c4eb14b 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -150,8 +150,8 @@ class AccountMove(models.Model): _logger.info(f"VALUES: {values}") - # self.env['mail.mail'].create(values).send() - # _logger.info(f"Reminder terkirim ke {partner.name} ({values['email_to']}) → {len(invs)} invoice") + self.env['mail.mail'].create(values).send() + _logger.info(f"Reminder terkirim ke {partner.name} ({values['email_to']}) → {len(invs)} invoice") @api.onchange('invoice_date') -- cgit v1.2.3 From b3003dfcffa29390ec078ed206c9b013e683d1c8 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 10 Jul 2025 15:41:26 +0700 Subject: (andri) try --- indoteknik_custom/models/account_move.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 6c4eb14b..33149cb0 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -78,7 +78,7 @@ class AccountMove(models.Model): def send_due_invoice_reminder(self): today = fields.Date.today() - partner = self.env['res.partner'].search([('name', 'ilike', 'TIRTA FRESINDO JAYA')], limit=1) + partner = self.env['res.partner'].search([('name', 'ilike', 'GEMILANG TUJUH BERSAUDARA')], limit=1) if not partner: _logger.info("Partner tidak ditemukan.") return -- cgit v1.2.3 From 9aa1682f36cad78e04d3367c1d30867c7706a5d1 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 10 Jul 2025 17:15:02 +0700 Subject: (andri) fix invoices date due --- indoteknik_custom/models/account_move.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 33149cb0..ddd2f7d9 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -77,8 +77,15 @@ class AccountMove(models.Model): def send_due_invoice_reminder(self): today = fields.Date.today() - - partner = self.env['res.partner'].search([('name', 'ilike', 'GEMILANG TUJUH BERSAUDARA')], limit=1) + target_dates = [ + today - timedelta(days=7), + today - timedelta(days=3), + today, + today + timedelta(days=3), + today + timedelta(days=7), + ] + + partner = self.env['res.partner'].search([('name', 'ilike', 'PROBAN OSTBURG TRISAKTI')], limit=1) if not partner: _logger.info("Partner tidak ditemukan.") return @@ -87,11 +94,7 @@ class AccountMove(models.Model): ('move_type', '=', 'out_invoice'), ('state', '=', 'posted'), ('payment_state', 'not in', ['paid','in_payment', 'reversed']), - ('invoice_date_due', '>=', today - timedelta(days=7)), - ('invoice_date_due', '>=', today - timedelta(days=3)), - ('invoice_date_due', '>=', today), - ('invoice_date_due', '<=', today + timedelta(days=3)), - ('invoice_date_due', '<=', today + timedelta(days=7)), + ('invoice_date_due', 'in', target_dates), ('partner_id', '=', partner.id), ]) @@ -138,8 +141,8 @@ class AccountMove(models.Model): template.body_html, flags=re.DOTALL ).replace('${object.name}', partner.name) \ - .replace('${object.partner_id.name}', partner.name) \ - .replace('${object.email}', partner.email or '') + .replace('${object.partner_id.name}', partner.name) + # .replace('${object.email}', partner.email or '') values = { 'subject': subject, @@ -150,7 +153,7 @@ class AccountMove(models.Model): _logger.info(f"VALUES: {values}") - self.env['mail.mail'].create(values).send() + template.send_mail(invs[0].id, force_send=True, email_values=values) _logger.info(f"Reminder terkirim ke {partner.name} ({values['email_to']}) → {len(invs)} invoice") -- cgit v1.2.3 From 575a7a506382487a625914a7bde9a18b20173cc6 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Fri, 11 Jul 2025 10:45:26 +0700 Subject: (andri) rev template email & fix sequence approval --- indoteknik_custom/models/account_move.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index ddd2f7d9..5ac1c6e5 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -85,7 +85,7 @@ class AccountMove(models.Model): today + timedelta(days=7), ] - partner = self.env['res.partner'].search([('name', 'ilike', 'PROBAN OSTBURG TRISAKTI')], limit=1) + partner = self.env['res.partner'].search([('name', 'ilike', 'FLYNINDO MEGA PERSADA')], limit=1) if not partner: _logger.info("Partner tidak ditemukan.") return -- cgit v1.2.3 From b298a37963027a08e0046629bbcb795effa58e3a Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Fri, 11 Jul 2025 15:40:38 +0700 Subject: view and add new status on approval payment term --- indoteknik_custom/models/approval_payment_term.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/approval_payment_term.py b/indoteknik_custom/models/approval_payment_term.py index 6e1c8103..cd54b53a 100644 --- a/indoteknik_custom/models/approval_payment_term.py +++ b/indoteknik_custom/models/approval_payment_term.py @@ -31,7 +31,13 @@ class ApprovalPaymentTerm(models.Model): approve_leader = fields.Boolean('Approve Pimpinan', tracking=True, copy=False) reason = fields.Text('Reason', tracking=True) approve_date = fields.Datetime('Approve Date') - state = fields.Selection([('waiting_approval', 'Waiting Approval'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='waiting_approval', tracking=True) + state = fields.Selection([ + ('waiting_approval_sales_manager', 'Waiting Approval Sales Manager'), + ('waiting_approval_finance', 'Waiting Approval Finance'), + ('waiting_approval_leader', 'Waiting Approval Leader'), + ('approved', 'Approved'), + ('rejected', 'Rejected')], + default='waiting_approval_sales_manager', tracking=True) reason_reject = fields.Selection([('reason1', 'Reason 1'), ('reason2', 'Reason 2'), ('reason3', 'Reason 3')], string='Reason Reject', tracking=True) sale_order_ids = fields.Many2many( 'sale.order', @@ -77,10 +83,12 @@ class ApprovalPaymentTerm(models.Model): if (not user.id ==7 and user.id == 19 and not self.approve_sales_manager) or is_it: self.approve_sales_manager = True + self.state = 'waiting_approval_finance' return if (not user.id ==7 and user.id == 688 and not self.approve_finance) or is_it: self.approve_finance = True + self.state = 'waiting_approval_leader' return if (user.id == 7 and self.approve_finance) or is_it: -- cgit v1.2.3 From f6d21f2f6a8bbc597e70aa07a4f6c2fc43b4c176 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Sat, 12 Jul 2025 10:20:18 +0700 Subject: add change log widya only --- indoteknik_custom/models/approval_payment_term.py | 62 +++++++++++++++++++++++ 1 file changed, 62 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/approval_payment_term.py b/indoteknik_custom/models/approval_payment_term.py index cd54b53a..025f9ed4 100644 --- a/indoteknik_custom/models/approval_payment_term.py +++ b/indoteknik_custom/models/approval_payment_term.py @@ -53,6 +53,68 @@ class ApprovalPaymentTerm(models.Model): grand_total = fields.Float(string='Grand Total', compute="_compute_grand_total") + change_log_688 = fields.Text(string="Change Log", readonly=True, copy=False) + + def write(self, vals): + # Ambil nilai lama sebelum perubahan + old_values_dict = { + rec.id: rec.read(vals.keys())[0] + for rec in self + } + + res = super().write(vals) + + self._track_changes_for_user_688(vals, old_values_dict) + return res + + def _track_changes_for_user_688(self, vals, old_values_dict): + if self.env.user.id != 688: + return + + for rec in self: + changes = [] + old_values = old_values_dict.get(rec.id, {}) + + for field_name, new_value in vals.items(): + if field_name not in rec._fields or field_name == 'change_log_688': + continue + + field = rec._fields[field_name] + old_value = old_values.get(field_name) + + field_label = field.string # Ambil label user-friendly + + # Relational field + if field.type == 'many2one': + old_id = old_value[0] if old_value else False + is_different = old_id != new_value + if is_different: + old_display = old_value[1] if old_value else 'False' + new_display = rec.env[field.comodel_name].browse(new_value).display_name if new_value else 'False' + changes.append(f"[{field_label}] dari '{old_display}' ke '{new_display}'") + + else: + # Float khusus + if field.type == 'float': + is_different = not self._float_equal(old_value, new_value) + else: + is_different = old_value != new_value + + if is_different: + changes.append(f"[{field_label}] dari '{old_value}' ke '{new_value}'") + + if changes: + timestamp = fields.Datetime.now().strftime('%Y-%m-%d %H:%M:%S') + rec.change_log_688 = f"{timestamp} - Perubahan oleh Widya:\n" + "\n".join(changes) + + + @staticmethod + def _float_equal(val1, val2, eps=1e-6): + try: + return abs(float(val1 or 0.0) - float(val2 or 0.0)) < eps + except Exception: + return False + def _compute_grand_total(self): for rec in self: grand_total = sum(order.amount_total for order in rec.sale_order_ids) -- cgit v1.2.3 From 6f4d19cd6985790414b06678fe2147c431caed9e Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 12 Jul 2025 11:28:38 +0700 Subject: (andri)chatter send email reminder due invoices terkait --- indoteknik_custom/models/account_move.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 5ac1c6e5..42f9f43a 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -85,7 +85,7 @@ class AccountMove(models.Model): today + timedelta(days=7), ] - partner = self.env['res.partner'].search([('name', 'ilike', 'FLYNINDO MEGA PERSADA')], limit=1) + partner = self.env['res.partner'].search([('name', 'ilike', 'BANGUNAN TEKNIK GRUP')], limit=1) if not partner: _logger.info("Partner tidak ditemukan.") return @@ -153,7 +153,22 @@ class AccountMove(models.Model): _logger.info(f"VALUES: {values}") - template.send_mail(invs[0].id, force_send=True, email_values=values) + # template.send_mail(invs[0].id, force_send=True, email_values=values) + + # Default System User + user_system = self.env['res.users'].browse(25) + system_id = user_system.partner_id.id if user_system else False + _logger.info(f"System User: {user_system.name} ({user_system.id})") + _logger.info(f"System User ID: {system_id}") + + for inv in invs: + inv.message_post( + subject=subject, + body=body_html, + subtype_id=self.env.ref('mail.mt_note').id, + author_id=system_id, + ) + _logger.info(f"Reminder terkirim ke {partner.name} ({values['email_to']}) → {len(invs)} invoice") -- cgit v1.2.3 From 8ff2da221c3e744706a69b0f8016f65169b61aca Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 12 Jul 2025 14:18:20 +0700 Subject: (andri) add reply dan masuk ke chatter --- indoteknik_custom/models/account_move.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 42f9f43a..72ac5452 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -149,11 +149,12 @@ class AccountMove(models.Model): 'email_to': 'andrifebriyadiputra@gmail.com', # Ubah ke partner.email untuk produksi 'email_from': 'finance@indoteknik.co.id', 'body_html': body_html, + 'reply_to': f'invoice+account.move_{invs[0].id}@indoteknik.co.id', } _logger.info(f"VALUES: {values}") - # template.send_mail(invs[0].id, force_send=True, email_values=values) + template.send_mail(invs[0].id, force_send=True, email_values=values) # Default System User user_system = self.env['res.users'].browse(25) -- cgit v1.2.3 From 2af07f60a639089efc553966799b3dc225a397a2 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 14 Jul 2025 10:41:17 +0700 Subject: (andri) tracking & sync edit shipping method pada SO ke DO terkait --- indoteknik_custom/models/sale_order.py | 4 ++++ indoteknik_custom/models/stock_picking.py | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 591951ca..e197a6af 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -3048,6 +3048,10 @@ class SaleOrder(models.Model): if picking.state == 'assigned': picking.carrier_id = vals['carrier_id'] + for picking in order.picking_ids: + if picking.state not in ['done', 'cancel', 'assigned']: + picking.write({'carrier_id': vals['carrier_id']}) + try: helper_ids = self._get_helper_ids() if str(self.env.user.id) in helper_ids: diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 69718c7e..ccd42e94 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -119,7 +119,7 @@ class StockPicking(models.Model): waybill_id = fields.One2many(comodel_name='airway.bill', inverse_name='do_id', string='Airway Bill') purchase_representative_id = fields.Many2one('res.users', related='move_lines.purchase_line_id.order_id.user_id', string="Purchase Representative") - carrier_id = fields.Many2one('delivery.carrier', string='Shipping Method') + carrier_id = fields.Many2one('delivery.carrier', string='Shipping Method', tracking=3) shipping_status = fields.Char(string='Shipping Status', compute="_compute_shipping_status") date_reserved = fields.Datetime(string="Date Reserved", help='Tanggal ter-reserved semua barang nya', copy=False) status_printed = fields.Selection([ @@ -281,12 +281,12 @@ class StockPicking(models.Model): biteship_shipping_price = fields.Monetary('Biteship Shipping Price', currency_field='currency_id', help="Harga pengiriman dari Biteship") currency_id = fields.Many2one('res.currency', related='sale_id.currency_id', string='Currency', readonly=True) final_seq = fields.Float(string='Remaining Time') - shipping_method_so_id = fields.Many2one('delivery.carrier', string='Shipping Method', related='sale_id.carrier_id', help="Shipping Method yang digunakan di SO") - shipping_option_so_id = fields.Many2one('shipping.option', string='Shipping Option', related='sale_id.shipping_option_id' , help="Shipping Option yang digunakan di SO") + shipping_method_so_id = fields.Many2one('delivery.carrier', string='Shipping Method', related='sale_id.carrier_id', help="Shipping Method yang digunakan di SO", tracking=3) + shipping_option_so_id = fields.Many2one('shipping.option', string='Shipping Option', related='sale_id.shipping_option_id' , help="Shipping Option yang digunakan di SO", tracking=3) select_shipping_option_so = fields.Selection([ ('biteship', 'Biteship'), ('custom', 'Custom'), - ], string='Shipping Type', related='sale_id.select_shipping_option', help="Shipping Type yang digunakan di SO") + ], string='Shipping Type', related='sale_id.select_shipping_option', help="Shipping Type yang digunakan di SO", tracking=3) state_packing = fields.Selection([('not_packing', 'Belum Packing'), ('packing_done', 'Sudah Packing')], string='Packing Status') approval_invoice_date_id = fields.Many2one('approval.invoice.date', string='Approval Invoice Date') last_update_date_doc_kirim = fields.Datetime(string='Last Update Tanggal Kirim', copy=False) -- cgit v1.2.3 From 27bd82c6e3e9ee8239751fffda02d4975ba23e68 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 14 Jul 2025 10:54:17 +0700 Subject: done --- indoteknik_custom/models/stock_picking.py | 100 ++++++++++++++++-------------- 1 file changed, 55 insertions(+), 45 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index ccd42e94..f7f854c7 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -20,8 +20,10 @@ _logger = logging.getLogger(__name__) _biteship_url = "https://api.biteship.com/v1" biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo" + + # biteship_api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA" - + class StockPicking(models.Model): _inherit = 'stock.picking' @@ -278,16 +280,22 @@ class StockPicking(models.Model): biteship_driver_plate_number = fields.Char('Biteship Driver Plate Number') biteship_courier_link = fields.Char('Biteship Courier Link') biteship_shipping_status = fields.Char('Biteship Shipping Status', help="Status pengiriman dari Biteship") - biteship_shipping_price = fields.Monetary('Biteship Shipping Price', currency_field='currency_id', help="Harga pengiriman dari Biteship") + biteship_shipping_price = fields.Monetary('Biteship Shipping Price', currency_field='currency_id', + help="Harga pengiriman dari Biteship") currency_id = fields.Many2one('res.currency', related='sale_id.currency_id', string='Currency', readonly=True) final_seq = fields.Float(string='Remaining Time') - shipping_method_so_id = fields.Many2one('delivery.carrier', string='Shipping Method', related='sale_id.carrier_id', help="Shipping Method yang digunakan di SO", tracking=3) - shipping_option_so_id = fields.Many2one('shipping.option', string='Shipping Option', related='sale_id.shipping_option_id' , help="Shipping Option yang digunakan di SO", tracking=3) + shipping_method_so_id = fields.Many2one('delivery.carrier', string='Shipping Method', related='sale_id.carrier_id', + help="Shipping Method yang digunakan di SO", tracking=3) + shipping_option_so_id = fields.Many2one('shipping.option', string='Shipping Option', + related='sale_id.shipping_option_id', + help="Shipping Option yang digunakan di SO", tracking=3) select_shipping_option_so = fields.Selection([ ('biteship', 'Biteship'), ('custom', 'Custom'), - ], string='Shipping Type', related='sale_id.select_shipping_option', help="Shipping Type yang digunakan di SO", tracking=3) - state_packing = fields.Selection([('not_packing', 'Belum Packing'), ('packing_done', 'Sudah Packing')], string='Packing Status') + ], string='Shipping Type', related='sale_id.select_shipping_option', help="Shipping Type yang digunakan di SO", + tracking=3) + state_packing = fields.Selection([('not_packing', 'Belum Packing'), ('packing_done', 'Sudah Packing')], + string='Packing Status') approval_invoice_date_id = fields.Many2one('approval.invoice.date', string='Approval Invoice Date') last_update_date_doc_kirim = fields.Datetime(string='Last Update Tanggal Kirim', copy=False) update_date_doc_kirim_add = fields.Boolean(string='Update Tanggal Kirim Lewat ADD') @@ -298,7 +306,7 @@ class StockPicking(models.Model): if not self.name or not self.origin: return False return f"{self.name}" - + def _download_pod_photo(self, url): """Mengunduh foto POD dari URL""" try: @@ -307,7 +315,7 @@ class StockPicking(models.Model): return base64.b64encode(response.content) except Exception as e: raise UserError(f"Gagal mengunduh foto POD: {str(e)}") - + def _parse_datetime(self, dt_str): """Parse datetime string dari format KGX""" try: @@ -318,37 +326,37 @@ class StockPicking(models.Model): return datetime.strptime(dt_str, '%Y-%m-%dT%H:%M:%S') except ValueError: return False - + def action_get_kgx_pod(self, shipment=False): self.ensure_one() - + awb_number = shipment or self._get_kgx_awb_number() if not awb_number: raise UserError("Nomor AWB tidak dapat dibuat, pastikan picking memiliki name dan origin") - + url = "https://kgx.co.id/get_detail_awb" headers = {'Content-Type': 'application/json'} - payload = {"params" : {'awb_number': awb_number}} - + payload = {"params": {'awb_number': awb_number}} + try: response = requests.post(url, headers=headers, data=json.dumps(payload)) response.raise_for_status() data = response.json() - + if data.get('result', {}).get('data', []): pod_data = data['result']['data'][0].get('connote_pod', {}) photo_url = pod_data.get('photo') - + self.kgx_pod_photo_url = photo_url self.kgx_pod_signature = pod_data.get('signature') self.kgx_pod_receiver = pod_data.get('receiver') self.kgx_pod_receive_time = self._parse_datetime(pod_data.get('timeReceive')) self.driver_arrival_date = self._parse_datetime(pod_data.get('timeReceive')) - + return data else: raise UserError(f"Tidak ditemukan data untuk AWB: {awb_number}") - + except requests.exceptions.RequestException as e: raise UserError(f"Gagal mengambil data POD: {str(e)}") @@ -695,8 +703,9 @@ class StockPicking(models.Model): raise UserError(f"Order ini sudah dikirim ke Biteship. Dengan Tracking Id: {self.biteship_tracking_id}") if self.sale_id.select_shipping_option == 'custom': - raise UserError("Shipping Option pada Sales Order ini adalah *Custom*. Tidak dapat dikirim melalui Biteship.") - + raise UserError( + "Shipping Option pada Sales Order ini adalah *Custom*. Tidak dapat dikirim melalui Biteship.") + def is_courier_need_coordinates(service_code): return service_code in [ "instant", "same_day", "instant_car", @@ -793,11 +802,11 @@ class StockPicking(models.Model): self.message_post( body=f"Biteship berhasil dilakukan.
" - f"Kurir: {self.carrier_id.name}
" - f"Tracking ID: {self.biteship_tracking_id or '-'}
" - f"Resi: {waybill_id or '-'}
" - f"Reference: {self.name}
" - f"SO: {self.sale_id.name}", + f"Kurir: {self.carrier_id.name}
" + f"Tracking ID: {self.biteship_tracking_id or '-'}
" + f"Resi: {waybill_id or '-'}
" + f"Reference: {self.name}
" + f"SO: {self.sale_id.name}", message_type="comment" ) @@ -939,6 +948,9 @@ class StockPicking(models.Model): pending_section = None # Invoice values. invoice_vals = order._prepare_invoice() + invoice_date = self.date_done + invoice_vals['date'] = invoice_date + invoice_vals['invoice_date'] = invoice_date # Invoice line values (keep only necessary sections). for line in self.move_ids_without_package: po_line = self.env['purchase.order.line'].search( @@ -1304,7 +1316,6 @@ class StockPicking(models.Model): ) ) - self.validation_minus_onhand_quantity() self.responsible = self.env.user.id # self.send_koli_to_so() @@ -1355,7 +1366,7 @@ class StockPicking(models.Model): if 'BU/PUT' in self.name: self.automatic_reserve_product() return res - + def automatic_reserve_product(self): if self.state == 'done': po = self.env['purchase.order'].search([ @@ -1373,11 +1384,12 @@ class StockPicking(models.Model): continue invoice = self.env['account.move'].search( - [('sale_id', '=', picking.sale_id.id), ('state', 'not in', ['draft', 'cancel']), ('move_type', '=', 'out_invoice')], limit=1) + [('sale_id', '=', picking.sale_id.id), ('state', 'not in', ['draft', 'cancel']), + ('move_type', '=', 'out_invoice')], limit=1) if not invoice: continue - + if not picking.so_lama and invoice and (not picking.date_doc_kirim or not invoice.invoice_date): raise UserError("Tanggal Kirim atau Tanggal Invoice belum diisi!") @@ -1661,7 +1673,7 @@ class StockPicking(models.Model): self.ensure_one() order = self.env['sale.order'].search([('name', '=', self.sale_id.name)], limit=1) - + sale_order_delay = self.env['sale.order.delay'].search([('so_number', '=', order.name)], limit=1) product_shipped = [] @@ -1676,11 +1688,12 @@ class StockPicking(models.Model): 'delivery_order': { 'name': self.name, 'carrier': self.carrier_id.name or '-', - 'service' : order.delivery_service_type or '-', + 'service': order.delivery_service_type or '-', 'receiver_name': '', 'receiver_city': '' }, - 'delivered_date': self.driver_departure_date.strftime('%d %b %Y') if self.driver_departure_date != False else '-', + 'delivered_date': self.driver_departure_date.strftime( + '%d %b %Y') if self.driver_departure_date != False else '-', 'delivered': False, 'status': self.shipping_status, 'waybill_number': self.delivery_tracking_no or '-', @@ -1703,7 +1716,7 @@ class StockPicking(models.Model): elif sale_order_delay.status == 'early': day_start = day_start - sale_order_delay.days_delayed day_end = day_end - sale_order_delay.days_delayed - + eta_start = order.date_order + timedelta(days=day_start) eta_end = order.date_order + timedelta(days=day_end) formatted_eta = f"{eta_start.strftime('%d %b')} - {eta_end.strftime('%d %b %Y')}" @@ -1732,7 +1745,7 @@ class StockPicking(models.Model): "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } - + manifests = [] try: @@ -1769,9 +1782,9 @@ class StockPicking(models.Model): "manifests": [], "delivered": False } - except Exception as e : + except Exception as e: _logger.error(f"Error fetching Biteship order for picking {self.id}: {str(e)}") - return { 'error': str(e) } + return {'error': str(e)} # ACTION GET TRACKING MANUAL BITESHIP # def action_sync_biteship_tracking(self): @@ -1855,7 +1868,6 @@ class StockPicking(models.Model): return description_map.get(status, f"Status '{status}' diterima dari Biteship") - def log_biteship_event_from_webhook(self, status, timestamp, description, extra_data=None): self.ensure_one() updated_fields = {} @@ -1916,7 +1928,7 @@ class StockPicking(models.Model): # Hindari log duplikat if not self._has_existing_log(log_line): - biteship_user = self.env['res.users'].sudo().browse(15710) # ID live + biteship_user = self.env['res.users'].sudo().browse(15710) # ID live # biteship_user = self.env['res.users'].sudo().browse(15710) # ID user (cek di db) self.sudo().message_post( body=log_line, @@ -1928,7 +1940,6 @@ class StockPicking(models.Model): self.write(updated_fields) _logger.info(f"[Webhook] Updated fields on picking {self.name}: {updated_fields}") - def _has_existing_log(self, log_line): self.ensure_one() self.env.cr.execute(""" @@ -1995,8 +2006,7 @@ class StockPicking(models.Model): days_end = self.sale_id.estimated_arrival_days or (self.sale_id.estimated_arrival_days + 3) start_date = self.sale_id.create_date + datetime.timedelta(days=days_start) end_date = self.sale_id.create_date + datetime.timedelta(days=days_end) - - + add_day_start = 0 add_day_end = 0 sale_order_delay = self.env['sale.order.delay'].search([('so_number', '=', self.sale_id.name)], limit=1) @@ -2005,11 +2015,11 @@ class StockPicking(models.Model): add_day_start = sale_order_delay.days_delayed add_day_end = sale_order_delay.days_delayed elif sale_order_delay.status == 'early': - add_day_start = -abs(sale_order_delay.days_delayed) - add_day_end = -abs(sale_order_delay.days_delayed) - - fastest_eta = start_date +datetime.timedelta(days=add_day_start + add_day_start) - + add_day_start = -abs(sale_order_delay.days_delayed) + add_day_end = -abs(sale_order_delay.days_delayed) + + fastest_eta = start_date + datetime.timedelta(days=add_day_start + add_day_start) + longest_eta = end_date + datetime.timedelta(days=add_day_end) format_time = '%d %b %Y' -- cgit v1.2.3 From 8c51dd97b7ddba2059d1b648799844aedd733501 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 14 Jul 2025 11:38:37 +0700 Subject: add biaya lain2 field in customer commision --- indoteknik_custom/models/commision.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py index 97184cdb..26b5df37 100644 --- a/indoteknik_custom/models/commision.py +++ b/indoteknik_custom/models/commision.py @@ -193,6 +193,7 @@ class CustomerCommision(models.Model): commision_amt_text = fields.Char(string='Amount Text', compute='compute_delivery_amt_text') total_cashback_text = fields.Char(string='Cashback Text', compute='compute_total_cashback_text') total_dpp = fields.Float(string='Total DPP', compute='_compute_total_dpp') + biaya_lain_lain = fields.Float(string='Biaya Lain-lain') commision_type = fields.Selection([ ('fee', 'Fee'), ('cashback', 'Cashback'), @@ -363,13 +364,13 @@ class CustomerCommision(models.Model): else: self.cashback = 0 self.total_commision = 0 - + def _compute_total_dpp(self): for data in self: total_dpp = 0 for line in data.commision_lines: total_dpp = total_dpp + line.dpp - data.total_dpp = total_dpp + data.total_dpp = total_dpp - data.biaya_lain_lain @api.model def create(self, vals): -- cgit v1.2.3 From f6229d1426fc0823e1b29721f7fbaaec285351ef Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 14 Jul 2025 16:34:59 +0700 Subject: fix bugs --- indoteknik_custom/models/tukar_guling.py | 110 +++++++++++-------------------- 1 file changed, 38 insertions(+), 72 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 5762abbb..24dbf3d8 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -267,8 +267,8 @@ class TukarGuling(models.Model): raise UserError("❌ Tidak bisa retur BU/PICK karena BU/OUT sudah done") if operasi == 30 and pp == 'tukar_guling': raise UserError("❌ BU/PICK tidak boleh di retur tukar guling") - else: - _logger.info("hehhe") + # else: + # _logger.info("hehhe") if 'operations' in vals and not vals.get('origin'): @@ -279,6 +279,8 @@ class TukarGuling(models.Model): return super(TukarGuling, self).write(vals) def unlink(self): + # if self.state == 'done': + # raise UserError ("Tidak Boleh delete ketika sudahh done") for record in self: if record.state == 'done': raise UserError( @@ -344,11 +346,10 @@ class TukarGuling(models.Model): operasi = self.operations.picking_type_id.id tipe = self.return_type - pp = vals.get('return_type', tipe) if operasi == 30 and self.operations.linked_manual_bu_out.state == 'done': raise UserError("❌ Tidak bisa retur BU/PICK karena BU/OUT sudah done") - if operasi == 30 and pp == 'tukar_guling': + if operasi == 30 and tipe == 'tukar_guling': raise UserError("❌ BU/PICK tidak boleh di retur tukar guling") else: _logger.info("hehhe") @@ -398,81 +399,52 @@ class TukarGuling(models.Model): if not record.operations: raise UserError("BU/OUT dari field operations tidak ditemukan.") - operation_picking = record.operations - - related_pickings = self.env['stock.picking'].search([ - ('origin', '=', record.origin), - ('state', '=', 'done'), - ('picking_type_id', 'in', [29, 30]) - ]) - if not related_pickings: - raise UserError( - "Tidak ditemukan BU/PICK atau BU/OUT dari SO: %s" % record.origin) - - # filter based on stock.picking picking type bu_pick_to_return = record.operations.konfirm_koli_lines.pick_id bu_out_to_return = record.operations - # if bu_pick_to_return and self.return_type == 'tukar_guling': - # raise UserError("BU/PICK tidak boleh di retur tukar guling") - if not bu_pick_to_return and not bu_out_to_return: raise UserError("Tidak ada BU/PICK atau BU/OUT yang selesai untuk diretur.") created_returns = [] - # Lokasi default untuk retur + # Picking types & locations srt_type = self.env['stock.picking.type'].browse(73) ort_type = self.env['stock.picking.type'].browse(74) bu_pick_type = self.env['stock.picking.type'].browse(30) bu_out_type = self.env['stock.picking.type'].browse(29) - stock_location = self.env['stock.location'] - - srt_src = stock_location.browse(5) - srt_dest = stock_location.browse(60) - - ort_src = stock_location.browse(60) - ort_dest = stock_location.browse(57) - - if not ort_src or not ort_dest or not srt_src or not srt_dest: - raise UserError("salahwoi") - - # Fungsi membuat retur dari picking tertentu def _create_return_from_picking(picking): - grup = self.operations.group_id + if not picking: + return None + grup = record.operations.group_id PARTNER_LOCATION_ID = 5 BU_OUTPUT_LOCATION_ID = 60 BU_STOCK_LOCATION_ID = 57 - # Determine locations based on picking type if picking.picking_type_id.id == 30: # BU/PICK → ORT return_type = ort_type default_location_id = BU_OUTPUT_LOCATION_ID default_location_dest_id = BU_STOCK_LOCATION_ID - elif picking.picking_type_id.id == 74: # ORT → BU/PICK return_type = bu_pick_type default_location_id = BU_STOCK_LOCATION_ID default_location_dest_id = BU_OUTPUT_LOCATION_ID - elif picking.picking_type_id.id == 29: # BU/OUT → SRT return_type = srt_type default_location_id = PARTNER_LOCATION_ID default_location_dest_id = BU_OUTPUT_LOCATION_ID - elif picking.picking_type_id.id == 73: # SRT → BU/OUT return_type = bu_out_type default_location_id = BU_OUTPUT_LOCATION_ID default_location_dest_id = PARTNER_LOCATION_ID - else: return None + return_context = dict(self.env.context) return_context.update({ 'active_id': picking.id, @@ -487,26 +459,26 @@ class TukarGuling(models.Model): 'original_location_id': default_location_id }) - # Create return lines return_lines = [] + # 🔥 Hanya pakai qty dari tukar guling line for line in record.line_ids: - move = picking.move_lines.filtered(lambda wkwk: wkwk.product_id == line.product_id) + move = picking.move_lines.filtered(lambda m: m.product_id == line.product_id) if move: return_lines.append((0, 0, { 'product_id': line.product_id.id, 'quantity': line.product_uom_qty, - 'move_id': move.id, + 'move_id': move[0].id, })) - if not move: - raise UserError("eror woi") + else: + raise UserError( + _("Tidak ditemukan move line di picking %s untuk produk %s") + % (picking.name, line.product_id.display_name) + ) + if not return_lines: - return None + raise UserError(_("Tidak ada product line valid untuk retur picking %s") % picking.name) return_wizard.product_return_moves = return_lines - - _logger.info("Creating return for picking %s", picking.name) - _logger.info("Default location src: %s", default_location_id) - _logger.info("Default location dest: %s", default_location_dest_id) return_vals = return_wizard.create_returns() return_id = return_vals.get('res_id') return_picking = self.env['stock.picking'].browse(return_id) @@ -514,7 +486,6 @@ class TukarGuling(models.Model): if not return_picking: raise UserError("Retur gagal dibuat. Hasil create_returns: %s" % str(return_vals)) - # Force the destination location return_picking.write({ 'location_dest_id': default_location_dest_id, 'location_id': default_location_id, @@ -524,37 +495,32 @@ class TukarGuling(models.Model): return return_picking - # CASE: Kalau retur BU/PICK jadi ort saja - if record.operations.picking_type_id.id == 30: - ort = _create_return_from_picking(record.operations) + # === PERBAIKI URUTAN === + srt = _create_return_from_picking(bu_out_to_return) + if srt: + created_returns.append(srt) + + picks = self.env['stock.picking'].search([ + ('origin', '=', record.origin), + ('state', '=', 'done'), + ('picking_type_id', '=', 30) + ]) + for picking in picks: + ort = _create_return_from_picking(picking) if ort: created_returns.append(ort) - # Kalau retur BU/OUT - else: - # CASE: Retur dari BU/OUT - srt = _create_return_from_picking(bu_out_to_return) - if srt: - created_returns.append(srt) - - ort = None - if bu_pick_to_return: - ort = _create_return_from_picking(bu_pick_to_return) - if ort: - created_returns.append(ort) - - if record.return_type == 'tukar_guling': - if ort: + if record.return_type == 'tukar_guling': bu_pick = _create_return_from_picking(ort) if bu_pick: created_returns.append(bu_pick) - if srt: - bu_out = _create_return_from_picking(srt) - if bu_out: - created_returns.append(bu_out) + if record.return_type == 'tukar_guling' and srt: + bu_out = _create_return_from_picking(srt) + if bu_out: + created_returns.append(bu_out) if not created_returns: - raise UserError("wkwkwk") + raise UserError("Tidak ada dokumen retur yang berhasil dibuat.") class TukarGulingLine(models.Model): -- cgit v1.2.3 From ee77ca5ec2947ba672c7becee3ecb583f4f25916 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Mon, 14 Jul 2025 17:02:45 +0700 Subject: deactivate code change name bu/in --- indoteknik_custom/models/stock_picking.py | 38 +++++++++++++++---------------- 1 file changed, 19 insertions(+), 19 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 69718c7e..6f260dfa 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1563,25 +1563,25 @@ class StockPicking(models.Model): new_picking.state_packing = 'packing_done' self._use_faktur(vals) self.sync_sale_line(vals) - for picking in self: - # Periksa apakah kondisi terpenuhi saat data diubah - if (vals.get('picking_type_code', picking.picking_type_code) == 'incoming' and - vals.get('location_dest_id', picking.location_dest_id.id) == 58): - if 'name' in vals or picking.name.startswith('BU/IN/'): - name_to_modify = vals.get('name', picking.name) - if name_to_modify.startswith('BU/IN/'): - vals['name'] = name_to_modify.replace('BU/IN/', 'BU/INPUT/', 1) - - if (vals.get('picking_type_code', picking.picking_type_code) == 'internal' and - vals.get('location_id', picking.location_id.id) == 58): - name_to_modify = vals.get('name', picking.name) - if name_to_modify.startswith('BU/INT'): - new_name = name_to_modify.replace('BU/INT', 'BU/IN', 1) - # Periksa apakah nama sudah ada - if self.env['stock.picking'].search_count( - [('name', '=', new_name), ('company_id', '=', picking.company_id.id)]) > 0: - new_name = f"{new_name}-DUP" - vals['name'] = new_name + # for picking in self: + # # Periksa apakah kondisi terpenuhi saat data diubah + # if (vals.get('picking_type_code', picking.picking_type_code) == 'incoming' and + # vals.get('location_dest_id', picking.location_dest_id.id) == 58): + # if 'name' in vals or picking.name.startswith('BU/IN/'): + # name_to_modify = vals.get('name', picking.name) + # if name_to_modify.startswith('BU/IN/'): + # vals['name'] = name_to_modify.replace('BU/IN/', 'BU/INPUT/', 1) + + # if (vals.get('picking_type_code', picking.picking_type_code) == 'internal' and + # vals.get('location_id', picking.location_id.id) == 58): + # name_to_modify = vals.get('name', picking.name) + # if name_to_modify.startswith('BU/INT'): + # new_name = name_to_modify.replace('BU/INT', 'BU/IN', 1) + # # Periksa apakah nama sudah ada + # if self.env['stock.picking'].search_count( + # [('name', '=', new_name), ('company_id', '=', picking.company_id.id)]) > 0: + # new_name = f"{new_name}-DUP" + # vals['name'] = new_name return super(StockPicking, self).write(vals) def _use_faktur(self, vals): -- cgit v1.2.3 From 02cb11bce98b50e66760bc1b755367c0f7ba63f7 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 14 Jul 2025 17:04:54 +0700 Subject: fix bugs --- indoteknik_custom/models/tukar_guling.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 24dbf3d8..8d14012a 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -500,11 +500,7 @@ class TukarGuling(models.Model): if srt: created_returns.append(srt) - picks = self.env['stock.picking'].search([ - ('origin', '=', record.origin), - ('state', '=', 'done'), - ('picking_type_id', '=', 30) - ]) + picks = record.operations.konfirm_koli_lines.pick_id for picking in picks: ort = _create_return_from_picking(picking) if ort: -- cgit v1.2.3 From f3902ac1523b8c5c149a8661ad106363a8c01baf Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 14 Jul 2025 17:46:22 +0700 Subject: vals bu pick --- indoteknik_custom/models/tukar_guling.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 8d14012a..89f5994d 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -205,7 +205,6 @@ class TukarGuling(models.Model): raise ValidationError( _("Tidak bisa memilih Return Type 'Revisi SO' karena dokumen %s sudah dibuat invoice.") % record.origin ) - @api.model def create(self, vals): # Generate sequence number @@ -309,8 +308,18 @@ class TukarGuling(models.Model): else: raise UserError("Hanya record yang di-cancel yang bisa dikembalikan ke draft") + def _check_not_allow_tukar_guling_on_bu_pick(self, return_type=None): + operasi = self.operations.picking_type_id.id + tipe = return_type or self.return_type + + if operasi == 30 and self.operations.linked_manual_bu_out.state == 'done': + raise UserError("❌ Tidak bisa retur BU/PICK karena BU/OUT sudah done") + if operasi == 30 and tipe == 'tukar_guling': + raise UserError("❌ BU/PICK tidak boleh di retur tukar guling") + def action_submit(self): self.ensure_one() + self._check_not_allow_tukar_guling_on_bu_pick() existing_tukar_guling = self.env['tukar.guling'].search([ ('operations', '=', self.operations.id), @@ -343,6 +352,7 @@ class TukarGuling(models.Model): self.ensure_one() self._validate_product_lines() self._check_invoice_on_revisi_so() + self._check_not_allow_tukar_guling_on_bu_pick() operasi = self.operations.picking_type_id.id tipe = self.return_type @@ -351,8 +361,8 @@ class TukarGuling(models.Model): raise UserError("❌ Tidak bisa retur BU/PICK karena BU/OUT sudah done") if operasi == 30 and tipe == 'tukar_guling': raise UserError("❌ BU/PICK tidak boleh di retur tukar guling") - else: - _logger.info("hehhe") + # else: + # _logger.info("hehhe") if not self.operations: raise UserError("Operations harus diisi!") -- cgit v1.2.3 From 2f90a316e0895b7b3167ee1ff36d282736456a2d Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 14 Jul 2025 20:06:30 +0700 Subject: remove replace bu input name --- indoteknik_custom/models/tukar_guling_po.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index 9bcced0d..3698f95a 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -472,11 +472,11 @@ class TukarGulingPO(models.Model): 'location_dest_id': default_location_dest_id, }) - # Rename BU INPUT jadi BU/INPUT/ - if return_picking.picking_type_id.id == 28 and return_picking.name.startswith('BU/IN/'): - old_name = return_picking.name - return_picking.name = return_picking.name.replace('BU/IN/', 'BU/INPUT/', 1) - _logger.info("Rename %s -> %s", old_name, return_picking.name) + # # Rename BU INPUT jadi BU/INPUT/ + # if return_picking.picking_type_id.id == 28 and return_picking.name.startswith('BU/IN/'): + # old_name = return_picking.name + # return_picking.name = return_picking.name.replace('BU/IN/', 'BU/INPUT/', 1) + # _logger.info("Rename %s -> %s", old_name, return_picking.name) return return_picking -- cgit v1.2.3 From 15198bbee7d54580fadec9be6cbf69fb4a2d500d Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 14 Jul 2025 20:25:52 +0700 Subject: vals PO --- indoteknik_custom/models/tukar_guling_po.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index 3698f95a..ba2b3800 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -6,6 +6,7 @@ import logging _logger = logging.getLogger(__name__) + class TukarGulingPO(models.Model): _name = 'tukar.guling.po' _description = 'Tukar Guling PO' @@ -27,7 +28,7 @@ class TukarGulingPO(models.Model): domain=[ ('picking_type_id.id', 'in', [75, 28]), ('state', '=', 'done') - ],help='Nomor BU INPUT atau BU PUT' + ], help='Nomor BU INPUT atau BU PUT' ) ba_num = fields.Char('Nomor BA') return_type = fields.Selection([ @@ -143,6 +144,15 @@ class TukarGulingPO(models.Model): self.origin = False + def _check_not_allow_tukar_guling_on_bu_input(self, return_type=None): + operasi = self.operations.picking_type_id.id + tipe = return_type or self.return_type + + if operasi == 28 and self.operations.linked_manual_bu_out.state == 'done': + raise UserError("❌ Tidak bisa retur BU/INPUT karena BU/PUT sudah done") + if operasi == 28 and tipe == 'tukar_guling': + raise UserError("❌ BU/INPUT tidak boleh di retur tukar guling") + def action_populate_lines(self): """Manual button untuk populate lines - sebagai alternatif""" self.ensure_one() @@ -270,7 +280,7 @@ class TukarGulingPO(models.Model): def unlink(self): for record in self: if record.state == 'done': - raise UserError ("Tidak bisa hapus pengajuan jika sudah done, set ke draft terlebih dahulu") + raise UserError("Tidak bisa hapus pengajuan jika sudah done, set ke draft terlebih dahulu") ongoing_bu = self.po_picking_ids.filtered(lambda p: p.state != 'done') for picking in ongoing_bu: picking.action_cancel() @@ -299,6 +309,7 @@ class TukarGulingPO(models.Model): self.ensure_one() self._check_bill_on_revisi_po() self._validate_product_lines() + self._check_not_allow_tukar_guling_on_bu_input() if self.operations and self.operations.picking_type_id.id == 28 and self.return_type == 'tukar_guling': group = self.operations.group_id @@ -332,6 +343,7 @@ class TukarGulingPO(models.Model): self.ensure_one() self._validate_product_lines() self._check_bill_on_revisi_po() + self._check_not_allow_tukar_guling_on_bu_input() if not self.operations: raise UserError("Operations harus diisi!") @@ -511,7 +523,6 @@ class TukarGulingPO(models.Model): raise UserError("Tidak ada dokumen retur yang berhasil dibuat.") - class TukarGulingLinePO(models.Model): _name = 'tukar.guling.line.po' _description = 'Tukar Guling PO Line' @@ -526,4 +537,4 @@ class TukarGulingLinePO(models.Model): class StockPicking(models.Model): _inherit = 'stock.picking' - tukar_guling_po_id = fields.Many2one('tukar.guling.po', string='Tukar Guling PO Ref') \ No newline at end of file + tukar_guling_po_id = fields.Many2one('tukar.guling.po', string='Tukar Guling PO Ref') -- cgit v1.2.3 From 7f4510f2459e16b7b3f1828f736ec84bc7ea97dc Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 15 Jul 2025 08:53:29 +0700 Subject: fix bug api kgx --- indoteknik_custom/models/stock_picking.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index b9c90551..0efffd2f 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -320,13 +320,18 @@ class StockPicking(models.Model): """Parse datetime string dari format KGX""" try: from datetime import datetime - # Hilangkan timezone jika ada masalah parsing + + if not dt_str: + return False + if '+' in dt_str: dt_str = dt_str.split('+')[0] + return datetime.strptime(dt_str, '%Y-%m-%dT%H:%M:%S') except ValueError: return False + def action_get_kgx_pod(self, shipment=False): self.ensure_one() -- cgit v1.2.3 From 4f8f59f274e8ea19fcaa5f1c6b0e6e30400c66f7 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Tue, 15 Jul 2025 09:12:40 +0700 Subject: try --- indoteknik_custom/models/tukar_guling.py | 2 +- indoteknik_custom/models/tukar_guling_po.py | 89 ++++++++++++++--------------- 2 files changed, 44 insertions(+), 47 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 89f5994d..7e857d02 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -205,6 +205,7 @@ class TukarGuling(models.Model): raise ValidationError( _("Tidak bisa memilih Return Type 'Revisi SO' karena dokumen %s sudah dibuat invoice.") % record.origin ) + @api.model def create(self, vals): # Generate sequence number @@ -269,7 +270,6 @@ class TukarGuling(models.Model): # else: # _logger.info("hehhe") - if 'operations' in vals and not vals.get('origin'): picking = self.env['stock.picking'].browse(vals['operations']) if picking.origin: diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index ba2b3800..6d7d7335 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -390,49 +390,40 @@ class TukarGulingPO(models.Model): created_returns = [] - # Ambil BU INPUT & BU PUT dari group yang sama group = record.operations.group_id - bu_input_to_return = bu_put_to_return = False + bu_inputs = bu_puts = self.env['stock.picking'] if group: po_pickings = self.env['stock.picking'].search([ ('group_id', '=', group.id), ('state', '=', 'done') ]) - bu_input_to_return = po_pickings.filtered(lambda p: p.picking_type_id.id == 28) - bu_put_to_return = po_pickings.filtered(lambda p: p.picking_type_id.id == 75) - bu_input_to_return = bu_input_to_return[0] if bu_input_to_return else False - bu_put_to_return = bu_put_to_return[0] if bu_put_to_return else False + bu_inputs = po_pickings.filtered(lambda p: p.picking_type_id.id == 28) + bu_puts = po_pickings.filtered(lambda p: p.picking_type_id.id == 75) else: raise UserError("Group ID tidak ditemukan pada BU Operations.") - # Fungsi buat return picking + PARTNER_LOCATION_ID = 4 + BU_INPUT_LOCATION_ID = 58 + BU_STOCK_LOCATION_ID = 57 + def _create_return_from_picking(picking): - grup = record.operations.group_id + if not picking: + return None - PARTNER_LOCATION_ID = 4 - BU_INPUT_LOCATION_ID = 58 - BU_STOCK_LOCATION_ID = 57 + grup = record.operations.group_id - # Lokasi sesuai type + # Mapping lokasi sesuai picking type if picking.picking_type_id.id == 28: - # Retur dari BU INPUT → hasilnya jadi PRT (BU Input → Partner) - # tapi wizard tetap diinput sebagai picking_id=28, dari input ke partner default_location_id = BU_INPUT_LOCATION_ID default_location_dest_id = PARTNER_LOCATION_ID - elif picking.picking_type_id.id == 75: - # Retur dari BU PUT → hasilnya jadi VRT (BU Stock → BU Input) default_location_id = BU_STOCK_LOCATION_ID default_location_dest_id = BU_INPUT_LOCATION_ID - elif picking.picking_type_id.id == 77: - # Retur dari VRT → hasilnya jadi PUT lagi (BU Input → BU Stock) default_location_id = BU_INPUT_LOCATION_ID default_location_dest_id = BU_STOCK_LOCATION_ID - elif picking.picking_type_id.id == 76: - # Retur dari PRT → hasilnya jadi INPUT lagi (Partner → BU Input) default_location_id = PARTNER_LOCATION_ID default_location_dest_id = BU_INPUT_LOCATION_ID else: @@ -452,7 +443,7 @@ class TukarGulingPO(models.Model): 'original_location_id': default_location_id }) - # Buat lines + # Sesuai line tukar guling return_lines = [] for line in record.line_ids: move = picking.move_lines.filtered(lambda m: m.product_id == line.product_id) @@ -462,6 +453,11 @@ class TukarGulingPO(models.Model): 'quantity': line.product_uom_qty, 'move_id': move[0].id, })) + else: + raise UserError( + _("Tidak ditemukan move line di picking %s untuk produk %s") + % (picking.name, line.product_id.display_name) + ) if not return_lines: return None @@ -470,7 +466,9 @@ class TukarGulingPO(models.Model): return_vals = return_wizard.create_returns() return_picking = self.env['stock.picking'].browse(return_vals.get('res_id')) - # Paksa locations di picking & moves + if not return_picking: + raise UserError("Retur gagal dibuat. Hasil create_returns: %s" % str(return_vals)) + return_picking.write({ 'location_id': default_location_id, 'location_dest_id': default_location_dest_id, @@ -478,46 +476,45 @@ class TukarGulingPO(models.Model): 'tukar_guling_po_id': record.id, }) + # Paksa lokasi di move lines juga for move in return_picking.move_lines: move.write({ 'location_id': default_location_id, 'location_dest_id': default_location_dest_id, }) - # # Rename BU INPUT jadi BU/INPUT/ - # if return_picking.picking_type_id.id == 28 and return_picking.name.startswith('BU/IN/'): - # old_name = return_picking.name - # return_picking.name = return_picking.name.replace('BU/IN/', 'BU/INPUT/', 1) - # _logger.info("Rename %s -> %s", old_name, return_picking.name) - return return_picking - # Kalau retur BU/INPUT Buat prt saja + # === Eksekusi pembuatan picking === if record.operations.picking_type_id.id == 28: + # Kalau dari BU INPUT → hanya PRT prt = _create_return_from_picking(record.operations) if prt: created_returns.append(prt) - # Kalau retur BU/PUT else: - vrt = _create_return_from_picking(bu_put_to_return) - if vrt: - created_returns.append(vrt) - - prt = None - if bu_input_to_return: - prt = _create_return_from_picking(bu_input_to_return) + # 1. Dari BU PUT buat VRT + for bu_put in bu_puts: + vrt = _create_return_from_picking(bu_put) + if vrt: + created_returns.append(vrt) + + # 2. Dari BU INPUT buat PRT + for bu_input in bu_inputs: + prt = _create_return_from_picking(bu_input) if prt: created_returns.append(prt) - if record.return_type == 'tukar_guling': - if prt: - bu_input = _create_return_from_picking(prt) - if bu_input: - created_returns.append(bu_input) - if vrt: - bu_put = _create_return_from_picking(vrt) - if bu_put: - created_returns.append(bu_put) + # 3. Kalau tukar guling buat lanjut INPUT & PUT + if record.return_type == 'tukar_guling': + for prt in created_returns.filtered(lambda p: p.picking_type_id.id == 76): + bu_input = _create_return_from_picking(prt) + if bu_input: + created_returns.append(bu_input) + + for vrt in created_returns.filtered(lambda p: p.picking_type_id.id == 77): + bu_put = _create_return_from_picking(vrt) + if bu_put: + created_returns.append(bu_put) if not created_returns: raise UserError("Tidak ada dokumen retur yang berhasil dibuat.") -- cgit v1.2.3 From 4dd6ef58e2c60df7cb25b65ae6ad23d3bee2ebc1 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 15 Jul 2025 11:13:04 +0700 Subject: add so number on purchasing job --- indoteknik_custom/models/approval_payment_term.py | 6 +++--- indoteknik_custom/models/mrp_production.py | 2 ++ indoteknik_custom/models/purchasing_job.py | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/approval_payment_term.py b/indoteknik_custom/models/approval_payment_term.py index 025f9ed4..6c857b45 100644 --- a/indoteknik_custom/models/approval_payment_term.py +++ b/indoteknik_custom/models/approval_payment_term.py @@ -143,17 +143,17 @@ class ApprovalPaymentTerm(models.Model): user = self.env.user is_it = user.has_group('indoteknik_custom.group_role_it') - if (not user.id ==7 and user.id == 19 and not self.approve_sales_manager) or is_it: + if (not user.id ==7 and user.id == 19 and not self.approve_sales_manager) or (is_it and not self.approve_sales_manager): self.approve_sales_manager = True self.state = 'waiting_approval_finance' return - if (not user.id ==7 and user.id == 688 and not self.approve_finance) or is_it: + if (not user.id ==7 and user.id == 688 and not self.approve_finance) or (is_it and not self.approve_finance): self.approve_finance = True self.state = 'waiting_approval_leader' return - if (user.id == 7 and self.approve_finance) or is_it: + if (user.id == 7 and self.approve_finance) or (is_it and not self.approve_leader): self.approve_leader = True if not self.approve_finance and not is_it: diff --git a/indoteknik_custom/models/mrp_production.py b/indoteknik_custom/models/mrp_production.py index 85b8405f..7977bdf7 100644 --- a/indoteknik_custom/models/mrp_production.py +++ b/indoteknik_custom/models/mrp_production.py @@ -156,6 +156,8 @@ class MrpProduction(models.Model): 'order_id': new_po.id }]) + new_po.button_confirm() + self.is_po = True return po_ids diff --git a/indoteknik_custom/models/purchasing_job.py b/indoteknik_custom/models/purchasing_job.py index ea2f46cb..58f1c067 100644 --- a/indoteknik_custom/models/purchasing_job.py +++ b/indoteknik_custom/models/purchasing_job.py @@ -26,6 +26,7 @@ class PurchasingJob(models.Model): purchase_representative_id = fields.Many2one('res.users', string="Purchase Representative", readonly=True) note = fields.Char(string="Note Detail") date_po = fields.Datetime(string='Date PO', copy=False) + so_number = fields.Char(string='SO Number', copy=False) def unlink(self): # Example: Delete related records from the underlying model @@ -66,6 +67,7 @@ class PurchasingJob(models.Model): max(pjs.status_apo::text) AS status_apo, max(pjs.note::text) AS note, max(pjs.date_po::text) AS date_po, + pmp.so_number, CASE WHEN pmp.brand IN ('Tekiro', 'RYU', 'Rexco', 'RYU (Sparepart)') THEN 27 WHEN sub.vendor_id = 9688 THEN 397 @@ -83,7 +85,7 @@ class PurchasingJob(models.Model): group by vso.product_id ) sub ON sub.product_id = pmp.product_id WHERE pmp.action = 'kurang'::text AND sub.vendor_id IS NOT NULL - GROUP BY pmp.product_id, pmp.brand, pmp.item_code, pmp.product, pmp.action, sub.vendor_id; + GROUP BY pmp.product_id, pmp.brand, pmp.item_code, pmp.product, pmp.action, sub.vendor_id, pmp.so_number; """ % self._table) def open_form_multi_generate_request_po(self): -- cgit v1.2.3 From 0198b0a0683cc7db1241ec9b7184c2cb608ba049 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 15 Jul 2025 11:52:33 +0700 Subject: (andri) fix edit unit price PO --- indoteknik_custom/models/purchase_order.py | 9 ++++++++- 1 file changed, 8 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 4dc26d74..6901ecb6 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -668,9 +668,16 @@ class PurchaseOrder(models.Model): resposible_ids = list(set(resposible_ids)) purchase.responsible_ids = resposible_ids + # def _compute_has_active_invoice(self): + # for order in self: + # order.has_active_invoice = any(invoice.state != 'cancel' for invoice in order.invoice_ids) + def _compute_has_active_invoice(self): for order in self: - order.has_active_invoice = any(invoice.state != 'cancel' for invoice in order.invoice_ids) + related_invoices = order.invoice_ids.filtered( + lambda inv: inv.purchase_order_id.id == order.id and inv.move_type == 'in_invoice' and inv.state != 'cancel' + ) + order.has_active_invoice = bool(related_invoices) def add_product_to_pricelist(self): i = 0 -- cgit v1.2.3 From 85caa56671d90cde807c44179680ef790d1a58c5 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Tue, 15 Jul 2025 11:59:36 +0700 Subject: tukar guling po vals rev --- indoteknik_custom/models/tukar_guling_po.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index 6d7d7335..3292eb7d 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -311,7 +311,7 @@ class TukarGulingPO(models.Model): self._validate_product_lines() self._check_not_allow_tukar_guling_on_bu_input() - if self.operations and self.operations.picking_type_id.id == 28 and self.return_type == 'tukar_guling': + if self.operations.picking_type_id.id == 28: group = self.operations.group_id if group: # Cari BU/PUT dalam group yang sama @@ -325,11 +325,12 @@ class TukarGulingPO(models.Model): raise UserError("❌ Tidak bisa retur BU/INPUT karena BU/PUT sudah Done!") picking = self.operations - if picking.picking_type_id.id == 75: + pick_id = self.operations.picking_type_id.id + if pick_id == 75: if picking.state != 'done': raise UserError("BU/PUT belum Done!") - if picking.picking_type_id.id != 75 or picking.picking_type_id.id != 28: + if pick_id not in [75, 28]: raise UserError("❌ Tidak bisa retur bukan BU/INPUT atau BU/PUT!") if self._is_already_returned(self.operations): -- cgit v1.2.3 From f3fc6d3d1a4c5ac0b287e3bb7e1163b99393c728 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 15 Jul 2025 13:20:58 +0700 Subject: (andri) fix --- indoteknik_custom/models/purchase_order.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 6901ecb6..f98a37be 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -668,16 +668,16 @@ class PurchaseOrder(models.Model): resposible_ids = list(set(resposible_ids)) purchase.responsible_ids = resposible_ids - # def _compute_has_active_invoice(self): - # for order in self: - # order.has_active_invoice = any(invoice.state != 'cancel' for invoice in order.invoice_ids) - def _compute_has_active_invoice(self): for order in self: - related_invoices = order.invoice_ids.filtered( - lambda inv: inv.purchase_order_id.id == order.id and inv.move_type == 'in_invoice' and inv.state != 'cancel' - ) - order.has_active_invoice = bool(related_invoices) + order.has_active_invoice = any(invoice.state != 'cancel' for invoice in order.invoice_ids) + + # def _compute_has_active_invoice(self): + # for order in self: + # related_invoices = order.invoice_ids.filtered( + # lambda inv: inv.purchase_order_id.id == order.id and inv.move_type == 'in_invoice' and inv.state != 'cancel' + # ) + # order.has_active_invoice = bool(related_invoices) def add_product_to_pricelist(self): i = 0 -- cgit v1.2.3 From 77d3bd2541a52270f03b84e38dd691630bcd82af Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 16 Jul 2025 08:47:04 +0700 Subject: mapping koli tukar guling --- indoteknik_custom/models/stock_picking.py | 2 + indoteknik_custom/models/stock_picking_return.py | 20 ++++++ indoteknik_custom/models/tukar_guling.py | 77 +++++++++++++++++++----- indoteknik_custom/models/tukar_guling_po.py | 37 +++++------- 4 files changed, 99 insertions(+), 37 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 7d3d963a..dc5b8ebd 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -2518,6 +2518,8 @@ class KonfirmKoli(models.Model): copy=False, ) pick_id = fields.Many2one('stock.picking', string='Pick') + product_id = fields.Many2one('product.product', string='Product') + qty_done = fields.Float(string='Qty Done') @api.constrains('pick_id') def _check_duplicate_pick_id(self): diff --git a/indoteknik_custom/models/stock_picking_return.py b/indoteknik_custom/models/stock_picking_return.py index a9781d3c..fa557ce8 100644 --- a/indoteknik_custom/models/stock_picking_return.py +++ b/indoteknik_custom/models/stock_picking_return.py @@ -89,6 +89,26 @@ class StockReturnPicking(models.TransientModel): if line_vals: context['default_line_ids'] = line_vals + if picking.picking_type_id.id == 29: + mapping_koli_vals = [] + sequence = 10 + returned_product_ids = set() + + # Ambil move lines dari BU/PICK + for move_line in picking.move_line_ids_without_package: + # Cek apakah produk ini ada di daftar retur dan qty_done > 0 + if move_line.product_id.id in returned_product_ids and move_line.qty_done > 0: + mapping_koli_vals.append((0, 0, { + 'sequence': sequence, + 'pick_id': picking.id, # ID BU/PICK itu sendiri + 'product_id': move_line.product_id.id, + 'qty_done': move_line.qty_done, + })) + sequence += 10 + + if mapping_koli_vals: + context['default_mapping_koli_ids'] = mapping_koli_vals + if picking.purchase_id or 'PO' in picking.origin: _logger.info("Redirect ke Tukar Guling PO via purchase_id / origin") return { diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 7e857d02..f0fe13f6 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -55,6 +55,7 @@ class TukarGuling(models.Model): ], default='draft', tracking=True, required=True) line_ids = fields.One2many('tukar.guling.line', 'tukar_guling_id', string='Product Lines') + mapping_koli_ids = fields.One2many('tukar.guling.mapping.koli', 'tukar_guling_id', string='Mapping Koli') @api.onchange('operations') def _onchange_operations(self): @@ -71,10 +72,10 @@ class TukarGuling(models.Model): # Hanya update origin, jangan ubah lines if self.operations.origin: self.origin = self.operations.origin - return # Clear existing lines hanya jika tidak dari return picking self.line_ids = [(5, 0, 0)] + self.mapping_koli_ids = [(5, 0, 0)] # Clear existing mapping koli juga # Set origin dari operations if self.operations.origin: @@ -94,26 +95,59 @@ class TukarGuling(models.Model): elif hasattr(self.operations, 'move_lines') and self.operations.move_lines: moves_to_check = self.operations.move_lines + # Collect product data for both lines and mapping koli + product_data = {} for move in moves_to_check: - _logger.info( - f"Move: {move.name}, Product: {move.product_id.name if move.product_id else 'No Product'}, Qty: {move.product_uom_qty}, State: {move.state}") - - # Ambil semua move yang ada quantity if move.product_id and move.product_uom_qty > 0: - lines_data.append((0, 0, { - 'sequence': sequence, - 'product_id': move.product_id.id, - 'product_uom_qty': move.product_uom_qty, - 'product_uom': move.product_uom.id, - 'name': move.name or move.product_id.display_name, - })) - sequence += 10 + product_id = move.product_id.id + if product_id not in product_data: + product_data[product_id] = { + 'product': move.product_id, + 'qty': move.product_uom_qty, + 'uom': move.product_uom.id, + 'name': move.name or move.product_id.display_name + } + + # Create lines_data for product lines + for product_id, data in product_data.items(): + lines_data.append((0, 0, { + 'sequence': sequence, + 'product_id': product_id, + 'product_uom_qty': data['qty'], + 'product_uom': data['uom'], + 'name': data['name'], + })) + sequence += 10 if lines_data: self.line_ids = lines_data - _logger.info(f"Created {len(lines_data)} lines") + _logger.info(f"Created {len(lines_data)} product lines") + + # Prepare mapping koli based on picking type + mapping_koli_data = [] + sequence = 10 + + # Case 1: BU/OUT (picking_type_id.id == 29) + if self.operations.picking_type_id.id == 29: + # Ambil dari konfirm_koli_lines BU/OUT + for koli_line in self.operations.konfirm_koli_lines: + if koli_line.pick_id.move_line_ids_without_package.product_id.id in product_data: + mapping_koli_data.append((0, 0, { + 'sequence': sequence, + 'pick_id': koli_line.pick_id.move_line_ids_without_package.picking_id.id, + 'product_id': koli_line.pick_id.move_line_ids_without_package.product_id.id, + 'qty_done': koli_line.pick_id.move_line_ids_without_package.qty_done + })) + sequence += 10 + + + if mapping_koli_data: + self.mapping_koli_ids = mapping_koli_data + _logger.info(f"Created {len(mapping_koli_data)} mapping koli lines") + else: + _logger.info("No mapping koli lines created") else: - _logger.info("No lines created - no valid moves found") + _logger.info("No product lines created - no valid moves found") else: # Clear lines jika operations dikosongkan, kecuali dari return picking from_return_picking = self.env.context.get('from_return_picking', False) or \ @@ -121,6 +155,7 @@ class TukarGuling(models.Model): if not from_return_picking: self.line_ids = [(5, 0, 0)] + self.mapping_koli_ids = [(5, 0, 0)] self.origin = False @@ -470,7 +505,6 @@ class TukarGuling(models.Model): }) return_lines = [] - # 🔥 Hanya pakai qty dari tukar guling line for line in record.line_ids: move = picking.move_lines.filtered(lambda m: m.product_id == line.product_id) if move: @@ -568,3 +602,14 @@ class StockPicking(models.Model): _inherit = 'stock.picking' tukar_guling_id = fields.Many2one('tukar.guling', string='Tukar Guling Ref') + + +class TukarGulingMappingKoli(models.Model): + _name = 'tukar.guling.mapping.koli' + _description = 'Mapping Koli di Tukar Guling' + + tukar_guling_id = fields.Many2one('tukar.guling', string='Tukar Guling') + pick_id = fields.Many2one('stock.picking', string='BU PICK') + product_id = fields.Many2one('product.product', string='Product') + qty_done = fields.Float(string='Qty Done di BU PICK') + sequence = fields.Integer(string='Sequence', default=10) diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index 3292eb7d..88c4722a 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -389,7 +389,7 @@ class TukarGulingPO(models.Model): if not record.operations: raise UserError("BU Operations belum dipilih.") - created_returns = [] + created_returns = self.env['stock.picking'] group = record.operations.group_id bu_inputs = bu_puts = self.env['stock.picking'] @@ -404,17 +404,17 @@ class TukarGulingPO(models.Model): else: raise UserError("Group ID tidak ditemukan pada BU Operations.") - PARTNER_LOCATION_ID = 4 - BU_INPUT_LOCATION_ID = 58 - BU_STOCK_LOCATION_ID = 57 - def _create_return_from_picking(picking): if not picking: - return None + return self.env['stock.picking'] grup = record.operations.group_id - # Mapping lokasi sesuai picking type + # Tentukan location + PARTNER_LOCATION_ID = 4 + BU_INPUT_LOCATION_ID = 58 + BU_STOCK_LOCATION_ID = 57 + if picking.picking_type_id.id == 28: default_location_id = BU_INPUT_LOCATION_ID default_location_dest_id = PARTNER_LOCATION_ID @@ -428,7 +428,7 @@ class TukarGulingPO(models.Model): default_location_id = PARTNER_LOCATION_ID default_location_dest_id = BU_INPUT_LOCATION_ID else: - return None + return self.env['stock.picking'] return_context = dict(self.env.context) return_context.update({ @@ -444,7 +444,6 @@ class TukarGulingPO(models.Model): 'original_location_id': default_location_id }) - # Sesuai line tukar guling return_lines = [] for line in record.line_ids: move = picking.move_lines.filtered(lambda m: m.product_id == line.product_id) @@ -456,20 +455,17 @@ class TukarGulingPO(models.Model): })) else: raise UserError( - _("Tidak ditemukan move line di picking %s untuk produk %s") - % (picking.name, line.product_id.display_name) + _("Tidak ditemukan move line di picking %s untuk produk %s") % + (picking.name, line.product_id.display_name) ) if not return_lines: - return None + raise UserError(_("Tidak ada product line valid untuk retur picking %s") % picking.name) return_wizard.product_return_moves = return_lines return_vals = return_wizard.create_returns() return_picking = self.env['stock.picking'].browse(return_vals.get('res_id')) - if not return_picking: - raise UserError("Retur gagal dibuat. Hasil create_returns: %s" % str(return_vals)) - return_picking.write({ 'location_id': default_location_id, 'location_dest_id': default_location_dest_id, @@ -477,7 +473,6 @@ class TukarGulingPO(models.Model): 'tukar_guling_po_id': record.id, }) - # Paksa lokasi di move lines juga for move in return_picking.move_lines: move.write({ 'location_id': default_location_id, @@ -491,31 +486,31 @@ class TukarGulingPO(models.Model): # Kalau dari BU INPUT → hanya PRT prt = _create_return_from_picking(record.operations) if prt: - created_returns.append(prt) + created_returns |= prt else: # 1. Dari BU PUT buat VRT for bu_put in bu_puts: vrt = _create_return_from_picking(bu_put) if vrt: - created_returns.append(vrt) + created_returns |= vrt # 2. Dari BU INPUT buat PRT for bu_input in bu_inputs: prt = _create_return_from_picking(bu_input) if prt: - created_returns.append(prt) + created_returns |= prt # 3. Kalau tukar guling buat lanjut INPUT & PUT if record.return_type == 'tukar_guling': for prt in created_returns.filtered(lambda p: p.picking_type_id.id == 76): bu_input = _create_return_from_picking(prt) if bu_input: - created_returns.append(bu_input) + created_returns |= bu_input for vrt in created_returns.filtered(lambda p: p.picking_type_id.id == 77): bu_put = _create_return_from_picking(vrt) if bu_put: - created_returns.append(bu_put) + created_returns |= bu_put if not created_returns: raise UserError("Tidak ada dokumen retur yang berhasil dibuat.") -- cgit v1.2.3 From 3b1bc7642b6a5d0ac19dd74563c5b353a7f3b8ba Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 16 Jul 2025 10:27:02 +0700 Subject: vals qty mapping koli tukar guling --- indoteknik_custom/models/tukar_guling.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index f0fe13f6..333e2c8e 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -57,6 +57,26 @@ class TukarGuling(models.Model): line_ids = fields.One2many('tukar.guling.line', 'tukar_guling_id', string='Product Lines') mapping_koli_ids = fields.One2many('tukar.guling.mapping.koli', 'tukar_guling_id', string='Mapping Koli') + @api.constrains('mapping_koli_ids') + def _check_mapping_koli(self): + for record in self: + if record.operations.picking_type_id.id == 29: # Only for BU/OUT + if not record.mapping_koli_ids: + raise UserError("❌ Mapping Koli belum diisi") + + # Calculate totals as integers + total_mapping_qty = sum(int(mapping.qty_done) for mapping in record.mapping_koli_ids) + total_line_qty = sum(int(line.product_uom_qty) for line in record.line_ids) + + # Strict integer comparison + if total_mapping_qty != total_line_qty: + raise UserError( + "❌ Total quantity mapping koli (%d) tidak sama dengan quantity retur (%d)" % + (total_mapping_qty, total_line_qty) + ) + else: + _logger.info("qty koli sesuai") + @api.onchange('operations') def _onchange_operations(self): """Auto-populate lines ketika operations dipilih""" -- cgit v1.2.3 From 0e330178d087b2b4cb519c4e078f0fe25a76dfe5 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 16 Jul 2025 13:37:08 +0700 Subject: fix qty prod line and add qty return --- indoteknik_custom/models/stock_picking_return.py | 1 + indoteknik_custom/models/tukar_guling.py | 51 ++++++++++++++++-------- 2 files changed, 36 insertions(+), 16 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking_return.py b/indoteknik_custom/models/stock_picking_return.py index fa557ce8..e274a147 100644 --- a/indoteknik_custom/models/stock_picking_return.py +++ b/indoteknik_custom/models/stock_picking_return.py @@ -103,6 +103,7 @@ class StockReturnPicking(models.TransientModel): 'pick_id': picking.id, # ID BU/PICK itu sendiri 'product_id': move_line.product_id.id, 'qty_done': move_line.qty_done, + 'qty_return': move_line.qty_done, })) sequence += 10 diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 333e2c8e..a2168f5b 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -65,13 +65,13 @@ class TukarGuling(models.Model): raise UserError("❌ Mapping Koli belum diisi") # Calculate totals as integers - total_mapping_qty = sum(int(mapping.qty_done) for mapping in record.mapping_koli_ids) + total_mapping_qty = sum(int(mapping.qty_done) for mapping in record.mapping_koli_ids.qty_return) total_line_qty = sum(int(line.product_uom_qty) for line in record.line_ids) # Strict integer comparison if total_mapping_qty != total_line_qty: raise UserError( - "❌ Total quantity mapping koli (%d) tidak sama dengan quantity retur (%d)" % + "❌ Total quantity return di mapping koli (%d) tidak sama dengan quantity retur product lines (%d)" % (total_mapping_qty, total_line_qty) ) else: @@ -92,6 +92,28 @@ class TukarGuling(models.Model): # Hanya update origin, jangan ubah lines if self.operations.origin: self.origin = self.operations.origin + _logger.info("📌 Menggunakan product lines dari return wizard, tidak populate ulang.") + + # 🚀 Tapi tetap populate mapping koli jika BU/OUT + if self.operations.picking_type_id.id == 29: + mapping_koli_data = [] + sequence = 10 + tg_product_ids = self.line_ids.mapped('product_id.id') + + for koli_line in self.operations.konfirm_koli_lines: + pick_move = koli_line.pick_id.move_line_ids_without_package + if pick_move.product_id.id in tg_product_ids: + mapping_koli_data.append((0, 0, { + 'sequence': sequence, + 'pick_id': koli_line.pick_id.id, + 'product_id': pick_move.product_id.id, + 'qty_done': pick_move.qty_done + })) + sequence += 10 + + self.mapping_koli_ids = mapping_koli_data + _logger.info(f"✅ Created {len(mapping_koli_data)} mapping koli lines (from return wizard)") + return # keluar supaya tidak populate ulang lines # Clear existing lines hanya jika tidak dari return picking self.line_ids = [(5, 0, 0)] @@ -108,14 +130,12 @@ class TukarGuling(models.Model): # Untuk Odoo 14, gunakan move_ids_without_package atau move_lines moves_to_check = [] - # 1. move_ids_without_package (standard di Odoo 14) if hasattr(self.operations, 'move_ids_without_package') and self.operations.move_ids_without_package: moves_to_check = self.operations.move_ids_without_package - # 2. move_lines (backup untuk versi lama) elif hasattr(self.operations, 'move_lines') and self.operations.move_lines: moves_to_check = self.operations.move_lines - # Collect product data for both lines and mapping koli + # Collect product data product_data = {} for move in moves_to_check: if move.product_id and move.product_uom_qty > 0: @@ -128,7 +148,7 @@ class TukarGuling(models.Model): 'name': move.name or move.product_id.display_name } - # Create lines_data for product lines + # Buat lines_data for product_id, data in product_data.items(): lines_data.append((0, 0, { 'sequence': sequence, @@ -143,24 +163,23 @@ class TukarGuling(models.Model): self.line_ids = lines_data _logger.info(f"Created {len(lines_data)} product lines") - # Prepare mapping koli based on picking type + # Prepare mapping koli jika BU/OUT mapping_koli_data = [] sequence = 10 - # Case 1: BU/OUT (picking_type_id.id == 29) if self.operations.picking_type_id.id == 29: - # Ambil dari konfirm_koli_lines BU/OUT + tg_product_ids = [p for p in product_data] for koli_line in self.operations.konfirm_koli_lines: - if koli_line.pick_id.move_line_ids_without_package.product_id.id in product_data: + pick_move = koli_line.pick_id.move_line_ids_without_package + if pick_move.product_id.id in tg_product_ids: mapping_koli_data.append((0, 0, { 'sequence': sequence, - 'pick_id': koli_line.pick_id.move_line_ids_without_package.picking_id.id, - 'product_id': koli_line.pick_id.move_line_ids_without_package.product_id.id, - 'qty_done': koli_line.pick_id.move_line_ids_without_package.qty_done + 'pick_id': koli_line.pick_id.id, + 'product_id': pick_move.product_id.id, + 'qty_done': pick_move.qty_done })) sequence += 10 - if mapping_koli_data: self.mapping_koli_ids = mapping_koli_data _logger.info(f"Created {len(mapping_koli_data)} mapping koli lines") @@ -169,7 +188,6 @@ class TukarGuling(models.Model): else: _logger.info("No product lines created - no valid moves found") else: - # Clear lines jika operations dikosongkan, kecuali dari return picking from_return_picking = self.env.context.get('from_return_picking', False) or \ self.env.context.get('default_line_ids', False) @@ -631,5 +649,6 @@ class TukarGulingMappingKoli(models.Model): tukar_guling_id = fields.Many2one('tukar.guling', string='Tukar Guling') pick_id = fields.Many2one('stock.picking', string='BU PICK') product_id = fields.Many2one('product.product', string='Product') - qty_done = fields.Float(string='Qty Done di BU PICK') + qty_done = fields.Float(string='Qty Done BU PICK') + qty_return = fields.Float(string='Qty yang mau diretur') sequence = fields.Integer(string='Sequence', default=10) -- cgit v1.2.3 From efa1650aae2bc2dca99624092adcc21f87dab648 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 16 Jul 2025 17:40:22 +0700 Subject: retur blm sesuai --- indoteknik_custom/models/tukar_guling.py | 99 +++++++++++++++++--------------- 1 file changed, 54 insertions(+), 45 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index a2168f5b..2c39b547 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -57,25 +57,23 @@ class TukarGuling(models.Model): line_ids = fields.One2many('tukar.guling.line', 'tukar_guling_id', string='Product Lines') mapping_koli_ids = fields.One2many('tukar.guling.mapping.koli', 'tukar_guling_id', string='Mapping Koli') - @api.constrains('mapping_koli_ids') def _check_mapping_koli(self): for record in self: if record.operations.picking_type_id.id == 29: # Only for BU/OUT if not record.mapping_koli_ids: raise UserError("❌ Mapping Koli belum diisi") - # Calculate totals as integers - total_mapping_qty = sum(int(mapping.qty_done) for mapping in record.mapping_koli_ids.qty_return) + # Calculate totals + total_mapping_qty = sum(int(mapping.qty_return) for mapping in record.mapping_koli_ids) total_line_qty = sum(int(line.product_uom_qty) for line in record.line_ids) - # Strict integer comparison if total_mapping_qty != total_line_qty: raise UserError( "❌ Total quantity return di mapping koli (%d) tidak sama dengan quantity retur product lines (%d)" % (total_mapping_qty, total_line_qty) ) else: - _logger.info("qty koli sesuai") + _logger.info("✅ Qty mapping koli sesuai dengan product lines") @api.onchange('operations') def _onchange_operations(self): @@ -107,7 +105,8 @@ class TukarGuling(models.Model): 'sequence': sequence, 'pick_id': koli_line.pick_id.id, 'product_id': pick_move.product_id.id, - 'qty_done': pick_move.qty_done + 'qty_done': pick_move.qty_done, + 'qty_return': 0 })) sequence += 10 @@ -482,47 +481,39 @@ class TukarGuling(models.Model): if not record.operations: raise UserError("BU/OUT dari field operations tidak ditemukan.") + created_returns = [] + bu_pick_to_return = record.operations.konfirm_koli_lines.pick_id bu_out_to_return = record.operations if not bu_pick_to_return and not bu_out_to_return: raise UserError("Tidak ada BU/PICK atau BU/OUT yang selesai untuk diretur.") - created_returns = [] - - # Picking types & locations srt_type = self.env['stock.picking.type'].browse(73) ort_type = self.env['stock.picking.type'].browse(74) bu_pick_type = self.env['stock.picking.type'].browse(30) bu_out_type = self.env['stock.picking.type'].browse(29) - def _create_return_from_picking(picking): + PARTNER_LOCATION_ID = 5 + BU_OUTPUT_LOCATION_ID = 60 + BU_STOCK_LOCATION_ID = 57 + + def _create_return_from_picking_grouped(picking): if not picking: return None - grup = record.operations.group_id - PARTNER_LOCATION_ID = 5 - BU_OUTPUT_LOCATION_ID = 60 - BU_STOCK_LOCATION_ID = 57 + grup = record.operations.group_id if picking.picking_type_id.id == 30: - # BU/PICK → ORT - return_type = ort_type default_location_id = BU_OUTPUT_LOCATION_ID default_location_dest_id = BU_STOCK_LOCATION_ID elif picking.picking_type_id.id == 74: - # ORT → BU/PICK - return_type = bu_pick_type default_location_id = BU_STOCK_LOCATION_ID default_location_dest_id = BU_OUTPUT_LOCATION_ID elif picking.picking_type_id.id == 29: - # BU/OUT → SRT - return_type = srt_type default_location_id = PARTNER_LOCATION_ID default_location_dest_id = BU_OUTPUT_LOCATION_ID elif picking.picking_type_id.id == 73: - # SRT → BU/OUT - return_type = bu_out_type default_location_id = BU_OUTPUT_LOCATION_ID default_location_dest_id = PARTNER_LOCATION_ID else: @@ -542,58 +533,75 @@ class TukarGuling(models.Model): 'original_location_id': default_location_id }) + # 🔥 If BU/OUT, ambil qty dari mapping koli (group by product) return_lines = [] - for line in record.line_ids: - move = picking.move_lines.filtered(lambda m: m.product_id == line.product_id) - if move: + if picking.picking_type_id.id == 29 and record.mapping_koli_ids: + grouped_qty = {} + for map_line in record.mapping_koli_ids: + pid = map_line.product_id.id + grouped_qty[pid] = grouped_qty.get(pid, 0) + map_line.qty_return + + for pid, qty in grouped_qty.items(): + move = picking.move_lines.filtered(lambda mv: mv.product_id.id == pid) + if move: + return_lines.append((0, 0, { + 'product_id': pid, + 'quantity': qty, + 'move_id': move[0].id, + })) + else: + raise UserError(_("Tidak ditemukan move line di picking %s untuk produk %s") + % (picking.name, map_line.product_id.display_name)) + else: + # Default kalau bukan BU/OUT (misalnya ORT), retur full qty done + for move in picking.move_lines: return_lines.append((0, 0, { - 'product_id': line.product_id.id, - 'quantity': line.product_uom_qty, - 'move_id': move[0].id, + 'product_id': move.product_id.id, + 'quantity': move.quantity_done or move.product_uom_qty, + 'move_id': move.id, })) - else: - raise UserError( - _("Tidak ditemukan move line di picking %s untuk produk %s") - % (picking.name, line.product_id.display_name) - ) if not return_lines: raise UserError(_("Tidak ada product line valid untuk retur picking %s") % picking.name) return_wizard.product_return_moves = return_lines return_vals = return_wizard.create_returns() - return_id = return_vals.get('res_id') - return_picking = self.env['stock.picking'].browse(return_id) + return_picking = self.env['stock.picking'].browse(return_vals.get('res_id')) if not return_picking: raise UserError("Retur gagal dibuat. Hasil create_returns: %s" % str(return_vals)) return_picking.write({ - 'location_dest_id': default_location_dest_id, 'location_id': default_location_id, + 'location_dest_id': default_location_dest_id, 'group_id': grup.id, 'tukar_guling_id': record.id, }) + for move in return_picking.move_lines: + move.write({ + 'location_id': default_location_id, + 'location_dest_id': default_location_dest_id, + }) + return return_picking - # === PERBAIKI URUTAN === - srt = _create_return_from_picking(bu_out_to_return) + # === FLOW === + srt = _create_return_from_picking_grouped(bu_out_to_return) if srt: created_returns.append(srt) - picks = record.operations.konfirm_koli_lines.pick_id - for picking in picks: - ort = _create_return_from_picking(picking) + for picking in bu_pick_to_return: + ort = _create_return_from_picking_grouped(picking) if ort: created_returns.append(ort) if record.return_type == 'tukar_guling': - bu_pick = _create_return_from_picking(ort) + bu_pick = _create_return_from_picking_grouped(ort) if bu_pick: created_returns.append(bu_pick) if record.return_type == 'tukar_guling' and srt: - bu_out = _create_return_from_picking(srt) + bu_out = _create_return_from_picking_grouped(srt) if bu_out: created_returns.append(bu_out) @@ -601,6 +609,7 @@ class TukarGuling(models.Model): raise UserError("Tidak ada dokumen retur yang berhasil dibuat.") + class TukarGulingLine(models.Model): _name = 'tukar.guling.line' _description = 'Tukar Guling Line' @@ -650,5 +659,5 @@ class TukarGulingMappingKoli(models.Model): pick_id = fields.Many2one('stock.picking', string='BU PICK') product_id = fields.Many2one('product.product', string='Product') qty_done = fields.Float(string='Qty Done BU PICK') - qty_return = fields.Float(string='Qty yang mau diretur') - sequence = fields.Integer(string='Sequence', default=10) + qty_return = fields.Float(string='Qty diretur') + sequence = fields.Integer(string='Sequence', default=10) \ No newline at end of file -- cgit v1.2.3 From 2c5f2513e9380da52d9893c8dc5ad148c298a882 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 16 Jul 2025 21:49:53 +0700 Subject: temp --- indoteknik_custom/models/tukar_guling.py | 227 ++++++++++++++++--------------- 1 file changed, 119 insertions(+), 108 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 2c39b547..295ca5d9 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -481,13 +481,8 @@ class TukarGuling(models.Model): if not record.operations: raise UserError("BU/OUT dari field operations tidak ditemukan.") - created_returns = [] - - bu_pick_to_return = record.operations.konfirm_koli_lines.pick_id - bu_out_to_return = record.operations - - if not bu_pick_to_return and not bu_out_to_return: - raise UserError("Tidak ada BU/PICK atau BU/OUT yang selesai untuk diretur.") + bu_out = record.operations + mapping_koli = record.mapping_koli_ids srt_type = self.env['stock.picking.type'].browse(73) ort_type = self.env['stock.picking.type'].browse(74) @@ -498,116 +493,132 @@ class TukarGuling(models.Model): BU_OUTPUT_LOCATION_ID = 60 BU_STOCK_LOCATION_ID = 57 - def _create_return_from_picking_grouped(picking): - if not picking: - return None - - grup = record.operations.group_id - - if picking.picking_type_id.id == 30: - default_location_id = BU_OUTPUT_LOCATION_ID - default_location_dest_id = BU_STOCK_LOCATION_ID - elif picking.picking_type_id.id == 74: - default_location_id = BU_STOCK_LOCATION_ID - default_location_dest_id = BU_OUTPUT_LOCATION_ID - elif picking.picking_type_id.id == 29: - default_location_id = PARTNER_LOCATION_ID - default_location_dest_id = BU_OUTPUT_LOCATION_ID - elif picking.picking_type_id.id == 73: - default_location_id = BU_OUTPUT_LOCATION_ID - default_location_dest_id = PARTNER_LOCATION_ID - else: - return None + created_returns = [] - return_context = dict(self.env.context) - return_context.update({ - 'active_id': picking.id, - 'default_location_id': default_location_id, - 'default_location_dest_id': default_location_dest_id, - 'from_ui': False, - }) + ### ============= SRT dari BU/OUT ================== + srt_return_lines = [] + for prod in mapping_koli.mapped('product_id'): + qty_total_return = sum(mk.qty_return for mk in mapping_koli.filtered(lambda m: m.product_id == prod)) + move = bu_out.move_lines.filtered(lambda m: m.product_id == prod) + if not move: + raise UserError(f"Tidak ditemukan move BU/OUT untuk product {prod.display_name}") + srt_return_lines.append((0, 0, { + 'product_id': prod.id, + 'quantity': qty_total_return, + 'move_id': move[0].id, + })) - return_wizard = self.env['stock.return.picking'].with_context(return_context).create({ - 'picking_id': picking.id, - 'location_id': default_location_dest_id, - 'original_location_id': default_location_id + srt_picking = None + if srt_return_lines: + srt_context = { + 'active_id': bu_out.id, + 'default_location_id': PARTNER_LOCATION_ID, + 'default_location_dest_id': BU_OUTPUT_LOCATION_ID, + 'from_ui': False, + } + srt_wizard = self.env['stock.return.picking'].with_context(srt_context).create({ + 'picking_id': bu_out.id, + 'location_id': BU_OUTPUT_LOCATION_ID, + 'original_location_id': PARTNER_LOCATION_ID, + 'product_return_moves': srt_return_lines }) - - # 🔥 If BU/OUT, ambil qty dari mapping koli (group by product) - return_lines = [] - if picking.picking_type_id.id == 29 and record.mapping_koli_ids: - grouped_qty = {} - for map_line in record.mapping_koli_ids: - pid = map_line.product_id.id - grouped_qty[pid] = grouped_qty.get(pid, 0) + map_line.qty_return - - for pid, qty in grouped_qty.items(): - move = picking.move_lines.filtered(lambda mv: mv.product_id.id == pid) - if move: - return_lines.append((0, 0, { - 'product_id': pid, - 'quantity': qty, - 'move_id': move[0].id, - })) - else: - raise UserError(_("Tidak ditemukan move line di picking %s untuk produk %s") - % (picking.name, map_line.product_id.display_name)) - else: - # Default kalau bukan BU/OUT (misalnya ORT), retur full qty done - for move in picking.move_lines: - return_lines.append((0, 0, { - 'product_id': move.product_id.id, - 'quantity': move.quantity_done or move.product_uom_qty, - 'move_id': move.id, - })) - - if not return_lines: - raise UserError(_("Tidak ada product line valid untuk retur picking %s") % picking.name) - - return_wizard.product_return_moves = return_lines - return_vals = return_wizard.create_returns() - return_picking = self.env['stock.picking'].browse(return_vals.get('res_id')) - - if not return_picking: - raise UserError("Retur gagal dibuat. Hasil create_returns: %s" % str(return_vals)) - - return_picking.write({ - 'location_id': default_location_id, - 'location_dest_id': default_location_dest_id, - 'group_id': grup.id, + srt_vals = srt_wizard.create_returns() + srt_picking = self.env['stock.picking'].browse(srt_vals.get('res_id')) + srt_picking.write({ + 'location_id': PARTNER_LOCATION_ID, + 'location_dest_id': BU_OUTPUT_LOCATION_ID, + 'group_id': bu_out.group_id.id, 'tukar_guling_id': record.id, }) - - for move in return_picking.move_lines: - move.write({ - 'location_id': default_location_id, - 'location_dest_id': default_location_dest_id, + created_returns.append(srt_picking) + + ### ============= ORT dari BU/PICK ================== + ort_pickings = [] + for pick in mapping_koli.mapped('pick_id'): + ort_return_lines = [] + pick_lines = mapping_koli.filtered(lambda m: m.pick_id == pick) + for mk in pick_lines: + move = pick.move_lines.filtered(lambda m: m.product_id == mk.product_id) + if not move: + raise UserError( + f"Tidak ditemukan move di BU/PICK {pick.name} untuk {mk.product_id.display_name}") + ort_return_lines.append((0, 0, { + 'product_id': mk.product_id.id, + 'quantity': mk.qty_return, + 'move_id': move[0].id + })) + if ort_return_lines: + ort_context = { + 'active_id': pick.id, + 'default_location_id': BU_STOCK_LOCATION_ID, + 'default_location_dest_id': BU_OUTPUT_LOCATION_ID, + 'from_ui': False, + } + ort_wizard = self.env['stock.return.picking'].with_context(ort_context).create({ + 'picking_id': pick.id, + 'location_id': BU_OUTPUT_LOCATION_ID, + 'original_location_id': BU_STOCK_LOCATION_ID, + 'product_return_moves': ort_return_lines }) - - return return_picking - - # === FLOW === - srt = _create_return_from_picking_grouped(bu_out_to_return) - if srt: - created_returns.append(srt) - - for picking in bu_pick_to_return: - ort = _create_return_from_picking_grouped(picking) - if ort: - created_returns.append(ort) - if record.return_type == 'tukar_guling': - bu_pick = _create_return_from_picking_grouped(ort) - if bu_pick: - created_returns.append(bu_pick) - - if record.return_type == 'tukar_guling' and srt: - bu_out = _create_return_from_picking_grouped(srt) - if bu_out: - created_returns.append(bu_out) + ort_vals = ort_wizard.create_returns() + ort_picking = self.env['stock.picking'].browse(ort_vals.get('res_id')) + ort_picking.write({ + 'location_id': BU_STOCK_LOCATION_ID, + 'location_dest_id': BU_OUTPUT_LOCATION_ID, + 'group_id': bu_out.group_id.id, + 'tukar_guling_id': record.id, + }) + ort_pickings.append(ort_picking) + created_returns.append(ort_picking) + + ### ============= BU/PICK & BU/OUT baru (tukar guling) ============== + if record.return_type == 'tukar_guling': + # Dari SRT → BU/OUT baru + if srt_picking: + bu_out_new = self.env['stock.return.picking'].with_context({ + 'active_id': srt_picking.id, + 'default_location_id': BU_OUTPUT_LOCATION_ID, + 'default_location_dest_id': PARTNER_LOCATION_ID, + 'from_ui': False, + }).create({ + 'picking_id': srt_picking.id, + 'location_id': PARTNER_LOCATION_ID, + 'original_location_id': BU_OUTPUT_LOCATION_ID + }).create_returns() + new_out = self.env['stock.picking'].browse(bu_out_new.get('res_id')) + new_out.write({ + 'location_id': BU_OUTPUT_LOCATION_ID, + 'location_dest_id': PARTNER_LOCATION_ID, + 'group_id': bu_out.group_id.id, + 'tukar_guling_id': record.id, + }) + created_returns.append(new_out) + + # Dari ORT → BU/PICK baru + for ort_p in ort_pickings: + bu_pick_new = self.env['stock.return.picking'].with_context({ + 'active_id': ort_p.id, + 'default_location_id': BU_OUTPUT_LOCATION_ID, + 'default_location_dest_id': BU_STOCK_LOCATION_ID, + 'from_ui': False, + }).create({ + 'picking_id': ort_p.id, + 'location_id': BU_STOCK_LOCATION_ID, + 'original_location_id': BU_OUTPUT_LOCATION_ID + }).create_returns() + new_pick = self.env['stock.picking'].browse(bu_pick_new.get('res_id')) + new_pick.write({ + 'location_id': BU_OUTPUT_LOCATION_ID, + 'location_dest_id': BU_STOCK_LOCATION_ID, + 'group_id': bu_out.group_id.id, + 'tukar_guling_id': record.id, + }) + created_returns.append(new_pick) if not created_returns: - raise UserError("Tidak ada dokumen retur yang berhasil dibuat.") + raise UserError("Tidak ada dokumen retur berhasil dibuat.") + _logger.info("✅ Created %s returns: %s", len(created_returns), ", ".join([p.name for p in created_returns])) class TukarGulingLine(models.Model): -- cgit v1.2.3 From 3c7d7783aa15fee124d94839822614de4049b9c1 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 17 Jul 2025 13:17:15 +0700 Subject: Done Tukar Guling SO --- indoteknik_custom/models/tukar_guling.py | 137 ++++++++++++++++++++----------- 1 file changed, 91 insertions(+), 46 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 295ca5d9..ded4e2a3 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -477,6 +477,7 @@ class TukarGuling(models.Model): self.state = 'cancel' def _create_pickings(self): + _logger.info("🛠 Starting _create_pickings()") for record in self: if not record.operations: raise UserError("BU/OUT dari field operations tidak ditemukan.") @@ -484,141 +485,185 @@ class TukarGuling(models.Model): bu_out = record.operations mapping_koli = record.mapping_koli_ids + # Constants + PARTNER_LOCATION_ID = 5 + BU_OUTPUT_LOCATION_ID = 60 + BU_STOCK_LOCATION_ID = 57 + + # Picking Types srt_type = self.env['stock.picking.type'].browse(73) ort_type = self.env['stock.picking.type'].browse(74) bu_pick_type = self.env['stock.picking.type'].browse(30) bu_out_type = self.env['stock.picking.type'].browse(29) - PARTNER_LOCATION_ID = 5 - BU_OUTPUT_LOCATION_ID = 60 - BU_STOCK_LOCATION_ID = 57 - created_returns = [] - ### ============= SRT dari BU/OUT ================== + ### ======== SRT dari BU/OUT ========= srt_return_lines = [] for prod in mapping_koli.mapped('product_id'): - qty_total_return = sum(mk.qty_return for mk in mapping_koli.filtered(lambda m: m.product_id == prod)) + qty_total = sum(mk.qty_return for mk in mapping_koli.filtered(lambda m: m.product_id == prod)) move = bu_out.move_lines.filtered(lambda m: m.product_id == prod) if not move: - raise UserError(f"Tidak ditemukan move BU/OUT untuk product {prod.display_name}") + raise UserError(f"Move BU/OUT tidak ditemukan untuk produk {prod.display_name}") srt_return_lines.append((0, 0, { 'product_id': prod.id, - 'quantity': qty_total_return, - 'move_id': move[0].id, + 'quantity': qty_total, + 'move_id': move.id, })) + _logger.info(f"📟 SRT line: {prod.display_name} | qty={qty_total}") srt_picking = None if srt_return_lines: - srt_context = { + srt_wizard = self.env['stock.return.picking'].with_context({ 'active_id': bu_out.id, 'default_location_id': PARTNER_LOCATION_ID, 'default_location_dest_id': BU_OUTPUT_LOCATION_ID, 'from_ui': False, - } - srt_wizard = self.env['stock.return.picking'].with_context(srt_context).create({ + }).create({ 'picking_id': bu_out.id, - 'location_id': BU_OUTPUT_LOCATION_ID, - 'original_location_id': PARTNER_LOCATION_ID, + 'location_id': PARTNER_LOCATION_ID, + 'original_location_id': BU_OUTPUT_LOCATION_ID, 'product_return_moves': srt_return_lines }) srt_vals = srt_wizard.create_returns() - srt_picking = self.env['stock.picking'].browse(srt_vals.get('res_id')) + srt_picking = self.env['stock.picking'].browse(srt_vals['res_id']) srt_picking.write({ 'location_id': PARTNER_LOCATION_ID, 'location_dest_id': BU_OUTPUT_LOCATION_ID, 'group_id': bu_out.group_id.id, 'tukar_guling_id': record.id, + 'sale_order': record.origin }) created_returns.append(srt_picking) + _logger.info(f"✅ SRT created: {srt_picking.name}") - ### ============= ORT dari BU/PICK ================== + ### ======== ORT dari BU/PICK ========= ort_pickings = [] for pick in mapping_koli.mapped('pick_id'): ort_return_lines = [] - pick_lines = mapping_koli.filtered(lambda m: m.pick_id == pick) - for mk in pick_lines: + for mk in mapping_koli.filtered(lambda m: m.pick_id == pick): move = pick.move_lines.filtered(lambda m: m.product_id == mk.product_id) if not move: raise UserError( - f"Tidak ditemukan move di BU/PICK {pick.name} untuk {mk.product_id.display_name}") + f"Move tidak ditemukan di BU/PICK {pick.name} untuk {mk.product_id.display_name}") ort_return_lines.append((0, 0, { 'product_id': mk.product_id.id, 'quantity': mk.qty_return, - 'move_id': move[0].id + 'move_id': move.id, })) + _logger.info(f"📟 ORT line: {pick.name} | {mk.product_id.display_name} | qty={mk.qty_return}") + if ort_return_lines: - ort_context = { + ort_wizard = self.env['stock.return.picking'].with_context({ 'active_id': pick.id, - 'default_location_id': BU_STOCK_LOCATION_ID, - 'default_location_dest_id': BU_OUTPUT_LOCATION_ID, + 'default_location_id': BU_OUTPUT_LOCATION_ID, + 'default_location_dest_id': BU_STOCK_LOCATION_ID, 'from_ui': False, - } - ort_wizard = self.env['stock.return.picking'].with_context(ort_context).create({ + }).create({ 'picking_id': pick.id, 'location_id': BU_OUTPUT_LOCATION_ID, 'original_location_id': BU_STOCK_LOCATION_ID, 'product_return_moves': ort_return_lines }) ort_vals = ort_wizard.create_returns() - ort_picking = self.env['stock.picking'].browse(ort_vals.get('res_id')) + ort_picking = self.env['stock.picking'].browse(ort_vals['res_id']) ort_picking.write({ - 'location_id': BU_STOCK_LOCATION_ID, - 'location_dest_id': BU_OUTPUT_LOCATION_ID, + 'location_id': BU_OUTPUT_LOCATION_ID, + 'location_dest_id': BU_STOCK_LOCATION_ID, 'group_id': bu_out.group_id.id, 'tukar_guling_id': record.id, + 'sale_order': record.origin }) - ort_pickings.append(ort_picking) created_returns.append(ort_picking) + ort_pickings.append(ort_picking) + _logger.info(f"✅ ORT created: {ort_picking.name}") - ### ============= BU/PICK & BU/OUT baru (tukar guling) ============== + ### ======== Tukar Guling: BU/OUT dan BU/PICK baru ======== if record.return_type == 'tukar_guling': - # Dari SRT → BU/OUT baru + # BU/OUT Baru dari SRT if srt_picking: - bu_out_new = self.env['stock.return.picking'].with_context({ + return_lines = [] + for move in srt_picking.move_lines: + if move.product_uom_qty > 0: + return_lines.append((0, 0, { + 'product_id': move.product_id.id, + 'quantity': move.product_uom_qty, + 'move_id': move.id, + })) + _logger.info( + f"🔁 BU/OUT baru dari SRT | {move.product_id.display_name} | qty={move.product_uom_qty}") + + bu_out_wizard = self.env['stock.return.picking'].with_context({ 'active_id': srt_picking.id, 'default_location_id': BU_OUTPUT_LOCATION_ID, 'default_location_dest_id': PARTNER_LOCATION_ID, 'from_ui': False, }).create({ 'picking_id': srt_picking.id, - 'location_id': PARTNER_LOCATION_ID, - 'original_location_id': BU_OUTPUT_LOCATION_ID - }).create_returns() - new_out = self.env['stock.picking'].browse(bu_out_new.get('res_id')) + 'location_id': BU_OUTPUT_LOCATION_ID, + 'original_location_id': PARTNER_LOCATION_ID, + 'product_return_moves': return_lines + }) + bu_out_vals = bu_out_wizard.create_returns() + new_out = self.env['stock.picking'].browse(bu_out_vals['res_id']) new_out.write({ 'location_id': BU_OUTPUT_LOCATION_ID, 'location_dest_id': PARTNER_LOCATION_ID, 'group_id': bu_out.group_id.id, 'tukar_guling_id': record.id, + 'sale_order': record.origin }) created_returns.append(new_out) + _logger.info(f"✅ BU/OUT Baru dari SRT created: {new_out.name}") - # Dari ORT → BU/PICK baru + # BU/PICK Baru dari ORT for ort_p in ort_pickings: - bu_pick_new = self.env['stock.return.picking'].with_context({ + return_lines = [] + for move in ort_p.move_lines: + if move.product_uom_qty > 0: + return_lines.append((0, 0, { + 'product_id': move.product_id.id, + 'quantity': move.product_uom_qty, + 'move_id': move.id + })) + _logger.info( + f"🔁 BU/PICK baru dari ORT {ort_p.name} | {move.product_id.display_name} | qty={move.product_uom_qty}") + + if not return_lines: + _logger.warning(f"❌ Tidak ada qty > 0 di ORT {ort_p.name}, dilewati.") + continue + + bu_pick_wizard = self.env['stock.return.picking'].with_context({ 'active_id': ort_p.id, - 'default_location_id': BU_OUTPUT_LOCATION_ID, - 'default_location_dest_id': BU_STOCK_LOCATION_ID, + 'default_location_id': BU_STOCK_LOCATION_ID, + 'default_location_dest_id': BU_OUTPUT_LOCATION_ID, 'from_ui': False, }).create({ 'picking_id': ort_p.id, 'location_id': BU_STOCK_LOCATION_ID, - 'original_location_id': BU_OUTPUT_LOCATION_ID - }).create_returns() - new_pick = self.env['stock.picking'].browse(bu_pick_new.get('res_id')) + 'original_location_id': BU_OUTPUT_LOCATION_ID, + 'product_return_moves': return_lines + }) + bu_pick_vals = bu_pick_wizard.create_returns() + new_pick = self.env['stock.picking'].browse(bu_pick_vals['res_id']) new_pick.write({ - 'location_id': BU_OUTPUT_LOCATION_ID, - 'location_dest_id': BU_STOCK_LOCATION_ID, + 'location_id': BU_STOCK_LOCATION_ID, + 'location_dest_id': BU_OUTPUT_LOCATION_ID, 'group_id': bu_out.group_id.id, 'tukar_guling_id': record.id, + 'sale_order': record.origin }) + new_pick.action_assign() # Penting agar bisa trigger check koli + new_pick.action_confirm() created_returns.append(new_pick) + _logger.info(f"✅ BU/PICK Baru dari ORT created: {new_pick.name}") if not created_returns: raise UserError("Tidak ada dokumen retur berhasil dibuat.") - _logger.info("✅ Created %s returns: %s", len(created_returns), ", ".join([p.name for p in created_returns])) + _logger.info("✅ Finished _create_pickings(). Created %s returns: %s", + len(created_returns), + ", ".join([p.name for p in created_returns])) class TukarGulingLine(models.Model): -- cgit v1.2.3 From c18b38bc03d9c260532f4a8e956b51421283fa73 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 17 Jul 2025 13:42:53 +0700 Subject: FIx bug Tukar Guling SO --- indoteknik_custom/models/tukar_guling.py | 71 ++++++++++++++++---------------- 1 file changed, 36 insertions(+), 35 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index ded4e2a3..20d43e4c 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -580,41 +580,6 @@ class TukarGuling(models.Model): ### ======== Tukar Guling: BU/OUT dan BU/PICK baru ======== if record.return_type == 'tukar_guling': - # BU/OUT Baru dari SRT - if srt_picking: - return_lines = [] - for move in srt_picking.move_lines: - if move.product_uom_qty > 0: - return_lines.append((0, 0, { - 'product_id': move.product_id.id, - 'quantity': move.product_uom_qty, - 'move_id': move.id, - })) - _logger.info( - f"🔁 BU/OUT baru dari SRT | {move.product_id.display_name} | qty={move.product_uom_qty}") - - bu_out_wizard = self.env['stock.return.picking'].with_context({ - 'active_id': srt_picking.id, - 'default_location_id': BU_OUTPUT_LOCATION_ID, - 'default_location_dest_id': PARTNER_LOCATION_ID, - 'from_ui': False, - }).create({ - 'picking_id': srt_picking.id, - 'location_id': BU_OUTPUT_LOCATION_ID, - 'original_location_id': PARTNER_LOCATION_ID, - 'product_return_moves': return_lines - }) - bu_out_vals = bu_out_wizard.create_returns() - new_out = self.env['stock.picking'].browse(bu_out_vals['res_id']) - new_out.write({ - 'location_id': BU_OUTPUT_LOCATION_ID, - 'location_dest_id': PARTNER_LOCATION_ID, - 'group_id': bu_out.group_id.id, - 'tukar_guling_id': record.id, - 'sale_order': record.origin - }) - created_returns.append(new_out) - _logger.info(f"✅ BU/OUT Baru dari SRT created: {new_out.name}") # BU/PICK Baru dari ORT for ort_p in ort_pickings: @@ -658,6 +623,42 @@ class TukarGuling(models.Model): created_returns.append(new_pick) _logger.info(f"✅ BU/PICK Baru dari ORT created: {new_pick.name}") + # BU/OUT Baru dari SRT + if srt_picking: + return_lines = [] + for move in srt_picking.move_lines: + if move.product_uom_qty > 0: + return_lines.append((0, 0, { + 'product_id': move.product_id.id, + 'quantity': move.product_uom_qty, + 'move_id': move.id, + })) + _logger.info( + f"🔁 BU/OUT baru dari SRT | {move.product_id.display_name} | qty={move.product_uom_qty}") + + bu_out_wizard = self.env['stock.return.picking'].with_context({ + 'active_id': srt_picking.id, + 'default_location_id': BU_OUTPUT_LOCATION_ID, + 'default_location_dest_id': PARTNER_LOCATION_ID, + 'from_ui': False, + }).create({ + 'picking_id': srt_picking.id, + 'location_id': BU_OUTPUT_LOCATION_ID, + 'original_location_id': PARTNER_LOCATION_ID, + 'product_return_moves': return_lines + }) + bu_out_vals = bu_out_wizard.create_returns() + new_out = self.env['stock.picking'].browse(bu_out_vals['res_id']) + new_out.write({ + 'location_id': BU_OUTPUT_LOCATION_ID, + 'location_dest_id': PARTNER_LOCATION_ID, + 'group_id': bu_out.group_id.id, + 'tukar_guling_id': record.id, + 'sale_order': record.origin + }) + created_returns.append(new_out) + _logger.info(f"✅ BU/OUT Baru dari SRT created: {new_out.name}") + if not created_returns: raise UserError("Tidak ada dokumen retur berhasil dibuat.") -- cgit v1.2.3 From 2b55913a52b05f3f62786d7ae56070e96878178c Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 17 Jul 2025 19:15:42 +0700 Subject: fix cannot retur bu pick tukar guling so --- indoteknik_custom/models/tukar_guling.py | 42 +++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 12 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 20d43e4c..d8e30006 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -539,19 +539,37 @@ class TukarGuling(models.Model): ### ======== ORT dari BU/PICK ========= ort_pickings = [] - for pick in mapping_koli.mapped('pick_id'): + is_retur_from_bu_pick = record.operations.picking_type_id.id == 30 + picks_to_return = [record.operations] if is_retur_from_bu_pick else mapping_koli.mapped('pick_id') or line.product_uom_qty + + for pick in picks_to_return: ort_return_lines = [] - for mk in mapping_koli.filtered(lambda m: m.pick_id == pick): - move = pick.move_lines.filtered(lambda m: m.product_id == mk.product_id) - if not move: - raise UserError( - f"Move tidak ditemukan di BU/PICK {pick.name} untuk {mk.product_id.display_name}") - ort_return_lines.append((0, 0, { - 'product_id': mk.product_id.id, - 'quantity': mk.qty_return, - 'move_id': move.id, - })) - _logger.info(f"📟 ORT line: {pick.name} | {mk.product_id.display_name} | qty={mk.qty_return}") + if is_retur_from_bu_pick: + # Ambil dari tukar.guling.line + for line in record.line_ids: + move = pick.move_lines.filtered(lambda m: m.product_id == line.product_id) + if not move: + raise UserError( + f"Move tidak ditemukan di BU/PICK {pick.name} untuk {line.product_id.display_name}") + ort_return_lines.append((0, 0, { + 'product_id': line.product_id.id, + 'quantity': line.product_uom_qty, + 'move_id': move.id, + })) + _logger.info(f"📟 ORT (BU/PICK langsung) | {pick.name} | {line.product_id.display_name} | qty={line.product_uom_qty}") + else: + # Ambil dari mapping koli + for mk in mapping_koli.filtered(lambda m: m.pick_id == pick): + move = pick.move_lines.filtered(lambda m: m.product_id == mk.product_id) + if not move: + raise UserError( + f"Move tidak ditemukan di BU/PICK {pick.name} untuk {mk.product_id.display_name}") + ort_return_lines.append((0, 0, { + 'product_id': mk.product_id.id, + 'quantity': mk.qty_return, + 'move_id': move.id, + })) + _logger.info(f"📟 ORT (mapping koli) | {pick.name} | {mk.product_id.display_name} | qty={mk.qty_return}") if ort_return_lines: ort_wizard = self.env['stock.return.picking'].with_context({ -- cgit v1.2.3 From 82c1a95f447d191018bed2f3a3c93831f6d398cc Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 18 Jul 2025 00:27:15 +0700 Subject: try po --- indoteknik_custom/models/tukar_guling_po.py | 45 +++++++++++++++++++---------- 1 file changed, 30 insertions(+), 15 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index 88c4722a..997e1963 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -93,8 +93,27 @@ class TukarGulingPO(models.Model): self.origin = self.operations.origin return - # Clear existing lines hanya jika tidak dari return picking - self.line_ids = [(5, 0, 0)] + if from_return_picking: + # Gunakan qty dari context (stock return wizard) + default_lines = self.env.context.get('default_line_ids', []) + parsed_lines = [] + sequence = 10 + for line_data in default_lines: + if isinstance(line_data, (list, tuple)) and len(line_data) == 3: + vals = line_data[2] + parsed_lines.append((0, 0, { + 'sequence': sequence, + 'product_id': vals.get('product_id'), + 'product_uom_qty': vals.get('quantity'), + 'product_uom': self.env['product.product'].browse(vals.get('product_id')).uom_id.id, + 'name': self.env['product.product'].browse(vals.get('product_id')).display_name, + })) + sequence += 10 + + self.line_ids = parsed_lines + return + else: + self.line_ids = [(5, 0, 0)] # Set origin dari operations if self.operations.origin: @@ -445,19 +464,15 @@ class TukarGulingPO(models.Model): }) return_lines = [] - for line in record.line_ids: - move = picking.move_lines.filtered(lambda m: m.product_id == line.product_id) - if move: - return_lines.append((0, 0, { - 'product_id': line.product_id.id, - 'quantity': line.product_uom_qty, - 'move_id': move[0].id, - })) - else: - raise UserError( - _("Tidak ditemukan move line di picking %s untuk produk %s") % - (picking.name, line.product_id.display_name) - ) + + for move in picking.move_lines: + line = record.line_ids.filtered(lambda l: l.product_id == move.product_id) + qty = line.product_uom_qty if line else 0.0 + return_lines.append((0, 0, { + 'product_id': move.product_id.id, + 'quantity': qty, + 'move_id': move.id, + })) if not return_lines: raise UserError(_("Tidak ada product line valid untuk retur picking %s") % picking.name) -- cgit v1.2.3 From c6f3edf6eaf705511b926b961b7ae4fcf017e17f Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 18 Jul 2025 14:05:48 +0700 Subject: don --- indoteknik_custom/models/stock_picking_return.py | 4 +- indoteknik_custom/models/tukar_guling_po.py | 116 ++++++++++++++++------- 2 files changed, 84 insertions(+), 36 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking_return.py b/indoteknik_custom/models/stock_picking_return.py index e274a147..1fc8d088 100644 --- a/indoteknik_custom/models/stock_picking_return.py +++ b/indoteknik_custom/models/stock_picking_return.py @@ -5,7 +5,7 @@ import logging _logger = logging.getLogger(__name__) -class StockReturnPicking(models.TransientModel): +class ReturnPicking(models.TransientModel): _inherit = 'stock.return.picking' # return_type = fields.Selection([ @@ -26,7 +26,7 @@ class StockReturnPicking(models.TransientModel): if self._context.get('from_ui', True): return self._redirect_to_tukar_guling() - return super(StockReturnPicking, self).create_returns() + return super(ReturnPicking, self).create_returns() def _redirect_to_tukar_guling(self): """Redirect ke Tukar Guling SO atau PO form dengan pre-filled data""" diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index 997e1963..72417a72 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -238,7 +238,8 @@ class TukarGulingPO(models.Model): def _is_already_returned(self, picking): return self.env['stock.picking'].search_count([ ('origin', '=', 'Return of %s' % picking.name), - ('state', '!=', 'cancel') + # ('returned_from_id', '=', picking.id), + ('state', 'not in', ['cancel', 'draft']), ]) > 0 def copy(self, default=None): @@ -286,9 +287,9 @@ class TukarGulingPO(models.Model): if self.operations.picking_type_id.id == 28 and tipe == 'tukar_guling': raise UserError("❌ BU/INPUT tidak boleh di retur tukar guling") - if self.operations.picking_type_id.id != 28: - if self._is_already_returned(self.operations): - raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") + # if self.operations.picking_type_id.id != 28: + # if self._is_already_returned(self.operations): + # raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") if 'operations' in vals and not vals.get('origin'): picking = self.env['stock.picking'].browse(vals['operations']) if picking.origin: @@ -413,6 +414,14 @@ class TukarGulingPO(models.Model): group = record.operations.group_id bu_inputs = bu_puts = self.env['stock.picking'] + # Buat qty map awal dari line_ids + bu_input_qty_map = { + line.product_id.id: line.product_uom_qty + for line in record.line_ids + if line.product_id and line.product_uom_qty > 0 + } + bu_put_qty_map = bu_input_qty_map.copy() + if group: po_pickings = self.env['stock.picking'].search([ ('group_id', '=', group.id), @@ -423,27 +432,28 @@ class TukarGulingPO(models.Model): else: raise UserError("Group ID tidak ditemukan pada BU Operations.") - def _create_return_from_picking(picking): + def _create_return_from_picking(picking, qty_map): if not picking: return self.env['stock.picking'] grup = record.operations.group_id - # Tentukan location + # Tentukan lokasi PARTNER_LOCATION_ID = 4 BU_INPUT_LOCATION_ID = 58 BU_STOCK_LOCATION_ID = 57 - if picking.picking_type_id.id == 28: + picking_type = picking.picking_type_id.id + if picking_type == 28: default_location_id = BU_INPUT_LOCATION_ID default_location_dest_id = PARTNER_LOCATION_ID - elif picking.picking_type_id.id == 75: + elif picking_type == 75: default_location_id = BU_STOCK_LOCATION_ID default_location_dest_id = BU_INPUT_LOCATION_ID - elif picking.picking_type_id.id == 77: + elif picking_type == 77: default_location_id = BU_INPUT_LOCATION_ID default_location_dest_id = BU_STOCK_LOCATION_ID - elif picking.picking_type_id.id == 76: + elif picking_type == 76: default_location_id = PARTNER_LOCATION_ID default_location_dest_id = BU_INPUT_LOCATION_ID else: @@ -464,18 +474,46 @@ class TukarGulingPO(models.Model): }) return_lines = [] + moves = getattr(picking, 'move_ids_without_package', False) or picking.move_lines + + for move in moves: + product = move.product_id + if not product: + continue + + pid = product.id + available_qty = qty_map.get(pid, 0.0) + move_qty = move.product_uom_qty + allocate_qty = min(available_qty, move_qty) + + if allocate_qty <= 0: + continue - for move in picking.move_lines: - line = record.line_ids.filtered(lambda l: l.product_id == move.product_id) - qty = line.product_uom_qty if line else 0.0 return_lines.append((0, 0, { - 'product_id': move.product_id.id, - 'quantity': qty, + 'product_id': pid, + 'quantity': allocate_qty, 'move_id': move.id, })) + qty_map[pid] -= allocate_qty + + _logger.info(f"📦 Alokasi {allocate_qty} untuk {product.display_name} | Sisa: {qty_map[pid]}") if not return_lines: - raise UserError(_("Tidak ada product line valid untuk retur picking %s") % picking.name) + # Tukar Guling lanjut dari PRT/VRT + if picking.picking_type_id.id in [76, 77]: + for move in moves: + if move.product_uom_qty > 0: + return_lines.append((0, 0, { + 'product_id': move.product_id.id, + 'quantity': move.product_uom_qty, + 'move_id': move.id, + })) + _logger.info( + f"🔁 TG lanjutan: Alokasi {move.product_uom_qty} untuk {move.product_id.display_name}") + else: + _logger.warning( + f"⏭️ Skipped return picking {picking.name}, tidak ada qty yang bisa dialokasikan.") + return self.env['stock.picking'] return_wizard.product_return_moves = return_lines return_vals = return_wizard.create_returns() @@ -488,42 +526,52 @@ class TukarGulingPO(models.Model): 'tukar_guling_po_id': record.id, }) - for move in return_picking.move_lines: - move.write({ - 'location_id': default_location_id, - 'location_dest_id': default_location_dest_id, - }) - return return_picking - # === Eksekusi pembuatan picking === + # ============================ + # Eksekusi utama return logic + # ============================ + if record.operations.picking_type_id.id == 28: - # Kalau dari BU INPUT → hanya PRT - prt = _create_return_from_picking(record.operations) + # Dari BU INPUT langsung buat PRT + prt = _create_return_from_picking(record.operations, bu_input_qty_map) if prt: created_returns |= prt else: - # 1. Dari BU PUT buat VRT - for bu_put in bu_puts: - vrt = _create_return_from_picking(bu_put) + # ✅ Pairing BU PUT ↔ BU INPUT + # Temukan index dari BU PUT yang dipilih user + try: + bu_put_index = sorted(bu_puts, key=lambda p: p.name).index(record.operations) + except ValueError: + raise UserError("Dokumen BU PUT yang dipilih tidak ditemukan dalam daftar BU PUT.") + + # Ambil pasangannya di BU INPUT (asumsi urutan sejajar) + sorted_bu_puts = sorted(bu_puts, key=lambda p: p.name) + sorted_bu_inputs = sorted(bu_inputs, key=lambda p: p.name) + + if bu_put_index >= len(sorted_bu_inputs): + raise UserError("Tidak ditemukan pasangan BU INPUT untuk BU PUT yang dipilih.") + + paired = [(sorted_bu_puts[bu_put_index], sorted_bu_inputs[bu_put_index])] + + for bu_put, bu_input in paired: + vrt = _create_return_from_picking(bu_put, bu_put_qty_map) if vrt: created_returns |= vrt - # 2. Dari BU INPUT buat PRT - for bu_input in bu_inputs: - prt = _create_return_from_picking(bu_input) + prt = _create_return_from_picking(bu_input, bu_input_qty_map) if prt: created_returns |= prt - # 3. Kalau tukar guling buat lanjut INPUT & PUT + # 🌀 Tukar Guling: buat dokumen baru dari PRT & VRT if record.return_type == 'tukar_guling': for prt in created_returns.filtered(lambda p: p.picking_type_id.id == 76): - bu_input = _create_return_from_picking(prt) + bu_input = _create_return_from_picking(prt, bu_input_qty_map) if bu_input: created_returns |= bu_input for vrt in created_returns.filtered(lambda p: p.picking_type_id.id == 77): - bu_put = _create_return_from_picking(vrt) + bu_put = _create_return_from_picking(vrt, bu_put_qty_map) if bu_put: created_returns |= bu_put -- cgit v1.2.3 From be0e6d6e04c85f0c2f77a490074dbeb7de98be0f Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 18 Jul 2025 17:29:14 +0700 Subject: validasi qty barang tidak sesuai --- indoteknik_custom/models/tukar_guling.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index d8e30006..a08d29bd 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -413,6 +413,14 @@ class TukarGuling(models.Model): raise UserError("❌ Tidak bisa retur BU/PICK karena BU/OUT suda Done!") if self._is_already_returned(self.operations): raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") + + for line in self.line_ids: + mapping_lines = self.mapping_koli_ids.filtered(lambda x: x.product_id == line.product_id) + total_qty = sum(l.qty_return for l in mapping_lines) + if total_qty != line.product_uom_qty: + raise UserError( + _("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name) + self._check_invoice_on_revisi_so() self._validate_product_lines() @@ -429,6 +437,13 @@ class TukarGuling(models.Model): operasi = self.operations.picking_type_id.id tipe = self.return_type + for line in self.line_ids: + mapping_lines = self.mapping_koli_ids.filtered(lambda x: x.product_id == line.product_id) + total_qty = sum(l.qty_return for l in mapping_lines) + if total_qty != line.product_uom_qty: + raise UserError( + _("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name) + if operasi == 30 and self.operations.linked_manual_bu_out.state == 'done': raise UserError("❌ Tidak bisa retur BU/PICK karena BU/OUT sudah done") if operasi == 30 and tipe == 'tukar_guling': -- cgit v1.2.3 From e0e6848123d06c58a506098124d4948fb72e2ba3 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 19 Jul 2025 15:31:39 +0700 Subject: (andri) fix --- indoteknik_custom/models/res_partner.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index 52947128..236df16f 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -165,26 +165,13 @@ class ResPartner(models.Model): "this feature", tracking=3) telegram_id = fields.Char(string="Telegram") avg_aging= fields.Float(string='Average Aging') - payment_difficulty = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', compute="_compute_payment_difficulty", inverse = "_inverse_payment_difficulty", tracking=3) + payment_difficulty = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', tracking=3) payment_history_url = fields.Text(string='Payment History URL') # no compute # payment_diff = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', tracking=3) # tidak terpakai - @api.depends('parent_id.payment_difficulty') - def _compute_payment_difficulty(self): - for partner in self: - if partner.parent_id: - partner.payment_difficulty = partner.parent_id.payment_difficulty - - # tidak terpakai - def _inverse_payment_difficulty(self): - for partner in self: - if not partner.parent_id: - partner.child_ids.write({ - 'payment_difficulty': partner.payment_difficulty - }) @api.model def _default_payment_term(self): -- cgit v1.2.3 From 6805a54ddeb69e64b1ba5f2d0dd23158aeea29df Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Mon, 21 Jul 2025 14:23:06 +0700 Subject: fix bug --- 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 4dc26d74..8e283c45 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -963,7 +963,7 @@ class PurchaseOrder(models.Model): # ) if not self.from_apo: - if not self.matches_so and not self.env.user.is_purchasing_manager and not self.env.user.is_leader: + if not self.matches_so and not self.env.user.is_purchasing_manager and not self.env.user.is_leader and not self.manufacturing_id: raise UserError("Tidak ada link dengan SO, harus di confirm oleh Purchasing Manager") send_email = False -- cgit v1.2.3 From 946e4b76e8280daad6d4849e9ced33a54d770a84 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Mon, 21 Jul 2025 14:50:24 +0700 Subject: push --- 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 25f45f6f..5b9e1acb 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -970,7 +970,7 @@ class PurchaseOrder(models.Model): # ) if not self.from_apo: - if not self.matches_so and not self.env.user.is_purchasing_manager and not self.env.user.is_leader and not self.manufacturing_id: + if (not self.matches_so or not self.sale_order_id) and not self.env.user.is_purchasing_manager and not self.env.user.is_leader and not self.manufacturing_id: raise UserError("Tidak ada link dengan SO, harus di confirm oleh Purchasing Manager") send_email = False -- cgit v1.2.3 From 87f38f9fcb68f04a2cc8157744622c2d0ebf1eab Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 22 Jul 2025 10:54:44 +0700 Subject: notif open purchasing job --- indoteknik_custom/models/purchasing_job.py | 55 +++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/purchasing_job.py b/indoteknik_custom/models/purchasing_job.py index 58f1c067..db733b5a 100644 --- a/indoteknik_custom/models/purchasing_job.py +++ b/indoteknik_custom/models/purchasing_job.py @@ -26,7 +26,46 @@ class PurchasingJob(models.Model): purchase_representative_id = fields.Many2one('res.users', string="Purchase Representative", readonly=True) note = fields.Char(string="Note Detail") date_po = fields.Datetime(string='Date PO', copy=False) - so_number = fields.Char(string='SO Number', copy=False) + so_number = fields.Text(string='SO Number', copy=False) + check_pj = fields.Boolean(compute='_get_check_pj', string='Linked') + + def action_open_job_detail(self): + self.ensure_one() + Seen = self.env['purchasing.job.seen'] + seen = Seen.search([ + ('user_id', '=', self.env.uid), + ('product_id', '=', self.product_id.id) + ], limit=1) + + if seen: + seen.so_snapshot = self.so_number + seen.seen_date = fields.Datetime.now() + else: + Seen.create({ + 'user_id': self.env.uid, + 'product_id': self.product_id.id, + 'so_snapshot': self.so_number, + }) + + return { + 'name': 'Purchasing Job Detail', + 'type': 'ir.actions.act_window', + 'res_model': 'v.purchasing.job', + 'res_id': self.id, + 'view_mode': 'form', + 'target': 'current', + } + + + @api.depends('so_number') + def _get_check_pj(self): + for rec in self: + seen = self.env['purchasing.job.seen'].search([ + ('user_id', '=', self.env.uid), + ('product_id', '=', rec.product_id.id) + ], limit=1) + rec.check_pj = bool(seen and seen.so_snapshot == rec.so_number) + def unlink(self): # Example: Delete related records from the underlying model @@ -199,3 +238,17 @@ class OutstandingSales(models.Model): and sp.name like '%OUT%' ) """) + +class PurchasingJobSeen(models.Model): + _name = 'purchasing.job.seen' + _description = 'User Seen SO Snapshot' + _rec_name = 'product_id' + + user_id = fields.Many2one('res.users', required=True, ondelete='cascade') + product_id = fields.Many2one('product.product', required=True, ondelete='cascade') + so_snapshot = fields.Text("Last Seen SO") + seen_date = fields.Datetime(default=fields.Datetime.now) + + _sql_constraints = [ + ('user_product_unique', 'unique(user_id, product_id)', 'User already tracked this product.') + ] -- cgit v1.2.3 From 69f0fbf98183ea00bb069df3ecd40e91df7081c3 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Tue, 22 Jul 2025 13:00:41 +0700 Subject: fix error when 2 item return --- indoteknik_custom/models/tukar_guling.py | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index a08d29bd..e546ad9c 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -99,16 +99,16 @@ class TukarGuling(models.Model): tg_product_ids = self.line_ids.mapped('product_id.id') for koli_line in self.operations.konfirm_koli_lines: - pick_move = koli_line.pick_id.move_line_ids_without_package - if pick_move.product_id.id in tg_product_ids: - mapping_koli_data.append((0, 0, { - 'sequence': sequence, - 'pick_id': koli_line.pick_id.id, - 'product_id': pick_move.product_id.id, - 'qty_done': pick_move.qty_done, - 'qty_return': 0 - })) - sequence += 10 + for move in koli_line.pick_id.move_line_ids_without_package: + if move.product_id.id in tg_product_ids: + mapping_koli_data.append((0, 0, { + 'sequence': sequence, + 'pick_id': koli_line.pick_id.id, + 'product_id': move.product_id.id, + 'qty_done': move.qty_done, + 'qty_return': 0 + })) + sequence += 10 self.mapping_koli_ids = mapping_koli_data _logger.info(f"✅ Created {len(mapping_koli_data)} mapping koli lines (from return wizard)") @@ -128,7 +128,6 @@ class TukarGuling(models.Model): # Untuk Odoo 14, gunakan move_ids_without_package atau move_lines moves_to_check = [] - if hasattr(self.operations, 'move_ids_without_package') and self.operations.move_ids_without_package: moves_to_check = self.operations.move_ids_without_package elif hasattr(self.operations, 'move_lines') and self.operations.move_lines: @@ -160,7 +159,7 @@ class TukarGuling(models.Model): if lines_data: self.line_ids = lines_data - _logger.info(f"Created {len(lines_data)} product lines") + _logger.info(f"✅ Created {len(lines_data)} product lines") # Prepare mapping koli jika BU/OUT mapping_koli_data = [] @@ -169,23 +168,23 @@ class TukarGuling(models.Model): if self.operations.picking_type_id.id == 29: tg_product_ids = [p for p in product_data] for koli_line in self.operations.konfirm_koli_lines: - pick_move = koli_line.pick_id.move_line_ids_without_package - if pick_move.product_id.id in tg_product_ids: - mapping_koli_data.append((0, 0, { - 'sequence': sequence, - 'pick_id': koli_line.pick_id.id, - 'product_id': pick_move.product_id.id, - 'qty_done': pick_move.qty_done - })) - sequence += 10 + for move in koli_line.pick_id.move_line_ids_without_package: + if move.product_id.id in tg_product_ids: + mapping_koli_data.append((0, 0, { + 'sequence': sequence, + 'pick_id': koli_line.pick_id.id, + 'product_id': move.product_id.id, + 'qty_done': move.qty_done + })) + sequence += 10 if mapping_koli_data: self.mapping_koli_ids = mapping_koli_data - _logger.info(f"Created {len(mapping_koli_data)} mapping koli lines") + _logger.info(f"✅ Created {len(mapping_koli_data)} mapping koli lines") else: - _logger.info("No mapping koli lines created") + _logger.info("⚠️ No mapping koli lines created") else: - _logger.info("No product lines created - no valid moves found") + _logger.info("⚠️ No product lines created - no valid moves found") else: from_return_picking = self.env.context.get('from_return_picking', False) or \ self.env.context.get('default_line_ids', False) @@ -196,6 +195,7 @@ class TukarGuling(models.Model): self.origin = False + def action_populate_lines(self): """Manual button untuk populate lines - sebagai alternatif""" self.ensure_one() -- cgit v1.2.3 From 1de8ad79dd0f0832dd14de9c1d004884f153bec4 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Tue, 22 Jul 2025 16:42:10 +0700 Subject: Chatter and sequence --- indoteknik_custom/models/tukar_guling.py | 40 +++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 9 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index e546ad9c..f27446d0 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -1,15 +1,25 @@ from odoo import models, fields, api, _ from odoo.exceptions import UserError, ValidationError import logging +from datetime import datetime _logger = logging.getLogger(__name__) +#TODO +# 1. tracking status dokumen BU [X] +# 2. ganti nama dokumen +# 3. Tracking ketika create dokumen [X] +# 4. Tracking ketika ganti field operations, date approval (sales, finance, logistic) [X] +# 5. Ganti proses approval ke Sales, Finance, Logistic [X] +# 6. Make sure bu pick dan out tidak bisa diedit ketika ort dan srt blm done +# 7. change approval class TukarGuling(models.Model): _name = 'tukar.guling' _description = 'Tukar Guling' _order = 'date desc, id desc' _rec_name = 'name' + _inherit = ['mail.thread', 'mail.activity.mixin'] origin = fields.Char(string='Origin SO') if_so = fields.Boolean('Is SO', default=True) @@ -38,24 +48,28 @@ class TukarGuling(models.Model): ('state', '=', 'done'), ('linked_manual_bu_out', '!=', 'done'), ], - help='Nomor BU/OUT atau BU/PICK' + help='Nomor BU/OUT atau BU/PICK', tracking=3, + required=True ) ba_num = fields.Text('Nomor BA') notes = fields.Text('Notes') return_type = fields.Selection(String='Return Type', selection=[ ('tukar_guling', 'Tukar Guling'), # -> barang yang sama - ('revisi_so', 'Revisi SO')], required=True) + ('revisi_so', 'Revisi SO')], required=True, tracking=3) state = fields.Selection(string='Status', selection=[ ('draft', 'Draft'), ('approval_sales', ' Approval Sales'), - ('approval_logistic', 'Approval Logistic'), ('approval_finance', 'Approval Finance'), + ('approval_logistic', 'Approval Logistic'), ('done', 'Done'), ('cancel', 'Canceled') ], default='draft', tracking=True, required=True) line_ids = fields.One2many('tukar.guling.line', 'tukar_guling_id', string='Product Lines') mapping_koli_ids = fields.One2many('tukar.guling.mapping.koli', 'tukar_guling_id', string='Mapping Koli') + date_done = fields.Datetime('Approved Date Finance', tracking=3, readonly=True) + date_sales = fields.Datetime('Approved Date Sales', tracking=3, readonly=True) + date_logistic = fields.Datetime('Approved Date Logistic', tracking=3, readonly=True) def _check_mapping_koli(self): for record in self: @@ -295,7 +309,9 @@ class TukarGuling(models.Model): if picking.origin: vals['origin'] = picking.origin - return super(TukarGuling, self).create(vals) + res = super(TukarGuling, self).create(vals) + res.message_post(body=_("CCM Created By %s" ) % self.env.user.name) + return res def copy(self, default=None): if default is None: @@ -457,23 +473,29 @@ class TukarGuling(models.Model): if not self.return_type: raise UserError("Return Type harus diisi!") + now = datetime.now() + # Cek hak akses berdasarkan state for rec in self: if rec.state == 'approval_sales': if not rec.env.user.has_group('indoteknik_custom.group_role_sales'): raise UserError("Hanya Sales Manager yang boleh approve tahap ini.") rec.state = 'approval_logistic' - - elif rec.state == 'approval_logistic': - if not rec.env.user.has_group('indoteknik_custom.group_role_logistic'): - raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.") - rec.state = 'approval_finance' + rec.date_sales = now elif rec.state == 'approval_finance': if not rec.env.user.has_group('indoteknik_custom.group_role_fat'): raise UserError("Hanya Finance Manager yang boleh approve tahap ini.") rec.state = 'done' + rec.date_done = now rec._create_pickings() + + elif rec.state == 'approval_logistic': + if not rec.env.user.has_group('indoteknik_custom.group_role_logistic'): + raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.") + rec.state = 'approval_finance' + rec.date_logistic = now + else: raise UserError("Status ini tidak bisa di-approve.") -- cgit v1.2.3 From 158c89a5048669f46d561aef3505042cc8c68a37 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Tue, 22 Jul 2025 19:51:47 +0700 Subject: change approval flow --- indoteknik_custom/models/tukar_guling.py | 48 +++++++++++++++++++------------- 1 file changed, 29 insertions(+), 19 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index f27446d0..2aaadecf 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -20,7 +20,8 @@ class TukarGuling(models.Model): _order = 'date desc, id desc' _rec_name = 'name' _inherit = ['mail.thread', 'mail.activity.mixin'] - + + partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) origin = fields.Char(string='Origin SO') if_so = fields.Boolean('Is SO', default=True) if_po = fields.Boolean('Is PO', default=False) @@ -67,10 +68,15 @@ class TukarGuling(models.Model): line_ids = fields.One2many('tukar.guling.line', 'tukar_guling_id', string='Product Lines') mapping_koli_ids = fields.One2many('tukar.guling.mapping.koli', 'tukar_guling_id', string='Mapping Koli') - date_done = fields.Datetime('Approved Date Finance', tracking=3, readonly=True) + date_finance = fields.Datetime('Approved Date Finance', tracking=3, readonly=True) date_sales = fields.Datetime('Approved Date Sales', tracking=3, readonly=True) date_logistic = fields.Datetime('Approved Date Logistic', tracking=3, readonly=True) + # @api.onchange('operations') + # def get_partner_id(self): + # if self.operations and self.operations.partner_id and self.operations.partner_id.name: + # self.partner_id == self.operations.partner_id.name + def _check_mapping_koli(self): for record in self: if record.operations.picking_type_id.id == 29: # Only for BU/OUT @@ -308,6 +314,8 @@ class TukarGuling(models.Model): picking = self.env['stock.picking'].browse(vals['operations']) if picking.origin: vals['origin'] = picking.origin + if picking.partner_id: + vals['partner_id'] = picking.partner_id.id res = super(TukarGuling, self).create(vals) res.message_post(body=_("CCM Created By %s" ) % self.env.user.name) @@ -430,12 +438,13 @@ class TukarGuling(models.Model): if self._is_already_returned(self.operations): raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") - for line in self.line_ids: - mapping_lines = self.mapping_koli_ids.filtered(lambda x: x.product_id == line.product_id) - total_qty = sum(l.qty_return for l in mapping_lines) - if total_qty != line.product_uom_qty: - raise UserError( - _("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name) + if self.operations.picking_type_id.id == 29: + for line in self.line_ids: + mapping_lines = self.mapping_koli_ids.filtered(lambda x: x.product_id == line.product_id) + total_qty = sum(l.qty_return for l in mapping_lines) + if total_qty != line.product_uom_qty: + raise UserError( + _("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name) self._check_invoice_on_revisi_so() self._validate_product_lines() @@ -453,12 +462,13 @@ class TukarGuling(models.Model): operasi = self.operations.picking_type_id.id tipe = self.return_type - for line in self.line_ids: - mapping_lines = self.mapping_koli_ids.filtered(lambda x: x.product_id == line.product_id) - total_qty = sum(l.qty_return for l in mapping_lines) - if total_qty != line.product_uom_qty: - raise UserError( - _("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name) + if self.operations.picking_type_id.id == 29: + for line in self.line_ids: + mapping_lines = self.mapping_koli_ids.filtered(lambda x: x.product_id == line.product_id) + total_qty = sum(l.qty_return for l in mapping_lines) + if total_qty != line.product_uom_qty: + raise UserError( + _("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name) if operasi == 30 and self.operations.linked_manual_bu_out.state == 'done': raise UserError("❌ Tidak bisa retur BU/PICK karena BU/OUT sudah done") @@ -480,21 +490,21 @@ class TukarGuling(models.Model): if rec.state == 'approval_sales': if not rec.env.user.has_group('indoteknik_custom.group_role_sales'): raise UserError("Hanya Sales Manager yang boleh approve tahap ini.") - rec.state = 'approval_logistic' + rec.state = 'approval_finance' rec.date_sales = now elif rec.state == 'approval_finance': if not rec.env.user.has_group('indoteknik_custom.group_role_fat'): raise UserError("Hanya Finance Manager yang boleh approve tahap ini.") - rec.state = 'done' - rec.date_done = now - rec._create_pickings() + rec.state = 'approval_logistic' + rec.date_finance = now elif rec.state == 'approval_logistic': if not rec.env.user.has_group('indoteknik_custom.group_role_logistic'): raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.") - rec.state = 'approval_finance' + rec.state = 'done' rec.date_logistic = now + rec._create_pickings() else: raise UserError("Status ini tidak bisa di-approve.") -- cgit v1.2.3 From bf1171579f04b102b289e26c426034b2b4458839 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Tue, 22 Jul 2025 20:35:07 +0700 Subject: sequence so --- indoteknik_custom/models/tukar_guling.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 2aaadecf..63c8c27e 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -302,12 +302,7 @@ class TukarGuling(models.Model): def create(self, vals): # Generate sequence number if not vals.get('name') or vals['name'] == 'New': - sequence = self.env['ir.sequence'].search([('code', '=', 'tukar.guling')], limit=1) - if sequence: - vals['name'] = sequence.next_by_id() - else: - # Fallback jika sequence belum dibuat - vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling') or 'PTG-00001' + vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling') # Auto-fill origin from operations if not vals.get('origin') and vals.get('operations'): @@ -318,7 +313,7 @@ class TukarGuling(models.Model): vals['partner_id'] = picking.partner_id.id res = super(TukarGuling, self).create(vals) - res.message_post(body=_("CCM Created By %s" ) % self.env.user.name) + res.message_post(body=_("CCM Created By %s") % self.env.user.name) return res def copy(self, default=None): -- cgit v1.2.3 From a638ef831668f0bedbad8d6a5331f6422ca582dc Mon Sep 17 00:00:00 2001 From: Miqdad Date: Tue, 22 Jul 2025 22:18:53 +0700 Subject: track validate picking from tukar guling --- indoteknik_custom/models/tukar_guling.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 63c8c27e..e5f98467 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -767,6 +767,25 @@ class StockPicking(models.Model): tukar_guling_id = fields.Many2one('tukar.guling', string='Tukar Guling Ref') + def button_validate(self): + res = super(StockPicking, self).button_validate() + + for picking in self: + if picking.tukar_guling_id: + message = _( + "📦 %s Validated by %s Status Changed %s at %s." + ) % ( + picking.name, + # picking.picking_type_id.name, + picking.env.user.name, + picking.state, + fields.Datetime.now().strftime("%d/%m/%Y %H:%M") + ) + picking.tukar_guling_id.message_post(body=message) + + return res + + class TukarGulingMappingKoli(models.Model): _name = 'tukar.guling.mapping.koli' -- cgit v1.2.3 From 365c9cbd0be8749873e4ec9953588d6b1b395677 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 23 Jul 2025 09:53:52 +0700 Subject: add ccm doc in SO --- indoteknik_custom/models/sale_order.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index e197a6af..0a15c997 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -125,6 +125,7 @@ class SaleOrderLine(models.Model): class SaleOrder(models.Model): _inherit = "sale.order" + ccm_id = fields.Many2one('tukar.guling', string='Doc. CCM', readonly=True, compute='_has_ccm', copy=False) ongkir_ke_xpdc = fields.Float(string='Ongkir ke Ekspedisi', help='Biaya ongkir ekspedisi', copy=False, index=True, tracking=3) @@ -357,6 +358,10 @@ class SaleOrder(models.Model): help="Tanggal pertama kali barang berhasil di-reservasi pada DO (BU/PICK/) yang berstatus Siap Dikirim." ) + def _has_ccm(self): + if self.id: + self.ccm_id = self.env['tukar.guling'].search([('origin', 'ilike', self.name)], limit=1) + @api.depends('order_line.product_id', 'date_order') def _compute_et_products(self): jakarta = pytz.timezone("Asia/Jakarta") -- cgit v1.2.3 From 1dbe906db264189b69a10633b3e7516650b3dfc5 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 23 Jul 2025 13:11:17 +0700 Subject: PO fix sequence on create and add vendor name --- indoteknik_custom/models/tukar_guling_po.py | 45 +++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 12 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index 72417a72..d2390854 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -10,7 +10,9 @@ _logger = logging.getLogger(__name__) class TukarGulingPO(models.Model): _name = 'tukar.guling.po' _description = 'Tukar Guling PO' + _inherit = ['mail.thread', 'mail.activity.mixin'] + vendor_id = fields.Many2one('res.partner', string='Vendor Name', readonly=True) origin = fields.Char(string='Origin PO') is_po = fields.Boolean('Is PO', default=True) is_so = fields.Boolean('Is SO', default=False) @@ -50,21 +52,22 @@ class TukarGulingPO(models.Model): @api.model def create(self, vals): # Generate sequence number + # ven_name = self.origin.search([('name', 'ilike', vals['origin'])]) if not vals.get('name') or vals['name'] == 'New': - sequence = self.env['ir.sequence'].search([('code', '=', 'tukar.guling.po')], limit=1) - if sequence: - vals['name'] = sequence.next_by_id() - else: - # Fallback jika sequence belum dibuat - vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling.po') or 'new' + vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling.po') # Auto-fill origin from operations if not vals.get('origin') and vals.get('operations'): picking = self.env['stock.picking'].browse(vals['operations']) if picking.origin: vals['origin'] = picking.origin + if picking.group_id.id: + vals['vendor_id'] = picking.group_id.partner_id.id + + res = super(TukarGulingPO, self).create(vals) + res.message_post(body=_("Tukar Guling PO Created By %s") % self.env.user.name) - return super(TukarGulingPO, self).create(vals) + return res @api.constrains('return_type', 'operations') def _check_bill_on_revisi_po(self): @@ -377,16 +380,16 @@ class TukarGulingPO(models.Model): if rec.state == 'approval_purchase': if not rec.env.user.has_group('indoteknik_custom.group_role_sales'): raise UserError("Hanya Sales Manager yang boleh approve tahap ini.") - rec.state = 'approval_logistic' - - elif rec.state == 'approval_logistic': - if not rec.env.user.has_group('indoteknik_custom.group_role_logistic'): - raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.") rec.state = 'approval_finance' elif rec.state == 'approval_finance': if not rec.env.user.has_group('indoteknik_custom.group_role_fat'): raise UserError("Hanya Finance Manager yang boleh approve tahap ini.") + rec.state = 'approval_logistic' + + elif rec.state == 'approval_logistic': + if not rec.env.user.has_group('indoteknik_custom.group_role_logistic'): + raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.") rec.state = 'done' rec._create_pickings() else: @@ -594,3 +597,21 @@ class TukarGulingLinePO(models.Model): class StockPicking(models.Model): _inherit = 'stock.picking' tukar_guling_po_id = fields.Many2one('tukar.guling.po', string='Tukar Guling PO Ref') + + + def button_validate(self): + res = super(StockPicking, self).button_validate() + for picking in self: + if picking.tukar_guling_po_id: + message = _( + "📦 %s Validated by %s Status Changed %s at %s." + ) % ( + picking.name, + # picking.picking_type_id.name, + picking.env.user.name, + picking.state, + fields.Datetime.now().strftime("%d/%m/%Y %H:%M") + ) + picking.tukar_guling_po_id.message_post(body=message) + + return res \ No newline at end of file -- cgit v1.2.3 From c7c058bcffef14b7fc08ee459d4198f1dd89dfc2 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 23 Jul 2025 13:16:47 +0700 Subject: fix state vcm --- indoteknik_custom/models/tukar_guling_po.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index d2390854..6a349a09 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -43,8 +43,8 @@ class TukarGulingPO(models.Model): state = fields.Selection([ ('draft', 'Draft'), ('approval_purchase', 'Approval Purchasing'), - ('approval_logistic', 'Approval Logistic'), ('approval_finance', 'Approval Finance'), + ('approval_logistic', 'Approval Logistic'), ('done', 'Done'), ('cancel', 'Cancel'), ], string='Status', default='draft') @@ -214,7 +214,7 @@ class TukarGulingPO(models.Model): def _check_product_lines(self): """Constraint: Product lines harus ada jika state bukan draft""" for record in self: - if record.state in ('approval_purchase', 'approval_logistic', 'approval_finance', + if record.state in ('approval_purchase', 'approval_finance', 'approval_logistic', 'done') and not record.line_ids: raise ValidationError("Product lines harus diisi sebelum submit atau approve!") -- cgit v1.2.3 From aa3b585bb0531c8d7af4402a58dd297ab2a17e9a Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 23 Jul 2025 13:34:28 +0700 Subject: approved date --- indoteknik_custom/models/tukar_guling_po.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index 6a349a09..3f072b88 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -3,6 +3,7 @@ from email.policy import default from odoo import models, fields, api, _ from odoo.exceptions import UserError, ValidationError import logging +from datetime import datetime _logger = logging.getLogger(__name__) @@ -24,22 +25,25 @@ class TukarGulingPO(models.Model): ) name = fields.Char('Number', required=True, copy=False, readonly=True, default='New') date = fields.Datetime('Date', default=fields.Datetime.now, required=True) + date_purchase = fields.Datetime('Date Approve Purchase', required=True, readonly=True) + date_finance = fields.Datetime('Date Approve Finance', required=True, readonly=True) + date_logistic = fields.Datetime('Date Approve Logistic', required=True, readonly=True) operations = fields.Many2one( 'stock.picking', string='Operations', domain=[ ('picking_type_id.id', 'in', [75, 28]), ('state', '=', 'done') - ], help='Nomor BU INPUT atau BU PUT' + ], help='Nomor BU INPUT atau BU PUT', tracking=3 ) - ba_num = fields.Char('Nomor BA') + ba_num = fields.Char('Nomor BA', tracking=3) return_type = fields.Selection([ ('revisi_po', 'Revisi PO'), ('tukar_guling', 'Tukar Guling'), - ], string='Return Type', required=True) - notes = fields.Text('Notes') + ], string='Return Type', required=True, tracking=3) + notes = fields.Text('Notes', tracking=3) tukar_guling_po_id = fields.Many2one('tukar.guling.po', string='Tukar Guling PO', ondelete='cascade') - line_ids = fields.One2many('tukar.guling.line.po', 'tukar_guling_po_id', string='Product Lines') + line_ids = fields.One2many('tukar.guling.line.po', 'tukar_guling_po_id', string='Product Lines', tracking=3) state = fields.Selection([ ('draft', 'Draft'), ('approval_purchase', 'Approval Purchasing'), @@ -47,7 +51,7 @@ class TukarGulingPO(models.Model): ('approval_logistic', 'Approval Logistic'), ('done', 'Done'), ('cancel', 'Cancel'), - ], string='Status', default='draft') + ], string='Status', default='draft', tracking=3) @api.model def create(self, vals): @@ -375,23 +379,28 @@ class TukarGulingPO(models.Model): if not self.return_type: raise UserError("Return Type harus diisi!") + now = datetime.now() + # Cek hak akses berdasarkan state for rec in self: if rec.state == 'approval_purchase': if not rec.env.user.has_group('indoteknik_custom.group_role_sales'): raise UserError("Hanya Sales Manager yang boleh approve tahap ini.") rec.state = 'approval_finance' + rec.date_purchase = now elif rec.state == 'approval_finance': if not rec.env.user.has_group('indoteknik_custom.group_role_fat'): raise UserError("Hanya Finance Manager yang boleh approve tahap ini.") rec.state = 'approval_logistic' + rec.date_finance = now elif rec.state == 'approval_logistic': if not rec.env.user.has_group('indoteknik_custom.group_role_logistic'): raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.") rec.state = 'done' rec._create_pickings() + rec.date_logistic = now else: raise UserError("Status ini tidak bisa di-approve.") -- cgit v1.2.3 From f8b95c402ca5850448fa6fd4fd59cdd21e49850b Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 23 Jul 2025 14:03:51 +0700 Subject: show vcm in PO --- indoteknik_custom/models/purchase_order.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 5b9e1acb..d157dc30 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -17,6 +17,7 @@ _logger = logging.getLogger(__name__) class PurchaseOrder(models.Model): _inherit = 'purchase.order' + vcm_id = fields.Many2one('tukar.guling.po', string='Doc VCM', readonly=True, compute='_has_vcm', copy=False) order_sales_match_line = fields.One2many('purchase.order.sales.match', 'purchase_order_id', string='Sales Match Lines', states={'cancel': [('readonly', True)], 'done': [('readonly', True)]}, copy=True) sale_order_id = fields.Many2one('sale.order', string='Sale Order') procurement_status = fields.Char(string='Procurement Status', compute='get_procurement_status', readonly=True) @@ -99,6 +100,10 @@ class PurchaseOrder(models.Model): ) manufacturing_id = fields.Many2one('mrp.production', string='Manufacturing Orders') + def _has_vcm(self): + if self.id: + self.vcm_id = self.env['tukar.guling.po'].search([('origin', '=', self.name)], limit=1) + @api.depends('name') def _compute_bu_related_count(self): StockPicking = self.env['stock.picking'] -- cgit v1.2.3 From deb60713ed39979b34083ee094de79fa3afac3b8 Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Wed, 23 Jul 2025 14:50:10 +0700 Subject: Refund System --- indoteknik_custom/models/__init__.py | 1 + indoteknik_custom/models/account_move.py | 35 +- indoteknik_custom/models/refund_sale_order.py | 649 ++++++++++++++++++++++++++ indoteknik_custom/models/sale_order.py | 123 ++++- 4 files changed, 806 insertions(+), 2 deletions(-) create mode 100644 indoteknik_custom/models/refund_sale_order.py (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index b815b472..44f383b0 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -152,4 +152,5 @@ from . import stock_inventory from . import sale_order_delay from . import approval_invoice_date from . import approval_payment_term +from . import refund_sale_order # from . import patch diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index b6627867..7bb71e03 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -1,5 +1,6 @@ from odoo import models, api, fields from odoo.exceptions import AccessError, UserError, ValidationError +from markupsafe import escape as html_escape from datetime import timedelta, date, datetime from pytz import timezone, utc import logging @@ -71,7 +72,24 @@ class AccountMove(models.Model): # Di model account.move bill_id = fields.Many2one('account.move', string='Vendor Bill', domain=[('move_type', '=', 'in_invoice')], help='Bill asal dari proses reklas ini') down_payment = fields.Boolean('Down Payments?') - + refund_id = fields.Many2one('refund.sale.order', string='Refund Reference') + refund_so_ids = fields.Many2many( + 'sale.order', + 'account_move_sale_order_rel', + 'move_id', + 'sale_order_id', + string='Group SO Number' + ) + + refund_so_links = fields.Html( + string="Group SO Numbers", + compute="_compute_refund_so_links", + ) + + has_refund_so = fields.Boolean( + string='Has Refund SO', + compute='_compute_has_refund_so', + ) # def name_get(self): # result = [] @@ -98,6 +116,21 @@ class AccountMove(models.Model): if self.date: self.invoice_date = self.date + @api.depends('refund_so_ids') + def _compute_refund_so_links(self): + for rec in self: + links = [] + for so in rec.refund_so_ids: + url = f"/web#id={so.id}&model=sale.order&view_type=form" + name = html_escape(so.name or so.display_name) + links.append(f'{name}') + rec.refund_so_links = ', '.join(links) if links else "-" + + @api.depends('refund_so_ids') + def _compute_has_refund_so(self): + for rec in self: + rec.has_refund_so = bool(rec.refund_so_ids) + # def compute_length_of_payment(self): # for rec in self: diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py new file mode 100644 index 00000000..5c9c4d83 --- /dev/null +++ b/indoteknik_custom/models/refund_sale_order.py @@ -0,0 +1,649 @@ +from odoo import fields, models, api, _ +from datetime import date, datetime +from terbilang import Terbilang +from odoo.exceptions import UserError, ValidationError +from markupsafe import escape as html_escape +import pytz +from lxml import etree + + +class RefundSaleOrder(models.Model): + _name = 'refund.sale.order' + _description = 'Refund Sales Order' + _inherit = ['mail.thread'] + _rec_name = 'name' + + name = fields.Char(string='Refund Number', default='New', copy=False, readonly=True) + note_refund = fields.Text(string='Note Refund') + sale_order_ids = fields.Many2many('sale.order', string='Sales Order Numbers') + uang_masuk = fields.Float(string='Uang Masuk', required=True) + total_invoice = fields.Float(string='Total Invoice', compute='_compute_total_invoice', readonly=True) + ongkir = fields.Float(string='Ongkir', required=True, default=0.0) + amount_refund = fields.Float(string='Total Refund', required=True) + amount_refund_text = fields.Char(string='Total Refund Text', compute='_compute_refund_text') + user_ids = fields.Many2many('res.users', string='Salespersons', compute='_compute_user_ids', domain=[('active', 'in', [True, False])]) + create_uid = fields.Many2one('res.users', string='Created By', readonly=True) + created_date = fields.Date(string='Tanggal Request Refund', readonly=True) + status = fields.Selection([ + ('draft', 'Draft'), + ('pengajuan1', 'Approval Sales Manager'), + ('pengajuan2', 'Approval AR'), + ('pengajuan3', 'Approval Pimpinan'), + ('reject', 'Cancel'), + ('refund', 'Approved') + ], string='Status Refund', default='draft', tracking=True) + + status_payment = fields.Selection([ + ('pending', 'Pending'), + ('reject', 'Cancel'), + ('done', 'Payment') + ], string='Status Payment', default='pending', tracking=True) + + reason_reject = fields.Text(string='Reason Cancel') + refund_date = fields.Date(string='Tanggal Refund') + invoice_ids = fields.Many2many('account.move', string='Invoices') + bank = fields.Char(string='Bank', required=True) + account_name = fields.Char(string='Account Name', required=True) + account_no = fields.Char(string='Account No', required=True) + finance_note = fields.Text(string='Finance Note') + invoice_names = fields.Html(string="Group Invoice Number", compute="_compute_invoice_names") + so_names = fields.Html(string="Group SO Number", compute="_compute_so_names") + + refund_type = fields.Selection([ + ('barang_kosong_sebagian', 'Refund Barang Kosong Sebagian'), + ('barang_kosong', 'Refund Barang Kosong Full'), + ('uang', 'Refund Lebih Bayar'), + ('retur_half', 'Refund Retur Sebagian'), + ('retur', 'Refund Retur Full'), + ('lainnya', 'Lainnya') + ], string='Refund Type', required=True) + + line_ids = fields.One2many('refund.sale.order.line', 'refund_id', string='Refund Lines') + invoice_line_ids = fields.One2many( + comodel_name='account.move.line', + inverse_name='move_id', + string='Invoice Lines', + compute='_compute_invoice_lines' + ) + + approved_by = fields.Text(string='Approved By', readonly=True) + date_approved_sales = fields.Datetime(string='Date Approved (Sales Manager)', readonly=True) + date_approved_ar = fields.Datetime(string='Date Approved (AR)', readonly=True) + date_approved_pimpinan = fields.Datetime(string='Date Approved (Pimpinan)', readonly=True) + position_sales = fields.Char(string='Position Sales', readonly=True) + position_ar = fields.Char(string='Position AR', readonly=True) + position_pimpinan = fields.Char(string='Position Pimpinan', readonly=True) + + partner_id = fields.Many2one( + 'res.partner', + string='Customer', + required=True + ) + advance_move_names = fields.Html(string="Group Journal SO", compute="_compute_advance_move_names") + uang_masuk_type = fields.Selection([ + ('pdf', 'PDF'), + ('image', 'Image'), + ], string="Attachment Type", default='image') + bukti_refund_type = fields.Selection([ + ('pdf', 'PDF'), + ('image', 'Image'), + ], string="Attachment Type", default='image') + bukti_uang_masuk_image = fields.Binary(string="Upload Bukti Uang Masuk") + bukti_transfer_refund_image = fields.Binary(string="Upload Bukti Transfer Refund") + bukti_uang_masuk_pdf = fields.Binary(string="Upload Bukti Uang Masuk") + bukti_transfer_refund_pdf = fields.Binary(string="Upload Bukti Transfer Refund") + journal_refund_move_id = fields.Many2one( + 'account.move', + string='Journal Refund', + compute='_compute_journal_refund_move_id', + ) + journal_refund_state = fields.Selection( + related='journal_refund_move_id.state', + string='Journal Refund State', + ) + + is_locked = fields.Boolean(string="Locked", compute="_compute_is_locked") + + + @api.model + def create(self, vals): + allowed_user_ids = [23, 19, 688, 7] + if not ( + self.env.user.has_group('indoteknik_custom.group_role_sales') or + self.env.user.has_group('indoteknik_custom.group_role_fat') or + self.env.user.id not in allowed_user_ids + ): + raise UserError("❌ Hanya user Sales dan Finance yang boleh membuat refund.") + + + if vals.get('name', 'New') == 'New': + vals['name'] = self.env['ir.sequence'].next_by_code('refund.sale.order') or 'New' + + vals['created_date'] = fields.Date.context_today(self) + vals['create_uid'] = self.env.user.id + + if 'sale_order_ids' in vals: + so_cmd = vals['sale_order_ids'] + so_ids = so_cmd[0][2] if so_cmd and so_cmd[0][0] == 6 else [] + if so_ids: + sale_orders = self.env['sale.order'].browse(so_ids) + vals['partner_id'] = sale_orders[0].partner_id.id + + invoices = sale_orders.mapped('invoice_ids').filtered( + lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.state != 'cancel' + ) + if invoices: + vals['invoice_ids'] = [(6, 0, invoices.ids)] + + + refund_type = vals.get('refund_type') + invoice_ids_data = vals.get('invoice_ids', []) + invoice_ids = invoice_ids_data[0][2] if invoice_ids_data and invoice_ids_data[0][0] == 6 else [] + + if invoice_ids and refund_type and refund_type not in ['uang', 'barang_kosong_sebagian', 'retur_half']: + raise UserError("Refund type Hanya Bisa Lebih Bayar, Barang Kosong Sebagian, atau Retur Sebagian jika ada invoice") + + if not invoice_ids and refund_type and refund_type in ['uang', 'barang_kosong_sebagian', 'retur_half']: + raise UserError("Refund type Lebih Bayar, Barang Kosong Sebagian, atau Retur Sebagian Hanya Bisa dipilih Jika Ada Invoice") + + + if not so_ids and refund_type != 'lainnya': + raise ValidationError("Jika tidak ada Sales Order yang dipilih, maka Tipe Refund hanya boleh 'Lainnya'.") + + refund = refund_type in ['retur', 'retur_half'] + if refund and so_ids: + so = self.env['sale.order'].browse(so_ids) + pickings = self.env['stock.picking'].search([ + ('state', '=', 'done'), + ('picking_type_id', '=', 73), + ('sale_id', 'in', so_ids) + ]) + if not pickings: + raise ValidationError(f"SO {', '.join(so.mapped('name'))} tidak melakukan retur barang.") + + if refund_type == 'retur_half' and not invoice_ids: + raise ValidationError(f"SO {', '.join(so.mapped('name'))} belum memiliki invoice untuk Retur Sebagian.") + + total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total')) if invoice_ids else 0.0 + uang_masuk = vals.get('uang_masuk', 0.0) + ongkir = vals.get('ongkir', 0.0) + pengurangan = total_invoice + ongkir + + if uang_masuk > pengurangan: + vals['amount_refund'] = uang_masuk - pengurangan + else: + raise UserError("Uang masuk harus lebih besar dari total invoice + ongkir untuk melakukan refund") + + return super().create(vals) + + + def write(self, vals): + allowed_user_ids = [23, 19, 688, 7] + if not ( + self.env.user.has_group('indoteknik_custom.group_role_sales') or + self.env.user.has_group('indoteknik_custom.group_role_fat') or + self.env.user.id in allowed_user_ids + ): + raise UserError("❌ Hanya user Sales dan Finance yang boleh mengedit refund.") + + for rec in self: + if 'sale_order_ids' in vals: + so_commands = vals['sale_order_ids'] + so_ids = [] + for cmd in so_commands: + if cmd[0] == 6: + so_ids = cmd[2] + elif cmd[0] == 4: + so_ids.append(cmd[1]) + elif cmd[0] == 3: + if cmd[1] in so_ids: + so_ids.remove(cmd[1]) + + if so_ids: + sale_orders = self.env['sale.order'].browse(so_ids) + vals['partner_id'] = sale_orders[0].partner_id.id + + sale_orders = self.env['sale.order'].browse(so_ids) + + valid_invoices = sale_orders.mapped('invoice_ids').filtered( + lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.state != 'cancel' + ) + vals['invoice_ids'] = [(6, 0, valid_invoices.ids)] + vals['ongkir'] = sum(so.delivery_amt or 0.0 for so in sale_orders) + else: + so_ids = rec.sale_order_ids.ids + + sale_orders = self.env['sale.order'].browse(so_ids) + + + refund_type = vals.get('refund_type', rec.refund_type) + + if not so_ids and refund_type != 'lainnya': + raise ValidationError("Jika tidak ada Sales Order yang dipilih, maka Tipe Refund hanya boleh 'Lainnya'.") + + + invoice_ids = vals.get('invoice_ids', False) + if invoice_ids: + final_invoice_ids = [] + for cmd in invoice_ids: + if cmd[0] == 6: + final_invoice_ids = cmd[2] + elif cmd[0] == 4: + final_invoice_ids.append(cmd[1]) + invoice_ids = final_invoice_ids + else: + invoice_ids = rec.invoice_ids.ids + + if invoice_ids and vals.get('refund_type', rec.refund_type) not in ['uang', 'barang_kosong_sebagian', 'retur_half']: + raise UserError("Refund type Hanya Bisa Lebih Bayar, Barang Kosong Sebagian, atau Retur Sebagian jika ada invoice") + + if not invoice_ids and vals.get('refund_type', rec.refund_type) in ['uang', 'barang_kosong_sebagian', 'retur_half']: + raise UserError("Refund type Lebih Bayar, Barang Kosong Sebagian, atau Retur Sebagian Hanya Bisa dipilih Jika Ada Invoice") + + if refund_type in ['retur', 'retur_half'] and so_ids: + so = self.env['sale.order'].browse(so_ids) + pickings = self.env['stock.picking'].search([ + ('state', '=', 'done'), + ('picking_type_id', '=', 73), + ('sale_id', 'in', so_ids) + ]) + + if not pickings: + raise ValidationError(f"SO {', '.join(so.mapped('name'))} tidak melakukan retur barang.") + + if refund_type == 'retur_half' and not invoice_ids: + raise ValidationError(f"SO {', '.join(so.mapped('name'))} belum memiliki invoice untuk retur sebagian.") + + if any(field in vals for field in ['uang_masuk', 'invoice_ids', 'ongkir', 'sale_order_ids']): + total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total')) + uang_masuk = vals.get('uang_masuk', rec.uang_masuk) + ongkir = vals.get('ongkir', rec.ongkir) + + if uang_masuk <= (total_invoice + ongkir): + raise UserError("Uang masuk harus lebih besar dari total invoice + ongkir") + vals['amount_refund'] = uang_masuk - (total_invoice + ongkir) + + if vals.get('status') == 'refund' and not vals.get('refund_date'): + vals['refund_date'] = fields.Date.context_today(self) + + return super().write(vals) + + @api.depends('status_payment') + def _compute_is_locked(self): + for rec in self: + rec.is_locked = rec.status_payment in ['done', 'reject'] + + @api.depends('invoice_ids.amount_total') + def _compute_total_invoice(self): + for rec in self: + rec.total_invoice = sum(inv.amount_total for inv in rec.invoice_ids) + + @api.depends('sale_order_ids') + def _compute_advance_move_names(self): + for rec in self: + move_links = [] + moves = self.env['account.move'].search([ + ('sale_id', 'in', rec.sale_order_ids.ids), + ('journal_id', '=', 11), + ('state', '=', 'posted') + ]) + for move in moves: + url = f"/web#id={move.id}&model=account.move&view_type=form" + name = html_escape(move.name or 'Unnamed') + move_links.append(f'{name}') + rec.advance_move_names = ', '.join(move_links) if move_links else "-" + + @api.depends('sale_order_ids.user_id') + def _compute_user_ids(self): + for rec in self: + user_ids = list({so.user_id.id for so in rec.sale_order_ids if so.user_id}) + rec.user_ids = [(6, 0, user_ids)] + + @api.onchange('sale_order_ids') + def _onchange_sale_order_ids(self): + self.invoice_ids = [(5, 0, 0)] + self.line_ids = [(5, 0, 0)] + self.ongkir = 0.0 + all_invoices = self.env['account.move'] + total_invoice = 0.0 + + for so in self.sale_order_ids: + self.ongkir += so.delivery_amt or 0.0 + valid_invoices = so.invoice_ids.filtered( + lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.state != 'cancel' + ) + all_invoices |= valid_invoices + total_invoice += sum(valid_invoices.mapped('amount_total')) + + + self.invoice_ids = all_invoices + self.total_invoice = total_invoice + self.refund_type = 'uang' if all_invoices else False + + pengurangan = total_invoice + self.ongkir + if self.uang_masuk > pengurangan: + self.amount_refund = self.uang_masuk - pengurangan + else: + self.amount_refund = 0.0 + + if self.sale_order_ids: + self.partner_id = self.sale_order_ids[0].partner_id + + + @api.onchange('refund_type') + def _onchange_refund_type(self): + self.line_ids = [(5, 0, 0)] + if self.refund_type in ['barang_kosong_sebagian', 'barang_kosong'] and self.sale_order_ids: + line_vals = [] + for so in self.sale_order_ids: + for line in so.order_line: + if line.qty_delivered == 0: + line_vals.append((0, 0, { + 'product_id': line.product_id.id, + 'quantity': line.product_uom_qty, + 'reason': '', + })) + + self.line_ids = line_vals + + elif self.refund_type in ['retur', 'retur_half'] and self.sale_order_ids: + line_vals = [] + StockPicking = self.env['stock.picking'] + for so in self.sale_order_ids: + pickings = StockPicking.search([ + ('state', '=', 'done'), + ('picking_type_id', '=', 73), + ('sale_id', 'in', so.ids) + ]) + + for picking in pickings: + for move in picking.move_lines: + line_vals.append((0, 0, { + 'product_id': move.product_id.id, + 'quantity': move.product_uom_qty, + 'reason': '', + })) + self.line_ids = line_vals + + + @api.depends('invoice_ids') + def _compute_invoice_lines(self): + for rec in self: + lines = self.env['account.move.line'] + for inv in rec.invoice_ids: + lines |= inv.invoice_line_ids + rec.invoice_line_ids = lines + + @api.depends('amount_refund') + def _compute_refund_text(self): + tb = Terbilang() + for record in self: + res = '' + try: + if record.amount_refund > 0: + tb.parse(int(record.amount_refund)) + res = tb.getresult().title() + record.amount_refund_text = res + ' Rupiah' + except: + record.amount_refund_text = '' + + def unlink(self): + not_draft = self.filtered(lambda r: r.status != 'draft') + if not_draft: + names = ', '.join(not_draft.mapped('name')) + raise UserError(f"Refund hanya bisa dihapus jika statusnya masih draft.\nTidak bisa hapus: {names}") + return super().unlink() + + @api.depends('invoice_ids') + def _compute_invoice_names(self): + for rec in self: + names = [] + for inv in rec.invoice_ids: + url = f"/web#id={inv.id}&model=account.move&view_type=form" + name = html_escape(inv.name) + names.append(f'{name}') + rec.invoice_names = ', '.join(names) + + + @api.depends('sale_order_ids') + def _compute_so_names(self): + for rec in self: + so_links = [] + for so in rec.sale_order_ids: + url = f"/web#id={so.id}&model=sale.order&view_type=form" + name = html_escape(so.name) + so_links.append(f'{name}') + rec.so_names = ', '.join(so_links) if so_links else "-" + + @api.onchange('uang_masuk', 'total_invoice', 'ongkir') + def _onchange_amount_refund(self): + for rec in self: + pengurangan = rec.total_invoice + rec.ongkir + refund = rec.uang_masuk - pengurangan + rec.amount_refund = refund if refund > 0 else 0.0 + + if rec.uang_masuk and rec.uang_masuk <= pengurangan: + return { + 'warning': { + 'title': 'Uang Masuk Kurang', + 'message': 'Uang masuk harus lebih besar dari total invoice + ongkir untuk dapat melakukan refund.' + } + } + + @api.model + def default_get(self, fields_list): + res = super().default_get(fields_list) + sale_order_id = self.env.context.get('default_sale_order_id') + if sale_order_id: + so = self.env['sale.order'].browse(sale_order_id) + res['sale_order_ids'] = [(6, 0, [so.id])] + invoice_ids = so.invoice_ids.filtered( + lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.state != 'cancel' + ).ids + res['invoice_ids'] = [(6, 0, invoice_ids)] + res['uang_masuk'] = 0.0 + res['ongkir'] = so.delivery_amt or 0.0 + line_vals = [] + for line in so.order_line: + line_vals.append((0, 0, { + 'product_id': line.product_id.id, + 'quantity': line.product_uom_qty, + 'reason': '', + })) + res['line_ids'] = line_vals + res['refund_type'] = 'uang' if invoice_ids else False + return res + + @api.onchange('invoice_ids') + def _onchange_invoice_ids(self): + if self.invoice_ids: + if self.refund_type not in ['uang', 'barang_kosong']: + self.refund_type = False + + self.total_invoice = sum(self.invoice_ids.mapped('amount_total')) + + def action_ask_approval(self): + for rec in self: + if rec.status == 'draft': + rec.status = 'pengajuan1' + + + def _get_status_label(self, code): + status_dict = dict(self.fields_get(allfields=['status'])['status']['selection']) + return status_dict.get(code, code) + + def action_approve_flow(self): + jakarta_tz = pytz.timezone('Asia/Jakarta') + now = datetime.now(jakarta_tz).replace(tzinfo=None) + + for rec in self: + user_name = self.env.user.name + + if not rec.status or rec.status == 'draft': + rec.status = 'pengajuan1' + + elif rec.status == 'pengajuan1' and self.env.user.id == 19: + rec.status = 'pengajuan2' + rec.approved_by = f"{rec.approved_by}, {user_name}" if rec.approved_by else user_name + rec.date_approved_sales = now + rec.position_sales = 'Sales Manager' + + elif rec.status == 'pengajuan2' and self.env.user.id == 688: + rec.status = 'pengajuan3' + rec.approved_by = f"{rec.approved_by}, {user_name}" if rec.approved_by else user_name + rec.date_approved_ar = now + rec.position_ar = 'AR' + + elif rec.status == 'pengajuan3' and self.env.user.id == 7: + rec.status = 'refund' + rec.approved_by = f"{rec.approved_by}, {user_name}" if rec.approved_by else user_name + rec.date_approved_pimpinan = now + rec.position_pimpinan = 'Pimpinan' + rec.refund_date = fields.Date.context_today(self) + + else: + raise UserError("❌ Hanya bisa diapproved oleh yang bersangkutan.") + + def action_trigger_cancel(self): + is_fat = self.env.user.has_group('indoteknik_custom.group_role_fat') + allowed_user_ids = [19, 688, 7] + for rec in self: + if self.user.id not in allowed_user_ids and not is_fat: + raise UserError("❌ Hanya user yang bersangkutan atau Finance (FAT) yang bisa melakukan penolakan.") + if rec.status not in ['refund', 'reject']: + rec.status = 'reject' + rec.status_payment = 'reject' + + @api.constrains('status', 'reason_reject') + def _check_reason_if_rejected(self): + for rec in self: + if rec.status == 'reject' and not rec.reason_reject: + raise ValidationError("Alasan pembatalan harus diisi ketika status Reject.") + + def action_confirm_refund(self): + is_fat = self.env.user.has_group('indoteknik_custom.group_role_fat') + for rec in self: + if not is_fat: + raise UserError("Hanya Finance yang dapat mengkonfirmasi refund.") + if rec.status_payment == 'pending': + rec.status_payment = 'done' + rec.refund_date = fields.Date.context_today(self) + else: + raise UserError("Refund hanya bisa dikonfirmasi setelah Approval Pimpinan.") + + def _compute_approval_label(self): + for rec in self: + label = 'Approval Done' + if rec.status == 'draft': + label = 'Approval Sales Manager' + elif rec.status == 'pengajuan1': + label = 'Approval AR' + elif rec.status == 'pengajuan2': + label = 'Approval Pimpinan' + elif rec.status == 'pengajuan3': + label = 'Confirm Refund' + rec.approval_button_label = label + + def action_create_journal_refund(self): + is_fat = self.env.user.has_group('indoteknik_custom.group_role_fat') + if not is_fat: + raise UserError("❌ Akses ditolak. Hanya Finance yang dapat membuat journal refund.") + + for refund in self: + current_time = fields.Datetime.now() + has_invoice = any(refund.sale_order_ids.mapped('invoice_ids')) + # Penentuan partner (dari SO atau partner_id langsung) + partner = ( + refund.sale_order_ids[0].partner_id.parent_id or + refund.sale_order_ids[0].partner_id + ) if refund.sale_order_ids else refund.partner_id + + # Ambil label refund type + refund_type_label = dict( + self.fields_get(allfields=['refund_type'])['refund_type']['selection'] + ).get(refund.refund_type, '').replace("Refund ", "").upper() + + + + if not partner: + raise UserError("❌ Partner tidak ditemukan.") + + # Ref format + ref_text = f"REFUND {refund_type_label} {refund.name or ''} {partner.display_name}".upper() + + # Buat Account Move (Journal Entry) + account_move = self.env['account.move'].create({ + 'ref': ref_text, + 'date': current_time, + 'journal_id': 11, + 'refund_id': refund.id, + 'refund_so_ids': [(6, 0, refund.sale_order_ids.ids)], + 'partner_id': partner.id, + }) + + amount = refund.amount_refund + + second_account_id = 450 if has_invoice else 668 + + debit_line = { + 'move_id': account_move.id, + 'account_id': second_account_id, + 'partner_id': partner.id, + 'currency_id': 12, + 'debit': amount, + 'credit': 0.0, + 'name': ref_text, + } + + credit_line = { + 'move_id': account_move.id, + 'account_id': 389, # Intransit BCA + 'partner_id': partner.id, + 'currency_id': 12, + 'debit': 0.0, + 'credit': amount, + 'name': ref_text, + } + + self.env['account.move.line'].create([debit_line, credit_line]) + + return { + 'name': _('Journal Entries'), + 'view_mode': 'form', + 'res_model': 'account.move', + 'type': 'ir.actions.act_window', + 'res_id': account_move.id, + 'target': 'current' + } + + def _compute_journal_refund_move_id(self): + for rec in self: + move = self.env['account.move'].search([ + ('refund_id', '=', rec.id) + ], limit=1) + rec.journal_refund_move_id = move + + def action_open_journal_refund(self): + self.ensure_one() + if self.journal_refund_move_id: + return { + 'name': _('Journal Refund'), + 'view_mode': 'form', + 'res_model': 'account.move', + 'type': 'ir.actions.act_window', + 'res_id': self.journal_refund_move_id.id, + 'target': 'current' + } + + + + +class RefundSaleOrderLine(models.Model): + _name = 'refund.sale.order.line' + _description = 'Refund Sales Order Line' + _inherit = ['mail.thread'] + + refund_id = fields.Many2one('refund.sale.order', string='Refund Ref') + product_id = fields.Many2one('product.product', string='Product') + quantity = fields.Float(string='Qty') + reason = fields.Char(string='Reason') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 591951ca..8d40bfb5 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -356,6 +356,14 @@ class SaleOrder(models.Model): compute="_compute_eta_date_reserved", help="Tanggal pertama kali barang berhasil di-reservasi pada DO (BU/PICK/) yang berstatus Siap Dikirim." ) + refund_ids = fields.Many2many('refund.sale.order', compute='_compute_refund_ids', string='Refunds') + has_refund = fields.Boolean(string='Has Refund', compute='_compute_has_refund') + refund_count = fields.Integer(string='Refund Count', compute='_compute_refund_count') + advance_payment_move_id = fields.Many2one( + 'account.move', + compute='_compute_advance_payment_move', + string='Advance Payment Move', + ) @api.depends('order_line.product_id', 'date_order') def _compute_et_products(self): @@ -3077,4 +3085,117 @@ class SaleOrder(models.Model): if any(field in vals for field in ["order_line", "client_order_ref"]): self._calculate_etrts_date() - return res \ No newline at end of file + return res + + def button_refund(self): + self.ensure_one() + + invoice_ids = self.invoice_ids.filtered(lambda inv: inv.state != 'cancel') + + return { + 'name': 'Refund Sale Order', + 'type': 'ir.actions.act_window', + 'res_model': 'refund.sale.order', + 'view_mode': 'form', + 'target': 'current', + 'context': { + 'default_sale_order_ids': [(6, 0, [self.id])], + 'default_invoice_ids': [(6, 0, invoice_ids.ids)], + 'default_uang_masuk': sum(invoice_ids.mapped('amount_total')) + (self.delivery_amt or 0.0) + 1000, + 'default_ongkir': self.delivery_amt or 0.0, + 'default_bank': '', # bisa isi default bank kalau mau + 'default_account_name': '', + 'default_account_no': '', + 'default_refund_type': '', + }, + } + + def open_form_multi_create_refund(self): + if not self: + raise UserError("Tidak ada Sale Order yang dipilih.") + + partner_set = set(self.mapped('partner_id.id')) + if len(partner_set) > 1: + raise UserError("Tidak dapat membuat refund untuk Multi SO dengan Customer berbeda. Harus memiliki Customer yang sama.") + + invoice_status_set = set(self.mapped('invoice_status')) + if len(invoice_status_set) > 1: + raise UserError("Tidak dapat membuat refund untuk SO dengan status invoice berbeda. Harus memiliki status invoice yang sama.") + + already_refunded = self.filtered(lambda so: so.has_refund) + if already_refunded: + so_names = ', '.join(already_refunded.mapped('name')) + raise UserError(f"❌ Tidak bisa refund ulang. {so_names} sudah melakukan refund.") + + invoice_ids = self.mapped('invoice_ids').filtered(lambda inv: inv.state != 'cancel') + delivery_total = sum(self.mapped('delivery_amt')) + total_invoice = sum(invoice_ids.mapped('amount_total')) + + return { + 'type': 'ir.actions.act_window', + 'name': 'Create Refund', + 'res_model': 'refund.sale.order', + 'view_mode': 'form', + 'target': 'current', + 'context': { + 'default_sale_order_ids': [(6, 0, self.ids)], + 'default_invoice_ids': [(6, 0, invoice_ids.ids)], + 'default_uang_masuk': total_invoice + delivery_total + 1000, + 'default_ongkir': delivery_total, + 'default_bank': '', + 'default_account_name': '', + 'default_account_no': '', + 'default_refund_type': '', + } + } + + @api.depends('refund_ids') + def _compute_has_refund(self): + for so in self: + so.has_refund = bool(so.refund_ids) + + def action_view_related_refunds(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': 'Refunds', + 'res_model': 'refund.sale.order', + 'view_mode': 'tree,form', + 'domain': [('sale_order_ids', 'in', [self.id])], + 'context': {'default_sale_order_ids': [self.id]}, + } + + def _compute_refund_ids(self): + for order in self: + refunds = self.env['refund.sale.order'].search([ + ('sale_order_ids', 'in', [order.id]) + ]) + order.refund_ids = refunds + + def _compute_refund_count(self): + for order in self: + order.refund_count = self.env['refund.sale.order'].search_count([ + ('sale_order_ids', 'in', order.id) + ]) + + @api.depends('invoice_ids') + def _compute_advance_payment_move(self): + for order in self: + move = self.env['account.move'].search([ + ('sale_id', '=', order.id), + ('journal_id', '=', 11), + ('state', '=', 'posted'), + ], limit=1, order="id desc") + order.advance_payment_move_id = move + + def action_open_advance_payment_move(self): + self.ensure_one() + if not self.advance_payment_move_id: + return + return { + 'type': 'ir.actions.act_window', + 'res_model': 'account.move', + 'res_id': self.advance_payment_move_id.id, + 'view_mode': 'form', + 'target': 'current', + } \ No newline at end of file -- cgit v1.2.3 From 712417255817d5142c28464818ca040a53517a09 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 23 Jul 2025 14:51:03 +0700 Subject: rev message post --- indoteknik_custom/models/tukar_guling_po.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index 3f072b88..5f990525 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -69,7 +69,7 @@ class TukarGulingPO(models.Model): vals['vendor_id'] = picking.group_id.partner_id.id res = super(TukarGulingPO, self).create(vals) - res.message_post(body=_("Tukar Guling PO Created By %s") % self.env.user.name) + res.message_post(body=_("VCM Created By %s") % self.env.user.name) return res -- cgit v1.2.3 From 4d0ba0dc130697e6dec1422efa7339b5e6445b53 Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Wed, 23 Jul 2025 15:11:52 +0700 Subject: fix --- indoteknik_custom/models/account_move.py | 192 +++++++++++++++---------------- 1 file changed, 96 insertions(+), 96 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index db52f398..1a6fad1c 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -109,102 +109,102 @@ class AccountMove(models.Model): # result.append((move.id, move.display_name)) # return result - def send_due_invoice_reminder(self): - today = fields.Date.today() - target_dates = [ - today - timedelta(days=7), - today - timedelta(days=3), - today, - today + timedelta(days=3), - today + timedelta(days=7), - ] - - partner = self.env['res.partner'].search([('name', 'ilike', 'BANGUNAN TEKNIK GRUP')], limit=1) - if not partner: - _logger.info("Partner tidak ditemukan.") - return - - invoices = self.env['account.move'].search([ - ('move_type', '=', 'out_invoice'), - ('state', '=', 'posted'), - ('payment_state', 'not in', ['paid','in_payment', 'reversed']), - ('invoice_date_due', 'in', target_dates), - ('partner_id', '=', partner.id), - ]) - - _logger.info(f"Invoices tahap 1: {invoices}") - - invoices = invoices.filtered( - lambda inv: inv.invoice_payment_term_id and 'tempo' in (inv.invoice_payment_term_id.name or '').lower() - ) - _logger.info(f"Invoices tahap 2: {invoices}") - - if not invoices: - _logger.info(f"Tidak ada invoice yang due untuk partner: {partner.name}") - return - - grouped = {} - for inv in invoices: - grouped.setdefault(inv.partner_id, []).append(inv) - - template = self.env.ref('indoteknik_custom.mail_template_invoice_due_reminder') - - for partner, invs in grouped.items(): - if not partner.email: - _logger.info(f"Partner {partner.name} tidak memiliki email") - continue - - invoice_table_rows = "" - for inv in invs: - days_to_due = (inv.invoice_date_due - today).days if inv.invoice_date_due else 0 - invoice_table_rows += f""" - - {inv.name} - {fields.Date.to_string(inv.invoice_date) or '-'} - {fields.Date.to_string(inv.invoice_date_due) or '-'} - {days_to_due} - {formatLang(self.env, inv.amount_total, currency_obj=inv.currency_id)} - {inv.ref or '-'} - - """ - - subject = f"Reminder Invoice Due - {partner.name}" - body_html = re.sub( - r"]*>.*?", - f"{invoice_table_rows}", - template.body_html, - flags=re.DOTALL - ).replace('${object.name}', partner.name) \ - .replace('${object.partner_id.name}', partner.name) - # .replace('${object.email}', partner.email or '') - - values = { - 'subject': subject, - 'email_to': 'andrifebriyadiputra@gmail.com', # Ubah ke partner.email untuk produksi - 'email_from': 'finance@indoteknik.co.id', - 'body_html': body_html, - 'reply_to': f'invoice+account.move_{invs[0].id}@indoteknik.co.id', - } - - _logger.info(f"VALUES: {values}") - - template.send_mail(invs[0].id, force_send=True, email_values=values) - - # Default System User - user_system = self.env['res.users'].browse(25) - system_id = user_system.partner_id.id if user_system else False - _logger.info(f"System User: {user_system.name} ({user_system.id})") - _logger.info(f"System User ID: {system_id}") - - for inv in invs: - inv.message_post( - subject=subject, - body=body_html, - subtype_id=self.env.ref('mail.mt_note').id, - author_id=system_id, - ) - - _logger.info(f"Reminder terkirim ke {partner.name} ({values['email_to']}) → {len(invs)} invoice") + # def send_due_invoice_reminder(self): + # today = fields.Date.today() + # target_dates = [ + # today - timedelta(days=7), + # today - timedelta(days=3), + # today, + # today + timedelta(days=3), + # today + timedelta(days=7), + # ] + + # partner = self.env['res.partner'].search([('name', 'ilike', 'BANGUNAN TEKNIK GRUP')], limit=1) + # if not partner: + # _logger.info("Partner tidak ditemukan.") + # return + + # invoices = self.env['account.move'].search([ + # ('move_type', '=', 'out_invoice'), + # ('state', '=', 'posted'), + # ('payment_state', 'not in', ['paid','in_payment', 'reversed']), + # ('invoice_date_due', 'in', target_dates), + # ('partner_id', '=', partner.id), + # ]) + + # _logger.info(f"Invoices tahap 1: {invoices}") + + # invoices = invoices.filtered( + # lambda inv: inv.invoice_payment_term_id and 'tempo' in (inv.invoice_payment_term_id.name or '').lower() + # ) + # _logger.info(f"Invoices tahap 2: {invoices}") + + # if not invoices: + # _logger.info(f"Tidak ada invoice yang due untuk partner: {partner.name}") + # return + + # grouped = {} + # for inv in invoices: + # grouped.setdefault(inv.partner_id, []).append(inv) + + # template = self.env.ref('indoteknik_custom.mail_template_invoice_due_reminder') + + # for partner, invs in grouped.items(): + # if not partner.email: + # _logger.info(f"Partner {partner.name} tidak memiliki email") + # continue + + # invoice_table_rows = "" + # for inv in invs: + # days_to_due = (inv.invoice_date_due - today).days if inv.invoice_date_due else 0 + # invoice_table_rows += f""" + # + # {inv.name} + # {fields.Date.to_string(inv.invoice_date) or '-'} + # {fields.Date.to_string(inv.invoice_date_due) or '-'} + # {days_to_due} + # {formatLang(self.env, inv.amount_total, currency_obj=inv.currency_id)} + # {inv.ref or '-'} + # + # """ + + # subject = f"Reminder Invoice Due - {partner.name}" + # body_html = re.sub( + # r"]*>.*?", + # f"{invoice_table_rows}", + # template.body_html, + # flags=re.DOTALL + # ).replace('${object.name}', partner.name) \ + # .replace('${object.partner_id.name}', partner.name) + # # .replace('${object.email}', partner.email or '') + + # values = { + # 'subject': subject, + # 'email_to': 'andrifebriyadiputra@gmail.com', # Ubah ke partner.email untuk produksi + # 'email_from': 'finance@indoteknik.co.id', + # 'body_html': body_html, + # 'reply_to': f'invoice+account.move_{invs[0].id}@indoteknik.co.id', + # } + + # _logger.info(f"VALUES: {values}") + + # template.send_mail(invs[0].id, force_send=True, email_values=values) + + # # Default System User + # user_system = self.env['res.users'].browse(25) + # system_id = user_system.partner_id.id if user_system else False + # _logger.info(f"System User: {user_system.name} ({user_system.id})") + # _logger.info(f"System User ID: {system_id}") + + # for inv in invs: + # inv.message_post( + # subject=subject, + # body=body_html, + # subtype_id=self.env.ref('mail.mt_note').id, + # author_id=system_id, + # ) + + # _logger.info(f"Reminder terkirim ke {partner.name} ({values['email_to']}) → {len(invs)} invoice") @api.onchange('invoice_date') -- cgit v1.2.3 From 6a804c8d3604d7654fce48ce9e1aa1cb7c95ae87 Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Wed, 23 Jul 2025 16:39:30 +0700 Subject: Label Refund Type --- indoteknik_custom/models/refund_sale_order.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py index 5c9c4d83..559ca07a 100644 --- a/indoteknik_custom/models/refund_sale_order.py +++ b/indoteknik_custom/models/refund_sale_order.py @@ -58,6 +58,8 @@ class RefundSaleOrder(models.Model): ('lainnya', 'Lainnya') ], string='Refund Type', required=True) + refund_type_display = fields.Char(string="Refund Type Label", compute="_compute_refund_type_display") + line_ids = fields.One2many('refund.sale.order.line', 'refund_id', string='Refund Lines') invoice_line_ids = fields.One2many( comodel_name='account.move.line', @@ -104,6 +106,11 @@ class RefundSaleOrder(models.Model): is_locked = fields.Boolean(string="Locked", compute="_compute_is_locked") + @api.depends('refund_type') + def _compute_refund_type_display(self): + for rec in self: + rec.refund_type_display = dict(self.fields_get(allfields=['refund_type'])['refund_type']['selection']).get(rec.refund_type, '') + @api.model def create(self, vals): -- cgit v1.2.3 From e661868aa6a593114de434dcd0d1b01d8ca3b426 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 23 Jul 2025 16:52:53 +0700 Subject: (andri) fix total margin SO keitung 2x --- indoteknik_custom/models/sale_order_line.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index 2a0160e8..5e9fc362 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -173,8 +173,8 @@ class SaleOrderLine(models.Model): # minus with delivery if covered by indoteknik if line.order_id.shipping_cost_covered == 'indoteknik': sales_price -= line.delivery_amt_line - if line.order_id.fee_third_party > 0: - sales_price -= line.fee_third_party_line + # if line.order_id.fee_third_party > 0: + # sales_price -= line.fee_third_party_line purchase_price = line.purchase_price if line.purchase_tax_id.price_include: -- cgit v1.2.3 From 3614078d30a5dd148d11720963eb49cb2d2cc886 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 24 Jul 2025 09:19:43 +0700 Subject: tracking when create a new return --- indoteknik_custom/models/tukar_guling.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index e5f98467..cd9a1bd1 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -578,6 +578,8 @@ class TukarGuling(models.Model): }) created_returns.append(srt_picking) _logger.info(f"✅ SRT created: {srt_picking.name}") + record.message_post( + body=f"📦 {srt_picking.name} created by {self.env.user.name} (state: {srt_picking.state})") ### ======== ORT dari BU/PICK ========= ort_pickings = [] @@ -637,6 +639,8 @@ class TukarGuling(models.Model): created_returns.append(ort_picking) ort_pickings.append(ort_picking) _logger.info(f"✅ ORT created: {ort_picking.name}") + record.message_post( + body=f"📦 {ort_picking.name} created by {self.env.user.name} (state: {ort_picking.state})") ### ======== Tukar Guling: BU/OUT dan BU/PICK baru ======== if record.return_type == 'tukar_guling': @@ -682,6 +686,8 @@ class TukarGuling(models.Model): new_pick.action_confirm() created_returns.append(new_pick) _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})") # BU/OUT Baru dari SRT if srt_picking: @@ -718,6 +724,8 @@ class TukarGuling(models.Model): }) created_returns.append(new_out) _logger.info(f"✅ BU/OUT Baru dari SRT created: {new_out.name}") + record.message_post( + body=f"📦 {new_out.name} created by {self.env.user.name} (state: {new_out.state})") if not created_returns: raise UserError("Tidak ada dokumen retur berhasil dibuat.") -- cgit v1.2.3 From 4b1e8b9c5d516daf80e78212acfe28f7e518a4ba Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 24 Jul 2025 09:20:55 +0700 Subject: date logistic --- indoteknik_custom/models/tukar_guling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index cd9a1bd1..d40c0ac8 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -498,8 +498,8 @@ class TukarGuling(models.Model): if not rec.env.user.has_group('indoteknik_custom.group_role_logistic'): raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.") rec.state = 'done' - rec.date_logistic = now rec._create_pickings() + rec.date_logistic = now else: raise UserError("Status ini tidak bisa di-approve.") -- cgit v1.2.3 From 6e3b560d3f115bfa89f3d5853cc9709b7cc963d6 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 24 Jul 2025 09:45:08 +0700 Subject: add permission to cancel ccm --- indoteknik_custom/models/tukar_guling.py | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index d40c0ac8..c5ccad80 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -507,6 +507,15 @@ class TukarGuling(models.Model): def action_cancel(self): self.ensure_one() # picking = self.env['stock.picking'] + + user = self.env.user + if not ( + user.has_group('indoteknik_custom.group_role_sales') or + user.has_group('indoteknik_custom.group_role_fat') or + user.has_group('indoteknik_custom.group_role_logistic') + ): + raise UserWarning('Anda tidak memiliki Permission untuk cancel document') + bu_done = self.picking_ids.filtered(lambda p: p.state == 'done') if bu_done: raise UserError("Dokuemen BU sudah Done, tidak bisa di cancel") -- cgit v1.2.3 From 365bf9fd1d6cff481c24342db44ef9aeffdd7d80 Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Thu, 24 Jul 2025 10:52:36 +0700 Subject: editable field --- indoteknik_custom/models/refund_sale_order.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py index 559ca07a..e3a0146d 100644 --- a/indoteknik_custom/models/refund_sale_order.py +++ b/indoteknik_custom/models/refund_sale_order.py @@ -17,7 +17,7 @@ class RefundSaleOrder(models.Model): note_refund = fields.Text(string='Note Refund') sale_order_ids = fields.Many2many('sale.order', string='Sales Order Numbers') uang_masuk = fields.Float(string='Uang Masuk', required=True) - total_invoice = fields.Float(string='Total Invoice', compute='_compute_total_invoice', readonly=True) + total_invoice = fields.Float(string='Total Invoice') ongkir = fields.Float(string='Ongkir', required=True, default=0.0) amount_refund = fields.Float(string='Total Refund', required=True) amount_refund_text = fields.Char(string='Total Refund Text', compute='_compute_refund_text') @@ -105,6 +105,10 @@ class RefundSaleOrder(models.Model): ) is_locked = fields.Boolean(string="Locked", compute="_compute_is_locked") + sale_order_names_jasper = fields.Char(string='Sales Order List', compute='_compute_order_invoice_names') + invoice_names_jasper = fields.Char(string='Invoice List', compute='_compute_order_invoice_names') + + @api.depends('refund_type') def _compute_refund_type_display(self): @@ -147,10 +151,10 @@ class RefundSaleOrder(models.Model): invoice_ids_data = vals.get('invoice_ids', []) invoice_ids = invoice_ids_data[0][2] if invoice_ids_data and invoice_ids_data[0][0] == 6 else [] - if invoice_ids and refund_type and refund_type not in ['uang', 'barang_kosong_sebagian', 'retur_half']: + if invoice_ids and refund_type and refund_type not in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half']: raise UserError("Refund type Hanya Bisa Lebih Bayar, Barang Kosong Sebagian, atau Retur Sebagian jika ada invoice") - if not invoice_ids and refund_type and refund_type in ['uang', 'barang_kosong_sebagian', 'retur_half']: + if not invoice_ids and refund_type and refund_type in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half']: raise UserError("Refund type Lebih Bayar, Barang Kosong Sebagian, atau Retur Sebagian Hanya Bisa dipilih Jika Ada Invoice") @@ -241,10 +245,10 @@ class RefundSaleOrder(models.Model): else: invoice_ids = rec.invoice_ids.ids - if invoice_ids and vals.get('refund_type', rec.refund_type) not in ['uang', 'barang_kosong_sebagian', 'retur_half']: + if invoice_ids and vals.get('refund_type', rec.refund_type) not in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half']: raise UserError("Refund type Hanya Bisa Lebih Bayar, Barang Kosong Sebagian, atau Retur Sebagian jika ada invoice") - if not invoice_ids and vals.get('refund_type', rec.refund_type) in ['uang', 'barang_kosong_sebagian', 'retur_half']: + if not invoice_ids and vals.get('refund_type', rec.refund_type) in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half']: raise UserError("Refund type Lebih Bayar, Barang Kosong Sebagian, atau Retur Sebagian Hanya Bisa dipilih Jika Ada Invoice") if refund_type in ['retur', 'retur_half'] and so_ids: @@ -279,11 +283,12 @@ class RefundSaleOrder(models.Model): def _compute_is_locked(self): for rec in self: rec.is_locked = rec.status_payment in ['done', 'reject'] - - @api.depends('invoice_ids.amount_total') - def _compute_total_invoice(self): + + @api.depends('sale_order_ids.name', 'invoice_ids.name') + def _compute_order_invoice_names(self): for rec in self: - rec.total_invoice = sum(inv.amount_total for inv in rec.invoice_ids) + rec.sale_order_names_jasper = ', '.join(rec.sale_order_ids.mapped('name')) or '' + rec.invoice_names_jasper = ', '.join(rec.invoice_ids.mapped('name')) or '' @api.depends('sale_order_ids') def _compute_advance_move_names(self): -- cgit v1.2.3 From 8720938222472b6e4f217229881067d5b0b126c0 Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Thu, 24 Jul 2025 11:10:10 +0700 Subject: editable field --- indoteknik_custom/models/refund_sale_order.py | 8 -------- 1 file changed, 8 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py index e3a0146d..11bfd07f 100644 --- a/indoteknik_custom/models/refund_sale_order.py +++ b/indoteknik_custom/models/refund_sale_order.py @@ -433,14 +433,6 @@ class RefundSaleOrder(models.Model): pengurangan = rec.total_invoice + rec.ongkir refund = rec.uang_masuk - pengurangan rec.amount_refund = refund if refund > 0 else 0.0 - - if rec.uang_masuk and rec.uang_masuk <= pengurangan: - return { - 'warning': { - 'title': 'Uang Masuk Kurang', - 'message': 'Uang masuk harus lebih besar dari total invoice + ongkir untuk dapat melakukan refund.' - } - } @api.model def default_get(self, fields_list): -- cgit v1.2.3 From bdf6d7dbd8850d525809726036c6783b9113028f Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 24 Jul 2025 13:33:07 +0700 Subject: (andri) fix raise ppn notif --- 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 5b9e1acb..a46c51d1 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -1043,7 +1043,7 @@ class PurchaseOrder(models.Model): for line in self.order_line: if line.taxes_id != reference_taxes: - raise UserError("PPN harus sama untuk semua baris pada line.") + raise UserError(f"PPN harus sama untuk semua baris pada line {line.product_id.name}") def check_data_vendor(self): vendor = self.partner_id -- cgit v1.2.3 From cbe2c5730bcd82ce594e03425c987b17c583b893 Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Thu, 24 Jul 2025 14:44:23 +0700 Subject: add journals in so --- indoteknik_custom/models/sale_order.py | 42 ++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 995cafba..febdaabd 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -364,6 +364,17 @@ class SaleOrder(models.Model): compute='_compute_advance_payment_move', string='Advance Payment Move', ) + advance_payment_move_ids = fields.Many2many( + 'account.move', + compute='_compute_advance_payment_moves', + string='All Advance Payment Moves', + ) + + advance_payment_move_count = fields.Integer( + string='Jumlah Jurnal Uang Muka', + compute='_compute_advance_payment_moves', + store=False + ) @api.depends('order_line.product_id', 'date_order') def _compute_et_products(self): @@ -3191,15 +3202,38 @@ class SaleOrder(models.Model): ('state', '=', 'posted'), ], limit=1, order="id desc") order.advance_payment_move_id = move + + @api.depends('invoice_ids') + def _compute_advance_payment_moves(self): + for order in self: + moves = self.env['account.move'].search([ + ('sale_id', '=', order.id), + ('journal_id', '=', 11), + ('state', '=', 'posted'), + ]) + order.advance_payment_move_ids = moves + + @api.depends('invoice_ids') + def _compute_advance_payment_moves(self): + for order in self: + moves = self.env['account.move'].search([ + ('sale_id', '=', order.id), + ('journal_id', '=', 11), + ('state', '=', 'posted'), + ]) + order.advance_payment_move_ids = moves + order.advance_payment_move_count = len(moves) - def action_open_advance_payment_move(self): + def action_open_advance_payment_moves(self): self.ensure_one() - if not self.advance_payment_move_id: + moves = self.advance_payment_move_ids + if not moves: return return { 'type': 'ir.actions.act_window', + 'name': 'Journals Sales Order', 'res_model': 'account.move', - 'res_id': self.advance_payment_move_id.id, - 'view_mode': 'form', + 'view_mode': 'tree,form', + 'domain': [('id', 'in', moves.ids)], 'target': 'current', } \ No newline at end of file -- cgit v1.2.3 From b8efc85091fe0af596872bffeb3cf6c78fe2beed Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 24 Jul 2025 17:35:11 +0700 Subject: cant delete when done and in approval state --- indoteknik_custom/models/tukar_guling.py | 29 ++++++++++++++++++++++++++++- indoteknik_custom/models/tukar_guling_po.py | 17 +++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index c5ccad80..7253afb7 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -98,6 +98,8 @@ class TukarGuling(models.Model): @api.onchange('operations') def _onchange_operations(self): """Auto-populate lines ketika operations dipilih""" + if self.operations.picking_type_id.id not in [29,30]: + raise UserError("❌ Picking type harus BU/OUT atau BU/PICK") for rec in self: if rec.operations and rec.operations.picking_type_id.id == 30: rec.return_type = 'revisi_so' @@ -343,6 +345,8 @@ class TukarGuling(models.Model): def write(self, vals): self.ensure_one() + if self.operations.picking_type_id.id not in [29,30]: + raise UserError("❌ Picking type harus BU/OUT atau BU/PICK") self._check_invoice_on_revisi_so() operasi = self.operations.picking_type_id.id tipe = self.return_type @@ -756,6 +760,18 @@ class TukarGulingLine(models.Model): product_uom = fields.Many2one('uom.uom', string='Unit of Measure') name = fields.Text('Description') + @api.constrains('product_uom_qty') + def _check_qty_change_allowed(self): + for rec in self: + if rec.tukar_guling_id and rec.tukar_guling_id.state not in ['draft', 'cancel']: + raise ValidationError("Tidak bisa mengubah Quantity karena status dokumen bukan Draft atau Cancel.") + + def unlink(self): + for rec in self: + if rec.tukar_guling_id and rec.tukar_guling_id.state not in ['draft', 'cancel']: + raise UserError("Tidak bisa menghapus data karena status dokumen bukan Draft atau Cancel.") + return super(TukarGulingLine, self).unlink() + @api.model_create_multi def create(self, vals_list): """Override create to auto-assign sequence""" @@ -813,4 +829,15 @@ class TukarGulingMappingKoli(models.Model): product_id = fields.Many2one('product.product', string='Product') qty_done = fields.Float(string='Qty Done BU PICK') qty_return = fields.Float(string='Qty diretur') - sequence = fields.Integer(string='Sequence', default=10) \ No newline at end of file + sequence = fields.Integer(string='Sequence', default=10) + @api.constrains('qty_return') + def _check_qty_return_editable(self): + for rec in self: + if rec.tukar_guling_id and rec.tukar_guling_id.state not in ['draft', 'cancel']: + raise ValidationError("Tidak Bisa ubah qty retur jika status sudah approval atau done.") + + def unlink(self): + for rec in self: + if rec.tukar_guling_id and rec.tukar_guling_id.state not in ['draft', 'cancel']: + raise UserError("Tidak bisa menghapus Mapping Koli karena status Tukar Guling bukan Draft atau Cancel.") + return super(TukarGulingMappingKoli, self).unlink() \ No newline at end of file diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index b3279077..7c9680f8 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -90,6 +90,9 @@ class TukarGulingPO(models.Model): @api.onchange('operations') def _onchange_operations(self): """Auto-populate lines ketika operations dipilih""" + if self.operations.picking_type_id.id not in [75, 28]: + raise UserError("❌ Picking type harus BU/INPUT atau BU/PUT") + if self.operations: from_return_picking = self.env.context.get('from_return_picking', False) or \ self.env.context.get('default_line_ids', False) @@ -275,6 +278,8 @@ class TukarGulingPO(models.Model): return new_record def write(self, vals): + if self.operations.picking_type_id.id not in [75, 28]: + raise UserError("❌ Tidak bisa retur bukan BU/INPUT atau BU/PUT!") self._check_bill_on_revisi_po() tipe = vals.get('return_type', self.return_type) @@ -621,6 +626,18 @@ class TukarGulingLinePO(models.Model): product_uom = fields.Many2one('uom.uom', string='Unit of Measure') name = fields.Text('Description') + @api.constrains('product_uom_qty') + def _check_qty_change_allowed(self): + for rec in self: + if rec.tukar_guling_id and rec.tukar_guling_po_id.state not in ['draft', 'cancel']: + raise ValidationError("Tidak bisa mengubah Quantity karena status dokumen bukan Draft atau Cancel.") + + def unlink(self): + for rec in self: + if rec.tukar_guling_po_id and rec.tukar_guling_po_id.state not in ['draft', 'cancel']: + raise UserError("Tidak bisa menghapus data karena status dokumen bukan Draft atau Cancel.") + return super(TukarGulingLinePO, self).unlink() + class StockPicking(models.Model): _inherit = 'stock.picking' -- cgit v1.2.3