summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--indoteknik_api/controllers/api_v1/sale_order.py13
-rw-r--r--indoteknik_api/controllers/api_v1/stock_picking.py45
-rwxr-xr-xindoteknik_custom/__manifest__.py3
-rwxr-xr-xindoteknik_custom/models/__init__.py2
-rw-r--r--indoteknik_custom/models/account_asset.py4
-rw-r--r--indoteknik_custom/models/account_payment.py2
-rw-r--r--indoteknik_custom/models/approval_payment_term.py1
-rw-r--r--indoteknik_custom/models/automatic_purchase.py161
-rw-r--r--indoteknik_custom/models/commision.py8
-rw-r--r--indoteknik_custom/models/domain_apo.py12
-rw-r--r--indoteknik_custom/models/letter_receivable.py188
-rw-r--r--indoteknik_custom/models/mrp_production.py19
-rw-r--r--indoteknik_custom/models/partial_delivery.py257
-rwxr-xr-xindoteknik_custom/models/purchase_order.py68
-rwxr-xr-xindoteknik_custom/models/purchase_order_line.py6
-rw-r--r--indoteknik_custom/models/purchase_order_sales_match.py5
-rw-r--r--indoteknik_custom/models/refund_sale_order.py5
-rwxr-xr-xindoteknik_custom/models/sale_order.py129
-rw-r--r--indoteknik_custom/models/sale_order_line.py62
-rw-r--r--indoteknik_custom/models/sj_tele.py83
-rw-r--r--indoteknik_custom/models/stock_move.py18
-rw-r--r--indoteknik_custom/models/stock_picking.py104
-rw-r--r--indoteknik_custom/models/tukar_guling.py26
-rw-r--r--indoteknik_custom/models/user_pengajuan_tempo_request.py5
-rw-r--r--indoteknik_custom/report/report_surat_piutang copy.xml149
-rw-r--r--indoteknik_custom/report/report_surat_piutang.xml6
-rw-r--r--indoteknik_custom/report/report_tutup_tempo.xml158
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv9
-rw-r--r--indoteknik_custom/views/account_asset_views.xml3
-rw-r--r--indoteknik_custom/views/approval_payment_term.xml4
-rw-r--r--indoteknik_custom/views/close_tempo_mail_template.xml56
-rw-r--r--indoteknik_custom/views/domain_apo.xml46
-rw-r--r--indoteknik_custom/views/letter_receivable.xml26
-rw-r--r--indoteknik_custom/views/mail_template_closing_apt.xml32
-rwxr-xr-xindoteknik_custom/views/purchase_order.xml2
-rwxr-xr-xindoteknik_custom/views/sale_order.xml54
-rw-r--r--indoteknik_custom/views/stock_move_line.xml3
-rw-r--r--indoteknik_custom/views/stock_picking.xml11
38 files changed, 1280 insertions, 505 deletions
diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py
index 1a75c830..cff1921d 100644
--- a/indoteknik_api/controllers/api_v1/sale_order.py
+++ b/indoteknik_api/controllers/api_v1/sale_order.py
@@ -728,6 +728,7 @@ class SaleOrder(controller.Controller):
if params['value']['type'] == 'sale_order':
parameters['approval_status'] = 'pengajuan1'
+ # parameters['approval_status'] = False
_logger.info("Setting approval_status to 'pengajuan1'")
sale_order = request.env['sale.order'].with_context(from_website_checkout=True).create([parameters])
@@ -788,7 +789,7 @@ class SaleOrder(controller.Controller):
order_line.product_id_change()
order_line.weight = order_line.product_id.weight
- order_line.onchange_vendor_id()
+ order_line._onchange_vendor_id_custom()
_logger.info(f"After onchanges - Price: {order_line.price_unit}, Disc: {order_line.discount}")
elif cart['cart_type'] == 'promotion':
@@ -808,6 +809,14 @@ class SaleOrder(controller.Controller):
sale_order.apply_promotion_program()
sale_order.add_free_product(promotions)
+ # Pastikan baris hasil promo/bonus ditandai supaya bisa di-skip voucher
+ promo_lines = sale_order.order_line.filtered(
+ lambda l: getattr(l, 'order_promotion_id', False) or (l.price_unit or 0.0) == 0.0
+ )
+ if promo_lines:
+ promo_lines.write({'is_has_disc': True})
+ _logger.info(f"[PROMO_MARK] Marked {len(promo_lines)} promo/free lines as is_has_disc=True")
+
voucher_code = params['value']['voucher']
if voucher_code:
_logger.info(f"Processing voucher: {voucher_code}")
@@ -816,7 +825,7 @@ class SaleOrder(controller.Controller):
voucher_shipping = request.env['voucher'].search(
[('code', '=', voucher_code), ('apply_type', 'in', ['shipping'])], limit=1)
- if voucher and len(promotions) == 0:
+ if voucher:
_logger.info("Applying regular voucher")
sale_order.voucher_id = voucher.id
sale_order.apply_voucher()
diff --git a/indoteknik_api/controllers/api_v1/stock_picking.py b/indoteknik_api/controllers/api_v1/stock_picking.py
index a4a9cf80..fe82e665 100644
--- a/indoteknik_api/controllers/api_v1/stock_picking.py
+++ b/indoteknik_api/controllers/api_v1/stock_picking.py
@@ -1,5 +1,5 @@
from .. import controller
-from odoo import http
+from odoo import http, fields
from odoo.http import request, Response
from pytz import timezone
from datetime import datetime
@@ -8,7 +8,6 @@ import logging
_logger = logging.getLogger(__name__)
-_logger = logging.getLogger(__name__)
class StockPicking(controller.Controller):
prefix = '/api/v1/'
@@ -125,30 +124,10 @@ 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, scanid, **kw):
- sj_document = kw.get('sj_document', False)
- paket_document = kw.get('paket_document', False)
- dispatch_document = kw.get('dispatch_document', False)
-
- # ===== Role by EMAIL =====
- driver_emails = {
- 'driverindoteknik@gmail.com',
- 'sulistianaridwan8@gmail.com',
- }
- dispatch_emails = {
- 'rahmat.afiudin@gmail.com',
- 'it@fixcomart.co.id'
- }
-
- login = (request.env.user.login or '').lower()
- is_dispatch_user = login in dispatch_emails
- is_driver_user = (login in driver_emails) and not is_dispatch_user
-
- # ===== Validasi minimal =====
- if not sj_document or not paket_document:
- return self.response(code=400, description='dispatch_document wajib untuk role dispatch login= %s' % login)
-
- # if is_dispatch_user and not dispatch_document and not is_driver_user:
- # return self.response(code=400, description='dispatch_document wajib untuk role dispatch login= %s' % login)
+ sj_document = kw.get('sj_document') if 'sj_document' in kw else None
+ paket_document = kw.get('paket_document') if 'paket_document' in kw else None
+ dispatch_document = kw.get('dispatch_document') if 'dispatch_document' in kw else None
+ self_pu= kw.get('self_pu') if 'self_pu' in kw else None
# ===== Cari picking by id / picking_code =====
picking_data = False
@@ -161,17 +140,21 @@ class StockPicking(controller.Controller):
if not picking_data:
return self.response(code=403, description='picking not found')
- params = {
- 'sj_documentation': sj_document,
- 'paket_documentation': paket_document,
- 'driver_arrival_date': datetime.utcnow(),
- }
+ params = {}
+ if sj_document:
+ params['sj_documentation'] = sj_document
+ if self_pu:
+ params['driver_arrival_date'] = datetime.utcnow()
+ if paket_document:
+ params['paket_documentation'] = paket_document
+ params['driver_arrival_date'] = datetime.utcnow()
if dispatch_document:
params['dispatch_documentation'] = dispatch_document
picking_data.write(params)
return self.response({'name': picking_data.name})
+
@http.route(prefix + 'webhook/biteship', type='json', auth='public', methods=['POST'], csrf=False)
def update_status_from_biteship(self, **kw):
_logger.info("Biteship Webhook: Request received at controller start (type='json').")
diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py
index 3c3c9a95..392e848d 100755
--- a/indoteknik_custom/__manifest__.py
+++ b/indoteknik_custom/__manifest__.py
@@ -167,6 +167,7 @@
'report/report_picking.xml',
'report/report_sale_order.xml',
'report/report_surat_piutang.xml',
+ 'report/report_tutup_tempo.xml',
'report/purchase_report.xml',
'views/vendor_sla.xml',
'views/coretax_faktur.xml',
@@ -184,6 +185,8 @@
'views/letter_receivable_mail_template.xml',
# 'views/reimburse.xml',
'views/sj_tele.xml',
+ 'views/close_tempo_mail_template.xml',
+ 'views/domain_apo.xml',
'views/sourcing.xml'
],
'demo': [],
diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py
index cb110342..36a992d2 100755
--- a/indoteknik_custom/models/__init__.py
+++ b/indoteknik_custom/models/__init__.py
@@ -160,4 +160,6 @@ from . import update_date_planned_po_wizard
from . import unpaid_invoice_view
from . import letter_receivable
from . import sj_tele
+from . import partial_delivery
+from . import domain_apo
from . import sourcing_job_order
diff --git a/indoteknik_custom/models/account_asset.py b/indoteknik_custom/models/account_asset.py
index bd5f9adb..211ab229 100644
--- a/indoteknik_custom/models/account_asset.py
+++ b/indoteknik_custom/models/account_asset.py
@@ -4,6 +4,10 @@ from odoo.exceptions import AccessError, UserError, ValidationError
class AccountAsset(models.Model):
_inherit = 'account.asset.asset'
+ asset_type = fields.Selection(string='Tipe Aset', selection=[
+ ('aset_gudang', ' Aset Gudang'),
+ ('aset_kantor', 'Aset Kantor'),
+ ], tracking=True )
def action_close_asset(self):
for asset in self:
diff --git a/indoteknik_custom/models/account_payment.py b/indoteknik_custom/models/account_payment.py
index 11c664eb..d2d3d175 100644
--- a/indoteknik_custom/models/account_payment.py
+++ b/indoteknik_custom/models/account_payment.py
@@ -42,7 +42,7 @@ class AccountPayment(models.Model):
def allocate_invoices(self):
for payment in self:
- if self.
+ # if self.
for line in payment.payment_line:
invoice = line.account_move_id
move_lines = payment.line_ids.filtered(lambda line: line.account_internal_type in ('receivable', 'payable'))
diff --git a/indoteknik_custom/models/approval_payment_term.py b/indoteknik_custom/models/approval_payment_term.py
index 449bd90b..e45305db 100644
--- a/indoteknik_custom/models/approval_payment_term.py
+++ b/indoteknik_custom/models/approval_payment_term.py
@@ -56,6 +56,7 @@ class ApprovalPaymentTerm(models.Model):
change_log_688 = fields.Text(string="Change Log", readonly=True, copy=False)
+
def write(self, vals):
# Ambil nilai lama sebelum perubahan
old_values_dict = {
diff --git a/indoteknik_custom/models/automatic_purchase.py b/indoteknik_custom/models/automatic_purchase.py
index 83a7cb3c..d9ec17f4 100644
--- a/indoteknik_custom/models/automatic_purchase.py
+++ b/indoteknik_custom/models/automatic_purchase.py
@@ -93,7 +93,7 @@ class AutomaticPurchase(models.Model):
counter = 0
for vendor in vendor_ids:
- self.create_po_by_vendor(vendor['partner_id'][0])
+ self.create_po_for_vendor(vendor['partner_id'][0])
# param_header = {
# 'partner_id': vendor['partner_id'][0],
@@ -191,95 +191,106 @@ class AutomaticPurchase(models.Model):
if qty_pj > qty_outgoing_pj:
raise UserError('Qty yang anda beli lebih dari qty outgoing. %s' %id_po)
- def create_po_by_vendor(self, vendor_id):
+ def create_po_for_vendor(self, vendor_id):
current_time = datetime.now()
name = "/PJ/" if not self.apo_type == 'reordering' else "/A/"
-
PRODUCT_PER_PO = 20
auto_purchase_line = self.env['automatic.purchase.line']
- # Domain untuk semua baris dengan vendor_id tertentu
- domain = [
+ config = self.env['apo.domain.config'].search([
+ ('vendor_id', '=', vendor_id)
+ ], limit=1)
+
+ base_domain = [
('automatic_purchase_id', '=', self.id),
('partner_id', '=', vendor_id),
('qty_purchase', '>', 0)
]
- # Tambahkan domain khusus untuk brand_id 22 dan 564
- special_brand_domain = domain + [('brand_id', 'in', [22, 564])]
- regular_domain = domain + [('brand_id', 'not in', [22, 564])]
-
- # Fungsi untuk membuat PO berdasarkan domain tertentu
- def create_po_for_domain(domain, special_payment_term=False):
- products_len = auto_purchase_line.search_count(domain)
- page = math.ceil(products_len / PRODUCT_PER_PO)
-
- for i in range(page):
- # Buat PO baru
- param_header = {
- 'partner_id': vendor_id,
- 'currency_id': 12,
- 'user_id': self.env.user.id,
- 'company_id': 1, # indoteknik dotcom gemilang
- 'picking_type_id': 28, # indoteknik bandengan receipts
- 'date_order': current_time,
- 'from_apo': True,
- 'note_description': 'Automatic PO'
- }
+ # Kalau vendor punya brand spesial → bikin domain sesuai config
+ if config and config.is_special:
+ special_brand_domain = base_domain + [('brand_id', 'in', config.brand_ids.ids)]
+ self._create_po_for_domain(
+ vendor_id, special_brand_domain, name, PRODUCT_PER_PO, current_time, config.payment_term_id, special=config.is_special
+ )
- new_po = self.env['purchase.order'].create([param_header])
+ # Regular domain (selain brand spesial)
+ regular_domain = base_domain
+ if config and config.is_special and config.brand_ids:
+ regular_domain = base_domain + [('brand_id', 'not in', config.brand_ids.ids)]
- # Set payment_term_id khusus jika diperlukan
- if special_payment_term:
- new_po.payment_term_id = 29
- else:
- new_po.payment_term_id = new_po.partner_id.property_supplier_payment_term_id
+ self._create_po_for_domain(
+ vendor_id, regular_domain, name, PRODUCT_PER_PO, current_time, config.payment_term_id
+ )
- new_po.name = new_po.name + name + str(i + 1)
- self.env['automatic.purchase.match'].create([{
- 'automatic_purchase_id': self.id,
- 'order_id': new_po.id
- }])
+ def _create_po_for_domain(self, vendor_id, domain, name, PRODUCT_PER_PO, current_time, payment_term_id, special=False):
+ auto_purchase_line = self.env['automatic.purchase.line']
+ products_len = auto_purchase_line.search_count(domain)
+ page = math.ceil(products_len / PRODUCT_PER_PO)
+
+ for i in range(page):
+ # Buat header PO
+ param_header = {
+ 'partner_id': vendor_id,
+ 'currency_id': 12,
+ 'user_id': self.env.user.id,
+ 'company_id': 1,
+ 'picking_type_id': 28,
+ 'date_order': current_time,
+ 'from_apo': True,
+ 'note_description': 'Automatic PO'
+ }
+ new_po = self.env['purchase.order'].create(param_header)
+
+ # Set payment term
+ new_po.payment_term_id = payment_term_id.id if special else (
+ new_po.partner_id.property_supplier_payment_term_id
+ )
+
+ new_po.name = new_po.name + name + str(i + 1)
+
+ self.env['automatic.purchase.match'].create([{
+ 'automatic_purchase_id': self.id,
+ 'order_id': new_po.id
+ }])
+
+ # Ambil lines
+ lines = auto_purchase_line.search(
+ domain,
+ offset=i * PRODUCT_PER_PO,
+ limit=PRODUCT_PER_PO
+ )
+
+ # Pre-fetch sales_match biar ga search per line
+ sales_matches = self.env['automatic.purchase.sales.match'].search([
+ ('automatic_purchase_id', '=', self.id),
+ ('product_id', 'in', lines.mapped('product_id').ids),
+ ])
+ match_map = {sm.product_id.id: sm for sm in sales_matches}
+
+ for line in lines:
+ product = line.product_id
+ sales_match = match_map.get(product.id)
+ param_line = {
+ 'order_id': new_po.id,
+ 'product_id': product.id,
+ 'product_qty': line.qty_purchase,
+ 'qty_available_store': product.qty_available_bandengan,
+ 'suggest': product._get_po_suggest(line.qty_purchase),
+ 'product_uom_qty': line.qty_purchase,
+ 'price_unit': line.last_price,
+ 'ending_price': line.last_price,
+ 'taxes_id': [(6, 0, [line.taxes_id.id])] if line.taxes_id else False,
+ 'so_line_id': sales_match.sale_line_id.id if sales_match else None,
+ 'so_id': sales_match.sale_id.id if sales_match else None
+ }
+ new_po_line = self.env['purchase.order.line'].create(param_line)
+ line.current_po_id = new_po.id
+ line.current_po_line_id = new_po_line.id
+
+ self.create_purchase_order_sales_match(new_po)
- # Ambil baris sesuai halaman
- lines = auto_purchase_line.search(
- domain,
- offset=i * PRODUCT_PER_PO,
- limit=PRODUCT_PER_PO
- )
-
- for line in lines:
- product = line.product_id
- sales_match = self.env['automatic.purchase.sales.match'].search([
- ('automatic_purchase_id', '=', self.id),
- ('product_id', '=', product.id),
- ])
- param_line = {
- 'order_id': new_po.id,
- 'product_id': product.id,
- 'product_qty': line.qty_purchase,
- 'qty_available_store': product.qty_available_bandengan,
- 'suggest': product._get_po_suggest(line.qty_purchase),
- 'product_uom_qty': line.qty_purchase,
- 'price_unit': line.last_price,
- 'ending_price': line.last_price,
- 'taxes_id': [line.taxes_id.id] if line.taxes_id else None,
- 'so_line_id': sales_match[0].sale_line_id.id if sales_match else None,
- 'so_id': sales_match[0].sale_id.id if sales_match else None
- }
- new_po_line = self.env['purchase.order.line'].create([param_line])
- line.current_po_id = new_po.id
- line.current_po_line_id = new_po_line.id
-
- self.create_purchase_order_sales_match(new_po)
-
- # Buat PO untuk special brand
- if vendor_id == 23:
- create_po_for_domain(special_brand_domain, special_payment_term=True)
-
- # Buat PO untuk regular domain
- create_po_for_domain(regular_domain, "")
def update_purchase_price_so_line(self, apo):
diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py
index 6d538b83..a937e2d0 100644
--- a/indoteknik_custom/models/commision.py
+++ b/indoteknik_custom/models/commision.py
@@ -428,22 +428,22 @@ class CustomerCommision(models.Model):
if not self.status or self.status == 'draft':
self.status = 'pengajuan1'
- elif self.status == 'pengajuan1' and self.env.user.id == 19:
+ elif self.status == 'pengajuan1' and (self.env.user.id == 19 or self.env.user.has_group('indoteknik_custom.group_role_it')):
self.status = 'pengajuan2'
self.approved_by = (self.approved_by + ', ' if self.approved_by else '') + self.env.user.name
self.date_approved_sales = now_naive
self.position_sales = 'Sales Manager'
- elif self.status == 'pengajuan2' and self.env.user.id == 216:
+ elif self.status == 'pengajuan2' and (self.env.user.id == 216 or self.env.user.has_group('indoteknik_custom.group_role_it')):
self.status = 'pengajuan3'
self.approved_by = (self.approved_by + ', ' if self.approved_by else '') + self.env.user.name
self.date_approved_marketing = now_naive
self.position_marketing = 'Marketing Manager'
- elif self.status == 'pengajuan3' and self.env.user.is_leader:
+ elif self.status == 'pengajuan3' and (self.env.user.is_leader or self.env.user.has_group('indoteknik_custom.group_role_it')):
self.status = 'pengajuan4'
self.approved_by = (self.approved_by + ', ' if self.approved_by else '') + self.env.user.name
self.date_approved_pimpinan = now_naive
self.position_pimpinan = 'Pimpinan'
- elif self.status == 'pengajuan4' and self.env.user.id == 1272:
+ elif self.status == 'pengajuan4' and (self.env.user.id == 1272 or self.env.user.has_group('indoteknik_custom.group_role_it')):
for line in self.commision_lines:
line.invoice_id.is_customer_commision = True
if self.commision_type == 'fee':
diff --git a/indoteknik_custom/models/domain_apo.py b/indoteknik_custom/models/domain_apo.py
new file mode 100644
index 00000000..585dd24c
--- /dev/null
+++ b/indoteknik_custom/models/domain_apo.py
@@ -0,0 +1,12 @@
+from odoo import models, fields
+
+
+class ApoDomainConfig(models.Model):
+ _name = 'apo.domain.config'
+ _description = 'Automatic Purchase Domain Config'
+
+ name = fields.Char(string="Config Name", required=True)
+ vendor_id = fields.Many2one('res.partner', string="Vendor", required=True, domain=[('supplier_rank', '>', 0)])
+ brand_ids = fields.Many2many('x_manufactures', string="Special Brands")
+ payment_term_id = fields.Many2one('account.payment.term', string="Payment Term")
+ is_special = fields.Boolean(string="Special Vendor?", default=False)
diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py
index 16034938..a98e46a1 100644
--- a/indoteknik_custom/models/letter_receivable.py
+++ b/indoteknik_custom/models/letter_receivable.py
@@ -23,6 +23,7 @@ class SuratPiutang(models.Model):
tujuan_nama = fields.Char(string="Nama Tujuan", tracking=True)
tujuan_email = fields.Char(string="Email Tujuan", tracking=True)
perihal = fields.Selection([
+ ('tutup_tempo', 'Surat Penutupan Pembayaran Tempo'),
('penagihan', 'Surat Resmi Penagihan'),
('sp1', 'Surat Peringatan Piutang ke-1'),
('sp2', 'Surat Peringatan Piutang ke-2'),
@@ -36,6 +37,7 @@ class SuratPiutang(models.Model):
("sent", "Approved & Sent")
], default="draft", tracking=True)
send_date = fields.Datetime(string="Tanggal Kirim", tracking=True)
+ due_date = fields.Date(string="Tanggal Jatuh Tempo", tracking=True, default= fields.Date.today)
seven_days_after_sent_date = fields.Char(string="7 Hari Setelah Tanggal Kirim")
periode_invoices_terpilih = fields.Char(
string="Periode Invoices Terpilih",
@@ -67,6 +69,18 @@ class SuratPiutang(models.Model):
"sp1": "sp2",
"sp2": "sp3",
}
+
+ def action_select_all_lines(self):
+ for rec in self:
+ if not rec.line_ids:
+ raise UserError(_("Tidak ada invoice line untuk dipilih."))
+ rec.line_ids.write({'selected': True})
+
+ def action_unselect_all_lines(self):
+ for rec in self:
+ if not rec.line_ids:
+ raise UserError(_("Tidak ada invoice line untuk dihapus seleksinya."))
+ rec.line_ids.write({'selected': False})
@api.onchange('partner_id')
def _onchange_partner_id_domain(self):
@@ -191,17 +205,17 @@ class SuratPiutang(models.Model):
line.amount_residual or 0.0 for line in rec.line_ids if line.selected
)
- @api.constrains("tujuan_email")
- def _check_email_format(self):
- for rec in self:
- if rec.tujuan_email and not mail.single_email_re.match(rec.tujuan_email):
- raise ValidationError(_("Format email tidak valid: %s") % rec.tujuan_email)
+ # @api.constrains("tujuan_email")
+ # def _check_email_format(self):
+ # for rec in self:
+ # if rec.tujuan_email and not mail.single_email_re.match(rec.tujuan_email):
+ # raise ValidationError(_("Format email tidak valid: %s") % rec.tujuan_email)
def action_approve(self):
wib = pytz.timezone('Asia/Jakarta')
now_wib = datetime.now(wib)
- sales_manager_ids = [10] # ganti dengan ID user Sales Manager
+ sales_manager_ids = [19] # ganti dengan ID user Sales Manager
pimpinan_user_ids = [7] # ganti dengan ID user Pimpinan
for rec in self:
@@ -228,20 +242,27 @@ class SuratPiutang(models.Model):
continue
# === Surat penagihan biasa (langsung Pimpinan approve) ===
- if rec.perihal == "penagihan":
+ if rec.perihal in ("tutup_tempo", "penagihan"):
# if self.env.user.id not in pimpinan_user_ids:
# raise UserError("Hanya Pimpinan yang boleh menyetujui surat penagihan.")
rec.state = "sent"
now_utc = now_wib.astimezone(pytz.UTC).replace(tzinfo=None)
rec.send_date = now_utc
rec.action_send_letter()
- rec.message_post(body="Surat Penagihan disetujui dan berhasil dikirim.")
+ rec.message_post(body=f"{rec.perihal_label} disetujui dan berhasil dikirim.")
self.env.user.notify_info(
message=f"Surat piutang {rec.name} berhasil dikirim ke {rec.partner_id.name} ({rec.tujuan_email})",
title="Informasi",
sticky=False
)
+
+ def action_print(self):
+ self.ensure_one()
+ if self.perihal == 'tutup_tempo':
+ return self.env.ref('indoteknik_custom.action_report_surat_tutup_tempo').report_action(self)
+ else:
+ return self.env.ref('indoteknik_custom.action_report_surat_piutang').report_action(self)
def action_send_letter(self):
self.ensure_one()
@@ -253,64 +274,79 @@ class SuratPiutang(models.Model):
if not self.tujuan_email:
raise UserError(_("Email tujuan harus diisi."))
- template = self.env.ref('indoteknik_custom.letter_receivable_mail_template')
- # today = fields.Date.today()
-
- month_map = {
- 1: "Januari", 2: "Februari", 3: "Maret", 4: "April",
- 5: "Mei", 6: "Juni", 7: "Juli", 8: "Agustus",
- 9: "September", 10: "Oktober", 11: "November", 12: "Desember",
- }
- target_date = (self.send_date or fields.Datetime.now()).date() + timedelta(days=7)
- self.seven_days_after_sent_date = f"{target_date.day} {month_map[target_date.month]}"
-
- perihal_map = {
- 'penagihan': 'Surat Resmi Penagihan',
- 'sp1': 'Surat Peringatan Pertama (I)',
- 'sp2': 'Surat Peringatan Kedua (II)',
- 'sp3': 'Surat Peringatan Ketiga (III)',
- }
- perihal_text = perihal_map.get(self.perihal, self.perihal or '')
-
- invoice_table_rows = ""
- grand_total = 0
- for line in selected_lines:
- # days_to_due = (line.invoice_date_due - today).days if line.invoice_date_due else 0
- grand_total += line.amount_residual
- invoice_table_rows += f"""
- <tr>
- <td>{line.invoice_number or '-'}</td>
- <td>{self.partner_id.name or '-'}</td>
- <td>{fields.Date.to_string(line.invoice_date) or '-'}</td>
- <td>{fields.Date.to_string(line.invoice_date_due) or '-'}</td>
- <td>{line.new_invoice_day_to_due}</td>
- <td>{line.ref or '-'}</td>
- <td>{formatLang(self.env, line.amount_residual, currency_obj=line.currency_id)}</td>
- <td>{line.payment_term_id.name or '-'}</td>
- </tr>
- """
-
- invoice_table_footer = f"""
- <tfoot>
- <tr style="font-weight:bold; background-color:#f9f9f9;">
- <td colspan="6" align="right">Grand Total</td>
- <td>{formatLang(self.env, grand_total, currency_obj=self.currency_id, monetary=True)}</td>
- <td colspan="2"></td>
+ template = None
+ report = None
+ body_html = None
+ subject = None
+
+ # Logika untuk memilih template dan report berdasarkan 'perihal'
+ if self.perihal == 'tutup_tempo':
+ template = self.env.ref('indoteknik_custom.close_tempo_mail_template')
+ report = self.env.ref('indoteknik_custom.action_report_surat_tutup_tempo')
+ due_date_str = self.due_date.strftime('%d %B %Y') if self.due_date else 'yang telah ditentukan'
+ body_html = template.body_html \
+ .replace('${object.partner_id.name}', self.partner_id.name or '') \
+ .replace('${object.due_date}', due_date_str or '')
+ subject = f"Pemberitahuan Penutupan Pembayaran Tempo – {self.partner_id.name}"
+ else:
+ template = self.env.ref('indoteknik_custom.letter_receivable_mail_template')
+
+ month_map = {
+ 1: "Januari", 2: "Februari", 3: "Maret", 4: "April",
+ 5: "Mei", 6: "Juni", 7: "Juli", 8: "Agustus",
+ 9: "September", 10: "Oktober", 11: "November", 12: "Desember",
+ }
+ target_date = (self.send_date or fields.Datetime.now()).date() + timedelta(days=7)
+ self.seven_days_after_sent_date = f"{target_date.day} {month_map[target_date.month]}"
+
+ perihal_map = {
+ 'penagihan': 'Surat Resmi Penagihan',
+ 'sp1': 'Surat Peringatan Pertama (I)',
+ 'sp2': 'Surat Peringatan Kedua (II)',
+ 'sp3': 'Surat Peringatan Ketiga (III)',
+ }
+ perihal_text = perihal_map.get(self.perihal, self.perihal or '')
+
+ invoice_table_rows = ""
+ grand_total = 0
+ for line in selected_lines:
+ grand_total += line.amount_residual
+ invoice_table_rows += f"""
+ <tr>
+ <td>{line.invoice_number or '-'}</td>
+ <td>{self.partner_id.name or '-'}</td>
+ <td>{fields.Date.to_string(line.invoice_date) or '-'}</td>
+ <td>{fields.Date.to_string(line.invoice_date_due) or '-'}</td>
+ <td>{line.new_invoice_day_to_due}</td>
+ <td>{line.ref or '-'}</td>
+ <td>{formatLang(self.env, line.amount_residual, currency_obj=line.currency_id)}</td>
+ <td>{line.payment_term_id.name or '-'}</td>
</tr>
- </tfoot>
- """
- # inject table rows ke template
- body_html = re.sub(
- r"<tbody[^>]*>.*?</tbody>",
- f"<tbody>{invoice_table_rows}</tbody>{invoice_table_footer}",
- template.body_html,
- flags=re.DOTALL
- ).replace('${object.name}', self.name or '') \
- .replace('${object.partner_id.name}', self.partner_id.name or '') \
- .replace('${object.seven_days_after_sent_date}', self.seven_days_after_sent_date or '') \
- .replace('${object.perihal}', perihal_text or '')
-
- report = self.env.ref('indoteknik_custom.action_report_surat_piutang')
+ """
+
+ invoice_table_footer = f"""
+ <tfoot>
+ <tr style="font-weight:bold; background-color:#f9f9f9;">
+ <td colspan="6" align="right">Grand Total</td>
+ <td>{formatLang(self.env, grand_total, currency_obj=self.currency_id, monetary=True)}</td>
+ <td colspan="2"></td>
+ </tr>
+ </tfoot>
+ """
+
+ body_html = re.sub(
+ r"<tbody[^>]*>.*?</tbody>",
+ f"<tbody>{invoice_table_rows}</tbody>{invoice_table_footer}",
+ template.body_html,
+ flags=re.DOTALL
+ ).replace('${object.name}', self.name or '') \
+ .replace('${object.partner_id.name}', self.partner_id.name or '') \
+ .replace('${object.seven_days_after_sent_date}', self.seven_days_after_sent_date or '') \
+ .replace('${object.perihal}', perihal_text or '')
+
+ report = self.env.ref('indoteknik_custom.action_report_surat_piutang')
+ subject = perihal_map.get(self.perihal, self.perihal or '') + " - " + (self.partner_id.name or '')
+
pdf_content, _ = report._render_qweb_pdf([self.id])
attachment_base64 = base64.b64encode(pdf_content)
@@ -335,12 +371,12 @@ class SuratPiutang(models.Model):
cc_list.append(sales_email)
values = {
- # 'subject': template.subject.replace('${object.name}', self.name or ''),
- 'subject': perihal_map.get(self.perihal, self.perihal or '') + " - " + (self.partner_id.name or ''),
+ 'subject': subject, # Menggunakan subject yang sudah ditentukan di atas
'email_to': self.tujuan_email,
'email_from': 'finance@indoteknik.co.id',
'email_cc': ",".join(sorted(set(cc_list))),
- 'body_html': body_html,
+ # 'email_cc': 'finance@indoteknik.co.id', # testing
+ 'body_html': body_html, # Menggunakan body_html yang sudah ditentukan di atas
'attachments': [(attachment.name, attachment.datas)],
'reply_to': 'finance@indoteknik.co.id',
}
@@ -352,7 +388,7 @@ class SuratPiutang(models.Model):
)
_logger.info(
- f"Surat Piutang {self.name} terkirim ke {self.tujuan_email} "
+ f"{self.name} terkirim ke {self.tujuan_email} "
f"({self.partner_id.name}), total {len(selected_lines)} invoice."
)
@@ -453,6 +489,18 @@ class SuratPiutang(models.Model):
body=f"Line Invoices diperbarui. Total line saat ini: {len(rec.line_ids)}"
)
+ @api.onchange('perihal', 'partner_id')
+ def _onchange_perihal_tutup_tempo(self):
+ if self.perihal == 'tutup_tempo':
+ for line in self.line_ids:
+ if line.new_invoice_day_to_due < -30:
+ line.selected = True
+ else:
+ line.selected = False
+ else:
+ for line in self.line_ids:
+ line.selected = False
+
@api.model
def create(self, vals):
# Generate nomor surat otomatis
@@ -462,7 +510,7 @@ class SuratPiutang(models.Model):
bulan_romawi = ["I","II","III","IV","V","VI","VII","VIII","IX","X","XI","XII"][today.month-1]
tahun = today.strftime("%y")
vals["name"] = f"{seq}/LO/FAT/IDG/{bulan_romawi}/{tahun}"
- if vals.get("perihal") == "penagihan":
+ if vals.get("perihal") in ("tutup_tempo", "penagihan"):
vals["state"] = "waiting_approval_pimpinan"
else:
vals["state"] = "waiting_approval_sales"
diff --git a/indoteknik_custom/models/mrp_production.py b/indoteknik_custom/models/mrp_production.py
index b39995b5..02679458 100644
--- a/indoteknik_custom/models/mrp_production.py
+++ b/indoteknik_custom/models/mrp_production.py
@@ -21,6 +21,22 @@ class MrpProduction(models.Model):
], string='Status Reserve', tracking=True, copy=False, help="The current state of the stock picking.")
date_reserved = fields.Datetime(string="Date Reserved", help='Tanggal ter-reserved semua barang nya', copy=False)
+ def action_cancel(self):
+ for production in self:
+ moves_with_forecast = production.move_raw_ids.filtered(
+ lambda m: m.reserved_availability > 0
+ )
+
+ if moves_with_forecast:
+ # bikin list produk per baris
+ product_list = "\n".join(
+ "- %s" % p.display_name for p in moves_with_forecast.mapped('product_id')
+ )
+ raise UserError(_(
+ "You cannot cancel this Manufacturing Order because the following raw materials "
+ "still have forecast availability:\n\n%s" % product_list
+ ))
+ return super(MrpProduction, self).action_cancel()
@api.constrains('check_bom_product_lines')
def constrains_check_bom_product_lines(self):
@@ -292,6 +308,9 @@ class CheckBomProduct(models.Model):
if not self.code_product:
return
+ if self.production_id.qty_producing == 0:
+ raise UserError("Isi dan Save dahulu Quantity To Produce yang diinginkan!")
+
# Cari product berdasarkan default_code, barcode, atau barcode_box
product = self.env['product.product'].search([
'|',
diff --git a/indoteknik_custom/models/partial_delivery.py b/indoteknik_custom/models/partial_delivery.py
new file mode 100644
index 00000000..4df7da1e
--- /dev/null
+++ b/indoteknik_custom/models/partial_delivery.py
@@ -0,0 +1,257 @@
+from odoo import fields, models, api, _
+from odoo.exceptions import UserError, ValidationError
+from datetime import datetime, timedelta, timezone, time
+import logging, random, string, requests, math, json, re, qrcode, base64
+
+_logger = logging.getLogger(__name__)
+
+class PartialDeliveryWizard(models.TransientModel):
+ _name = 'partial.delivery.wizard'
+ _description = 'Partial Delivery Wizard'
+
+ sale_id = fields.Many2one('sale.order')
+ picking_ids = fields.Many2many('stock.picking')
+ picking_id = fields.Many2one(
+ 'stock.picking',
+ string='Delivery Order',
+ domain="[('id','in',picking_ids), ('state', 'not in', ('done', 'cancel')), ('name', 'like', 'BU/PICK/%')]"
+ )
+ line_ids = fields.One2many('partial.delivery.wizard.line', 'wizard_id')
+
+ # @api.model
+ # def default_get(self, fields_list):
+ # res = super().default_get(fields_list)
+ # picking_ids_ctx = self.env.context.get('default_picking_ids')
+ # lines = []
+ # if picking_ids_ctx:
+ # if isinstance(picking_ids_ctx, list) and picking_ids_ctx and isinstance(picking_ids_ctx[0], tuple):
+ # picking_ids = picking_ids_ctx[0][2]
+ # else:
+ # picking_ids = picking_ids_ctx
+
+ # pickings = self.env['stock.picking'].browse(picking_ids)
+ # moves = pickings.move_ids_without_package.filtered(lambda m: m.reserved_availability > 0)
+
+ # for move in moves:
+ # lines.append((0, 0, {
+ # 'product_id': move.product_id.id,
+ # 'reserved_qty': move.reserved_availability,
+ # 'move_id': move.id,
+ # }))
+ # res['line_ids'] = lines
+ # return res
+
+ def action_select_all(self):
+ if len(self.line_ids) == 0:
+ raise UserError(_("Tidak ada produk yang dipilih."))
+ for line in self.line_ids:
+ line.selected = True
+ return {
+ 'type': 'ir.actions.act_window',
+ 'res_model': self._name,
+ 'view_mode': 'form',
+ 'res_id': self.id,
+ 'target': 'new',
+ }
+
+ def action_unselect_all(self):
+ if len(self.line_ids) == 0:
+ raise UserError(_("Tidak ada produk yang dipilih."))
+ for line in self.line_ids:
+ line.selected = False
+ return {
+ 'type': 'ir.actions.act_window',
+ 'res_model': self._name,
+ 'view_mode': 'form',
+ 'res_id': self.id,
+ 'target': 'new',
+ }
+
+ @api.onchange('picking_id')
+ def _onchange_picking_id(self):
+ """Generate lines whenever picking_id is changed"""
+ if not self.picking_id:
+ self.line_ids = [(5, 0, 0)]
+ return
+
+ if self.line_ids:
+ self.line_ids.unlink()
+
+ moves = self.picking_id.move_lines or self.picking_id.move_ids_without_package
+ moves = moves.filtered(lambda m: m.product_id and m.reserved_availability > 0)
+
+ if not moves:
+ _logger.warning(f"[PartialDeliveryWizard] Tidak ada move line di picking {self.picking_id.name}")
+ return
+
+ for move in moves:
+ reserved_qty = move.reserved_availability or 0.0
+ ordered_qty = move.sale_line_id.product_uom_qty if move.sale_line_id else 0.0
+
+ self.env['partial.delivery.wizard.line'].create({
+ 'wizard_id': self.id,
+ 'product_id': move.product_id.id,
+ 'reserved_qty': reserved_qty,
+ # 'selected_qty': reserved_qty,
+ 'move_id': move.id,
+ 'sale_line_id': move.sale_line_id.id if move.sale_line_id else False,
+ })
+
+ _logger.info(
+ f"[PartialDeliveryWizard] ✅ Created line for {move.product_id.display_name} "
+ f"(reserved={reserved_qty}, move_id={move.id})"
+ )
+
+
+ def action_confirm_partial_delivery(self):
+ self.ensure_one()
+ StockPicking = self.env['stock.picking']
+
+ picking = self.picking_id
+ if not picking:
+ raise UserError(_("Tidak ada picking yang dipilih."))
+
+ if picking.state != "assigned":
+ raise UserError(_("Picking harus dalam status Ready (assigned)."))
+
+ lines_by_qty = self.line_ids.filtered(lambda l: l.selected_qty > 0)
+ lines_by_selected = self.line_ids.filtered(lambda l: l.selected and not l.selected_qty)
+ selected_lines = lines_by_qty | lines_by_selected # gabung dua domain hasil filter
+
+ if not selected_lines:
+ raise UserError(_("Tidak ada produk yang dipilih atau diisi jumlahnya."))
+
+ # 🧠 Cek apakah semua move di DO sudah muncul di wizard dan semua dipilih
+ picking_move_ids = picking.move_ids_without_package.ids
+ wizard_move_ids = self.line_ids.mapped('move_id').ids
+
+ # Semua move DO muncul di wizard, dan semua baris dipilih
+ full_selected = (
+ set(picking_move_ids) == set(wizard_move_ids)
+ and len(selected_lines) == len(self.line_ids)
+ and all(
+ (line.selected_qty or line.reserved_qty) >= line.reserved_qty
+ for line in selected_lines
+ )
+ )
+
+ if full_selected:
+ # 💡 Gak perlu bikin picking baru, langsung ubah state_reserve
+ picking.write({'state_reserve': 'partial'})
+
+ picking.message_post(
+ body=f"<b>Full Picking Confirmed</b> via wizard partial delivery oleh {self.env.user.name} (tanpa DO baru)",
+ message_type="comment",
+ subtype_xmlid="mail.mt_note",
+ )
+
+ return {
+ "type": "ir.actions.act_window",
+ "res_model": "stock.picking",
+ "view_mode": "form",
+ "res_id": picking.id,
+ "target": "current",
+ "effect": {
+ "fadeout": "slow",
+ "message": f"✅ Semua produk dari DO ini dikirim penuh — tidak dibuat DO baru.",
+ "type": "rainbow_man",
+ },
+ }
+
+ # 🧩 Kalau bukan full selected, lanjut bikin DO baru
+ new_picking = StockPicking.create({
+ 'origin': picking.origin,
+ 'partner_id': picking.partner_id.id,
+ 'picking_type_id': picking.picking_type_id.id,
+ 'location_id': picking.location_id.id,
+ 'location_dest_id': picking.location_dest_id.id,
+ 'company_id': picking.company_id.id,
+ 'state_reserve': 'partial',
+ })
+
+ for line in selected_lines:
+ if line.selected_qty > line.reserved_qty:
+ raise UserError(_("Jumlah produk %s yang dipilih melebihi jumlah reserved.") % line.product_id.display_name)
+ move = line.move_id
+ move._do_unreserve()
+
+ if line.selected and not line.selected_qty:
+ line.selected_qty = line.reserved_qty
+
+ if line.selected_qty > 0:
+ if line.selected_qty > move.product_uom_qty:
+ raise UserError(_(
+ f"Qty kirim ({line.selected_qty}) untuk {move.product_id.display_name} melebihi qty move ({move.product_uom_qty})."
+ ))
+
+ if line.selected_qty < move.product_uom_qty:
+ qty_to_keep = move.product_uom_qty - line.selected_qty
+ new_move = move.copy(default={
+ 'product_uom_qty': line.selected_qty,
+ 'picking_id': new_picking.id,
+ 'partial': True,
+ })
+ move.write({'product_uom_qty': qty_to_keep})
+ else:
+ move.write({'picking_id': new_picking.id, 'partial': True})
+
+ new_picking.action_confirm()
+ new_picking.action_assign()
+ picking.action_assign()
+
+ existing_partials = self.env['stock.picking'].search([
+ ('origin', '=', picking.origin),
+ ('state_reserve', '=', 'partial'),
+ ('id', '!=', new_picking.id),
+ ], order='name asc')
+
+ suffix_number = len(existing_partials) + 1
+ new_picking.name = f"{picking.name}/{suffix_number}"
+
+ if picking.origin:
+ sale_order = self.env['sale.order'].search([('name', '=', picking.origin)], limit=1)
+ if sale_order:
+ sale_order.message_post(
+ body=f"<b>Partial Delivery Created:</b> <a href=# data-oe-model='stock.picking' data-oe-id='{new_picking.id}'>{new_picking.name}</a> "
+ f"oleh {self.env.user.name}",
+ message_type="comment",
+ subtype_xmlid="mail.mt_note",
+ )
+
+ new_picking.message_post(
+ body=f"<b>Partial Picking created</b> dari {picking.name} oleh {self.env.user.name}",
+ message_type="comment",
+ subtype_xmlid="mail.mt_note",
+ )
+
+ return {
+ "type": "ir.actions.act_window",
+ "res_model": "stock.picking",
+ "view_mode": "form",
+ "res_id": new_picking.id,
+ "target": "current",
+ "effect": {
+ "fadeout": "slow",
+ "message": f"🚚 Partial Delivery {new_picking.name} berhasil dibuat!",
+ "type": "rainbow_man",
+ },
+ }
+
+class PartialDeliveryWizardLine(models.TransientModel):
+ _name = 'partial.delivery.wizard.line'
+ _description = 'Partial Delivery Wizard Line'
+
+ wizard_id = fields.Many2one('partial.delivery.wizard')
+ product_id = fields.Many2one('product.product', string="Product")
+ reserved_qty = fields.Float(string="Reserved Qty")
+ selected_qty = fields.Float(string="Send Qty")
+ move_id = fields.Many2one('stock.move')
+ selected = fields.Boolean(string="Select")
+ sale_line_id = fields.Many2one('sale.order.line', string="SO Line", related='move_id.sale_line_id')
+ ordered_qty = fields.Float(related='sale_line_id.product_uom_qty', string="Ordered Qty")
+
+ @api.onchange('selected')
+ def onchange_selected(self):
+ if self.selected:
+ self.selected_qty = self.reserved_qty
+
diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py
index b34ec926..e79417aa 100755
--- a/indoteknik_custom/models/purchase_order.py
+++ b/indoteknik_custom/models/purchase_order.py
@@ -1069,6 +1069,21 @@ class PurchaseOrder(models.Model):
) % order.name)
def button_confirm(self):
+ if self.env.user.id != 7 and not self.env.user.is_leader: # Pimpinan
+ if '/PJ/' in self.name:
+ low_margin_lines = self.order_sales_match_line.filtered(
+ lambda match: match.so_header_margin <= 15.0
+ )
+ price_change_detected = any(line.price_unit_before for line in self.order_line)
+ if low_margin_lines and price_change_detected:
+ # raise UserError("Matches SO terdapat item dengan header margin SO <= 15%. Approval Pimpinan diperlukan.")
+ raise UserError("Approval Pimpinan diperlukan jika terdapat perubahan Unit Price pada PO Line yang Matches SO item memiliki header margin SO <= 15%")
+ # else:
+ # is_po_manual = '/A/' not in self.name and '/MO/' not in self.name
+ # if is_po_manual:
+ # if not self.order_sales_match_line:
+ # raise UserError("Tidak ada matches SO, Approval Pimpinan diperlukan.")
+
self._check_assets_note()
# self._check_payment_term() # check payment term
res = super(PurchaseOrder, self).button_confirm()
@@ -1077,7 +1092,7 @@ class PurchaseOrder(models.Model):
self.check_different_vendor_so_po()
# self.check_data_vendor()
- if self.amount_untaxed >= 50000000 and not self.env.user.id == 21:
+ if self.amount_untaxed >= 50000000 and not self.env.user.id in (21, 7):
raise UserError("Hanya Rafly Hanggara yang bisa approve")
if not self.date_planned:
@@ -1405,63 +1420,50 @@ class PurchaseOrder(models.Model):
('product_id', '=', line.product_id.id),
('order_id', '=', line.purchase_order_id.id)
], limit=1)
- sale_order_line = line.sale_line_id
- if not sale_order_line:
- sale_order_line = self.env['sale.order.line'].search([
- ('product_id', '=', line.product_id.id),
- ('order_id', '=', line.sale_id.id)
- ], limit=1, order='price_reduce_taxexcl')
+ sale_order_line = line.sale_line_id or self.env['sale.order.line'].search([
+ ('product_id', '=', line.product_id.id),
+ ('order_id', '=', line.sale_id.id)
+ ], limit=1, order='price_reduce_taxexcl')
if sale_order_line and po_line:
- so_margin = (line.qty_po / line.qty_so) * sale_order_line.item_margin
+ qty_so = line.qty_so or 0
+ qty_po = line.qty_po or 0
+
+ # Hindari division by zero
+ so_margin = (qty_po / qty_so) * sale_order_line.item_margin if qty_so > 0 else 0
sum_so_margin += so_margin
- sales_price = sale_order_line.price_reduce_taxexcl * line.qty_po
+ sales_price = sale_order_line.price_reduce_taxexcl * qty_po
if sale_order_line.order_id.shipping_cost_covered == 'indoteknik':
- sales_price -= (sale_order_line.delivery_amt_line / sale_order_line.product_uom_qty) * line.qty_po
+ sales_price -= (sale_order_line.delivery_amt_line / sale_order_line.product_uom_qty) * qty_po
if sale_order_line.order_id.fee_third_party > 0:
- sales_price -= (sale_order_line.fee_third_party_line / sale_order_line.product_uom_qty) * line.qty_po
+ sales_price -= (sale_order_line.fee_third_party_line / sale_order_line.product_uom_qty) * qty_po
sum_sales_price += sales_price
-
purchase_price = po_line.price_subtotal
if po_line.ending_price > 0:
if po_line.taxes_id.id == 22:
- ending_price = po_line.ending_price / 1.11
- purchase_price = ending_price
+ purchase_price = po_line.ending_price / 1.11
else:
purchase_price = po_line.ending_price
if line.purchase_order_id.delivery_amount > 0:
- purchase_price += (po_line.delivery_amt_line / po_line.product_qty) * line.qty_po
+ purchase_price += (po_line.delivery_amt_line / po_line.product_qty) * qty_po
if line.purchase_order_id.delivery_amt > 0:
purchase_price += line.purchase_order_id.delivery_amt
+
real_item_margin = sales_price - purchase_price
sum_margin += real_item_margin
- if sum_so_margin != 0 and sum_sales_price != 0 and sum_margin != 0:
+ # Akumulasi hasil akhir
+ if sum_sales_price != 0:
self.total_so_margin = sum_so_margin
self.total_so_percent_margin = round((sum_so_margin / sum_sales_price), 2) * 100
self.total_margin = sum_margin
self.total_percent_margin = round((sum_margin / sum_sales_price), 2) * 100
-
else:
- self.total_margin = 0
- self.total_percent_margin = 0
- self.total_so_margin = 0
- self.total_so_percent_margin = 0
-
-
- if sum_so_margin != 0 and sum_sales_price != 0 and sum_margin != 0:
- self.total_so_margin = sum_so_margin
- self.total_so_percent_margin = round((sum_so_margin / sum_sales_price), 2) * 100
- self.total_margin = sum_margin
- self.total_percent_margin = round((sum_margin / sum_sales_price), 2) * 100
+ self.total_margin = self.total_percent_margin = 0
+ self.total_so_margin = self.total_so_percent_margin = 0
- else:
- self.total_margin = 0
- self.total_percent_margin = 0
- self.total_so_margin = 0
- self.total_so_percent_margin = 0
def compute_amt_total_without_service(self):
for order in self:
diff --git a/indoteknik_custom/models/purchase_order_line.py b/indoteknik_custom/models/purchase_order_line.py
index a3c3a33b..8c72887d 100755
--- a/indoteknik_custom/models/purchase_order_line.py
+++ b/indoteknik_custom/models/purchase_order_line.py
@@ -51,6 +51,12 @@ class PurchaseOrderLine(models.Model):
contribution_cost_service = fields.Float(string='Contribution Cost Service', compute='_compute_doc_delivery_amt')
ending_price = fields.Float(string='Ending Price', compute='_compute_doc_delivery_amt')
show_description = fields.Boolean(string='Show Description', help="Show Description when print po", default=True)
+ price_unit_before = fields.Float(string='Unit Price Before', help="Harga awal yang sebelumnya telah diinputkan")
+
+ @api.onchange('price_unit')
+ def _onchange_price_unit_before(self):
+ if self._origin:
+ self.price_unit_before = self._origin.price_unit
def _compute_doc_delivery_amt(self):
for line in self:
diff --git a/indoteknik_custom/models/purchase_order_sales_match.py b/indoteknik_custom/models/purchase_order_sales_match.py
index 084b93f7..ea25a3b1 100644
--- a/indoteknik_custom/models/purchase_order_sales_match.py
+++ b/indoteknik_custom/models/purchase_order_sales_match.py
@@ -29,6 +29,11 @@ class PurchaseOrderSalesMatch(models.Model):
purchase_line_id = fields.Many2one('purchase.order.line', string='Purchase Line', compute='_compute_purchase_line_id')
hold_outgoing_so = fields.Boolean(string='Hold Outgoing SO', related='sale_id.hold_outgoing')
bu_pick = fields.Many2one('stock.picking', string='BU Pick', compute='compute_bu_pick')
+ so_header_margin = fields.Float(
+ related='sale_id.total_percent_margin',
+ string='SO Header Margin %',
+ readonly=True
+ )
def compute_bu_pick(self):
for rec in self:
diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py
index 47565dfc..e6547a88 100644
--- a/indoteknik_custom/models/refund_sale_order.py
+++ b/indoteknik_custom/models/refund_sale_order.py
@@ -753,10 +753,11 @@ class RefundSaleOrder(models.Model):
line_vals = []
for so in self.sale_order_ids:
for line in so.order_line:
- if line.qty_delivered == 0:
+ barang_kurang = line.product_uom_qty - line.qty_delivered
+ if line.qty_delivered == 0 or barang_kurang > 0:
line_vals.append((0, 0, {
'product_id': line.product_id.id,
- 'quantity': line.product_uom_qty,
+ 'quantity': barang_kurang,
'from_name': so.name,
'prod_id': so.id,
'reason': '',
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index 663cba58..5c8f34c6 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -399,20 +399,21 @@ class SaleOrder(models.Model):
)
def action_open_partial_delivery_wizard(self):
- self.ensure_one()
- pickings = self.picking_ids.filtered(lambda p: p.state not in ['done', 'cancel'] and p.name and 'BU/PICK/' in p.name)
- return {
- 'type': 'ir.actions.act_window',
- 'name': 'Partial Delivery',
- 'res_model': 'partial.delivery.wizard',
- 'view_mode': 'form',
- 'target': 'new',
- 'context': {
- 'default_sale_id': self.id,
- # kasih langsung list of int biar ga ribet di wizard
- 'default_picking_ids': pickings.ids,
- }
- }
+ raise UserError("Fitur ini sedang dalam pengembangan")
+ # self.ensure_one()
+ # pickings = self.picking_ids.filtered(lambda p: p.state not in ['done', 'cancel'] and p.name and 'BU/PICK/' in p.name)
+ # return {
+ # 'type': 'ir.actions.act_window',
+ # 'name': 'Partial Delivery',
+ # 'res_model': 'partial.delivery.wizard',
+ # 'view_mode': 'form',
+ # 'target': 'new',
+ # 'context': {
+ # 'default_sale_id': self.id,
+ # # kasih langsung list of int biar ga ribet di wizard
+ # 'default_picking_ids': pickings.ids,
+ # }
+ # }
@api.depends('partner_id.is_cbd_locked')
@@ -1813,10 +1814,10 @@ class SaleOrder(models.Model):
# rec.commitment_date = rec.expected_ready_to_ship
- @api.onchange('expected_ready_to_ship') #Hangle Onchange form Expected Ready to Ship
+ @api.onchange('expected_ready_to_ship')
def _onchange_expected_ready_ship_date(self):
self._validate_expected_ready_ship_date()
-
+
def _set_etrts_date(self):
for order in self:
if order.state in ('done', 'cancel', 'sale'):
@@ -1984,10 +1985,10 @@ class SaleOrder(models.Model):
# raise UserError('Kelurahan Real Delivery Address harus diisi')
def generate_payment_link_midtrans_sales_order(self):
- # midtrans_url = 'https://app.sandbox.midtrans.com/snap/v1/transactions' # dev - sandbox
- # midtrans_auth = 'Basic U0ItTWlkLXNlcnZlci1uLVY3ZDJjMlpCMFNWRUQyOU95Q1dWWXA6' # dev - sandbox
- midtrans_url = 'https://app.midtrans.com/snap/v1/transactions' # production
- midtrans_auth = 'Basic TWlkLXNlcnZlci1SbGMxZ2gzWGpSVW5scl9JblZzTV9OTnU6' # production
+ midtrans_url = 'https://app.sandbox.midtrans.com/snap/v1/transactions' # dev - sandbox
+ midtrans_auth = 'Basic U0ItTWlkLXNlcnZlci1uLVY3ZDJjMlpCMFNWRUQyOU95Q1dWWXA6' # dev - sandbox
+ # midtrans_url = 'https://app.midtrans.com/snap/v1/transactions' # production
+ # midtrans_auth = 'Basic TWlkLXNlcnZlci1SbGMxZ2gzWGpSVW5scl9JblZzTV9OTnU6' # production
so_number = self.name
so_number = so_number.replace('/', '-')
@@ -2000,8 +2001,8 @@ class SaleOrder(models.Model):
}
# ==== ENV ====
- # check_url = f'https://api.sandbox.midtrans.com/v2/{so_number}/status' # dev - sandbox
- check_url = f'https://api.midtrans.com/v2/{so_number}/status' # production
+ check_url = f'https://api.sandbox.midtrans.com/v2/{so_number}/status' # dev - sandbox
+ # check_url = f'https://api.midtrans.com/v2/{so_number}/status' # production
# =============================================
check_response = requests.get(check_url, headers=headers)
@@ -2241,7 +2242,7 @@ class SaleOrder(models.Model):
raise UserError("Payment Term pada Master Data Customer harus diisi")
if not partner.active_limit and term_days > 0:
raise UserError("Credit Limit pada Master Data Customer harus diisi")
- if order.payment_term_id != partner.property_payment_term_id:
+ if order.payment_term_id != partner.property_payment_term_id and not order.partner_id.id == 29179:
raise UserError("Payment Term berbeda pada Master Data Customer")
if (partner.customer_type == 'pkp' or order.customer_type == 'pkp') and order.npwp != partner.npwp:
raise UserError("NPWP berbeda pada Master Data Customer")
@@ -2324,7 +2325,7 @@ class SaleOrder(models.Model):
raise UserError("Payment Term pada Master Data Customer harus diisi")
if not partner.active_limit and term_days > 0:
raise UserError("Credit Limit pada Master Data Customer harus diisi")
- if order.payment_term_id != partner.property_payment_term_id:
+ if order.payment_term_id != partner.property_payment_term_id and not order.partner_id.id == 29179:
raise UserError("Payment Term berbeda pada Master Data Customer")
if (partner.customer_type == 'pkp' or order.customer_type == 'pkp') and order.npwp != partner.npwp:
raise UserError("NPWP berbeda pada Master Data Customer")
@@ -2871,6 +2872,7 @@ class SaleOrder(models.Model):
def action_apply_voucher(self):
for line in self.order_line:
if line.order_promotion_id:
+ _logger.warning(f"[CHECKOUT FAILED] Produk promo ditemukan: {line.product_id.display_name}")
raise UserError('Voucher tidak dapat digabung dengan promotion program')
voucher = self.voucher_id
@@ -2913,42 +2915,82 @@ class SaleOrder(models.Model):
self.apply_voucher_shipping()
def apply_voucher(self):
+ def _is_promo_line(line):
+ # TRUE jika baris tidak boleh kena voucher
+ if getattr(line, 'order_promotion_id', False):
+ return True # baris dari program promo
+ if (line.price_unit or 0.0) == 0.0:
+ return True # free item
+ if getattr(line, 'is_has_disc', False):
+ return True # sudah promo/flashsale/berdiskon
+ if (line.discount or 0.0) >= 100.0:
+ return True # safety
+ return False
+
+ # --- LOOP 1: susun input untuk voucher.apply() ---
order_line = []
for line in self.order_line:
+ if _is_promo_line(line):
+ continue
order_line.append({
'product_id': line.product_id,
'price': line.price_unit,
'discount': line.discount,
'qty': line.product_uom_qty,
- 'subtotal': line.price_subtotal
+ 'subtotal': line.price_subtotal,
})
+
+ if not order_line:
+ return
+
voucher = self.voucher_id.apply(order_line)
+ # --- LOOP 2: tulis hasilnya HANYA ke non-promo ---
for line in self.order_line:
+ if _is_promo_line(line):
+ continue
+
line.initial_discount = line.discount
voucher_type = voucher['type']
- used_total = voucher['total'][voucher_type]
- used_discount = voucher['discount'][voucher_type]
+ total_map = voucher['total'][voucher_type]
+ discount_map = voucher['discount'][voucher_type]
- manufacture_id = line.product_id.x_manufacture.id
if voucher_type == 'brand':
- used_total = used_total.get(manufacture_id)
- used_discount = used_discount.get(manufacture_id)
+ m_id = line.product_id.x_manufacture.id
+ used_total = (total_map or {}).get(m_id)
+ used_discount = (discount_map or {}).get(m_id)
+ else:
+ used_total = total_map
+ used_discount = discount_map
- if not used_total or not used_discount:
+ if not used_total or not used_discount or (line.product_uom_qty or 0.0) == 0.0:
continue
line_contribution = line.price_subtotal / used_total
line_voucher = used_discount * line_contribution
- line_voucher_item = line_voucher / line.product_uom_qty
+ per_item_voucher = line_voucher / line.product_uom_qty
+
+ has_ppn_11 = any(tax.id == 23 for tax in line.tax_id)
+ base_unit = line.price_unit / 1.11 if has_ppn_11 else line.price_unit
- line_price_unit = line.price_unit / 1.11 if any(tax.id == 23 for tax in line.tax_id) else line.price_unit
- line_discount_item = line_price_unit * line.discount / 100 + line_voucher_item
- line_voucher_item = line_discount_item / line_price_unit * 100
+ new_disc_value = base_unit * line.discount / 100 + per_item_voucher
+ new_disc_pct = (new_disc_value / base_unit) * 100
line.amount_voucher_disc = line_voucher
- line.discount = line_voucher_item
+ line.discount = new_disc_pct
+
+ _logger.info(
+ "[VOUCHER_APPLIED] SO=%s voucher=%s type=%s line_id=%s product=%s qty=%s discount_pct=%.2f amount_voucher=%s",
+ self.name,
+ getattr(self.voucher_id, "code", None),
+ voucher.get("type"),
+ line.id,
+ line.product_id.display_name,
+ line.product_uom_qty,
+ line.discount,
+ line.amount_voucher_disc,
+ )
self.amount_voucher_disc = voucher['discount']['all']
self.applied_voucher_id = self.voucher_id
@@ -3189,6 +3231,10 @@ class SaleOrder(models.Model):
# order._auto_set_shipping_from_website()
order._compute_etrts_date()
order._validate_expected_ready_ship_date()
+ # for line in order.order_line:
+ # updated_vals = line._update_purchase_info()
+ # if updated_vals:
+ # line.write(updated_vals)
# order._validate_delivery_amt()
# order._check_total_margin_excl_third_party()
# order._update_partner_details()
@@ -3287,12 +3333,15 @@ class SaleOrder(models.Model):
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:
+ if vals['payment_term_id'] != customer_payment_term.id and not order.partner_id.id == 29179:
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."
)
+
+ if order.partner_id.id == 29179 and vals['payment_term_id'] not in [25,28]:
+ raise UserError(_("Pilih payment term 60 hari atau 30 hari."))
res = super(SaleOrder, self).write(vals)
@@ -3317,6 +3366,12 @@ class SaleOrder(models.Model):
if any(field in vals for field in ["order_line", "client_order_ref"]):
self._calculate_etrts_date()
+ # for order in self:
+ # for line in order.order_line:
+ # updated_vals = line._update_purchase_info()
+ # if updated_vals:
+ # line.write(updated_vals)
+
return res
def button_refund(self):
diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py
index 1f2ea1fb..1df1a058 100644
--- a/indoteknik_custom/models/sale_order_line.py
+++ b/indoteknik_custom/models/sale_order_line.py
@@ -247,29 +247,29 @@ class SaleOrderLine(models.Model):
margin_per_item = sales_price - purchase_price
line.item_before_margin = margin_per_item
- @api.onchange('vendor_id')
- def onchange_vendor_id(self):
- # TODO : need to change this logic @stephan
- if not self.product_id or self.product_id.type == 'service':
- return
- elif self.product_id.categ_id.id == 34: # finish good / manufacturing only
- cost = self.product_id.standard_price
- self.purchase_price = cost
- elif self.product_id.x_manufacture.override_vendor_id:
- # purchase_price = self.env['purchase.pricelist'].search(
- # [('vendor_id', '=', self.product_id.x_manufacture.override_vendor_id.id),
- # ('product_id', '=', self.product_id.id)],
- # limit=1, order='count_trx_po desc, count_trx_po_vendor desc')
- price, taxes, vendor_id = self._get_purchase_price_by_vendor(self.product_id, self.vendor_id)
- self.purchase_price = price
- self.purchase_tax_id = taxes
- # else:
- # purchase_price = self.env['purchase.pricelist'].search(
- # [('vendor_id', '=', self.vendor_id.id), ('product_id', '=', self.product_id.id)],
- # limit=1, order='count_trx_po desc, count_trx_po_vendor desc')
- # price, taxes = self._get_valid_purchase_price(purchase_price)
- # self.purchase_price = price
- # self.purchase_tax_id = taxes
+ # @api.onchange('vendor_id')
+ # def onchange_vendor_id(self):
+ # # TODO : need to change this logic @stephan
+ # if not self.product_id or self.product_id.type == 'service':
+ # return
+ # elif self.product_id.categ_id.id == 34: # finish good / manufacturing only
+ # cost = self.product_id.standard_price
+ # self.purchase_price = cost
+ # elif self.product_id.x_manufacture.override_vendor_id:
+ # # purchase_price = self.env['purchase.pricelist'].search(
+ # # [('vendor_id', '=', self.product_id.x_manufacture.override_vendor_id.id),
+ # # ('product_id', '=', self.product_id.id)],
+ # # limit=1, order='count_trx_po desc, count_trx_po_vendor desc')
+ # price, taxes, vendor_id = self._get_purchase_price_by_vendor(self.product_id, self.vendor_id)
+ # self.purchase_price = price
+ # self.purchase_tax_id = taxes
+ # # else:
+ # # purchase_price = self.env['purchase.pricelist'].search(
+ # # [('vendor_id', '=', self.vendor_id.id), ('product_id', '=', self.product_id.id)],
+ # # limit=1, order='count_trx_po desc, count_trx_po_vendor desc')
+ # # price, taxes = self._get_valid_purchase_price(purchase_price)
+ # # self.purchase_price = price
+ # # self.purchase_tax_id = taxes
# def _calculate_selling_price(self):
# rec_purchase_price, rec_taxes, rec_vendor_id = self._get_purchase_price(self.product_id)
@@ -512,3 +512,19 @@ class SaleOrderLine(models.Model):
else:
line.product_updatable = False
# line.desc_updatable = False
+
+ @api.onchange('vendor_id')
+ def _onchange_vendor_id_custom(self):
+ self._update_purchase_info()
+
+ def _update_purchase_info(self):
+ if not self.product_id or self.product_id.type == 'service':
+ return
+
+ if self.product_id.categ_id.id == 34:
+ self.purchase_price = self.product_id.standard_price
+ self.purchase_tax_id = False
+ elif self.product_id.x_manufacture.override_vendor_id:
+ price, taxes, vendor_id = self._get_purchase_price_by_vendor(self.product_id, self.vendor_id)
+ self.purchase_price = price
+ self.purchase_tax_id = taxes
diff --git a/indoteknik_custom/models/sj_tele.py b/indoteknik_custom/models/sj_tele.py
index d44aa338..53ba26fc 100644
--- a/indoteknik_custom/models/sj_tele.py
+++ b/indoteknik_custom/models/sj_tele.py
@@ -18,42 +18,24 @@ class SjTele(models.Model):
sale_name = fields.Char(string='Sale Name')
create_date = fields.Datetime(string='Create Date')
date_doc_kirim = fields.Datetime(string='Tanggal Kirim SJ')
-
- # @api.model
- # def run_pentaho_carte(self):
- # carte = "http://127.0.0.1:8080"
- # job_kjb = r"C:/Users/Indoteknik/Desktop/tes.kjb"
- # params = {"job": job_kjb, "level": "Basic", "block": "Y"}
- # try:
- # r = requests.get(
- # f"{carte}/kettle/executeJob/",
- # params=params,
- # auth=("cluster", "cluster"),
- # timeout=900,
- # )
- # r.raise_for_status()
- # # kalau Carte mengembalikan <result>ERROR</result>, anggap gagal
- # if "<result>ERROR</result>" in r.text:
- # raise UserError(f"Carte error: {r.text}")
- # except Exception as e:
- # _logger.exception("Carte call failed: %s", e)
- # raise UserError(f"Gagal memanggil Carte: {e}")
-
- # time.sleep(3)
-
- # self.env['sj.tele'].sudo().woi()
-
- # return True
+ is_sent = fields.Boolean(default=False)
def woi(self):
bot_mqdd = '8203414501:AAHy_XwiUAVrgRM2EJzW7sZx9npRLITZpb8'
chat_id_mqdd = '-1003087280519'
api_base = f'https://api.telegram.org/bot{bot_mqdd}'
+ # bot_testing = '8306689189:AAHEFe5xwAkapoQ8xKoNZs-6gVfv3kO3kaU'
+ # chat_id_testing = '-4920864331'
+ # api_testing = f'https://api.telegram.org/bot{bot_testing}'
+
+ # Select Data
+ data = self.search([('is_sent', '=', False)], order='create_date asc')
- data = self.search([], order='create_date asc', limit=15)
+ # Old
+ # data = self.search([], order='create_date asc')
if not data:
- text = "Berikut merupakan nomor BU/OUT yang belum ada di Logbook SJ report:\n✅ tidak ada data (semua sudah tercatat)."
+ text = "✅ tidak ada data (semua sudah tercatat)."
try:
r = requests.post(api_base + "/sendMessage",
json={'chat_id': chat_id_mqdd, 'text': text},
@@ -74,7 +56,6 @@ class SjTele(models.Model):
dttm = (rec.picking_id.date_doc_kirim if (rec.picking_id and rec.picking_id.date_doc_kirim)
else getattr(rec, 'date_doc_kirim', None))
- # format header tanggal (string), tanpa konversi Waktu/WIB
if dttm:
date_header = dttm if isinstance(dttm, str) else fields.Datetime.to_string(dttm)
date_header = date_header[:10]
@@ -84,19 +65,41 @@ class SjTele(models.Model):
if name:
groups.setdefault(date_header, []).append(f"- ({pid}) - {name} - {so}")
- # build output berurutan per tanggal
for header_date, items in groups.items():
lines.append(header_date)
lines.extend(items)
-
header = "Berikut merupakan nomor BU/OUT yang belum ada di Logbook SJ report:\n"
- text = header + "\n".join(lines)
-
- try:
- r = requests.post(api_base + "/sendMessage",
- json={'chat_id': chat_id_mqdd, 'text': text})
- r.raise_for_status()
- except Exception as e:
- _logger.exception("Gagal kirim Telegram: %s", e)
- return True \ No newline at end of file
+ BUB = 20 # jumlah baris per bubble
+ total = (len(lines) + BUB - 1) // BUB # total bubble
+
+ for i in range(0, len(lines), BUB):
+ body = "\n".join(lines[i:i + BUB])
+ bagian = (i // BUB) + 1
+ text = f"{header}Lampiran ke {bagian}/{total}\n{body}"
+ try:
+ r = requests.post(
+ api_base + "/sendMessage",
+ json={'chat_id': chat_id_mqdd, 'text': text},
+ timeout=20
+ )
+ r.raise_for_status()
+ except Exception as e:
+ _logger.exception("Gagal kirim Telegram (batch %s-%s): %s", i + 1, min(i + BUB, len(lines)), e)
+ time.sleep(5) # jeda kecil biar rapi & aman rate limit
+
+ # Set sent = true ketika sudah terkirim
+ data.write({'is_sent': True})
+
+ return True
+
+ # header = "Berikut merupakan nomor BU/OUT yang belum ada di Logbook SJ report:\n"
+ # text = header + "\n".join(lines)
+ #
+ # try:
+ # r = requests.post(api_base + "/sendMessage",
+ # json={'chat_id': chat_id_mqdd, 'text': text})
+ # r.raise_for_status()
+ # except Exception as e:
+ # _logger.exception("Gagal kirim Telegram: %s", e)
+ # return True \ No newline at end of file
diff --git a/indoteknik_custom/models/stock_move.py b/indoteknik_custom/models/stock_move.py
index d6505a86..1da2befe 100644
--- a/indoteknik_custom/models/stock_move.py
+++ b/indoteknik_custom/models/stock_move.py
@@ -19,6 +19,15 @@ class StockMove(models.Model):
vendor_id = fields.Many2one('res.partner' ,string='Vendor')
hold_outgoingg = fields.Boolean('Hold Outgoing', default=False)
product_image = fields.Binary(related="product_id.image_128", string="Product Image", readonly=True)
+ partial = fields.Boolean('Partial?', default=False)
+
+ # Ambil product uom dari SO line
+ @api.model
+ def create(self, vals):
+ if vals.get('sale_line_id'):
+ sale_line = self.env['sale.order.line'].browse(vals['sale_line_id'])
+ vals['product_uom'] = sale_line.product_uom.id
+ return super().create(vals)
# @api.model_create_multi
# def create(self, vals_list):
@@ -177,3 +186,12 @@ class StockMoveLine(models.Model):
line_no = fields.Integer('No', default=0)
note = fields.Char('Note')
manufacture = fields.Many2one('x_manufactures', string="Brands", related="product_id.x_manufacture", store=True)
+
+ # Ambil uom dari stock move
+ @api.model
+ def create(self, vals):
+ if 'move_id' in vals and 'product_uom_id' not in vals:
+ move = self.env['stock.move'].browse(vals['move_id'])
+ if move.product_uom:
+ vals['product_uom_id'] = move.product_uom.id
+ return super().create(vals) \ No newline at end of file
diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py
index b27e6b5d..d6096cc0 100644
--- a/indoteknik_custom/models/stock_picking.py
+++ b/indoteknik_custom/models/stock_picking.py
@@ -97,6 +97,7 @@ class StockPicking(models.Model):
approval_status = fields.Selection([
('pengajuan1', 'Approval Accounting'),
+ ('pengajuan2', 'Approval Logistic'),
('approved', 'Approved'),
], string='Approval Status', readonly=True, copy=False, index=True, tracking=3,
help="Approval Status untuk Internal Use")
@@ -152,6 +153,7 @@ class StockPicking(models.Model):
state_reserve = fields.Selection([
('waiting', 'Waiting For Fullfilment'),
('ready', 'Ready to Ship'),
+ ('partial', 'Ready to Ship Partial'),
('done', 'Done'),
('cancel', 'Cancelled'),
], string='Status Reserve', tracking=True, copy=False, help="The current state of the stock picking.")
@@ -174,6 +176,29 @@ class StockPicking(models.Model):
linked_manual_bu_out = fields.Many2one('stock.picking', string='BU Out', copy=False)
area_name = fields.Char(string="Area", compute="_compute_area_name")
+ is_bu_iu = fields.Boolean('Is BU/IU', compute='_compute_is_bu_iu', default=False, copy=False, readonl=True)
+
+ @api.depends('name')
+ def _compute_is_bu_iu(self):
+ for record in self:
+ if 'BU/IU' in record.name:
+ record.is_bu_iu = True
+ else:
+ record.is_bu_iu = False
+
+ def action_bu_iu_to_pengajuan2(self):
+ for rec in self:
+ if not rec.is_bu_iu or not rec.is_internal_use:
+ raise UserError(_("Tombol ini hanya untuk dokumen BU/IU - Internal Use."))
+ if rec.approval_status == False:
+ raise UserError("Harus Ask Approval terlebih dahulu")
+ if rec.approval_status in ['pengajuan1'] and self.env.user.is_accounting:
+ rec.approval_status = 'pengajuan2'
+ rec.message_post(body=_("Status naik ke Approval Logistik oleh %s") % self.env.user.display_name)
+ if rec.approval_status in ['pengajuan1', 'pengajuan2', ''] and not self.env.user.is_accounting:
+ raise UserError("Tombol hanya untuk accounting")
+
+ return True
# def _get_biteship_api_key(self):
# # return self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_test')
@@ -189,7 +214,7 @@ class StockPicking(models.Model):
# def write(self, vals):
# if 'linked_manual_bu_out' in vals:
# for record in self:
- # if (record.picking_type_code == 'internal'
+ # if (record.picking_type_code == 'internal'
# and 'BU/PICK/' in record.name):
# # Jika menghapus referensi (nilai di-set False/None)
# if record.linked_manual_bu_out and not vals['linked_manual_bu_out']:
@@ -203,8 +228,8 @@ class StockPicking(models.Model):
# @api.model
# def create(self, vals):
# record = super().create(vals)
- # if (record.picking_type_code == 'internal'
- # and 'BU/PICK/' in record.name
+ # if (record.picking_type_code == 'internal'
+ # and 'BU/PICK/' in record.name
# and vals.get('linked_manual_bu_out')):
# picking = self.env['stock.picking'].browse(vals['linked_manual_bu_out'])
# picking.state_packing = 'packing_done'
@@ -393,7 +418,7 @@ class StockPicking(models.Model):
deadline = kirim_date + timedelta(days=1)
deadline = deadline.replace(hour=10, minute=0, second=0)
- if now > deadline:
+ if now > deadline and not self.so_lama:
raise ValidationError(
_("Anda tidak dapat mengubah Tanggal Kirim setelah jam 10:00 pada hari berikutnya!")
)
@@ -439,15 +464,15 @@ class StockPicking(models.Model):
rec.last_update_date_doc_kirim = datetime.datetime.utcnow()
- @api.constrains('scan_koli_lines')
- def _constrains_scan_koli_lines(self):
- now = datetime.datetime.utcnow()
- for picking in self:
- if len(picking.scan_koli_lines) > 0:
- if len(picking.scan_koli_lines) != picking.total_mapping_koli:
- raise UserError("Scan Koli Tidak Sesuai Dengan Total Mapping Koli")
+ # @api.constrains('scan_koli_lines')
+ # def _constrains_scan_koli_lines(self):
+ # now = datetime.datetime.utcnow()
+ # for picking in self:
+ # if len(picking.scan_koli_lines) > 0:
+ # if len(picking.scan_koli_lines) != picking.total_mapping_koli:
+ # raise UserError("Scan Koli Tidak Sesuai Dengan Total Mapping Koli")
- picking.driver_departure_date = now
+ # picking.driver_departure_date = now
@api.depends('total_so_koli')
def _compute_total_so_koli(self):
@@ -509,7 +534,7 @@ class StockPicking(models.Model):
# rts_days = rts.days
# rts_hours = divmod(rts.seconds, 3600)
- # estimated_by_erts = rts.total_seconds() / 3600
+ # estimated_by_erts = rts.total_seconds() / 3600
# record.countdown_ready_to_ship = f"{rts_days} days, {rts_hours} hours"
# record.countdown_hours = estimated_by_erts
@@ -1080,9 +1105,16 @@ class StockPicking(models.Model):
def ask_approval(self):
- if self.env.user.is_accounting:
+ # if self.env.user.is_accounting:
+ # if self.env.user.is_accounting and self.location_id.id == 57 or self.location_id == 57 and self.approval_status in ['pengajuan1', ''] and 'BU/IU' in self.name and self.approval_status == 'pengajuan1':
+ # raise UserError("Bisa langsung set ke approval logistik")
+ if self.env.user.is_accounting and self.approval_status == "pengajuan2" and 'BU/IU' in self.name:
+ raise UserError("Tidak perlu ask approval sudah approval logistik")
+ if self.env.user.is_logistic_approver and self.location_id.id == 57 or self.location_id== 57 and self.approval_status == 'pengajuan2' and 'BU/IU' in self.name:
raise UserError("Bisa langsung Validate")
+
+
# for calendar distribute only
# if self.is_internal_use:
# stock_move_lines = self.env['stock.move.line'].search([
@@ -1105,6 +1137,7 @@ class StockPicking(models.Model):
raise UserError("Qty tidak boleh 0")
pick.approval_status = 'pengajuan1'
+
def ask_receipt_approval(self):
if self.env.user.is_logistic_approver:
raise UserError('Bisa langsung validate tanpa Ask Receipt')
@@ -1270,6 +1303,9 @@ class StockPicking(models.Model):
and self.create_date > threshold_datetime
and not self.so_lama):
raise UserError(_("Tidak ada scan koli! Harap periksa kembali."))
+
+ if 'BU/OUT/' in self.name:
+ self.driver_departure_date = datetime.datetime.utcnow()
# if self.driver_departure_date == False and 'BU/OUT/' in self.name and self.picking_type_code == 'outgoing':
# raise UserError(_("Isi Driver Departure Date dulu sebelum validate"))
@@ -1306,7 +1342,16 @@ class StockPicking(models.Model):
if self.picking_type_id.code == 'incoming' and self.group_id.id == False and self.is_internal_use == False:
raise UserError(_('Tidak bisa Validate jika tidak dari Document SO / PO'))
- if self.is_internal_use and not self.env.user.is_accounting:
+ # if self.is_internal_use and not self.env.user.is_logistic_approver and self.location_id.id == 57 and self.approval_status == 'pengajuan2':
+ # raise UserError("Harus di Approve oleh Logistik")
+
+ if self.is_internal_use and self.approval_status in ['pengajuan1', '', False] and 'BU/IU' in self.name and self.is_bu_iu == True:
+ raise UserError("Tidak Bisa Validate, set approval status ke approval logistik terlebih dahhulu")
+
+ if self.is_internal_use and not self.env.user.is_logistic_approver and self.approval_status in ['pengajuan2'] and self.is_bu_iu == True and 'BU/IU' in self.name:
+ raise UserError("Harus di Approve oleh Logistik")
+
+ if self.is_internal_use and not self.env.user.is_accounting and self.approval_status in ['pengajuan1', '', False] and self.is_bu_iu == False:
raise UserError("Harus di Approve oleh Accounting")
if self.picking_type_id.id == 28 and not self.env.user.is_logistic_approver:
@@ -1315,7 +1360,7 @@ class StockPicking(models.Model):
if self.location_dest_id.id == 47 and not self.env.user.is_purchasing_manager:
raise UserError("Transfer ke gudang selisih harus di approve Rafly Hanggara")
- if self.is_internal_use:
+ if self.is_internal_use and self.approval_status == 'pengajuan2':
self.approval_status = 'approved'
elif self.picking_type_id.code == 'incoming':
self.approval_receipt_status = 'approved'
@@ -1332,7 +1377,7 @@ class StockPicking(models.Model):
current_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.date_reserved = current_time
-
+
# Validate Qty Demand Can't higher than Qty Product
if self.location_dest_id.id == 58 and 'BU/INPUT/' in self.name:
for move in self.move_ids_without_package:
@@ -1353,14 +1398,14 @@ class StockPicking(models.Model):
if self.picking_type_code == 'outgoing' and 'BU/OUT/' in self.name:
self.check_koli()
res = super(StockPicking, self).button_validate()
-
+
# Penambahan link PO di Stock Journal untuk Picking BD
for picking in self:
if picking.name and 'BD/' in picking.name and picking.purchase_id:
stock_journal = self.env['account.move'].search([
('ref', 'ilike', picking.name + '%'),
- ('journal_id', '=', 3) # Stock Journal ID
- ], limit = 1)
+ ('journal_id', '=', 3) # Stock Journal ID
+ ], limit=1)
if stock_journal:
stock_journal.write({
'purchase_order_id': picking.purchase_id.id
@@ -2537,9 +2582,22 @@ class ScanKoli(models.Model):
out_moves = self.env['stock.move.line'].search([('picking_id', '=', picking.linked_out_picking_id.id)])
for pick_move in pick_moves:
- corresponding_out_move = out_moves.filtered(lambda m: m.product_id == pick_move.product_id)
- if corresponding_out_move:
- corresponding_out_move.qty_done += pick_move.qty_done
+ corresponding_out_moves = out_moves.filtered(lambda m: m.product_id == pick_move.product_id)
+
+ if len(corresponding_out_moves) == 1:
+ corresponding_out_moves.qty_done += pick_move.qty_done
+
+ elif len(corresponding_out_moves) > 1:
+ qty_koli = pick_move.qty_done
+ for out_move in corresponding_out_moves:
+ if qty_koli <= 0:
+ break
+ # ambil sesuai kebutuhan atau sisa qty
+ qty_to_assign = min(qty_koli, out_move.product_uom_qty)
+ out_move.qty_done += qty_to_assign
+ qty_koli -= qty_to_assign
+
+
def _reset_qty_done_if_no_scan(self, picking_id):
product_bu_pick = self.env['stock.move.line'].search([('picking_id', '=', picking_id)])
diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py
index c683f75a..cb630a04 100644
--- a/indoteknik_custom/models/tukar_guling.py
+++ b/indoteknik_custom/models/tukar_guling.py
@@ -538,6 +538,11 @@ class TukarGuling(models.Model):
self.state = 'approval_sales'
def update_doc_state(self):
+ bu_pick = self.env['stock.picking'].search([
+ ('origin', '=', self.operations.origin),
+ ('name', 'ilike', 'BU/PICK'),
+ ])
+
# 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([
@@ -552,7 +557,26 @@ class TukarGuling(models.Model):
if self.state == 'approved' and total_out > 0 and done_out == total_out:
self.state = 'done'
- # OUT revisi SO
+ #SO Lama (gk ada bu pick)
+ elif self.operations.picking_type_id.id == 29 and self.return_type == 'retur_so' and not bu_pick:
+ # so_lama = self.env['sale.order'].search([
+ # ('name', '=', self.operations.origin),
+ # ('state', '=', 'done'),
+ # ('group_id.name', '=', self.operations.origin)
+ # ])
+ total_ort = self.env['stock.picking'].search_count([
+ ('tukar_guling_id', '=', self.id),
+ ('picking_type_id', '=', 74),
+ ])
+ done_srt = self.env['stock.picking'].search([
+ ('tukar_guling_id', '=', self.id),
+ ('picking_type_id', '=', 73),
+ ('state', '=', 'done')
+ ])
+ if self.state == 'approved' and total_ort == 0 and done_srt and not bu_pick:
+ self.state = 'done'
+
+ # OUT retur SO
elif self.operations.picking_type_id.id == 29 and self.return_type == 'retur_so':
total_ort = self.env['stock.picking'].search_count([
('tukar_guling_id', '=', self.id),
diff --git a/indoteknik_custom/models/user_pengajuan_tempo_request.py b/indoteknik_custom/models/user_pengajuan_tempo_request.py
index 600381c0..6e8498f7 100644
--- a/indoteknik_custom/models/user_pengajuan_tempo_request.py
+++ b/indoteknik_custom/models/user_pengajuan_tempo_request.py
@@ -365,7 +365,7 @@ class UserPengajuanTempoRequest(models.Model):
@api.onchange('tempo_duration')
def _tempo_duration_change(self):
for tempo in self:
- if tempo.env.user.id not in (7, 688, 28, 377, 12182, 375):
+ if tempo.env.user.id not in (7, 688, 28, 377, 12182, 375, 19):
raise UserError("Durasi tempo hanya bisa di ubah oleh Sales Manager atau Direktur")
@api.onchange('tempo_limit')
@@ -381,7 +381,8 @@ class UserPengajuanTempoRequest(models.Model):
if tempo.env.user.id in (688, 28, 7):
raise UserError("Pengajuan tempo harus di approve oleh sales manager terlebih dahulu")
else:
- if tempo.env.user.id not in (375, 19):
+ # if tempo.env.user.id not in (375, 19):
+ if tempo.env.user.id != 19:
# if tempo.env.user.id != 12182:
raise UserError("Pengajuan tempo hanya bisa di approve oleh sales manager")
else:
diff --git a/indoteknik_custom/report/report_surat_piutang copy.xml b/indoteknik_custom/report/report_surat_piutang copy.xml
deleted file mode 100644
index cb5762f3..00000000
--- a/indoteknik_custom/report/report_surat_piutang copy.xml
+++ /dev/null
@@ -1,149 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
- <data>
-
- <!-- External Layout tanpa company -->
- <template id="external_layout_no_company">
- <!-- HEADER -->
- <div class="header">
- <img t-att-src="'https://erp.indoteknik.com/api/image/ir.attachment/datas/2498521'"
- class="img img-fluid w-100"/>
- </div>
-
- <!-- CONTENT -->
- <div class="content mt-5 mb-5 ml-3 mr-3">
- <t t-raw="0"/>
- </div>
-
- <!-- FOOTER -->
- <div class="footer">
- <img t-att-src="'https://erp.indoteknik.com/api/image/ir.attachment/datas/2498529'"
- style="height:60px;"/>
- </div>
- </template>
-
- <!-- Report Action -->
- <record id="action_report_surat_piutang" model="ir.actions.report">
- <field name="name">Surat Peringatan Piutang</field>
- <field name="model">surat.piutang</field>
- <field name="report_type">qweb-pdf</field>
- <field name="report_name">indoteknik_custom.report_surat_piutang_formal_custom</field>
- <field name="report_file">indoteknik_custom.report_surat_piutang_formal_custom</field>
- <field name="binding_model_id" ref="model_surat_piutang"/>
- <field name="binding_type">report</field>
- </record>
-
- <!-- QWeb Template Surat -->
- <template id="report_surat_piutang_formal_custom">
- <t t-call="indoteknik_custom.external_layout_no_company">
- <t t-set="doc" t-value="docs[0] if docs else None"/>
-
- <!-- SURAT CONTENT -->
- <main class="o_report_layout_standard" style="font-size:12pt; font-family: Arial, sans-serif;">
-
- <!-- Header Surat -->
- <div class="row mb-3">
- <div class="col-6">
- Ref. No: <t t-esc="doc.name or '-'"/>
- </div>
- <div class="col-6 text-right">
- Jakarta, <t t-esc="doc.send_date and doc.send_date.strftime('%d %B %Y') or '-'"/>
- </div>
- </div>
-
- <!-- Tujuan -->
- <div class="mb-3">
- <strong>Kepada Yth.</strong><br/>
- <t t-esc="doc.partner_id.name if doc and doc.partner_id else '-'"/><br/>
- <t t-esc="doc.partner_id.street if doc and doc.partner_id else '-'"/><br/>
- <t t-esc="doc.partner_id.country_id.name if doc and doc.partner_id and doc.partner_id.country_id else '-'"/>
- </div>
-
- <!-- UP & Perihal -->
- <div class="mb-4">
- U.P. : <t t-esc="doc.tujuan_nama or '-'"/><br/>
- <strong>Perihal:</strong> <t t-esc="doc.perihal or '-'"/>
- </div>
-
- <!-- Isi Surat -->
- <div class="mb-3">Dengan Hormat,</div>
- <div class="mb-3">Yang bertanda tangan di bawah ini menyampaikan sebagai berikut:</div>
-
- <div class="mb-3 text-justify">
- Namun, bersama surat ini kami ingin mengingatkan bahwa hingga tanggal surat ini dibuat, masih terdapat tagihan yang belum diselesaikan oleh pihak
- <t t-esc="doc.partner_id.name if doc and doc.partner_id else '-'"/> periode bulan <t t-esc="doc.periode_invoices_terpilih or '-'"/>, berdasarkan data korespondensi dan laporan keuangan yang kami kelola,
- <t t-esc="doc.partner_id.name if doc and doc.partner_id else '-'"/> (“Saudara”) masih mempunyai tagihan yang telah jatuh tempo dan belum dibayarkan sejumlah
- <t t-esc="doc.grand_total_text or '-'"/> (“Tagihan”).
- </div>
-
- <div class="mb-3">Berikut kami lampirkan Rincian Tagihan yang telah Jatuh Tempo:</div>
-
- <!-- Tabel Invoice -->
- <table class="table table-sm table-bordered mb-4">
- <thead class="thead-light">
- <tr>
- <th>Invoice Number</th>
- <th>Invoice Date</th>
- <th>Due Date</th>
- <th class="text-center">Day to Due</th>
- <th>Reference</th>
- <th class="text-right">Amount Due</th>
- <th>Payment Terms</th>
- </tr>
- </thead>
- <tbody>
- <t t-foreach="doc.line_ids.filtered(lambda l: l.selected)" t-as="line">
- <tr>
- <td><t t-esc="line.invoice_number or '-'"/></td>
- <td><t t-esc="line.invoice_date and line.invoice_date.strftime('%d-%m-%Y') or '-'"/></td>
- <td><t t-esc="line.invoice_date_due and line.invoice_date_due.strftime('%d-%m-%Y') or '-'"/></td>
- <td class="text-center"><t t-esc="line.new_invoice_day_to_due or '-'"/></td>
- <td><t t-esc="line.ref or '-'"/></td>
- <td class="text-right"><t t-esc="line.amount_residual or '-'"/></td>
- <td><t t-esc="line.payment_term_id.name or '-'"/></td>
- </tr>
- </t>
- </tbody>
- <tfoot>
- <tr class="font-weight-bold">
- <td colspan="6" class="text-right">
- GRAND TOTAL INVOICE YANG BELUM DIBAYAR DAN TELAH JATUH TEMPO
- </td>
- <td class="text-right">
- <t t-esc="doc.grand_total or '-'"/> (<t t-esc="doc.grand_total_text or '-'"/>)
- </td>
- </tr>
- </tfoot>
- </table>
-
- <!-- Isi Penutup -->
- <div class="mb-3">
- Kami belum menerima konfirmasi pelunasan ataupun pembayaran sebagian dari total kewajiban tersebut. Kami sangat terbuka untuk berdiskusi serta mencari solusi terbaik agar kerja sama tetap berjalan baik.
- </div>
-
- <div class="mb-3">
- Oleh karena itu, kami mohon perhatian dan itikad baik dari pihak <t t-esc="doc.partner_id.name if doc and doc.partner_id else '-'"/> untuk segera melakukan pelunasan atau memberikan informasi terkait rencana pembayaran paling lambat dalam waktu 7 (tujuh) hari kerja sejak surat ini diterima.
- </div>
-
- <div class="mb-3">
- Jika dalam waktu yang telah ditentukan belum ada penyelesaian atau tanggapan, kami akan mempertimbangkan untuk melanjutkan proses sesuai ketentuan yang berlaku.
- </div>
-
- <div class="mb-4">
- Demikian kami sampaikan. Atas perhatian dan kerja samanya, kami ucapkan terima kasih.
- </div>
-
- <div class="mb-2">Hormat kami,</div>
-
- <!-- TTD -->
- <div class="mt-5">
- <img t-att-src="'https://erp.indoteknik.com/api/image/ir.attachment/datas/2851919'" style="width:200px; height:auto;"/><br/>
- <div>Nama: Akbar Prabawa<br/>Jabatan: General Manager</div>
- </div>
-
- </main>
- </t>
- </template>
-
- </data>
-</odoo>
diff --git a/indoteknik_custom/report/report_surat_piutang.xml b/indoteknik_custom/report/report_surat_piutang.xml
index 62db7982..f41ae604 100644
--- a/indoteknik_custom/report/report_surat_piutang.xml
+++ b/indoteknik_custom/report/report_surat_piutang.xml
@@ -10,8 +10,8 @@
<field name="report_name">indoteknik_custom.report_surat_piutang</field>
<field name="report_file">indoteknik_custom.report_surat_piutang</field>
<field name="print_report_name">'%s - %s' % (object.perihal_label or '', object.partner_id.name or '')</field>
- <field name="binding_model_id" ref="model_surat_piutang"/>
- <field name="binding_type">report</field>
+ <!-- <field name="binding_model_id" ref="model_surat_piutang"/>
+ <field name="binding_type">report</field> -->
</record>
<template id="external_layout_surat_piutang">
@@ -216,7 +216,7 @@
<div style="height:120px; position: relative;">
<t t-if="doc.perihal != 'penagihan'">
<img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2851919"
- style="width:300px; height:auto; margin-top:-40px;"/>
+ style="width:300px; height:auto; margin-top:-40px; margin-left:-20px;"/>
</t>
<t t-else="">
<div style="height:100px;"></div>
diff --git a/indoteknik_custom/report/report_tutup_tempo.xml b/indoteknik_custom/report/report_tutup_tempo.xml
new file mode 100644
index 00000000..1aa1367d
--- /dev/null
+++ b/indoteknik_custom/report/report_tutup_tempo.xml
@@ -0,0 +1,158 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+
+ <record id="action_report_surat_tutup_tempo" model="ir.actions.report">
+ <field name="name">Surat Penutupan Tempo</field>
+ <field name="model">surat.piutang</field>
+ <field name="report_type">qweb-pdf</field>
+ <field name="report_name">indoteknik_custom.report_surat_tutup_tempo</field>
+ <field name="report_file">indoteknik_custom.report_surat_tutup_tempo</field>
+ <field name="print_report_name">'%s - %s' % (object.perihal_label or '', object.partner_id.name or '')</field>
+ <!-- <field name="binding_model_id" ref="model_surat_piutang"/>
+ <field name="binding_type">report</field> -->
+ </record>
+
+ <template id="external_layout_surat_tutup_tempo">
+ <t t-call="web.html_container">
+ <div class="header">
+ <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2498521"
+ style="width:100%; display: block;"/>
+ </div>
+ <div class="article" style="margin: 0 1.5cm 0 1.5cm; ">
+ <t t-raw="0"/>
+ </div>
+ <div class="footer">
+ <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2859765"
+ style="width:100%; display: block;"/>
+ </div>
+ </t>
+ </template>
+
+ <template id="report_surat_tutup_tempo">
+ <t t-call="web.html_container">
+ <t t-foreach="docs" t-as="doc">
+ <t t-call="indoteknik_custom.report_surat_tutup_tempo_document"
+ t-lang="doc.partner_id.lang"/>
+ </t>
+ </t>
+ </template>
+
+ <template id="report_surat_tutup_tempo_document">
+ <t t-call="indoteknik_custom.external_layout_surat_tutup_tempo">
+ <t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
+ <div class="page">
+
+ <div class="row mb3">
+ <div class="col-6">
+ Ref. No: <t t-esc="doc.name or '-'"/>
+ </div>
+ <div class="col-6 text-right">
+ Jakarta, <t t-esc="doc.send_date and doc.send_date.strftime('%d %B %Y') or '-'"/>
+ </div>
+ </div>
+ <br/>
+ <div class="mb3" style="max-width:500px; word-wrap:break-word; white-space:normal;">
+ <strong>Kepada Yth.</strong><br/>
+ <strong><t t-esc="doc.partner_id.name or '-'"/></strong><br/>
+ <span style="display:inline-block; max-width:400px; word-wrap:break-word; white-space:normal;">
+ <t t-esc="doc.partner_id.street or ''"/>
+ </span><br/>
+ <u>Republik Indonesia</u>
+ </div>
+ <br/>
+ <table style="margin-left:2cm;">
+ <tr style="font-weight: bold;">
+ <td style="padding-right:10px;">U.P.</td>
+ <td style="white-space: nowrap;">: <t t-esc="doc.tujuan_nama or '-'"/></td>
+ </tr>
+ <tr style="font-weight: bold;">
+ <td style="padding-right:10px;">Perihal</td>
+ <td>: <u><t t-esc="doc.perihal_label or '-'"/></u></td>
+ </tr>
+ </table>
+ <br/>
+ <p><strong>Dengan Hormat,</strong></p>
+ <t t-set="selected_lines" t-value="doc.line_ids.filtered(lambda l: l.selected)"/>
+ <t t-set="line_count" t-value="len(selected_lines)"/>
+ <t t-if="line_count == 1">
+ <t t-set="line" t-value="selected_lines[0]"/>
+ <p class="text-justify">
+ Berdasarkan catatan kami, pembayaran atas invoice
+ <strong><t t-esc="line.invoice_number"/></strong>
+ yang jatuh tempo pada tanggal
+ <strong><t t-esc="line.invoice_date_due and line.invoice_date_due.strftime('%d %B %Y')"/></strong>
+ telah melewati batas waktu 30 (tiga puluh) hari. Sehubungan dengan hal tersebut, bersama ini kami sampaikan kebijakan perusahaan sebagai berikut:
+ </p>
+ </t>
+
+ <t t-else="">
+ <p class="text-justify">
+ Berdasarkan catatan kami, pembayaran atas beberapa invoice yang telah melewati batas waktu 30 (tiga puluh) hari adalah sebagai berikut:
+ </p>
+
+ <table class="table table-sm" style="font-size:13px; border:1px solid #000; margin-top:16px; margin-bottom:16px;">
+ <thead style="background:#f5f5f5;">
+ <tr>
+ <th style="border:1px solid #000; padding:4px; font-weight: bold;">Invoice</th>
+ <th style="border:1px solid #000; padding:4px; font-weight: bold;">Due Date</th>
+ <th style="border:1px solid #000; padding:4px; font-weight: bold;" class="text-center">Day to Due</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr t-foreach="selected_lines" t-as="line">
+ <td style="border:1px solid #000; padding:4px;"><t t-esc="line.invoice_number"/></td>
+ <td style="border:1px solid #000; padding:4px;"><t t-esc="line.invoice_date_due and line.invoice_date_due.strftime('%d-%m-%Y')"/></td>
+ <td style="border:1px solid #000; padding:4px;" class="text-center"><t t-esc="line.new_invoice_day_to_due"/></td>
+ </tr>
+ </tbody>
+ </table>
+
+ <p class="text-justify">
+ Sehubungan dengan hal tersebut, bersama ini kami sampaikan kebijakan perusahaan sebagai berikut:
+ </p>
+ </t>
+
+ <ol style="padding-left: 1.5em; margin-bottom: 1em;">
+ <li class="text-justify" style="margin-bottom: 0.5em;">
+ Secara sistem, akun akan otomatis terkunci (locked) apabila pembayaran telah melewati 30 (tiga puluh) hari dari tanggal jatuh tempo.
+ </li>
+ <li class="text-justify" style="margin-bottom: 0.5em;">
+ Payment term yang semula Tempo akan otomatis berubah menjadi <strong>Cash Before Delivery (CBD)</strong>.
+ </li>
+ <li class="text-justify">
+ Apabila Bapak/Ibu telah melakukan konfirmasi pembayaran atau memberikan informasi lanjutan terkait pelunasan, maka payment term dapat dibukakan kembali menjadi Tempo berdasarkan pengajuan dari tim Sales kami.
+ </li>
+ </ol>
+
+ <p class="text-justify">
+ Kebijakan ini kami terapkan untuk menjaga kelancaran proses transaksi serta memastikan hubungan kerja sama dapat terus berjalan dengan baik.
+ </p>
+
+ <p class="text-justify">
+ Atas perhatian dan kerja samanya kami ucapkan terima kasih.
+ </p>
+ <div class="mt32" style="page-break-inside: avoid;">
+ <p>Hormat kami,<br/>
+ <strong>PT. Indoteknik Dotcom Gemilang</strong>
+ </p>
+ <div style="height:120px; position: relative;">
+ <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2869838"
+ style="width:300px; height:auto; margin-left:-20px;"/>
+ </div>
+ <table style="margin-top:10px;">
+ <tr style="border-top:1px solid #000; font-weight: bold;">
+ <td style="padding-right:50px; white-space: nowrap;">Nama</td>
+ <td>: Stephan Christianus</td>
+ </tr>
+ <tr style="font-weight: bold;">
+ <td style="padding-right:50px; white-space: nowrap;">Jabatan</td>
+ <td>: FAT Manager</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </t>
+ </template>
+ </data>
+</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv
index 17372e48..b9934d7a 100755
--- a/indoteknik_custom/security/ir.model.access.csv
+++ b/indoteknik_custom/security/ir.model.access.csv
@@ -185,6 +185,9 @@ access_v_sale_notin_matchpo,access.v.sale.notin.matchpo,model_v_sale_notin_match
access_approval_payment_term,access.approval.payment.term,model_approval_payment_term,,1,1,1,1
access_purchase_order_update_date_wizard,access.purchase.order.update.date.wizard,model_purchase_order_update_date_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_partial_delivery_wizard,access.partial.delivery.wizard,model_partial_delivery_wizard,,1,1,1,1
+access_partial_delivery_wizard_line,access.partial.delivery.wizard.line,model_partial_delivery_wizard_line,,1,1,1,1
+access_apo_domain_config,access.apo.domain.config,model_apo_domain_config,base.group_user,1,1,1,1
access_refund_sale_order,access.refund.sale.order,model_refund_sale_order,base.group_user,1,1,1,1
access_refund_sale_order_line,access.refund.sale.order.line,model_refund_sale_order_line,base.group_user,1,1,1,1
@@ -198,9 +201,9 @@ access_tukar_guling_mapping_koli_all_users,tukar.guling.mapping.koli.all.users,m
access_sync_promise_date_wizard,access.sync.promise.date.wizard,model_sync_promise_date_wizard,base.group_user,1,1,1,1
access_sync_promise_date_wizard_line,access.sync.promise.date.wizard.line,model_sync_promise_date_wizard_line,base.group_user,1,1,1,1
access_change_date_planned_wizard,access.change.date.planned.wizard,model_change_date_planned_wizard,,1,1,1,1
-access_unpaid_invoice_view,access.unpaid.invoice.view,model_unpaid_invoice_view,base.group_user,1,1,1,1
-access_surat_piutang_user,surat.piutang user,model_surat_piutang,base.group_user,1,1,1,1
-access_surat_piutang_line_user,surat.piutang.line user,model_surat_piutang_line,base.group_user,1,1,1,1
+access_unpaid_invoice_view,access.unpaid.invoice.view,model_unpaid_invoice_view,,1,1,1,1
+access_surat_piutang_user,surat.piutang user,model_surat_piutang,,1,1,1,1
+access_surat_piutang_line_user,surat.piutang.line user,model_surat_piutang_line,,1,1,1,1
access_sj_tele,access.sj.tele,model_sj_tele,base.group_system,1,1,1,1
access_sourcing_job_order,access.sourcing_job_order,model_sourcing_job_order,base.group_system,1,1,1,1
access_sourcing_job_order_line_user,sourcing.job.order.line,model_sourcing_job_order_line,base.group_user,1,1,1,1
diff --git a/indoteknik_custom/views/account_asset_views.xml b/indoteknik_custom/views/account_asset_views.xml
index 90c53623..776ab51f 100644
--- a/indoteknik_custom/views/account_asset_views.xml
+++ b/indoteknik_custom/views/account_asset_views.xml
@@ -12,6 +12,9 @@
type="object"
/>
</button>
+ <field name="invoice_id" position="after">
+ <field name="asset_type"/>
+ </field>
</field>
</record>
</data>
diff --git a/indoteknik_custom/views/approval_payment_term.xml b/indoteknik_custom/views/approval_payment_term.xml
index b0b99689..090c9b5c 100644
--- a/indoteknik_custom/views/approval_payment_term.xml
+++ b/indoteknik_custom/views/approval_payment_term.xml
@@ -32,6 +32,10 @@
<field name="arch" type="xml">
<form>
<header>
+ <!-- <button name="button_closing_mail"
+ string="Send Closing by Email"
+ type="object"
+ /> -->
<button name="button_approve"
string="Approve"
type="object"
diff --git a/indoteknik_custom/views/close_tempo_mail_template.xml b/indoteknik_custom/views/close_tempo_mail_template.xml
new file mode 100644
index 00000000..0f16d3ef
--- /dev/null
+++ b/indoteknik_custom/views/close_tempo_mail_template.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data noupdate="0">
+ <record id="close_tempo_mail_template" model="mail.template">
+ <field name="name">Surat Penutupan Tempo</field>
+ <field name="model_id" ref="indoteknik_custom.model_surat_piutang"/>
+ <field name="subject">${object.perihal_label} - ${object.partner_id.name}</field>
+ <field name="email_from">finance@indoteknik.co.id</field>
+ <field name="email_to">${object.tujuan_email}</field>
+ <field name="body_html" type="html">
+ <div style="font-family:Arial, sans-serif; font-size:13px; color:#333;">
+ <div><b>Dengan hormat,</b></div>
+ <br/>
+ <div>Kepada Yth.</div>
+ <div><b>Manajemen ${object.partner_id.name}</b></div>
+ <br/>
+ <div>
+ Bersama email ini, kami sampaikan surat pemberitahuan resmi terkait <strong>penutupan pembayaran tempo</strong>
+ yang selama ini berlaku bagi ${object.partner_id.name}.
+ </div>
+ <br/>
+
+ <div>
+ <b>Terhitung mulai tanggal ${object.due_date}</b>,
+ seluruh transaksi dengan ${object.partner_id.name} akan diberlakukan dengan sistem pembayaran
+ <b>Cash Before Delivery (CBD)</b>.
+ </div>
+
+ <p>
+ Adapun surat resminya kami lampirkan, apabila diperlukan klarifikasi atau penyesuaian terkait kebijakan ini,
+ kami terbuka untuk mendiskusikannya lebih lanjut.
+ </p>
+ <br/>
+ <p>
+ Atas perhatian dan kerja samanya, kami ucapkan terima kasih.
+ </p>
+
+ <br/><br/>
+ <p>
+ <b>
+ Best Regards,<br/><br/>
+ Widya R.<br/>
+ Dept. Finance<br/>
+ PT. INDOTEKNIK DOTCOM GEMILANG<br/>
+ <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2135765"
+ alt="Indoteknik" style="max-width:18%; height:auto;"/><br/>
+ <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>
+ </div>
+ </field>
+ <field name="auto_delete" eval="True"/>
+ </record>
+ </data>
+</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/domain_apo.xml b/indoteknik_custom/views/domain_apo.xml
new file mode 100644
index 00000000..1dae473d
--- /dev/null
+++ b/indoteknik_custom/views/domain_apo.xml
@@ -0,0 +1,46 @@
+<odoo>
+ <record id="view_apo_domain_config_tree" model="ir.ui.view">
+ <field name="name">apo.domain.config.tree</field>
+ <field name="model">apo.domain.config</field>
+ <field name="arch" type="xml">
+ <tree>
+ <field name="name"/>
+ <field name="vendor_id"/>
+ <field name="brand_ids"/>
+ <field name="is_special"/>
+ <field name="payment_term_id"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="view_apo_domain_config_form" model="ir.ui.view">
+ <field name="name">apo.domain.config.form</field>
+ <field name="model">apo.domain.config</field>
+ <field name="arch" type="xml">
+ <form>
+ <sheet>
+ <group>
+ <field name="name"/>
+ <field name="vendor_id"/>
+ <field name="brand_ids"/>
+ <field name="is_special"/>
+ <field name="payment_term_id"/>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="domain_apo_action" model="ir.actions.act_window">
+ <field name="name">Domain APO</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">apo.domain.config</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ <menuitem id="menu_automatic_purchase"
+ name="Domain APO"
+ action="domain_apo_action"
+ parent="menu_monitoring_in_purchase"
+ sequence="200"/>
+</odoo>
diff --git a/indoteknik_custom/views/letter_receivable.xml b/indoteknik_custom/views/letter_receivable.xml
index 98ea7768..3241d5f1 100644
--- a/indoteknik_custom/views/letter_receivable.xml
+++ b/indoteknik_custom/views/letter_receivable.xml
@@ -26,6 +26,7 @@
<form string="Surat Piutang">
<header>
<field name="state" widget="statusbar" statusbar_visible="draft,waiting_approval_sales,waiting_approval_pimpinan,sent"/>
+ <button name="action_print" string="Print" type="object" />
<button name="action_approve"
type="object"
string="Approve"
@@ -47,6 +48,12 @@
<div class="alert alert-info"
role="alert"
style="height: 40px; margin-bottom:0px;"
+ attrs="{'invisible': ['|', ('perihal', '!=', 'tutup_tempo'), ('state', '!=', 'waiting_approval_pimpinan')]}">
+ <strong>Info!</strong> Surat penutupan tempo telah diajukan &amp; surat otomatis terkirim bila telah di approve.
+ </div>
+ <div class="alert alert-info"
+ role="alert"
+ style="height: 40px; margin-bottom:0px;"
attrs="{'invisible': ['|', ('perihal', '!=', 'penagihan'), ('state', '!=', 'waiting_approval_pimpinan')]}">
<strong>Info!</strong> Surat resmi penagihan telah diajukan &amp; surat otomatis terkirim bila telah di approve.
</div>
@@ -60,7 +67,7 @@
<div class="alert alert-info"
role="alert"
style="margin-bottom:0px;"
- attrs="{'invisible': ['|', ('perihal', '=', 'penagihan'), ('state', '!=', 'waiting_approval_pimpinan')]}">
+ attrs="{'invisible': ['|', ('perihal', 'in', ['penagihan', 'tutup_tempo']), ('state', '!=', 'waiting_approval_pimpinan')]}">
<strong>Info!</strong> Surat peringatan piutang ini sedang menunggu persetujuan dari <b>Pimpinan</b>.
Silakan hubungi Pimpinan terkait untuk melakukan approval agar surat dapat terkirim otomatis ke customer.
</div>
@@ -81,6 +88,7 @@
<field name="tujuan_nama" attrs="{'readonly':[('state','=','sent')]}"/>
<field name="tujuan_email" attrs="{'readonly':[('state','=','sent')]}"/>
<field name="perihal" attrs="{'readonly':[('state','=','sent')]}"/>
+ <field name="due_date" attrs="{'invisible': [('perihal', '!=', 'tutup_tempo')]}"/>
<field name="partner_id" options="{'no_create': True}" attrs="{'readonly':[('state','=','sent')]}"/>
</group>
<group>
@@ -120,11 +128,23 @@
</div>
</div>
<div>
+ <button name="action_select_all_lines"
+ type="object"
+ string="Select All"
+ class="btn btn-secondary"
+ icon="fa-check-square"/>
+ <button name="action_unselect_all_lines"
+ type="object"
+ string="Unselect All"
+ class="btn btn-secondary"
+ icon="fa-square-o"
+ style="margin-left:5px;"/>
<button name="action_refresh_lines"
string="Refresh Invoices"
type="object"
- class="btn-primary"
- style="margin-left:10px;"
+ class="btn btn-secondary"
+ icon="fa-refresh"
+ style="margin-left:5px;"
help="Refresh Invoices agar data tetap update"/>
</div>
</div>
diff --git a/indoteknik_custom/views/mail_template_closing_apt.xml b/indoteknik_custom/views/mail_template_closing_apt.xml
new file mode 100644
index 00000000..5df2813b
--- /dev/null
+++ b/indoteknik_custom/views/mail_template_closing_apt.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data noupdate="0">
+ <record id="mail_template_closing_apt" model="mail.template">
+ <field name="name"></field>
+ <field name="model_id" ref="indoteknik_custom.model_approval_payment_term"/>
+ <field name="subject"></field>
+ <field name="email_from">finance@indoteknik.co.id</field>
+ <field name="email_to"></field>
+ <field name="body_html" type="html">
+ <div style="font-family:Arial, sans-serif; font-size:13px; color:#333;">
+ <p>
+ <b>
+ Best Regards,<br/><br/>
+ Widya R.<br/>
+ Dept. Finance<br/>
+ PT. INDOTEKNIK DOTCOM GEMILANG<br/>
+ <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2135765"
+ alt="Indoteknik" style="max-width:18%; height:auto;"/><br/>
+ <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 style="font-size:11px; color:#777;">
+ <i>Email ini dikirim secara otomatis. Abaikan jika pembayaran telah dilakukan.</i>
+ </p> -->
+ </div>
+ </field>
+ <field name="auto_delete" eval="True"/>
+ </record>
+ </data>
+</odoo>
diff --git a/indoteknik_custom/views/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml
index 7feec934..09d901b9 100755
--- a/indoteknik_custom/views/purchase_order.xml
+++ b/indoteknik_custom/views/purchase_order.xml
@@ -144,6 +144,7 @@
<field name="cost_service_per_item" optional="hide"/>
<field name="contribution_cost_service" optional="hide"/>
<field name="ending_price" optional="hide"/>
+ <field name="price_unit_before" readonly="1" optional="hide" force_save="1"/>
<!-- <field name="suggest" readonly="1"/> -->
</field>
<field name="product_id" position="before">
@@ -390,6 +391,7 @@
<field name="hold_outgoing_so" optional="hide"/>
<field name="bu_pick" optional="hide"/>
<field name="margin_so"/>
+ <field name="so_header_margin" optional="hide"/>
</tree>
</field>
</record>
diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml
index 8d56bbbd..82daa36f 100755
--- a/indoteknik_custom/views/sale_order.xml
+++ b/indoteknik_custom/views/sale_order.xml
@@ -7,6 +7,12 @@
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<button id="action_confirm" position="after">
+ <button name="action_open_partial_delivery_wizard"
+ string="Partial Delivery"
+ type="object"
+ class="oe_highlight"
+ attrs="{'invisible': [('state','!=','sale')]}"/>
+
<button name="calculate_line_no"
string="Create No"
type="object"
@@ -365,9 +371,13 @@
</field>
<field name="payment_term_id" position="attributes">
<attribute name="attrs">
- {'readonly': ['|', ('approval_status', 'in', ['pengajuan1', 'pengajuan2', 'approved']),
- ('state', 'not in',
- ['cancel', 'draft'])]}
+ {
+ 'readonly': [
+ '|',
+ ('approval_status', 'in', ['pengajuan1', 'pengajuan2', 'approved']),
+ ('state', 'not in', ['cancel', 'draft'])
+ ]
+ }
</attribute>
</field>
@@ -502,6 +512,44 @@
</field>
</record>
+ <record id="view_partial_delivery_wizard_form" model="ir.ui.view">
+ <field name="name">partial.delivery.wizard.form</field>
+ <field name="model">partial.delivery.wizard</field>
+ <field name="arch" type="xml">
+ <form string="Partial Delivery Wizard">
+ <group>
+ <!-- Field ini WAJIB ada walau invisible -->
+ <field name="picking_ids" invisible="1"/>
+
+ <field name="picking_id"/>
+ </group>
+
+ <separator string="Products"/>
+ <div class="oe_button_box" name="select_all_box">
+ <button name="action_select_all" string="✅ Select All" type="object" class="btn-primary"/>
+ <button name="action_unselect_all" string="❌ Unselect All" type="object" class="btn-secondary"/>
+ </div>
+
+ <field name="line_ids" context="{'default_wizard_id': active_id}" widget="many2many">
+ <tree editable="bottom">
+ <field name="selected"/>
+ <field name="product_id"/>
+ <field name="ordered_qty" readonly="1"/>
+ <field name="reserved_qty" readonly="1"/>
+ <field name="selected_qty"/>
+ </tree>
+ </field>
+
+ <footer>
+ <button string="Confirm" type="object" name="action_confirm_partial_delivery" class="btn-primary"/>
+ <button string="Cancel" class="btn-secondary" special="cancel"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+
+
+
<record id="sale_order_multi_update_ir_actions_server" model="ir.actions.server">
<field name="name">Mark As Cancel</field>
<field name="model_id" ref="sale.model_sale_order"/>
diff --git a/indoteknik_custom/views/stock_move_line.xml b/indoteknik_custom/views/stock_move_line.xml
index 94c0bf53..ac8d3dbe 100644
--- a/indoteknik_custom/views/stock_move_line.xml
+++ b/indoteknik_custom/views/stock_move_line.xml
@@ -20,6 +20,9 @@
<field name="product_id" position="after">
<field name="manufacture"/>
</field>
+ <field name="qty_done" position="after">
+ <field name="product_uom_qty"/>
+ </field>
</field>
</record>
</odoo>
diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml
index 21762202..44ab6355 100644
--- a/indoteknik_custom/views/stock_picking.xml
+++ b/indoteknik_custom/views/stock_picking.xml
@@ -45,6 +45,14 @@
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_form"/>
<field name="arch" type="xml">
+ <!-- Tambahkan tombol custom: tampil hanya saat BU/IU + pengajuan1 -->
+ <xpath expr="//header" position="inside">
+ <button name="action_bu_iu_to_pengajuan2"
+ type="object"
+ string="Approve by Accounting"
+ class="btn-primary"
+ attrs="{'invisible': [('is_bu_iu', '=', False), ('approval_status', 'in', ['pengajuan2', False, 'false', ''])]}"/>
+ </xpath>
<button name="action_confirm" position="before">
<button name="ask_approval"
string="Ask Approval"
@@ -158,6 +166,8 @@
<field name="purchase_id"/>
<field name="sale_order"/>
<field name="invoice_status"/>
+ <field name="is_bu_iu" />
+ <field name="approval_status" attrs="{'invisible': [('is_bu_iu', '=', False)]}"/>
<field name="date_doc_kirim" attrs="{'readonly':[('invoice_status', '=', 'invoiced')]}"/>
<field name="summary_qty_operation"/>
<field name="count_line_operation"/>
@@ -197,6 +207,7 @@
<field name="product_uom" position="after">
<field name="sale_id" attrs="{'readonly': 1}" optional="hide"/>
<field name="print_barcode" optional="hide"/>
+ <field name="partial" widget="boolean_toggle" optional="hide"/>
<field name="qr_code_variant" widget="image" optional="hide"/>
<field name="barcode" optional="hide"/>
</field>