from .. import controller from odoo import http, fields from odoo.http import request, Response from pytz import timezone from datetime import datetime import json import logging _logger = logging.getLogger(__name__) class StockPicking(controller.Controller): prefix = '/api/v1/' PREFIX_PARTNER = prefix + 'partner//' @http.route(PREFIX_PARTNER + 'stock-picking', auth='public', method=['GET', 'OPTIONS']) @controller.Controller.must_authorized(private=True, private_key='partner_id') def get_partner_stock_picking(self, **kw): get_params = self.get_request_params(kw, { 'partner_id': ['number'], 'q': [], 'status': [], 'limit': ['default:0', 'number'], 'offset': ['default:0', 'number'] }) if not get_params['valid']: return self.response(code=400, description=get_params) params = get_params['value'] partner_id = params['partner_id'] limit = params['limit'] offset = params['offset'] child_ids = request.env['res.partner'].browse(partner_id).get_child_ids() pending_domain = [('driver_departure_date', '=', False), ('driver_arrival_date', '=', False)] shipment_domain = [('driver_departure_date', '!=', False), ('driver_arrival_date', '=', False)] shipment_domain2 = [('driver_departure_date', '!=', False), ('sj_return_date', '=', False)] completed_domain = [('driver_departure_date', '!=', False), '|', ('driver_arrival_date', '!=', False), ('sj_return_date', '!=', False)] completed_domain2 = [('driver_departure_date', '!=', False), ('sj_return_date', '!=', False)] picking_model = request.env['stock.picking'] domain = [ ('partner_id', 'in', child_ids), ('sale_id', '!=', False), ('origin', 'ilike', 'SO%'), ('state', '!=', 'cancel'), ('name', 'ilike', 'BU/OUT%') ] if params['q']: query_like = '%' + params['q'].replace(' ', '%') + '%' domain += ['|', '|', ('name', 'ilike', query_like), ('sale_id.client_order_ref', 'ilike', query_like), ('delivery_tracking_no', 'ilike', query_like) ] default_domain = domain.copy() if params['status'] == 'pending': domain += pending_domain elif params['status'] == 'shipment': domain += shipment_domain + shipment_domain2 elif params['status'] == 'completed': domain += completed_domain stock_pickings = picking_model.search(domain, offset=offset, limit=limit, order='create_date desc') res_pickings = [] for picking in stock_pickings: manifests = picking.get_manifests() res_pickings.append({ 'id': picking.id, 'name': picking.name, 'date': self.time_to_str(picking.create_date, '%d/%m/%Y'), 'tracking_number': picking.delivery_tracking_no or '', 'sale_order': { 'id': picking.sale_id.id, 'name': picking.sale_id.name, 'client_order_ref': picking.sale_id.client_order_ref or '' }, 'delivered': picking.waybill_id.delivered or picking.driver_arrival_date != False or picking.sj_return_date != False, 'status': picking.shipping_status, 'carrier_name': picking.carrier_id.name or '', 'last_manifest': next(iter(manifests), None) }) return self.response({ 'summary': { 'pending_count': picking_model.search_count(default_domain + pending_domain), 'shipment_count': picking_model.search_count(default_domain + shipment_domain + shipment_domain2), 'completed_count': picking_model.search_count(default_domain + completed_domain) }, 'picking_total': picking_model.search_count(domain), 'pickings': res_pickings }) @http.route(PREFIX_PARTNER + 'stock-picking//tracking', auth='public', method=['GET', 'OPTIONS']) @controller.Controller.must_authorized(private=True, private_key='partner_id') def get_partner_stock_picking_detail_tracking(self, **kw): id = int(kw.get('id', 0)) picking_model = request.env['stock.picking'] picking = picking_model.browse(id) if not picking: return self.response(None) return self.response(picking.get_tracking_detail()) @http.route(prefix + 'stock-picking//tracking', auth='public', method=['GET', 'OPTIONS']) @controller.Controller.must_authorized() def get_partner_stock_picking_detail_tracking_iman(self, **kw): id = int(kw.get('id', 0)) picking_model = request.env['stock.picking'] picking = picking_model.browse(id) if not picking: return self.response(None) return self.response(picking.get_tracking_detail()) @http.route(prefix + 'stock-picking//documentation', auth='public', methods=['PUT', 'OPTIONS'], csrf=False) @controller.Controller.must_authorized() def write_partner_stock_picking_documentation(self, scanid, **kw): paket_document = kw.get('paket_document') if 'paket_document' in kw else None dispatch_document = kw.get('dispatch_document') if 'dispatch_document' in kw else None self_pu = kw.get('self_pu') if 'self_pu' in kw else None # ===== Cari picking by id / picking_code ===== picking = False if scanid.isdigit() and int(scanid) < 2147483646: picking = request.env['stock.picking'].search([('id', '=', int(scanid))], limit=0) if not picking: picking = request.env['stock.picking'].search([('picking_code', '=', scanid)], limit=0) if not picking: return self.response(code=403, description='picking not found') # ===== Ambil MULTIPLE SJ dari form: sj_documentations=...&sj_documentations=... ===== form = request.httprequest.form or {} sj_list = form.getlist('sj_documentations') # list of base64 strings # fallback: kalau FE kirim single dengan nama yang sama (bukan list) if not sj_list and 'sj_documentations' in kw and kw.get('sj_documentations'): sj_list = [kw.get('sj_documentations')] params = {} if paket_document: params['paket_documentation'] = paket_document params['driver_arrival_date'] = datetime.utcnow() if dispatch_document: params['dispatch_documentation'] = dispatch_document if sj_list and self_pu: params['driver_arrival_date'] = datetime.utcnow() if params: picking.write(params) if sj_list: Child = request.env['stock.picking.sj.document'].sudo() seq = (picking.sj_documentations[:1].sequence or 10) if picking.sj_documentations else 10 for b64 in sj_list: if not b64: continue Child.create({ 'picking_id': picking.id, 'sequence': seq, 'image': b64, }) seq += 10 return self.response({'name': picking.name}) # @http.route(prefix + 'stock-picking//documentation', auth='public', methods=['PUT', 'OPTIONS'], csrf=False) # @controller.Controller.must_authorized() # def write_partner_stock_picking_documentation(self, scanid, **kw): # sj_document = kw.get('sj_document') if 'sj_document' in kw else None # paket_document = kw.get('paket_document') if 'paket_document' in kw else None # dispatch_document = kw.get('dispatch_document') if 'dispatch_document' in kw else None # self_pu= kw.get('self_pu') if 'self_pu' in kw else None # # # ===== Cari picking by id / picking_code ===== # picking_data = False # if scanid.isdigit() and int(scanid) < 2147483646: # picking_data = request.env['stock.picking'].search([('id', '=', int(scanid))], limit=0) # # if not picking_data: # picking_data = request.env['stock.picking'].search([('picking_code', '=', scanid)], limit=0) # # if not picking_data: # return self.response(code=403, description='picking not found') # # params = {} # if sj_document: # params['sj_documentation'] = sj_document # if self_pu: # params['driver_arrival_date'] = datetime.utcnow() # if paket_document: # params['paket_documentation'] = paket_document # params['driver_arrival_date'] = datetime.utcnow() # if dispatch_document: # params['dispatch_documentation'] = dispatch_document # # picking_data.write(params) # return self.response({'name': picking_data.name}) @http.route(prefix + 'webhook/biteship', type='json', auth='public', methods=['POST'], csrf=False) def update_status_from_biteship(self, **kw): _logger.info("Biteship Webhook: Request received at controller start (type='json').") try: # Karena type='json', Odoo secara otomatis akan mem-parsing JSON untuk Anda. # 'data' akan berisi dictionary Python dari payload JSON Biteship. data = request.jsonrequest # Log ini akan menunjukkan payload yang diterima (sudah dalam bentuk dict) _logger.info(f"Biteship Webhook: Parsed JSON data from request.jsonrequest: {json.dumps(data)}") event = data.get('event') if event: _logger.info(f"Biteship Webhook: Processing event: {event}") if event == "order.status": self.process_order_status(data) elif event == "order.price": self.process_order_price(data) elif event == "order.waybill_id": self.process_order_waybill(data) # Tambahkan logika untuk event lain jika ada else: _logger.info("Biteship Webhook: No specific event in payload. Likely an installation/verification ping or unknown event type.") # Untuk route type='json', Anda cukup mengembalikan dictionary Python. # Odoo akan secara otomatis mengonversinya menjadi respons JSON yang valid. return {'status': 'ok'} except Exception as e: _logger.error(f"Biteship Webhook: Unhandled error during processing: {e}", exc_info=True) # Untuk error, kembalikan dictionary error juga, Odoo akan mengonversinya ke JSON return {'status': 'error', 'message': str(e)} def process_order_status(self, data): picking = request.env['stock.picking'].sudo().search([ ('biteship_id', '=', data.get('order_id')) ], limit=1) if not picking: _logger.warning(f"[Webhook] Tidak ditemukan picking untuk order_id {data.get('order_id')}") return status = data.get('status') timestamp = data.get('updated_at') or datetime.utcnow().isoformat() description = picking._get_biteship_status_description(status, { "courier": {"company": data.get("courier_company", "")}, "destination": {"contact_name": picking.partner_id.name or ""} }) # Tambahkan extra data dari webhook extra_data = { "courier_driver_name": data.get("courier_driver_name"), "courier_driver_phone": data.get("courier_driver_phone"), "courier_driver_plate_number": data.get("courier_driver_plate_number"), "courier_link": data.get("courier_link"), "order_price": data.get("order_price"), "status": data.get("status"), } picking.log_biteship_event_from_webhook(status, timestamp, description, extra_data=extra_data) def process_order_price(self, data): picking = request.env['stock.picking'].sudo().search([('biteship_id', '=', data.get('order_id'))], limit=1) if not picking: _logger.warning(f"Tidak ditemukan picking untuk order_id {data.get('order_id')}") return picking.log_biteship_event_from_webhook( status='order.price', timestamp=data.get('updated_at') or datetime.utcnow().isoformat(), description='Biaya pengiriman telah diperbarui berdasarkan informasi terbaru dari Biteship.', extra_data={ "order_price": data.get("price") } ) def process_order_waybill(self, data): picking = request.env['stock.picking'].sudo().search([ ('biteship_id', '=', data.get('order_id')) ], limit=1) if not picking: _logger.warning(f"Tidak ditemukan picking untuk order_id {data.get('order_id')}") return picking.log_biteship_event_from_webhook( status='order.waybill_id', timestamp=data.get('updated_at') or datetime.utcnow().isoformat(), description="Nomor waybill dan tracking diperbarui melalui Biteship.", extra_data={ "tracking_id": data.get("courier_tracking_id"), "waybill_id": data.get("courier_waybill_id") } ) @http.route(prefix + 'locator/picking', auth='public', methods=['GET', 'OPTIONS']) @controller.Controller.must_authorized() def get_picking_by_name(self, **kw): name = str(kw.get('name')) if not name: return self.response({'status': 'error', 'message': 'Picking name is required'}) picking = request.env['stock.picking'].search([('name', 'ilike', name)], limit=1) if 'BU/INPUT/' in name: if picking.state != 'done': return self.response({'status': 'error', 'message': 'BU Input nya belum done'}) move_dest = picking.move_ids_without_package[0] picking = move_dest.move_dest_ids.filtered(lambda m: m.state not in ['done', 'cancel']).picking_id if not picking: return self.response({'status': 'error', 'message': 'Picking not found'}) lines = [] for move in picking.move_line_ids_without_package: lines.append({ 'move_id': move.id, 'product_id': move.product_id.id, 'product_name': move.product_id.display_name, 'product_uom_qty': move.product_uom_qty, 'qty_done': move.qty_done, 'uom_name': move.product_uom_id.name, 'source_location': move.location_id.complete_name, 'dest_location': move.location_dest_id.complete_name, }) data = { 'status': 'success', 'picking': { 'id': picking.id, 'name': picking.name, 'type_code': picking.picking_type_id.code, 'source_location': picking.location_id.complete_name, 'dest_location': picking.location_dest_id.complete_name, 'state': picking.state, 'lines': lines, } } return self.response(data) @http.route('/api/v1/locator/picking/update', auth='public', methods=['POST', 'OPTIONS'], csrf=False) @controller.Controller.must_authorized() def update_picking_lines(self, **kw): picking_id = int(kw.get('picking_id')) lines_str = kw.get('lines', '[]') try: updates = json.loads(lines_str) except: updates = [] picking = request.env['stock.picking'].sudo().browse(picking_id) if not picking.exists(): return self.response({'status': 'error', 'message': 'Picking not found'}) for line in updates: move = request.env['stock.move.line'].sudo().browse(line['move_id']) if not move.exists(): continue location_id, location_dest_id = self.get_location_locator(line) for move_line in move: move_line.qty_done = line.get('qty_done', move_line.qty_done) if 'source_location' in line: move_line.location_id = location_id if 'dest_location' in line: move_line.location_dest_id = location_dest_id return self.response({'status': 'success', 'message': 'Picking updated'}) @http.route('/api/v1/locator/picking/validate', auth='public', methods=['POST', 'OPTIONS'], csrf=False) @controller.Controller.must_authorized() def validate_picking(self, **kw): try: picking_id = int(kw.get('picking_id')) picking = request.env['stock.picking'].sudo().browse(picking_id) if not picking.exists(): return self.response({ 'status': 'error', 'message': 'Picking not found' }) action = picking.button_validate() backorder = None if isinstance(action, dict) and action.get('res_model') == 'stock.backorder.confirmation': ctx = action.get('context', {}) or {} pick_ids = ctx.get('default_pick_ids') or [] if pick_ids and isinstance(pick_ids[0], (tuple, list)): pick_ids = [p[1] for p in pick_ids] Wizard = request.env['stock.backorder.confirmation'].with_context({ **ctx, "default_pick_ids": pick_ids, "button_validate_picking_ids": pick_ids, }).sudo() wizard = Wizard.create({}) # --- Step 4: Jalankan wizard.process() → Odoo create backorder --- wizard.process() # --- Step 5: Ambil backorder --- backorder = request.env['stock.picking'].sudo().search([ ('backorder_id', '=', picking.id) ], limit=1) # --- FINAL RESPONSE --- return self.response({ 'status': 'success', 'picking_name': picking.name, 'validated': True, 'backorder_created': bool(backorder), 'backorder_name': backorder.name if backorder else None, }) except Exception as e: return self.response({ 'status': 'error', 'message': str(e) }) def get_location_locator(self, line): location_id = request.env['stock.location'].sudo().search([('complete_name', '=', line.get('source_location'))], limit=1).id location_dest_id = request.env['stock.location'].sudo().search([('complete_name', '=', line.get('dest_location'))], limit=1).id return location_id, location_dest_id