summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIndoteknik . <it@fixcomart.co.id>2025-08-09 14:45:48 +0700
committerIndoteknik . <it@fixcomart.co.id>2025-08-09 14:45:48 +0700
commit979ffc90fffe2f09788016376d71a940a28f8fed (patch)
treea5408296eb204a0a5066578664ed0acb13174f27
parent33621956bdb9d807b480eda44ce7f2152f508695 (diff)
parentdf5ff0fc9bcd45c19b74288d8e5cfee018ba5bdd (diff)
Merge branch 'odoo-backup' of https://bitbucket.org/altafixco/indoteknik-addons into pum-v2
-rw-r--r--indoteknik_api/controllers/api_v1/sale_order.py440
-rw-r--r--indoteknik_api/controllers/api_v1/stock_picking.py17
-rw-r--r--indoteknik_api/controllers/api_v1/voucher.py2
-rw-r--r--indoteknik_api/models/sale_order.py65
-rwxr-xr-xindoteknik_custom/__manifest__.py1
-rwxr-xr-xindoteknik_custom/models/__init__.py1
-rw-r--r--indoteknik_custom/models/account_move.py290
-rw-r--r--indoteknik_custom/models/account_move_due_extension.py9
-rw-r--r--indoteknik_custom/models/approval_payment_term.py1
-rw-r--r--indoteknik_custom/models/commision.py30
-rw-r--r--indoteknik_custom/models/cust_commision.py1
-rw-r--r--indoteknik_custom/models/mrp_production.py8
-rwxr-xr-xindoteknik_custom/models/purchase_order.py152
-rw-r--r--indoteknik_custom/models/res_partner.py5
-rwxr-xr-xindoteknik_custom/models/sale_order.py80
-rw-r--r--indoteknik_custom/models/sale_order_line.py7
-rw-r--r--indoteknik_custom/models/stock_picking.py34
-rw-r--r--indoteknik_custom/models/tukar_guling.py348
-rw-r--r--indoteknik_custom/models/tukar_guling_po.py170
-rw-r--r--indoteknik_custom/models/update_date_planned_po_wizard.py14
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv4
-rw-r--r--indoteknik_custom/views/account_move.xml2
-rw-r--r--indoteknik_custom/views/account_move_views.xml6
-rw-r--r--indoteknik_custom/views/approval_payment_term.xml3
-rw-r--r--indoteknik_custom/views/mail_template_invoice_reminder.xml22
-rwxr-xr-xindoteknik_custom/views/purchase_order.xml63
-rw-r--r--indoteknik_custom/views/purchasing_job.xml2
-rw-r--r--indoteknik_custom/views/res_partner.xml5
-rwxr-xr-xindoteknik_custom/views/sale_order.xml7
-rw-r--r--indoteknik_custom/views/stock_picking.xml4
-rw-r--r--indoteknik_custom/views/tukar_guling.xml13
-rw-r--r--indoteknik_custom/views/tukar_guling_po.xml13
-rw-r--r--indoteknik_custom/views/update_date_planned_po_wizard_view.xml25
-rw-r--r--indoteknik_custom/views/user_company_request.xml3
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"