summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIndoteknik . <it@fixcomart.co.id>2025-08-29 08:18:41 +0700
committerIndoteknik . <it@fixcomart.co.id>2025-08-29 08:18:41 +0700
commitbec429267fd4baca3a914faefba974f18c2695cb (patch)
tree37739237e2d9650b866423f844709c68c2171f28
parent83759c763cbde2b179472446231bca007c153449 (diff)
parent0112ac064a7484685119cf9371ffbea32de6fd59 (diff)
Merge branch 'odoo-backup' of https://bitbucket.org/altafixco/indoteknik-addons into pum-v2
-rwxr-xr-xindoteknik_custom/__manifest__.py3
-rw-r--r--indoteknik_custom/models/account_move.py210
-rw-r--r--indoteknik_custom/models/account_move_due_extension.py15
-rw-r--r--indoteknik_custom/models/approval_payment_term.py8
-rwxr-xr-xindoteknik_custom/models/product_template.py1
-rwxr-xr-xindoteknik_custom/models/purchase_order.py32
-rw-r--r--indoteknik_custom/models/res_partner.py12
-rwxr-xr-xindoteknik_custom/models/sale_order.py77
-rw-r--r--indoteknik_custom/models/sale_order_line.py38
-rw-r--r--indoteknik_custom/models/solr/promotion_program_line.py112
-rw-r--r--indoteknik_custom/models/stock_picking.py28
-rw-r--r--indoteknik_custom/models/tukar_guling.py7
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv4
-rw-r--r--indoteknik_custom/static/src/js/check_product_barcode.js76
-rw-r--r--indoteknik_custom/views/account_move.xml40
-rw-r--r--indoteknik_custom/views/account_move_views.xml3
-rw-r--r--indoteknik_custom/views/approval_payment_term.xml11
-rw-r--r--indoteknik_custom/views/assets.xml7
-rw-r--r--indoteknik_custom/views/customer_commision.xml1
-rw-r--r--indoteknik_custom/views/dunning_run.xml2
-rw-r--r--indoteknik_custom/views/mail_template_invoice_reminder.xml55
-rwxr-xr-xindoteknik_custom/views/product_template.xml1
-rwxr-xr-xindoteknik_custom/views/purchase_order.xml26
-rw-r--r--indoteknik_custom/views/res_partner.xml5
-rwxr-xr-xindoteknik_custom/views/sale_order.xml10
-rw-r--r--indoteknik_custom/views/user_pengajuan_tempo.xml2
26 files changed, 659 insertions, 127 deletions
diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py
index 39995b21..4b15c0a6 100755
--- a/indoteknik_custom/__manifest__.py
+++ b/indoteknik_custom/__manifest__.py
@@ -8,8 +8,9 @@
'author': 'Rafi Zadanly',
'website': '',
'images': ['assets/favicon.ico'],
- 'depends': ['base', 'coupon', 'delivery', 'sale', 'sale_management', 'vit_kelurahan', 'vit_efaktur' ],
+ 'depends': ['base', 'coupon', 'delivery', 'sale', 'sale_management', 'vit_kelurahan', 'vit_efaktur'],
'data': [
+ 'views/assets.xml',
'security/ir.model.access.csv',
'views/group_partner.xml',
'views/blog_post.xml',
diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py
index b0ffd8b9..c44cad78 100644
--- a/indoteknik_custom/models/account_move.py
+++ b/indoteknik_custom/models/account_move.py
@@ -99,6 +99,12 @@ class AccountMove(models.Model):
reminder_sent_date = fields.Date(string="Tanggal Reminder Terkirim")
+ customer_promise_date = fields.Date(
+ string="Janji Bayar",
+ help="Tanggal janji bayar dari customer setelah reminder dikirim.",
+ tracking=True
+ )
+
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':
@@ -121,6 +127,39 @@ class AccountMove(models.Model):
else:
move.payment_date = False
+ def action_sync_promise_date(self):
+ self.ensure_one()
+ finance_user_ids = [688]
+ if self.env.user.id not in finance_user_ids:
+ raise UserError('Hanya Finance (Widya) yang dapat menggunakan fitur ini.')
+ if not self.customer_promise_date:
+ raise UserError("Isi Janji Bayar terlebih dahulu sebelum melakukan sinkronisasi.")
+
+ other_invoices = self.env['account.move'].search([
+ ('id', '!=', self.id),
+ ('partner_id', '=', self.partner_id.id),
+ ('invoice_date_due', '=', self.invoice_date_due),
+ ('move_type', '=', 'out_invoice'),
+ ('state', '=', 'posted'),
+ ('date_terima_tukar_faktur', '!=', False)
+ ])
+ lines = []
+ for inv in other_invoices:
+ lines.append((0, 0, {'invoice_id': inv.id, 'sync_check': True})) # default dicentang semua
+
+ wizard = self.env['sync.promise.date.wizard'].create({
+ 'invoice_id': self.id,
+ 'line_ids': lines,
+ })
+
+ return {
+ 'name': 'Sync Janji Bayar',
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'sync.promise.date.wizard',
+ 'view_mode': 'form',
+ 'res_id': wizard.id,
+ 'target': 'new',
+ }
def send_due_invoice_reminder(self):
today = fields.Date.today()
@@ -140,8 +179,9 @@ class AccountMove(models.Model):
('state', '=', 'posted'),
('payment_state', 'not in', ['paid', 'in_payment', 'reversed']),
('invoice_date_due', 'in', target_dates),
- ('date_terima_tukar_faktur', '!=', False)
- ])
+ ('date_terima_tukar_faktur', '!=', False),
+ ('partner_id', 'in' , [94603])
+ ], limit=5)
_logger.info(f"Invoices: {invoices}")
invoices = invoices.filtered(
@@ -168,6 +208,24 @@ class AccountMove(models.Model):
_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
+ if today <= earliest_promise:
+ _logger.info(
+ f"Skip reminder untuk {partner.name} karena ada Janji Bayar sampai {earliest_promise}"
+ )
+ continue
+
+ emails = []
+ # skip semua jika partner centang dont_send_reminder_inv_all
+ if partner.dont_send_reminder_inv_all:
+ _logger.info(f"Partner {partner.name} skip karena dont_send_reminder_inv_all aktif")
+ continue
+ # cek parent hanya dengan flag dont_sent_reminder_inv_parent
+ if not partner.dont_send_reminder_inv_parent and partner.email:
+ emails.append(partner.email)
+
# Ambil child contact yang di-checklist reminder_invoices
reminder_contacts = self.env['res.partner'].search([
('parent_id', '=', partner.id),
@@ -175,12 +233,8 @@ class AccountMove(models.Model):
('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')
+
+ emails += reminder_contacts.mapped('email')
if reminder_contacts:
_logger.info(f"Email Reminder Child {reminder_contacts}")
else:
@@ -194,8 +248,10 @@ class AccountMove(models.Model):
_logger.info(f"Email tujuan: {email_to}")
invoice_table_rows = ""
+ grand_total = 0
for inv in invs:
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>{inv.partner_id.name}</td>
@@ -208,6 +264,66 @@ class AccountMove(models.Model):
<td>{days_to_due}</td>
</tr>
"""
+ invoice_table_footer = f"""
+ <tfoot>
+ <tr style="font-weight:bold; background-color:#f9f9f9;">
+ <td colspan="5" align="right">Grand Total</td>
+ <td>{formatLang(self.env, grand_total, currency_obj=invs[0].currency_id)}</td>
+ <td colspan="2"></td>
+ </tr>
+ </tfoot>
+ """
+
+ blocking_limit = partner.blocking_stage or 0.0
+
+ # semua invoice tempo yang masih open
+ outstanding_invoices = self.env['account.move'].search([
+ ('move_type', '=', 'out_invoice'),
+ ('state', '=', 'posted'),
+ ('payment_state', 'not in', ['paid', 'in_payment', 'reversed']),
+ ('partner_id', '=', partner.id),
+ ('invoice_payment_term_id.name', 'ilike', 'tempo')
+ ])
+
+ outstanding_amount = sum(outstanding_invoices.mapped('amount_total'))
+
+ # invoice tempo yang sudah jatuh tempo
+ overdue_invoices = outstanding_invoices.filtered(
+ lambda inv: inv.invoice_date_due and inv.invoice_date_due < fields.Date.today()
+ )
+
+ overdue_amount = sum(overdue_invoices.mapped('amount_total'))
+
+ 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'
+
+ 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 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>
+ <li style="color:red;">
+ Kredit Limit Terpakai: {formatLang(self.env, outstanding_amount, currency_obj=currency)}
+ <span style="font-size:12px; color:#666;">({len(outstanding_invoices)} Transaksi)</span>
+ </li>
+ <li style="color:red;">
+ Jatuh Tempo: {formatLang(self.env, overdue_amount, currency_obj=currency)}
+ <span style="font-size:12px; color:#666;">({len(overdue_invoices)} Invoice)</span>
+ </li>
+ </ul>
+ <p style="margin-top:10px;">
+ <a href="{tempo_link or '#'}"
+ style="display:inline-block; padding:8px 16px;
+ background-color:#007bff; color:#fff; text-decoration:none;
+ border-radius:4px; font-size:12px;">
+ Cek Selengkapnya
+ </a>
+ </p>
+ """
days_to_due_message = ""
closing_message = ""
@@ -249,13 +365,14 @@ class AccountMove(models.Model):
body_html = re.sub(
r"<tbody[^>]*>.*?</tbody>",
- f"<tbody>{invoice_table_rows}</tbody>",
+ f"<tbody>{invoice_table_rows}</tbody>{invoice_table_footer}",
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)
+ .replace('${closing_message}', closing_message) \
+ .replace('${limit_info_html}', limit_info_html)
cc_list = [
'finance@indoteknik.co.id',
@@ -278,8 +395,9 @@ class AccountMove(models.Model):
'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)
+ _logger.info(f"Mengirim email ke: {values['email_to']} > email CC: {values['email_cc']}")
+ _logger.info(f"Reminder terkirim ke {partner.name} ({values['email_to']}) → {len(invs)} invoice (dtd = {dtd})")
# flag
invs.write({'reminder_sent_date': today})
# Post ke chatter
@@ -293,7 +411,6 @@ class AccountMove(models.Model):
author_id=system_id,
)
- _logger.info(f"Reminder terkirim ke {partner.name} ({values['email_to']}) → {len(invs)} invoice (dtd = {dtd})")
@api.onchange('invoice_date')
@@ -689,4 +806,71 @@ class AccountMove(models.Model):
'date_efaktur_exported': datetime.utcnow(),
})
- return response \ No newline at end of file
+ return response
+
+class SyncPromiseDateWizard(models.TransientModel):
+ _name = "sync.promise.date.wizard"
+ _description = "Sync Janji Bayar Wizard"
+
+ invoice_id = fields.Many2one('account.move', string="Invoice Utama", required=True)
+ promise_date = fields.Date(string="Janji Bayar", related="invoice_id.customer_promise_date", readonly=True)
+ line_ids = fields.One2many('sync.promise.date.wizard.line', 'wizard_id', string="Invoices Terkait")
+
+ def action_check_all(self):
+ for line in self.line_ids:
+ line.sync_check = True
+ return {
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'sync.promise.date.wizard',
+ 'view_mode': 'form',
+ 'res_id': self.id,
+ 'target': 'new',
+ }
+
+ def action_uncheck_all(self):
+ for line in self.line_ids:
+ line.sync_check = False
+ return {
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'sync.promise.date.wizard',
+ 'view_mode': 'form',
+ 'res_id': self.id,
+ 'target': 'new',
+ }
+
+ def action_confirm(self):
+ self.ensure_one()
+ selected_lines = self.line_ids.filtered(lambda l: l.sync_check)
+ selected_invoices = selected_lines.mapped('invoice_id')
+
+ if not selected_invoices:
+ raise UserError("Tidak ada invoice dipilih untuk sinkronisasi.")
+
+ # Update hanya invoice yang dipilih
+ for inv in selected_invoices:
+ inv.write({'customer_promise_date': self.promise_date})
+ inv.message_post(
+ body=f"Janji Bayar {self.promise_date} disinkronkan dari invoice {self.invoice_id.name}."
+ )
+
+ # Log di invoice utama
+ self.invoice_id.message_post(
+ body=f"Janji Bayar {self.promise_date} disinkronkan ke {len(selected_invoices)} invoice lain: {', '.join(selected_invoices.mapped('name'))}."
+ )
+ return {'type': 'ir.actions.act_window_close'}
+
+
+class SyncPromiseDateWizardLine(models.TransientModel):
+ _name = "sync.promise.date.wizard.line"
+ _description = "Sync Janji Bayar Wizard Line"
+
+ wizard_id = fields.Many2one('sync.promise.date.wizard', string="Wizard")
+ invoice_id = fields.Many2one('account.move', string="Invoice")
+ sync_check = fields.Boolean(string="Sync?")
+ invoice_name = fields.Char(related="invoice_id.name", string="Nomor Invoice", readonly=True)
+ invoice_date_due = fields.Date(related="invoice_id.invoice_date_due", string="Due Date", readonly=True)
+ invoice_day_to_due = fields.Integer(related="invoice_id.invoice_day_to_due", string="Day to Due", readonly=True)
+ new_invoice_day_to_due = fields.Integer(related="invoice_id.new_invoice_day_to_due", string="New Day Due", readonly=True)
+ date_terima_tukar_faktur = fields.Date(related="invoice_id.date_terima_tukar_faktur", string="Tanggal Terima Tukar Faktur", readonly=True)
+ amount_total = fields.Monetary(related="invoice_id.amount_total", string="Total", readonly=True)
+ currency_id = fields.Many2one(related="invoice_id.currency_id", readonly=True) \ No newline at end of file
diff --git a/indoteknik_custom/models/account_move_due_extension.py b/indoteknik_custom/models/account_move_due_extension.py
index d354e3e3..40059bd9 100644
--- a/indoteknik_custom/models/account_move_due_extension.py
+++ b/indoteknik_custom/models/account_move_due_extension.py
@@ -14,6 +14,16 @@ 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)
order_id = fields.Many2one('sale.order', string="SO", readonly=True)
+ amount_total = fields.Monetary(
+ string="Amount Total SO",
+ compute="_compute_amount_total",
+ readonly=True
+ )
+ currency_id = fields.Many2one(
+ 'res.currency',
+ related="order_id.currency_id",
+ readonly=True
+ )
invoice_id = fields.Many2one('account.move', related='due_line.invoice_id', string='Invoice', readonly=False)
due_line = fields.One2many('due.extension.line', 'due_id', string='Due Extension Lines', auto_join=True)
old_due = fields.Date(string="Old Due")
@@ -34,6 +44,11 @@ class DueExtension(models.Model):
approve_by = fields.Many2one('res.users', string="Approve By", readonly=True)
date_approve = fields.Datetime(string="Date Approve", readonly=True)
+ @api.depends('order_id')
+ def _compute_amount_total(self):
+ for rec in self:
+ rec.amount_total = rec.order_id.amount_total if rec.order_id else 0.0
+
def _compute_counter(self):
for due in self:
due.counter = due.partner_id.counter
diff --git a/indoteknik_custom/models/approval_payment_term.py b/indoteknik_custom/models/approval_payment_term.py
index 4cf9a4c8..8618856a 100644
--- a/indoteknik_custom/models/approval_payment_term.py
+++ b/indoteknik_custom/models/approval_payment_term.py
@@ -72,20 +72,20 @@ class ApprovalPaymentTerm(models.Model):
if self.env.user.id != 688:
return
+ tracked_fields = {"blocking_stage", "warning_stage", "property_payment_term_id"}
+
for rec in self:
changes = []
old_values = old_values_dict.get(rec.id, {})
for field_name, new_value in vals.items():
- if field_name not in rec._fields or field_name == 'change_log_688':
+ if field_name not in tracked_fields:
continue
field = rec._fields[field_name]
old_value = old_values.get(field_name)
+ field_label = field.string # label user-friendly
- field_label = field.string # Ambil label user-friendly
-
- # Relational field
if field.type == 'many2one':
old_id = old_value[0] if old_value else False
is_different = old_id != new_value
diff --git a/indoteknik_custom/models/product_template.py b/indoteknik_custom/models/product_template.py
index f59bea6b..13e99707 100755
--- a/indoteknik_custom/models/product_template.py
+++ b/indoteknik_custom/models/product_template.py
@@ -1365,4 +1365,5 @@ class ImageCarousel(models.Model):
_order = 'product_id, id'
product_id = fields.Many2one('product.template', string='Product', required=True, ondelete='cascade', index=True, copy=False)
+ sequence = fields.Integer("Sequence", default=10)
image = fields.Binary(string='Image')
diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py
index 103a9131..50913a80 100755
--- a/indoteknik_custom/models/purchase_order.py
+++ b/indoteknik_custom/models/purchase_order.py
@@ -103,6 +103,11 @@ class PurchaseOrder(models.Model):
string="BU Related Count",
compute='_compute_bu_related_count'
)
+
+ bills_related_count = fields.Integer(
+ string="Bills DP & Pelunasan",
+ compute="_compute_bills_related_count"
+ )
manufacturing_id = fields.Many2one('mrp.production', string='Manufacturing Orders')
complete_bu_in_count = fields.Integer(
@@ -260,6 +265,33 @@ class PurchaseOrder(models.Model):
'target': 'current',
}
+ def action_view_bills(self):
+ self.ensure_one()
+
+ bill_ids = []
+ if self.bills_dp_id:
+ bill_ids.append(self.bills_dp_id.id)
+ if self.bills_pelunasan_id:
+ bill_ids.append(self.bills_pelunasan_id.id)
+
+ return {
+ 'name': 'Bills (DP & Pelunasan)',
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'account.move',
+ 'view_mode': 'tree,form',
+ 'target': 'current',
+ 'domain': [('id', 'in', bill_ids)],
+ }
+
+ def _compute_bills_related_count(self):
+ for order in self:
+ count = 0
+ if order.bills_dp_id:
+ count += 1
+ if order.bills_pelunasan_id:
+ count += 1
+ order.bills_related_count = count
+
# cek payment term
def _check_payment_term(self):
_logger.info("Check Payment Term Terpanggil")
diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py
index e5e382a1..148a3fd0 100644
--- a/indoteknik_custom/models/res_partner.py
+++ b/indoteknik_custom/models/res_partner.py
@@ -29,7 +29,15 @@ class ResPartner(models.Model):
reminder_invoices = fields.Boolean(
string='Reminder Invoice?',
- help='Centang jika kontak ini harus menerima email pengingat invoice.', default=False
+ help='Centang jika kontak ini harus menerima email pengingat invoice.', default=False, tracking=True
+ )
+ dont_send_reminder_inv_parent = fields.Boolean(
+ string='Dont Send Reminder Invoices Parent?',
+ help='Centang jika kontak utama tidak perlu menerima sent Reminder Invoices Otomatis', default=False, tracking=True
+ )
+ dont_send_reminder_inv_all = fields.Boolean(
+ string='Dont Send Reminder Invoices to All?',
+ help='Centang jika semua kontak utama dan child tidak menerima sent Reminder Invoices', default=False, tracking=True
)
# informasi perusahaan
@@ -275,7 +283,7 @@ class ResPartner(models.Model):
('email', '=', record.email)
], limit=1)
- if existing_partner:
+ if existing_partner and not record.parent_id:
raise ValidationError(f"Nama '{record.name}' dengan email '{record.email}' sudah digunakan oleh partner lain!")
@api.constrains('npwp')
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index 0acfa0b0..ffb53dce 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -376,6 +376,83 @@ class SaleOrder(models.Model):
compute='_compute_advance_payment_moves',
store=False
)
+ reserved_percent = fields.Float(
+ string="Reserved %", digits=(16, 2),
+ compute="_compute_reserved_delivered_pie", store=False
+ )
+ delivered_percent = fields.Float(
+ string="Delivered %", digits=(16, 2),
+ compute="_compute_reserved_delivered_pie", store=False
+ )
+ unreserved_percent = fields.Float(
+ string="Unreserved %", digits=(16, 2),
+ compute="_compute_reserved_delivered_pie", store=False
+ )
+ payment_state_custom = fields.Selection([
+ ('unpaid', 'Unpaid'),
+ ('partial', 'Partially Paid'),
+ ('paid', 'Full Paid'),
+ ('no_invoice', 'No Invoice'),
+ ], string="Payment Status Invoice", compute="_compute_payment_state_custom", store=False)
+
+ @api.depends('invoice_ids.payment_state', 'invoice_ids.amount_total', 'invoice_ids.amount_residual')
+ def _compute_payment_state_custom(self):
+ for order in self:
+ invoices = order.invoice_ids.filtered(lambda inv: inv.state != 'cancel')
+ total = sum(invoices.mapped('amount_total'))
+ residual = sum(invoices.mapped('amount_residual'))
+
+ if not invoices or total == 0:
+ order.payment_state_custom = 'no_invoice'
+ continue
+
+ paid = total - residual
+ percent_paid = (paid / total) * 100 if total > 0 else 0.0
+
+ if percent_paid == 100:
+ order.payment_state_custom = 'paid'
+ elif percent_paid == 0:
+ order.payment_state_custom = 'unpaid'
+ else:
+ order.payment_state_custom = 'partial'
+
+ @api.depends(
+ 'order_line.move_ids.state',
+ 'order_line.move_ids.reserved_availability',
+ 'order_line.move_ids.quantity_done',
+ 'order_line.move_ids.picking_type_id'
+ )
+ def _compute_reserved_delivered_pie(self):
+ for order in self:
+ order_qty = sum(order.order_line.mapped('product_uom_qty')) or 0.0
+ reserved_qty = delivered_qty = 0.0
+
+ if order_qty > 0:
+ # Kumpulin semua moves dari order
+ all_moves = order.order_line.mapped('move_ids')
+
+ for move in all_moves:
+ # --- CASE 1: Move belum selesai ---
+ if move.state not in ('done', 'cancel'):
+ # Reserved qty hanya dari move yang belum selesai
+ 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
+ order.reserved_percent = min((reserved_qty / order_qty) * 100, 100) if order_qty else 0
+ order.delivered_percent = min((delivered_qty / order_qty) * 100, 100) if order_qty else 0
+ order.unreserved_percent = max(100 - order.reserved_percent - order.delivered_percent, 0)
def _has_ccm(self):
if self.id:
diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py
index 64b9f9bc..47a24264 100644
--- a/indoteknik_custom/models/sale_order_line.py
+++ b/indoteknik_custom/models/sale_order_line.py
@@ -54,7 +54,43 @@ class SaleOrderLine(models.Model):
desc_updatable = fields.Boolean(string='desc boolean', default=True, compute='_get_desc_updatable')
is_has_disc = fields.Boolean('Flash Sale', default=False)
-
+ reserved_percent = fields.Float(string="Reserved %", digits=(16, 2), compute="_compute_reserved_delivered_pie", store=False)
+ delivered_percent = fields.Float(string="Delivered %", digits=(16, 2), compute="_compute_reserved_delivered_pie", store=False)
+ unreserved_percent = fields.Float(string="Unreserved %", digits=(16, 2), compute="_compute_reserved_delivered_pie", store=False)
+
+ @api.depends(
+ 'move_ids.state',
+ 'move_ids.reserved_availability',
+ 'move_ids.quantity_done',
+ 'move_ids.picking_type_id'
+ )
+ def _compute_reserved_delivered_pie(self):
+ for line in self:
+ order_qty = line.product_uom_qty or 0.0
+ reserved_qty = delivered_qty = 0.0
+
+ 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)
def _get_outgoing_incoming_moves(self):
outgoing_moves = self.env['stock.move']
diff --git a/indoteknik_custom/models/solr/promotion_program_line.py b/indoteknik_custom/models/solr/promotion_program_line.py
index 64ad4209..b1b2f88e 100644
--- a/indoteknik_custom/models/solr/promotion_program_line.py
+++ b/indoteknik_custom/models/solr/promotion_program_line.py
@@ -2,6 +2,10 @@ from odoo import models, api
from typing import Type
import pysolr
import json
+import logging
+from odoo.exceptions import UserError
+
+_logger = logging.getLogger(__name__)
class PromotionProgramLine(models.Model):
_inherit = 'promotion.program.line'
@@ -20,58 +24,64 @@ class PromotionProgramLine(models.Model):
def _sync_to_solr(self):
solr_model = self.env['apache.solr']
-
for rec in self:
- document = solr_model.get_doc(self._solr_schema, rec.id)
-
- products = [{
- 'product_id': x.product_id.id,
- 'qty': x.qty,
- 'qty_sold': x.product_id.qty_sold
- } for x in rec.product_ids]
-
- free_products = [{
- 'product_id': x.product_id.id,
- 'qty': x.qty
- } for x in rec.free_product_ids]
-
- promotion_type = rec._res_promotion_type()
-
- # Gathering all categories
- category_names = [category.name for category in rec.product_ids.product_id.public_categ_ids]
-
- # Set sequence_i to None if rec.sequence is 0
- sequence_value = None if rec.sequence == 0 else rec.sequence
-
- document.update({
- 'id': rec.id,
- 'program_id_i': rec.program_id.id or 0,
- 'name_s': rec.name,
- 'type_value_s': promotion_type['value'],
- 'type_label_s': promotion_type['label'],
- 'package_limit_i': rec.package_limit,
- 'package_limit_user_i': rec.package_limit_user,
- 'package_limit_trx_i': rec.package_limit_trx,
- 'price_f': rec.price,
- 'price_tier_1_f': rec.price_tier_1,
- 'price_tier_2_f': rec.price_tier_2,
- 'price_tier_3_f': rec.price_tier_3,
- 'price_tier_4_f': rec.price_tier_4,
- 'price_tier_5_f': rec.price_tier_5,
- 'sequence_i': sequence_value,
- 'product_ids': [x.product_id.id for x in rec.product_ids],
- 'products_s': json.dumps(products),
- 'free_product_ids': [x.product_id.id for x in rec.free_product_ids],
- 'free_products_s': json.dumps(free_products),
- 'total_qty_i': sum([x.qty for x in rec.product_ids] + [x.qty for x in rec.free_product_ids]),
- 'total_qty_sold_f': [x.product_id.qty_sold for x in rec.product_ids],
- 'active_b': rec.active,
- "manufacture_name_s": rec.product_ids.product_id.x_manufacture.x_name or '',
- "category_name": category_names,
- })
-
- self.solr().add([document])
- self.solr().commit()
+ try:
+ document = solr_model.get_doc(self._solr_schema, rec.id)
+
+ products = [{
+ 'product_id': x.product_id.id,
+ 'qty': x.qty,
+ 'qty_sold': x.product_id.qty_sold
+ } for x in rec.product_ids]
+
+ free_products = [{
+ 'product_id': x.product_id.id,
+ 'qty': x.qty
+ } for x in rec.free_product_ids]
+
+ promotion_type = rec._res_promotion_type()
+
+ category_names = [category.name for category in rec.product_ids.product_id.public_categ_ids]
+ sequence_value = None if rec.sequence == 0 else rec.sequence
+
+ document.update({
+ 'id': rec.id,
+ 'program_id_i': rec.program_id.id or 0,
+ 'name_s': rec.name,
+ 'type_value_s': promotion_type['value'],
+ 'type_label_s': promotion_type['label'],
+ 'package_limit_i': rec.package_limit,
+ 'package_limit_user_i': rec.package_limit_user,
+ 'package_limit_trx_i': rec.package_limit_trx,
+ 'price_f': rec.price,
+ 'price_tier_1_f': rec.price_tier_1,
+ 'price_tier_2_f': rec.price_tier_2,
+ 'price_tier_3_f': rec.price_tier_3,
+ 'price_tier_4_f': rec.price_tier_4,
+ 'price_tier_5_f': rec.price_tier_5,
+ 'sequence_i': sequence_value,
+ 'product_ids': [x.product_id.id for x in rec.product_ids],
+ 'products_s': json.dumps(products),
+ 'free_product_ids': [x.product_id.id for x in rec.free_product_ids],
+ 'free_products_s': json.dumps(free_products),
+ 'total_qty_i': sum([x.qty for x in rec.product_ids] + [x.qty for x in rec.free_product_ids]),
+ 'total_qty_sold_f': sum([x.product_id.qty_sold for x in rec.product_ids]),
+ 'active_b': rec.active,
+ "manufacture_name_s": rec.product_ids[0].product_id.x_manufacture.x_name or '',
+ "category_name": category_names,
+ })
+
+ self.solr().add([document])
+ self.solr().commit()
+
+ except Exception as e:
+ _logger.error(
+ "Failed to sync record %s (ID: %s) to Solr. Error: %s",
+ rec._name, rec.id, str(e),
+ exc_info=True # biar stack trace keluar
+ )
+ # opsional -> kalau mau hard fail:
+ raise UserError(_("Sync to Solr failed for record %s: %s") % (rec.name, str(e)))
@api.model
def create(self, vals):
diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py
index d63c5d4c..a48e0ed1 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):
@@ -1330,18 +1330,20 @@ class StockPicking(models.Model):
current_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.date_reserved = current_time
+
# Validate Qty Demand Can't higher than Qty Product
- for move_line in self.move_line_ids_without_package:
- purchase_line = move_line.move_id.purchase_line_id
- if purchase_line:
- if purchase_line.product_uom_qty < move_line.product_uom_qty:
- raise UserError(
- _("Quantity demand (%s) tidak bisa lebih besar dari qty product (%s) untuk produk %s") % (
- move_line.product_uom_qty,
- purchase_line.product_uom_qty,
- move_line.product_id.display_name
+ if self.location_dest_id.id == 58 and 'BU/INPUT/' in self.name:
+ for move in self.move_ids_without_package:
+ purchase_line = move.purchase_line_id
+ if purchase_line:
+ if purchase_line.product_qty < move.quantity_done:
+ raise UserError(
+ _("Quantity demand (%s) tidak bisa lebih besar dari qty product (%s) untuk produk %s") % (
+ move.quantity_done,
+ purchase_line.product_qty,
+ move.product_id.display_name
+ )
)
- )
self.validation_minus_onhand_quantity()
self.responsible = self.env.user.id
@@ -2070,6 +2072,8 @@ class CheckProduct(models.Model):
_name = 'check.product'
_description = 'Check Product'
_order = 'picking_id, id'
+ _inherit = ['barcodes.barcode_events_mixin']
+
picking_id = fields.Many2one(
'stock.picking',
diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py
index 4b03d4b0..699ee670 100644
--- a/indoteknik_custom/models/tukar_guling.py
+++ b/indoteknik_custom/models/tukar_guling.py
@@ -109,9 +109,6 @@ class TukarGuling(models.Model):
('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))
@@ -420,8 +417,8 @@ class TukarGuling(models.Model):
if not self.return_type:
raise UserError("Return Type harus diisi!")
- if operasi == 30 and self.operations.linked_manual_bu_out.state == 'done':
- raise UserError("❌ Tidak bisa retur BU/PICK karena BU/OUT sudah done")
+ # if operasi == 30 and self.operations.linked_manual_bu_out.state == 'done':
+ # raise UserError("❌ Tidak bisa retur BU/PICK karena BU/OUT sudah done")
if operasi == 30 and pp == 'tukar_guling':
raise UserError("❌ BU/PICK tidak boleh di retur tukar guling")
# else:
diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv
index e20709e4..98ef5a8c 100755
--- a/indoteknik_custom/security/ir.model.access.csv
+++ b/indoteknik_custom/security/ir.model.access.csv
@@ -198,4 +198,6 @@ 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 \ No newline at end of file
+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 \ No newline at end of file
diff --git a/indoteknik_custom/static/src/js/check_product_barcode.js b/indoteknik_custom/static/src/js/check_product_barcode.js
new file mode 100644
index 00000000..2fddc616
--- /dev/null
+++ b/indoteknik_custom/static/src/js/check_product_barcode.js
@@ -0,0 +1,76 @@
+odoo.define('indoteknik_custom.buffered_scanner', function (require) {
+ 'use strict';
+ console.log('✅ Indoteknik_Custom JS Loaded');
+
+ var GAP_MS = 120;
+ var MIN_LEN = 3;
+ var COMMIT_TIMEOUT = 180;
+
+ var buffer = '';
+ var last = 0;
+ var timer = null;
+
+ function reset() {
+ buffer = '';
+ last = 0;
+ if (timer) { clearTimeout(timer); timer = null; }
+ }
+
+ function isCodeProduct(el) {
+ return el && el instanceof HTMLInputElement && el.name === 'code_product';
+ }
+
+ function commit() {
+ var el = document.activeElement;
+ if (!isCodeProduct(el)) { reset(); return; }
+ if (buffer.length >= MIN_LEN) {
+ el.value = buffer;
+ el.dispatchEvent(new Event('input', { bubbles: true }));
+ el.dispatchEvent(new Event('change', { bubbles: true }));
+ }
+ reset();
+ }
+
+ document.addEventListener('keydown', function (e) {
+ var el = document.activeElement;
+ if (!isCodeProduct(el)) return;
+
+ var key = e.key;
+
+ // ENTER mengakhiri scan
+ if (key === 'Enter') {
+ e.preventDefault();
+ commit();
+ return;
+ }
+
+ // abaikan tombol kontrol (Shift, Tab, Arrow, Backspace, dll.)
+ if (key.length !== 1) return;
+
+ var now = performance.now();
+ var gap = now - (last || now);
+ last = now;
+
+ if (!buffer || gap <= GAP_MS) {
+ // bagian dari "scan cepat" → tangani sendiri (hindari karakter hilang)
+ e.preventDefault();
+ buffer += key;
+
+ if (timer) clearTimeout(timer);
+ timer = setTimeout(function () {
+ // auto-commit jika scanner tidak mengirim Enter
+ commit();
+ }, COMMIT_TIMEOUT);
+ } else {
+ // jeda besar → kemungkinan manual. Kalau mau benar-benar melarang,
+ // buka komentar 2 baris di bawah.
+ // e.preventDefault();
+ // e.stopPropagation();
+ reset(); // keluar dari mode buffer agar manual normal
+ }
+ }, true);
+
+ document.addEventListener('focusin', function (e) {
+ if (isCodeProduct(e.target)) reset();
+ }, true);
+}); \ No newline at end of file
diff --git a/indoteknik_custom/views/account_move.xml b/indoteknik_custom/views/account_move.xml
index e5d1cf8a..b399d4c9 100644
--- a/indoteknik_custom/views/account_move.xml
+++ b/indoteknik_custom/views/account_move.xml
@@ -68,6 +68,14 @@
<field name="nomor_kwitansi"/>
<field name="down_payment"/>
</field>
+ <field name="website_id" position="after">
+ <field name="customer_promise_date"/>
+ <button name="action_sync_promise_date"
+ string="Sync Janji Bayar ke Invoice Lain"
+ type="object"
+ class="btn-primary"
+ help="Sync Janji Bayar Customer ke Invoices dengan jumlah Due Date yang sama"/>
+ </field>
<field name="to_check" position="after">
<field name="already_paid"/>
<field name="so_shipping_paid_by"/>
@@ -195,5 +203,37 @@
<field name="state">code</field>
<field name="code">action = records.export_faktur_to_xml()</field>
</record>
+
+ <record id="view_sync_promise_date_wizard_form" model="ir.ui.view">
+ <field name="name">sync.promise.date.wizard.form</field>
+ <field name="model">sync.promise.date.wizard</field>
+ <field name="arch" type="xml">
+ <form string="Sync Janji Bayar">
+ <group>
+ <field name="invoice_id" readonly="1"/>
+ <field name="promise_date" readonly="1"/>
+ </group>
+ <field name="line_ids">
+ <tree create="false" delete="false" editable="bottom">
+ <field name="sync_check"/>
+ <field name="invoice_name" readonly="1"/>
+ <field name="invoice_date_due" readonly="1"/>
+ <field name="invoice_day_to_due" readonly="1"/>
+ <field name="new_invoice_day_to_due" readonly="1"/>
+ <field name="date_terima_tukar_faktur" readonly="1"/>
+ <field name="amount_total" readonly="1"/>
+ </tree>
+ </field>
+
+ <button name="action_check_all" string="Check All" type="object" class="btn-secondary"/>
+ <button name="action_uncheck_all" string="Uncheck All" type="object" class="btn-secondary"/>
+
+ <footer>
+ <button name="action_confirm" string="Konfirmasi Sync" type="object" class="btn-primary"/>
+ <button string="Batal" class="btn-secondary" special="cancel"/>
+ </footer>
+ </form>
+ </field>
+ </record>
</data>
</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/account_move_views.xml b/indoteknik_custom/views/account_move_views.xml
index 0fd7c9cd..7c1f8913 100644
--- a/indoteknik_custom/views/account_move_views.xml
+++ b/indoteknik_custom/views/account_move_views.xml
@@ -33,7 +33,7 @@
<field name="efaktur_id"/>
<field name="reference"/>
<field name="total_amt"/>
- <field name="open_amt"/>
+ <field name="open_amt" sum="Grand Total Open Amount"/>
</tree>
</field>
</record>
@@ -68,6 +68,7 @@
<field name="partner_id" readonly="1"/>
<field name="day_extension" attrs="{'readonly': [('is_approve', '=', True)]}"/>
<field name="order_id" readonly="1"/>
+ <field name="amount_total" readonly="1"/>
</group>
<group>
<field name="is_approve" readonly="1"/>
diff --git a/indoteknik_custom/views/approval_payment_term.xml b/indoteknik_custom/views/approval_payment_term.xml
index f7c24737..5c130f3f 100644
--- a/indoteknik_custom/views/approval_payment_term.xml
+++ b/indoteknik_custom/views/approval_payment_term.xml
@@ -86,6 +86,17 @@
<field name="view_mode">tree,form</field>
</record>
+ <record id="approval_payment_term_search" model="ir.ui.view">
+ <field name="name">approval.payment.term.search</field>
+ <field name="model">approval.payment.term</field>
+ <field name="arch" type="xml">
+ <search>
+ <field name="number" string="Document Number"/>
+ <field name="partner_id" string="Partner"/>
+ </search>
+ </field>
+ </record>
+
<menuitem id="menu_approval_payment_term" name="Approval Payment Term"
parent="account.menu_finance_receivables"
action="approval_payment_term_action"
diff --git a/indoteknik_custom/views/assets.xml b/indoteknik_custom/views/assets.xml
new file mode 100644
index 00000000..4475004e
--- /dev/null
+++ b/indoteknik_custom/views/assets.xml
@@ -0,0 +1,7 @@
+<odoo>
+ <template id="indoteknik_assets_backend" inherit_id="web.assets_backend" name="Indoteknik Custom Backend Assets">
+ <xpath expr="." position="inside">
+ <script type="text/javascript" src="/indoteknik_custom/static/src/js/check_product_barcode.js"/>
+ </xpath>
+ </template>
+</odoo>
diff --git a/indoteknik_custom/views/customer_commision.xml b/indoteknik_custom/views/customer_commision.xml
index 514e6284..7c72d86a 100644
--- a/indoteknik_custom/views/customer_commision.xml
+++ b/indoteknik_custom/views/customer_commision.xml
@@ -164,6 +164,7 @@
<field name="priority" eval="15"/>
<field name="arch" type="xml">
<search string="Search Customer Commision">
+ <field name="number" string="Document Number"/>
<field name="partner_ids"/>
<group expand="0" string="Group By">
<filter string="Partner" name="group_partner"
diff --git a/indoteknik_custom/views/dunning_run.xml b/indoteknik_custom/views/dunning_run.xml
index 2117a7bb..210f7917 100644
--- a/indoteknik_custom/views/dunning_run.xml
+++ b/indoteknik_custom/views/dunning_run.xml
@@ -29,7 +29,7 @@
<field name="date_invoice"/>
<field name="efaktur_id"/>
<field name="reference"/>
- <field name="total_amt"/>
+ <field name="total_amt" sum="Grand Total Amount"/>
<field name="open_amt"/>
<field name="due_date"/>
</tree>
diff --git a/indoteknik_custom/views/mail_template_invoice_reminder.xml b/indoteknik_custom/views/mail_template_invoice_reminder.xml
index 8450be28..13c02a08 100644
--- a/indoteknik_custom/views/mail_template_invoice_reminder.xml
+++ b/indoteknik_custom/views/mail_template_invoice_reminder.xml
@@ -8,45 +8,50 @@
<field name="email_from">finance@indoteknik.co.id</field>
<field name="email_to"></field>
<field name="body_html" type="html">
- <div>
+ <div style="font-family:Arial, sans-serif; font-size:13px; color:#333;">
<p><b>Dear ${object.name},</b></p>
<p>${days_to_due_message}</p>
- <table border="1" cellpadding="4" cellspacing="0" style="border-collapse: collapse; font-size: 12px">
+ <table cellpadding="6" cellspacing="0" width="100%"
+ style="border-collapse:collapse; font-size:12px; border:1px solid #ddd;">
<thead>
- <tr style="background-color: #f2f2f2;" align="left">
- <th>Customer</th>
- <th>No. PO</th>
- <th>Invoice Number</th>
- <th>Invoice Date</th>
- <th>Due Date</th>
- <th>Amount</th>
- <th>Term</th>
- <th>Days To Due</th>
+ <tr style="background-color:#f2f2f2; text-align:left;">
+ <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>
+ <th style="border:1px solid #ddd;">Invoice Date</th>
+ <th style="border:1px solid #ddd;">Due Date</th>
+ <th style="border:1px solid #ddd;">Amount</th>
+ <th style="border:1px solid #ddd;">Term</th>
+ <th style="border:1px solid #ddd;">Days To Due</th>
</tr>
</thead>
<tbody>
+ <!-- baris invoice akan diinject dari Python -->
</tbody>
</table>
<p>${closing_message}</p>
+ ${limit_info_html}
<br/>
<p>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;"></img><br/>
- <a href="https://wa.me/6285716970374" target="_blank">+62-857-1697-0374</a> |
- <a href="mailto:finance@indoteknik.co.id">finance@indoteknik.co.id</a>
- </b></p>
- <p><i>Email ini dikirim secara otomatis. Abaikan jika pembayaran telah dilakukan.</i></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>
+ <p style="font-size:11px; color:#777;">
+ <i>Email ini dikirim secara otomatis. Abaikan jika pembayaran telah dilakukan.</i>
+ </p>
</div>
</field>
<field name="auto_delete" eval="True"/>
diff --git a/indoteknik_custom/views/product_template.xml b/indoteknik_custom/views/product_template.xml
index 8f9d1190..177449f4 100755
--- a/indoteknik_custom/views/product_template.xml
+++ b/indoteknik_custom/views/product_template.xml
@@ -66,6 +66,7 @@
<field name="model">image.carousel</field>
<field name="arch" type="xml">
<tree editable="bottom">
+ <field name="sequence" widget="handle"/>
<field name="image" widget="image" width="80"/>
</tree>
</field>
diff --git a/indoteknik_custom/views/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml
index 15cdc788..821f3295 100755
--- a/indoteknik_custom/views/purchase_order.xml
+++ b/indoteknik_custom/views/purchase_order.xml
@@ -14,6 +14,16 @@
attrs="{'invisible': ['|', ('sale_order_id', '=', False), ('state', 'not in', ['draft'])]}"
/>
</div>
+ <xpath expr="//button[@name='action_view_invoice']" position="after">
+ <button type="object"
+ name="action_view_related_bu"
+ class="oe_stat_button"
+ icon="fa-truck"
+ attrs="{'invisible': [('state', 'in', ['draft', 'sent'])]}">
+ <field name="bu_related_count" widget="statinfo" string="BU Related"/>
+ </button>
+ <field name="picking_count" invisible="1"/>
+ </xpath>
<xpath expr="//button[@name='action_view_invoice']" position="before">
<field name="is_cab_visible" invisible="1"/>
<button type="object"
@@ -21,21 +31,19 @@
class="oe_stat_button"
icon="fa-book"
attrs="{'invisible': [('is_cab_visible', '=', False)]}"
- style="width: 200px;">
+ >
<field name="move_id" widget="statinfo" string="Journal Uang Muka"/>
<span class="o_stat_text">
<t t-esc="record.move_id.name"/>
</span>
</button>
- <button type="object"
- name="action_view_related_bu"
- class="oe_stat_button"
- icon="fa-truck"
- style="width: 200px;"
- attrs="{'invisible': [('state', 'in', ['draft', 'sent'])]}">
- <field name="bu_related_count" widget="statinfo" string="BU Related"/>
+ <button name="action_view_bills"
+ type="object"
+ icon="fa-pencil-square-o"
+ attrs="{'invisible': [
+ ('bills_related_count', '=', 0)]}">
+ <field string="Bills DP &amp; Pelunasan" name="bills_related_count" widget="statinfo"/>
</button>
- <field name="picking_count" invisible="1"/>
</xpath>
<button id="draft_confirm" position="after">
<button name="po_approve"
diff --git a/indoteknik_custom/views/res_partner.xml b/indoteknik_custom/views/res_partner.xml
index b081f6f2..ca1a36de 100644
--- a/indoteknik_custom/views/res_partner.xml
+++ b/indoteknik_custom/views/res_partner.xml
@@ -87,6 +87,7 @@
<field name="use_so_approval" attrs="{'invisible': [('parent_id', '!=', False), ('company_type', '!=', 'company')]}" />
<field name="use_only_ready_stock" attrs="{'invisible': [('parent_id', '!=', False), ('company_type', '!=', 'company')]}" />
<field name="web_role" attrs="{'invisible': ['|', ('parent_id', '=', False), ('company_type', '=', 'company')]}" />
+ <field name="reminder_invoices" attrs="{'invisible': ['|', ('parent_id', '=', False), ('company_type', '=', 'company')]}"/>
</field>
<xpath expr="//field[@name='child_ids']/form//field[@name='name']" position="before">
<field name="parent_id" invisible="1" />
@@ -215,6 +216,10 @@
<field name="dokumen_laporan_keuangan" />
<field name="dokumen_ktp_dirut" />
</group>
+ <group string="Reminder Invoices" attrs="{'invisible': [('parent_id', '!=', False), ('company_type', '!=', 'company')]}">
+ <field name="dont_send_reminder_inv_parent"/>
+ <field name="dont_send_reminder_inv_all" />
+ </group>
</page>
<page string="Aging Info">
<group string="Aging Info">
diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml
index 868bce7b..a1a5e0cd 100755
--- a/indoteknik_custom/views/sale_order.xml
+++ b/indoteknik_custom/views/sale_order.xml
@@ -290,6 +290,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="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"/>
@@ -477,6 +480,13 @@
<field name="date_kirim_ril"/>
<field name="date_driver_departure"/>
<field name="date_driver_arrival"/>
+ <field name="payment_state_custom" widget="badge"
+ decoration-danger="payment_state_custom == 'unpaid'"
+ decoration-success="payment_state_custom == 'paid'"
+ decoration-warning="payment_state_custom == 'partial'" optional="hide"/>
+ <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="payment_type" optional="hide"/>
<field name="payment_status" optional="hide"/>
<field name="pareto_status" optional="hide"/>
diff --git a/indoteknik_custom/views/user_pengajuan_tempo.xml b/indoteknik_custom/views/user_pengajuan_tempo.xml
index f9dca4ca..ad35d7fa 100644
--- a/indoteknik_custom/views/user_pengajuan_tempo.xml
+++ b/indoteknik_custom/views/user_pengajuan_tempo.xml
@@ -271,7 +271,7 @@
<field name="subject">Pengajuan Tempo Harus di Periksa!</field>
<field name="email_from">"Indoteknik.com" &lt;noreply@indoteknik.com&gt;</field>
<field name="reply_to">sales@indoteknik.com</field>
- <field name="email_to">finance@indoteknik.co.id, stephan@indoteknik.co.id, sapiabon768@gmail.com</field>
+ <field name="email_to">finance@indoteknik.co.id, stephan@indoteknik.co.id</field>
<!-- <field name="email_to">sapiabon768@gmail.com</field>-->
<field name="body_html" type="html">
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Inter, Helvetica, Verdana, Arial,sans-serif; line-height: 24px; color: #454748; width: 100%; border-collapse:separate;">