diff options
34 files changed, 1370 insertions, 477 deletions
diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py index e1c643e5..6f5a3d44 100644 --- a/indoteknik_api/controllers/api_v1/sale_order.py +++ b/indoteknik_api/controllers/api_v1/sale_order.py @@ -1,8 +1,11 @@ from .. import controller -from odoo import http +from odoo import http, fields from datetime import datetime, timedelta from odoo.http import request import json +import logging + +_logger = logging.getLogger(__name__) class SaleOrder(controller.Controller): @@ -100,7 +103,11 @@ class SaleOrder(controller.Controller): 'site': [], 'limit': ['default:0', 'number'], 'offset': ['default:0', 'number'], - 'context': [] + 'context': [], + 'status': [], + 'sort': [], + 'startDate': [], + 'endDate': [], }) limit = params['value']['limit'] offset = params['value']['offset'] @@ -120,10 +127,19 @@ class SaleOrder(controller.Controller): if params['value']['name']: name = params['value']['name'].replace(' ', '%') - domain += [ + order_lines = request.env['sale.order.line'].search([ + ('order_id.partner_id', 'in', partner_child_ids), '|', - ('name', 'ilike', '%' + name + '%'), - ('partner_purchase_order_name', 'ilike', '%' + name + '%') + ('product_id.name', 'ilike', name), + ('product_id.default_code', 'ilike', name), + ]) + + sale_order_ids_from_lines = order_lines.mapped('order_id.id') + + domain += ['|', '|', + ('name', 'ilike', name), + ('partner_purchase_order_name', 'ilike', name), + ('id', 'in', sale_order_ids_from_lines) ] if params['value']['site']: @@ -132,11 +148,86 @@ class SaleOrder(controller.Controller): ('partner_id.site_id.name', 'ilike', '%' + site + '%') ] + status = params['value'].get('status') + if status: + if status == 'quotation': + domain += [('state', '=', 'draft')] + domain += [('approval_status', '=', False)] + + elif status == 'cancel': + domain += [('state', '=', 'cancel')] + + elif status == 'diproses': + domain += [ + ('state', '=', 'draft'), + ('approval_status', 'in', ['pengajuan1', 'pengajuan2']), + ] + + elif status in ['dikemas', 'dikirim', 'selesai', 'partial']: + domain += [('state', '=', 'sale')] + + elif status == 'all': + domain += [] + + # Sorting + order = None + if params['value']['sort']: + if params['value']['sort'] == 'asc': + order = 'amount_total asc' + elif params['value']['sort'] == 'desc': + order = 'amount_total desc' + + # Filter berdasarkan tanggal order + try: + if params['value']['startDate'] and params['value']['endDate']: + start_date = datetime.strptime(params['value']['startDate'], '%d/%m/%Y').strftime('%Y-%m-%d 00:00:00') + end_date = datetime.strptime(params['value']['endDate'], '%d/%m/%Y').strftime('%Y-%m-%d 23:59:59') + else: + start_date = '2023-01-01 00:00:00' + end_date = fields.Datetime.now().strftime('%Y-%m-%d 23:59:59') + + domain.append(('date_order', '>=', start_date)) + domain.append(('date_order', '<=', end_date)) + + except ValueError: + return self.response(code=400, description="Invalid date format. Use 'DD/MM/YYYY'.") + + + sale_orders = request.env['sale.order'].search( - domain, offset=offset, limit=limit) + domain, order=order) + status = params['value'].get('status') + if status in ['dikemas', 'dikirim', 'selesai', 'partial']: + filtered_orders = [] + for sale_order in sale_orders: + bu_pickings = [ + p for p in sale_order.picking_ids + if p.picking_type_id and p.picking_type_id.id == 29 + ] + total = len(bu_pickings) + done_pickings = [p for p in bu_pickings if p.state == 'done'] + done_with_driver = [p for p in done_pickings if p.sj_return_date] + done_without_driver = [p for p in done_pickings if not p.sj_return_date] + + if status == 'dikemas' and len(done_pickings) == 0: + filtered_orders.append(sale_order) + elif status == 'dikirim' and len(done_pickings) == total and len(done_pickings) > 0 and len(done_without_driver) == total: + filtered_orders.append(sale_order) + elif status == 'selesai' and len(done_pickings) == total and len(done_pickings) > 0 and len(done_with_driver) == total: + filtered_orders.append(sale_order) + elif status == 'partial' and ( + len(done_pickings) != total or + (done_with_driver and done_without_driver) + ): + filtered_orders.append(sale_order) + else: + filtered_orders = sale_orders + + filtered_orders_paginated = filtered_orders[offset: offset + limit] + data = { - 'sale_order_total': request.env['sale.order'].search_count(domain), - 'sale_orders': [request.env['sale.order'].api_v1_single_response(x) for x in sale_orders] + 'sale_order_total': len(filtered_orders), + 'sale_orders': [request.env['sale.order'].api_v1_single_response(x) for x in filtered_orders_paginated] } return self.response(data) @@ -146,7 +237,7 @@ class SaleOrder(controller.Controller): def partner_get_sale_order_detail(self, **kw): params = self.get_request_params(kw, { 'partner_id': ['number'], - 'id': ['number'] + 'id': ['number'], }) if not params['valid']: return self.response(code=400, description=params) @@ -333,8 +424,8 @@ class SaleOrder(controller.Controller): return self.response('Unauthorized') sale_order = request.env['sale.order'].sudo().search_read([('id', '=', id)], ['name']) - pdf, type = request.env['ir.actions.report'].sudo().search([('report_name', '=', 'quotation_so_new')]).render_jasper([id], {}) - # pdf, type = request.env['ir.actions.report'].sudo().search([('report_name', '=', 'indoteknik_custom.report_saleorder_website')])._render_qweb_pdf([id]) + # pdf, type = request.env['ir.actions.report'].sudo().search([('report_name', '=', 'quotation_so_new')]).render_jasper([id], {}) + pdf, type = request.env['ir.actions.report'].sudo().search([('report_name', '=', 'indoteknik_custom.report_saleorder_website')])._render_qweb_pdf([id]) if pdf and len(sale_order) > 0: return rest_api.response_attachment({ @@ -390,138 +481,207 @@ class SaleOrder(controller.Controller): @http.route(PREFIX_PARTNER + 'sale_order/checkout', auth='public', method=['POST', 'OPTIONS'], csrf=False) @controller.Controller.must_authorized() def create_partner_sale_order(self, **kw): - config = request.env['ir.config_parameter'] - product_pricelist_default_discount_id = int(config.get_param('product.pricelist.tier1_v2')) - user_pricelist = request.env.context.get('user_pricelist').id or False + _logger.info("=== START CREATE PARTNER SALE ORDER ===") + + try: + config = request.env['ir.config_parameter'] + product_pricelist_default_discount_id = int(config.get_param('product.pricelist.tier1_v2')) + user_pricelist = request.env.context.get('user_pricelist').id or False + _logger.info( + f"Config - Default Pricelist: {product_pricelist_default_discount_id}, User Pricelist: {user_pricelist}") + + params = self.get_request_params(kw, { + 'user_id': ['number'], + 'partner_id': ['number'], + 'partner_shipping_id': ['required', 'number'], + 'partner_invoice_id': ['required', 'number'], + 'order_line': ['required', 'default:[]'], + 'po_number': [], + 'po_file': [], + 'type': [], + 'delivery_amount': ['number', 'default:0'], + 'carrier_id': [], + 'delivery_service_type': [], + 'flash_sale': ['boolean'], + 'note_website': [], + 'voucher': [], + 'source': [], + 'estimated_arrival_days': ['number', 'default:0'], + 'estimated_arrival_days_start': ['number', 'default:0'] + }) - params = self.get_request_params(kw, { - 'user_id': ['number'], - 'partner_id': ['number'], - 'partner_shipping_id': ['required', 'number'], - 'partner_invoice_id': ['required', 'number'], - 'order_line': ['required', 'default:[]'], - 'po_number': [], - 'po_file': [], - 'type': [], - 'delivery_amount': ['number', 'default:0'], - 'carrier_id': [], - 'delivery_service_type': [], - 'flash_sale': ['boolean'], - 'note_website': [], - 'voucher': [], - 'source': [], - 'estimated_arrival_days': ['number', 'default:0'], - 'estimated_arrival_days_start': ['number', 'default:0'] - }) - - if not params['valid']: - return self.response(code=400, description=params) + _logger.info(f"Raw input params: {kw}") + _logger.info(f"Processed params: {params}") - # Fetch partner details - sales_partner = request.env['res.partner'].browse(params['value']['partner_id']) - partner_invoice = request.env['res.partner'].browse(params['value']['partner_invoice_id']) - main_partner = partner_invoice.get_main_parent() - parameters = { - 'warehouse_id': 8, - 'carrier_id': 1, - 'sales_tax_id': 23, - 'pricelist_id': user_pricelist or product_pricelist_default_discount_id, - 'payment_term_id': 26, - 'team_id': 2, - 'company_id': 1, - 'currency_id': 12, - 'source_id': 59, - 'state': 'draft', - 'picking_policy': 'direct', - 'partner_id': params['value']['partner_id'], - 'partner_shipping_id': params['value']['partner_shipping_id'], - 'real_shipping_id': params['value']['partner_shipping_id'], - 'partner_invoice_id': main_partner.id, - 'real_invoice_id': params['value']['partner_invoice_id'], - 'partner_purchase_order_name': params['value']['po_number'], - 'partner_purchase_order_file': params['value']['po_file'], - 'delivery_amt': params['value']['delivery_amount'] * 1.10, - 'estimated_arrival_days': params['value']['estimated_arrival_days'], - 'estimated_arrival_days_start': params['value']['estimated_arrival_days_start'], - 'shipping_cost_covered': 'customer', - 'shipping_paid_by': 'customer', - 'carrier_id': params['value']['carrier_id'], - 'delivery_service_type': params['value']['delivery_service_type'], - 'flash_sale': params['value']['flash_sale'], - 'note_website': params['value']['note_website'], - 'customer_type': sales_partner.customer_type if sales_partner else 'nonpkp', # Get Customer Type from partner - 'npwp': sales_partner.npwp or '0', # Get NPWP from partner - 'sppkp': sales_partner.sppkp, # Get SPPKP from partner - 'email': sales_partner.email, # Get Email from partner - 'user_id': 11314 # User ID: Boy Revandi - } - - sales_partner = request.env['res.partner'].browse(parameters['partner_id']) - if sales_partner and sales_partner.user_id and sales_partner.user_id.id not in [25]: # 25: System - parameters['user_id'] = sales_partner.user_id.id - - if params['value']['type'] == 'sale_order': - parameters['approval_status'] = 'pengajuan1' - sale_order = request.env['sale.order'].with_context(from_website_checkout=True).create([parameters]) - sale_order.onchange_partner_contact() - - user_id = params['value']['user_id'] - user_cart = request.env['website.user.cart'] - source = params['value']['source'] - carts = user_cart.get_product_by_user(user_id=user_id, selected=True, source=source) - - promotions = [] - for cart in carts: - if cart['cart_type'] == 'product': - order_line = request.env['sale.order.line'].create({ - 'company_id': 1, - 'order_id': sale_order.id, - 'product_id': cart['id'], - 'product_uom_qty': cart['quantity'], - 'product_available_quantity': cart['available_quantity'] - }) - order_line.product_id_change() - order_line.weight = order_line.product_id.weight - order_line.onchange_vendor_id() - order_line.price_unit = cart['price']['price'] - order_line.discount = cart['price']['discount_percentage'] - elif cart['cart_type'] == 'promotion': - promotions.append({ - 'order_id': sale_order.id, - 'program_line_id': cart['id'], - 'quantity': cart['quantity'] - }) - - sale_order._compute_etrts_date() + if not params['valid']: + _logger.error(f"Invalid params: {params}") + return self.response(code=400, description=params) - request.env['sale.order.promotion'].create(promotions) - - if len(promotions) > 0: - sale_order.apply_promotion_program() - sale_order.add_free_product(promotions) - - voucher_code = params['value']['voucher'] - voucher = request.env['voucher'].search([('code', '=', voucher_code),('apply_type', 'in', ['all', 'brand'])], limit=1) - voucher_shipping = request.env['voucher'].search([('code', '=', voucher_code),('apply_type', 'in', ['shipping'])], limit=1) - if voucher and len(promotions) == 0: - sale_order.voucher_id = voucher.id - sale_order.apply_voucher() - - if voucher_shipping and len(promotions) == 0: - sale_order.voucher_shipping_id = voucher_shipping.id - sale_order.apply_voucher_shipping() - - cart_ids = [x['cart_id'] for x in carts] - if sale_order._requires_approval_margin_leader(): #jika ada error tambahkan kondisi if params['value']['type'] == 'sale_order': - sale_order.approval_status = 'pengajuan2' - elif sale_order._requires_approval_margin_manager(): - sale_order.approval_status = 'pengajuan1' - # user_cart.browse(cart_ids).unlink() - sale_order._auto_set_shipping_from_website() - return self.response({ - 'id': sale_order.id, - 'name': sale_order.name - }) + # Fetch partner details + sales_partner = request.env['res.partner'].browse(params['value']['partner_id']) + partner_invoice = request.env['res.partner'].browse(params['value']['partner_invoice_id']) + main_partner = partner_invoice.get_main_parent() + _logger.info( + f"Partner Info - Sales: {sales_partner.id}, Invoice: {partner_invoice.id}, Main: {main_partner.id}") + + parameters = { + 'warehouse_id': 8, + 'carrier_id': 1, + 'sales_tax_id': 23, + 'pricelist_id': user_pricelist or product_pricelist_default_discount_id, + 'payment_term_id': 26, + 'team_id': 2, + 'company_id': 1, + 'currency_id': 12, + 'source_id': 59, + 'state': 'draft', + 'picking_policy': 'direct', + 'partner_id': params['value']['partner_id'], + 'partner_shipping_id': params['value']['partner_shipping_id'], + 'real_shipping_id': params['value']['partner_shipping_id'], + 'partner_invoice_id': main_partner.id, + 'real_invoice_id': params['value']['partner_invoice_id'], + 'partner_purchase_order_name': params['value']['po_number'], + 'partner_purchase_order_file': params['value']['po_file'], + 'delivery_amt': params['value']['delivery_amount'] * 1.10, + 'estimated_arrival_days': params['value']['estimated_arrival_days'], + 'estimated_arrival_days_start': params['value']['estimated_arrival_days_start'], + 'shipping_cost_covered': 'customer', + 'shipping_paid_by': 'customer', + 'carrier_id': params['value']['carrier_id'], + 'delivery_service_type': params['value']['delivery_service_type'], + 'flash_sale': params['value']['flash_sale'], + 'note_website': params['value']['note_website'], + 'customer_type': sales_partner.customer_type if sales_partner else 'nonpkp', + 'npwp': sales_partner.npwp or '0', + 'sppkp': sales_partner.sppkp, + 'email': sales_partner.email, + 'user_id': 11314 + } + _logger.info(f"Order parameters: {parameters}") + + sales_partner = request.env['res.partner'].browse(parameters['partner_id']) + if sales_partner and sales_partner.user_id and sales_partner.user_id.id not in [25]: + parameters['user_id'] = sales_partner.user_id.id + _logger.info(f"Updated user_id from partner: {parameters['user_id']}") + + if params['value']['type'] == 'sale_order': + parameters['approval_status'] = 'pengajuan1' + _logger.info("Setting approval_status to 'pengajuan1'") + + sale_order = request.env['sale.order'].with_context(from_website_checkout=True).create([parameters]) + sale_order.onchange_partner_contact() + _logger.info(f"Created SO: {sale_order.id} - {sale_order.name}") + + user_id = params['value']['user_id'] + user_cart = request.env['website.user.cart'] + source = params['value']['source'] + _logger.info(f"Getting cart for user: {user_id}, source: {source}") + + carts = user_cart.get_product_by_user(user_id=user_id, selected=True, source=source) + _logger.info(f"Found {len(carts)} cart items") + + promotions = [] + for idx, cart in enumerate(carts, 1): + _logger.info(f"\n=== Processing Cart Item {idx}/{len(carts)} ===") + _logger.info(f"Full cart data: {cart}") + + if cart['cart_type'] == 'product': + product = request.env['product.product'].browse(cart['id']) + _logger.info(f"Product: {product.id} - {product.name}") + _logger.info(f"Cart Price Data: {cart['price']}") + + # Determine discount status based on: + # 1. has_flashsale flag from cart data + # 2. discount percentage > 0 + # 3. global flash sale parameter + is_flash_sale_item = cart.get('has_flashsale', False) + discount_percent = float(cart['price'].get('discount_percentage', 0)) + global_flash_sale = params['value'].get('flash_sale', False) + + is_has_disc = False + + # Item is considered discounted if: + # - It's specifically marked as flash sale item, OR + # - It has significant discount (>0%) and not affected by global flash sale + if is_flash_sale_item: + is_has_disc = True + _logger.info("Item is flash sale product - marked as discounted") + elif global_flash_sale: + _logger.info("Global flash sale active but item not eligible - not marked as discounted") + + _logger.info(f"Final is_has_disc: {is_has_disc}") + + order_line = request.env['sale.order.line'].create({ + 'company_id': 1, + 'order_id': sale_order.id, + 'product_id': product.id, + 'product_uom_qty': cart['quantity'], + 'product_available_quantity': cart['available_quantity'], + 'price_unit': cart['price']['price'], + 'discount': discount_percent, + 'is_has_disc': is_has_disc + }) + _logger.info(f"Created order line: {order_line.id}") + + order_line.product_id_change() + order_line.weight = order_line.product_id.weight + order_line.onchange_vendor_id() + _logger.info(f"After onchanges - Price: {order_line.price_unit}, Disc: {order_line.discount}") + + elif cart['cart_type'] == 'promotion': + promotions.append({ + 'order_id': sale_order.id, + 'program_line_id': cart['id'], + 'quantity': cart['quantity'] + }) + _logger.info(f"Added promotion: {cart['id']}") + + _logger.info("Processing promotions...") + sale_order._compute_etrts_date() + request.env['sale.order.promotion'].create(promotions) + + if len(promotions) > 0: + _logger.info(f"Applying {len(promotions)} promotions") + sale_order.apply_promotion_program() + sale_order.add_free_product(promotions) + + voucher_code = params['value']['voucher'] + if voucher_code: + _logger.info(f"Processing voucher: {voucher_code}") + voucher = request.env['voucher'].search( + [('code', '=', voucher_code), ('apply_type', 'in', ['all', 'brand'])], limit=1) + voucher_shipping = request.env['voucher'].search( + [('code', '=', voucher_code), ('apply_type', 'in', ['shipping'])], limit=1) + + if voucher and len(promotions) == 0: + _logger.info("Applying regular voucher") + sale_order.voucher_id = voucher.id + sale_order.apply_voucher() + + if voucher_shipping and len(promotions) == 0: + _logger.info("Applying shipping voucher") + sale_order.voucher_shipping_id = voucher_shipping.id + sale_order.apply_voucher_shipping() + + cart_ids = [x['cart_id'] for x in carts] + if sale_order._requires_approval_margin_leader(): + sale_order.approval_status = 'pengajuan2' + _logger.info("Approval status set to 'pengajuan2'") + elif sale_order._requires_approval_margin_manager(): + sale_order.approval_status = 'pengajuan1' + _logger.info("Approval status set to 'pengajuan1'") + + sale_order._auto_set_shipping_from_website() + _logger.info("=== END CREATE PARTNER SALE ORDER ===") + return self.response({ + 'id': sale_order.id, + 'name': sale_order.name + }) + + except Exception as e: + _logger.error(f"Error in create_partner_sale_order: {str(e)}", exc_info=True) + return self.response(code=500, description=str(e)) @http.route(PREFIX_PARTNER + 'sale-order/<id>/awb', auth='public', methods=['GET', 'OPTIONS']) @controller.Controller.must_authorized(private=True, private_key='partner_id') diff --git a/indoteknik_api/controllers/api_v1/stock_picking.py b/indoteknik_api/controllers/api_v1/stock_picking.py index 85b0fbba..762e17c5 100644 --- a/indoteknik_api/controllers/api_v1/stock_picking.py +++ b/indoteknik_api/controllers/api_v1/stock_picking.py @@ -125,28 +125,33 @@ class StockPicking(controller.Controller): @http.route(prefix + 'stock-picking/<scanid>/documentation', auth='public', methods=['PUT', 'OPTIONS'], csrf=False) @controller.Controller.must_authorized() def write_partner_stock_picking_documentation(self, **kw): - scanid = int(kw.get('scanid', 0)) + scanid = kw.get('scanid', '').strip() sj_document = kw.get('sj_document', False) paket_document = kw.get('paket_document', False) - params = {'sj_documentation': sj_document, - 'paket_documentation': paket_document, - 'driver_arrival_date': datetime.utcnow(), - } + params = { + 'sj_documentation': sj_document, + 'paket_documentation': paket_document, + 'driver_arrival_date': datetime.utcnow(), + } - picking_data = request.env['stock.picking'].search([('id', '=', scanid)], limit=1) + picking_data = False + if scanid.isdigit() and int(scanid) < 2147483647: + picking_data = request.env['stock.picking'].search([('id', '=', int(scanid))], limit=1) if not picking_data: picking_data = request.env['stock.picking'].search([('picking_code', '=', scanid)], limit=1) if not picking_data: return self.response(code=404, description='picking not found') + 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').") diff --git a/indoteknik_api/controllers/api_v1/voucher.py b/indoteknik_api/controllers/api_v1/voucher.py index 9ffeeace..0338360b 100644 --- a/indoteknik_api/controllers/api_v1/voucher.py +++ b/indoteknik_api/controllers/api_v1/voucher.py @@ -74,7 +74,7 @@ class Voucher(controller.Controller): partner_voucher_orders = [] for order in voucher.order_ids: - if order.partner_id.id == user.partner_id.id: + if order.partner_id.id == user.partner_id.id and order.state != 'cancel' and (order.payment_status or order.payment_status is None): partner_voucher_orders.append(order) if voucher.limit_user > 0 and len(partner_voucher_orders) >= voucher.limit_user: diff --git a/indoteknik_api/models/sale_order.py b/indoteknik_api/models/sale_order.py index 45461974..0561043b 100644 --- a/indoteknik_api/models/sale_order.py +++ b/indoteknik_api/models/sale_order.py @@ -1,4 +1,5 @@ from odoo import models +from datetime import datetime class SaleOrder(models.Model): @@ -20,12 +21,14 @@ class SaleOrder(models.Model): 'amount_untaxed': sale_order.amount_untaxed, 'amount_tax': sale_order.amount_tax, 'amount_total': sale_order.grand_total, + 'amount_discount': sale_order.amount_voucher_shipping_disc, 'purchase_order_name': sale_order.partner_purchase_order_name or sale_order.client_order_ref, 'purchase_order_file': True if sale_order.partner_purchase_order_file else False, 'invoice_count': sale_order.invoice_count, 'status': 'draft', 'approval_step': APPROVAL_STEP[sale_order.web_approval] if sale_order.web_approval else 0, 'date_order': self.env['rest.api'].datetime_to_str(sale_order.date_order, '%d/%m/%Y %H:%M:%S'), + 'payment_type': sale_order.payment_type, 'pickings': [] } for picking in sale_order.picking_ids: @@ -49,29 +52,32 @@ class SaleOrder(models.Model): }) if sale_order.state == 'cancel': data['status'] = 'cancel' - if sale_order.state in ['draft', 'sent']: + if sale_order.state == 'draft' and sale_order.approval_status == False: data['status'] = 'draft' - if sale_order.is_continue_transaction: - data['status'] = 'waiting' - if sale_order.approval_status in ['pengajuan1', 'pengajuan2']: - data['status'] = 'waiting' - if sale_order.state == 'sale': - data['status'] = 'sale' - picking_count = { - 'assigned': 0, - 'done': 0, - } - for picking in sale_order.picking_ids: - if picking.state in ['confirmed', 'assigned']: - picking_count['assigned'] += 1 - if picking.state == 'done': - picking_count['done'] += 1 - if picking_count['done'] > 0: + if sale_order.state == 'draft' and sale_order.approval_status in ['pengajuan1', 'pengajuan2']: + data['status'] = 'waiting' + + + if sale_order.state == 'sale': + bu_pickings = [ + p for p in sale_order.picking_ids + if p.picking_type_id and p.picking_type_id.id == 29 + ] + + # Hitung status masing-masing picking + total = len(bu_pickings) + done_pickings = [p for p in bu_pickings if p.state == 'done'] + done_with_driver = [p for p in done_pickings if p.sj_return_date] + done_without_driver = [p for p in done_pickings if not p.sj_return_date] + + if len(done_pickings) == 0: + data['status'] = 'sale' + elif len(done_pickings) == total and len(done_pickings) > 0 and len(done_with_driver) == total: + data['status'] = 'done' + elif len(done_pickings) == total and len(done_pickings) > 0 and len(done_without_driver) == total: data['status'] = 'shipping' - if picking_count['assigned'] > 0: - data['status'] = 'partial_shipping' - if sale_order.state == 'done': - data['status'] = 'done' + else: + data['status'] = 'partial_shipping' res_users = self.env['res.users'] if context: @@ -116,11 +122,28 @@ class SaleOrder(models.Model): data.update(data_with_detail) else: data_with_detail = { + 'products': [], 'address': { 'customer': res_users.api_address_response(sale_order.partner_id), } } data.update(data_with_detail) + for line in sale_order.order_line: + product = self.env['product.product'].api_single_response(line.product_id) + product['price'] = { + 'price': line.price_unit, + 'discount_percentage': line.discount, + 'price_discount': line.price_reduce_taxexcl, + 'subtotal': line.price_subtotal + } + product['quantity'] = line.product_uom_qty + product['available_quantity'] = line.product_available_quantity + for data_v2 in sale_order.fulfillment_line_v2: + product_v2 = self.env['product.product'].api_single_response(data_v2.product_id) + if product['id'] == product_v2['id']: + product['so_qty'] = data_v2.so_qty + product['reserved_stock_qty'] = data_v2.reserved_stock_qty + data_with_detail['products'].append(product) return data diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index 109cc90a..39995b21 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -178,6 +178,7 @@ # 'views/tukar_guling_return_views.xml' 'views/tukar_guling_po.xml', # 'views/refund_sale_order.xml', + 'views/update_date_planned_po_wizard_view.xml', ], 'demo': [], 'css': [], diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index 87310614..930e60e7 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -157,3 +157,4 @@ from . import refund_sale_order from . import down_payment from . import tukar_guling from . import tukar_guling_po +from . import update_date_planned_po_wizard
\ No newline at end of file diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 1a6fad1c..fd08ed60 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -94,6 +94,31 @@ class AccountMove(models.Model): compute='_compute_has_refund_so', ) + payment_date = fields.Date(string="Payment Date", compute='_compute_payment_date') + partial_payment = fields.Float(string="Partial Payment", compute='compute_partial_payment') + + def compute_partial_payment(self): + for move in self: + if move.amount_total_signed > 0 and move.amount_residual_signed > 0 and move.payment_state == 'partial': + move.partial_payment = move.amount_total_signed - move.amount_residual_signed + else: + move.partial_payment = 0 + + def _compute_payment_date(self): + for move in self: + accountPayment = self.env['account.payment'] + + payment = accountPayment.search([]).filtered( + lambda p: move.id in p.reconciled_invoice_ids.ids + ) + + if payment: + move.payment_date = payment[0].date + elif move.reklas_misc_id: + move.payment_date = move.reklas_misc_id.date + else: + move.payment_date = False + # def name_get(self): # result = [] # for move in self: @@ -109,102 +134,175 @@ 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""" - # <tr> - # <td>{inv.name}</td> - # <td>{fields.Date.to_string(inv.invoice_date) or '-'}</td> - # <td>{fields.Date.to_string(inv.invoice_date_due) or '-'}</td> - # <td>{days_to_due}</td> - # <td>{formatLang(self.env, inv.amount_total, currency_obj=inv.currency_id)}</td> - # <td>{inv.ref or '-'}</td> - # </tr> - # """ - - # subject = f"Reminder Invoice Due - {partner.name}" - # body_html = re.sub( - # r"<tbody[^>]*>.*?</tbody>", - # f"<tbody>{invoice_table_rows}</tbody>", - # 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), + ] + + # --- TESTING --- + # partner = self.env['res.partner'].search([('name', 'ilike', 'DIRGANTARA YUDHA ARTHA')], 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), + # ]) + + 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), + ]) + _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("Tidak ada invoice yang due") + return + + invoice_group = {} + for inv in invoices: + dtd = (inv.invoice_date_due - today).days if inv.invoice_date_due else 0 + invoice_group.setdefault((inv.partner_id, dtd), []).append(inv) + + template = self.env.ref('indoteknik_custom.mail_template_invoice_due_reminder') + + for (partner, dtd), invs in invoice_group.items(): + # Ambil child contact yang di-checklist reminder_invoices + reminder_contacts = self.env['res.partner'].search([ + ('parent_id', '=', partner.id), + ('reminder_invoices', '=', True), + ('email', '!=', False), + ]) + _logger.info(f"Email Reminder Child {reminder_contacts}") + + if not reminder_contacts: + _logger.info(f"Partner {partner.name} tidak memiliki email yang sudah ceklis reminder") + continue + + emails = list(filter(None, [partner.email])) + reminder_contacts.mapped('email') + if not emails: + _logger.info(f"Partner {partner.name} tidak memiliki email yang bisa dikirimi") + continue + + email_to = ",".join(emails) + _logger.info(f"Email tujuan: {email_to}") + + 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""" + <tr> + <td>{inv.partner_id.name}</td> + <td>{inv.purchase_order_id.name or '-'}</td> + <td>{inv.name}</td> + <td>{fields.Date.to_string(inv.invoice_date) or '-'}</td> + <td>{fields.Date.to_string(inv.invoice_date_due) or '-'}</td> + <td>{formatLang(self.env, inv.amount_total, currency_obj=inv.currency_id)}</td> + <td>{inv.invoice_payment_term_id.name or '-'}</td> + <td>{days_to_due}</td> + </tr> + """ + + days_to_due_message = "" + closing_message = "" + if dtd < 0: + days_to_due_message = ( + f"Kami ingin mengingatkan bahwa tagihan anda akan jatuh tempo dalam {abs(dtd)} hari ke depan, " + "dengan rincian sebagai berikut:" + ) + closing_message = ( + "Kami mengharapkan pembayaran dapat dilakukan tepat waktu untuk mendukung kelancaran " + "hubungan kerja sama yang baik antara kedua belah pihak.<br/>" + "Mohon konfirmasi apabila pembayaran telah dijadwalkan. " + "Terima kasih atas perhatian dan kerja samanya." + ) + + if dtd == 0: + days_to_due_message = ( + "Kami ingin mengingatkan bahwa tagihan anda telah memasuki tanggal jatuh tempo pada hari ini, " + "dengan rincian sebagai berikut:" + ) + closing_message = ( + "Mohon kesediaannya untuk segera melakukan pembayaran tepat waktu guna menghindari status " + "keterlambatan dan menjaga kelancaran hubungan kerja sama yang telah terjalin dengan baik.<br/>" + "Apabila pembayaran telah dijadwalkan atau diproses, mohon dapat dikonfirmasi kepada kami. " + "Terima kasih atas perhatian dan kerja samanya." + ) + + if dtd > 0: + days_to_due_message = ( + f"Kami ingin mengingatkan bahwa tagihan anda telah jatuh tempo selama {dtd} hari, " + "dengan rincian sebagai berikut:" + ) + closing_message = ( + "Mohon kesediaan Bapak/Ibu untuk segera melakukan pembayaran guna menghindari keterlambatan " + "dan menjaga kelancaran kerja sama yang telah terjalin dengan baik.<br/>" + "Apabila pembayaran sudah dilakukan, mohon konfirmasi dan lampirkan bukti transfer agar dapat kami proses lebih lanjut. " + "Terima kasih atas perhatian dan kerja samanya." + ) + + body_html = re.sub( + r"<tbody[^>]*>.*?</tbody>", + f"<tbody>{invoice_table_rows}</tbody>", + template.body_html, + flags=re.DOTALL + ).replace('${object.name}', partner.name) \ + .replace('${object.partner_id.name}', partner.name) \ + .replace('${days_to_due_message}', days_to_due_message) \ + .replace('${closing_message}', closing_message) + + cc_list = [ + 'finance@indoteknik.co.id', + 'akbar@indoteknik.co.id', + 'stephan@indoteknik.co.id', + 'darren@indoteknik.co.id' + ] + sales_email = invs[0].invoice_user_id.partner_id.email if invs[0].invoice_user_id else None + if sales_email and sales_email not in cc_list: + cc_list.append(sales_email) + + # Siapkan email values + values = { + 'subject': f"Reminder Invoice Due - {partner.name}", + # 'email_to': 'andrifebriyadiputra@gmail.com', + 'email_to': email_to, + 'email_from': 'finance@indoteknik.co.id', + 'email_cc': ",".join(cc_list), + 'body_html': body_html, + 'reply_to': 'finance@indoteknik.co.id', + } + + _logger.info(f"Mengirim email ke: {values['email_to']} CC: {values['email_cc']}") + template.send_mail(invs[0].id, force_send=True, email_values=values) + + # Post ke chatter + user_system = self.env['res.users'].browse(25) + system_id = user_system.partner_id.id if user_system else False + + for inv in invs: + inv.message_post( + subject=values['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 (dtd = {dtd})") @api.onchange('invoice_date') diff --git a/indoteknik_custom/models/account_move_due_extension.py b/indoteknik_custom/models/account_move_due_extension.py index 4a3f40e2..d354e3e3 100644 --- a/indoteknik_custom/models/account_move_due_extension.py +++ b/indoteknik_custom/models/account_move_due_extension.py @@ -33,6 +33,7 @@ class DueExtension(models.Model): counter = fields.Integer(string="Counter", compute='_compute_counter') approve_by = fields.Many2one('res.users', string="Approve By", readonly=True) date_approve = fields.Datetime(string="Date Approve", readonly=True) + def _compute_counter(self): for due in self: due.counter = due.partner_id.counter @@ -102,6 +103,14 @@ class DueExtension(models.Model): self.date_approve = datetime.utcnow() template = self.env.ref('indoteknik_custom.mail_template_due_extension_approve') template.send_mail(self.id, force_send=True) + return { + 'type': 'ir.actions.act_window', + 'res_model': 'sale.order', + 'view_mode': 'form', + 'res_id': self.order_id.id, + 'views': [(False, 'form')], + 'target': 'current', + } def generate_due_line(self): partners = self.partner_id.get_child_ids() diff --git a/indoteknik_custom/models/approval_payment_term.py b/indoteknik_custom/models/approval_payment_term.py index 6c857b45..4cf9a4c8 100644 --- a/indoteknik_custom/models/approval_payment_term.py +++ b/indoteknik_custom/models/approval_payment_term.py @@ -39,6 +39,7 @@ class ApprovalPaymentTerm(models.Model): ('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) + reject_reason = fields.Text('Reject Reason', tracking=True) sale_order_ids = fields.Many2many( 'sale.order', string='Sale Orders', diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py index 26b5df37..6d538b83 100644 --- a/indoteknik_custom/models/commision.py +++ b/indoteknik_custom/models/commision.py @@ -208,16 +208,15 @@ class CustomerCommision(models.Model): ('pending', 'Pending'), ('payment', 'Payment'), ], string='Payment Status', copy=False, readonly=True, tracking=3, default='pending') - note_finnance = fields.Text('Notes Finnance') + note_finnance = fields.Text('Notes Finance') reason_reject = fields.Char(string='Reason Reaject', tracking=True, track_visibility='onchange') approved_by = fields.Char(string='Approved By', tracking=True, track_visibility='always') grouped_so_number = fields.Char(string='Group SO Number', compute='_compute_grouped_numbers') grouped_invoice_number = fields.Char(string='Group Invoice Number', compute='_compute_grouped_numbers') - sales_id = fields.Many2one('res.users', string="Sales", tracking=True, default=lambda self: self.env.user, - domain=lambda self: [ - ('groups_id', 'in', self.env.ref('sales_team.group_sale_salesman').id)]) + sales_id = fields.Many2one('res.users', string="Sales", tracking=True, required=True, + domain=[('groups_id', 'in', [94]),('id', '!=', 15710)]) date_approved_sales = fields.Datetime(string="Date Approved Sales", tracking=True) date_approved_marketing = fields.Datetime(string="Date Approved Marketing", tracking=True) @@ -400,6 +399,27 @@ class CustomerCommision(models.Model): # result = super(CustomerCommision, self).create(vals) # return result + def _fill_note_finance(self): + for rec in self: + fee_percent = rec.commision_percent or 0.0 + dpp = rec.total_dpp or 0.0 + + fee = dpp * fee_percent / 100 + pph21 = 0.5 * fee * 0.05 + fee_net = fee - pph21 + rec.note_finnance = ( + "Kelengkapan data penerima fee sudah lengkap (NPWP dan KTP)\n" + f"Perhitungan Fee ({fee_percent:.0f}%) dari nilai DPP pada Invoice terlampir sudah\n" + f"sesuai yaitu Rp {fee:,.0f}\n" + "Sesuai PMK No. 168 tahun 2023, komisi fee dikenakan PPH 21\n" + "sebesar :\n" + f"= 50% x Penghasilan Bruto x 5%\n" + f"= 50% x Rp {fee:,.0f} x 5%\n" + f"= Rp {pph21:,.0f}\n" + "Sehingga fee bersih sebesar\n" + f"= Rp {fee:,.0f} - Rp {pph21:,.0f}\n" + f"= Rp {fee_net:,.0f}" + ) def action_confirm_customer_commision(self): jakarta_tz = pytz.timezone('Asia/Jakarta') now = datetime.now(jakarta_tz) @@ -426,6 +446,8 @@ class CustomerCommision(models.Model): elif self.status == 'pengajuan4' and self.env.user.id == 1272: for line in self.commision_lines: line.invoice_id.is_customer_commision = True + if self.commision_type == 'fee': + self._fill_note_finance() self.status = 'approved' self.approved_by = (self.approved_by + ', ' if self.approved_by else '') + self.env.user.name self.date_approved_accounting = now_naive diff --git a/indoteknik_custom/models/cust_commision.py b/indoteknik_custom/models/cust_commision.py index c3105cfd..05c68935 100644 --- a/indoteknik_custom/models/cust_commision.py +++ b/indoteknik_custom/models/cust_commision.py @@ -34,4 +34,3 @@ class CustCommision(models.Model): for rec in duplicate_partner: if self.commision_type == rec.commision_type: raise UserError('Partner already exists') -
\ No newline at end of file diff --git a/indoteknik_custom/models/mrp_production.py b/indoteknik_custom/models/mrp_production.py index 7977bdf7..91da0597 100644 --- a/indoteknik_custom/models/mrp_production.py +++ b/indoteknik_custom/models/mrp_production.py @@ -156,7 +156,7 @@ class MrpProduction(models.Model): 'order_id': new_po.id }]) - new_po.button_confirm() + # new_po.button_confirm() self.is_po = True @@ -247,7 +247,7 @@ class CheckBomProduct(models.Model): @api.constrains('production_id', 'product_id') def _check_product_bom_validation(self): for record in self: - if record.production_id.sale_order.state not in ['sale', 'done']: + if record.production_id.sale_order and record.production_id.sale_order.state not in ['sale', 'done']: raise UserError(( "SO harus diconfirm terlebih dahulu." )) @@ -273,13 +273,13 @@ class CheckBomProduct(models.Model): if existing_lines: total_quantity = sum(existing_lines.mapped('quantity')) - if total_quantity < total_qty_in_moves: + if total_quantity > total_qty_in_moves: raise UserError(( "Quantity Product '%s' kurang dari quantity demand." ) % (record.product_id.display_name)) else: # Check if the quantity exceeds the allowed total - if record.quantity < total_qty_in_moves: + if record.quantity > total_qty_in_moves: raise UserError(( "Quantity Product '%s' kurang dari quantity demand." ) % (record.product_id.display_name)) diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 45134939..103a9131 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -92,6 +92,11 @@ class PurchaseOrder(models.Model): is_cab_visible = fields.Boolean(string='Tampilkan Tombol CAB', compute='_compute_is_cab_visible') + reason_change_date_planned = fields.Selection([ + ('delay', 'Delay By Vendor'), + ('urgent', 'Urgent Delivery'), + ], string='Reason Change Date Planned', tracking=True) + # picking_ids = fields.One2many('stock.picking', 'purchase_id', string='Pickings') bu_related_count = fields.Integer( @@ -100,9 +105,68 @@ class PurchaseOrder(models.Model): ) manufacturing_id = fields.Many2one('mrp.production', string='Manufacturing Orders') + complete_bu_in_count = fields.Integer( + string="Complete BU In Count", + compute='_compute_complete_bu_in_count' + ) + + def _compute_complete_bu_in_count(self): + for order in self: + if order.state not in ['done', 'cancel']: + order.complete_bu_in_count = 1 + else: + relevant_pickings = order.picking_ids.filtered( + lambda p: p.state != 'done' + and p.state != 'cancel' + and p.picking_type_code == 'incoming' + and p.origin == order.name + and p.name.startswith('BU/IN') + ) + order.complete_bu_in_count = len(relevant_pickings) + def _has_vcm(self): if self.id: self.vcm_id = self.env['tukar.guling.po'].search([('origin', '=', self.name)], limit=1) + + @api.depends('order_line.date_planned') + def _compute_date_planned(self): + """ date_planned = the earliest date_planned across all order lines. """ + for order in self: + order.date_planned = False + + @api.constrains('date_planned') + def constrains_date_planned(self): + for rec in self: + if not self.env.user.has_group('indoteknik_custom.group_role_purchasing'): + raise ValidationError("Hanya dapat diisi oleh Purchasing") + + base_bu = self.env['stock.picking'].search([ + ('name', 'ilike', 'BU/'), + ('origin', 'ilike', rec.name), + ('group_id', '=', rec.group_id.id), + ('state', 'not in', ['cancel','done']) + ]) + + for bu in base_bu: + bu.write({ + 'scheduled_date': rec.date_planned, + 'reason_change_date_planned': rec.reason_change_date_planned + }) + + rec.sync_date_planned_to_so() + + def sync_date_planned_to_so(self): + for line in self.order_sales_match_line: + other_sales_match = self.env['purchase.order.sales.match'].search([ + # ('product_id', '=', line.product_id.id), + ('sale_id', '=', line.sale_id.id), + # ('sale_line_id', '=', line.sale_line_id.id) + ]) + + dates = [d for d in other_sales_match.mapped('purchase_order_id.date_planned') if d] + if dates: + date_planned = max(dates) + line.sale_id.write({'et_products': date_planned, 'reason_change_date_planned': line.purchase_order_id.reason_change_date_planned}) @api.depends('name') def _compute_bu_related_count(self): @@ -677,13 +741,6 @@ class PurchaseOrder(models.Model): 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) - def add_product_to_pricelist(self): i = 0 for line in self.order_line: @@ -766,16 +823,16 @@ class PurchaseOrder(models.Model): """ purchase_pricelist.message_post(body=message, subtype_id=self.env.ref("mail.mt_note").id) - def _compute_date_planned(self): - for order in self: - if order.date_approve: - leadtime = order.partner_id.leadtime - current_time = order.date_approve - delta_time = current_time + timedelta(days=leadtime) - delta_time = delta_time.strftime('%Y-%m-%d %H:%M:%S') - order.date_planned = delta_time - else: - order.date_planned = False + # def _compute_date_planned(self): + # for order in self: + # if order.date_approve: + # leadtime = order.partner_id.leadtime + # current_time = order.date_approve + # delta_time = current_time + timedelta(days=leadtime) + # delta_time = delta_time.strftime('%Y-%m-%d %H:%M:%S') + # order.date_planned = delta_time + # else: + # order.date_planned = False def action_create_invoice(self): res = super(PurchaseOrder, self).action_create_invoice() @@ -959,6 +1016,9 @@ 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 not self.date_planned: + raise UserError("Receipt Date harus diisi") if self.total_percent_margin < self.total_so_percent_margin: self.env.user.notify_danger( @@ -975,7 +1035,7 @@ class PurchaseOrder(models.Model): # ) if not self.from_apo: - 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: + 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 di confirm oleh Purchasing Manager") send_email = False @@ -1010,10 +1070,10 @@ class PurchaseOrder(models.Model): self.approve_by = self.env.user.id # override date planned added with two days - leadtime = self.partner_id.leadtime - delta_time = current_time + timedelta(days=leadtime) - delta_time = delta_time.strftime('%Y-%m-%d %H:%M:%S') - self.date_planned = delta_time + # leadtime = self.partner_id.leadtime + # delta_time = current_time + timedelta(days=leadtime) + # delta_time = delta_time.strftime('%Y-%m-%d %H:%M:%S') + # self.date_planned = delta_time self.date_deadline_ref_date_planned() self.unlink_purchasing_job_state() @@ -1023,9 +1083,9 @@ class PurchaseOrder(models.Model): # 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() + # if len(self) == 1: + # _logger.info("Redirecting ke BU") + # return self.action_view_related_bu() return res @@ -1391,6 +1451,20 @@ class PurchaseOrder(models.Model): # Tambahkan pemanggilan method untuk handle pricelist system update self._handle_pricelist_system_update(vals) return res + + def action_open_change_date_wizard(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'res_model': 'change.date.planned.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'default_purchase_id': self.id, + 'default_new_date_planned': self.date_planned, + } + } + def _handle_pricelist_system_update(self, vals): if 'order_line' in vals or any(key in vals for key in ['state', 'approval_status']): @@ -1479,4 +1553,32 @@ class PurchaseOrderUnlockWizard(models.TransientModel): order.approval_status_unlock = 'pengajuanFinance' return {'type': 'ir.actions.act_window_close'} +class ChangeDatePlannedWizard(models.TransientModel): + _name = 'change.date.planned.wizard' + _description = 'Change Date Planned Wizard' + + purchase_id = fields.Many2one('purchase.order', string="Purchase Order", required=True) + new_date_planned = fields.Datetime(string="New Date Planned") # <- harus DTTM biar match + old_date_planned = fields.Datetime(string="Current Planned Date", related='purchase_id.date_planned', readonly=True) + reason = fields.Selection([ + ('delay', 'Delay By Vendor'), + ('urgent', 'Urgent Delivery'), + ], string='Reason') + date_changed = fields.Boolean(string="Date Changed", compute="_compute_date_changed") + + @api.depends('old_date_planned', 'new_date_planned') + def _compute_date_changed(self): + for rec in self: + rec.date_changed = ( + rec.old_date_planned and rec.new_date_planned and + rec.old_date_planned != rec.new_date_planned + ) + + def confirm_change(self): + self.purchase_id.write({ + 'date_planned': self.new_date_planned, + 'reason_change_date_planned': self.reason, + }) + + diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index 236df16f..f260f58e 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -27,6 +27,11 @@ class ResPartner(models.Model): # Referensi supplier_ids = fields.Many2many('user.pengajuan.tempo.line', string="Suppliers") + reminder_invoices = fields.Boolean( + string='Reminder Invoice?', + help='Centang jika kontak ini harus menerima email pengingat invoice.', default=False + ) + # informasi perusahaan name_tempo = fields.Many2one('res.partner', string='Nama Perusahaan',tracking=True) industry_id_tempo = fields.Many2one('res.partner.industry', 'Customer Industry', readonly=True) diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 7be0e8ff..e71e3830 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -163,7 +163,7 @@ class SaleOrder(models.Model): carrier_id = fields.Many2one('delivery.carrier', string='Shipping Method', tracking=3) have_visit_service = fields.Boolean(string='Have Visit Service', compute='_have_visit_service', help='To compute is customer get visit service') - delivery_amt = fields.Float(string='Delivery Amt', copy=False) + delivery_amt = fields.Float(string='Delivery Amt', copy=False, tracking=True) shipping_cost_covered = fields.Selection([ ('indoteknik', 'Indoteknik'), ('customer', 'Customer') @@ -350,7 +350,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', compute='_compute_et_products', help="Leadtime produk berdasarkan SLA vendor, tanpa logistik.") + et_products = fields.Datetime(string='ET Products', help="Leadtime produk berdasarkan SLA vendor, tanpa logistik.", tracking=True) eta_date_reserved = fields.Datetime( string="Date Reserved", @@ -381,6 +381,11 @@ class SaleOrder(models.Model): if self.id: self.ccm_id = self.env['tukar.guling'].search([('origin', 'ilike', self.name)], limit=1) + reason_change_date_planned = fields.Selection([ + ('delay', 'Delay By Vendor'), + ('urgent', 'Urgent Delivery'), + ], string='Reason Change Date Planned', tracking=True) + @api.depends('order_line.product_id', 'date_order') def _compute_et_products(self): jakarta = pytz.timezone("Asia/Jakarta") @@ -2154,7 +2159,12 @@ class SaleOrder(models.Model): # if order.validate_partner_invoice_due(): # return self._create_notification_action('Notification', # 'Terdapat invoice yang telah melewati batas waktu, mohon perbarui pada dokumen Due Extension') - + + if not order.with_context(ask_approval=True)._is_request_to_own_team_leader(): + return self._create_notification_action( + 'Peringatan', + 'Hanya bisa konfirmasi SO tim Anda.' + ) if order._requires_approval_margin_leader(): order.approval_status = 'pengajuan2' return self._create_approval_notification('Pimpinan') @@ -2164,6 +2174,12 @@ class SaleOrder(models.Model): self.check_limit_so_to_invoice() order.approval_status = 'pengajuan1' return self._create_approval_notification('Sales Manager') + elif order._requires_approval_team_sales(): + self.check_product_bom() + self.check_credit_limit() + self.check_limit_so_to_invoice() + order.approval_status = 'approved' + return self._create_approval_notification('Team Sales') raise UserError("Bisa langsung Confirm") @@ -2380,12 +2396,20 @@ class SaleOrder(models.Model): return self._create_notification_action('Notification', 'Terdapat invoice yang telah melewati batas waktu, mohon perbarui pada dokumen Due Extension') + if not order._is_request_to_own_team_leader(): + return self._create_notification_action( + 'Warning', + 'Hanya bisa konfirmasi SO tim Anda.' + ) if order._requires_approval_margin_leader(): order.approval_status = 'pengajuan2' return self._create_approval_notification('Pimpinan') elif order._requires_approval_margin_manager(): order.approval_status = 'pengajuan1' return self._create_approval_notification('Sales Manager') + elif order._requires_approval_team_sales(): + order.approval_status = 'approved' + return self._create_approval_notification('Team Sales') order.approval_status = 'approved' order._set_sppkp_npwp_contact() @@ -2486,8 +2510,36 @@ class SaleOrder(models.Model): return self.total_percent_margin <= 15 and not self.env.user.is_leader def _requires_approval_margin_manager(self): - return 15 < self.total_percent_margin <= 24 and not self.env.user.is_sales_manager and not self.env.user.id == 375 and not self.env.user.is_leader - # return self.total_percent_margin >= 15 and not self.env.user.is_leader and not self.env.user.is_sales_manager + return 15 < self.total_percent_margin < 18 and not self.env.user.is_sales_manager and not self.env.user.id == 375 and not self.env.user.is_leader + + def _requires_approval_team_sales(self): + return ( + 18 <= self.total_percent_margin <= 24 + and self.env.user.id not in [11, 9, 375] # Eko, Ade, Putra + and not self.env.user.is_sales_manager + and not self.env.user.is_leader + ) + + + def _is_request_to_own_team_leader(self): + user = self.env.user + + # Pengecualian Pak Akbar & Darren + if user.is_leader or user.is_sales_manager: + return True + + if user.id in (3401, 20, 3988): # admin (fida, nabila, ninda) + return True + + if self.env.context.get("ask_approval") and user.id in (3401, 20, 3988): + return True + + salesperson_id = self.user_id.id + approver_id = user.id + team_leader_id = self.team_id.user_id.id + + return salesperson_id == approver_id or approver_id == team_leader_id + def _create_approval_notification(self, approval_role): title = 'Warning' @@ -3083,6 +3135,24 @@ class SaleOrder(models.Model): except: pass + #payment term vals + if 'payment_term_id' in vals and any( + order.approval_status in ['pengajuan1', 'pengajuan2', 'approved'] for order in self): + raise UserError( + "Payment Term tidak dapat diubah karena Sales Order sedang dalam proses approval atau sudah diapprove.") + + if 'payment_term_id' in vals: + for order in self: + partner = order.partner_id.parent_id or order.partner_id + customer_payment_term = partner.property_payment_term_id + if vals['payment_term_id'] != customer_payment_term.id: + raise UserError( + f"Payment Term berbeda pada Master Data Customer. " + f"Harap ganti ke '{customer_payment_term.name}' " + f"sesuai dengan payment term yang terdaftar pada customer." + ) + + res = super(SaleOrder, self).write(vals) # Update before margin setelah write diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index 5e9fc362..64b9f9bc 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -1,6 +1,10 @@ from odoo import fields, models, api, _ from odoo.exceptions import UserError from datetime import datetime, timedelta +import logging +from odoo.tools.float_utils import float_compare + +_logger = logging.getLogger(__name__) class SaleOrderLine(models.Model): @@ -49,6 +53,9 @@ class SaleOrderLine(models.Model): qty_free_bu = fields.Float(string='Free BU', compute='_get_qty_free_bandengan') desc_updatable = fields.Boolean(string='desc boolean', default=True, compute='_get_desc_updatable') + is_has_disc = fields.Boolean('Flash Sale', default=False) + + def _get_outgoing_incoming_moves(self): outgoing_moves = self.env['stock.move'] incoming_moves = self.env['stock.move'] diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 3e152f10..82f81642 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -303,6 +303,10 @@ class StockPicking(models.Model): 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') + reason_change_date_planned = fields.Selection([ + ('delay', 'Delay By Vendor'), + ('urgent', 'Urgent Delivery'), + ], string='Reason Change Date Planned', tracking=True) def _get_kgx_awb_number(self): """Menggabungkan name dan origin untuk membuat AWB Number""" @@ -806,6 +810,7 @@ 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 = self.biteship_waybill_id + self.biteship_shipping_price = data.get("price", 0.0) waybill_id = self.biteship_waybill_id @@ -1055,16 +1060,23 @@ class StockPicking(models.Model): self.sale_id.date_doc_kirim = self.date_doc_kirim def action_assign(self): - res = super(StockPicking, self).action_assign() - for move in self: - # if not move.sale_id.hold_outgoing and move.location_id.id != 57 and move.location_dest_id.id != 60: - # TODO cant skip hold outgoing cause of not singleton method - current_time = datetime.datetime.utcnow() - move.real_shipping_id = move.sale_id.real_shipping_id - move.date_availability = current_time - # self.check_state_reserve() + if self.env.context.get('default_picking_type_id'): + pickings_to_assign = self.filtered( + lambda p: not (p.sale_id and p.sale_id.hold_outgoing) + ) + else: + pickings_to_assign = self + + res = super(StockPicking, pickings_to_assign).action_assign() + + current_time = datetime.datetime.utcnow() + for picking in pickings_to_assign: + picking.real_shipping_id = picking.sale_id.real_shipping_id + picking.date_availability = current_time + return res + def ask_approval(self): if self.env.user.is_accounting: raise UserError("Bisa langsung Validate") @@ -1380,6 +1392,12 @@ class StockPicking(models.Model): self.send_mail_bills() if 'BU/PUT' in self.name: self.automatic_reserve_product() + + if self.tukar_guling_id: + self.tukar_guling_id.update_doc_state() + elif self.tukar_guling_po_id: + self.tukar_guling_po_id.update_doc_state() + return res def automatic_reserve_product(self): diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 43bc156e..6aedb70e 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -5,7 +5,8 @@ from datetime import datetime _logger = logging.getLogger(__name__) -#TODO + +# TODO # 1. tracking status dokumen BU [X] # 2. ganti nama dokumen # 3. Tracking ketika create dokumen [X] @@ -20,7 +21,7 @@ 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) @@ -31,7 +32,7 @@ class TukarGuling(models.Model): 'tukar_guling_id', string='Transfers' ) - # origin_so = fields.Many2one('sale.order', string='Origin SO') + origin_so = fields.Many2one('sale.order', string='Origin SO', compute='_compute_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( @@ -62,6 +63,7 @@ class TukarGuling(models.Model): ('approval_sales', ' Approval Sales'), ('approval_finance', 'Approval Finance'), ('approval_logistic', 'Approval Logistic'), + ('approved', 'Waiting for Operations'), ('done', 'Done'), ('cancel', 'Canceled') ], default='draft', tracking=True, required=True) @@ -72,6 +74,47 @@ class TukarGuling(models.Model): date_sales = fields.Datetime('Approved Date Sales', tracking=3, readonly=True) date_logistic = fields.Datetime('Approved Date Logistic', tracking=3, readonly=True) + val_inv_opt = fields.Selection([ + ('tanpa_cancel', 'Tanpa Cancel Invoice'), + ('cancel_invoice', 'Cancel Invoice'), + ], tracking=3, string='Invoice Option') + + is_has_invoice = fields.Boolean('Has Invoice?', compute='_compute_is_has_invoice', readonly=True, default=False) + + invoice_id = fields.Many2many('account.move', string='Invoice Ref', readonly=True) + + @api.depends('origin', 'operations') + def _compute_origin_so(self): + for rec in self: + rec.origin_so = False + origin_str = rec.origin or rec.operations.origin + if origin_str: + so = self.env['sale.order'].search([('name', '=', origin_str)], limit=1) + rec.origin_so = so.id if so else False + + @api.depends('origin') + def _compute_is_has_invoice(self): + for rec in self: + invoices = self.env['account.move'].search([ + ('invoice_origin', 'ilike', rec.origin), + ('move_type', '=', 'out_invoice'), # hanya invoice + ('state', 'not in', ['draft', 'cancel']) + ]) + if invoices: + rec.is_has_invoice = True + rec.invoice_id = invoices + else: + rec.is_has_invoice = False + + def set_opt(self): + if not self.val_inv_opt and self.is_has_invoice == True: + raise UserError("Kalau sudah ada invoice Return Invoice Option harus diisi!") + for rec in self: + if rec.val_inv_opt == 'cancel_invoice' and self.is_has_invoice == True: + raise UserError("Tidak bisa mengubah Return karena sudah ada invoice dan belum di cancel.") + elif rec.val_inv_opt == 'tanpa_cancel' and self.is_has_invoice == True: + continue + # @api.onchange('operations') # def get_partner_id(self): # if self.operations and self.operations.partner_id and self.operations.partner_id.name: @@ -98,7 +141,7 @@ 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]: + 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: @@ -110,8 +153,6 @@ class TukarGuling(models.Model): if self.line_ids and from_return_picking: # 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 @@ -143,6 +184,7 @@ class TukarGuling(models.Model): # Set origin dari operations if self.operations.origin: self.origin = self.operations.origin + self.origin_so = self.operations.group_id.id # Auto-populate lines dari move_ids operations lines_data = [] @@ -217,7 +259,6 @@ class TukarGuling(models.Model): self.origin = False - def action_populate_lines(self): """Manual button untuk populate lines - sebagai alternatif""" self.ensure_one() @@ -257,7 +298,7 @@ class TukarGuling(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', + if record.state in ('approval_sales', 'approval_logistic', 'approval_finance', 'approved', 'done') and not record.line_ids: raise ValidationError("Product lines harus diisi sebelum submit atau approve!") @@ -281,36 +322,38 @@ 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 + # def _is_already_returned(self, picking): + # return self.env['stock.picking'].search_count([ + # ('origin', '=', 'Return of %s' % picking.name), + # ('state', '!=', 'cancel') + # ]) > 0 + + # def _check_invoice_on_revisi_so(self): + # for record in self: + # if record.return_type == 'revisi_so' and record.origin: + # invoices = self.env['account.move'].search([ + # ('invoice_origin', 'ilike', record.origin), + # ('state', 'not in', ['draft', 'cancel']) + # ]) + # if invoices: + # raise ValidationError( + # _("Tidak bisa memilih Return Type 'Revisi SO' karena dokumen %s sudah dibuat invoice.") % record.origin + # ) - @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: - invoices = self.env['account.move'].search([ - ('invoice_origin', 'ilike', record.origin), - ('state', 'not in', ['draft', 'cancel']) - ]) - if invoices: - 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 if not vals.get('name') or vals['name'] == 'New': 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'): + if vals.get('operations'): picking = self.env['stock.picking'].browse(vals['operations']) if picking.origin: vals['origin'] = picking.origin + # Find matching SO + so = self.env['sale.order'].search([('name', '=', picking.origin)], limit=1) + if so: + vals['origin_so'] = so.id if picking.partner_id: vals['partner_id'] = picking.partner_id.id @@ -318,6 +361,10 @@ class TukarGuling(models.Model): res.message_post(body=_("CCM Created By %s") % self.env.user.name) return res + 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: default = {} @@ -345,9 +392,9 @@ class TukarGuling(models.Model): def write(self, vals): self.ensure_one() - if self.operations.picking_type_id.id not in [29,30]: + 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() + # self._check_invoice_on_revisi_so() operasi = self.operations.picking_type_id.id tipe = self.return_type pp = vals.get('return_type', tipe) @@ -376,24 +423,33 @@ class TukarGuling(models.Model): # if self.state == 'done': # raise UserError ("Tidak Boleh delete ketika sudahh done") for record in self: - if record.state == 'done': + if record.state == 'approved' or record.state == 'done': 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') + "Tidak bisa hapus pengajuan jika sudah Approved, set ke draft terlebih dahulu jika ingin menghapus") + ongoing_bu = self.picking_ids.filtered(lambda p: p.state != 'approved') for picking in ongoing_bu: picking.action_cancel() return super(TukarGuling, self).unlink() 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 + + # picking_origin = f"Return of {self.operations.name}" + returs = self.env['stock.picking'].search([ + ('tukar_guling_id', '=', self.id), + ]) + + if not returs: + raise UserError("Doc Retrun Not Found") + + return { + 'type': 'ir.actions.act_window', + 'name': 'Delivery Pengajuan Retur SO', + 'res_model': 'stock.picking', + 'view_mode': 'tree,form', + 'domain': [('id', 'in', returs.ids)], + 'target': 'current', + } def action_draft(self): """Reset to draft state""" @@ -434,40 +490,100 @@ 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._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.") 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() + # Cek apakah ada BU/PICK di origin + origin = self.operations.origin + has_bu_pick = self.env['stock.picking'].search_count([ + ('origin', '=', origin), + ('picking_type_id', '=', 30), + ('state', '!=', 'cancel') + ]) > 0 + + if has_bu_pick: + 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() if self.state != 'draft': raise UserError("Submit hanya bisa dilakukan dari Draft.") self.state = 'approval_sales' + def update_doc_state(self): + # OUT tukar guling + if self.operations.picking_type_id.id == 29 and self.return_type == 'tukar_guling': + total_out = self.env['stock.picking'].search_count([ + ('tukar_guling_id', '=', self.id), + ('picking_type_id', '=', 29), + ]) + done_out = self.env['stock.picking'].search_count([ + ('tukar_guling_id', '=', self.id), + ('picking_type_id', '=', 29), + ('state', '=', 'done'), + ]) + if self.state == 'approved' and total_out > 0 and done_out == total_out: + self.state = 'done' + + # OUT revisi SO + elif self.operations.picking_type_id.id == 29 and self.return_type == 'revisi_so': + total_ort = self.env['stock.picking'].search_count([ + ('tukar_guling_id', '=', self.id), + ('picking_type_id', '=', 74), + ]) + done_ort = self.env['stock.picking'].search_count([ + ('tukar_guling_id', '=', self.id), + ('picking_type_id', '=', 74), + ('state', '=', 'done'), + ]) + if self.state == 'approved' and total_ort > 0 and done_ort == total_ort: + self.state = 'done' + + # PICK revisi SO + elif self.operations.picking_type_id.id == 30 and self.return_type == 'revisi_so': + done_ort = self.env['stock.picking'].search([ + ('tukar_guling_id', '=', self.id), + ('picking_type_id', '=', 74), + ('state', '=', 'done'), + ]) + if self.state == 'approved' and done_ort: + self.state = 'done' + else: + raise UserError("Tidak bisa menentukan jenis retur.") + def action_approve(self): self.ensure_one() self._validate_product_lines() - self._check_invoice_on_revisi_so() + # 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 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) + # Cek apakah ada BU/PICK di origin + origin = self.operations.origin + has_bu_pick = self.env['stock.picking'].search_count([ + ('origin', '=', origin), + ('picking_type_id', '=', 30), + ('state', '!=', 'cancel') + ]) > 0 + + if has_bu_pick: + 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") @@ -495,13 +611,15 @@ class TukarGuling(models.Model): 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._check_invoice_on_revisi_so() + rec.set_opt() 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.state = 'approved' rec._create_pickings() rec.date_logistic = now @@ -533,6 +651,23 @@ class TukarGuling(models.Model): def _create_pickings(self): _logger.info("🛠 Starting _create_pickings()") + + def _force_locations(picking, from_loc, to_loc): + picking.write({ + 'location_id': from_loc, + 'location_dest_id': to_loc, + }) + for move in picking.move_lines: + move.write({ + 'location_id': from_loc, + 'location_dest_id': to_loc, + }) + for move_line in move.move_line_ids: + move_line.write({ + 'location_id': from_loc, + 'location_dest_id': to_loc, + }) + for record in self: if not record.operations: raise UserError("BU/OUT dari field operations tidak ditemukan.") @@ -555,36 +690,53 @@ class TukarGuling(models.Model): ### ======== SRT dari BU/OUT ========= srt_return_lines = [] - for prod in mapping_koli.mapped('product_id'): - 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"Move BU/OUT tidak ditemukan untuk produk {prod.display_name}") - srt_return_lines.append((0, 0, { - 'product_id': prod.id, - 'quantity': qty_total, - 'move_id': move.id, - })) - _logger.info(f"📟 SRT line: {prod.display_name} | qty={qty_total}") + if mapping_koli: + for prod in mapping_koli.mapped('product_id'): + 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"Move BU/OUT tidak ditemukan untuk produk {prod.display_name}") + srt_return_lines.append((0, 0, { + 'product_id': prod.id, + 'quantity': qty_total, + 'move_id': move.id, + })) + _logger.info(f"📟 SRT line: {prod.display_name} | qty={qty_total}") + + elif not mapping_koli: + for line in record.line_ids: + move = bu_out.move_lines.filtered(lambda m: m.product_id == line.product_id) + if not move: + raise UserError(f"Move BU/OUT tidak ditemukan untuk produk {line.product_id.display_name}") + srt_return_lines.append((0, 0, { + 'product_id': line.product_id.id, + 'quantity': line.product_uom_qty, + 'move_id': move.id, + })) + _logger.info( + f"📟 SRT line (fallback line_ids): {line.product_id.display_name} | qty={line.product_uom_qty}") srt_picking = None if srt_return_lines: + # Tentukan tujuan lokasi berdasarkan ada/tidaknya mapping_koli + dest_location_id = BU_OUTPUT_LOCATION_ID if mapping_koli else BU_STOCK_LOCATION_ID + 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, + 'default_location_dest_id': dest_location_id, 'from_ui': False, }).create({ 'picking_id': bu_out.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['res_id']) + _force_locations(srt_picking, PARTNER_LOCATION_ID, dest_location_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 @@ -597,12 +749,11 @@ class TukarGuling(models.Model): ### ======== ORT dari BU/PICK ========= ort_pickings = [] 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 + picks_to_return = [record.operations] if is_retur_from_bu_pick else mapping_koli.mapped('pick_id') for pick in picks_to_return: ort_return_lines = [] 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: @@ -613,9 +764,9 @@ class TukarGuling(models.Model): '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}") + _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: @@ -626,7 +777,8 @@ class TukarGuling(models.Model): '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}") + _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({ @@ -637,27 +789,27 @@ class TukarGuling(models.Model): }).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['res_id']) + _force_locations(ort_picking, BU_OUTPUT_LOCATION_ID, BU_STOCK_LOCATION_ID) + ort_picking.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, 'sale_order': record.origin }) + created_returns.append(ort_picking) ort_pickings.append(ort_picking) _logger.info(f"✅ ORT created: {ort_picking.name}") record.message_post( body=f"📦 <b>{ort_picking.name}</b> created by <b>{self.env.user.name}</b> (state: <b>{ort_picking.state}</b>)") - ### ======== Tukar Guling: BU/OUT dan BU/PICK baru ======== + ### ======== BU/PICK & BU/OUT Baru dari SRT/ORT ======== if record.return_type == 'tukar_guling': - # BU/PICK Baru dari ORT for ort_p in ort_pickings: return_lines = [] @@ -683,19 +835,18 @@ class TukarGuling(models.Model): }).create({ 'picking_id': ort_p.id, 'location_id': BU_STOCK_LOCATION_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']) + _force_locations(new_pick, BU_STOCK_LOCATION_ID, BU_OUTPUT_LOCATION_ID) + new_pick.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, 'sale_order': record.origin }) - new_pick.action_assign() # Penting agar bisa trigger check koli + new_pick.action_assign() new_pick.action_confirm() created_returns.append(new_pick) _logger.info(f"✅ BU/PICK Baru dari ORT created: {new_pick.name}") @@ -723,14 +874,13 @@ class TukarGuling(models.Model): }).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']) + _force_locations(new_out, BU_OUTPUT_LOCATION_ID, PARTNER_LOCATION_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 @@ -808,18 +958,17 @@ class StockPicking(models.Model): message = _( "📦 <b>%s</b> Validated by <b>%s</b> Status Changed <b>%s</b> at <b>%s</b>." ) % ( - picking.name, - # picking.picking_type_id.name, - picking.env.user.name, - picking.state, - fields.Datetime.now().strftime("%d/%m/%Y %H:%M") - ) + 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' _description = 'Mapping Koli di Tukar Guling' @@ -830,6 +979,7 @@ class TukarGulingMappingKoli(models.Model): qty_done = fields.Float(string='Qty Done BU PICK') qty_return = fields.Float(string='Qty diretur') sequence = fields.Integer(string='Sequence', default=10) + @api.constrains('qty_return') def _check_qty_return_editable(self): for rec in self: @@ -840,4 +990,4 @@ class TukarGulingMappingKoli(models.Model): 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 + return super(TukarGulingMappingKoli, self).unlink() diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index 14f2cc96..03d7668f 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -49,10 +49,53 @@ class TukarGulingPO(models.Model): ('approval_purchase', 'Approval Purchasing'), ('approval_finance', 'Approval Finance'), ('approval_logistic', 'Approval Logistic'), + ('approved', 'Waiting for Operations'), ('done', 'Done'), ('cancel', 'Cancel'), ], string='Status', default='draft', tracking=3) + val_bil_opt = fields.Selection([ + ('tanpa_cancel', 'Tanpa Cancel Bill'), + ('cancel_bill', 'Cancel Bill'), + ], tracking=3, string='Bill Option') + + is_has_bill = fields.Boolean('Has Bill?', compute='_compute_is_has_bill', readonly=True, default=False) + + bill_id = fields.Many2many('account.move', string='Bill Ref', readonly=True) + origin_po = fields.Many2one('purchase.order', string='Origin PO', compute='_compute_origin_po') + + @api.depends('origin', 'operations') + def _compute_origin_po(self): + for rec in self: + rec.origin_po = False + origin_str = rec.origin or rec.operations.origin + if origin_str: + so = self.env['purchase.order'].search([('name', '=', origin_str)], limit=1) + rec.origin_po = so.id if so else False + + @api.depends('origin') + def _compute_is_has_bill(self): + for rec in self: + bills = self.env['account.move'].search([ + ('invoice_origin', 'ilike', rec.origin), + ('move_type', '=', 'in_invoice'), # hanya vendor bill + ('state', 'not in', ['draft', 'cancel']) + ]) + if bills: + rec.is_has_bill = True + rec.bill_id = bills + else: + rec.is_has_bill = False + + def set_opt(self): + if not self.val_bil_opt and self.is_has_bill == True: + raise UserError("Kalau sudah ada bill Return Bill Option harus diisi!") + for rec in self: + if rec.val_bil_opt == 'cancel_bill' and self.is_has_bill == True: + raise UserError("Tidak bisa mengubah Return karena sudah ada bill dan belum di cancel.") + elif rec.val_bil_opt == 'tanpa_cancel' and self.is_has_bill == True: + continue + @api.model def create(self, vals): # Generate sequence number @@ -73,19 +116,18 @@ class TukarGulingPO(models.Model): return res - @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 - ) + # 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. Harus Cancel Jika ingin melanjutkan") % record.origin + # ) @api.onchange('operations') def _onchange_operations(self): @@ -101,6 +143,7 @@ class TukarGulingPO(models.Model): # Hanya update origin, jangan ubah lines if self.operations.origin: self.origin = self.operations.origin + self.origin_po = self.operations.group_id.id return if from_return_picking: @@ -245,12 +288,12 @@ class TukarGulingPO(models.Model): return True - def _is_already_returned(self, picking): - return self.env['stock.picking'].search_count([ - ('origin', '=', 'Return of %s' % picking.name), - # ('returned_from_id', '=', picking.id), - ('state', 'not in', ['cancel', 'draft']), - ]) > 0 + # def _is_already_returned(self, picking): + # return self.env['stock.picking'].search_count([ + # ('origin', '=', 'Return of %s' % picking.name), + # # ('returned_from_id', '=', picking.id), + # ('state', 'not in', ['cancel', 'draft']), + # ]) > 0 def copy(self, default=None): if default is None: @@ -280,7 +323,7 @@ class TukarGulingPO(models.Model): 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() + # 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': @@ -311,7 +354,7 @@ class TukarGulingPO(models.Model): def unlink(self): for record in self: - if record.state == 'done': + if record.state == 'done' or record.state == 'approved': 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: @@ -320,14 +363,23 @@ class TukarGulingPO(models.Model): 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 + + # picking_origin = f"Return of {self.operations.name}" + returs = self.env['stock.picking'].search([ + ('tukar_guling_po_id', '=', self.id), + ]) + + if not returs: + raise UserError("Doc Retrun Not Found") + + return { + 'type': 'ir.actions.act_window', + 'name': 'Delivery Pengajuan Retur PO', + 'res_model': 'stock.picking', + 'view_mode': 'tree,form', + 'domain': [('id', 'in', returs.ids)], + 'target': 'current', + } def action_draft(self): """Reset to draft state""" @@ -339,7 +391,7 @@ class TukarGulingPO(models.Model): def action_submit(self): self.ensure_one() - self._check_bill_on_revisi_po() + # self._check_bill_on_revisi_po() self._validate_product_lines() self._check_not_allow_tukar_guling_on_bu_input() @@ -365,8 +417,8 @@ class TukarGulingPO(models.Model): 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): - 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.") if self.state != 'draft': raise UserError("Submit hanya bisa dilakukan dari Draft.") @@ -375,7 +427,7 @@ class TukarGulingPO(models.Model): def action_approve(self): self.ensure_one() self._validate_product_lines() - self._check_bill_on_revisi_po() + # self._check_bill_on_revisi_po() self._check_not_allow_tukar_guling_on_bu_input() if not self.operations: @@ -389,26 +441,66 @@ class TukarGulingPO(models.Model): # 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.") + if not rec.env.user.has_group('indoteknik_custom.group_role_purchasing'): + raise UserError("Hanya Purchasing 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.") + raise UserError("Hanya Finance yang boleh approve tahap ini.") + # rec._check_bill_on_revisi_po() + rec.set_opt() 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' + raise UserError("Hanya Logistic yang boleh approve tahap ini.") + rec.state = 'approved' rec._create_pickings() rec.date_logistic = now else: raise UserError("Status ini tidak bisa di-approve.") + def update_doc_state(self): + # bu input rev po + if self.operations.picking_type_id.id == 28 and self.return_type == 'revisi_po': + prt = self.env['stock.picking'].search([ + ('tukar_guling_po_id', '=', self.id), + ('state', '=', 'done'), + ('picking_type_id.id', '=', 76) + ]) + if self.state == 'approved' and prt: + self.state = 'done' + # bu put rev po + elif self.operations.picking_type_id.id == 75 and self.return_type == 'revisi_po': + total_prt = self.env['stock.picking'].search_count([ + ('tukar_guling_po_id', '=', self.id), + ('picking_type_id.id', '=', 76) + ]) + prt = self.env['stock.picking'].search_count([ + ('tukar_guling_po_id', '=', self.id), + ('state', '=', 'done'), + ('picking_type_id.id', '=', 76) + ]) + if self.state == 'approved' and total_prt > 0 and prt == total_prt: + self.state = 'done' + # bu put tukar guling + elif self.operations.picking_type_id.id == 75 and self.return_type == 'tukar_guling': + total_put = self.env['stock.picking'].search_count([ + ('tukar_guling_po_id', '=', self.id), + ('picking_type_id.id', '=', 75) + ]) + put = self.env['stock.picking'].search_count([ + ('tukar_guling_po_id', '=', self.id), + ('state', '=', 'done'), + ('picking_type_id.id', '=', 75) + ]) + if self.state == 'aproved' and total_put > 0 and put == total_put: + self.state = 'done' + + def action_cancel(self): self.ensure_one() # if self.state == 'done': @@ -416,7 +508,7 @@ class TukarGulingPO(models.Model): user = self.env.user if not ( - user.has_group('indoteknik_custom.group_role_sales') or + user.has_group('indoteknik_custom.group_role_purchasing') or user.has_group('indoteknik_custom.group_role_fat') or user.has_group('indoteknik_custom.group_role_logistic') ): diff --git a/indoteknik_custom/models/update_date_planned_po_wizard.py b/indoteknik_custom/models/update_date_planned_po_wizard.py new file mode 100644 index 00000000..a0d241c8 --- /dev/null +++ b/indoteknik_custom/models/update_date_planned_po_wizard.py @@ -0,0 +1,14 @@ +from odoo import models, fields, api + +class PurchaseOrderUpdateDateWizard(models.TransientModel): + _name = 'purchase.order.update.date.wizard' + _description = 'Wizard to Update Receipt Date on Purchase Order Lines' + + date_planned = fields.Datetime(string="New Receipt Date", required=True) + + def action_update_date(self): + active_ids = self.env.context.get('active_ids', []) + orders = self.env['purchase.order'].browse(active_ids) + for order in orders: + order.write({'date_planned': self.date_planned}) + return {'type': 'ir.actions.act_window_close'} diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index 6b9ac164..e20709e4 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -161,6 +161,7 @@ access_konfirm_koli,access.konfirm.koli,model_konfirm_koli,,1,1,1,1 access_stock_immediate_transfer,access.stock.immediate.transfer,model_stock_immediate_transfer,,1,1,1,1 access_coretax_faktur,access.coretax.faktur,model_coretax_faktur,,1,1,1,1 access_purchase_order_unlock_wizard,access.purchase.order.unlock.wizard,model_purchase_order_unlock_wizard,,1,1,1,1 +access_change_date_planned_wizard,access.change.date.planned.wizard,model_change_date_planned_wizard,,1,1,1,1 access_sales_order_koli,access.sales.order.koli,model_sales_order_koli,,1,1,1,1 access_stock_backorder_confirmation,access.stock.backorder.confirmation,model_stock_backorder_confirmation,,1,1,1,1 access_warning_modal_wizard,access.warning.modal.wizard,model_warning_modal_wizard,,1,1,1,1 @@ -196,4 +197,5 @@ access_tukar_guling_all_users,tukar.guling.all.users,model_tukar_guling,base.gro access_tukar_guling_line_all_users,tukar.guling.line.all.users,model_tukar_guling_line,base.group_user,1,1,1,1 access_tukar_guling_po_all_users,tukar.guling.po.all.users,model_tukar_guling_po,base.group_user,1,1,1,1 access_tukar_guling_line_po_all_users,tukar.guling.line.po.all.users,model_tukar_guling_line_po,base.group_user,1,1,1,1 -access_tukar_guling_mapping_koli_all_users,tukar.guling.mapping.koli.all.users,model_tukar_guling_mapping_koli,base.group_user,1,1,1,1
\ No newline at end of file +access_tukar_guling_mapping_koli_all_users,tukar.guling.mapping.koli.all.users,model_tukar_guling_mapping_koli,base.group_user,1,1,1,1 +access_purchase_order_update_date_wizard,access.purchase.order.update.date.wizard,model_purchase_order_update_date_wizard,base.group_user,1,1,1,1
\ No newline at end of file diff --git a/indoteknik_custom/views/account_move.xml b/indoteknik_custom/views/account_move.xml index 9b1c791b..e5d1cf8a 100644 --- a/indoteknik_custom/views/account_move.xml +++ b/indoteknik_custom/views/account_move.xml @@ -29,6 +29,8 @@ </field> <field name="payment_reference" position="after"> <field name="date_completed" readonly="1" attrs="{'invisible': [('move_type', '!=', 'out_invoice')]}"/> + <field name="payment_date" readonly="1" attrs="{'invisible': [('move_type', '!=', 'out_invoice'), ('payment_date', '=', False)]}"/> + <field name="partial_payment" readonly="1" attrs="{'invisible': [('move_type', '!=', 'out_invoice'), ('payment_state', '!=', 'partial')]}"/> <field name="reklas_id" attrs="{'invisible': [('move_type', '!=', 'out_invoice')]}"/> </field> <field name="invoice_date" position="after"> diff --git a/indoteknik_custom/views/account_move_views.xml b/indoteknik_custom/views/account_move_views.xml index da25636e..0fd7c9cd 100644 --- a/indoteknik_custom/views/account_move_views.xml +++ b/indoteknik_custom/views/account_move_views.xml @@ -47,15 +47,20 @@ <button name="approve_new_due" string="Approve" type="object" + attrs="{'readonly': [('approval_status', 'in', ('approved'))]}" /> <button name="due_extension_approval" string="Ask Approval" type="object" + attrs="{'readonly': [('approval_status', 'in', ('approved'))]}" /> <button name="due_extension_cancel" string="Cancel" type="object" + attrs="{'readonly': [('approval_status', 'in', ('approved'))]}" /> + <field name="approval_status" widget="statusbar" + statusbar_visible="pengajuan,approved"/> </header> <sheet> <group> @@ -67,7 +72,6 @@ <group> <field name="is_approve" readonly="1"/> <field name="counter" readonly="1"/> - <field name="approval_status" readonly="1"/> <field name="approve_by" readonly="1"/> <field name="date_approve" readonly="1"/> </group> diff --git a/indoteknik_custom/views/approval_payment_term.xml b/indoteknik_custom/views/approval_payment_term.xml index cc9db914..f7c24737 100644 --- a/indoteknik_custom/views/approval_payment_term.xml +++ b/indoteknik_custom/views/approval_payment_term.xml @@ -59,7 +59,8 @@ </group> <group> <field name="reason"/> - <field name="reason_reject" attrs="{'invisible': [('state', '!=', 'rejected')]}"/> + <field name="reason_reject" invisible="1"/> + <field name="reject_reason" attrs="{'invisible': [('state', '!=', 'rejected')]}"/> <field name="approve_date" readonly="1"/> <field name="approve_sales_manager" readonly="1"/> <field name="approve_finance" readonly="1"/> diff --git a/indoteknik_custom/views/mail_template_invoice_reminder.xml b/indoteknik_custom/views/mail_template_invoice_reminder.xml index 21055eb0..8450be28 100644 --- a/indoteknik_custom/views/mail_template_invoice_reminder.xml +++ b/indoteknik_custom/views/mail_template_invoice_reminder.xml @@ -6,29 +6,32 @@ <field name="model_id" ref="account.model_account_move"/> <field name="subject">Reminder Invoice Due - ${object.name}</field> <field name="email_from">finance@indoteknik.co.id</field> - <field name="email_to">andrifebriyadiputra@gmail.com</field> + <field name="email_to"></field> <field name="body_html" type="html"> <div> <p><b>Dear ${object.name},</b></p> - <p>Berikut adalah daftar invoice Anda yang mendekati atau telah jatuh tempo:</p> + <p>${days_to_due_message}</p> - <table border="1" cellpadding="4" cellspacing="0" style="border-collapse: collapse; width: 100%; font-size: 12px"> + <table border="1" cellpadding="4" cellspacing="0" style="border-collapse: collapse; font-size: 12px"> <thead> <tr style="background-color: #f2f2f2;" align="left"> + <th>Customer</th> + <th>No. PO</th> <th>Invoice Number</th> - <th>Tanggal Invoice</th> - <th>Jatuh Tempo</th> - <th>Sisa Hari</th> - <th>Total</th> - <th>Referensi</th> + <th>Invoice Date</th> + <th>Due Date</th> + <th>Amount</th> + <th>Term</th> + <th>Days To Due</th> </tr> </thead> <tbody> </tbody> </table> - <p>Mohon bantuan dan kerjasamanya agar tetap bisa bekerjasama dengan baik</p> + <p>${closing_message}</p> + <br/> <p>Terima Kasih.</p> <br/> <br/> @@ -42,6 +45,7 @@ <a href="https://wa.me/6285716970374" target="_blank">+62-857-1697-0374</a> | <a href="mailto:finance@indoteknik.co.id">finance@indoteknik.co.id</a> </b></p> + <p><i>Email ini dikirim secara otomatis. Abaikan jika pembayaran telah dilakukan.</i></p> </div> </field> diff --git a/indoteknik_custom/views/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml index ff223125..15cdc788 100755 --- a/indoteknik_custom/views/purchase_order.xml +++ b/indoteknik_custom/views/purchase_order.xml @@ -75,11 +75,13 @@ </field> <field name="partner_id" position="after"> <field name="purchase_order_count"/> + <field name="complete_bu_in_count" invisible="1"/> </field> <field name="incoterm_id" position="after"> <field name="amount_total_without_service"/> <field name="delivery_amt"/> <field name="approve_by"/> + <field name="reason_change_date_planned"/> </field> <field name="currency_id" position="after"> <field name="summary_qty_po"/> @@ -106,9 +108,16 @@ <field name="product_id" position="attributes"> <attribute name="options">{'no_create': True}</attribute> </field> - <field name="date_planned" position="attributes"> - <attribute name="invisible">1</attribute> - </field> + <xpath expr="//field[@name='date_planned']" position="replace"> + <field name="date_planned" readonly="1"/> + </xpath> + <xpath expr="//field[@name='date_planned']" position="after"> + <button name="action_open_change_date_wizard" + type="object" + string="Change Receipt Date" + class="btn-primary" + attrs="{'invisible': ['|', ('state', '=', 'cancel'), ('complete_bu_in_count', '=', 0)]}"/> + </xpath> <field name="product_qty" position="before"> <field name="is_edit_product_qty" readonly="1" optional="hide"/> <field name="qty_onhand" readonly="1" optional="hide"/> @@ -225,6 +234,34 @@ </data> <data> + <record id="view_change_date_planned_wizard_form" model="ir.ui.view"> + <field name="name">change.date.planned.wizard.form</field> + <field name="model">change.date.planned.wizard</field> + <field name="arch" type="xml"> + <form string="Change Date Planned"> + <group> + <field name="purchase_id" readonly="1"/> + <field name="old_date_planned" readonly="1"/> + <field name="date_changed" invisible="1"/> + <field name="new_date_planned"/> + <field name="reason" + attrs="{ + 'invisible': ['|', ('old_date_planned', '=', False), ('date_changed', '=', False)], + 'required': [('date_changed', '=', True)] + }"/> + </group> + + <footer> + <button name="confirm_change" type="object" string="Confirm" class="btn-primary"/> + <button string="Cancel" class="btn-secondary" special="cancel"/> + </footer> + </form> + </field> + </record> + </data> + + + <data> <record id="rfq_order_tree_view_inherit" model="ir.ui.view"> <field name="name">Purchase</field> <field name="model">purchase.order</field> @@ -391,4 +428,24 @@ <field name="code">action = records.open_form_multi_cancel()</field> </record> </data> + <data> + <record id="action_update_receipt_date_po" model="ir.actions.server"> + <field name="name">Update Receipt Date</field> + <field name="model_id" ref="purchase.model_purchase_order"/> + <field name="binding_model_id" ref="purchase.model_purchase_order"/> + <field name="state">code</field> + <field name="binding_view_types">list</field> + <field name="code"> + action = { + 'type': 'ir.actions.act_window', + 'res_model': 'purchase.order.update.date.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'active_ids': env.context.get('active_ids', []), + }, + } + </field> + </record> + </data> </odoo>
\ No newline at end of file diff --git a/indoteknik_custom/views/purchasing_job.xml b/indoteknik_custom/views/purchasing_job.xml index e3866d84..2466e7be 100644 --- a/indoteknik_custom/views/purchasing_job.xml +++ b/indoteknik_custom/views/purchasing_job.xml @@ -17,7 +17,7 @@ <field name="status_apo" invisible="1"/> <field name="action"/> <field name="note"/> - <field name="date_po"/> + <field name="date_po" optional="hide"/> <field name="so_number"/> <field name="check_pj" invisible="1"/> <button name="action_open_job_detail" diff --git a/indoteknik_custom/views/res_partner.xml b/indoteknik_custom/views/res_partner.xml index a030a75c..b081f6f2 100644 --- a/indoteknik_custom/views/res_partner.xml +++ b/indoteknik_custom/views/res_partner.xml @@ -102,6 +102,9 @@ /> </div> </xpath> + <xpath expr="//field[@name='child_ids']/form//field[@name='mobile']" position="after"> + <field name="reminder_invoices"/> + </xpath> <xpath expr="//field[@name='property_payment_term_id']" position="attributes"> <attribute name="readonly">0</attribute> </xpath> @@ -217,7 +220,7 @@ <group string="Aging Info"> <field name="avg_aging" readonly="1"/> <field name="payment_difficulty" attrs="{'readonly': [('parent_id', '!=', False)]}" /> - <field name="payment_history_url" readonly="1" /> + <field name="payment_history_url" readonly="1" widget="url"/> </group> </page> </notebook> diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml index c1f1fe61..868bce7b 100755 --- a/indoteknik_custom/views/sale_order.xml +++ b/indoteknik_custom/views/sale_order.xml @@ -172,6 +172,7 @@ <xpath expr="//page[@name='other_information']/group/group[@name='sale_reporting']" position="after"> <group string="ETA"> <field name="et_products"/> + <field name="reason_change_date_planned" readonly="1"/> <field name="eta_date_reserved"/> <field name="expected_ready_to_ship"/> <field name="eta_date_start"/> @@ -290,6 +291,7 @@ <field name="note_procurement" optional="hide"/> <field name="vendor_subtotal" optional="hide"/> <field name="weight" optional="hide"/> + <field name="is_has_disc" string="Flash Sale Item?" readonly="1" optional="hide"/> <field name="amount_voucher_disc" string="Voucher" readonly="1" optional="hide"/> <field name="order_promotion_id" string="Promotion" readonly="1" optional="hide"/> </xpath> @@ -353,8 +355,9 @@ </field> <field name="payment_term_id" position="attributes"> <attribute name="attrs"> - {'readonly': [('approval_status', '=', 'approved'), ('state', 'not in', - ['cancel', 'draft'])]} + {'readonly': ['|', ('approval_status', 'in', ['pengajuan1', 'pengajuan2', 'approved']), + ('state', 'not in', + ['cancel', 'draft'])]} </attribute> </field> diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml index f9200dfa..b3f0ce9f 100644 --- a/indoteknik_custom/views/stock_picking.xml +++ b/indoteknik_custom/views/stock_picking.xml @@ -129,6 +129,9 @@ <field name="date_done" position="after"> <field name="arrival_time"/> </field> + <field name="scheduled_date" position="attributes"> + <attribute name="readonly">1</attribute> + </field> <field name="origin" position="after"> <!-- <field name="show_state_approve_md" invisible="1" optional="hide"/>--> <field name="state_approve_md" widget="badge"/> @@ -165,6 +168,7 @@ <field name="approval_receipt_status"/> <field name="approval_return_status"/> <field name="so_lama"/> + <field name="reason_change_date_planned" readonly="1"/> </field> <field name="product_id" position="before"> <field name="line_no" attrs="{'readonly': 1}" optional="hide"/> diff --git a/indoteknik_custom/views/tukar_guling.xml b/indoteknik_custom/views/tukar_guling.xml index fa3db0d2..a8d8b7b7 100644 --- a/indoteknik_custom/views/tukar_guling.xml +++ b/indoteknik_custom/views/tukar_guling.xml @@ -21,7 +21,7 @@ <field name="name">pengajuan.tukar.guling.tree</field> <field name="model">tukar.guling</field> <field name="arch" type="xml"> - <tree create="1" delete="1" default_order="create_date desc"> + <tree create="0" delete="1" default_order="create_date desc"> <field name="name"/> <field name="partner_id" string="Customer"/> <field name="origin" string="SO Number"/> @@ -29,6 +29,7 @@ <field name="return_type" string="Return Type"/> <field name="state" widget="badge" decoration-info="state in ('draft', 'approval_sales', 'approval_finance','approval_logistic')" + decoration-warning="state == 'approved'" decoration-success="state == 'done'" decoration-muted="state == 'cancel'" /> @@ -58,7 +59,7 @@ class="btn-secondary" attrs="{'invisible': [('state', '!=', 'cancel')]}"/> <field name="state" widget="statusbar" readonly="1" - statusbar_visible="draft,approval_sales,approval_logistic,approval_finance,done"/> + statusbar_visible="draft,approval_sales,approval_logistic,approval_finance,approved,done"/> </header> <sheet> <div class="oe_button_box"> @@ -66,7 +67,7 @@ type="object" class="oe_stat_button" icon="fa-truck" - attrs="{'invisible': [('picking_ids', '=', False), ('state', 'in', ['draft', 'approval_sales', 'approval_logistic', 'approval_finance'])]}"> + attrs="{'invisible': [('picking_ids', '=', False), ('state', 'in', ['draft', 'approval_sales', 'approval_logistic', 'approval_finance', 'approved', 'done', 'cancel'])]}"> <field name="picking_ids" widget="statinfo" string="Delivery"/> </button> </div> @@ -82,9 +83,13 @@ <field name="return_type" attrs="{'readonly': [('state', 'not in', 'draft')]}"/> <field name="operations" attrs="{'readonly': [('state', 'not in', 'draft')]}"/> - <field name="origin" readonly="1"/> +<!-- <field name="origin" readonly="1"/>--> + <field name="origin_so" readonly="1"/> + <field name="is_has_invoice" readonly="1"/> + <field name="invoice_id" readonly="1" widget="many2many_tags"/> </group> <group> + <field name="val_inv_opt" attrs="{'invisible': [('is_has_invoice', '=', False)]}"/> <field name="ba_num" string="Nomor BA"/> <field name="notes"/> <field name="date_sales" readonly="1"/> diff --git a/indoteknik_custom/views/tukar_guling_po.xml b/indoteknik_custom/views/tukar_guling_po.xml index 26c0a0d4..d0ae9e96 100644 --- a/indoteknik_custom/views/tukar_guling_po.xml +++ b/indoteknik_custom/views/tukar_guling_po.xml @@ -24,11 +24,12 @@ <tree create="1" delete="1" default_order="create_date desc"> <field name="name"/> <field name="vendor_id" string="Customer"/> - <field name="origin" string="SO Number"/> + <field name="origin" string="PO Number"/> <field name="operations" string="Operations"/> <field name="return_type" string="Return Type"/> <field name="state" widget="badge" decoration-info="state in ('draft', 'approval_purchase', 'approval_finance','approval_logistic')" + decoration-warning="state == 'approved'" decoration-success="state == 'done'" decoration-muted="state == 'cancel'" /> @@ -60,7 +61,7 @@ attrs="{'invisible': [('state', '!=', 'cancel')]}" confirm="Are you sure you want to reset this record to draft?"/> <field name="state" widget="statusbar" readonly="1" - statusbar_visible="draft,approval_purchase,approval_logistic,approval_finance,done"/> + statusbar_visible="draft,approval_purchase,approval_logistic,approval_finance,approved,done"/> </header> <sheet> <div class="oe_button_box"> @@ -88,10 +89,14 @@ attrs="{ 'required': [('return_type', 'in', ['revisi_po', 'tukar_guling'])] }"/> - <field name="origin" readonly="1"/> - <!-- <field name="origin_so" readonly="1"/>--> +<!-- <field name="origin" readonly="1"/>--> + <field name="origin_po" readonly="1"/> + <field name="is_has_bill" readonly="1"/> +<!-- <field name="bill_id" readonly="1" />--> + <field name="bill_id" readonly="1" widget="many2many_tags"/> </group> <group> + <field name="val_bil_opt" attrs="{'invisible': [('is_has_bill', '=', False)]}"/> <field name="ba_num" string="Nomor BA"/> <field name="notes"/> <field name="date_purchase" readonly="1"/> diff --git a/indoteknik_custom/views/update_date_planned_po_wizard_view.xml b/indoteknik_custom/views/update_date_planned_po_wizard_view.xml new file mode 100644 index 00000000..6b3ab991 --- /dev/null +++ b/indoteknik_custom/views/update_date_planned_po_wizard_view.xml @@ -0,0 +1,25 @@ +<odoo> + <record id="view_update_date_planned_po_wizard_form" model="ir.ui.view"> + <field name="name">purchase.order.update.date.wizard.form</field> + <field name="model">purchase.order.update.date.wizard</field> + <field name="arch" type="xml"> + <form string="Update Receipt Date"> + <group> + <field name="date_planned"/> + </group> + <footer> + <button string="Apply" type="object" name="action_update_date" class="btn-primary"/> + <button string="Cancel" special="cancel"/> + </footer> + </form> + </field> + </record> + + <record id="action_update_date_planned_po_wizard" model="ir.actions.act_window"> + <field name="name">Update Receipt Date</field> + <field name="res_model">purchase.order.update.date.wizard</field> + <field name="view_mode">form</field> + <field name="target">new</field> + </record> + </odoo> +
\ No newline at end of file diff --git a/indoteknik_custom/views/user_company_request.xml b/indoteknik_custom/views/user_company_request.xml index 88d04c64..5f296cb0 100644 --- a/indoteknik_custom/views/user_company_request.xml +++ b/indoteknik_custom/views/user_company_request.xml @@ -31,7 +31,8 @@ <group> <field name="user_id" readonly="1"/> <field name="similar_company_ids" invisible="1"/> - <field name="user_company_id" domain="[('id', 'in', similar_company_ids)]"/> +<!-- <field name="user_company_id" domain="[('id', 'in', similar_company_ids)]"/>--> + <field name="user_company_id" /> <field name="user_input" readonly="1"/> <field name="is_approve" |
