summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--indoteknik_api/controllers/api_v1/partner.py4
-rw-r--r--indoteknik_api/controllers/api_v1/sale_order.py32
-rw-r--r--indoteknik_api/controllers/api_v1/stock_picking.py17
-rw-r--r--indoteknik_api/models/sale_order.py14
-rwxr-xr-xindoteknik_custom/__manifest__.py1
-rwxr-xr-xindoteknik_custom/models/__init__.py1
-rw-r--r--indoteknik_custom/models/account_move.py304
-rw-r--r--indoteknik_custom/models/account_move_due_extension.py9
-rw-r--r--indoteknik_custom/models/approval_payment_term.py1
-rw-r--r--indoteknik_custom/models/commision.py25
-rw-r--r--indoteknik_custom/models/cust_commision.py1
-rwxr-xr-xindoteknik_custom/models/purchase_order.py6
-rw-r--r--indoteknik_custom/models/res_partner.py5
-rwxr-xr-xindoteknik_custom/models/sale_order.py81
-rw-r--r--indoteknik_custom/models/stock_picking.py31
-rw-r--r--indoteknik_custom/models/tukar_guling.py245
-rw-r--r--indoteknik_custom/models/tukar_guling_po.py117
-rw-r--r--indoteknik_custom/models/update_date_planned_po_wizard.py14
-rw-r--r--indoteknik_custom/report/report_sale_order.xml6
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv3
-rw-r--r--indoteknik_custom/views/account_move.xml2
-rw-r--r--indoteknik_custom/views/account_move_views.xml6
-rw-r--r--indoteknik_custom/views/approval_payment_term.xml3
-rw-r--r--indoteknik_custom/views/mail_template_invoice_reminder.xml22
-rwxr-xr-xindoteknik_custom/views/purchase_order.xml20
-rw-r--r--indoteknik_custom/views/res_partner.xml3
-rw-r--r--indoteknik_custom/views/tukar_guling.xml10
-rw-r--r--indoteknik_custom/views/tukar_guling_po.xml12
-rw-r--r--indoteknik_custom/views/update_date_planned_po_wizard_view.xml25
29 files changed, 775 insertions, 245 deletions
diff --git a/indoteknik_api/controllers/api_v1/partner.py b/indoteknik_api/controllers/api_v1/partner.py
index b8bd21be..14136ca3 100644
--- a/indoteknik_api/controllers/api_v1/partner.py
+++ b/indoteknik_api/controllers/api_v1/partner.py
@@ -150,7 +150,7 @@ class Partner(controller.Controller):
partner_params = self.get_request_params(request_data, {
'tax_name': ['alias:nama_wajib_pajak'],
- 'company_type_id': ['number'],
+ # 'company_type_id': ['number'],
'industry_id': ['number'],
'npwp': [],
'alamat_lengkap_text': [],
@@ -170,7 +170,7 @@ class Partner(controller.Controller):
if 'id_user' in request_data:
user_params = self.get_request_params(request_data, {
'id_user': ['required', 'number'],
- 'company_type_id': ['number'],
+ # 'company_type_id': ['number'],
'industry_id': ['number'],
'tax_name': ['alias:nama_wajib_pajak'],
'npwp': [],
diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py
index fd460ea0..d199cd84 100644
--- a/indoteknik_api/controllers/api_v1/sale_order.py
+++ b/indoteknik_api/controllers/api_v1/sale_order.py
@@ -87,6 +87,8 @@ class SaleOrder(controller.Controller):
'amount_tax': sale.amount_tax,
'amount_total': sale.amount_total,
'expected_ready_to_ship': f"{sale.expected_ready_to_ship.day} {INDONESIAN_MONTHS[sale.expected_ready_to_ship.month]} {sale.expected_ready_to_ship.year}",
+ 'eta_date_start': f"{sale.eta_date_start.day} {INDONESIAN_MONTHS[sale.eta_date_start.month]} {sale.eta_date_start.year}",
+ 'eta_date_end': f"{sale.eta_date.day} {INDONESIAN_MONTHS[sale.eta_date.month]} {sale.eta_date.year}",
'product_name': product_name,
'product_not_in_id': product_not_in_id,
'details': [request.env['sale.order.line'].api_single_response(x, context='with_detail') for x in sale.order_line]
@@ -206,8 +208,8 @@ class SaleOrder(controller.Controller):
]
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.driver_arrival_date]
- done_without_driver = [p for p in done_pickings if not p.driver_arrival_date]
+ done_with_driver = [p for p in done_pickings if p.sj_return_date]
+ done_without_driver = [p for p in done_pickings if not p.sj_return_date]
if status == 'dikemas' and len(done_pickings) == 0:
filtered_orders.append(sale_order)
@@ -257,6 +259,25 @@ class SaleOrder(controller.Controller):
bulan = bulan_id[sale_order.expected_ready_to_ship.month - 1]
tahun = sale_order.expected_ready_to_ship.year
data['expected_ready_to_ship'] = f"{tanggal} {bulan} {tahun}"
+ if sale_order.eta_date_start:
+ bulan_id = [
+ "Januari", "Februari", "Maret", "April", "Mei", "Juni",
+ "Juli", "Agustus", "September", "Oktober", "November", "Desember"
+ ]
+ tanggal = sale_order.eta_date_start.day
+ bulan = bulan_id[sale_order.eta_date_start.month - 1]
+ tahun = sale_order.eta_date_start.year
+ data['eta_date_start'] = f"{tanggal} {bulan} {tahun}"
+
+ if sale_order.eta_date:
+ bulan_id = [
+ "Januari", "Februari", "Maret", "April", "Mei", "Juni",
+ "Juli", "Agustus", "September", "Oktober", "November", "Desember"
+ ]
+ tanggal = sale_order.eta_date.day
+ bulan = bulan_id[sale_order.eta_date.month - 1]
+ tahun = sale_order.eta_date.year
+ data['eta_date_end'] = f"{tanggal} {bulan} {tahun}"
return self.response(data)
@@ -424,8 +445,8 @@ class SaleOrder(controller.Controller):
return self.response('Unauthorized')
sale_order = request.env['sale.order'].sudo().search_read([('id', '=', id)], ['name'])
- pdf, type = request.env['ir.actions.report'].sudo().search([('report_name', '=', 'quotation_so_new')]).render_jasper([id], {})
- # pdf, type = request.env['ir.actions.report'].sudo().search([('report_name', '=', 'indoteknik_custom.report_saleorder_website')])._render_qweb_pdf([id])
+ # pdf, type = request.env['ir.actions.report'].sudo().search([('report_name', '=', 'quotation_so_new')]).render_jasper([id], {})
+ pdf, type = request.env['ir.actions.report'].sudo().search([('report_name', '=', 'indoteknik_custom.report_saleorder_website')])._render_qweb_pdf([id])
if pdf and len(sale_order) > 0:
return rest_api.response_attachment({
@@ -607,9 +628,6 @@ class SaleOrder(controller.Controller):
if is_flash_sale_item:
is_has_disc = True
_logger.info("Item is flash sale product - marked as discounted")
- elif discount_percent > 0 and not global_flash_sale:
- is_has_disc = True
- _logger.info(f"Item has discount {discount_percent}% - marked as discounted")
elif global_flash_sale:
_logger.info("Global flash sale active but item not eligible - not marked as discounted")
diff --git a/indoteknik_api/controllers/api_v1/stock_picking.py b/indoteknik_api/controllers/api_v1/stock_picking.py
index 85b0fbba..762e17c5 100644
--- a/indoteknik_api/controllers/api_v1/stock_picking.py
+++ b/indoteknik_api/controllers/api_v1/stock_picking.py
@@ -125,28 +125,33 @@ class StockPicking(controller.Controller):
@http.route(prefix + 'stock-picking/<scanid>/documentation', auth='public', methods=['PUT', 'OPTIONS'], csrf=False)
@controller.Controller.must_authorized()
def write_partner_stock_picking_documentation(self, **kw):
- scanid = int(kw.get('scanid', 0))
+ scanid = kw.get('scanid', '').strip()
sj_document = kw.get('sj_document', False)
paket_document = kw.get('paket_document', False)
- params = {'sj_documentation': sj_document,
- 'paket_documentation': paket_document,
- 'driver_arrival_date': datetime.utcnow(),
- }
+ params = {
+ 'sj_documentation': sj_document,
+ 'paket_documentation': paket_document,
+ 'driver_arrival_date': datetime.utcnow(),
+ }
- picking_data = request.env['stock.picking'].search([('id', '=', scanid)], limit=1)
+ picking_data = False
+ if scanid.isdigit() and int(scanid) < 2147483647:
+ picking_data = request.env['stock.picking'].search([('id', '=', int(scanid))], limit=1)
if not picking_data:
picking_data = request.env['stock.picking'].search([('picking_code', '=', scanid)], limit=1)
if not picking_data:
return self.response(code=404, description='picking not found')
+
picking_data.write(params)
return self.response({
'name': picking_data.name
})
+
@http.route(prefix + 'webhook/biteship', type='json', auth='public', methods=['POST'], csrf=False)
def update_status_from_biteship(self, **kw):
_logger.info("Biteship Webhook: Request received at controller start (type='json').")
diff --git a/indoteknik_api/models/sale_order.py b/indoteknik_api/models/sale_order.py
index 615dcdcb..9be03927 100644
--- a/indoteknik_api/models/sale_order.py
+++ b/indoteknik_api/models/sale_order.py
@@ -29,6 +29,9 @@ class SaleOrder(models.Model):
'approval_step': APPROVAL_STEP[sale_order.web_approval] if sale_order.web_approval else 0,
'date_order': self.env['rest.api'].datetime_to_str(sale_order.date_order, '%d/%m/%Y %H:%M:%S'),
'payment_type': sale_order.payment_type,
+ 'carrier_id': sale_order.carrier_id.id,
+ 'carrier_name': sale_order.carrier_id.name,
+ 'service_type': sale_order.shipping_option_id.name,
'pickings': []
}
for picking in sale_order.picking_ids:
@@ -47,6 +50,13 @@ class SaleOrder(models.Model):
'eta' : response['eta'],
'id': picking.id,
'name': picking.name,
+ 'products': [{
+ 'id': product.id,
+ 'name': product.name,
+ 'image': self.env['ir.attachment'].api_image('product.template', 'image_128', product.product_tmpl_id.id),
+ 'code': product.default_code or ''
+ } for product in picking.move_line_ids.product_id],
+ 'product_count': len(picking.move_line_ids)
# 'tracking_number': picking.delivery_tracking_no or '',
# 'delivered': picking.waybill_id.delivered or picking.driver_arrival_date != False or picking.sj_return_date != False,
})
@@ -67,8 +77,8 @@ class SaleOrder(models.Model):
# Hitung status masing-masing picking
total = len(bu_pickings)
done_pickings = [p for p in bu_pickings if p.state == 'done']
- done_with_driver = [p for p in done_pickings if p.driver_arrival_date]
- done_without_driver = [p for p in done_pickings if not p.driver_arrival_date]
+ done_with_driver = [p for p in done_pickings if p.sj_return_date]
+ done_without_driver = [p for p in done_pickings if not p.sj_return_date]
if len(done_pickings) == 0:
data['status'] = 'sale'
diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py
index 91a7128d..68759e5c 100755
--- a/indoteknik_custom/__manifest__.py
+++ b/indoteknik_custom/__manifest__.py
@@ -175,6 +175,7 @@
# 'views/tukar_guling_return_views.xml'
'views/tukar_guling_po.xml',
# 'views/refund_sale_order.xml',
+ 'views/update_date_planned_po_wizard_view.xml',
],
'demo': [],
'css': [],
diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py
index 51d25c1f..3a9f9312 100755
--- a/indoteknik_custom/models/__init__.py
+++ b/indoteknik_custom/models/__init__.py
@@ -156,3 +156,4 @@ from . import refund_sale_order
# from . import patch
from . import tukar_guling
from . import tukar_guling_po
+from . import update_date_planned_po_wizard \ No newline at end of file
diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py
index 1a6fad1c..273bcdf9 100644
--- a/indoteknik_custom/models/account_move.py
+++ b/indoteknik_custom/models/account_move.py
@@ -94,6 +94,33 @@ class AccountMove(models.Model):
compute='_compute_has_refund_so',
)
+ payment_date = fields.Date(string="Payment Date", compute='_compute_payment_date')
+ partial_payment = fields.Float(string="Partial Payment", compute='compute_partial_payment')
+
+ reminder_sent_date = fields.Date(string="Tanggal Reminder Terkirim")
+
+ def compute_partial_payment(self):
+ for move in self:
+ if move.amount_total_signed > 0 and move.amount_residual_signed > 0 and move.payment_state == 'partial':
+ move.partial_payment = move.amount_total_signed - move.amount_residual_signed
+ else:
+ move.partial_payment = 0
+
+ def _compute_payment_date(self):
+ for move in self:
+ accountPayment = self.env['account.payment']
+
+ payment = accountPayment.search([]).filtered(
+ lambda p: move.id in p.reconciled_invoice_ids.ids
+ )
+
+ if payment:
+ move.payment_date = payment[0].date
+ elif move.reklas_misc_id:
+ move.payment_date = move.reklas_misc_id.date
+ else:
+ move.payment_date = False
+
# def name_get(self):
# result = []
# for move in self:
@@ -109,102 +136,187 @@ class AccountMove(models.Model):
# result.append((move.id, move.display_name))
# return result
- # def send_due_invoice_reminder(self):
- # today = fields.Date.today()
- # target_dates = [
- # today - timedelta(days=7),
- # today - timedelta(days=3),
- # today,
- # today + timedelta(days=3),
- # today + timedelta(days=7),
- # ]
-
- # partner = self.env['res.partner'].search([('name', 'ilike', 'BANGUNAN TEKNIK GRUP')], limit=1)
- # if not partner:
- # _logger.info("Partner tidak ditemukan.")
- # return
-
- # invoices = self.env['account.move'].search([
- # ('move_type', '=', 'out_invoice'),
- # ('state', '=', 'posted'),
- # ('payment_state', 'not in', ['paid','in_payment', 'reversed']),
- # ('invoice_date_due', 'in', target_dates),
- # ('partner_id', '=', partner.id),
- # ])
-
- # _logger.info(f"Invoices tahap 1: {invoices}")
-
- # invoices = invoices.filtered(
- # lambda inv: inv.invoice_payment_term_id and 'tempo' in (inv.invoice_payment_term_id.name or '').lower()
- # )
- # _logger.info(f"Invoices tahap 2: {invoices}")
-
- # if not invoices:
- # _logger.info(f"Tidak ada invoice yang due untuk partner: {partner.name}")
- # return
-
- # grouped = {}
- # for inv in invoices:
- # grouped.setdefault(inv.partner_id, []).append(inv)
-
- # template = self.env.ref('indoteknik_custom.mail_template_invoice_due_reminder')
-
- # for partner, invs in grouped.items():
- # if not partner.email:
- # _logger.info(f"Partner {partner.name} tidak memiliki email")
- # continue
-
- # invoice_table_rows = ""
- # for inv in invs:
- # days_to_due = (inv.invoice_date_due - today).days if inv.invoice_date_due else 0
- # invoice_table_rows += f"""
- # <tr>
- # <td>{inv.name}</td>
- # <td>{fields.Date.to_string(inv.invoice_date) or '-'}</td>
- # <td>{fields.Date.to_string(inv.invoice_date_due) or '-'}</td>
- # <td>{days_to_due}</td>
- # <td>{formatLang(self.env, inv.amount_total, currency_obj=inv.currency_id)}</td>
- # <td>{inv.ref or '-'}</td>
- # </tr>
- # """
-
- # subject = f"Reminder Invoice Due - {partner.name}"
- # body_html = re.sub(
- # r"<tbody[^>]*>.*?</tbody>",
- # f"<tbody>{invoice_table_rows}</tbody>",
- # template.body_html,
- # flags=re.DOTALL
- # ).replace('${object.name}', partner.name) \
- # .replace('${object.partner_id.name}', partner.name)
- # # .replace('${object.email}', partner.email or '')
-
- # values = {
- # 'subject': subject,
- # 'email_to': 'andrifebriyadiputra@gmail.com', # Ubah ke partner.email untuk produksi
- # 'email_from': 'finance@indoteknik.co.id',
- # 'body_html': body_html,
- # 'reply_to': f'invoice+account.move_{invs[0].id}@indoteknik.co.id',
- # }
-
- # _logger.info(f"VALUES: {values}")
-
- # template.send_mail(invs[0].id, force_send=True, email_values=values)
-
- # # Default System User
- # user_system = self.env['res.users'].browse(25)
- # system_id = user_system.partner_id.id if user_system else False
- # _logger.info(f"System User: {user_system.name} ({user_system.id})")
- # _logger.info(f"System User ID: {system_id}")
-
- # for inv in invs:
- # inv.message_post(
- # subject=subject,
- # body=body_html,
- # subtype_id=self.env.ref('mail.mt_note').id,
- # author_id=system_id,
- # )
-
- # _logger.info(f"Reminder terkirim ke {partner.name} ({values['email_to']}) → {len(invs)} invoice")
+ def send_due_invoice_reminder(self):
+ today = fields.Date.today()
+ target_dates = [
+ today - timedelta(days=7),
+ today - timedelta(days=3),
+ today,
+ today + timedelta(days=3),
+ today + timedelta(days=7),
+ ]
+
+ # --- TESTING ---
+ # partner = self.env['res.partner'].search([('name', 'ilike', 'DIRGANTARA YUDHA ARTHA')], limit=1)
+ # if not partner:
+ # _logger.info("Partner tidak ditemukan.")
+ # return
+ # invoices = self.env['account.move'].search([
+ # ('move_type', '=', 'out_invoice'),
+ # ('state', '=', 'posted'),
+ # ('payment_state', 'not in', ['paid', 'in_payment', 'reversed']),
+ # ('invoice_date_due', 'in', target_dates),
+ # ('partner_id', '=', partner.id),
+ # ])
+
+ invoices = self.env['account.move'].search([
+ ('move_type', '=', 'out_invoice'),
+ ('state', '=', 'posted'),
+ ('payment_state', 'not in', ['paid', 'in_payment', 'reversed']),
+ ('invoice_date_due', 'in', target_dates),
+ ])
+ _logger.info(f"Invoices tahap 1: {invoices}")
+
+ invoices = invoices.filtered(
+ lambda inv: inv.invoice_payment_term_id and 'tempo' in (inv.invoice_payment_term_id.name or '').lower()
+ )
+ _logger.info(f"Invoices tahap 2: {invoices}")
+
+ if not invoices:
+ _logger.info("Tidak ada invoice yang due")
+ return
+
+ invoice_group = {}
+ for inv in invoices:
+ dtd = (inv.invoice_date_due - today).days if inv.invoice_date_due else 0
+ key = (inv.partner_id, dtd)
+ 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
+
+ # Ambil child contact yang di-checklist reminder_invoices
+ reminder_contacts = self.env['res.partner'].search([
+ ('parent_id', '=', partner.id),
+ ('reminder_invoices', '=', True),
+ ('email', '!=', False),
+ ])
+ _logger.info(f"Email Reminder Child {reminder_contacts}")
+
+ # if not reminder_contacts:
+ # _logger.info(f"Partner {partner.name} tidak memiliki email yang sudah ceklis reminder")
+ # continue
+
+ emails = list(filter(None, [partner.email])) + reminder_contacts.mapped('email')
+ if reminder_contacts:
+ _logger.info(f"Email Reminder Child {reminder_contacts}")
+ else:
+ _logger.info(f"Tidak ada child contact reminder, gunakan email utama partner")
+
+ if not emails:
+ _logger.info(f"Partner {partner.name} tidak memiliki email yang bisa dikirimi")
+ continue
+
+ email_to = ",".join(emails)
+ _logger.info(f"Email tujuan: {email_to}")
+
+ invoice_table_rows = ""
+ for inv in invs:
+ days_to_due = (inv.invoice_date_due - today).days if inv.invoice_date_due else 0
+ invoice_table_rows += f"""
+ <tr>
+ <td>{inv.partner_id.name}</td>
+ <td>{inv.ref or '-'}</td>
+ <td>{inv.name}</td>
+ <td>{fields.Date.to_string(inv.invoice_date) or '-'}</td>
+ <td>{fields.Date.to_string(inv.invoice_date_due) or '-'}</td>
+ <td>{formatLang(self.env, inv.amount_total, currency_obj=inv.currency_id)}</td>
+ <td>{inv.invoice_payment_term_id.name or '-'}</td>
+ <td>{days_to_due}</td>
+ </tr>
+ """
+
+ days_to_due_message = ""
+ closing_message = ""
+ if dtd > 0:
+ days_to_due_message = (
+ f"Kami ingin mengingatkan bahwa tagihan anda akan jatuh tempo dalam {dtd} hari ke depan, "
+ "dengan rincian sebagai berikut:"
+ )
+ closing_message = (
+ "Kami mengharapkan pembayaran dapat dilakukan tepat waktu untuk mendukung kelancaran "
+ "hubungan kerja sama yang baik antara kedua belah pihak.<br/>"
+ "Mohon konfirmasi apabila pembayaran telah dijadwalkan. "
+ "Terima kasih atas perhatian dan kerja samanya."
+ )
+
+ if dtd == 0:
+ days_to_due_message = (
+ "Kami ingin mengingatkan bahwa tagihan anda telah memasuki tanggal jatuh tempo pada hari ini, "
+ "dengan rincian sebagai berikut:"
+ )
+ closing_message = (
+ "Mohon kesediaannya untuk segera melakukan pembayaran tepat waktu guna menghindari status "
+ "keterlambatan dan menjaga kelancaran hubungan kerja sama yang telah terjalin dengan baik.<br/>"
+ "Apabila pembayaran telah dijadwalkan atau diproses, mohon dapat dikonfirmasi kepada kami. "
+ "Terima kasih atas perhatian dan kerja samanya."
+ )
+
+ if dtd < 0:
+ days_to_due_message = (
+ f"Kami ingin mengingatkan bahwa tagihan anda telah jatuh tempo selama {abs(dtd)} hari, "
+ "dengan rincian sebagai berikut:"
+ )
+ closing_message = (
+ "Mohon kesediaan Bapak/Ibu untuk segera melakukan pembayaran guna menghindari keterlambatan "
+ "dan menjaga kelancaran kerja sama yang telah terjalin dengan baik.<br/>"
+ "Apabila pembayaran sudah dilakukan, mohon konfirmasi dan lampirkan bukti transfer agar dapat kami proses lebih lanjut. "
+ "Terima kasih atas perhatian dan kerja samanya."
+ )
+
+ body_html = re.sub(
+ r"<tbody[^>]*>.*?</tbody>",
+ f"<tbody>{invoice_table_rows}</tbody>",
+ template.body_html,
+ flags=re.DOTALL
+ ).replace('${object.name}', partner.name) \
+ .replace('${object.partner_id.name}', partner.name) \
+ .replace('${days_to_due_message}', days_to_due_message) \
+ .replace('${closing_message}', closing_message)
+
+ cc_list = [
+ 'finance@indoteknik.co.id',
+ 'akbar@indoteknik.co.id',
+ 'stephan@indoteknik.co.id',
+ 'darren@indoteknik.co.id'
+ ]
+ sales_email = invs[0].invoice_user_id.partner_id.email if invs[0].invoice_user_id else None
+ if sales_email and sales_email not in cc_list:
+ cc_list.append(sales_email)
+
+ # Siapkan email values
+ values = {
+ 'subject': f"Reminder Invoice Due - {partner.name}",
+ # 'email_to': 'andrifebriyadiputra@gmail.com',
+ 'email_to': email_to,
+ 'email_from': 'finance@indoteknik.co.id',
+ 'email_cc': ",".join(sorted(set(cc_list))),
+ 'body_html': body_html,
+ 'reply_to': 'finance@indoteknik.co.id',
+ }
+
+ _logger.info(f"Mengirim email ke: {values['email_to']} > email CC: {values['email_cc']}")
+ template.send_mail(invs[0].id, force_send=True, email_values=values)
+ # flag
+ invs.write({'reminder_sent_date': today})
+ # Post ke chatter
+ user_system = self.env['res.users'].browse(25)
+ system_id = user_system.partner_id.id if user_system else False
+ for inv in invs:
+ inv.message_post(
+ subject=values['subject'],
+ body=body_html,
+ subtype_id=self.env.ref('mail.mt_note').id,
+ author_id=system_id,
+ )
+
+ _logger.info(f"Reminder terkirim ke {partner.name} ({values['email_to']}) → {len(invs)} invoice (dtd = {dtd})")
@api.onchange('invoice_date')
diff --git a/indoteknik_custom/models/account_move_due_extension.py b/indoteknik_custom/models/account_move_due_extension.py
index 4a3f40e2..d354e3e3 100644
--- a/indoteknik_custom/models/account_move_due_extension.py
+++ b/indoteknik_custom/models/account_move_due_extension.py
@@ -33,6 +33,7 @@ class DueExtension(models.Model):
counter = fields.Integer(string="Counter", compute='_compute_counter')
approve_by = fields.Many2one('res.users', string="Approve By", readonly=True)
date_approve = fields.Datetime(string="Date Approve", readonly=True)
+
def _compute_counter(self):
for due in self:
due.counter = due.partner_id.counter
@@ -102,6 +103,14 @@ class DueExtension(models.Model):
self.date_approve = datetime.utcnow()
template = self.env.ref('indoteknik_custom.mail_template_due_extension_approve')
template.send_mail(self.id, force_send=True)
+ return {
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'sale.order',
+ 'view_mode': 'form',
+ 'res_id': self.order_id.id,
+ 'views': [(False, 'form')],
+ 'target': 'current',
+ }
def generate_due_line(self):
partners = self.partner_id.get_child_ids()
diff --git a/indoteknik_custom/models/approval_payment_term.py b/indoteknik_custom/models/approval_payment_term.py
index 6c857b45..4cf9a4c8 100644
--- a/indoteknik_custom/models/approval_payment_term.py
+++ b/indoteknik_custom/models/approval_payment_term.py
@@ -39,6 +39,7 @@ class ApprovalPaymentTerm(models.Model):
('rejected', 'Rejected')],
default='waiting_approval_sales_manager', tracking=True)
reason_reject = fields.Selection([('reason1', 'Reason 1'), ('reason2', 'Reason 2'), ('reason3', 'Reason 3')], string='Reason Reject', tracking=True)
+ reject_reason = fields.Text('Reject Reason', tracking=True)
sale_order_ids = fields.Many2many(
'sale.order',
string='Sale Orders',
diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py
index 9f7df464..6d538b83 100644
--- a/indoteknik_custom/models/commision.py
+++ b/indoteknik_custom/models/commision.py
@@ -208,7 +208,7 @@ class CustomerCommision(models.Model):
('pending', 'Pending'),
('payment', 'Payment'),
], string='Payment Status', copy=False, readonly=True, tracking=3, default='pending')
- note_finnance = fields.Text('Notes Finnance')
+ note_finnance = fields.Text('Notes Finance')
reason_reject = fields.Char(string='Reason Reaject', tracking=True, track_visibility='onchange')
approved_by = fields.Char(string='Approved By', tracking=True, track_visibility='always')
@@ -399,6 +399,27 @@ class CustomerCommision(models.Model):
# result = super(CustomerCommision, self).create(vals)
# return result
+ def _fill_note_finance(self):
+ for rec in self:
+ fee_percent = rec.commision_percent or 0.0
+ dpp = rec.total_dpp or 0.0
+
+ fee = dpp * fee_percent / 100
+ pph21 = 0.5 * fee * 0.05
+ fee_net = fee - pph21
+ rec.note_finnance = (
+ "Kelengkapan data penerima fee sudah lengkap (NPWP dan KTP)\n"
+ f"Perhitungan Fee ({fee_percent:.0f}%) dari nilai DPP pada Invoice terlampir sudah\n"
+ f"sesuai yaitu Rp {fee:,.0f}\n"
+ "Sesuai PMK No. 168 tahun 2023, komisi fee dikenakan PPH 21\n"
+ "sebesar :\n"
+ f"= 50% x Penghasilan Bruto x 5%\n"
+ f"= 50% x Rp {fee:,.0f} x 5%\n"
+ f"= Rp {pph21:,.0f}\n"
+ "Sehingga fee bersih sebesar\n"
+ f"= Rp {fee:,.0f} - Rp {pph21:,.0f}\n"
+ f"= Rp {fee_net:,.0f}"
+ )
def action_confirm_customer_commision(self):
jakarta_tz = pytz.timezone('Asia/Jakarta')
now = datetime.now(jakarta_tz)
@@ -425,6 +446,8 @@ class CustomerCommision(models.Model):
elif self.status == 'pengajuan4' and self.env.user.id == 1272:
for line in self.commision_lines:
line.invoice_id.is_customer_commision = True
+ if self.commision_type == 'fee':
+ self._fill_note_finance()
self.status = 'approved'
self.approved_by = (self.approved_by + ', ' if self.approved_by else '') + self.env.user.name
self.date_approved_accounting = now_naive
diff --git a/indoteknik_custom/models/cust_commision.py b/indoteknik_custom/models/cust_commision.py
index c3105cfd..05c68935 100644
--- a/indoteknik_custom/models/cust_commision.py
+++ b/indoteknik_custom/models/cust_commision.py
@@ -34,4 +34,3 @@ class CustCommision(models.Model):
for rec in duplicate_partner:
if self.commision_type == rec.commision_type:
raise UserError('Partner already exists')
- \ No newline at end of file
diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py
index 27aca0d1..103a9131 100755
--- a/indoteknik_custom/models/purchase_order.py
+++ b/indoteknik_custom/models/purchase_order.py
@@ -1083,9 +1083,9 @@ class PurchaseOrder(models.Model):
# Tambahan: redirect ke BU hanya untuk single PO yang berhasil dikonfirmasi
_logger.info("Jumlah PO: %s | State: %s", len(self), self.state)
- if len(self) == 1:
- _logger.info("Redirecting ke BU")
- return self.action_view_related_bu()
+ # if len(self) == 1:
+ # _logger.info("Redirecting ke BU")
+ # return self.action_view_related_bu()
return res
diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py
index 236df16f..f260f58e 100644
--- a/indoteknik_custom/models/res_partner.py
+++ b/indoteknik_custom/models/res_partner.py
@@ -27,6 +27,11 @@ class ResPartner(models.Model):
# Referensi
supplier_ids = fields.Many2many('user.pengajuan.tempo.line', string="Suppliers")
+ reminder_invoices = fields.Boolean(
+ string='Reminder Invoice?',
+ help='Centang jika kontak ini harus menerima email pengingat invoice.', default=False
+ )
+
# informasi perusahaan
name_tempo = fields.Many2one('res.partner', string='Nama Perusahaan',tracking=True)
industry_id_tempo = fields.Many2one('res.partner.industry', 'Customer Industry', readonly=True)
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index 72f5dd4c..aa534d0c 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -163,7 +163,7 @@ class SaleOrder(models.Model):
carrier_id = fields.Many2one('delivery.carrier', string='Shipping Method', tracking=3)
have_visit_service = fields.Boolean(string='Have Visit Service', compute='_have_visit_service',
help='To compute is customer get visit service')
- delivery_amt = fields.Float(string='Delivery Amt', copy=False)
+ delivery_amt = fields.Float(string='Delivery Amt', copy=False, tracking=True)
shipping_cost_covered = fields.Selection([
('indoteknik', 'Indoteknik'),
('customer', 'Customer')
@@ -1116,8 +1116,9 @@ class SaleOrder(models.Model):
break
if not selected_option and shipping_options:
+ if not self.env.context.get('from_website_checkout'):
+ _logger.info(f"[DEFAULT] Tidak ada yang cocok, pakai opsi pertama: {shipping_options[0].name}")
selected_option = shipping_options[0]
- _logger.info(f"[DEFAULT] Tidak ada yang cocok, pakai opsi pertama: {selected_option.name}")
# ❗ Ganti carrier_id hanya jika BELUM terisi sama sekali (contoh: user dari backend)
if not self.carrier_id:
@@ -1137,9 +1138,18 @@ class SaleOrder(models.Model):
# Set shipping option dan nilai ongkir
if selected_option:
- self.shipping_option_id = selected_option.id
- self.delivery_amt = selected_option.price
- self.delivery_service_type = selected_option.courier_service_code
+ if self.env.context.get('from_website_checkout'):
+ # Simpan di context sebagai nilai sementara
+ self = self.with_context(
+ _temp_delivery_amt=selected_option.price,
+ _temp_delivery_service=selected_option.courier_service_code,
+ _temp_shipping_option=selected_option.id
+ )
+ else:
+ self.shipping_option_id = selected_option.id
+ self.delivery_amt = selected_option.price
+ self.delivery_service_type = selected_option.courier_service_code
+
message_lines = [f"<b>Estimasi Ongkos Kirim Biteship:</b><br/>"]
for courier, options in courier_options.items():
@@ -2159,7 +2169,12 @@ class SaleOrder(models.Model):
# if order.validate_partner_invoice_due():
# return self._create_notification_action('Notification',
# 'Terdapat invoice yang telah melewati batas waktu, mohon perbarui pada dokumen Due Extension')
-
+
+ if not order.with_context(ask_approval=True)._is_request_to_own_team_leader():
+ return self._create_notification_action(
+ 'Peringatan',
+ 'Hanya bisa konfirmasi SO tim Anda.'
+ )
if order._requires_approval_margin_leader():
order.approval_status = 'pengajuan2'
return self._create_approval_notification('Pimpinan')
@@ -2169,6 +2184,12 @@ class SaleOrder(models.Model):
self.check_limit_so_to_invoice()
order.approval_status = 'pengajuan1'
return self._create_approval_notification('Sales Manager')
+ elif order._requires_approval_team_sales():
+ self.check_product_bom()
+ self.check_credit_limit()
+ self.check_limit_so_to_invoice()
+ order.approval_status = 'pengajuan1'
+ return self._create_approval_notification('Team Sales')
raise UserError("Bisa langsung Confirm")
@@ -2385,12 +2406,20 @@ class SaleOrder(models.Model):
return self._create_notification_action('Notification',
'Terdapat invoice yang telah melewati batas waktu, mohon perbarui pada dokumen Due Extension')
+ if not order._is_request_to_own_team_leader():
+ return self._create_notification_action(
+ 'Warning',
+ 'Hanya bisa konfirmasi SO tim Anda.'
+ )
if order._requires_approval_margin_leader():
order.approval_status = 'pengajuan2'
return self._create_approval_notification('Pimpinan')
elif order._requires_approval_margin_manager():
order.approval_status = 'pengajuan1'
return self._create_approval_notification('Sales Manager')
+ elif order._requires_approval_team_sales():
+ order.approval_status = 'pengajuan1'
+ return self._create_approval_notification('Team Sales')
order.approval_status = 'approved'
order._set_sppkp_npwp_contact()
@@ -2491,8 +2520,36 @@ class SaleOrder(models.Model):
return self.total_percent_margin <= 15 and not self.env.user.is_leader
def _requires_approval_margin_manager(self):
- return 15 < self.total_percent_margin <= 24 and not self.env.user.is_sales_manager and not self.env.user.id == 375 and not self.env.user.is_leader
- # return self.total_percent_margin >= 15 and not self.env.user.is_leader and not self.env.user.is_sales_manager
+ return 15 < self.total_percent_margin < 18 and not self.env.user.is_sales_manager and not self.env.user.id == 375 and not self.env.user.is_leader
+
+ def _requires_approval_team_sales(self):
+ return (
+ 18 <= self.total_percent_margin <= 24
+ and self.env.user.id not in [11, 9, 375] # Eko, Ade, Putra
+ and not self.env.user.is_sales_manager
+ and not self.env.user.is_leader
+ )
+
+
+ def _is_request_to_own_team_leader(self):
+ user = self.env.user
+
+ # Pengecualian Pak Akbar & Darren
+ if user.is_leader or user.is_sales_manager:
+ return True
+
+ if user.id in (3401, 20, 3988): # admin (fida, nabila, ninda)
+ return True
+
+ if self.env.context.get("ask_approval") and user.id in (3401, 20, 3988):
+ return True
+
+ salesperson_id = self.user_id.id
+ approver_id = user.id
+ team_leader_id = self.team_id.user_id.id
+
+ return salesperson_id == approver_id or approver_id == team_leader_id
+
def _create_approval_notification(self, approval_role):
title = 'Warning'
@@ -2903,6 +2960,14 @@ class SaleOrder(models.Model):
order.select_shipping_option = 'biteship'
order.action_estimate_shipping()
+ temp_price = self.env.context.get('_temp_delivery_amt')
+ temp_service = self.env.context.get('_temp_delivery_service')
+ temp_option_id = self.env.context.get('_temp_shipping_option')
+ if temp_price and temp_option_id:
+ order.shipping_option_id = temp_option_id
+ order.delivery_amt = temp_price
+ order.delivery_service_type = temp_service
+
# Restore pilihan user setelah estimasi
if user_carrier_id and user_service:
# Dapatkan provider
diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py
index f2f5f52a..eb1c7085 100644
--- a/indoteknik_custom/models/stock_picking.py
+++ b/indoteknik_custom/models/stock_picking.py
@@ -19,10 +19,10 @@ import re
_logger = logging.getLogger(__name__)
_biteship_url = "https://api.biteship.com/v1"
-biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo"
+# biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo"
-# biteship_api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA"
+biteship_api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA"
class StockPicking(models.Model):
@@ -1060,16 +1060,23 @@ class StockPicking(models.Model):
self.sale_id.date_doc_kirim = self.date_doc_kirim
def action_assign(self):
- res = super(StockPicking, self).action_assign()
- for move in self:
- # if not move.sale_id.hold_outgoing and move.location_id.id != 57 and move.location_dest_id.id != 60:
- # TODO cant skip hold outgoing cause of not singleton method
- current_time = datetime.datetime.utcnow()
- move.real_shipping_id = move.sale_id.real_shipping_id
- move.date_availability = current_time
- # self.check_state_reserve()
+ if self.env.context.get('default_picking_type_id'):
+ pickings_to_assign = self.filtered(
+ lambda p: not (p.sale_id and p.sale_id.hold_outgoing)
+ )
+ else:
+ pickings_to_assign = self
+
+ res = super(StockPicking, pickings_to_assign).action_assign()
+
+ current_time = datetime.datetime.utcnow()
+ for picking in pickings_to_assign:
+ picking.real_shipping_id = picking.sale_id.real_shipping_id
+ picking.date_availability = current_time
+
return res
+
def ask_approval(self):
if self.env.user.is_accounting:
raise UserError("Bisa langsung Validate")
@@ -1707,7 +1714,9 @@ class StockPicking(models.Model):
if move_line.qty_done > 0:
product_shipped.append({
'name': move_line.product_id.name,
- 'qty': move_line.qty_done
+ 'code': move_line.product_id.default_code,
+ 'qty': move_line.qty_done,
+ 'image': self.env['ir.attachment'].api_image('product.template', 'image_128', move_line.product_id.product_tmpl_id.id),
})
response = {
diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py
index 3f81393a..ff641f34 100644
--- a/indoteknik_custom/models/tukar_guling.py
+++ b/indoteknik_custom/models/tukar_guling.py
@@ -32,7 +32,7 @@ class TukarGuling(models.Model):
'tukar_guling_id',
string='Transfers'
)
- # origin_so = fields.Many2one('sale.order', string='Origin SO')
+ origin_so = fields.Many2one('sale.order', string='Origin SO', compute='_compute_origin_so')
name = fields.Char('Number', required=True, copy=False, readonly=True, default='New')
date = fields.Datetime('Date', default=fields.Datetime.now, required=True)
operations = fields.Many2one(
@@ -74,6 +74,66 @@ class TukarGuling(models.Model):
date_sales = fields.Datetime('Approved Date Sales', tracking=3, readonly=True)
date_logistic = fields.Datetime('Approved Date Logistic', tracking=3, readonly=True)
+ val_inv_opt = fields.Selection([
+ ('tanpa_cancel', 'Tanpa Cancel Invoice'),
+ ('cancel_invoice', 'Cancel Invoice'),
+ ], tracking=3, string='Invoice Option')
+
+ is_has_invoice = fields.Boolean('Has Invoice?', compute='_compute_is_has_invoice', readonly=True, default=False)
+
+ invoice_id = fields.Many2many('account.move', string='Invoice Ref', readonly=True)
+
+ @api.depends('origin', 'operations')
+ def _compute_origin_so(self):
+ for rec in self:
+ rec.origin_so = False
+ origin_str = rec.origin or rec.operations.origin
+ if origin_str:
+ so = self.env['sale.order'].search([('name', '=', origin_str)], limit=1)
+ rec.origin_so = so.id if so else False
+
+ @api.depends('origin', 'origin_so', 'partner_id', 'line_ids.product_id')
+ def _compute_is_has_invoice(self):
+ Move = self.env['account.move']
+ for rec in self:
+ rec.is_has_invoice = False
+ rec.invoice_id = [(5, 0, 0)]
+
+ product_ids = rec.line_ids.mapped('product_id').ids
+ if not product_ids:
+ continue
+
+ domain = [
+ ('move_type', 'in', ['out_invoice', 'in_invoice']),
+ ('state', 'not in', ['draft', 'cancel']),
+ ('invoice_line_ids.product_id', 'in', product_ids),
+ ]
+
+ if rec.partner_id:
+ domain.append(('partner_id', '=', rec.partner_id.id))
+
+ extra = []
+ if rec.origin:
+ extra.append(('invoice_origin', 'ilike', rec.origin))
+ if rec.origin_so:
+ extra.append(('invoice_line_ids.sale_line_ids.order_id', '=', rec.origin_so.id))
+ if extra:
+ domain = domain + ['|'] * (len(extra) - 1) + extra
+
+ invoices = Move.search(domain).with_context(active_test=False)
+ if invoices:
+ rec.invoice_id = [(6, 0, invoices.ids)]
+ rec.is_has_invoice = True
+
+ def set_opt(self):
+ if not self.val_inv_opt and self.is_has_invoice == True:
+ raise UserError("Kalau sudah ada invoice Return Invoice Option harus diisi!")
+ for rec in self:
+ if rec.val_inv_opt == 'cancel_invoice' and self.is_has_invoice == True:
+ raise UserError("Tidak bisa mengubah Return karena sudah ada invoice dan belum di cancel.")
+ elif rec.val_inv_opt == 'tanpa_cancel' and self.is_has_invoice == True:
+ continue
+
# @api.onchange('operations')
# def get_partner_id(self):
# if self.operations and self.operations.partner_id and self.operations.partner_id.name:
@@ -112,8 +172,6 @@ class TukarGuling(models.Model):
if self.line_ids and from_return_picking:
# Hanya update origin, jangan ubah lines
- if self.operations.origin:
- self.origin = self.operations.origin
_logger.info("📌 Menggunakan product lines dari return wizard, tidak populate ulang.")
# 🚀 Tapi tetap populate mapping koli jika BU/OUT
@@ -145,6 +203,7 @@ class TukarGuling(models.Model):
# Set origin dari operations
if self.operations.origin:
self.origin = self.operations.origin
+ self.origin_so = self.operations.group_id.id
# Auto-populate lines dari move_ids operations
lines_data = []
@@ -288,30 +347,32 @@ class TukarGuling(models.Model):
# ('state', '!=', 'cancel')
# ]) > 0
- @api.constrains('return_type', 'operations')
- def _check_invoice_on_revisi_so(self):
- for record in self:
- if record.return_type == 'revisi_so' and record.origin:
- invoices = self.env['account.move'].search([
- ('invoice_origin', 'ilike', record.origin),
- ('state', 'not in', ['draft', 'cancel'])
- ])
- if invoices:
- raise ValidationError(
- _("Tidak bisa memilih Return Type 'Revisi SO' karena dokumen %s sudah dibuat invoice.") % record.origin
- )
+ # def _check_invoice_on_revisi_so(self):
+ # for record in self:
+ # if record.return_type == 'revisi_so' and record.origin:
+ # invoices = self.env['account.move'].search([
+ # ('invoice_origin', 'ilike', record.origin),
+ # ('state', 'not in', ['draft', 'cancel'])
+ # ])
+ # if invoices:
+ # raise ValidationError(
+ # _("Tidak bisa memilih Return Type 'Revisi SO' karena dokumen %s sudah dibuat invoice.") % record.origin
+ # )
+
@api.model
def create(self, vals):
- # Generate sequence number
if not vals.get('name') or vals['name'] == 'New':
vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling')
- # Auto-fill origin from operations
- if not vals.get('origin') and vals.get('operations'):
+ if vals.get('operations'):
picking = self.env['stock.picking'].browse(vals['operations'])
if picking.origin:
vals['origin'] = picking.origin
+ # Find matching SO
+ so = self.env['sale.order'].search([('name', '=', picking.origin)], limit=1)
+ if so:
+ vals['origin_so'] = so.id
if picking.partner_id:
vals['partner_id'] = picking.partner_id.id
@@ -319,6 +380,10 @@ class TukarGuling(models.Model):
res.message_post(body=_("CCM Created By %s") % self.env.user.name)
return res
+ res = super(TukarGuling, self).create(vals)
+ res.message_post(body=_("CCM Created By %s") % self.env.user.name)
+ return res
+
def copy(self, default=None):
if default is None:
default = {}
@@ -348,7 +413,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_revisi_so()
operasi = self.operations.picking_type_id.id
tipe = self.return_type
pp = vals.get('return_type', tipe)
@@ -448,14 +513,23 @@ class TukarGuling(models.Model):
# raise UserError("BU ini sudah pernah diretur oleh dokumen lain.")
if self.operations.picking_type_id.id == 29:
- for line in self.line_ids:
- mapping_lines = self.mapping_koli_ids.filtered(lambda x: x.product_id == line.product_id)
- total_qty = sum(l.qty_return for l in mapping_lines)
- if total_qty != line.product_uom_qty:
- raise UserError(
- _("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name)
-
- self._check_invoice_on_revisi_so()
+ # Cek apakah ada BU/PICK di origin
+ origin = self.operations.origin
+ has_bu_pick = self.env['stock.picking'].search_count([
+ ('origin', '=', origin),
+ ('picking_type_id', '=', 30),
+ ('state', '!=', 'cancel')
+ ]) > 0
+
+ if has_bu_pick:
+ for line in self.line_ids:
+ mapping_lines = self.mapping_koli_ids.filtered(lambda x: x.product_id == line.product_id)
+ total_qty = sum(l.qty_return for l in mapping_lines)
+ if total_qty != line.product_uom_qty:
+ raise UserError(
+ _("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name
+ )
+ # self._check_invoice_on_revisi_so()
self._validate_product_lines()
if self.state != 'draft':
@@ -506,19 +580,29 @@ 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_revisi_so()
self._check_not_allow_tukar_guling_on_bu_pick()
operasi = self.operations.picking_type_id.id
tipe = self.return_type
if self.operations.picking_type_id.id == 29:
- for line in self.line_ids:
- mapping_lines = self.mapping_koli_ids.filtered(lambda x: x.product_id == line.product_id)
- total_qty = sum(l.qty_return for l in mapping_lines)
- if total_qty != line.product_uom_qty:
- raise UserError(
- _("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name)
+ # Cek apakah ada BU/PICK di origin
+ origin = self.operations.origin
+ has_bu_pick = self.env['stock.picking'].search_count([
+ ('origin', '=', origin),
+ ('picking_type_id', '=', 30),
+ ('state', '!=', 'cancel')
+ ]) > 0
+
+ if has_bu_pick:
+ for line in self.line_ids:
+ mapping_lines = self.mapping_koli_ids.filtered(lambda x: x.product_id == line.product_id)
+ total_qty = sum(l.qty_return for l in mapping_lines)
+ if total_qty != line.product_uom_qty:
+ raise UserError(
+ _("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name
+ )
if operasi == 30 and self.operations.linked_manual_bu_out.state == 'done':
raise UserError("❌ Tidak bisa retur BU/PICK karena BU/OUT sudah done")
@@ -546,6 +630,8 @@ class TukarGuling(models.Model):
elif rec.state == 'approval_finance':
if not rec.env.user.has_group('indoteknik_custom.group_role_fat'):
raise UserError("Hanya Finance Manager yang boleh approve tahap ini.")
+ # rec._check_invoice_on_revisi_so()
+ rec.set_opt()
rec.state = 'approval_logistic'
rec.date_finance = now
@@ -584,6 +670,23 @@ class TukarGuling(models.Model):
def _create_pickings(self):
_logger.info("🛠 Starting _create_pickings()")
+
+ def _force_locations(picking, from_loc, to_loc):
+ picking.write({
+ 'location_id': from_loc,
+ 'location_dest_id': to_loc,
+ })
+ for move in picking.move_lines:
+ move.write({
+ 'location_id': from_loc,
+ 'location_dest_id': to_loc,
+ })
+ for move_line in move.move_line_ids:
+ move_line.write({
+ 'location_id': from_loc,
+ 'location_dest_id': to_loc,
+ })
+
for record in self:
if not record.operations:
raise UserError("BU/OUT dari field operations tidak ditemukan.")
@@ -606,36 +709,53 @@ class TukarGuling(models.Model):
### ======== SRT dari BU/OUT =========
srt_return_lines = []
- for prod in mapping_koli.mapped('product_id'):
- qty_total = sum(mk.qty_return for mk in mapping_koli.filtered(lambda m: m.product_id == prod))
- move = bu_out.move_lines.filtered(lambda m: m.product_id == prod)
- if not move:
- raise UserError(f"Move BU/OUT tidak ditemukan untuk produk {prod.display_name}")
- srt_return_lines.append((0, 0, {
- 'product_id': prod.id,
- 'quantity': qty_total,
- 'move_id': move.id,
- }))
- _logger.info(f"📟 SRT line: {prod.display_name} | qty={qty_total}")
+ if mapping_koli:
+ for prod in mapping_koli.mapped('product_id'):
+ qty_total = sum(mk.qty_return for mk in mapping_koli.filtered(lambda m: m.product_id == prod))
+ move = bu_out.move_lines.filtered(lambda m: m.product_id == prod)
+ if not move:
+ raise UserError(f"Move BU/OUT tidak ditemukan untuk produk {prod.display_name}")
+ srt_return_lines.append((0, 0, {
+ 'product_id': prod.id,
+ 'quantity': qty_total,
+ 'move_id': move.id,
+ }))
+ _logger.info(f"📟 SRT line: {prod.display_name} | qty={qty_total}")
+
+ elif not mapping_koli:
+ for line in record.line_ids:
+ move = bu_out.move_lines.filtered(lambda m: m.product_id == line.product_id)
+ if not move:
+ raise UserError(f"Move BU/OUT tidak ditemukan untuk produk {line.product_id.display_name}")
+ srt_return_lines.append((0, 0, {
+ 'product_id': line.product_id.id,
+ 'quantity': line.product_uom_qty,
+ 'move_id': move.id,
+ }))
+ _logger.info(
+ f"📟 SRT line (fallback line_ids): {line.product_id.display_name} | qty={line.product_uom_qty}")
srt_picking = None
if srt_return_lines:
+ # Tentukan tujuan lokasi berdasarkan ada/tidaknya mapping_koli
+ dest_location_id = BU_OUTPUT_LOCATION_ID if mapping_koli else BU_STOCK_LOCATION_ID
+
srt_wizard = self.env['stock.return.picking'].with_context({
'active_id': bu_out.id,
'default_location_id': PARTNER_LOCATION_ID,
- 'default_location_dest_id': BU_OUTPUT_LOCATION_ID,
+ 'default_location_dest_id': dest_location_id,
'from_ui': False,
}).create({
'picking_id': bu_out.id,
'location_id': PARTNER_LOCATION_ID,
- 'original_location_id': BU_OUTPUT_LOCATION_ID,
'product_return_moves': srt_return_lines
})
+
srt_vals = srt_wizard.create_returns()
srt_picking = self.env['stock.picking'].browse(srt_vals['res_id'])
+ _force_locations(srt_picking, PARTNER_LOCATION_ID, dest_location_id)
+
srt_picking.write({
- 'location_id': PARTNER_LOCATION_ID,
- 'location_dest_id': BU_OUTPUT_LOCATION_ID,
'group_id': bu_out.group_id.id,
'tukar_guling_id': record.id,
'sale_order': record.origin
@@ -648,13 +768,11 @@ class TukarGuling(models.Model):
### ======== ORT dari BU/PICK =========
ort_pickings = []
is_retur_from_bu_pick = record.operations.picking_type_id.id == 30
- picks_to_return = [record.operations] if is_retur_from_bu_pick else mapping_koli.mapped(
- 'pick_id') or line.product_uom_qty
+ picks_to_return = [record.operations] if is_retur_from_bu_pick else mapping_koli.mapped('pick_id')
for pick in picks_to_return:
ort_return_lines = []
if is_retur_from_bu_pick:
- # Ambil dari tukar.guling.line
for line in record.line_ids:
move = pick.move_lines.filtered(lambda m: m.product_id == line.product_id)
if not move:
@@ -668,7 +786,6 @@ class TukarGuling(models.Model):
_logger.info(
f"📟 ORT (BU/PICK langsung) | {pick.name} | {line.product_id.display_name} | qty={line.product_uom_qty}")
else:
- # Ambil dari mapping koli
for mk in mapping_koli.filtered(lambda m: m.pick_id == pick):
move = pick.move_lines.filtered(lambda m: m.product_id == mk.product_id)
if not move:
@@ -691,27 +808,27 @@ class TukarGuling(models.Model):
}).create({
'picking_id': pick.id,
'location_id': BU_OUTPUT_LOCATION_ID,
- 'original_location_id': BU_STOCK_LOCATION_ID,
'product_return_moves': ort_return_lines
})
+
ort_vals = ort_wizard.create_returns()
ort_picking = self.env['stock.picking'].browse(ort_vals['res_id'])
+ _force_locations(ort_picking, BU_OUTPUT_LOCATION_ID, BU_STOCK_LOCATION_ID)
+
ort_picking.write({
- 'location_id': BU_OUTPUT_LOCATION_ID,
- 'location_dest_id': BU_STOCK_LOCATION_ID,
'group_id': bu_out.group_id.id,
'tukar_guling_id': record.id,
'sale_order': record.origin
})
+
created_returns.append(ort_picking)
ort_pickings.append(ort_picking)
_logger.info(f"✅ ORT created: {ort_picking.name}")
record.message_post(
body=f"📦 <b>{ort_picking.name}</b> created by <b>{self.env.user.name}</b> (state: <b>{ort_picking.state}</b>)")
- ### ======== Tukar Guling: BU/OUT dan BU/PICK baru ========
+ ### ======== BU/PICK & BU/OUT Baru dari SRT/ORT ========
if record.return_type == 'tukar_guling':
-
# BU/PICK Baru dari ORT
for ort_p in ort_pickings:
return_lines = []
@@ -737,19 +854,18 @@ class TukarGuling(models.Model):
}).create({
'picking_id': ort_p.id,
'location_id': BU_STOCK_LOCATION_ID,
- 'original_location_id': BU_OUTPUT_LOCATION_ID,
'product_return_moves': return_lines
})
bu_pick_vals = bu_pick_wizard.create_returns()
new_pick = self.env['stock.picking'].browse(bu_pick_vals['res_id'])
+ _force_locations(new_pick, BU_STOCK_LOCATION_ID, BU_OUTPUT_LOCATION_ID)
+
new_pick.write({
- 'location_id': BU_STOCK_LOCATION_ID,
- 'location_dest_id': BU_OUTPUT_LOCATION_ID,
'group_id': bu_out.group_id.id,
'tukar_guling_id': record.id,
'sale_order': record.origin
})
- new_pick.action_assign() # Penting agar bisa trigger check koli
+ new_pick.action_assign()
new_pick.action_confirm()
created_returns.append(new_pick)
_logger.info(f"✅ BU/PICK Baru dari ORT created: {new_pick.name}")
@@ -777,14 +893,13 @@ class TukarGuling(models.Model):
}).create({
'picking_id': srt_picking.id,
'location_id': BU_OUTPUT_LOCATION_ID,
- 'original_location_id': PARTNER_LOCATION_ID,
'product_return_moves': return_lines
})
bu_out_vals = bu_out_wizard.create_returns()
new_out = self.env['stock.picking'].browse(bu_out_vals['res_id'])
+ _force_locations(new_out, BU_OUTPUT_LOCATION_ID, PARTNER_LOCATION_ID)
+
new_out.write({
- 'location_id': BU_OUTPUT_LOCATION_ID,
- 'location_dest_id': PARTNER_LOCATION_ID,
'group_id': bu_out.group_id.id,
'tukar_guling_id': record.id,
'sale_order': record.origin
diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py
index 92d8c9a6..cc1c79c0 100644
--- a/indoteknik_custom/models/tukar_guling_po.py
+++ b/indoteknik_custom/models/tukar_guling_po.py
@@ -54,6 +54,75 @@ class TukarGulingPO(models.Model):
('cancel', 'Cancel'),
], string='Status', default='draft', tracking=3)
+ val_bil_opt = fields.Selection([
+ ('tanpa_cancel', 'Tanpa Cancel Bill'),
+ ('cancel_bill', 'Cancel Bill'),
+ ], tracking=3, string='Bill Option')
+
+ is_has_bill = fields.Boolean('Has Bill?', compute='_compute_is_has_bill', readonly=True, default=False)
+
+ bill_id = fields.Many2many('account.move', string='Bill Ref', readonly=True)
+ origin_po = fields.Many2one('purchase.order', string='Origin PO', compute='_compute_origin_po')
+
+ @api.depends('origin', 'operations')
+ def _compute_origin_po(self):
+ for rec in self:
+ rec.origin_po = False
+ origin_str = rec.origin or rec.operations.origin
+ if origin_str:
+ so = self.env['purchase.order'].search([('name', '=', origin_str)], limit=1)
+ rec.origin_po = so.id if so else False
+
+ @api.depends('origin', 'origin_po', 'vendor_id', 'line_ids.product_id')
+ def _compute_is_has_bill(self):
+ Move = self.env['account.move']
+ for rec in self:
+ # reset
+ rec.is_has_bill = False
+ rec.bill_id = [(5, 0, 0)]
+
+ product_ids = rec.line_ids.mapped('product_id').ids
+ if not product_ids:
+ continue
+
+ # dasar: bill atau vendor credit note yang linennya mengandung produk TG
+ domain = [
+ ('move_type', 'in', ['in_invoice', 'in_refund']),
+ ('state', 'not in', ['draft', 'cancel']),
+ ('invoice_line_ids.product_id', 'in', product_ids),
+ ]
+
+ # batasi ke vendor sama (kalau ada)
+ if rec.vendor_id:
+ domain.append(('partner_id', '=', rec.vendor_id.id))
+
+ # bantu pembatasan ke asal dokumen
+ extra = []
+ if rec.origin:
+ extra.append(('invoice_origin', 'ilike', rec.origin))
+ if rec.origin_po:
+ # di Odoo 14, invoice line biasanya link ke purchase.line lewat purchase_line_id
+ extra.append(('invoice_line_ids.purchase_line_id.order_id', '=', rec.origin_po.id))
+
+ # OR-kan semua extra filter jika ada
+ if extra:
+ domain = domain + ['|'] * (len(extra) - 1) + extra
+
+ bills = Move.search(domain).with_context(active_test=False)
+
+ # --- Opsi 1: minimal salah satu produk TG muncul di bill (default) ---
+ rec.bill_id = [(6, 0, bills.ids)]
+ rec.is_has_bill = bool(bills)
+
+ def set_opt(self):
+ if not self.val_bil_opt and self.is_has_bill == True:
+ raise UserError("Kalau sudah ada bill Return Bill Option harus diisi!")
+ for rec in self:
+ if rec.val_bil_opt == 'cancel_bill' and self.is_has_bill == True:
+ raise UserError("Tidak bisa mengubah Return karena sudah ada bill dan belum di cancel.")
+ elif rec.val_bil_opt == 'tanpa_cancel' and self.is_has_bill == True:
+ continue
+
@api.model
def create(self, vals):
# Generate sequence number
@@ -74,19 +143,18 @@ class TukarGulingPO(models.Model):
return res
- @api.constrains('return_type', 'operations')
- def _check_bill_on_revisi_po(self):
- for record in self:
- if record.return_type == 'revisi_po' and record.origin:
- bills = self.env['account.move'].search([
- ('invoice_origin', 'ilike', record.origin),
- ('move_type', '=', 'in_invoice'), # hanya vendor bill
- ('state', 'not in', ['draft', 'cancel'])
- ])
- if bills:
- raise ValidationError(
- _("Tidak bisa memilih Return Type 'Revisi PO' karena PO %s sudah dibuat vendor bill.") % record.origin
- )
+ # def _check_bill_on_revisi_po(self):
+ # for record in self:
+ # if record.return_type == 'revisi_po' and record.origin:
+ # bills = self.env['account.move'].search([
+ # ('invoice_origin', 'ilike', record.origin),
+ # ('move_type', '=', 'in_invoice'), # hanya vendor bill
+ # ('state', 'not in', ['draft', 'cancel'])
+ # ])
+ # if bills:
+ # raise ValidationError(
+ # _("Tidak bisa memilih Return Type 'Revisi PO' karena PO %s sudah dibuat vendor bill. Harus Cancel Jika ingin melanjutkan") % record.origin
+ # )
@api.onchange('operations')
def _onchange_operations(self):
@@ -102,6 +170,7 @@ class TukarGulingPO(models.Model):
# Hanya update origin, jangan ubah lines
if self.operations.origin:
self.origin = self.operations.origin
+ self.origin_po = self.operations.group_id.id
return
if from_return_picking:
@@ -281,7 +350,7 @@ class TukarGulingPO(models.Model):
def write(self, vals):
if self.operations.picking_type_id.id not in [75, 28]:
raise UserError("❌ Tidak bisa retur bukan BU/INPUT atau BU/PUT!")
- self._check_bill_on_revisi_po()
+ # self._check_bill_on_revisi_po()
tipe = vals.get('return_type', self.return_type)
if self.operations and self.operations.picking_type_id.id == 28 and tipe == 'tukar_guling':
@@ -349,7 +418,7 @@ class TukarGulingPO(models.Model):
def action_submit(self):
self.ensure_one()
- self._check_bill_on_revisi_po()
+ # self._check_bill_on_revisi_po()
self._validate_product_lines()
self._check_not_allow_tukar_guling_on_bu_input()
@@ -385,7 +454,7 @@ class TukarGulingPO(models.Model):
def action_approve(self):
self.ensure_one()
self._validate_product_lines()
- self._check_bill_on_revisi_po()
+ # self._check_bill_on_revisi_po()
self._check_not_allow_tukar_guling_on_bu_input()
if not self.operations:
@@ -399,20 +468,22 @@ class TukarGulingPO(models.Model):
# Cek hak akses berdasarkan state
for rec in self:
if rec.state == 'approval_purchase':
- if not rec.env.user.has_group('indoteknik_custom.group_role_sales'):
- raise UserError("Hanya Sales Manager yang boleh approve tahap ini.")
+ if not rec.env.user.has_group('indoteknik_custom.group_role_purchasing'):
+ raise UserError("Hanya Purchasing yang boleh approve tahap ini.")
rec.state = 'approval_finance'
rec.date_purchase = now
elif rec.state == 'approval_finance':
if not rec.env.user.has_group('indoteknik_custom.group_role_fat'):
- raise UserError("Hanya Finance Manager yang boleh approve tahap ini.")
+ raise UserError("Hanya Finance yang boleh approve tahap ini.")
+ # rec._check_bill_on_revisi_po()
+ rec.set_opt()
rec.state = 'approval_logistic'
rec.date_finance = now
elif rec.state == 'approval_logistic':
if not rec.env.user.has_group('indoteknik_custom.group_role_logistic'):
- raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.")
+ raise UserError("Hanya Logistic yang boleh approve tahap ini.")
rec.state = 'approved'
rec._create_pickings()
rec.date_logistic = now
@@ -427,7 +498,7 @@ class TukarGulingPO(models.Model):
('state', '=', 'done'),
('picking_type_id.id', '=', 76)
])
- if self.state == 'aproved' and prt:
+ 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':
@@ -440,7 +511,7 @@ class TukarGulingPO(models.Model):
('state', '=', 'done'),
('picking_type_id.id', '=', 76)
])
- if self.state == 'aproved' and total_prt > 0 and prt == total_prt:
+ if self.state == 'approved' and total_prt > 0 and prt == total_prt:
self.state = 'done'
# bu put tukar guling
elif self.operations.picking_type_id.id == 75 and self.return_type == 'tukar_guling':
@@ -464,7 +535,7 @@ class TukarGulingPO(models.Model):
user = self.env.user
if not (
- user.has_group('indoteknik_custom.group_role_sales') or
+ user.has_group('indoteknik_custom.group_role_purchasing') or
user.has_group('indoteknik_custom.group_role_fat') or
user.has_group('indoteknik_custom.group_role_logistic')
):
diff --git a/indoteknik_custom/models/update_date_planned_po_wizard.py b/indoteknik_custom/models/update_date_planned_po_wizard.py
new file mode 100644
index 00000000..a0d241c8
--- /dev/null
+++ b/indoteknik_custom/models/update_date_planned_po_wizard.py
@@ -0,0 +1,14 @@
+from odoo import models, fields, api
+
+class PurchaseOrderUpdateDateWizard(models.TransientModel):
+ _name = 'purchase.order.update.date.wizard'
+ _description = 'Wizard to Update Receipt Date on Purchase Order Lines'
+
+ date_planned = fields.Datetime(string="New Receipt Date", required=True)
+
+ def action_update_date(self):
+ active_ids = self.env.context.get('active_ids', [])
+ orders = self.env['purchase.order'].browse(active_ids)
+ for order in orders:
+ order.write({'date_planned': self.date_planned})
+ return {'type': 'ir.actions.act_window_close'}
diff --git a/indoteknik_custom/report/report_sale_order.xml b/indoteknik_custom/report/report_sale_order.xml
index b9928790..d1a6de28 100644
--- a/indoteknik_custom/report/report_sale_order.xml
+++ b/indoteknik_custom/report/report_sale_order.xml
@@ -96,8 +96,8 @@
<th name="th_description" class="text-left">Description</th>
<th name="th_quantity" class="text-right">Quantity</th>
<th name="th_priceunit" class="text-right">Unit Price</th>
- <th name="th_discount" t-if="display_discount" class="text-right" groups="product.group_discount_per_so_line">
- <span>Disc.%</span>
+ <th name="th_discount" class="text-right" groups="product.group_discount_per_so_line">
+ Disc.%
</th>
<th name="th_taxes" class="text-right">Taxes</th>
<th name="th_subtotal" class="text-right">
@@ -125,7 +125,7 @@
<td name="td_priceunit" class="text-right">
<span t-field="line.price_unit"/>
</td>
- <td t-if="display_discount" class="text-right" groups="product.group_discount_per_so_line">
+ <td class="text-right" groups="product.group_discount_per_so_line">
<span t-field="line.discount"/>
</td>
<td name="td_taxes" class="text-right">
diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv
index 0ac3e86c..91d1e493 100755
--- a/indoteknik_custom/security/ir.model.access.csv
+++ b/indoteknik_custom/security/ir.model.access.csv
@@ -191,4 +191,5 @@ access_tukar_guling_all_users,tukar.guling.all.users,model_tukar_guling,base.gro
access_tukar_guling_line_all_users,tukar.guling.line.all.users,model_tukar_guling_line,base.group_user,1,1,1,1
access_tukar_guling_po_all_users,tukar.guling.po.all.users,model_tukar_guling_po,base.group_user,1,1,1,1
access_tukar_guling_line_po_all_users,tukar.guling.line.po.all.users,model_tukar_guling_line_po,base.group_user,1,1,1,1
-access_tukar_guling_mapping_koli_all_users,tukar.guling.mapping.koli.all.users,model_tukar_guling_mapping_koli,base.group_user,1,1,1,1 \ No newline at end of file
+access_tukar_guling_mapping_koli_all_users,tukar.guling.mapping.koli.all.users,model_tukar_guling_mapping_koli,base.group_user,1,1,1,1
+access_purchase_order_update_date_wizard,access.purchase.order.update.date.wizard,model_purchase_order_update_date_wizard,base.group_user,1,1,1,1 \ No newline at end of file
diff --git a/indoteknik_custom/views/account_move.xml b/indoteknik_custom/views/account_move.xml
index ae944a4a..667a6b3e 100644
--- a/indoteknik_custom/views/account_move.xml
+++ b/indoteknik_custom/views/account_move.xml
@@ -29,6 +29,8 @@
</field>
<field name="payment_reference" position="after">
<field name="date_completed" readonly="1" attrs="{'invisible': [('move_type', '!=', 'out_invoice')]}"/>
+ <field name="payment_date" readonly="1" attrs="{'invisible': [('move_type', '!=', 'out_invoice'), ('payment_date', '=', False)]}"/>
+ <field name="partial_payment" readonly="1" attrs="{'invisible': [('move_type', '!=', 'out_invoice'), ('payment_state', '!=', 'partial')]}"/>
<field name="reklas_id" attrs="{'invisible': [('move_type', '!=', 'out_invoice')]}"/>
</field>
<field name="invoice_date" position="after">
diff --git a/indoteknik_custom/views/account_move_views.xml b/indoteknik_custom/views/account_move_views.xml
index da25636e..0fd7c9cd 100644
--- a/indoteknik_custom/views/account_move_views.xml
+++ b/indoteknik_custom/views/account_move_views.xml
@@ -47,15 +47,20 @@
<button name="approve_new_due"
string="Approve"
type="object"
+ attrs="{'readonly': [('approval_status', 'in', ('approved'))]}"
/>
<button name="due_extension_approval"
string="Ask Approval"
type="object"
+ attrs="{'readonly': [('approval_status', 'in', ('approved'))]}"
/>
<button name="due_extension_cancel"
string="Cancel"
type="object"
+ attrs="{'readonly': [('approval_status', 'in', ('approved'))]}"
/>
+ <field name="approval_status" widget="statusbar"
+ statusbar_visible="pengajuan,approved"/>
</header>
<sheet>
<group>
@@ -67,7 +72,6 @@
<group>
<field name="is_approve" readonly="1"/>
<field name="counter" readonly="1"/>
- <field name="approval_status" readonly="1"/>
<field name="approve_by" readonly="1"/>
<field name="date_approve" readonly="1"/>
</group>
diff --git a/indoteknik_custom/views/approval_payment_term.xml b/indoteknik_custom/views/approval_payment_term.xml
index cc9db914..f7c24737 100644
--- a/indoteknik_custom/views/approval_payment_term.xml
+++ b/indoteknik_custom/views/approval_payment_term.xml
@@ -59,7 +59,8 @@
</group>
<group>
<field name="reason"/>
- <field name="reason_reject" attrs="{'invisible': [('state', '!=', 'rejected')]}"/>
+ <field name="reason_reject" invisible="1"/>
+ <field name="reject_reason" attrs="{'invisible': [('state', '!=', 'rejected')]}"/>
<field name="approve_date" readonly="1"/>
<field name="approve_sales_manager" readonly="1"/>
<field name="approve_finance" readonly="1"/>
diff --git a/indoteknik_custom/views/mail_template_invoice_reminder.xml b/indoteknik_custom/views/mail_template_invoice_reminder.xml
index 21055eb0..8450be28 100644
--- a/indoteknik_custom/views/mail_template_invoice_reminder.xml
+++ b/indoteknik_custom/views/mail_template_invoice_reminder.xml
@@ -6,29 +6,32 @@
<field name="model_id" ref="account.model_account_move"/>
<field name="subject">Reminder Invoice Due - ${object.name}</field>
<field name="email_from">finance@indoteknik.co.id</field>
- <field name="email_to">andrifebriyadiputra@gmail.com</field>
+ <field name="email_to"></field>
<field name="body_html" type="html">
<div>
<p><b>Dear ${object.name},</b></p>
- <p>Berikut adalah daftar invoice Anda yang mendekati atau telah jatuh tempo:</p>
+ <p>${days_to_due_message}</p>
- <table border="1" cellpadding="4" cellspacing="0" style="border-collapse: collapse; width: 100%; font-size: 12px">
+ <table border="1" cellpadding="4" cellspacing="0" style="border-collapse: collapse; font-size: 12px">
<thead>
<tr style="background-color: #f2f2f2;" align="left">
+ <th>Customer</th>
+ <th>No. PO</th>
<th>Invoice Number</th>
- <th>Tanggal Invoice</th>
- <th>Jatuh Tempo</th>
- <th>Sisa Hari</th>
- <th>Total</th>
- <th>Referensi</th>
+ <th>Invoice Date</th>
+ <th>Due Date</th>
+ <th>Amount</th>
+ <th>Term</th>
+ <th>Days To Due</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
- <p>Mohon bantuan dan kerjasamanya agar tetap bisa bekerjasama dengan baik</p>
+ <p>${closing_message}</p>
+ <br/>
<p>Terima Kasih.</p>
<br/>
<br/>
@@ -42,6 +45,7 @@
<a href="https://wa.me/6285716970374" target="_blank">+62-857-1697-0374</a> |
<a href="mailto:finance@indoteknik.co.id">finance@indoteknik.co.id</a>
</b></p>
+ <p><i>Email ini dikirim secara otomatis. Abaikan jika pembayaran telah dilakukan.</i></p>
</div>
</field>
diff --git a/indoteknik_custom/views/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml
index fedcb4f9..15cdc788 100755
--- a/indoteknik_custom/views/purchase_order.xml
+++ b/indoteknik_custom/views/purchase_order.xml
@@ -428,4 +428,24 @@
<field name="code">action = records.open_form_multi_cancel()</field>
</record>
</data>
+ <data>
+ <record id="action_update_receipt_date_po" model="ir.actions.server">
+ <field name="name">Update Receipt Date</field>
+ <field name="model_id" ref="purchase.model_purchase_order"/>
+ <field name="binding_model_id" ref="purchase.model_purchase_order"/>
+ <field name="state">code</field>
+ <field name="binding_view_types">list</field>
+ <field name="code">
+ action = {
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'purchase.order.update.date.wizard',
+ 'view_mode': 'form',
+ 'target': 'new',
+ 'context': {
+ 'active_ids': env.context.get('active_ids', []),
+ },
+ }
+ </field>
+ </record>
+ </data>
</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/res_partner.xml b/indoteknik_custom/views/res_partner.xml
index 38cd2756..b081f6f2 100644
--- a/indoteknik_custom/views/res_partner.xml
+++ b/indoteknik_custom/views/res_partner.xml
@@ -102,6 +102,9 @@
/>
</div>
</xpath>
+ <xpath expr="//field[@name='child_ids']/form//field[@name='mobile']" position="after">
+ <field name="reminder_invoices"/>
+ </xpath>
<xpath expr="//field[@name='property_payment_term_id']" position="attributes">
<attribute name="readonly">0</attribute>
</xpath>
diff --git a/indoteknik_custom/views/tukar_guling.xml b/indoteknik_custom/views/tukar_guling.xml
index c23995d3..9dd31905 100644
--- a/indoteknik_custom/views/tukar_guling.xml
+++ b/indoteknik_custom/views/tukar_guling.xml
@@ -21,7 +21,7 @@
<field name="name">pengajuan.tukar.guling.tree</field>
<field name="model">tukar.guling</field>
<field name="arch" type="xml">
- <tree create="1" delete="1" default_order="create_date desc">
+ <tree create="0" delete="1" default_order="create_date desc">
<field name="name"/>
<field name="partner_id" string="Customer"/>
<field name="origin" string="SO Number"/>
@@ -83,15 +83,21 @@
<field name="return_type" attrs="{'readonly': [('state', 'not in', 'draft')]}"/>
<field name="operations"
attrs="{'readonly': [('state', 'not in', 'draft')]}"/>
- <field name="origin" readonly="1"/>
+<!-- <field name="origin" readonly="1"/>-->
+ <field name="origin_so" readonly="1"/>
+ <field name="is_has_invoice" readonly="1"/>
</group>
<group>
+ <field name="val_inv_opt" attrs="{'invisible': [('is_has_invoice', '=', False)]}"/>
<field name="ba_num" string="Nomor BA"/>
<field name="notes"/>
<field name="date_sales" readonly="1"/>
<field name="date_finance" readonly="1"/>
<field name="date_logistic" readonly="1"/>
</group>
+ <group>
+ <field name="invoice_id" readonly="1" attrs="{'invisible': [('is_has_invoice', '=', False)]}"/>
+ </group>
</group>
<notebook>
<page string="Product Lines" name="product_lines">
diff --git a/indoteknik_custom/views/tukar_guling_po.xml b/indoteknik_custom/views/tukar_guling_po.xml
index accf7dbc..1c6a86ea 100644
--- a/indoteknik_custom/views/tukar_guling_po.xml
+++ b/indoteknik_custom/views/tukar_guling_po.xml
@@ -24,7 +24,7 @@
<tree create="1" delete="1" default_order="create_date desc">
<field name="name"/>
<field name="vendor_id" string="Customer"/>
- <field name="origin" string="SO Number"/>
+ <field name="origin" string="PO Number"/>
<field name="operations" string="Operations"/>
<field name="return_type" string="Return Type"/>
<field name="state" widget="badge"
@@ -89,16 +89,22 @@
attrs="{
'required': [('return_type', 'in', ['revisi_po', 'tukar_guling'])]
}"/>
- <field name="origin" readonly="1"/>
- <!-- <field name="origin_so" readonly="1"/>-->
+<!-- <field name="origin" readonly="1"/>-->
+ <field name="origin_po" readonly="1"/>
+ <field name="is_has_bill" readonly="1"/>
+<!-- <field name="bill_id" readonly="1" />-->
</group>
<group>
+ <field name="val_bil_opt" attrs="{'invisible': [('is_has_bill', '=', False)]}"/>
<field name="ba_num" string="Nomor BA"/>
<field name="notes"/>
<field name="date_purchase" readonly="1"/>
<field name="date_finance" readonly="1"/>
<field name="date_logistic" readonly="1"/>
</group>
+ <group>
+ <field name="bill_id" readonly="1" attrs="{'invisible': [('is_has_bill', '=', False)]}"/>
+ </group>
</group>
<!-- Product Lines -->
<notebook>
diff --git a/indoteknik_custom/views/update_date_planned_po_wizard_view.xml b/indoteknik_custom/views/update_date_planned_po_wizard_view.xml
new file mode 100644
index 00000000..6b3ab991
--- /dev/null
+++ b/indoteknik_custom/views/update_date_planned_po_wizard_view.xml
@@ -0,0 +1,25 @@
+<odoo>
+ <record id="view_update_date_planned_po_wizard_form" model="ir.ui.view">
+ <field name="name">purchase.order.update.date.wizard.form</field>
+ <field name="model">purchase.order.update.date.wizard</field>
+ <field name="arch" type="xml">
+ <form string="Update Receipt Date">
+ <group>
+ <field name="date_planned"/>
+ </group>
+ <footer>
+ <button string="Apply" type="object" name="action_update_date" class="btn-primary"/>
+ <button string="Cancel" special="cancel"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <record id="action_update_date_planned_po_wizard" model="ir.actions.act_window">
+ <field name="name">Update Receipt Date</field>
+ <field name="res_model">purchase.order.update.date.wizard</field>
+ <field name="view_mode">form</field>
+ <field name="target">new</field>
+ </record>
+ </odoo>
+ \ No newline at end of file