summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFIN-IT_AndriFP <it@fixcomart.co.id>2025-10-02 14:04:56 +0700
committerFIN-IT_AndriFP <it@fixcomart.co.id>2025-10-02 14:04:56 +0700
commit7d89516ce05d0ea67733c04436cc2de544189efe (patch)
tree2521fbf09f4695253fd7db1ff7613b7f003f20ea
parent10da5e27e658030f171f694d6696f93e4a472447 (diff)
parent875b20796c7fa64abebe430b2707df597e29836b (diff)
Merge branch 'pum-v2' of https://bitbucket.org/altafixco/indoteknik-addons into pum-v2
-rw-r--r--indoteknik_api/controllers/api_v1/partner.py11
-rw-r--r--indoteknik_api/controllers/api_v1/sale_order.py2
-rw-r--r--indoteknik_api/controllers/api_v1/stock_picking.py58
-rw-r--r--indoteknik_api/controllers/api_v1/user.py3
-rw-r--r--indoteknik_api/models/res_users.py9
-rw-r--r--indoteknik_api/models/sale_order.py2
-rwxr-xr-xindoteknik_custom/__manifest__.py7
-rwxr-xr-xindoteknik_custom/models/__init__.py5
-rw-r--r--indoteknik_custom/models/account_move.py135
-rw-r--r--indoteknik_custom/models/account_move_due_extension.py8
-rw-r--r--indoteknik_custom/models/approval_payment_term.py10
-rw-r--r--indoteknik_custom/models/dunning_run.py3
-rw-r--r--indoteknik_custom/models/letter_receivable.py508
-rw-r--r--indoteknik_custom/models/logbook_sj.py5
-rw-r--r--indoteknik_custom/models/manufacturing.py22
-rwxr-xr-xindoteknik_custom/models/purchase_order.py48
-rwxr-xr-xindoteknik_custom/models/purchase_order_line.py1
-rw-r--r--indoteknik_custom/models/purchasing_job.py19
-rw-r--r--indoteknik_custom/models/refund_sale_order.py333
-rw-r--r--indoteknik_custom/models/report_logbook_sj.py61
-rw-r--r--indoteknik_custom/models/res_partner.py16
-rwxr-xr-xindoteknik_custom/models/sale_order.py274
-rw-r--r--indoteknik_custom/models/sale_order_line.py6
-rw-r--r--indoteknik_custom/models/sj_tele.py102
-rw-r--r--indoteknik_custom/models/stock_move.py3
-rw-r--r--indoteknik_custom/models/stock_picking.py69
-rw-r--r--indoteknik_custom/models/stock_picking_return.py2
-rw-r--r--indoteknik_custom/models/tukar_guling.py54
-rw-r--r--indoteknik_custom/models/tukar_guling_po.py48
-rw-r--r--indoteknik_custom/models/unpaid_invoice_view.py55
-rw-r--r--indoteknik_custom/report/purchase_report.xml178
-rw-r--r--indoteknik_custom/report/report_surat_piutang copy.xml149
-rw-r--r--indoteknik_custom/report/report_surat_piutang.xml241
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv9
-rw-r--r--indoteknik_custom/views/account_move.xml10
-rw-r--r--indoteknik_custom/views/account_move_line.xml3
-rw-r--r--indoteknik_custom/views/account_move_views.xml3
-rw-r--r--indoteknik_custom/views/approval_payment_term.xml2
-rw-r--r--indoteknik_custom/views/dunning_run.xml5
-rw-r--r--indoteknik_custom/views/ir_sequence.xml9
-rw-r--r--indoteknik_custom/views/letter_receivable.xml190
-rw-r--r--indoteknik_custom/views/letter_receivable_mail_template.xml77
-rw-r--r--indoteknik_custom/views/mail_template_invoice_reminder.xml1
-rwxr-xr-xindoteknik_custom/views/purchase_order.xml11
-rw-r--r--indoteknik_custom/views/refund_sale_order.xml3
-rw-r--r--indoteknik_custom/views/report_logbook_sj.xml92
-rw-r--r--indoteknik_custom/views/res_partner.xml14
-rwxr-xr-xindoteknik_custom/views/sale_order.xml21
-rw-r--r--indoteknik_custom/views/sj_tele.xml15
-rw-r--r--indoteknik_custom/views/stock_picking.xml10
-rw-r--r--indoteknik_custom/views/tukar_guling_po.xml2
-rw-r--r--indoteknik_custom/views/unpaid_invoice_view.xml100
52 files changed, 2633 insertions, 391 deletions
diff --git a/indoteknik_api/controllers/api_v1/partner.py b/indoteknik_api/controllers/api_v1/partner.py
index b1d8d5f3..8d67800c 100644
--- a/indoteknik_api/controllers/api_v1/partner.py
+++ b/indoteknik_api/controllers/api_v1/partner.py
@@ -295,7 +295,14 @@ class Partner(controller.Controller):
partner = partner.parent_id or partner
- if any(line.days == 0 for line in partner.property_payment_term_id.line_ids):
+ payment_term = (
+ partner.previous_payment_term_id
+ if partner.is_cbd_locked
+ else partner.property_payment_term_id
+ )
+
+ # if any(line.days == 0 for line in partner.property_payment_term_id.line_ids):
+ if any(line.days == 0 for line in payment_term.line_ids):
return self.response(code=402, description='Partner not tempo')
domain_result_tempo = [('partner_id', '=', partner.id), ('payment_state', '=', 'not_paid'), ('state', '=', 'posted')]
@@ -315,7 +322,7 @@ class Partner(controller.Controller):
data = {
'name': partner.name,
- 'payment_term': partner.property_payment_term_id.name,
+ 'payment_term': payment_term.name,
'amount_due': result_tempo,
'amount_due_total': result_tempo_total,
'amount_jatuh_tempo_total': result_jatuh_tempo_total,
diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py
index accc7531..1a75c830 100644
--- a/indoteknik_api/controllers/api_v1/sale_order.py
+++ b/indoteknik_api/controllers/api_v1/sale_order.py
@@ -198,7 +198,7 @@ class SaleOrder(controller.Controller):
if status in ['dikemas', 'dikirim', 'selesai', 'partial']:
filtered_orders = []
for sale_order in sale_orders:
- bu_pickings = [p for p in sale_order.picking_ids if p.picking_type_id and p.picking_type_id.id == 29]
+ bu_pickings = [p for p in sale_order.picking_ids if p.picking_type_id and p.picking_type_id.id == 29 and p.state != 'cancel']
total = len(bu_pickings)
done_pickings = [p for p in bu_pickings if p.state == 'done']
done_with_driver = [p for p in done_pickings if p.sj_return_date]
diff --git a/indoteknik_api/controllers/api_v1/stock_picking.py b/indoteknik_api/controllers/api_v1/stock_picking.py
index 762e17c5..a4a9cf80 100644
--- a/indoteknik_api/controllers/api_v1/stock_picking.py
+++ b/indoteknik_api/controllers/api_v1/stock_picking.py
@@ -124,33 +124,53 @@ class StockPicking(controller.Controller):
@http.route(prefix + 'stock-picking/<scanid>/documentation', auth='public', methods=['PUT', 'OPTIONS'], csrf=False)
@controller.Controller.must_authorized()
- def write_partner_stock_picking_documentation(self, **kw):
- scanid = kw.get('scanid', '').strip()
- sj_document = kw.get('sj_document', False)
- paket_document = kw.get('paket_document', False)
-
- params = {
- 'sj_documentation': sj_document,
- 'paket_documentation': paket_document,
- 'driver_arrival_date': datetime.utcnow(),
+ 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)
+
+ # ===== Cari picking by id / picking_code =====
picking_data = False
- if scanid.isdigit() and int(scanid) < 2147483647:
- picking_data = request.env['stock.picking'].search([('id', '=', int(scanid))], limit=1)
+ if scanid.isdigit() and int(scanid) < 2147483646:
+ picking_data = request.env['stock.picking'].search([('id', '=', int(scanid))], limit=0)
if not picking_data:
- picking_data = request.env['stock.picking'].search([('picking_code', '=', scanid)], limit=1)
+ picking_data = request.env['stock.picking'].search([('picking_code', '=', scanid)], limit=0)
if not picking_data:
- return self.response(code=404, description='picking not found')
-
- picking_data.write(params)
+ return self.response(code=403, description='picking not found')
- return self.response({
- 'name': picking_data.name
- })
+ params = {
+ 'sj_documentation': sj_document,
+ 'paket_documentation': paket_document,
+ '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):
@@ -160,7 +180,7 @@ class StockPicking(controller.Controller):
# Karena type='json', Odoo secara otomatis akan mem-parsing JSON untuk Anda.
# 'data' akan berisi dictionary Python dari payload JSON Biteship.
data = request.jsonrequest
-
+
# Log ini akan menunjukkan payload yang diterima (sudah dalam bentuk dict)
_logger.info(f"Biteship Webhook: Parsed JSON data from request.jsonrequest: {json.dumps(data)}")
diff --git a/indoteknik_api/controllers/api_v1/user.py b/indoteknik_api/controllers/api_v1/user.py
index c75e4954..3511bc52 100644
--- a/indoteknik_api/controllers/api_v1/user.py
+++ b/indoteknik_api/controllers/api_v1/user.py
@@ -90,7 +90,8 @@ class User(controller.Controller):
'login': email,
'oauth_provider_id': request.env.ref('auth_oauth.provider_google').id,
'sel_groups_1_9_10': 9,
- 'active': True
+ 'active': True,
+
}
user = request.env['res.users'].create(user_data)
diff --git a/indoteknik_api/models/res_users.py b/indoteknik_api/models/res_users.py
index 015b9e0e..c4e19bf3 100644
--- a/indoteknik_api/models/res_users.py
+++ b/indoteknik_api/models/res_users.py
@@ -14,6 +14,11 @@ class ResUsers(models.Model):
'manager': 2,
'director': 3
}
+ payment_term = (
+ main_partner.previous_payment_term_id
+ if main_partner.is_cbd_locked
+ else main_partner.property_payment_term_id
+ )
partner_tempo = False
is_tempo_request = request.env['user.pengajuan.tempo.request'].search([('user_company_id', '=', main_partner.id)], limit=1)
tempo_progres = (
@@ -21,8 +26,8 @@ class ResUsers(models.Model):
'rejected' if is_tempo_request.state_tempo == 'reject' else
'approve' if is_tempo_request.state_tempo == 'approval_director' else ''
)
- if main_partner:
- partner_tempo = True if 'tempo' in main_partner.get_check_payment_term().lower() else False
+ if payment_term:
+ partner_tempo = True if 'tempo' in payment_term.name.lower() else False
data = {
'id': res_user.id,
diff --git a/indoteknik_api/models/sale_order.py b/indoteknik_api/models/sale_order.py
index c59dead9..23be358a 100644
--- a/indoteknik_api/models/sale_order.py
+++ b/indoteknik_api/models/sale_order.py
@@ -75,7 +75,7 @@ class SaleOrder(models.Model):
if sale_order.state == 'sale':
bu_pickings = [
p for p in sale_order.picking_ids
- if p.picking_type_id and p.picking_type_id.id == 29
+ if p.picking_type_id and p.picking_type_id.id == 29 and p.state != 'cancel'
]
# Hitung status masing-masing picking
diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py
index 008c7f0f..c0f65cf5 100755
--- a/indoteknik_custom/__manifest__.py
+++ b/indoteknik_custom/__manifest__.py
@@ -167,6 +167,8 @@
'report/report_invoice.xml',
'report/report_picking.xml',
'report/report_sale_order.xml',
+ 'report/report_surat_piutang.xml',
+ 'report/purchase_report.xml',
'views/vendor_sla.xml',
'views/coretax_faktur.xml',
'views/public_holiday.xml',
@@ -181,6 +183,11 @@
'views/tukar_guling_po.xml',
# 'views/refund_sale_order.xml',
'views/update_date_planned_po_wizard_view.xml',
+ 'views/unpaid_invoice_view.xml',
+ 'views/letter_receivable.xml',
+ 'views/letter_receivable_mail_template.xml',
+ # 'views/reimburse.xml',
+ 'views/sj_tele.xml'
],
'demo': [],
'css': [],
diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py
index 930e60e7..f3501f7b 100755
--- a/indoteknik_custom/models/__init__.py
+++ b/indoteknik_custom/models/__init__.py
@@ -157,4 +157,7 @@ from . import refund_sale_order
from . import down_payment
from . import tukar_guling
from . import tukar_guling_po
-from . import update_date_planned_po_wizard \ No newline at end of file
+from . import update_date_planned_po_wizard
+from . import unpaid_invoice_view
+from . import letter_receivable
+from . import sj_tele
diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py
index c93cfb76..44b3cb76 100644
--- a/indoteknik_custom/models/account_move.py
+++ b/indoteknik_custom/models/account_move.py
@@ -99,12 +99,42 @@ class AccountMove(models.Model):
reminder_sent_date = fields.Date(string="Tanggal Reminder Terkirim")
+ payment_difficulty = fields.Selection(string="Payment Difficulty", related='partner_id.payment_difficulty', readonly=True)
+
customer_promise_date = fields.Date(
string="Janji Bayar",
help="Tanggal janji bayar dari customer setelah reminder dikirim.",
tracking=True
)
+ # def _check_and_lock_cbd(self):
+ # cbd_term = self.env['account.payment.term'].browse(26)
+ # today = date.today()
+
+ # # Cari semua invoice overdue
+ # overdue_invoices = self.search([
+ # ('move_type', '=', 'out_invoice'),
+ # ('state', '=', 'posted'),
+ # ('payment_state', 'not in', ['paid', 'in_payment', 'reversed']),
+ # ('invoice_date_due', '!=', False),
+ # ('invoice_date_due', '<=', today - timedelta(days=30)),
+ # ], limit=3)
+
+ # _logger.info(f"Found {len(overdue_invoices)} overdue invoices for CBD lock check.")
+ # _logger.info(f"Overdue Invoices: {overdue_invoices.mapped('name')}")
+
+ # # Ambil partner unik dari invoice
+ # partners_to_lock = overdue_invoices.mapped('partner_id').filtered(lambda p: not p.is_cbd_locked)
+ # _logger.info(f"Partners to lock: {partners_to_lock.mapped('name')}")
+
+ # # Lock hanya partner yang belum locked
+ # if partners_to_lock:
+ # partners_to_lock.write({
+ # 'is_cbd_locked': True,
+ # 'property_payment_term_id': cbd_term.id,
+ # })
+
+
def compute_partial_payment(self):
for move in self:
if move.amount_total_signed > 0 and move.amount_residual_signed > 0 and move.payment_state == 'partial':
@@ -164,49 +194,56 @@ class AccountMove(models.Model):
def send_due_invoice_reminder(self):
today = fields.Date.today()
target_dates = [
- today - timedelta(days=7),
- today - timedelta(days=3),
- today,
- today + timedelta(days=3),
today + timedelta(days=7),
+ today + timedelta(days=3),
+ today,
]
-
- for days_after_due in range(14, 181, 7):
- target_dates.append(today - timedelta(days=days_after_due))
-
invoices = self.env['account.move'].search([
('move_type', '=', 'out_invoice'),
('state', '=', 'posted'),
('payment_state', 'not in', ['paid', 'in_payment', 'reversed']),
('invoice_date_due', 'in', target_dates),
- ('date_terima_tukar_faktur', '!=', False)
- ])
- _logger.info(f"Invoices: {invoices}")
+ ('date_terima_tukar_faktur', '!=', False),
+ ('invoice_payment_term_id.name', 'ilike', 'tempo')])
+ _logger.info(f"Found {len(invoices)} invoices due for reminder {invoices}.")
+ if not invoices:
+ _logger.info("Tidak ada invoice yang due")
+ return
- invoices = invoices.filtered(
- lambda inv: inv.invoice_payment_term_id and 'tempo' in (inv.invoice_payment_term_id.name or '').lower()
- )
- # _logger.info(f"Invoices tahap 2: {invoices}")
+ self._send_invoice_reminders(invoices, mode='due')
+ def send_overdue_invoice_reminder(self):
+ today = fields.Date.today()
+ invoices = self.env['account.move'].search([
+ ('move_type', '=', 'out_invoice'),
+ ('state', '=', 'posted'),
+ ('payment_state', 'not in', ['paid', 'in_payment', 'reversed']),
+ ('invoice_date_due', '<', today),
+ ('date_terima_tukar_faktur', '!=', False),
+ ('invoice_payment_term_id.name', 'ilike', 'tempo')])
+ _logger.info(f"Found {len(invoices)} invoices overdue for reminder {invoices}.")
if not invoices:
- _logger.info("Tidak ada invoice yang due")
+ _logger.info("Tidak ada invoice yang overdue")
return
+ self._send_invoice_reminders(invoices, mode='overdue')
+
+ def _send_invoice_reminders(self, invoices, mode):
+ today = fields.Date.today()
+ template = self.env.ref('indoteknik_custom.mail_template_invoice_due_reminder')
invoice_group = {}
for inv in invoices:
dtd = (inv.invoice_date_due - today).days if inv.invoice_date_due else 0
- key = (inv.partner_id, dtd)
+ key = (inv.partner_id, dtd if mode == 'due' else "overdue")
if key not in invoice_group:
invoice_group[key] = self.env['account.move'] # recordset kosong
invoice_group[key] |= inv # gabung recordset
- template = self.env.ref('indoteknik_custom.mail_template_invoice_due_reminder')
-
for (partner, dtd), invs in invoice_group.items():
if all(inv.reminder_sent_date == today for inv in invs):
_logger.info(f"Reminder untuk {partner.name} sudah terkirim hari ini, skip.")
continue
-
+
promise_dates = [inv.customer_promise_date for inv in invs if inv.customer_promise_date]
if promise_dates:
earliest_promise = min(promise_dates) # ambil janji paling awal
@@ -248,11 +285,12 @@ class AccountMove(models.Model):
invoice_table_rows = ""
grand_total = 0
- for inv in invs:
+ for idx, inv in enumerate(invs, start=1): # numbering
days_to_due = (inv.invoice_date_due - today).days if inv.invoice_date_due else 0
grand_total += inv.amount_total
invoice_table_rows += f"""
<tr>
+ <td>{idx}</td>
<td>{inv.partner_id.name}</td>
<td>{inv.ref or '-'}</td>
<td>{inv.name}</td>
@@ -263,6 +301,7 @@ class AccountMove(models.Model):
<td>{days_to_due}</td>
</tr>
"""
+
invoice_table_footer = f"""
<tfoot>
<tr style="font-weight:bold; background-color:#f9f9f9;">
@@ -296,12 +335,14 @@ class AccountMove(models.Model):
currency = invs[0].currency_id if invs else partner.company_id.currency_id
tempo_link = 'https://indoteknik.com/my/tempo'
# tempo_link = 'http://localhost:2100/my/tempo'
+ # payment_term = partner.previous_payment_term_id if partner.is_cbd_locked else partner.property_payment_term_id
+ payment_term = invs[0].invoice_payment_term_id
limit_info_html = f"""
<p><b>Informasi Tambahan:</b></p>
<ul style="font-size:12px; color:#333; line-height:1.4; margin:0; padding-left:0; list-style-position:inside;">
<li>Kredit Limit Anda: {formatLang(self.env, blocking_limit, currency_obj=currency)}</li>
- <li>Status Detail Tempo: {partner.property_payment_term_id.name or 'Review'}</li>
+ <li>Status Detail Tempo: {payment_term.name or ''}</li>
<li style="color:{'red' if (blocking_limit - outstanding_amount) < 0 else 'green'};">
Sisa Kredit Limit: {formatLang(self.env, blocking_limit - outstanding_amount, currency_obj=currency)}
</li>
@@ -326,33 +367,33 @@ class AccountMove(models.Model):
days_to_due_message = ""
closing_message = ""
- if dtd > 0:
- days_to_due_message = (
- f"Kami ingin mengingatkan bahwa tagihan anda akan jatuh tempo dalam {dtd} hari ke depan, "
- "dengan rincian sebagai berikut:"
- )
- closing_message = (
- "Kami mengharapkan pembayaran dapat dilakukan tepat waktu untuk mendukung kelancaran "
- "hubungan kerja sama yang baik antara kedua belah pihak.<br/>"
- "Mohon konfirmasi apabila pembayaran telah dijadwalkan. "
- "Terima kasih atas perhatian dan kerja samanya."
- )
-
- if dtd == 0:
- days_to_due_message = (
- "Kami ingin mengingatkan bahwa tagihan anda telah memasuki tanggal jatuh tempo pada hari ini, "
- "dengan rincian sebagai berikut:"
- )
- closing_message = (
- "Mohon kesediaannya untuk segera melakukan pembayaran tepat waktu guna menghindari status "
- "keterlambatan dan menjaga kelancaran hubungan kerja sama yang telah terjalin dengan baik.<br/>"
- "Apabila pembayaran telah dijadwalkan atau diproses, mohon dapat dikonfirmasi kepada kami. "
- "Terima kasih atas perhatian dan kerja samanya."
- )
+ if mode == "due":
+ if dtd > 0:
+ days_to_due_message = (
+ f"Kami ingin mengingatkan bahwa tagihan anda akan jatuh tempo dalam {dtd} hari ke depan, "
+ "dengan rincian sebagai berikut:"
+ )
+ closing_message = (
+ "Kami mengharapkan pembayaran dapat dilakukan tepat waktu untuk mendukung kelancaran "
+ "hubungan kerja sama yang baik antara kedua belah pihak.<br/>"
+ "Mohon konfirmasi apabila pembayaran telah dijadwalkan. "
+ "Terima kasih atas perhatian dan kerja samanya."
+ )
- if dtd < 0:
+ elif dtd == 0:
+ days_to_due_message = (
+ "Kami ingin mengingatkan bahwa tagihan anda telah memasuki tanggal jatuh tempo pada hari ini, "
+ "dengan rincian sebagai berikut:"
+ )
+ closing_message = (
+ "Mohon kesediaannya untuk segera melakukan pembayaran tepat waktu guna menghindari status "
+ "keterlambatan dan menjaga kelancaran hubungan kerja sama yang telah terjalin dengan baik.<br/>"
+ "Apabila pembayaran telah dijadwalkan atau diproses, mohon dapat dikonfirmasi kepada kami. "
+ "Terima kasih atas perhatian dan kerja samanya."
+ )
+ else: # mode overdue
days_to_due_message = (
- f"Kami ingin mengingatkan bahwa tagihan anda telah jatuh tempo selama {abs(dtd)} hari, "
+ f"Kami ingin mengingatkan bahwa beberapa tagihan anda telah jatuh tempo, "
"dengan rincian sebagai berikut:"
)
closing_message = (
diff --git a/indoteknik_custom/models/account_move_due_extension.py b/indoteknik_custom/models/account_move_due_extension.py
index 40059bd9..55fc6c65 100644
--- a/indoteknik_custom/models/account_move_due_extension.py
+++ b/indoteknik_custom/models/account_move_due_extension.py
@@ -13,6 +13,7 @@ class DueExtension(models.Model):
number = fields.Char(string='Document No', index=True, copy=False, readonly=True, tracking=True)
partner_id = fields.Many2one('res.partner', string="Customer", readonly=True)
+ payment_term = fields.Char(string="Payment Term", readonly=True, compute='_compute_payment_term')
order_id = fields.Many2one('sale.order', string="SO", readonly=True)
amount_total = fields.Monetary(
string="Amount Total SO",
@@ -43,7 +44,12 @@ class DueExtension(models.Model):
counter = fields.Integer(string="Counter", compute='_compute_counter')
approve_by = fields.Many2one('res.users', string="Approve By", readonly=True)
date_approve = fields.Datetime(string="Date Approve", readonly=True)
-
+
+ @api.depends('partner_id')
+ def _compute_payment_term(self):
+ for rec in self:
+ rec.payment_term = rec.partner_id.property_payment_term_id.name
+
@api.depends('order_id')
def _compute_amount_total(self):
for rec in self:
diff --git a/indoteknik_custom/models/approval_payment_term.py b/indoteknik_custom/models/approval_payment_term.py
index 8618856a..449bd90b 100644
--- a/indoteknik_custom/models/approval_payment_term.py
+++ b/indoteknik_custom/models/approval_payment_term.py
@@ -69,8 +69,8 @@ class ApprovalPaymentTerm(models.Model):
return res
def _track_changes_for_user_688(self, vals, old_values_dict):
- if self.env.user.id != 688:
- return
+ # if self.env.user.id != 688:
+ # return
tracked_fields = {"blocking_stage", "warning_stage", "property_payment_term_id"}
@@ -106,7 +106,8 @@ class ApprovalPaymentTerm(models.Model):
if changes:
timestamp = fields.Datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- rec.change_log_688 = f"{timestamp} - Perubahan oleh Widya:\n" + "\n".join(changes)
+ user = self.env.user
+ rec.change_log_688 = f"{timestamp} - Perubahan oleh {user.name}:\n" + "\n".join(changes)
@staticmethod
@@ -171,7 +172,8 @@ class ApprovalPaymentTerm(models.Model):
'blocking_stage': self.blocking_stage,
'warning_stage': self.warning_stage,
'active_limit': self.active_limit,
- 'property_payment_term_id': self.property_payment_term_id.id
+ 'property_payment_term_id': self.property_payment_term_id.id,
+ 'is_cbd_locked': False,
})
self.approve_date = datetime.utcnow()
self.state = 'approved'
diff --git a/indoteknik_custom/models/dunning_run.py b/indoteknik_custom/models/dunning_run.py
index 5a6aebac..9feea1d1 100644
--- a/indoteknik_custom/models/dunning_run.py
+++ b/indoteknik_custom/models/dunning_run.py
@@ -1,6 +1,6 @@
from odoo import models, api, fields
from odoo.exceptions import AccessError, UserError, ValidationError
-from datetime import timedelta
+from datetime import timedelta, date
import logging
@@ -149,4 +149,5 @@ class DunningRunLine(models.Model):
total_amt = fields.Float(string='Total Amount')
open_amt = fields.Float(string='Open Amount')
due_date = fields.Date(string='Due Date')
+ payment_term = fields.Many2one('account.payment.term', related='invoice_id.invoice_payment_term_id', string='Payment Term')
diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py
new file mode 100644
index 00000000..16034938
--- /dev/null
+++ b/indoteknik_custom/models/letter_receivable.py
@@ -0,0 +1,508 @@
+from odoo import models, fields, api, _
+from odoo.exceptions import UserError
+from odoo.exceptions import ValidationError
+from odoo.tools import mail, formatLang
+from terbilang import Terbilang
+import re
+import logging
+from datetime import datetime, timedelta
+import babel
+import base64
+import pytz
+
+_logger = logging.getLogger(__name__)
+
+class SuratPiutang(models.Model):
+ _name = "surat.piutang"
+ _description = "Surat Piutang"
+ _inherit = ['mail.thread', 'mail.activity.mixin']
+ _order = 'name desc'
+
+ name = fields.Char(string="Nomor Surat", readonly=True, copy=False)
+ partner_id = fields.Many2one("res.partner", string="Customer", required=True, tracking=True)
+ tujuan_nama = fields.Char(string="Nama Tujuan", tracking=True)
+ tujuan_email = fields.Char(string="Email Tujuan", tracking=True)
+ perihal = fields.Selection([
+ ('penagihan', 'Surat Resmi Penagihan'),
+ ('sp1', 'Surat Peringatan Piutang ke-1'),
+ ('sp2', 'Surat Peringatan Piutang ke-2'),
+ ('sp3', 'Surat Peringatan Piutang ke-3')
+ ], string="Perihal", required=True, tracking=True)
+ line_ids = fields.One2many("surat.piutang.line", "surat_id", string="Invoice Lines")
+ state = fields.Selection([
+ ("draft", "Draft"),
+ ("waiting_approval_sales", "Menunggu Approval Sales Manager"),
+ ("waiting_approval_pimpinan", "Menunggu Approval Pimpinan / Kirim Surat"),
+ ("sent", "Approved & Sent")
+ ], default="draft", tracking=True)
+ send_date = fields.Datetime(string="Tanggal Kirim", tracking=True)
+ seven_days_after_sent_date = fields.Char(string="7 Hari Setelah Tanggal Kirim")
+ periode_invoices_terpilih = fields.Char(
+ string="Periode Invoices Terpilih",
+ compute="_compute_periode_invoices",
+ )
+
+ currency_id = fields.Many2one('res.currency')
+
+ # Grand total (total sisa semua line yang dicentang)
+ grand_total = fields.Monetary(
+ string='Total Sisa',
+ currency_field='currency_id',
+ compute='_compute_grand_total',
+ )
+
+ grand_total_text = fields.Char(
+ string="Total Terbilang",
+ compute="_compute_grand_total_text",
+ )
+
+ perihal_label = fields.Char(compute="_compute_perihal_label", string="Perihal Label")
+
+ payment_difficulty = fields.Selection(string="Payment Difficulty", related='partner_id.payment_difficulty', readonly=True)
+
+ sales_person_id = fields.Many2one('res.users', string='Salesperson', related='partner_id.user_id', readonly=True)
+
+ PERIHAL_SEQUENCE = {
+ "penagihan": "sp1",
+ "sp1": "sp2",
+ "sp2": "sp3",
+ }
+
+ @api.onchange('partner_id')
+ def _onchange_partner_id_domain(self):
+ unpaid_partner_ids = self.env['unpaid.invoice.view'].search([]).mapped('partner_id.id')
+ return {
+ 'domain': {
+ 'partner_id': [('id', 'in', unpaid_partner_ids)]
+ }
+ }
+ def _compute_perihal_label(self):
+ for rec in self:
+ rec.perihal_label = dict(self._fields['perihal'].selection).get(rec.perihal, '')
+
+ def action_create_next_letter(self):
+ for rec in self:
+ if rec.state != "sent":
+ raise UserError("Surat harus sudah terkirim sebelum bisa membuat surat lanjutan.")
+
+ next_perihal = self.PERIHAL_SEQUENCE.get(rec.perihal)
+ if not next_perihal:
+ raise UserError("Surat ini sudah pada tahap terakhir (SP3). Tidak bisa membuat lanjutan lagi.")
+
+ existing = self.search([
+ ('partner_id', '=', rec.partner_id.id),
+ ('perihal', '=', next_perihal),
+ ('state', '!=', 'draft') # optional: cek hanya yang sudah dikirim
+ ])
+ if existing:
+ raise UserError(f"Surat lanjutan {dict(self._fields['perihal'].selection).get(next_perihal)} "
+ f"untuk customer ini sudah dibuat: {', '.join(existing.mapped('name'))}")
+
+ # copy surat lama
+ new_vals = {
+ "tujuan_nama": rec.tujuan_nama,
+ "tujuan_email": rec.tujuan_email,
+ "perihal": next_perihal,
+ "partner_id": rec.partner_id.id,
+ "line_ids": [(0, 0, {
+ 'invoice_id': line.invoice_id.id,
+ 'invoice_number': line.invoice_number,
+ 'invoice_date': line.invoice_date,
+ 'invoice_date_due': line.invoice_date_due,
+ 'invoice_day_to_due': line.invoice_day_to_due,
+ 'new_invoice_day_to_due': line.new_invoice_day_to_due,
+ 'ref': line.ref,
+ 'amount_residual': line.amount_residual,
+ 'currency_id': line.currency_id.id,
+ 'payment_term_id': line.payment_term_id.id,
+ 'date_kirim_tukar_faktur': line.date_kirim_tukar_faktur,
+ 'date_terima_tukar_faktur': line.date_terima_tukar_faktur,
+ 'invoice_user_id': line.invoice_user_id.id,
+ 'sale_id': line.sale_id.id,
+ "selected": line.selected,
+ }) for line in rec.line_ids],
+ }
+ new_letter = self.create(new_vals)
+ self.env.user.notify_info(
+ message=f"{dict(self._fields['perihal'].selection).get(next_perihal)} berhasil dibuat ({new_letter.name}).",
+ title="Informasi",
+ sticky=False
+ )
+ new_letter.message_post(
+ body=
+ f"<b>{dict(self._fields['perihal'].selection).get(next_perihal)}</b> "
+ f"berhasil dibuat berdasarkan surat sebelumnya.<br/>"
+ f"Nomor Surat: <b>{new_letter.name}</b>"
+ )
+ rec.message_post(
+ body=(
+ f"Surat lanjutan dengan perihal <b>{dict(self._fields['perihal'].selection).get(next_perihal)}</b> "
+ f"telah dibuat sebagai kelanjutan dari surat ini.<br/>"
+ f"Nomor Surat Baru: <a href='/web#id={new_letter.id}&model=surat.piutang&view_type=form'><b>{new_letter.name}</b></a>"
+ )
+ )
+ return True
+
+
+ @api.depends("line_ids.selected", "line_ids.invoice_date")
+ def _compute_periode_invoices(self):
+ for rec in self:
+ selected_lines = rec.line_ids.filtered(lambda l: l.selected and l.invoice_date)
+ if not selected_lines:
+ rec.periode_invoices_terpilih = "-"
+ continue
+
+ dates = selected_lines.mapped("invoice_date")
+ min_date, max_date = min(dates), max(dates)
+
+ # Ambil bagian bulan & tahun
+ min_month = babel.dates.format_date(min_date, "MMMM", locale="id_ID")
+ min_year = min_date.year
+ max_month = babel.dates.format_date(max_date, "MMMM", locale="id_ID")
+ max_year = max_date.year
+
+ if min_year == max_year:
+ if min_month == max_month:
+ # example: Januari 2025
+ rec.periode_invoices_terpilih = f"{min_month} {min_year}"
+ else:
+ # example: Mei s/d Juni 2025
+ rec.periode_invoices_terpilih = f"{min_month} s/d {max_month} {max_year}"
+ else:
+ # example: Desember 2024 s/d Januari 2025
+ rec.periode_invoices_terpilih = f"{min_month} {min_year} s/d {max_month} {max_year}"
+
+ def _compute_grand_total_text(self):
+ tb = Terbilang()
+ for record in self:
+ res = ""
+ if record.grand_total and record.grand_total > 0:
+ try:
+ tb.parse(int(record.grand_total))
+ res = tb.getresult().title() + " Rupiah"
+ except Exception:
+ res = ""
+ record.grand_total_text = res
+
+ @api.depends('line_ids.amount_residual', 'line_ids.selected')
+ def _compute_grand_total(self):
+ for rec in self:
+ rec.grand_total = sum(
+ 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)
+
+ def action_approve(self):
+ wib = pytz.timezone('Asia/Jakarta')
+ now_wib = datetime.now(wib)
+
+ sales_manager_ids = [10] # ganti dengan ID user Sales Manager
+ pimpinan_user_ids = [7] # ganti dengan ID user Pimpinan
+
+ for rec in self:
+ # === SP1 s/d SP3 butuh dua tahap approval ===
+ if rec.perihal in ("sp1", "sp2", "sp3"):
+
+ # Tahap 1: Sales Manager approval
+ if rec.state == "waiting_approval_sales":
+ if self.env.user.id not in sales_manager_ids:
+ raise UserError("Hanya Sales Manager yang boleh menyetujui tahap ini.")
+ rec.state = "waiting_approval_pimpinan"
+ rec.message_post(body="Disetujui oleh Sales Manager. Menunggu Approval Pimpinan.")
+ continue
+
+ # Tahap 2: Pimpinan approval
+ if rec.state == "waiting_approval_pimpinan":
+ if self.env.user.id not in pimpinan_user_ids:
+ raise UserError("Hanya Pimpinan yang berhak menyetujui surat ini.")
+ 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 Piutang disetujui oleh Pimpinan dan berhasil dikirim.")
+ continue
+
+ # === Surat penagihan biasa (langsung Pimpinan approve) ===
+ if rec.perihal == "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.")
+
+ 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_send_letter(self):
+ self.ensure_one()
+
+ selected_lines = self.line_ids.filtered('selected')
+ if not selected_lines:
+ raise UserError(_("Tidak ada invoice yang dicentang untuk dikirim."))
+
+ 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>
+ </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')
+ pdf_content, _ = report._render_qweb_pdf([self.id])
+ attachment_base64 = base64.b64encode(pdf_content)
+
+ attachment = self.env['ir.attachment'].create({
+ 'name': f"{self.perihal_label} - {self.partner_id.name}.pdf",
+ 'type': 'binary',
+ 'datas': attachment_base64,
+ 'res_model': 'surat.piutang',
+ 'res_id': self.id,
+ 'mimetype': 'application/pdf',
+ })
+
+ cc_list = [
+ 'finance@indoteknik.co.id',
+ 'akbar@indoteknik.co.id',
+ 'stephan@indoteknik.co.id',
+ 'darren@indoteknik.co.id'
+ ]
+
+ sales_email = self.sales_person_id.email if self.sales_person_id else None
+ if sales_email and sales_email not in cc_list:
+ 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 ''),
+ 'email_to': self.tujuan_email,
+ 'email_from': 'finance@indoteknik.co.id',
+ 'email_cc': ",".join(sorted(set(cc_list))),
+ 'body_html': body_html,
+ 'attachments': [(attachment.name, attachment.datas)],
+ 'reply_to': 'finance@indoteknik.co.id',
+ }
+
+ template.with_context(mail_post_autofollow=False).send_mail(
+ self.id,
+ force_send=True,
+ email_values=values
+ )
+
+ _logger.info(
+ f"Surat Piutang {self.name} terkirim ke {self.tujuan_email} "
+ f"({self.partner_id.name}), total {len(selected_lines)} invoice."
+ )
+
+ @api.onchange('partner_id')
+ def _onchange_partner_id(self):
+ if self.partner_id:
+ invoice_lines = self.env['unpaid.invoice.view'].search(
+ [('partner_id', '=', self.partner_id.id)],
+ order='new_invoice_day_to_due asc'
+ )
+ selected_invoice_id = self.env.context.get('default_selected_invoice_id')
+ lines = [(5, 0, 0)] # hapus semua line lama
+ lines += [(0, 0, {
+ 'invoice_id': inv.invoice_id.id,
+ 'invoice_number': inv.invoice_number,
+ 'invoice_date': inv.invoice_date,
+ 'invoice_date_due': inv.invoice_date_due,
+ 'invoice_day_to_due': inv.invoice_day_to_due,
+ 'new_invoice_day_to_due': inv.new_invoice_day_to_due,
+ 'ref': inv.ref,
+ 'amount_residual': inv.amount_residual,
+ 'currency_id': inv.currency_id.id,
+ 'payment_term_id': inv.payment_term_id.id,
+ 'date_kirim_tukar_faktur': inv.date_kirim_tukar_faktur,
+ 'date_terima_tukar_faktur': inv.date_terima_tukar_faktur,
+ 'invoice_user_id': inv.invoice_user_id.id,
+ 'sale_id': inv.sale_id.id,
+ 'selected': True if inv.invoice_id.id == selected_invoice_id else False,
+ }) for inv in invoice_lines]
+ self.line_ids = lines
+
+ def action_refresh_lines(self):
+ for rec in self:
+ if not rec.partner_id:
+ continue
+
+ # Ambil semua unpaid terbaru
+ invoice_views = self.env['unpaid.invoice.view'].search(
+ [('partner_id', '=', rec.partner_id.id)],
+ order='new_invoice_day_to_due asc'
+ )
+
+ existing_lines = {line.invoice_id.id: line for line in rec.line_ids}
+
+ # Cache selected status per invoice id
+ selected_map = {line.invoice_id.id: line.selected for line in rec.line_ids}
+
+ # Invoice id yang masih ada di unpaid
+ new_invoice_ids = invoice_views.mapped('invoice_id.id')
+
+ for inv in invoice_views:
+ if inv.invoice_id.id in existing_lines:
+ # update line lama
+ line = existing_lines[inv.invoice_id.id]
+ line.write({
+ # 'invoice_view_id': inv.id,
+ 'invoice_number': inv.invoice_number,
+ 'invoice_date': inv.invoice_date,
+ 'invoice_date_due': inv.invoice_date_due,
+ 'invoice_day_to_due': inv.invoice_day_to_due,
+ 'new_invoice_day_to_due': inv.new_invoice_day_to_due,
+ 'ref': inv.ref,
+ 'amount_residual': inv.amount_residual,
+ 'currency_id': inv.currency_id.id,
+ 'payment_term_id': inv.payment_term_id.id,
+ 'date_kirim_tukar_faktur': inv.date_kirim_tukar_faktur,
+ 'date_terima_tukar_faktur': inv.date_terima_tukar_faktur,
+ 'invoice_user_id': inv.invoice_user_id.id,
+ 'sale_id': inv.sale_id.id,
+ 'selected': selected_map.get(inv.invoice_id.id, line.selected),
+ })
+ else:
+ # preserve selected kalau pernah ada di cache
+ self.env['surat.piutang.line'].create({
+ 'surat_id': rec.id,
+ # 'invoice_view_id': inv.id,
+ 'invoice_id': inv.invoice_id.id,
+ 'invoice_number': inv.invoice_number,
+ 'invoice_date': inv.invoice_date,
+ 'invoice_date_due': inv.invoice_date_due,
+ 'invoice_day_to_due': inv.invoice_day_to_due,
+ 'new_invoice_day_to_due': inv.new_invoice_day_to_due,
+ 'ref': inv.ref,
+ 'amount_residual': inv.amount_residual,
+ 'currency_id': inv.currency_id.id,
+ 'payment_term_id': inv.payment_term_id.id,
+ 'date_kirim_tukar_faktur': inv.date_kirim_tukar_faktur,
+ 'date_terima_tukar_faktur': inv.date_terima_tukar_faktur,
+ 'invoice_user_id': inv.invoice_user_id.id,
+ 'sale_id': inv.sale_id.id,
+ 'selected': selected_map.get(inv.invoice_id.id, False),
+ })
+
+ # Hapus line yang tidak ada lagi di unpaid view
+ rec.line_ids.filtered(lambda l: l.invoice_id.id not in new_invoice_ids).unlink()
+
+ rec.message_post(
+ body=f"Line Invoices diperbarui. Total line saat ini: {len(rec.line_ids)}"
+ )
+
+ @api.model
+ def create(self, vals):
+ # Generate nomor surat otomatis
+ if not vals.get("name"):
+ seq = self.env["ir.sequence"].next_by_code("surat.piutang") or "000"
+ today = fields.Date.today()
+ 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":
+ vals["state"] = "waiting_approval_pimpinan"
+ else:
+ vals["state"] = "waiting_approval_sales"
+ return super().create(vals)
+
+class SuratPiutangLine(models.Model):
+ _name = 'surat.piutang.line'
+ _description = 'Surat Piutang Line'
+
+ surat_id = fields.Many2one('surat.piutang', string='Surat Piutang', ondelete='cascade')
+ # invoice_view_id = fields.Many2one('unpaid.invoice.view', string='Unpaid Invoice')
+ invoice_id = fields.Many2one('account.move', string='Invoice')
+ selected = fields.Boolean(string="Pilih", default=False)
+
+ invoice_number = fields.Char(string='Invoice Number')
+ invoice_date = fields.Date(string='Invoice Date')
+ invoice_date_due = fields.Date(string='Due Date')
+ invoice_day_to_due = fields.Integer(string='Day to Due')
+ new_invoice_day_to_due = fields.Integer(string='New Day to Due')
+ ref = fields.Char(string='Reference')
+ amount_residual = fields.Monetary(string='Amount Due Signed')
+ currency_id = fields.Many2one('res.currency')
+ payment_term_id = fields.Many2one('account.payment.term', string='Payment Terms')
+
+ date_kirim_tukar_faktur = fields.Date(string='Kirim Faktur')
+ date_terima_tukar_faktur = fields.Date(string='Terima Faktur')
+ invoice_user_id = fields.Many2one('res.users', string='Salesperson')
+ sale_id = fields.Many2one('sale.order', string='Sale Order')
+
+ sort = fields.Integer(string='No Urut', compute='_compute_sort', store=False)
+
+ @api.depends('surat_id.line_ids.selected')
+ def _compute_sort(self):
+ for line in self:
+ if line.surat_id:
+ # Ambil semua line yang selected
+ selected_lines = line.surat_id.line_ids.filtered(lambda l: l.selected)
+ try:
+ line.sort = selected_lines.ids.index(line.id) + 1
+ except ValueError:
+ line.sort = 0
+ else:
+ line.sort = 0
diff --git a/indoteknik_custom/models/logbook_sj.py b/indoteknik_custom/models/logbook_sj.py
index 75b2622f..0cda9c8b 100644
--- a/indoteknik_custom/models/logbook_sj.py
+++ b/indoteknik_custom/models/logbook_sj.py
@@ -24,6 +24,7 @@ class LogbookSJ(models.TransientModel):
}
report_logbook = self.env['report.logbook.sj'].create([parameters_header])
+ seq=1
for line in logbook_line:
picking = self.env['stock.picking'].search([('picking_code', '=', line.name)], limit=1)
if not picking:
@@ -43,9 +44,11 @@ class LogbookSJ(models.TransientModel):
'tracking_no': stock.delivery_tracking_no,
'partner_id': parent_id,
'report_logbook_sj_id': report_logbook.id,
- 'note': line.note
+ 'note': line.note,
+ 'line_num': seq
}
self.env['report.logbook.sj.line'].create([data])
+ seq += 1
report_logbook_ids.append(report_logbook.id)
line.unlink()
diff --git a/indoteknik_custom/models/manufacturing.py b/indoteknik_custom/models/manufacturing.py
index aea01362..f986fd4f 100644
--- a/indoteknik_custom/models/manufacturing.py
+++ b/indoteknik_custom/models/manufacturing.py
@@ -4,54 +4,56 @@ import logging
_logger = logging.getLogger(__name__)
+
class Manufacturing(models.Model):
_inherit = 'mrp.production'
unbuild_counter = fields.Integer(string='Unbuild Counter', default=0, help='For restrict unbuild more than once')
-
+
def action_confirm(self):
if self._name != 'mrp.production':
return super(Manufacturing, self).action_confirm()
if not self.env.user.is_purchasing_manager:
raise UserError("Hanya bisa di confirm oleh Purchasing Manager")
-
+
# if self.location_src_id.id != 75:
# raise UserError('Component Location hanya bisa di AS/Stock')
# elif self.location_dest_id.id != 75:
# raise UserError('Finished Product Location hanya bisa di AS/Stock')
-
+
result = super(Manufacturing, self).action_confirm()
return result
-
+
def button_mark_done(self):
if self._name != 'mrp.production':
return super(Manufacturing, self).button_mark_done()
# Check product category
if self.product_id.categ_id.name != 'Finish Good':
raise UserError('Tidak bisa di complete karna product category bukan Unit / Finish Good')
-
+
if self.sale_order and self.sale_order.state != 'sale':
raise UserError(
('Tidak bisa Mark as Done.\nSales Order "%s" (Nomor: %s) belum dikonfirmasi.')
% (self.sale_order.partner_id.name, self.sale_order.name)
)
-
+
for line in self.move_raw_ids:
# if line.quantity_done > 0 and line.quantity_done != self.product_uom_qty:
# raise UserError('Qty Consume per Line tidak sama dengan Qty to Produce')
if line.forecast_availability != line.product_uom_qty:
- raise UserError('Qty Reserved belum sesuai dengan yang seharusnya, product: %s' % line.product_id.display_name)
+ raise UserError(
+ 'Qty Reserved belum sesuai dengan yang seharusnya, product: %s' % line.product_id.display_name)
result = super(Manufacturing, self).button_mark_done()
return result
-
+
def button_unbuild(self):
if self._name != 'mrp.production':
return super(Manufacturing, self).button_unbuild()
-
+
if self.unbuild_counter >= 1:
raise UserError('Tidak bisa unbuild lebih dari 1 kali')
-
+
self.unbuild_counter = self.unbuild_counter + 1
result = super(Manufacturing, self).button_unbuild()
diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py
index 18811b85..b34ec926 100755
--- a/indoteknik_custom/models/purchase_order.py
+++ b/indoteknik_custom/models/purchase_order.py
@@ -6,6 +6,7 @@ import logging
from pytz import timezone, utc
import io
import base64
+from odoo.tools import lazy_property
try:
from odoo.tools.misc import xlsxwriter
except ImportError:
@@ -115,6 +116,20 @@ class PurchaseOrder(models.Model):
compute='_compute_complete_bu_in_count'
)
+ show_description = fields.Boolean(
+ string='Show Description',
+ default=True
+ )
+
+ @api.onchange('show_description')
+ def onchange_show_description(self):
+ if self.show_description == True:
+ for line in self.order_line:
+ line.show_description = True
+ else:
+ for line in self.order_line:
+ line.show_description = False
+
def _compute_complete_bu_in_count(self):
for order in self:
if order.state not in ['done', 'cancel']:
@@ -137,7 +152,7 @@ class PurchaseOrder(models.Model):
def _compute_date_planned(self):
""" date_planned = the earliest date_planned across all order lines. """
for order in self:
- order.date_planned = False
+ order.date_planned = order.date_planned
@api.constrains('date_planned')
def constrains_date_planned(self):
@@ -183,8 +198,11 @@ class PurchaseOrder(models.Model):
# Ambil semua BU awal dari PO
base_bu = StockPicking.search([
+ '|',
+ '&',
('name', 'ilike', 'BU/'),
- ('origin', 'ilike', order.name)
+ ('group_id.id', '=', order.group_id.id),
+ ('origin', '=', order.name),
])
all_bu = base_bu
@@ -214,10 +232,12 @@ class PurchaseOrder(models.Model):
# Step 1: cari semua BU pertama (PUT, INT) yang berasal dari PO ini
base_bu = StockPicking.search([
+ '|',
+ '&',
('name', 'ilike', 'BU/'),
- ('origin', 'ilike', self.name)
+ ('group_id.id', '=', self.group_id.id),
+ ('origin', '=', self.name),
])
-
all_bu = base_bu
seen_names = set(base_bu.mapped('name'))
@@ -228,10 +248,10 @@ class PurchaseOrder(models.Model):
('origin', 'in', ['Return of %s' % name for name in seen_names])
])
next_names = set(next_bu.mapped('name'))
-
+
if not next_names - seen_names:
break
-
+
all_bu |= next_bu
seen_names |= next_names
@@ -1037,8 +1057,19 @@ class PurchaseOrder(models.Model):
message="Produk "+line.product_id.name+" memiliki vendor berbeda dengan SO (Vendor PO: "+str(self.partner_id.name)+", Vendor SO: "+str(line.so_line_id.vendor_id.name)+")",
sticky=True
)
+
+ def _check_assets_note(self):
+ for order in self:
+ # Cari apakah ada line dengan produk ID 614469 ('Assets Mesin & Peralatan')
+ asset_line = order.order_line.filtered(lambda l: l.product_id.id == 595346)
+ if asset_line and not order.notes:
+ raise UserError(_(
+ "%s berisi produk 'Assets Mesin & Peralatan'. "
+ "Harap isi Notes untuk menjelaskan kebutuhan dan divisi terkait."
+ ) % order.name)
def button_confirm(self):
+ self._check_assets_note()
# self._check_payment_term() # check payment term
res = super(PurchaseOrder, self).button_confirm()
current_time = datetime.now()
@@ -1066,8 +1097,11 @@ class PurchaseOrder(models.Model):
# sticky=True
# )
+ has_bom = self.product_bom_id.id
+ has_manufacturing = self.manufacturing_id.id
+
if not self.from_apo:
- if not self.matches_so and not self.env.user.is_purchasing_manager and not self.env.user.is_leader:
+ if not self.matches_so and not self.env.user.is_purchasing_manager and not self.env.user.is_leader and not has_bom and not has_manufacturing:
raise UserError("Tidak ada link dengan SO, harus di confirm oleh Purchasing Manager")
send_email = False
diff --git a/indoteknik_custom/models/purchase_order_line.py b/indoteknik_custom/models/purchase_order_line.py
index 315795d5..a3c3a33b 100755
--- a/indoteknik_custom/models/purchase_order_line.py
+++ b/indoteknik_custom/models/purchase_order_line.py
@@ -50,6 +50,7 @@ class PurchaseOrderLine(models.Model):
cost_service_per_item = fields.Float(string='Biaya Jasa Per Item', compute='_compute_doc_delivery_amt')
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)
def _compute_doc_delivery_amt(self):
for line in self:
diff --git a/indoteknik_custom/models/purchasing_job.py b/indoteknik_custom/models/purchasing_job.py
index db733b5a..3151f0f6 100644
--- a/indoteknik_custom/models/purchasing_job.py
+++ b/indoteknik_custom/models/purchasing_job.py
@@ -29,20 +29,23 @@ class PurchasingJob(models.Model):
so_number = fields.Text(string='SO Number', copy=False)
check_pj = fields.Boolean(compute='_get_check_pj', string='Linked')
+
def action_open_job_detail(self):
self.ensure_one()
Seen = self.env['purchasing.job.seen']
+
seen = Seen.search([
- ('user_id', '=', self.env.uid),
('product_id', '=', self.product_id.id)
], limit=1)
if seen:
- seen.so_snapshot = self.so_number
- seen.seen_date = fields.Datetime.now()
+ seen.write({
+ 'so_snapshot': self.so_number,
+ 'seen_date': fields.Datetime.now(),
+ 'user_id': self.env.user.id, })
else:
Seen.create({
- 'user_id': self.env.uid,
+ 'user_id': self.env.user.id,
'product_id': self.product_id.id,
'so_snapshot': self.so_number,
})
@@ -56,17 +59,13 @@ class PurchasingJob(models.Model):
'target': 'current',
}
-
@api.depends('so_number')
def _get_check_pj(self):
+ Seen = self.env['purchasing.job.seen']
for rec in self:
- seen = self.env['purchasing.job.seen'].search([
- ('user_id', '=', self.env.uid),
- ('product_id', '=', rec.product_id.id)
- ], limit=1)
+ seen = Seen.search([('product_id', '=', rec.product_id.id)], limit=1)
rec.check_pj = bool(seen and seen.so_snapshot == rec.so_number)
-
def unlink(self):
# Example: Delete related records from the underlying model
underlying_records = self.env['purchasing.job'].search([
diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py
index 9ab18f27..de9870f6 100644
--- a/indoteknik_custom/models/refund_sale_order.py
+++ b/indoteknik_custom/models/refund_sale_order.py
@@ -51,12 +51,14 @@ class RefundSaleOrder(models.Model):
account_no = fields.Char(string='Account No', required=True)
kcp = fields.Char(string='Alamat KCP')
finance_note = fields.Text(string='Finance Note')
+ biaya_admin = fields.Float(string='Biaya Admin Transfer')
invoice_names = fields.Html(string="Group Invoice Number", compute="_compute_invoice_names")
so_names = fields.Html(string="Group SO Number", compute="_compute_so_names")
refund_type = fields.Selection([
('barang_kosong_sebagian', 'Refund Barang Kosong Sebagian'),
('barang_kosong', 'Refund Barang Kosong Full'),
+ ('barang_kosong_indent', 'Refund Barang Kosong Sebagian(Indent)'),
('uang', 'Refund Lebih Bayar'),
('retur_half', 'Refund Retur Sebagian'),
('retur', 'Refund Retur Full'),
@@ -77,7 +79,7 @@ class RefundSaleOrder(models.Model):
'account.move',
string="Journal Payment",
copy=False,
- help="Pilih transaksi salah transfer dari jurnal Uang Muka (journal_id=11) yang tidak terkait SO."
+ help="Pilih transaksi salah transfer dari jurnal Uang Muka yang tidak terkait SO."
)
tukar_guling_count = fields.Integer(
@@ -113,7 +115,7 @@ class RefundSaleOrder(models.Model):
string='Customer',
required=True
)
- advance_move_names = fields.Html(string="Group Journal SO", compute="_compute_advance_move_names")
+ advance_move_names = fields.Html(string="Group Journal Payment", compute="_compute_advance_move_names")
uang_masuk_type = fields.Selection([
('pdf', 'PDF'),
('image', 'Image'),
@@ -216,7 +218,7 @@ class RefundSaleOrder(models.Model):
vals['created_date'] = fields.Date.context_today(self)
vals['create_uid'] = self.env.user.id
-
+ refund_type = vals.get('refund_type')
if 'sale_order_ids' in vals:
so_cmd = vals['sale_order_ids']
so_ids = so_cmd[0][2] if so_cmd and so_cmd[0][0] == 6 else []
@@ -226,6 +228,18 @@ class RefundSaleOrder(models.Model):
if len(partner) > 1:
raise UserError("❌ Tidak dapat membuat refund untuk Multi SO dengan Customer berbeda. Harus memiliki Customer yang sama.")
vals['partner_id'] = sale_orders[0].partner_id.id
+ if refund_type not in ['barang_kosong_indent', 'salah_transfer']:
+ for so in sale_orders:
+ if so.state not in ['cancel', 'sale']:
+ raise UserError(f"❌ SO {so.name} tidak bisa direfund. Status harus Cancel atau Sale.")
+ if so.state == 'sale':
+ not_done_pickings = so.picking_ids.filtered(lambda p: p.state not in ['done', 'cancel'])
+ if not_done_pickings:
+ raise UserError(
+ f"❌ SO {so.name} Belum melakukan kirim barang "
+ f"({', '.join(not_done_pickings.mapped('name'))}). "
+ "Selesaikan Pengiriman untuk melakukan refund."
+ )
invoices = sale_orders.mapped('invoice_ids').filtered(
lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.state != 'cancel'
@@ -234,16 +248,16 @@ class RefundSaleOrder(models.Model):
vals['invoice_ids'] = [(6, 0, invoices.ids)]
- refund_type = vals.get('refund_type')
invoice_ids_data = vals.get('invoice_ids', [])
invoice_ids = invoice_ids_data[0][2] if invoice_ids_data and invoice_ids_data[0][0] == 6 else []
+ invoices = self.env['account.move'].browse(invoice_ids)
if invoice_ids and refund_type and refund_type not in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half']:
raise UserError("Refund type Hanya Bisa Lebih Bayar, Barang Kosong Sebagian, atau Retur jika ada invoice")
if not invoice_ids and refund_type and refund_type in ['uang', 'barang_kosong_sebagian', 'retur_half']:
raise UserError("Refund type Lebih Bayar dan Barang Kosong Sebagian Hanya Bisa dipilih Jika Ada Invoice")
- if refund_type in ['barang_kosong', 'barang_kosong_sebagian'] and so_ids:
+ if refund_type in ['barang_kosong', 'barang_kosong_sebagian', 'barang_kosong_indent'] and so_ids:
sale_orders = self.env['sale.order'].browse(so_ids)
if refund_type == 'barang_kosong':
@@ -267,8 +281,11 @@ class RefundSaleOrder(models.Model):
if refund_type == 'salah_transfer' and vals.get('transfer_move_id'):
move = self.env['account.move'].browse(vals['transfer_move_id'])
if move:
+ sisa_uang_masuk = move.amount_total_signed # ← set dengan nilai move
vals['uang_masuk'] = move.amount_total_signed
vals['remaining_refundable'] = 0
+ else:
+ sisa_uang_masuk = 0.0
else:
# ==== perhitungan normal ====
moves = self.env['account.move'].search([
@@ -276,18 +293,81 @@ class RefundSaleOrder(models.Model):
('journal_id', '=', 11),
('state', '=', 'posted'),
])
- total_uang_muka = sum(moves.mapped('amount_total_signed')) if moves else 0.0
- total_midtrans = sum(self.env['sale.order'].browse(so_ids).mapped('gross_amount')) if so_ids else 0.0
- total_pembayaran = total_uang_muka + total_midtrans
+ piutangbca = self.env['account.move'].search([
+ ('ref', 'in', invoices.mapped('name')),
+ ('journal_id', '=', 4),
+ ('state', '=', 'posted'),
+ ])
+ piutangmdr = self.env['account.move'].search([
+ ('ref', 'in', invoices.mapped('name')),
+ ('journal_id', '=', 7),
+ ('state', '=', 'posted'),
+ ])
+
+ misc = self.env['account.move'].search([
+ ('ref', 'ilike', invoices.mapped('name')[0]),
+ ('ref', 'not ilike', 'reklas'),
+ ('journal_id', '=', 13),
+ ('state', '=', 'posted'),
+ ])
+ moves2 = self.env['account.move']
+ if so_ids:
+ so_names = self.env['sale.order'].browse(so_ids).mapped('name')
+ domain = [
+ ('journal_id', '=', 11),
+ ('state', '=', 'posted'),
+ ('ref', 'ilike', 'dp')
+ ]
+ if so_names:
+ domain += ['|'] * (len(so_names) - 1)
+ for n in so_names:
+ domain.append(('ref', 'ilike', n))
+ moves2 = self.env['account.move'].search(domain)
+
+ has_moves = bool(moves)
+ has_moves2 = bool(moves2)
+ has_piutangmdr = bool(piutangmdr)
+ has_piutangbca = bool(piutangbca)
+ has_misc = bool(misc)
+ ssos = self.env['sale.order'].browse(so_ids)
+ has_settlement = any(so.payment_status == 'settlement' for so in ssos)
+
+ sisa_uang_masuk = 0.0
+ amounts = []
+ if has_moves and has_settlement:
+ amounts.append(sum(moves.mapped('amount_total_signed')))
+ amounts.append(sum(ssos.mapped('gross_amount')))
+ else:
+ if has_moves:
+ amounts.append(sum(moves.mapped('amount_total_signed')))
+ if has_settlement:
+ amounts.append(sum(ssos.mapped('gross_amount')))
+
+ # sisanya bisa dijumlahkan tanpa konflik
+ if has_moves2:
+ amounts.append(sum(moves2.mapped('amount_total_signed')))
+ if has_piutangbca:
+ amounts.append(sum(piutangbca.mapped('amount_total_signed')))
+ if has_piutangmdr:
+ amounts.append(sum(piutangmdr.mapped('amount_total_signed')))
+ if has_misc:
+ amounts.append(sum(misc.mapped('amount_total_signed')))
+
+ sisa_uang_masuk = sum(amounts)
+
+ if not sisa_uang_masuk:
+ raise UserError(
+ "❌ Tidak bisa melakukan refund karena SO tidak memiliki Record Uang Masuk "
+ "(Journal Uang Muka / Payment Invoices / Midtrans Payment)."
+ )
+
existing_refunds = self.env['refund.sale.order'].search([
('sale_order_ids', 'in', so_ids)
], order='id desc', limit=1)
if existing_refunds:
sisa_uang_masuk = existing_refunds.remaining_refundable
- else:
- sisa_uang_masuk = total_pembayaran
if sisa_uang_masuk < 0:
raise UserError("❌ Tidak ada sisa transaksi untuk di-refund.")
@@ -297,6 +377,14 @@ class RefundSaleOrder(models.Model):
total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total_signed')) if invoice_ids else 0.0
vals['total_invoice'] = total_invoice
amount_refund = vals.get('amount_refund', 0.0)
+ can_refund = sisa_uang_masuk - total_invoice
+
+ if amount_refund > can_refund or can_refund == 0.0:
+ raise ValidationError(
+ _("Maksimal refund yang bisa dilakukan adalah sebesar %s. "
+ "Silakan sesuaikan jumlah refund.") % (can_refund)
+ )
+
if amount_refund <= 0.00:
raise ValidationError('Total Refund harus lebih dari 0 jika ingin mengajukan refund')
@@ -345,6 +433,7 @@ class RefundSaleOrder(models.Model):
sale_orders = self.env['sale.order'].browse(so_ids)
+
valid_invoices = sale_orders.mapped('invoice_ids').filtered(
lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.state != 'cancel'
)
@@ -354,9 +443,19 @@ class RefundSaleOrder(models.Model):
so_ids = rec.sale_order_ids.ids
sale_orders = self.env['sale.order'].browse(so_ids)
-
-
refund_type = vals.get('refund_type', rec.refund_type)
+ if refund_type not in ['barang_kosong_indent', 'salah_transfer']:
+ for so in sale_orders:
+ if so.state not in ['cancel', 'sale']:
+ raise UserError(f"❌ SO {so.name} tidak bisa direfund. Status harus Cancel atau Sale.")
+ if so.state == 'sale':
+ not_done_pickings = so.picking_ids.filtered(lambda p: p.state not in ['done', 'cancel'])
+ if not_done_pickings:
+ raise UserError(
+ f"❌ SO {so.name} Belum melakukan kirim barang "
+ f"({', '.join(not_done_pickings.mapped('name'))}). "
+ "Selesaikan Pengiriman untuk melakukan refund."
+ )
if refund_type in ['barang_kosong', 'barang_kosong_sebagian'] and sale_orders:
zero_delivery_lines = sale_orders.mapped('order_line').filtered(lambda l: l.qty_delivered >= 0 or l.product_uom_qty > l.qty_delivered)
@@ -393,9 +492,16 @@ class RefundSaleOrder(models.Model):
total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total_signed'))
vals['total_invoice'] = total_invoice
uang_masuk = rec.uang_masuk
+ can_refund = uang_masuk - total_invoice
amount_refund = vals.get('amount_refund', rec.amount_refund)
+ if amount_refund > can_refund:
+ raise ValidationError(
+ _("Maksimal refund yang bisa dilakukan adalah sebesar %s. "
+ "Silakan sesuaikan jumlah refund.") % (can_refund)
+ )
+
if amount_refund <= 0:
raise ValidationError("Total Refund harus lebih dari 0.")
@@ -437,15 +543,59 @@ class RefundSaleOrder(models.Model):
def _compute_advance_move_names(self):
for rec in self:
move_links = []
+
+ invoice_ids = rec.sale_order_ids.mapped('invoice_ids')
+
moves = self.env['account.move'].search([
('sale_id', 'in', rec.sale_order_ids.ids),
('journal_id', '=', 11),
- ('state', '=', 'posted')
+ ('state', '=', 'posted'),
])
- for move in moves:
+
+ piutangbca = self.env['account.move'].search([
+ ('ref', 'in', invoice_ids.mapped('name')),
+ ('journal_id', '=', 4),
+ ('state', '=', 'posted'),
+ ])
+
+ piutangmdr = self.env['account.move'].search([
+ ('ref', 'in', invoice_ids.mapped('name')),
+ ('journal_id', '=', 7),
+ ('state', '=', 'posted'),
+ ])
+
+ moves2 = self.env['account.move']
+ if rec.sale_order_ids:
+ so_names = rec.sale_order_ids.mapped('name')
+
+ domain = [
+ ('journal_id', '=', 11),
+ ('state', '=', 'posted'),
+ ('ref', 'ilike', 'dp')
+ ]
+ domain += ['|'] * (len(so_names) - 1)
+ for n in so_names:
+ domain.append(('ref', 'ilike', n))
+
+ moves2 = self.env['account.move'].search(domain)
+
+ misc = self.env['account.move']
+ if invoice_ids:
+ invoice_name = invoice_ids.mapped('name')[0]
+ misc = self.env['account.move'].search([
+ ('ref', 'ilike', invoice_name),
+ ('ref', 'not ilike', 'reklas'),
+ ('journal_id', '=', 13),
+ ('state', '=', 'posted'),
+ ])
+
+ all_moves = moves | piutangbca | piutangmdr | misc | moves2
+
+ for move in all_moves:
url = f"/web#id={move.id}&model=account.move&view_type=form"
name = html_escape(move.name or 'Unnamed')
move_links.append(f'<a href="{url}" target="_blank">{name}</a>')
+
rec.advance_move_names = ', '.join(move_links) if move_links else "-"
@api.depends('sale_order_ids.user_id')
@@ -463,7 +613,7 @@ class RefundSaleOrder(models.Model):
total_invoice = 0.0
so_ids = self.sale_order_ids.ids
-
+ amount_refund_before = 0.0
for so in self.sale_order_ids:
self.ongkir += so.delivery_amt or 0.0
valid_invoices = so.invoice_ids.filtered(
@@ -471,15 +621,76 @@ class RefundSaleOrder(models.Model):
)
all_invoices |= valid_invoices
total_invoice += sum(valid_invoices.mapped('amount_total_signed'))
+ refunds = self.env['refund.sale.order'].search([
+ ('sale_order_ids', 'in', so_ids)
+ ])
+ amount_refund_before += sum(refunds.mapped('amount_refund')) if refunds else 0.0
moves = self.env['account.move'].search([
('sale_id', 'in', so_ids),
('journal_id', '=', 11),
('state', '=', 'posted'),
])
- total_uang_muka = sum(moves.mapped('amount_total_signed')) if moves else 0.0
- total_midtrans = sum(self.env['sale.order'].browse(so_ids).mapped('gross_amount')) if so_ids else 0.0
- self.uang_masuk = total_uang_muka + total_midtrans
+ piutangbca = self.env['account.move'].search([
+ ('ref', 'in', all_invoices.mapped('name')),
+ ('journal_id', '=', 4),
+ ('state', '=', 'posted'),
+ ])
+ piutangmdr = self.env['account.move'].search([
+ ('ref', 'in', all_invoices.mapped('name')),
+ ('journal_id', '=', 7),
+ ('state', '=', 'posted'),
+ ])
+ misc = self.env['account.move'].search([
+ ('ref', 'ilike', all_invoices.mapped('name')[0]),
+ ('ref', 'not ilike', 'reklas'),
+ ('journal_id', '=', 13),
+ ('state', '=', 'posted'),
+ ])
+ moves2 = self.env['account.move']
+ if so_ids:
+ so_records = self.env['sale.order'].browse(so_ids)
+ so_names = so_records.mapped('name')
+
+ domain = [
+ ('journal_id', '=', 11),
+ ('state', '=', 'posted'),
+ ('ref', 'ilike', 'dp')
+ ]
+ domain += ['|'] * (len(so_names) - 1)
+ for n in so_names:
+ domain.append(('ref', 'ilike', n))
+
+ moves2 = self.env['account.move'].search(domain)
+
+ has_moves = bool(moves)
+ has_moves2 = bool(moves2)
+ has_piutangmdr = bool(piutangmdr)
+ has_piutangbca = bool(piutangbca)
+ has_misc = bool(misc)
+ ssos = self.env['sale.order'].browse(so_ids)
+ has_settlement = any(so.payment_status == 'settlement' for so in ssos)
+
+ sisa_uang_masuk = 0.0
+
+ amounts = []
+
+ if has_moves:
+ amounts.append(sum(moves.mapped('amount_total_signed')))
+ if has_moves2:
+ amounts.append(sum(moves2.mapped('amount_total_signed')))
+ if has_piutangbca:
+ amounts.append(sum(piutangbca.mapped('amount_total_signed')))
+ if has_piutangmdr:
+ amounts.append(sum(piutangmdr.mapped('amount_total_signed')))
+ if has_misc:
+ amounts.append(sum(misc.mapped('amount_total_signed')))
+ if has_settlement:
+ amounts.append(sum(ssos.mapped('gross_amount')))
+
+ sisa_uang_masuk = sum(amounts)
+
+ self.uang_masuk = sisa_uang_masuk - amount_refund_before
self.invoice_ids = all_invoices
@@ -500,7 +711,6 @@ class RefundSaleOrder(models.Model):
""" Validasi SO harus punya uang masuk (Journal Uang Muka / Midtrans) """
for rec in self:
invalid_orders = []
- total_uang_masuk = 0.0
for so in rec.sale_order_ids:
# cari journal uang muka
@@ -509,28 +719,31 @@ class RefundSaleOrder(models.Model):
('journal_id', '=', 11), # Journal Uang Muka
('state', '=', 'posted'),
])
+ piutangbca = self.env['account.move'].search([
+ ('ref', 'in', rec.invoice_ids.mapped('name')),
+ ('journal_id', '=', 4),
+ ('state', '=', 'posted'),
+ ])
+ piutangmdr = self.env['account.move'].search([
+ ('ref', 'in', rec.invoice_ids.mapped('name')),
+ ('journal_id', '=', 7),
+ ('state', '=', 'posted'),
+ ])
- if not moves and so.payment_status != 'settlement':
+ if not moves and so.payment_status != 'settlement' and not piutangbca and not piutangmdr:
invalid_orders.append(so.name)
- if moves:
- total_uang_muka = sum(moves.mapped('amount_total_signed')) or 0.0
- total_uang_masuk += total_uang_muka
- else:
- # fallback Midtrans gross_amount
- total_uang_masuk += so.gross_amount or 0.0
-
if invalid_orders:
raise ValidationError(
f"Tidak dapat membuat refund untuk SO {', '.join(invalid_orders)} "
- "karena tidak memiliki Record Uang Masuk (Journal Uang Muka/Midtrans).\n"
+ "karena tidak memiliki Record Uang Masuk (Journal Uang Muka/Payment Invoice/Midtrans).\n"
"Pastikan semua SO yang dipilih sudah memiliki Record pembayaran yang valid."
)
@api.onchange('refund_type')
def _onchange_refund_type(self):
self.line_ids = [(5, 0, 0)]
- if self.refund_type in ['barang_kosong_sebagian', 'barang_kosong'] and self.sale_order_ids:
+ if self.refund_type in ['barang_kosong_sebagian', 'barang_kosong', 'barang_kosong_indent'] and self.sale_order_ids:
line_vals = []
for so in self.sale_order_ids:
for line in so.order_line:
@@ -741,7 +954,7 @@ class RefundSaleOrder(models.Model):
if not rec.status or rec.status == 'draft':
rec.status = 'pengajuan1'
- elif rec.status == 'pengajuan1' and self.env.user.id == 19:
+ elif rec.status == 'pengajuan1' and self.env.user.id in [19, 28]:
rec.status = 'pengajuan2'
rec.approved_by = f"{rec.approved_by}, {user_name}" if rec.approved_by else user_name
rec.date_approved_sales = now
@@ -784,6 +997,15 @@ class RefundSaleOrder(models.Model):
for rec in self:
if not is_fat:
raise UserError("Hanya Finance yang dapat mengkonfirmasi pembayaran refund.")
+ is_journal = self.env['account.move'].search([
+ ('refund_id', '=', rec.id),
+ ('state', '=', 'posted')
+ ])
+ amount = rec.amount_refund + rec.biaya_admin
+ if not is_journal:
+ raise UserError("Journal Payment Refund belum dibuat, buat Journal Payment Refund sebelum confirm refund.")
+ if is_journal and amount != sum(is_journal.mapped('amount_total_signed')):
+ raise UserError("Total Refund dengan Total Journal Harus Sama.")
if rec.status_payment == 'pending':
rec.status_payment = 'done'
rec.refund_date = fields.Date.context_today(self)
@@ -820,15 +1042,27 @@ class RefundSaleOrder(models.Model):
# Ambil label refund type
refund_type_label = dict(
self.fields_get(allfields=['refund_type'])['refund_type']['selection']
- ).get(refund.refund_type, '').replace("Refund ", "").upper()
-
+ ).get(refund.refund_type, '')
+
+ # Normalisasi
+ refund_type_label = refund_type_label.upper()
+
+ if refund.refund_type in ['barang_kosong', 'barang_kosong_sebagian', 'barang_kosong_indent']:
+ refund_type_label = "REFUND BARANG KOSONG"
+ elif refund.refund_type in ['retur_half', 'retur']:
+ refund_type_label = "REFUND RETUR BARANG"
+ elif refund.refund_type == 'uang':
+ refund_type_label = "REFUND LEBIH BAYAR"
+ elif refund.refund_type == 'salah_transfer':
+ refund_type_label = "REFUND SALAH TRANSFER"
if not partner:
raise UserError("❌ Partner tidak ditemukan.")
# Ref format
- ref_text = f"REFUND {refund_type_label} {refund.name or ''} {partner.display_name}".upper()
+ ref_text = f"{refund_type_label} {refund.name or ''} {partner.display_name}".upper()
+ admintex = f"BIAYA ADMIN BANK {refund_type_label} {refund.name or ''} {partner.display_name}".upper()
# Buat Account Move (Journal Entry)
account_move = self.env['account.move'].create({
@@ -839,10 +1073,10 @@ class RefundSaleOrder(models.Model):
'refund_so_ids': [(6, 0, refund.sale_order_ids.ids)],
'partner_id': partner.id,
})
-
+ admintf = refund.biaya_admin
amount = refund.amount_refund
# 450 Penerimaan Belum Teridentifikasi, 668 Penerimaan Belum Alokasi
- second_account_id = 450 if refund.refund_type not in ['barang_kosong', 'barang_kosong_sebagian'] else 668
+ second_account_id = 450 if refund.refund_type not in ['barang_kosong', 'barang_kosong_sebagian', 'barang_kosong_indent'] else 668
debit_line = {
'move_id': account_move.id,
@@ -854,6 +1088,16 @@ class RefundSaleOrder(models.Model):
'name': ref_text,
}
+ adminline = {
+ 'move_id': account_move.id,
+ 'account_id': 555,
+ 'partner_id': partner.id,
+ 'currency_id': 12,
+ 'debit': admintf,
+ 'credit': 0.0,
+ 'name': admintex,
+ }
+
credit_line = {
'move_id': account_move.id,
'account_id': 389, # Intransit BCA
@@ -864,7 +1108,19 @@ class RefundSaleOrder(models.Model):
'name': ref_text,
}
- self.env['account.move.line'].create([debit_line, credit_line])
+ credit_admin_line = {
+ 'move_id': account_move.id,
+ 'account_id': 389, # Intransit BCA
+ 'partner_id': partner.id,
+ 'currency_id': 12,
+ 'debit': 0.0,
+ 'credit': admintf,
+ 'name': admintex,
+ }
+
+ journal_line = [debit_line, credit_line, adminline, credit_admin_line] if admintf > 0 else [debit_line, credit_line]
+
+ self.env['account.move.line'].create(journal_line)
return {
'name': _('Journal Entries'),
@@ -878,7 +1134,8 @@ class RefundSaleOrder(models.Model):
def _compute_journal_refund_move_id(self):
for rec in self:
move = self.env['account.move'].search([
- ('refund_id', '=', rec.id)
+ ('refund_id', '=', rec.id),
+ ('state', '!=', 'cancel')
], limit=1)
rec.journal_refund_move_id = move
@@ -1039,7 +1296,7 @@ class RefundSaleOrder(models.Model):
'origin': ','.join(refund.sale_order_ids.mapped('name')),
'origin_so': refund.sale_order_ids.id,
'operations': picking.id,
- 'return_type': 'revisi_so',
+ 'return_type': 'retur_so',
'invoice_id': [(6, 0, refund.invoice_ids.ids)],
'refund_id': refund.id,
'line_ids': line_vals,
diff --git a/indoteknik_custom/models/report_logbook_sj.py b/indoteknik_custom/models/report_logbook_sj.py
index 17119c12..3b07ff02 100644
--- a/indoteknik_custom/models/report_logbook_sj.py
+++ b/indoteknik_custom/models/report_logbook_sj.py
@@ -1,7 +1,14 @@
+from operator import index
+
from odoo import models, fields, api
from odoo.exceptions import UserError
from pytz import timezone
from datetime import datetime
+import requests
+import json
+import logging
+
+_logger = logging.getLogger(__name__)
class ReportLogbookSJ(models.Model):
_name = 'report.logbook.sj'
@@ -60,9 +67,28 @@ class ReportLogbookSJ(models.Model):
self.state = 'terima_semua'
else:
raise UserError('Hanya Accounting yang bisa Approve')
-
+
+
+ def write(self, vals):
+ res = super(ReportLogbookSJ, self).write(vals)
+ if 'report_logbook_sj_line' in vals or any(f in vals for f in ()):
+ self._resequence_lines()
+ return res
+
+ def _resequence_lines(self):
+ for rec in self:
+ lines = rec.report_logbook_sj_line.sorted(key=lambda l: (l.line_num or 0, l.id))
+ for idx, line in enumerate(lines, start=1):
+ if line.line_num != idx:
+ line.line_num = idx
+
+ @api.onchange('report_logbook_sj_line')
+ def _onchange_report_logbook_sj_line(self):
+ self._resequence_lines()
+
class ReportLogbookSJLine(models.Model):
_name = 'report.logbook.sj.line'
+ _order = 'sequence, id' # urut default di UI & ORM (drag pakai sequence)
name = fields.Char(string='SJ Number')
driver_id = fields.Many2one(comodel_name='res.users', string='Driver')
@@ -70,10 +96,41 @@ class ReportLogbookSJLine(models.Model):
arrival_date = fields.Char(string='Arrival Date')
carrier_id = fields.Many2one('delivery.carrier', string='Shipping Method')
tracking_no = fields.Char(string='Tracking No')
- logbook_sj_id = fields.Many2one('report.logbook.sj', string='Logbook SJ') # Corrected model name
+
+ # NOTE: field ini duplikat relasi; pakai salah satu saja.
+ # kamu boleh hapus logbook_sj_id kalau tidak dipakai di tempat lain.
+ logbook_sj_id = fields.Many2one('report.logbook.sj', string='Logbook SJ')
+
partner_id = fields.Many2one('res.partner', string='Customer')
picking_id = fields.Many2one('stock.picking', string='Picking')
sale_id = fields.Many2one('sale.order', string='Sale Order')
+
report_logbook_sj_id = fields.Many2one('report.logbook.sj', string='Logbook SJ')
not_exist = fields.Boolean(string='Not Exist')
note = fields.Char(string='Note')
+
+ sequence = fields.Integer(string='Sequence', default=0, index=True)
+
+ line_num = fields.Integer(string='No', compute='_compute_line_num', store=False)
+
+ @api.depends(
+ 'report_logbook_sj_id.report_logbook_sj_line',
+ 'report_logbook_sj_id.report_logbook_sj_line.sequence'
+ )
+ def _compute_line_num(self):
+ for parent in self.mapped('report_logbook_sj_id'):
+ lines = parent.report_logbook_sj_line.sorted(key=lambda l: (l.sequence or 0, l.id))
+ for i, l in enumerate(lines, start=1):
+ l.line_num = i
+ for rec in self.filtered(lambda r: not r.report_logbook_sj_id):
+ rec.line_num = 0
+
+ @api.model
+ def create(self, vals):
+ if not vals.get('sequence') and vals.get('report_logbook_sj_id'):
+ last = self.search(
+ [('report_logbook_sj_id', '=', vals['report_logbook_sj_id'])],
+ order='sequence desc, id desc', limit=1
+ )
+ vals['sequence'] = (last.sequence or 0) + 1
+ return super().create(vals)
diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py
index 148a3fd0..ef1a5cf4 100644
--- a/indoteknik_custom/models/res_partner.py
+++ b/indoteknik_custom/models/res_partner.py
@@ -1,6 +1,6 @@
from odoo import models, fields, api
from odoo.exceptions import UserError, ValidationError
-from datetime import datetime
+from datetime import datetime, timedelta
from odoo.http import request
import re
import requests
@@ -181,10 +181,8 @@ class ResPartner(models.Model):
payment_difficulty = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', tracking=3)
payment_history_url = fields.Text(string='Payment History URL')
- # no compute
- # payment_diff = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', tracking=3)
-
- # tidak terpakai
+ is_cbd_locked = fields.Boolean("Locked to CBD?", default=False, tracking=True, help="Jika dicentang, maka partner ini terkunci pada payment term CBD karena memiliki invoice yang sudah jatuh tempo lebih dari 30 hari.")
+
@api.model
def _default_payment_term(self):
@@ -193,9 +191,15 @@ class ResPartner(models.Model):
property_payment_term_id = fields.Many2one(
'account.payment.term',
string='Payment Terms',
- default=_default_payment_term
+ default=_default_payment_term, tracking=3
+ )
+
+ previous_payment_term_id = fields.Many2one(
+ 'account.payment.term',
+ string='Previous Payment Term'
)
+
@api.depends("street", "street2", "city", "state_id", "country_id", "blok", "nomor", "rt", "rw", "kelurahan_id",
"kecamatan_id")
def _alamat_lengkap_text(self):
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index 9952af9a..663cba58 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -234,9 +234,9 @@ class SaleOrder(models.Model):
customer_type = fields.Selection([
('pkp', 'PKP'),
('nonpkp', 'Non PKP')
- ], required=True, compute='_compute_partner_field')
- sppkp = fields.Char(string="SPPKP", required=True, tracking=True, compute='_compute_partner_field')
- npwp = fields.Char(string="NPWP", required=True, tracking=True, compute='_compute_partner_field')
+ ], related="partner_id.customer_type", string="Customer Type", readonly=True)
+ sppkp = fields.Char(string="SPPKP", related="partner_id.sppkp")
+ npwp = fields.Char(string="NPWP", related="partner_id.npwp")
purchase_total = fields.Monetary(string='Purchase Total', compute='_compute_purchase_total')
voucher_id = fields.Many2one(comodel_name='voucher', string='Voucher', copy=False)
applied_voucher_id = fields.Many2one(comodel_name='voucher', string='Applied Voucher', copy=False)
@@ -393,6 +393,43 @@ class SaleOrder(models.Model):
('paid', 'Full Paid'),
('no_invoice', 'No Invoice'),
], string="Payment Status Invoice", compute="_compute_payment_state_custom", store=False)
+ partner_is_cbd_locked = fields.Boolean(
+ string="Partner Locked CBD",
+ compute="_compute_partner_is_cbd_locked"
+ )
+
+ 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,
+ }
+ }
+
+
+ @api.depends('partner_id.is_cbd_locked')
+ def _compute_partner_is_cbd_locked(self):
+ for order in self:
+ order.partner_is_cbd_locked = order.partner_id.is_cbd_locked
+
+
+ @api.constrains('payment_term_id', 'partner_id', 'state')
+ def _check_cbd_lock_sale_order(self):
+ cbd_term = self.env['account.payment.term'].browse(26)
+ for rec in self:
+ if rec.state == 'draft' and rec.partner_id.is_cbd_locked:
+ if rec.payment_term_id and rec.payment_term_id != cbd_term:
+ raise ValidationError(
+ "Customer ini terkunci ke CBD, hanya boleh pakai Payment Term CBD."
+ )
@api.depends('invoice_ids.payment_state', 'invoice_ids.amount_total', 'invoice_ids.amount_residual')
def _compute_payment_state_custom(self):
@@ -1654,7 +1691,7 @@ class SaleOrder(models.Model):
rec.expected_ready_to_ship = eta_date
@api.depends("order_line.product_id", "date_order")
- def _compute_etrts_date(self): # Function to calculate Estimated Ready To Ship Date
+ def _compute_etrts_date(self):
self._calculate_etrts_date()
@@ -1728,9 +1765,54 @@ class SaleOrder(models.Model):
# sinkronkan ke field commitment_date
rec.commitment_date = rec.expected_ready_to_ship
+ # def _validate_expected_ready_ship_date(self):
+ # """
+ # Pastikan expected_ready_to_ship tidak lebih awal dari SLA minimum.
+ # Dipanggil setiap onchange / simpan SO.
+ # """
+ # for rec in self:
+ # if not rec.expected_ready_to_ship:
+ # continue
+ #
+ # # ADDED: gunakan "sekarang" lokal user, bukan datetime.now() server
+ # current_date = fields.Datetime.context_timestamp(rec, fields.Datetime.now())
+ #
+ # # Hitung SLA
+ # products = rec.order_line
+ # if products:
+ # sla_data = rec.calculate_sla_by_vendor(products)
+ # max_sla_time = sla_data.get('slatime', 1)
+ # else:
+ # max_sla_time = 1
+ #
+ # # offset hari libur/weekend
+ # offset, is3pm = rec.get_days_until_next_business_day(current_date)
+ # min_days = max_sla_time + offset - 1
+ # eta_minimum = current_date + timedelta(days=min_days)
+ #
+ # if rec._fields['expected_ready_to_ship'].type == 'date':
+ # exp_date_local = rec.expected_ready_to_ship
+ # else:
+ # exp_date_local = fields.Datetime.context_timestamp(
+ # rec, rec.expected_ready_to_ship
+ # ).date()
+ #
+ # if exp_date_local < eta_minimum.date():
+ # # (opsional) auto-set ke minimum → konversi balik ke UTC naive bila field Datetime
+ # if rec._fields['expected_ready_to_ship'].type == 'date':
+ # rec.expected_ready_to_ship = eta_minimum.date()
+ # else:
+ # rec.expected_ready_to_ship = eta_minimum.astimezone(pytz.UTC).replace(tzinfo=None)
+ #
+ # raise ValidationError(
+ # _("Tanggal 'Expected Ready to Ship' tidak boleh "
+ # "lebih kecil dari %(tgl)s. Mohon pilih minimal %(tgl)s.")
+ # % {'tgl': eta_minimum.strftime('%d-%m-%Y')}
+ # )
+ # else:
+ # rec.commitment_date = rec.expected_ready_to_ship
-
-
+
@api.onchange('expected_ready_to_ship') #Hangle Onchange form Expected Ready to Ship
def _onchange_expected_ready_ship_date(self):
self._validate_expected_ready_ship_date()
@@ -1824,11 +1906,12 @@ class SaleOrder(models.Model):
def override_allow_create_invoice(self):
if not self.env.user.is_accounting:
raise UserError('Hanya Finance Accounting yang dapat klik tombol ini')
- for term in self.payment_term_id.line_ids:
- if term.days > 0:
- raise UserError('Hanya dapat digunakan pada Cash Before Delivery')
+ # for term in self.payment_term_id.line_ids:
+ # if term.days > 0:
+ # raise UserError('Hanya dapat digunakan pada Cash Before Delivery')
for line in self.order_line:
- line.qty_to_invoice = line.product_uom_qty
+ if line.product_id.type == 'product':
+ line.qty_to_invoice = line.product_uom_qty
# def _get_pickings(self):
# state = ['assigned']
@@ -1856,6 +1939,8 @@ class SaleOrder(models.Model):
})
def open_form_multi_update_status(self):
+ if self.env.user.id != 688 or self.env.user.has_group('indoteknik_custom.group_role_it'):
+ raise UserError("Hanya Finance nya yang bisa approve.")
action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_sale_orders_multi_update')
action['context'] = {
'sale_ids': [x.id for x in self]
@@ -2029,22 +2114,22 @@ class SaleOrder(models.Model):
# return [('id', 'not in', order_ids)]
# return ['&', ('order_line.invoice_lines.move_id.move_type', 'in', ('out_invoice', 'out_refund')), ('order_line.invoice_lines.move_id', operator, value)]
- @api.depends('partner_id')
- def _compute_partner_field(self):
- for order in self:
- partner = order.partner_id.parent_id or order.partner_id
- order.npwp = partner.npwp
- order.sppkp = partner.sppkp
- order.customer_type = partner.customer_type
+ # @api.depends('partner_id')
+ # def _compute_partner_field(self):
+ # for order in self:
+ # partner = order.partner_id.parent_id or order.partner_id
+ # order.npwp = partner.npwp
+ # order.sppkp = partner.sppkp
+ # order.customer_type = partner.customer_type
@api.onchange('partner_id')
def onchange_partner_contact(self):
parent_id = self.partner_id.parent_id
parent_id = parent_id if parent_id else self.partner_id
- self.npwp = parent_id.npwp
- self.sppkp = parent_id.sppkp
- self.customer_type = parent_id.customer_type
+ # self.npwp = parent_id.npwp
+ # self.sppkp = parent_id.sppkp
+ # self.customer_type = parent_id.customer_type
self.email = parent_id.email
self.pareto_status = parent_id.pareto_status
self.user_id = parent_id.user_id
@@ -2115,15 +2200,21 @@ class SaleOrder(models.Model):
if self.payment_term_id.id == 31 and self.total_percent_margin < 25:
raise UserError("Jika ingin menggunakan Tempo 90 Hari maka margin harus di atas 25%")
- if self.warehouse_id.id != 8 and self.warehouse_id.id != 10: # GD Bandengan
- raise UserError('Gudang harus Bandengan')
+ if self.warehouse_id.id != 8 and self.warehouse_id.id != 10 and self.warehouse_id.id != 12: # GD Bandengan / Pameran
+ raise UserError('Gudang harus Bandengan atau Pameran')
if self.state not in ['draft', 'sent']:
raise UserError("Status harus draft atau sent")
- self._validate_npwp()
-
def _validate_npwp(self):
+ if not self.npwp:
+ raise UserError("NPWP partner kosong, silahkan isi terlebih dahulu npwp nya di contact partner")
+
+ if not self.customer_type:
+ raise UserError("Customer Type partner kosong, silahkan isi terlebih dahulu Customer Type nya di contact partner")
+
+ if not self.sppkp:
+ raise UserError("SPPKP partner kosong, silahkan isi terlebih dahulu SPPKP nya di contact partner")
num_digits = sum(c.isdigit() for c in self.npwp)
if num_digits < 10:
@@ -2137,6 +2228,7 @@ class SaleOrder(models.Model):
self._validate_order()
for order in self:
+ order._validate_npwp()
order._validate_uniform_taxes()
order.order_line.validate_line()
@@ -2184,16 +2276,15 @@ class SaleOrder(models.Model):
raise UserError("Terdapat DUPLIKASI data pada Product {}".format(line.product_id.display_name))
def sale_order_approve(self):
- self.check_duplicate_product()
+ # self.check_duplicate_product()
self.check_product_bom()
self.check_credit_limit()
self.check_limit_so_to_invoice()
if self.validate_different_vendor() and not self.vendor_approval:
return self._create_notification_action('Notification', 'Terdapat Vendor yang berbeda dengan MD Vendor')
self.check_due()
-
- self._validate_order()
for order in self:
+ order._validate_npwp()
order._validate_delivery_amt()
order._validate_uniform_taxes()
order.order_line.validate_line()
@@ -2386,17 +2477,22 @@ class SaleOrder(models.Model):
# Ambil blocking stage dari partner
block_stage = rec.partner_id.parent_id.blocking_stage if rec.partner_id.parent_id else rec.partner_id.blocking_stage or 0
is_cbd = rec.partner_id.parent_id.property_payment_term_id.id == 26 if rec.partner_id.parent_id else rec.partner_id.property_payment_term_id.id == 26 or False
+ partner_term = rec.partner_id.property_payment_term_id
+ partner_term_days_total = 0
+ if partner_term:
+ partner_term_days_total = sum((line.days or 0) for line in partner_term.line_ids)
+ is_partner_cbd = (partner_term_days_total == 0)
+ is_so_cbd = bool(rec.payment_term_id.id == 26)
- # Ambil jumlah nilai dari SO yang invoice_status masih 'to invoice'
so_to_invoice = 0
for sale in rec.partner_id.sale_order_ids:
if sale.invoice_status == 'to invoice':
so_to_invoice = so_to_invoice + sale.amount_total
- # Hitung remaining credit limit
- remaining_credit_limit = block_stage - current_total - so_to_invoice
+
+ remaining_credit_limit = block_stage - current_total - so_to_invoice if not is_cbd and not is_partner_cbd else 0
# Validasi limit
- if remaining_credit_limit <= 0 and block_stage > 0 and not is_cbd:
+ if remaining_credit_limit <= 0 and block_stage > 0 and not is_cbd and not is_so_cbd and not is_partner_cbd:
raise UserError(
_("The credit limit for %s will exceed the Blocking Stage if the Sale Order is confirmed. The remaining credit limit is %s, from %s and the outstanding amount is %s.")
% (rec.partner_id.name, block_stage - current_total, block_stage, outstanding_amount))
@@ -2450,7 +2546,7 @@ class SaleOrder(models.Model):
for order in self:
order._validate_delivery_amt()
order._validate_uniform_taxes()
- order.check_duplicate_product()
+ # order.check_duplicate_product()
order.check_product_bom()
order.check_credit_limit()
order.check_limit_so_to_invoice()
@@ -2460,6 +2556,7 @@ class SaleOrder(models.Model):
order.check_data_real_delivery_address()
order.sale_order_check_approve()
order._validate_order()
+ order._validate_npwp()
order.order_line.validate_line()
main_parent = order.partner_id.get_main_parent()
@@ -2617,7 +2714,7 @@ class SaleOrder(models.Model):
if user.is_leader or user.is_sales_manager:
return True
- if user.id in (3401, 20, 3988): # admin (fida, nabila, ninda)
+ if user.id in (3401, 20, 3988, 17340): # admin (fida, nabila, ninda)
return True
if self.env.context.get("ask_approval") and user.id in (3401, 20, 3988):
@@ -2645,23 +2742,17 @@ class SaleOrder(models.Model):
def _set_sppkp_npwp_contact(self):
partner = self.partner_id.parent_id or self.partner_id
- if not partner.sppkp:
- partner.sppkp = self.sppkp
- if not partner.npwp:
- partner.npwp = self.npwp
+ # if not partner.sppkp:
+ # partner.sppkp = self.sppkp
+ # if not partner.npwp:
+ # partner.npwp = self.npwp
if not partner.email:
partner.email = self.email
- if not partner.customer_type:
- partner.customer_type = self.customer_type
+ # if not partner.customer_type:
+ # partner.customer_type = self.customer_type
if not partner.user_id:
partner.user_id = self.user_id.id
- # if not partner.sppkp or not partner.npwp or not partner.email or partner.customer_type:
- # partner.customer_type = self.customer_type
- # partner.npwp = self.npwp
- # partner.sppkp = self.sppkp
- # partner.email = self.email
-
def _compute_total_margin(self):
for order in self:
total_margin = sum(line.item_margin for line in order.order_line if line.product_id)
@@ -3103,52 +3194,6 @@ class SaleOrder(models.Model):
# order._update_partner_details()
return order
- # def write(self, vals):
- # Call the super method to handle the write operation
- # res = super(SaleOrder, self).write(vals)
- # self._compute_etrts_date()
- # Check if the update is coming from a save operation
- # if any(field in vals for field in ['sppkp', 'npwp', 'email', 'customer_type']):
- # self._update_partner_details()
-
- # return res
-
- def _update_partner_details(self):
- for order in self:
- partner = order.partner_id.parent_id or order.partner_id
- if partner:
- # Update partner details
- partner.sppkp = order.sppkp
- partner.npwp = order.npwp
- partner.email = order.email
- partner.customer_type = order.customer_type
-
- # Save changes to the partner record
- partner.write({
- 'sppkp': partner.sppkp,
- 'npwp': partner.npwp,
- 'email': partner.email,
- 'customer_type': partner.customer_type,
- })
-
- # def write(self, vals):
- # for order in self:
- # if order.state in ['sale', 'cancel']:
- # if 'order_line' in vals:
- # new_lines = vals.get('order_line', [])
- # for command in new_lines:
- # if command[0] == 0: # A new line is being added
- # raise UserError(
- # "SO tidak dapat ditambahkan produk baru karena SO sudah menjadi sale order.")
- #
- # res = super(SaleOrder, self).write(vals)
- # # self._check_total_margin_excl_third_party()
- # if any(fields in vals for fields in ['delivery_amt', 'carrier_id', 'shipping_cost_covered']):
- # self._validate_delivery_amt()
- # if any(field in vals for field in ["order_line", "client_order_ref"]):
- # self._calculate_etrts_date()
- # return res
-
# @api.depends('commitment_date')
def _compute_ready_to_ship_status_detail(self):
def is_empty(val):
@@ -3276,40 +3321,57 @@ class SaleOrder(models.Model):
def button_refund(self):
self.ensure_one()
+
+ invoice_ids = self.invoice_ids.filtered(lambda inv: inv.state != 'cancel')
- if self.state not in ['cancel', 'sale']:
- raise UserError(f"❌ SO {self.name} tidak bisa direfund. Status harus Cancel atau Sale.")
- if self.state == 'sale':
- not_done_pickings = self.picking_ids.filtered(lambda p: p.state not in ['done', 'cancel'])
- if not_done_pickings:
- raise UserError(
- f"❌ SO {self.name} Belum melakukan kirim barang "
- f"({', '.join(not_done_pickings.mapped('name'))}). Selesaikan Pengiriman untuk melakukan refund."
- )
moves = self.env['account.move'].search([
('sale_id', '=', self.id),
('journal_id', '=', 11),
('state', '=', 'posted'),
])
+ piutangbca = self.env['account.move'].search([
+ ('ref', 'in', invoice_ids.mapped('name')),
+ ('journal_id', '=', 4),
+ ('state', '=', 'posted'),
+ ])
+ piutangmdr = self.env['account.move'].search([
+ ('ref', 'in', invoice_ids.mapped('name')),
+ ('journal_id', '=', 7),
+ ('state', '=', 'posted'),
+ ])
+
+ moves2 = self.env['account.move'].search([
+ ('ref', 'ilike', self.name),
+ ('journal_id', '=', 11),
+ ('state', '=', 'posted'),
+ ])
# Default 0
total_uang_muka = 0.0
has_moves = bool(moves)
+ has_moves2 = bool(moves2)
+ has_piutangmdr = bool(piutangmdr)
+ has_piutangbca = bool(piutangbca)
has_settlement = self.payment_status == 'settlement'
if has_moves and has_settlement:
total_uang_muka = sum(moves.mapped('amount_total_signed')) + self.gross_amount
elif has_moves:
total_uang_muka = sum(moves.mapped('amount_total_signed'))
+ elif has_moves2:
+ total_uang_muka = sum(moves2.mapped('amount_total_signed'))
elif has_settlement:
total_uang_muka = self.gross_amount
+ elif has_piutangbca:
+ total_uang_muka = sum(piutangbca.mapped('amount_total_signed'))
+ elif has_piutangmdr:
+ total_uang_muka = sum(piutangmdr.mapped('amount_total_signed'))
else:
raise UserError(
"Tidak bisa melakukan refund karena SO tidak memiliki Record Uang Masuk "
- "(Journal Uang Muka/Midtrans Payment)."
+ "(Journal Uang Muka/Payment Invoices/Midtrans Payment)."
)
- invoice_ids = self.invoice_ids.filtered(lambda inv: inv.state != 'cancel')
total_refunded = sum(self.refund_ids.mapped('amount_refund'))
sisa_uang_muka = total_uang_muka - total_refunded
@@ -3386,12 +3448,20 @@ class SaleOrder(models.Model):
('state', '=', 'posted'),
])
+ moves2 = self.env['account.move'].search([
+ ('ref', 'ilike', order.name),
+ ('journal_id', '=', 11),
+ ('state', '=', 'posted'),
+ ])
+
total_uang_muka = 0.0
if moves and order.payment_status == 'settlement':
total_uang_muka = order.gross_amount + sum(moves.mapped('amount_total_signed')) or 0.0
elif moves:
total_uang_muka = sum(moves.mapped('amount_total_signed')) or 0.0
+ elif moves2:
+ total_uang_muka = sum(moves2.mapped('amount_total_signed')) or 0.0
elif order.payment_status == 'settlement':
total_uang_muka = order.gross_amount
else:
diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py
index 47a24264..1f2ea1fb 100644
--- a/indoteknik_custom/models/sale_order_line.py
+++ b/indoteknik_custom/models/sale_order_line.py
@@ -71,23 +71,17 @@ class SaleOrderLine(models.Model):
if order_qty > 0:
for move in line.move_ids:
- # --- CASE 1: Move belum selesai ---
if move.state not in ('done', 'cancel'):
reserved_qty += move.reserved_availability or 0.0
continue
- # --- CASE 2: Move sudah done ---
if move.location_dest_id.usage == 'customer':
- # Barang dikirim ke customer
delivered_qty += move.quantity_done or 0.0
elif move.location_id.usage == 'customer':
- # Barang balik dari customer (retur)
delivered_qty -= move.quantity_done or 0.0
- # Clamp supaya delivered gak minus
delivered_qty = max(delivered_qty, 0)
- # Hitung persen
line.reserved_percent = min((reserved_qty / order_qty) * 100, 100) if order_qty else 0
line.delivered_percent = min((delivered_qty / order_qty) * 100, 100) if order_qty else 0
line.unreserved_percent = max(100 - line.reserved_percent - line.delivered_percent, 0)
diff --git a/indoteknik_custom/models/sj_tele.py b/indoteknik_custom/models/sj_tele.py
new file mode 100644
index 00000000..d44aa338
--- /dev/null
+++ b/indoteknik_custom/models/sj_tele.py
@@ -0,0 +1,102 @@
+from odoo import models, fields, api
+from odoo.exceptions import UserError
+import requests
+import json
+import logging, subprocess
+import time
+from collections import OrderedDict
+
+_logger = logging.getLogger(__name__)
+
+class SjTele(models.Model):
+ _name = 'sj.tele'
+ _description = 'sj.tele'
+
+ picking_id = fields.Many2one('stock.picking', string='Picking')
+ sale_id = fields.Many2one('sale.order', string='Sales Order')
+ picking_name = fields.Char(string='Picking Name')
+ 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
+
+ def woi(self):
+ bot_mqdd = '8203414501:AAHy_XwiUAVrgRM2EJzW7sZx9npRLITZpb8'
+ chat_id_mqdd = '-1003087280519'
+ api_base = f'https://api.telegram.org/bot{bot_mqdd}'
+
+ data = self.search([], order='create_date asc', limit=15)
+
+ if not data:
+ text = "Berikut merupakan nomor BU/OUT yang belum ada di Logbook SJ report:\n✅ tidak ada data (semua sudah tercatat)."
+ 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 (no data): %s", e)
+ return True
+
+
+ lines = []
+ groups = OrderedDict()
+
+ for rec in data:
+ name = rec.picking_name or (rec.picking_id.name if rec.picking_id else '')
+ pid = rec.picking_id.id if rec.picking_id else ''
+ so = rec.sale_id.name or rec.sale_name or ''
+ 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]
+ else:
+ date_header = '(Tidak ada tanggal kirim SJ)'
+
+ 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
diff --git a/indoteknik_custom/models/stock_move.py b/indoteknik_custom/models/stock_move.py
index 24d405a6..d6505a86 100644
--- a/indoteknik_custom/models/stock_move.py
+++ b/indoteknik_custom/models/stock_move.py
@@ -18,7 +18,8 @@ class StockMove(models.Model):
barcode = fields.Char(string='Barcode', related='product_id.barcode')
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)
+ product_image = fields.Binary(related="product_id.image_128", string="Product Image", readonly=True)
+
# @api.model_create_multi
# def create(self, vals_list):
# moves = super(StockMove, self).create(vals_list)
diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py
index a48e0ed1..b27e6b5d 100644
--- a/indoteknik_custom/models/stock_picking.py
+++ b/indoteknik_custom/models/stock_picking.py
@@ -89,8 +89,9 @@ class StockPicking(models.Model):
readonly=True,
related="id",
)
- sj_documentation = fields.Binary(string="Dokumentasi Surat Jalan", )
- paket_documentation = fields.Binary(string="Dokumentasi Paket", )
+ sj_documentation = fields.Binary(string="Dokumentasi Surat Jalan")
+ paket_documentation = fields.Binary(string="Dokumentasi Paket")
+ dispatch_documentation = fields.Binary(string="Dokumentasi Dispatch")
sj_return_date = fields.Datetime(string="SJ Return Date", copy=False)
responsible = fields.Many2one('res.users', string='Responsible', tracking=True)
@@ -307,6 +308,7 @@ class StockPicking(models.Model):
('delay', 'Delay By Vendor'),
('urgent', 'Urgent Delivery'),
], string='Reason Change Date Planned', tracking=True)
+ delivery_date = fields.Datetime(string='Delivery Date', copy=False)
def _get_kgx_awb_number(self):
"""Menggabungkan name dan origin untuk membuat AWB Number"""
@@ -1351,6 +1353,19 @@ 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)
+ if stock_journal:
+ stock_journal.write({
+ 'purchase_order_id': picking.purchase_id.id
+ })
+
self.date_done = datetime.datetime.utcnow()
self.state_reserve = 'done'
self.final_seq = 0
@@ -1743,27 +1758,37 @@ class StockPicking(models.Model):
}
if self.biteship_id:
- histori = self.get_manifest_biteship()
- day_start = order.estimated_arrival_days_start
- day_end = order.estimated_arrival_days
- if sale_order_delay:
- if sale_order_delay.status == 'delayed':
- day_start = day_start + sale_order_delay.days_delayed
- day_end = day_end + sale_order_delay.days_delayed
- elif sale_order_delay.status == 'early':
- day_start = day_start - sale_order_delay.days_delayed
- day_end = day_end - sale_order_delay.days_delayed
-
- eta_start = order.date_order + timedelta(days=day_start)
- eta_end = order.date_order + timedelta(days=day_end)
- formatted_eta = f"{eta_start.strftime('%d %b')} - {eta_end.strftime('%d %b %Y')}"
- response['eta'] = formatted_eta
- response['manifests'] = histori.get("manifests", [])
- response['delivered'] = histori.get("delivered",
- False) or self.sj_return_date != False or self.driver_arrival_date != False
- response['status'] = self._map_status_biteship(histori.get("delivered"))
+ try:
+ histori = self.get_manifest_biteship()
+ day_start = order.estimated_arrival_days_start
+ day_end = order.estimated_arrival_days
+ if sale_order_delay:
+ if sale_order_delay.status == 'delayed':
+ day_start += sale_order_delay.days_delayed
+ day_end += sale_order_delay.days_delayed
+ elif sale_order_delay.status == 'early':
+ day_start -= sale_order_delay.days_delayed
+ day_end -= sale_order_delay.days_delayed
+
+ eta_start = order.date_order + timedelta(days=day_start)
+ eta_end = order.date_order + timedelta(days=day_end)
+ formatted_eta = f"{eta_start.strftime('%d %b')} - {eta_end.strftime('%d %b %Y')}"
+
+ response['eta'] = formatted_eta
+ response['manifests'] = histori.get("manifests", [])
+ response['delivered'] = (
+ histori.get("delivered", False)
+ or self.sj_return_date != False
+ or self.driver_arrival_date != False
+ )
+ response['status'] = self._map_status_biteship(histori.get("delivered"))
- return response
+ return response
+
+ except Exception as e:
+ # Kalau ada error di biteship, log dan fallback ke Odoo
+ _logger.warning("Biteship error pada DO %s: %s", self.name, str(e))
+ # biarkan lanjut ke kondisi di bawah (pakai Odoo waybill_id)
if not self.waybill_id or len(self.waybill_id.manifest_ids) == 0:
response['delivered'] = self.sj_return_date != False or self.driver_arrival_date != False
diff --git a/indoteknik_custom/models/stock_picking_return.py b/indoteknik_custom/models/stock_picking_return.py
index 88acf83c..53a85f67 100644
--- a/indoteknik_custom/models/stock_picking_return.py
+++ b/indoteknik_custom/models/stock_picking_return.py
@@ -110,7 +110,7 @@ class ReturnPicking(models.TransientModel):
if mapping_koli_vals:
context['default_mapping_koli_ids'] = mapping_koli_vals
- if picking.purchase_id or 'PO' in (picking.origin or ''):
+ if picking.name and any(k in picking.name.upper() for k in ('PUT', 'INPUT')):
_logger.info("Redirect ke Tukar Guling PO via purchase_id / origin")
return {
'name': _('Tukar Guling PO'),
diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py
index 6e839bf0..cb630a04 100644
--- a/indoteknik_custom/models/tukar_guling.py
+++ b/indoteknik_custom/models/tukar_guling.py
@@ -61,7 +61,7 @@ class TukarGuling(models.Model):
notes = fields.Text('Notes')
return_type = fields.Selection(String='Return Type', selection=[
('tukar_guling', 'Tukar Guling'), # -> barang yang sama
- ('revisi_so', 'Revisi SO')], required=True, tracking=3)
+ ('retur_so', 'Retur SO')], required=True, tracking=3, help='Retur SO (ORT-SRT),\n Tukar Guling (ORT-SRT-PICK-OUT)')
state = fields.Selection(string='Status', selection=[
('draft', 'Draft'),
('approval_sales', ' Approval Sales'),
@@ -169,7 +169,7 @@ class TukarGuling(models.Model):
raise UserError("❌ Picking type harus BU/OUT atau BU/PICK")
for rec in self:
if rec.operations and rec.operations.picking_type_id.id == 30:
- rec.return_type = 'revisi_so'
+ rec.return_type = 'retur_so'
if self.operations:
from_return_picking = self.env.context.get('from_return_picking', False) or \
@@ -315,7 +315,7 @@ class TukarGuling(models.Model):
@api.constrains('return_type', 'operations')
def _check_required_bu_fields(self):
for record in self:
- if record.return_type in ['revisi_so', 'tukar_guling'] and not record.operations:
+ if record.return_type in ['retur_so', 'tukar_guling'] and not record.operations:
raise ValidationError("Operations harus diisi")
@api.constrains('line_ids', 'state')
@@ -352,16 +352,16 @@ class TukarGuling(models.Model):
# ('state', '!=', 'cancel')
# ]) > 0
- # def _check_invoice_on_revisi_so(self):
+ # def _check_invoice_on_retur_so(self):
# for record in self:
- # if record.return_type == 'revisi_so' and record.origin:
+ # if record.return_type == 'retur_so' and record.origin:
# invoices = self.env['account.move'].search([
# ('invoice_origin', 'ilike', record.origin),
# ('state', 'not in', ['draft', 'cancel'])
# ])
# if invoices:
# raise ValidationError(
- # _("Tidak bisa memilih Return Type 'Revisi SO' karena dokumen %s sudah dibuat invoice.") % record.origin
+ # _("Tidak bisa memilih Return Type 'Retur SO' karena dokumen %s sudah dibuat invoice.") % record.origin
# )
@@ -414,7 +414,7 @@ class TukarGuling(models.Model):
self.ensure_one()
if self.operations.picking_type_id.id not in [29, 30]:
raise UserError("❌ Picking type harus BU/OUT atau BU/PICK")
- # self._check_invoice_on_revisi_so()
+ # self._check_invoice_on_retur_so()
operasi = self.operations.picking_type_id.id
tipe = self.return_type
pp = vals.get('return_type', tipe)
@@ -530,7 +530,7 @@ class TukarGuling(models.Model):
raise UserError(
_("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name
)
- # self._check_invoice_on_revisi_so()
+ # self._check_invoice_on_retur_so()
self._validate_product_lines()
if self.state != 'draft':
@@ -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,8 +557,27 @@ class TukarGuling(models.Model):
if self.state == 'approved' and total_out > 0 and done_out == total_out:
self.state = 'done'
- # OUT revisi SO
- elif self.operations.picking_type_id.id == 29 and self.return_type == 'revisi_so':
+ #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),
('picking_type_id', '=', 74),
@@ -567,7 +591,7 @@ class TukarGuling(models.Model):
self.state = 'done'
# PICK revisi SO
- elif self.operations.picking_type_id.id == 30 and self.return_type == 'revisi_so':
+ elif self.operations.picking_type_id.id == 30 and self.return_type == 'retur_so':
done_ort = self.env['stock.picking'].search([
('tukar_guling_id', '=', self.id),
('picking_type_id', '=', 74),
@@ -581,7 +605,7 @@ class TukarGuling(models.Model):
def action_approve(self):
self.ensure_one()
self._validate_product_lines()
- # self._check_invoice_on_revisi_so()
+ # self._check_invoice_on_retur_so()
self._check_not_allow_tukar_guling_on_bu_pick()
operasi = self.operations.picking_type_id.id
@@ -631,7 +655,7 @@ class TukarGuling(models.Model):
elif rec.state == 'approval_finance':
if not rec.env.user.has_group('indoteknik_custom.group_role_fat'):
raise UserError("Hanya Finance Manager yang boleh approve tahap ini.")
- # rec._check_invoice_on_revisi_so()
+ # rec._check_invoice_on_retur_so()
rec.set_opt()
rec.state = 'approval_logistic'
rec.date_finance = now
@@ -710,7 +734,7 @@ class TukarGuling(models.Model):
### ======== SRT dari BU/OUT =========
srt_return_lines = []
- if mapping_koli:
+ if mapping_koli and record.operations.picking_type_id.id == 29:
for prod in mapping_koli.mapped('product_id'):
qty_total = sum(mk.qty_return for mk in mapping_koli.filtered(lambda m: m.product_id == prod))
move = bu_out.move_lines.filtered(lambda m: m.product_id == prod)
@@ -723,7 +747,7 @@ class TukarGuling(models.Model):
}))
_logger.info(f"📟 SRT line: {prod.display_name} | qty={qty_total}")
- elif not mapping_koli:
+ elif not mapping_koli and record.operations.picking_type_id.id == 29:
for line in record.line_ids:
move = bu_out.move_lines.filtered(lambda m: m.product_id == line.product_id)
if not move:
diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py
index f2f37606..2a5ca3dd 100644
--- a/indoteknik_custom/models/tukar_guling_po.py
+++ b/indoteknik_custom/models/tukar_guling_po.py
@@ -38,9 +38,9 @@ class TukarGulingPO(models.Model):
)
ba_num = fields.Char('Nomor BA', tracking=3)
return_type = fields.Selection([
- ('revisi_po', 'Revisi PO'),
+ ('retur_po', 'Retur PO'),
('tukar_guling', 'Tukar Guling'),
- ], string='Return Type', required=True, tracking=3)
+ ], string='Return Type', required=True, tracking=3, help='Retur PO (VRT-PRT),\n Tukar Guling (VRT-PRT-INPUT-PUT')
notes = fields.Text('Notes', tracking=3)
tukar_guling_po_id = fields.Many2one('tukar.guling.po', string='Tukar Guling PO', ondelete='cascade')
line_ids = fields.One2many('tukar.guling.line.po', 'tukar_guling_po_id', string='Product Lines', tracking=3)
@@ -143,9 +143,9 @@ class TukarGulingPO(models.Model):
return res
- # def _check_bill_on_revisi_po(self):
+ # def _check_bill_on_retur_po(self):
# for record in self:
- # if record.return_type == 'revisi_po' and record.origin:
+ # if record.return_type == 'retur_po' and record.origin:
# bills = self.env['account.move'].search([
# ('invoice_origin', 'ilike', record.origin),
# ('move_type', '=', 'in_invoice'), # hanya vendor bill
@@ -153,7 +153,7 @@ class TukarGulingPO(models.Model):
# ])
# if bills:
# raise ValidationError(
- # _("Tidak bisa memilih Return Type 'Revisi PO' karena PO %s sudah dibuat vendor bill. Harus Cancel Jika ingin melanjutkan") % record.origin
+ # _("Tidak bisa memilih Return Type 'Retur PO' karena PO %s sudah dibuat vendor bill. Harus Cancel Jika ingin melanjutkan") % record.origin
# )
@api.onchange('operations')
@@ -284,7 +284,7 @@ class TukarGulingPO(models.Model):
@api.constrains('return_type', 'operations')
def _check_required_bu_fields(self):
for record in self:
- if record.return_type in ['revisi_po', 'tukar_guling'] and not record.operations:
+ if record.return_type in ['retur_po', 'tukar_guling'] and not record.operations:
raise ValidationError("Operations harus diisi")
@api.constrains('line_ids', 'state')
@@ -350,21 +350,21 @@ class TukarGulingPO(models.Model):
def write(self, vals):
if self.operations.picking_type_id.id not in [75, 28]:
raise UserError("❌ Tidak bisa retur bukan BU/INPUT atau BU/PUT!")
- # self._check_bill_on_revisi_po()
+ # self._check_bill_on_retur_po()
tipe = vals.get('return_type', self.return_type)
- if self.operations and self.operations.picking_type_id.id == 28 and tipe == 'tukar_guling':
- group = self.operations.group_id
- if group:
- # Cari BU/PUT dalam group yang sama
- bu_put = self.env['stock.picking'].search([
- ('group_id', '=', group.id),
- ('picking_type_id.id', '=', 75), # 75 = ID BU/PUT
- ('state', '=', 'done')
- ], limit=1)
-
- if bu_put:
- raise UserError("❌ Tidak bisa retur BU/INPUT karena BU/PUT sudah Done!")
+ # if self.operations and self.operations.picking_type_id.id == 28 and tipe == 'tukar_guling':
+ # group = self.operations.group_id
+ # if group:
+ # # Cari BU/PUT dalam group yang sama
+ # bu_put = self.env['stock.picking'].search([
+ # ('group_id', '=', group.id),
+ # ('picking_type_id.id', '=', 75), # 75 = ID BU/PUT
+ # ('state', '=', 'done')
+ # ], limit=1)
+ #
+ # if bu_put:
+ # raise UserError("❌ Tidak bisa retur BU/INPUT karena BU/PUT sudah Done!")
if self.operations.picking_type_id.id == 28 and tipe == 'tukar_guling':
raise UserError("❌ BU/INPUT tidak boleh di retur tukar guling")
@@ -418,7 +418,7 @@ class TukarGulingPO(models.Model):
def action_submit(self):
self.ensure_one()
- # self._check_bill_on_revisi_po()
+ # self._check_bill_on_retur_po()
self._validate_product_lines()
self._check_not_allow_tukar_guling_on_bu_input()
@@ -463,7 +463,7 @@ class TukarGulingPO(models.Model):
def action_approve(self):
self.ensure_one()
self._validate_product_lines()
- # self._check_bill_on_revisi_po()
+ # self._check_bill_on_retur_po()
self._check_not_allow_tukar_guling_on_bu_input()
if not self.operations:
@@ -485,7 +485,7 @@ class TukarGulingPO(models.Model):
elif rec.state == 'approval_finance':
if not rec.env.user.has_group('indoteknik_custom.group_role_fat'):
raise UserError("Hanya Finance yang boleh approve tahap ini.")
- # rec._check_bill_on_revisi_po()
+ # rec._check_bill_on_retur_po()
rec.set_opt()
rec.state = 'approval_logistic'
rec.date_finance = now
@@ -501,7 +501,7 @@ class TukarGulingPO(models.Model):
def update_doc_state(self):
# bu input rev po
- if self.operations.picking_type_id.id == 28 and self.return_type == 'revisi_po':
+ if self.operations.picking_type_id.id == 28 and self.return_type == 'retur_po':
prt = self.env['stock.picking'].search([
('tukar_guling_po_id', '=', self.id),
('state', '=', 'done'),
@@ -510,7 +510,7 @@ class TukarGulingPO(models.Model):
if self.state == 'approved' and prt:
self.state = 'done'
# bu put rev po
- elif self.operations.picking_type_id.id == 75 and self.return_type == 'revisi_po':
+ elif self.operations.picking_type_id.id == 75 and self.return_type == 'retur_po':
total_prt = self.env['stock.picking'].search_count([
('tukar_guling_po_id', '=', self.id),
('picking_type_id.id', '=', 76)
diff --git a/indoteknik_custom/models/unpaid_invoice_view.py b/indoteknik_custom/models/unpaid_invoice_view.py
new file mode 100644
index 00000000..3eb6efc7
--- /dev/null
+++ b/indoteknik_custom/models/unpaid_invoice_view.py
@@ -0,0 +1,55 @@
+from odoo import models, fields
+
+class UnpaidInvoiceView(models.Model):
+ _name = 'unpaid.invoice.view'
+ _description = 'Unpaid Invoices Monitoring'
+ _auto = False
+ _rec_name = 'partner_name'
+ _order = 'partner_name, new_invoice_day_to_due DESC'
+
+ partner_id = fields.Many2one('res.partner', string='Partner')
+ partner_name = fields.Char(string='Partner Name')
+ # email = fields.Char()
+ # phone = fields.Char()
+ invoice_id = fields.Many2one('account.move', string='Invoice')
+ invoice_number = fields.Char(string='Invoice Number')
+ invoice_date = fields.Date()
+ invoice_date_due = fields.Date(string='Due Date')
+ date_terima_tukar_faktur = fields.Date(string='Terima Faktur')
+ currency_id = fields.Many2one('res.currency', string='Currency')
+ amount_total = fields.Monetary(string='Total Amount', currency_field='currency_id')
+ amount_residual = fields.Monetary(string='Sisa Amount', currency_field='currency_id')
+ payment_state = fields.Selection([
+ ('not_paid', 'Not Paid'),
+ ('in_payment', 'In Payment'),
+ ('paid', 'Paid'),
+ ('partial', 'Partially Paid'),
+ ('reversed', 'Reversed')], string='Payment State')
+ payment_term_id = fields.Many2one('account.payment.term', string='Payment Term')
+ invoice_day_to_due = fields.Integer(string="Day to Due")
+ new_invoice_day_to_due = fields.Integer(string="New Day Due")
+
+ ref = fields.Char(string='Reference')
+ invoice_user_id = fields.Many2one('res.users', string='Salesperson')
+ date_kirim_tukar_faktur = fields.Date(string='Kirim Faktur')
+ sale_id = fields.Many2one('sale.order', string='Sale Order')
+
+ payment_difficulty = fields.Selection([
+ ('bermasalah', 'Bermasalah'),
+ ('sulit', 'Sulit'),
+ ('agak_sulit', 'Agak Sulit'),
+ ('normal', 'Normal'),
+ ], string="Payment Difficulty")
+
+ def action_create_surat_piutang(self):
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'surat.piutang',
+ 'view_mode': 'form',
+ 'target': 'current',
+ 'context': {
+ 'default_partner_id': self.partner_id.id,
+ 'default_selected_invoice_id': self.invoice_id.id,
+ }
+ }
diff --git a/indoteknik_custom/report/purchase_report.xml b/indoteknik_custom/report/purchase_report.xml
new file mode 100644
index 00000000..23fa4d52
--- /dev/null
+++ b/indoteknik_custom/report/purchase_report.xml
@@ -0,0 +1,178 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+ <!-- Report Action -->
+ <record id="action_report_purchaseorder_website" model="ir.actions.report">
+ <field name="name">Purchase Order (Website)</field>
+ <field name="model">purchase.order</field>
+ <field name="report_type">qweb-pdf</field>
+ <field name="report_name">indoteknik_custom.report_purchaseorder_website</field>
+ <field name="report_file">indoteknik_custom.report_purchaseorder_website</field>
+ <field name="print_report_name">
+ ('%s - %s' % (object.name, object.partner_id.name))
+ </field>
+ <field name="binding_model_id" ref="purchase.model_purchase_order"/>
+ <field name="binding_type">report</field>
+ </record>
+ </data>
+
+ <!-- Wrapper Template -->
+ <template id="report_purchaseorder_website">
+ <t t-call="web.html_container">
+ <t t-foreach="docs" t-as="doc">
+ <t t-call="indoteknik_custom.report_purchaseorder_website_document" t-lang="doc.partner_id.lang"/>
+ </t>
+ </t>
+ </template>
+
+ <template id="report_purchaseorder_website_document">
+ <t t-call="web.html_container">
+ <t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)" />
+
+ <!-- HEADER -->
+ <div class="header">
+ <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2498521"
+ style="width:100%; display:block;"/>
+ </div>
+
+ <!-- PAGE CONTENT -->
+ <div class="article" style="margin: 0 1.5cm 0 1.5cm; font-family:Arial, sans-serif; font-size:14px; color:#333;">
+
+ <!-- TITLE -->
+ <h2 style="text-align:center; margin:8px 0 0 0; color:#d32f2f; font-weight:800; letter-spacing:1px;">
+ PURCHASE ORDER
+ </h2>
+ <h4 style="text-align:center; margin:4px 0 20px 0; font-weight:normal; color:#555;">
+ No. <span t-field="doc.name"/>
+ </h4>
+
+ <!-- TOP INFO -->
+ <table style="width:100%; margin-bottom:20px; border-radius:8px; box-shadow:0 1px 4px rgba(0,0,0,0.1); overflow:hidden; border:1px solid #ddd;">
+ <tr style="background:#fafafa;">
+ <td style="padding:10px 12px;"><strong>Term Of Payment:</strong> <span t-field="doc.payment_term_id.name"/></td>
+ <td style="padding:10px 12px;"><strong>Order Date:</strong> <span t-field="doc.date_order" t-options='{"widget": "date"}'/></td>
+ <td style="padding:10px 12px;"><strong>Responsible:</strong> <span t-field="doc.user_id"/></td>
+ </tr>
+ </table>
+
+ <!-- VENDOR & DELIVERY -->
+ <table style="width:100%; margin-bottom:24px; border-spacing:16px 0;">
+ <tr>
+ <td style="width:50%; border:1px solid #ccc; border-radius:8px; padding:10px; background:#fcfcfc; vertical-align:top;">
+ <strong style="color:#d32f2f;">Alamat Pengiriman</strong><br/>
+ PT. Indoteknik Dotcom Gemilang<br/>
+ Jl. Bandengan Utara Komp A 8 B
+ RT. Penjaringan, Kec. Penjaringan, Jakarta
+ (BELAKANG INDOMARET)<br/>
+ Daerah Khusus Ibukota Jakarta 14440
+ </td>
+ <td style="width:50%; border:1px solid #ccc; border-radius:8px; padding:10px; background:#fcfcfc; vertical-align:top;">
+ <strong style="color:#d32f2f;">Nama Vendor</strong><br/>
+ <span t-field="doc.partner_id.name"/><br/>
+ <span t-field="doc.partner_id.street"/><br/>
+ <span t-field="doc.partner_id.city"/> - <span t-field="doc.partner_id.zip"/>
+ </td>
+ </tr>
+ </table>
+
+ <!-- ORDER LINES -->
+ <table style="border-collapse:collapse; width:100%; margin-top:16px; font-size:14px;">
+ <tbody>
+ <!-- HEADER -->
+ <tr style="background:#e53935; color:white;">
+ <th style="border:1px solid #ccc; padding:8px; text-align:left;">No. &amp; Description</th>
+ <th style="border:1px solid #ccc; padding:8px; text-align:left;">Image</th>
+ <th style="border:1px solid #ccc; padding:8px; text-align:center;">Quantity</th>
+ <th style="border:1px solid #ccc; padding:8px; text-align:center;">Unit Price</th>
+ <th style="border:1px solid #ccc; padding:8px; text-align:center;">Taxes</th>
+ <th style="border:1px solid #ccc; padding:8px; text-align:center;">Subtotal</th>
+ </tr>
+
+ <!-- ISI ORDER LINE -->
+ <t t-foreach="doc.order_line" t-as="line" t-index="line_index">
+ <tr t-attf-style="background-color: #{ '#fafafa' if line_index % 2 == 0 else 'white' };">
+
+ <!-- NO & DESCRIPTION + IMAGE -->
+ <td style="border:1px solid #ccc; padding: 6px; display:flex; align-items:center; gap:10px;">
+ <!-- TEKS -->
+ <div style="display:flex; flex-direction:column; flex:1;">
+ <span style="font-weight:bold; margin-bottom:2px;">
+ <t t-esc="line_index + 1"/>. <t t-esc="line.product_id.display_name"/>
+ </span>
+ </div>
+ </td>
+
+ <td style="border:1px solid #ccc; padding:6px; text-align:center;">
+ <t t-if="line.image_small">
+ <img t-att-src="image_data_uri(line.image_small)"
+ style="width:100px; height:100px; object-fit:contain; border:1px solid #ddd; border-radius:6px; background:#fff;"/>
+ </t>
+ </td>
+ <!-- QTY -->
+ <td style="border:1px solid #ccc; padding:6px; text-align:center;">
+ <span t-field="line.product_qty"/> <span t-field="line.product_uom"/>
+ </td>
+
+ <!-- UNIT PRICE -->
+ <td style="border:1px solid #ccc; padding:6px; text-align:center;">
+ <span t-field="line.price_unit"/>
+ </td>
+
+ <!-- TAXES -->
+ <td style="border:1px solid #ccc; padding:6px; text-align:center;">
+ <span t-esc="', '.join(map(lambda x: (x.description or x.name), line.taxes_id))"/>
+ </td>
+
+ <!-- SUBTOTAL -->
+ <td style="border:1px solid #ccc; padding:6px; text-align:right; font-weight:bold;">
+ <span t-field="line.price_subtotal"/>
+ </td>
+ </tr>
+
+ <!-- WEBSITE DESCRIPTION -->
+ <t t-if="line.show_description">
+ <tr t-attf-style="background-color: #{ '#fef5f5' if line_index % 2 == 0 else '#fffafa' }; ">
+ <td colspan="6" style="padding: 10px 14px; font-size:10px; line-height:1.3; font-style:italic; color:#555; border-left:1px solid #ccc; border-right:1px solid #ccc; border-bottom:1px solid #ccc;">
+ <div t-raw="line.product_id.website_description"/>
+ </td>
+ </tr>
+ </t>
+ </t>
+ </tbody>
+ </table>
+
+
+ <!-- TOTALS -->
+ <table style="margin-top:24px; margin-left:auto; width:40%; font-size:14px; border:1px solid #ddd; border-radius:6px; box-shadow:0 1px 3px rgba(0,0,0,0.08);">
+ <tr style="background:#fafafa;">
+ <td style="padding:8px;"><strong>Subtotal</strong></td>
+ <td style="text-align:right; padding:8px;"><span t-field="doc.amount_untaxed"/></td>
+ </tr>
+ <tr>
+ <td style="padding:8px;">Taxes</td>
+ <td style="text-align:right; padding:8px;"><span t-field="doc.amount_tax"/></td>
+ </tr>
+ <tr style="background:#fbe9e7; font-weight:bold; color:#d32f2f;">
+ <td style="padding:8px;">Total</td>
+ <td style="text-align:right; padding:8px;"><span t-field="doc.amount_total"/></td>
+ </tr>
+ </table>
+
+ <!-- NOTES -->
+ <div style="margin-top:24px; padding:12px; border-top:1px solid #ddd; font-style:italic; color:#555;">
+ <p t-field="doc.notes"/>
+ </div>
+ </div>
+
+ <!-- STATIC FOOTER -->
+ <div class="footer">
+ <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2859765"
+ style="width:100%; display:block;"/>
+ </div>
+
+ </t>
+ </template>
+
+
+
+</odoo>
diff --git a/indoteknik_custom/report/report_surat_piutang copy.xml b/indoteknik_custom/report/report_surat_piutang copy.xml
new file mode 100644
index 00000000..cb5762f3
--- /dev/null
+++ b/indoteknik_custom/report/report_surat_piutang copy.xml
@@ -0,0 +1,149 @@
+<?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
new file mode 100644
index 00000000..62db7982
--- /dev/null
+++ b/indoteknik_custom/report/report_surat_piutang.xml
@@ -0,0 +1,241 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+
+ <!-- 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</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>
+ </record>
+
+ <template id="external_layout_surat_piutang">
+ <t t-call="web.html_container">
+
+ <!-- Header -->
+ <div class="header">
+ <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2498521"
+ style="width:100%; display: block;"/>
+ </div>
+
+ <!-- Body -->
+ <div class="article" style="margin: 0 1.5cm 0 1.5cm; ">
+ <t t-raw="0"/>
+ </div>
+
+ <!-- Footer -->
+ <div class="footer">
+ <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2859765"
+ style="width:100%; display: block;"/>
+ </div>
+ </t>
+ </template>
+
+
+
+ <!-- Wrapper Template -->
+ <template id="report_surat_piutang">
+ <t t-call="web.html_container">
+ <t t-foreach="docs" t-as="doc">
+ <t t-call="indoteknik_custom.report_surat_piutang_document"
+ t-lang="doc.partner_id.lang"/>
+ </t>
+ </t>
+ </template>
+
+ <!-- Document Template -->
+ <template id="report_surat_piutang_document">
+ <t t-call="indoteknik_custom.external_layout_surat_piutang">
+ <t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
+ <div class="page">
+
+ <!-- Ref & Date -->
+ <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/>
+ <!-- Tujuan -->
+ <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/>
+
+ <!-- UP & Perihal -->
+ <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/>
+
+ <!-- Isi Surat -->
+ <p><strong>Dengan Hormat,</strong></p>
+ <p>Yang bertanda tangan di bawah ini:</p>
+
+ <p class="text-justify">
+ <strong>PT. Indoteknik Dotcom Gemilang</strong>, suatu perseroan terbatas yang didirikan berdasarkan hukum Negara Republik
+ Indonesia, yang beralamat di Jalan Bandengan Utara 85A No. 8-9, RT.003/RW.016, Penjaringan, Penjaringan, Jakarta
+ Utara, DKI Jakarta 14440, Republik Indonesia, dalam hal ini diwakili secara sah oleh Akbar Prabawa selaku General
+ Manager, dengan ini menyampaikan sebagai berikut:
+ </p>
+
+ <p class="text-justify">
+ Kami mengucapkan terima kasih atas kerja sama yang telah terjalin dengan baik selama ini antara perusahaan kami
+ dengan <strong><t t-esc="doc.partner_id.name or '-'"/></strong>.
+ </p>
+
+ <p class="text-justify">
+ Namun, bersama surat ini kami ingin mengingatkan bahwa hingga tanggal surat ini dibuat, masih terdapat tagihan yang
+ belum diselesaikan oleh pihak <strong><t t-esc="doc.partner_id.name or '-'"/></strong> kepada kami periode bulan
+ <t t-esc="doc.periode_invoices_terpilih or '-'"/>, bahwa berdasarkan data korespondensi dan laporan keuangan yang kami kelola,
+ <t t-esc="doc.partner_id.name or '-'"/> <b>(“Saudara”)</b> masih mempunyai tagihan yang telah jatuh tempo dan belum dibayarkan sejumlah
+ <t t-esc="doc.grand_total_text or '-'"/> <b>(“Tagihan”)</b>.
+ </p>
+
+ <p>Berikut kami lampirkan Rincian Tagihan yang telah Jatuh Tempo:</p>
+
+ <!-- Tabel Invoice -->
+ <table class="table table-sm o_main_table"
+ style="font-size:13px; border:1px solid #000; border-collapse: collapse; width:100%; table-layout: fixed;">
+
+ <thead style="background:#f5f5f5;">
+ <tr>
+ <th style="border:1px solid #000; padding:4px; width:5%; font-weight: bold;" class="text-center">No.</th>
+ <th style="border:1px solid #000; padding:4px; width:16%; font-weight: bold;">Invoice Number</th>
+ <th style="border:1px solid #000; padding:4px; width:10%; font-weight: bold;">Invoice Date</th>
+ <th style="border:1px solid #000; padding:4px; width:10%; font-weight: bold;">Due Date</th>
+ <th style="border:1px solid #000; padding:4px; width:6%; font-weight: bold;" class="text-center">Day to Due</th>
+ <th style="border:1px solid #000; padding:4px; width:16%; font-weight: bold;">Reference</th>
+ <th style="border:1px solid #000; padding:4px; width:17%; font-weight: bold;" class="text-right">Amount Due</th>
+ <th style="border:1px solid #000; padding:4px; width:11%; font-weight: bold;">Payment Terms</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ <tr t-foreach="doc.line_ids.filtered(lambda l: l.selected)" t-as="line">
+
+ <!-- Nomor Urut -->
+ <td style="border:1px solid #000; padding:4px; text-align:center;">
+ <t t-esc="line.sort or '-'"/>
+ </td>
+
+ <!-- Invoice Number -->
+ <td style="border:1px solid #000; padding:4px; word-wrap: break-word;">
+ <t t-esc="line.invoice_number or '-'"/>
+ </td>
+
+ <!-- Invoice Date -->
+ <td style="border:1px solid #000; padding:4px;">
+ <t t-esc="line.invoice_date and line.invoice_date.strftime('%d-%m-%Y') or '-'"/>
+ </td>
+
+ <!-- Due Date -->
+ <td style="border:1px solid #000; padding:4px;">
+ <t t-esc="line.invoice_date_due and line.invoice_date_due.strftime('%d-%m-%Y') or '-'"/>
+ </td>
+
+ <!-- Day to Due -->
+ <td style="border:1px solid #000; padding:4px; text-align:center;">
+ <t t-esc="line.new_invoice_day_to_due or '-'"/>
+ </td>
+
+ <!-- Reference -->
+ <td style="border:1px solid #000; padding:4px; word-wrap: break-word;">
+ <t t-esc="line.ref or '-'"/>
+ </td>
+
+ <!-- Amount Due -->
+ <td style="border:1px solid #000; padding:4px; text-align:right;">
+ Rp. <t t-esc="'{:,.0f}'.format(line.amount_residual).replace(',', '.')"/>
+ </td>
+
+ <!-- Payment Terms -->
+ <td style="border:1px solid #000; padding:4px; word-wrap: break-word;">
+ <t t-esc="line.payment_term_id.name or '-'"/>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="5" class="text-left" style="border:1px solid #000; padding:4px; word-wrap: break-word; white-space: normal; font-weight: bold;">
+ GRAND TOTAL INVOICE YANG BELUM DIBAYAR DAN TELAH JATUH TEMPO
+ </td>
+ <td colspan="3" class="text-right" style="border:1px solid #000; padding:4px; word-wrap: break-word; white-space: normal; font-weight: bold;">
+ Rp. <t t-esc="'{:,.0f}'.format(doc.grand_total).replace(',', '.')"/>
+ (<t t-esc="doc.grand_total_text or '-'"/>)
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+
+ <!-- Penutup -->
+ <p class="text-justify">
+ Kami belum menerima konfirmasi pelunasan ataupun pembayaran sebagian dari total kewajiban tersebut dan kami
+ memahami bahwa setiap perusahaan bisa saja menghadapi kendala operasional maupun keuangan, dan kami sangat
+ terbuka untuk berdiskusi serta mencari solusi terbaik bersama agar kerja sama kita tetap berjalan baik ke depannya.
+ </p>
+
+ <p class="text-justify">
+ Oleh karena itu, kami mohon perhatian dan itikad baik dari pihak <strong><t t-esc="doc.partner_id.name or '-'"/></strong>
+ untuk segera melakukan pelunasan atau memberikan informasi terkait rencana pembayaran paling lambat dalam waktu 7 (tujuh) hari kerja sejak surat ini diterima.
+ </p>
+
+ <p class="text-justify">
+ Jika dalam waktu yang telah ditentukan belum ada penyelesaian atau tanggapan, kami akan mempertimbangkan untuk
+ melanjutkan proses sesuai ketentuan yang berlaku.
+ </p>
+
+ <p class="text-justify">
+ Demikian kami sampaikan. 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;">
+ <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;"/>
+ </t>
+ <t t-else="">
+ <div style="height:100px;"></div>
+ </t>
+ </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>: Akbar Prabawa</td>
+ </tr>
+ <tr style="font-weight: bold;">
+ <td style="padding-right:50px; white-space: nowrap;">Jabatan</td>
+ <td>: General Manager</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </t>
+ </template>
+
+ </data>
+</odoo>
diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv
index 258dcda1..12cc3098 100755
--- a/indoteknik_custom/security/ir.model.access.csv
+++ b/indoteknik_custom/security/ir.model.access.csv
@@ -183,6 +183,8 @@ access_production_purchase_match,access.production.purchase.match,model_producti
access_image_carousel,access.image.carousel,model_image_carousel,,1,1,1,1
access_v_sale_notin_matchpo,access.v.sale.notin.matchpo,model_v_sale_notin_matchpo,,1,1,1,1
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_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
access_down_payment,access.down.payment,model_down_payment,,1,1,1,1
@@ -199,7 +201,10 @@ access_tukar_guling_line_all_users,tukar.guling.line.all.users,model_tukar_gulin
access_tukar_guling_po_all_users,tukar.guling.po.all.users,model_tukar_guling_po,base.group_user,1,1,1,1
access_tukar_guling_line_po_all_users,tukar.guling.line.po.all.users,model_tukar_guling_line_po,base.group_user,1,1,1,1
access_tukar_guling_mapping_koli_all_users,tukar.guling.mapping.koli.all.users,model_tukar_guling_mapping_koli,base.group_user,1,1,1,1
-access_purchase_order_update_date_wizard,access.purchase.order.update.date.wizard,model_purchase_order_update_date_wizard,base.group_user,1,1,1,1
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 \ No newline at end of file
+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_sj_tele,access.sj.tele,model_sj_tele,base.group_system,1,1,1,1
diff --git a/indoteknik_custom/views/account_move.xml b/indoteknik_custom/views/account_move.xml
index 1b477c6d..ba86277a 100644
--- a/indoteknik_custom/views/account_move.xml
+++ b/indoteknik_custom/views/account_move.xml
@@ -39,6 +39,7 @@
</field>
<field name="ref" position="after">
<field name="sale_id" readonly="1" attrs="{'invisible': ['|', ('move_type', '!=', 'entry'), ('has_refund_so', '=', True)]}"/>
+ <field name="refund_id" readonly="1" attrs="{'invisible': ['|', ('move_type', '!=', 'entry'), ('has_refund_so', '=', False)]}"/>
<field name="refund_so_links" readonly="1" widget="html" attrs="{'invisible': ['|', ('move_type', '!=', 'entry'), ('has_refund_so', '=', False)]}"/>
<field name="has_refund_so" invisible="1"/>
</field>
@@ -58,6 +59,10 @@
<attribute name="widget">pdf_viewer</attribute>
</field>
<field name="invoice_user_id" position="after">
+ <field name="payment_difficulty" widget="badge"
+ decoration-info="payment_difficulty == 'normal'"
+ decoration-warning="payment_difficulty in ('agak_sulit', 'sulit')"
+ decoration-danger="payment_difficulty == 'bermasalah'"/>
<field name="invoice_origin"/>
<field name="date_kirim_tukar_faktur"/>
<field name="shipper_faktur_id"/>
@@ -121,6 +126,11 @@
decoration-danger="mark_upload_efaktur == 'belum_upload'"
decoration-success="mark_upload_efaktur == 'sudah_upload'" />
<field name="due_extension" optional="hide"/>
+ <field name="payment_difficulty" widget="badge"
+ decoration-info="payment_difficulty == 'normal'"
+ decoration-warning="payment_difficulty in ('agak_sulit', 'sulit')"
+ decoration-danger="payment_difficulty == 'bermasalah'"
+ optional="hide"/>
</field>
<field name="payment_state" position="after">
<field name="invoice_payment_term_id" optional="hide"/>
diff --git a/indoteknik_custom/views/account_move_line.xml b/indoteknik_custom/views/account_move_line.xml
index 02b936f1..017a9eda 100644
--- a/indoteknik_custom/views/account_move_line.xml
+++ b/indoteknik_custom/views/account_move_line.xml
@@ -6,6 +6,9 @@
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
+ <xpath expr="//page[@id='aml_tab']/field[@name='line_ids']" position="attributes">
+ <attribute name="attrs">{'readonly': [('refund_id','!=',False)]}</attribute>
+ </xpath>
<xpath expr="//page[@id='aml_tab']/field[@name='line_ids']/tree/field[@name='currency_id']" position="before">
<field name="is_required" invisible="1"/>
</xpath>
diff --git a/indoteknik_custom/views/account_move_views.xml b/indoteknik_custom/views/account_move_views.xml
index 7c1f8913..08b93f1f 100644
--- a/indoteknik_custom/views/account_move_views.xml
+++ b/indoteknik_custom/views/account_move_views.xml
@@ -66,7 +66,8 @@
<group>
<group>
<field name="partner_id" readonly="1"/>
- <field name="day_extension" attrs="{'readonly': [('is_approve', '=', True)]}"/>
+ <field name="payment_term"/>
+ <field name="day_extension" attrs="{'readonly': [('is_approve', '=', True)]}"/>
<field name="order_id" readonly="1"/>
<field name="amount_total" readonly="1"/>
</group>
diff --git a/indoteknik_custom/views/approval_payment_term.xml b/indoteknik_custom/views/approval_payment_term.xml
index 5c130f3f..b0b99689 100644
--- a/indoteknik_custom/views/approval_payment_term.xml
+++ b/indoteknik_custom/views/approval_payment_term.xml
@@ -7,7 +7,7 @@
<tree default_order="create_date desc">
<field name="number"/>
<field name="partner_id"/>
- <field name="parent_id"/>
+ <field name="parent_id" optional="hide"/>
<field name="property_payment_term_id"/>
<field name="create_date" optional="hide"/>
<field name="approve_date" optional="hide"/>
diff --git a/indoteknik_custom/views/dunning_run.xml b/indoteknik_custom/views/dunning_run.xml
index f624c42e..51377f78 100644
--- a/indoteknik_custom/views/dunning_run.xml
+++ b/indoteknik_custom/views/dunning_run.xml
@@ -25,13 +25,14 @@
<field name="arch" type="xml">
<tree>
<field name="partner_id"/>
+ <field name="reference"/>
<field name="invoice_id"/>
<field name="date_invoice"/>
- <field name="efaktur_id"/>
- <field name="reference"/>
+ <field name="efaktur_id" optional="hide"/>
<field name="total_amt" sum="Grand Total Amount"/>
<field name="open_amt"/>
<field name="due_date"/>
+ <field name="payment_term"/>
</tree>
</field>
</record>
diff --git a/indoteknik_custom/views/ir_sequence.xml b/indoteknik_custom/views/ir_sequence.xml
index 07888036..9ab4dd22 100644
--- a/indoteknik_custom/views/ir_sequence.xml
+++ b/indoteknik_custom/views/ir_sequence.xml
@@ -238,5 +238,14 @@
<field name="number_increment">1</field>
<field name="active">True</field>
</record>
+
+ <record id="seq_surat_piutang" model="ir.sequence">
+ <field name="name">Surat Piutang</field>
+ <field name="code">surat.piutang</field>
+ <field name="prefix"></field> <!-- format manual di model -->
+ <field name="padding">3</field>
+ <field name="number_next">1</field>
+ <field name="number_increment">1</field>
+ </record>
</data>
</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/letter_receivable.xml b/indoteknik_custom/views/letter_receivable.xml
new file mode 100644
index 00000000..98ea7768
--- /dev/null
+++ b/indoteknik_custom/views/letter_receivable.xml
@@ -0,0 +1,190 @@
+<odoo>
+ <!-- Tree View -->
+ <record id="view_surat_piutang_tree" model="ir.ui.view">
+ <field name="name">surat.piutang.tree</field>
+ <field name="model">surat.piutang</field>
+ <field name="arch" type="xml">
+ <tree string="Surat Piutang">
+ <field name="name"/>
+ <field name="partner_id"/>
+ <field name="perihal"/>
+ <field name="state" widget="badge"
+ decoration-danger="state == 'draft'"
+ decoration-warning="state in ('waiting_approval_sales', 'waiting_approval_pimpinan')"
+ decoration-success="state == 'sent'"/>
+ <field name="send_date"/>
+ <!-- <field name="line_ids" widget="one2many_list"/> -->
+ </tree>
+ </field>
+ </record>
+
+ <!-- Form View -->
+ <record id="view_surat_piutang_form" model="ir.ui.view">
+ <field name="name">surat.piutang.form</field>
+ <field name="model">surat.piutang</field>
+ <field name="arch" type="xml">
+ <form string="Surat Piutang">
+ <header>
+ <field name="state" widget="statusbar" statusbar_visible="draft,waiting_approval_sales,waiting_approval_pimpinan,sent"/>
+ <button name="action_approve"
+ type="object"
+ string="Approve"
+ class="btn-primary"
+ attrs="{'invisible':[('state', '=', 'sent')]}"/>
+ <button name="action_create_next_letter"
+ string="Buat Surat Lanjutan"
+ type="object"
+ class="btn-primary"
+ attrs="{'invisible': ['|', ('state', '!=', 'sent'), ('perihal', '=', 'sp3')]}"/>
+ <!-- <button name="action_send_letter" type="object" string="Email Send" class="btn-primary"/> -->
+ </header>
+ <div class="alert alert-info"
+ role="alert"
+ style="height: 40px; margin-bottom:0px;"
+ attrs="{'invisible':[('state', '!=', 'draft')]}">
+ Selamat Datang di form Pengajuan Surat Piutang, Pastikan data sudah benar sebelum mengajukan approval.
+ </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>
+ <div class="alert alert-info"
+ role="alert"
+ style="height: 40px; margin-bottom:0px;"
+ attrs="{'invisible':[('state', '!=', 'waiting_approval_sales')]}">
+ <strong>Info!</strong> Surat peringatan piutang ini sedang menunggu persetujuan dari <b>Sales Manager</b>.
+ Silakan hubungi Sales Manager terkait untuk melakukan approval agar proses dapat dilanjutkan ke tahap berikutnya.
+ </div>
+ <div class="alert alert-info"
+ role="alert"
+ style="margin-bottom:0px;"
+ attrs="{'invisible': ['|', ('perihal', '=', 'penagihan'), ('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>
+ <div class="alert alert-success"
+ role="alert"
+ style="height: 40px; margin-bottom:0px;"
+ attrs="{'invisible': ['|', ('perihal', '!=', 'sp3'), ('state', 'not in', ['draft', 'sent'])]}">
+ Surat Piutang berhasil terkirim dan silahkan klik tombol 'Buat Surat Lanjutan' untuk membuat surat piutang lanjutan.
+ </div>
+ <sheet>
+ <div class="oe_title">
+ <h1>
+ <field name="name" readonly="1"/>
+ </h1>
+ </div>
+ <group colspan="2">
+ <group>
+ <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="partner_id" options="{'no_create': True}" attrs="{'readonly':[('state','=','sent')]}"/>
+ </group>
+ <group>
+ <field name="payment_difficulty"/>
+ <field name="sales_person_id"/>
+ <field name="send_date" readonly="1"/>
+ </group>
+ </group>
+ <!-- <group>
+ <button name="action_refresh_lines"
+ string="Refresh Invoices"
+ type="object"
+ class="btn-primary"
+ help="Refresh Invoices agar data tetap update"/>
+ </group> -->
+ <notebook>
+ <page string="Invoice Lines">
+ <div class="alert alert-info"
+ role="alert"
+ style="height: 40px; margin-bottom:0px;">
+ <strong>Info!</strong> Hanya invoice yang dipilih (tercentang) akan disertakan dalam dokumen surat piutang.
+ </div>
+ <!-- Flex container -->
+ <div style="display:flex; justify-content:space-between; align-items:center;">
+ <div>
+ <div>
+ <strong>Grand Total Invoice Terpilih:<br/>Rp.
+ <field name="grand_total"/> (
+ <field name="grand_total_text"/>
+ )
+ </strong>
+ </div>
+ <div>
+ <strong>Periode Invoices Terpilih:
+ <field name="periode_invoices_terpilih"/>
+ </strong>
+ </div>
+ </div>
+ <div>
+ <button name="action_refresh_lines"
+ string="Refresh Invoices"
+ type="object"
+ class="btn-primary"
+ style="margin-left:10px;"
+ help="Refresh Invoices agar data tetap update"/>
+ </div>
+ </div>
+ <field name="line_ids" attrs="{'readonly': [('state', '=', 'sent')]}">
+ <tree editable="bottom" create="false" delete="false">
+ <field name="selected"/>
+ <field name="invoice_id" readonly="1" optional="hide" force_save="1"/>
+ <field name="invoice_number" readonly="1" force_save="1"/>
+ <field name="ref" readonly="1" force_save="1"/>
+ <field name="invoice_date" readonly="1" force_save="1"/>
+ <field name="invoice_date_due" readonly="1" force_save="1"/>
+ <field name="invoice_day_to_due" readonly="1" force_save="1"/>
+ <field name="new_invoice_day_to_due" readonly="1" force_save="1"/>
+ <field name="amount_residual" readonly="1" force_save="1" sum="Grand Total"/>
+ <field name="currency_id" readonly="1" optional="hide" force_save="1"/>
+ <field name="payment_term_id" readonly="1" force_save="1"/>
+ <field name="date_kirim_tukar_faktur" readonly="1" optional="hide" force_save="1"/>
+ <field name="date_terima_tukar_faktur" readonly="1" optional="hide" force_save="1"/>
+ <field name="invoice_user_id" readonly="1" optional="hide" force_save="1"/>
+ <field name="sale_id" readonly="1" optional="hide" force_save="1"/>
+ </tree>
+ </field>
+ </page>
+ </notebook>
+ <div style="font-size:13px; color:#444; line-height:1.5;">
+ Surat piutang akan diterbitkan berdasarkan lama keterlambatan pembayaran.<br/>
+ Pilih invoice yang sesuai dengan kriteria berikut:
+ <ul style="margin:4px 0 0 18px;">
+ <li>Keterlambatan ≥ 45 hari → <em>Surat Resmi Penagihan (tanpa ttd digital &amp; cap stempel pimpinan)</em></li>
+ <li>Keterlambatan ≥ 60 hari → <em>Surat Peringatan Piutang ke-1 </em></li>
+ <li>Keterlambatan ≥ 70 hari → <em>Surat Peringatan Piutang ke-2 </em></li>
+ <li>Keterlambatan ≥ 80 hari → <em>Surat Peringatan Piutang ke-3 </em></li>
+ </ul>
+ </div>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
+ </form>
+ </field>
+ </record>
+
+ <!-- Menu -->
+ <record id="menu_surat_piutang_root" model="ir.ui.menu">
+ <field name="name">Surat Piutang</field>
+ <field name="parent_id" ref="account.menu_finance"/>
+ <field name="sequence" eval="10"/>
+ </record>
+
+ <record id="action_surat_piutang" model="ir.actions.act_window">
+ <field name="name">Surat Piutang</field>
+ <field name="res_model">surat.piutang</field>
+ <field name="view_mode">tree,form</field>
+ <field name="view_id" ref="view_surat_piutang_tree"/>
+ </record>
+
+ <menuitem id="menu_surat_piutang"
+ name="Surat Piutang"
+ parent="account.menu_finance_receivables"
+ action="action_surat_piutang"
+ sequence="1"/>
+</odoo>
diff --git a/indoteknik_custom/views/letter_receivable_mail_template.xml b/indoteknik_custom/views/letter_receivable_mail_template.xml
new file mode 100644
index 00000000..fa0fbc86
--- /dev/null
+++ b/indoteknik_custom/views/letter_receivable_mail_template.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data noupdate="0">
+ <record id="letter_receivable_mail_template" model="mail.template">
+ <field name="name">Surat Piutang Invoices</field>
+ <field name="model_id" ref="indoteknik_custom.model_surat_piutang"/>
+ <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;">
+ <div><b>Dengan hormat,</b></div>
+ <br/>
+ <div>Kepada Yth.</div>
+ <div><b>Manajemen ${object.partner_id.name}</b></div>
+ <br/>
+ <div>
+ Melalui email ini, kami ingin mengingatkan kembali terkait kewajiban pembayaran
+ ${object.partner_id.name} atas transaksi dengan rincian sebagai berikut:
+ </div>
+ <br/>
+
+ <table cellpadding="6" cellspacing="0" width="100%"
+ style="border-collapse:collapse; font-size:12px; border:1px solid #ddd;">
+ <thead>
+ <tr style="background-color:#f2f2f2; text-align:left;">
+ <th style="border:1px solid #ddd;">Invoice Number</th>
+ <th style="border:1px solid #ddd;">Customer</th>
+ <th style="border:1px solid #ddd;">Invoice Date</th>
+ <th style="border:1px solid #ddd;">Due Date</th>
+ <th style="border:1px solid #ddd;">Days To Due</th>
+ <th style="border:1px solid #ddd;">Reference</th>
+ <th style="border:1px solid #ddd;">Amount Due Signed</th>
+ <th style="border:1px solid #ddd;">Payment Terms</th>
+ </tr>
+ </thead>
+ <tbody>
+ <!-- baris invoice akan diinject dari Python -->
+ </tbody>
+ </table>
+
+ <p>
+ Hingga saat ini, kami belum menerima pembayaran atas tagihan tersebut.
+ Mohon konfirmasi dan tindak lanjut dari pihak saudara paling lambat pada
+ tanggal <b>${object.seven_days_after_sent_date}</b> (7 hari setelah email ini dikirimkan).
+ </p>
+
+ <p>
+ Sebagai informasi, kami lampirkan <b>${object.perihal}</b> untuk menjadi perhatian.
+ Jika tidak ada tanggapan atau penyelesaian dalam batas waktu tersebut, kami akan
+ melanjutkan dengan pengiriman surat peringatan berikutnya dan mengambil langkah-langkah
+ penyelesaian sesuai ketentuan yang berlaku.
+ </p>
+
+ <p>
+ Demikian kami sampaikan. 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>
diff --git a/indoteknik_custom/views/mail_template_invoice_reminder.xml b/indoteknik_custom/views/mail_template_invoice_reminder.xml
index 13c02a08..3534f7f6 100644
--- a/indoteknik_custom/views/mail_template_invoice_reminder.xml
+++ b/indoteknik_custom/views/mail_template_invoice_reminder.xml
@@ -17,6 +17,7 @@
style="border-collapse:collapse; font-size:12px; border:1px solid #ddd;">
<thead>
<tr style="background-color:#f2f2f2; text-align:left;">
+ <th style="border:1px solid #ddd;">No.</th>
<th style="border:1px solid #ddd;">Customer</th>
<th style="border:1px solid #ddd;">No. PO</th>
<th style="border:1px solid #ddd;">Invoice Number</th>
diff --git a/indoteknik_custom/views/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml
index 821f3295..7feec934 100755
--- a/indoteknik_custom/views/purchase_order.xml
+++ b/indoteknik_custom/views/purchase_order.xml
@@ -99,6 +99,7 @@
<field name="total_delivery_amt" attrs="{'required': [('partner_id', 'in', [9688, 29712])]}"/>
<field name="product_bom_id" attrs="{'invisible': [('product_bom_id', '=', None)]}"/>
<field name="manufacturing_id" attrs="{'invisible': [('product_bom_id', '=', None)]}"/>
+ <field name="show_description" attrs="{'invisible': [('partner_id', 'not in', [5571, 35475, 38198, 88261,37905, 9688, 20625, 1371, 37902, 12119])]}" widget="boolean_toggle"/>
<!-- <field name="move_id" domain="[('move_type','=','entry')]" context="{'form_view_ref': 'account.view_move_form'}" options="{'no_create': True}"/> -->
</field>
<field name="amount_total" position="after">
@@ -112,6 +113,7 @@
</field>
<field name="product_id" position="before">
<field name="line_no" attrs="{'readonly': 1}" optional="hide"/>
+ <field name="show_description" optional="hide" widget="boolean_toggle"/>
</field>
<field name="product_id" position="attributes">
<attribute name="options">{'no_create': True}</attribute>
@@ -181,19 +183,20 @@
</field>
<field name="order_line" position="attributes">
- <attribute name="attrs">{'readonly': ['|', ('state', 'in', ['purchase', 'done', 'cancel']), ('has_active_invoice', '=', True)]}</attribute>
+ <!-- <attribute name="attrs">{'readonly': ['|', ('state', 'in', ['purchase', 'done', 'cancel']), ('has_active_invoice', '=', True)]}</attribute> -->
+ <attribute name="attrs">{'readonly': [('has_active_invoice', '=', True)]}</attribute>
</field>
<xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='price_unit']" position="attributes">
- <attribute name="attrs">{'readonly': [], 'required': True}</attribute>
+ <attribute name="attrs">{'readonly': [('state', 'in', ['purchase', 'done', 'cancel'])], 'required': True}</attribute>
</xpath>
<xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='taxes_id']" position="attributes">
- <attribute name="attrs">{'readonly': []}</attribute>
+ <attribute name="attrs">{'readonly': [('state', 'in', ['purchase', 'done', 'cancel'])]}</attribute>
</xpath>
<xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='product_qty']" position="attributes">
- <attribute name="attrs">{'required': True}</attribute>
+ <attribute name="attrs">{'readonly': [('state', 'in', ['purchase', 'done', 'cancel'])], 'required': True}</attribute>
</xpath>
<xpath expr="//form/sheet/notebook/page[@name='purchase_delivery_invoice']" position="before">
diff --git a/indoteknik_custom/views/refund_sale_order.xml b/indoteknik_custom/views/refund_sale_order.xml
index 0c6cd371..afa7c1cb 100644
--- a/indoteknik_custom/views/refund_sale_order.xml
+++ b/indoteknik_custom/views/refund_sale_order.xml
@@ -92,7 +92,7 @@
</div>
<field name="show_approval_alert" invisible="1"/>
<div class="alert alert-info" role="alert"
- attrs="{'invisible': ['|', ('show_approval_alert', '=', False), ('status', 'in', ['reject', 'refund'])]}">
+ attrs="{'invisible': ['|', ('show_approval_alert', '=', False), '|', ('status', 'in', ['reject', 'refund']), ('refund_type', 'not in', ['retur_full', 'retur_sebagian'])]}">
⚠️ SO sudah melakukan retur barang. Silakan lanjutkan refund.
</div>
</xpath>
@@ -217,6 +217,7 @@
<field name="finance_note"/>
</group>
<group>
+ <field name="biaya_admin"/>
<field name="bukti_refund_type" reqiured="1"/>
<field name="bukti_transfer_refund_pdf" widget="pdf_viewer" attrs="{'invisible': [('bukti_refund_type', '=', 'image')]}"/>
<field name="bukti_transfer_refund_image" widget="image" attrs="{'invisible': [('bukti_refund_type', '=', 'pdf')]}"/>
diff --git a/indoteknik_custom/views/report_logbook_sj.xml b/indoteknik_custom/views/report_logbook_sj.xml
index 94f6c2ab..46260cd5 100644
--- a/indoteknik_custom/views/report_logbook_sj.xml
+++ b/indoteknik_custom/views/report_logbook_sj.xml
@@ -12,15 +12,18 @@
<field name="date_approve"/>
<field name="approve_by_finance"/>
<field name="state"/>
+
</tree>
</field>
- </record>
+ </record>
<record id="report_logbook_sj_line_tree" model="ir.ui.view">
<field name="name">report.logbook.sj.line.tree</field>
<field name="model">report.logbook.sj.line</field>
<field name="arch" type="xml">
<tree editable="bottom">
+<!-- <field name="sequence" widget="handle"/>-->
+ <field name="line_num" string="No" readonly="1"/>
<field name="name"/>
<field name="driver_id"/>
<field name="departure_date"/>
@@ -42,50 +45,47 @@
<field name="arch" type="xml">
<form>
<header>
- <button name="approve"
- string="Validate"
- type="object"
- />
+ <button name="approve" string="Validate" type="object" />
</header>
<sheet string="Report logbook SJ">
- <div class="oe_button_box" name="button_box"/>
+ <div class="oe_button_box" name="button_box"/>
+ <group>
<group>
- <group>
- <field name="name" readonly="1"/>
- <field name="date" readonly="1"/>
- <field name="date_approve" readonly="1"/>
- </group>
- <group>
- <field name="approve_by_finance" readonly="1"/>
- <field name="state" readonly="1"/>
- <field name="created_by" readonly="1"/>
- <field name="approve_by" readonly="1"/>
- <field name="count_line" readonly="1"/>
- </group>
+ <field name="name" readonly="1"/>
+ <field name="date" readonly="1"/>
+ <field name="date_approve" readonly="1"/>
</group>
- <notebook>
- <page string="Line">
- <field name="report_logbook_sj_line"/>
- </page>
- </notebook>
- </sheet>
- <div class="oe_chatter">
- <field name="message_follower_ids" widget="mail_followers"/>
- <field name="message_ids" widget="mail_thread"/>
- </div>
+ <group>
+ <field name="approve_by_finance" readonly="1"/>
+ <field name="state" readonly="1"/>
+ <field name="created_by" readonly="1"/>
+ <field name="approve_by" readonly="1"/>
+ <field name="count_line" readonly="1"/>
+ </group>
+ </group>
+ <notebook>
+ <page string="Line">
+ <field name="report_logbook_sj_line"/>
+ </page>
+ </notebook>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
</form>
</field>
</record>
<record id="report_logbook_sj_view_search" model="ir.ui.view">
- <field name="name">report.logbook.sj.search.view</field> <!-- Made the name more descriptive -->
- <field name="model">report.logbook.sj</field>
- <field name="arch" type="xml">
- <search string="Search Report">
- <field name="sj_number"/>
- </search>
- </field>
- </record>
+ <field name="name">report.logbook.sj.search.view</field> <!-- Made the name more descriptive -->
+ <field name="model">report.logbook.sj</field>
+ <field name="arch" type="xml">
+ <search string="Search Report">
+ <field name="sj_number"/>
+ </search>
+ </field>
+ </record>
<record id="report_logbook_sj_action" model="ir.actions.act_window">
<field name="name">Report Logbook SJ</field>
@@ -94,9 +94,19 @@
<field name="view_mode">tree,form</field>
</record>
- <menuitem id="menu_report_logbook_sj"
- name="Report Logbook SJ"
- action="report_logbook_sj_action"
- parent="account.menu_finance_reports"
- sequence="200"/>
+ <menuitem id="menu_report_logbook_sj" name="Report Logbook SJ" action="report_logbook_sj_action" parent="account.menu_finance_reports" sequence="200"/>
+
+ <data noupdate="1">
+ <record id="cron_daily_logbook_gap_to_telegram" model="ir.cron">
+ <field name="name">Daily Logbook SJ Gap → Telegram</field>
+ <field name="model_id" ref="model_report_logbook_sj"/>
+ <field name="state">code</field>
+ <field name="code">model.cron_daily_logbook_gap_to_telegram()</field>
+ <field name="interval_number">1</field>
+ <field name="interval_type">days</field>
+ <field name="numbercall">-1</field>
+ <field name="user_id" ref="base.user_root"/>
+ <field name="active">False</field>
+ </record>
+ </data>
</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/res_partner.xml b/indoteknik_custom/views/res_partner.xml
index ca1a36de..72751187 100644
--- a/indoteknik_custom/views/res_partner.xml
+++ b/indoteknik_custom/views/res_partner.xml
@@ -21,6 +21,8 @@
<field name="reference_number"/>
</field>
<field name="property_payment_term_id" position="after">
+ <field name="previous_payment_term_id" readonly="1"/>
+ <field name="is_cbd_locked" readonly="1"/>
<field name="user_payment_terms_sales" readonly="1"/>
<field name="date_payment_terms_sales" readonly="1"/>
</field>
@@ -35,9 +37,9 @@
<field name="pareto_status"/>
<field name="digital_invoice_tax"/>
</field>
- <field name="nama_wajib_pajak" position="attributes">
+ <!-- <field name="nama_wajib_pajak" position="attributes">
<attribute name="required">1</attribute>
- </field>
+ </field> -->
<field name="kota_id" position="attributes">
<attribute name="required">0</attribute>
</field>
@@ -47,14 +49,14 @@
<field name="kelurahan_id" position="attributes">
<attribute name="required">0</attribute>
</field>
- <field name="npwp" position="attributes">
+ <!-- <field name="npwp" position="attributes">
<attribute name="required">1</attribute>
</field>
<field name="alamat_lengkap_text" position="attributes">
<attribute name="required">1</attribute>
- </field>
+ </field> -->
<field name="npwp" position="before">
- <field name="customer_type" required="1"/>
+ <field name="customer_type"/>
</field>
<field name="alamat_lengkap_text" position="after">
<field name="nitku" />
@@ -107,7 +109,7 @@
<field name="reminder_invoices"/>
</xpath>
<xpath expr="//field[@name='property_payment_term_id']" position="attributes">
- <attribute name="readonly">0</attribute>
+ <attribute name="readonly">1</attribute>
</xpath>
<xpath expr="//field[@name='property_supplier_payment_term_id']" position="attributes">
<attribute name="readonly">1</attribute>
diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml
index 156c48d7..8d56bbbd 100755
--- a/indoteknik_custom/views/sale_order.xml
+++ b/indoteknik_custom/views/sale_order.xml
@@ -41,6 +41,15 @@
string="Refund"
class="btn-primary" />
</xpath>
+ <xpath expr="//sheet" position="before">
+ <field name="partner_is_cbd_locked" invisible="1"/>
+ <div class="alert alert-danger"
+ role="alert"
+ style="height: 40px; margin-bottom:0px;"
+ attrs="{'invisible':['|', ('partner_is_cbd_locked','=',False), ('state', 'not in', ['draft', 'cancel'])]}">
+ <strong>Warning!</strong> Payment Terms Customer terkunci menjadi <b>Cash Before Delivery (C.B.D.)</b> karena ada invoice telah jatuh tempo <b>30 hari</b>. Silakan ajukan <b>Approval Payment Term</b> untuk membuka kunci.
+ </div>
+ </xpath>
<div class="oe_button_box" name="button_box">
<field name="advance_payment_move_ids" invisible="1"/>
<button name="action_open_advance_payment_moves"
@@ -139,9 +148,9 @@
<field name="pareto_status"/>
</field>
<field name="analytic_account_id" position="after">
- <field name="customer_type" readonly="1"/>
- <field name="npwp" placeholder='99.999.999.9-999.999' readonly="1"/>
- <field name="sppkp" attrs="{'required': [('customer_type', '=', 'pkp')]}" readonly="1"/>
+ <field name="customer_type"/>
+ <field name="npwp" placeholder='99.999.999.9-999.999'/>
+ <field name="sppkp" attrs="{'required': [('customer_type', '=', 'pkp')]}"/>
<field name="email" required="1"/>
<field name="unreserve_id"/>
<field name="due_id" readonly="1"/>
@@ -288,9 +297,9 @@
<field name="note" optional="hide"/>
<field name="note_procurement" optional="hide"/>
<field name="vendor_subtotal" optional="hide"/>
- <field name="unreserved_percent" widget="percentpie" string="Unreserved"/>
- <field name="reserved_percent" widget="percentpie" string="Reserved"/>
- <field name="delivered_percent" widget="percentpie" string="Delivered"/>
+ <field name="unreserved_percent" widget="percentpie" string="Unreserved" optional="hide"/>
+ <field name="reserved_percent" widget="percentpie" string="Reserved" optional="hide"/>
+ <field name="delivered_percent" widget="percentpie" string="Delivered" optional="hide"/>
<field name="weight" optional="hide"/>
<field name="is_has_disc" string="Flash Sale Item?" readonly="1" optional="hide"/>
<field name="amount_voucher_disc" string="Voucher" readonly="1" optional="hide"/>
diff --git a/indoteknik_custom/views/sj_tele.xml b/indoteknik_custom/views/sj_tele.xml
new file mode 100644
index 00000000..cefcc968
--- /dev/null
+++ b/indoteknik_custom/views/sj_tele.xml
@@ -0,0 +1,15 @@
+<odoo>
+ <data noupdate="1">
+ <record id="woi" model="ir.cron">
+ <field name="name">SJ TELE</field>
+ <field name="model_id" ref="model_sj_tele"/>
+ <field name="state">code</field>
+ <field name="code">model.woi()</field>
+ <field name="interval_number">1</field>
+ <field name="interval_type">days</field>
+ <field name="numbercall">-1</field>
+ <field name="user_id" ref="base.user_root"/>
+ <field name="active">False</field>
+ </record>
+ </data>
+</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml
index fc8be790..21762202 100644
--- a/indoteknik_custom/views/stock_picking.xml
+++ b/indoteknik_custom/views/stock_picking.xml
@@ -28,10 +28,14 @@
<!-- <field name="countdown_hours" optional="hide"/>
<field name="countdown_ready_to_ship" /> -->
</field>
+
+ <field name="scheduled_date" position="after">
+ <field name="delivery_date" widget="remaining_days" optional="hide"/>
+ </field>
<field name="partner_id" position="after">
<field name="area_name" optional="hide"/>
<field name="purchase_representative_id"/>
- <field name="status_printed"/>
+ <field name="status_printed" optional="hide"/>
</field>
</field>
</record>
@@ -140,6 +144,9 @@
<field name="scheduled_date" position="attributes">
<attribute name="readonly">1</attribute>
</field>
+ <field name="arrival_time" position="after">
+ <field name="delivery_date" attrs="{'invisible': [('location_id', '!=', 60)]}"/>
+ </field>
<xpath expr="//field[@name='move_ids_without_package']/form/group/field[@name='description_picking']"
position="after">
<field name="product_image" widget="image" string="Product Image"/>
@@ -228,6 +235,7 @@
<field name='sj_return_date'/>
<field name="sj_documentation" widget="image"/>
<field name="paket_documentation" widget="image"/>
+ <field name="dispatch_documentation" widget="image"/>
</group>
<!-- Biteship Group -->
<group attrs="{'invisible': [('select_shipping_option_so', '!=', 'biteship')]}">
diff --git a/indoteknik_custom/views/tukar_guling_po.xml b/indoteknik_custom/views/tukar_guling_po.xml
index 548a209f..4a9ab25d 100644
--- a/indoteknik_custom/views/tukar_guling_po.xml
+++ b/indoteknik_custom/views/tukar_guling_po.xml
@@ -87,7 +87,7 @@
<!-- <field name="srt_num" readonly="1"/>-->
<field name="operations" string="Operations"
attrs="{
- 'required': [('return_type', 'in', ['revisi_po', 'tukar_guling'])]
+ 'required': [('return_type', 'in', ['retur_po', 'tukar_guling'])]
}"/>
<!-- <field name="origin" readonly="1"/>-->
<field name="origin_po" readonly="1"/>
diff --git a/indoteknik_custom/views/unpaid_invoice_view.xml b/indoteknik_custom/views/unpaid_invoice_view.xml
new file mode 100644
index 00000000..ec6c749d
--- /dev/null
+++ b/indoteknik_custom/views/unpaid_invoice_view.xml
@@ -0,0 +1,100 @@
+<odoo>
+ <!-- Tree view -->
+ <record id="view_unpaid_invoice_tree" model="ir.ui.view">
+ <field name="name">unpaid.invoice.view.tree</field>
+ <field name="model">unpaid.invoice.view</field>
+ <field name="arch" type="xml">
+ <tree string="Unpaid Invoices Monitoring" create="false" delete="false" edit="false">
+ <field name="partner_id"/>
+ <field name="invoice_number"/>
+ <field name="ref"/>
+ <field name="invoice_date"/>
+ <field name="date_kirim_tukar_faktur"/>
+ <field name="date_terima_tukar_faktur"/>
+ <field name="payment_term_id"/>
+ <field name="invoice_date_due" widget="badge" decoration-danger="invoice_day_to_due &lt; 0"/>
+ <field name="invoice_day_to_due" readonly="1"/>
+ <field name="new_invoice_day_to_due" readonly="1"/>
+ <field name="amount_total"/>
+ <field name="amount_residual"/>
+ <field name="payment_state" widget="badge"
+ decoration-danger="payment_state == 'not_paid'"
+ decoration-warning="payment_state == 'partial'"/>
+ <field name="invoice_user_id"/>
+ <field name="payment_difficulty" widget="badge"
+ decoration-info="payment_difficulty == 'normal'"
+ decoration-warning="payment_difficulty in ('agak_sulit', 'sulit')"
+ decoration-danger="payment_difficulty == 'bermasalah'"/>
+ </tree>
+ </field>
+ </record>
+
+ <!-- Form view -->
+ <record id="view_unpaid_invoice_form" model="ir.ui.view">
+ <field name="name">unpaid.invoice.view.form</field>
+ <field name="model">unpaid.invoice.view</field>
+ <field name="arch" type="xml">
+ <form string="Unpaid Invoice Detail" create="false" edit="false" delete="false">
+ <sheet>
+ <group>
+ <group>
+ <field name="partner_id"/>
+ <field name="invoice_id"/>
+ <field name="ref"/>
+ <field name="invoice_date"/>
+ <field name="invoice_date_due" widget="badge" decoration-danger="invoice_day_to_due &lt; 0"/>
+ <field name="date_kirim_tukar_faktur"/>
+ <field name="date_terima_tukar_faktur"/>
+ <field name="payment_term_id"/>
+ <button name="action_create_surat_piutang"
+ type="object"
+ string="Create Surat Piutang"
+ class="oe_highlight"/>
+ </group>
+ <group>
+ <field name="sale_id"/>
+ <field name="invoice_user_id"/>
+ <field name="invoice_day_to_due"/>
+ <field name="new_invoice_day_to_due"/>
+ <field name="payment_state" widget="badge"
+ decoration-danger="payment_state == 'not_paid'"
+ decoration-warning="payment_state == 'partial'"/>
+ <field name="amount_total"/>
+ <field name="amount_residual"/>
+ <field name="payment_difficulty" widget="badge"
+ decoration-info="payment_difficulty == 'normal'"
+ decoration-warning="payment_difficulty in ('agak_sulit', 'sulit')"
+ decoration-danger="payment_difficulty == 'bermasalah'"/>
+ </group>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="view_unpaid_invoice_search" model="ir.ui.view">
+ <field name="name">unpaid.invoice.view.search</field>
+ <field name="model">unpaid.invoice.view</field>
+ <field name="arch" type="xml">
+ <search string="Search Unpaid Invoices">
+ <field name="partner_id"/>
+ <field name="invoice_number"/>
+ </search>
+ </field>
+ </record>
+
+ <!-- Action -->
+ <record id="action_unpaid_invoice_view" model="ir.actions.act_window">
+ <field name="name">Unpaid Invoices Monitoring</field>
+ <field name="res_model">unpaid.invoice.view</field>
+ <field name="view_mode">tree,form</field>
+ <field name="view_id" ref="view_unpaid_invoice_tree"/>
+ <field name="search_view_id" ref="view_unpaid_invoice_search"/>
+ </record>
+
+ <!-- Menu -->
+ <menuitem id="menu_unpaid_invoice_root"
+ name="Unpaid Invoices Monitoring"
+ parent="account.menu_finance_receivables"
+ action="action_unpaid_invoice_view"/>
+</odoo>