summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorit-fixcomart <it@fixcomart.co.id>2025-08-30 09:25:35 +0700
committerit-fixcomart <it@fixcomart.co.id>2025-08-30 09:25:35 +0700
commit5ef97855847141eaa705be36a2aae17cdf928258 (patch)
tree449d86549d69024e92032b2246f8481bf081302a
parent0298605049e29ef436a5e6984b743f89fed712b3 (diff)
parent63426e9de8700daff0c0f7cf0389d2be55e982fb (diff)
<hafid> fix conflict merge
-rw-r--r--indoteknik_api/controllers/api_v1/partner.py69
-rw-r--r--indoteknik_api/controllers/api_v1/sale_order.py68
-rw-r--r--indoteknik_api/controllers/api_v1/user.py31
-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
-rw-r--r--indoteknik_custom/models/mrp_production.py16
-rwxr-xr-xindoteknik_custom/models/product_template.py1
-rwxr-xr-xindoteknik_custom/models/purchase_order.py32
-rw-r--r--indoteknik_custom/models/res_partner.py163
-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.py30
-rw-r--r--indoteknik_custom/models/stock_picking_return.py7
-rw-r--r--indoteknik_custom/models/tukar_guling.py4
-rw-r--r--indoteknik_custom/models/user_company_request.py10
-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
32 files changed, 932 insertions, 205 deletions
diff --git a/indoteknik_api/controllers/api_v1/partner.py b/indoteknik_api/controllers/api_v1/partner.py
index acec19f7..b1d8d5f3 100644
--- a/indoteknik_api/controllers/api_v1/partner.py
+++ b/indoteknik_api/controllers/api_v1/partner.py
@@ -65,44 +65,61 @@ class Partner(controller.Controller):
@controller.Controller.must_authorized()
def write_partner_address_by_id(self, id, **kw):
headers = {
- 'Access-Control-Allow-Origin': '*',
- 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
- 'Access-Control-Allow-Headers': '*'
- }
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
+ 'Access-Control-Allow-Headers': '*'
+ }
if request.httprequest.method == 'OPTIONS':
return Response(status=200, headers=headers)
+
try:
params = self.get_request_params(request.jsonrequest, {
- 'id': ['required', 'number'],
- 'type': ['default:other'],
- 'name': ['required'],
- 'email': ['required'],
- 'mobile': ['required'],
- 'phone': [''],
- 'street': ['required'],
- 'state_id': ['required', 'alias:state_id'],
- 'city_id': ['required', 'alias:kota_id'],
- 'district_id': ['alias:kecamatan_id'],
- 'sub_district_id': ['alias:kelurahan_id', 'exclude_if_null'],
- 'zip': ['required'],
- 'longtitude': '',
- 'latitude': '',
- 'address_map': [],
- 'alamat_lengkap_text': []
- })
+ 'id': ['required', 'number'],
+ 'type': ['default:other'],
+ 'name': ['required'],
+ 'email': ['required'],
+ 'mobile': ['required'],
+ 'phone': [''],
+ 'street': ['required'],
+ 'state_id': ['required', 'alias:state_id'],
+ 'city_id': ['required', 'alias:kota_id'],
+ 'district_id': ['alias:kecamatan_id'],
+ 'sub_district_id': ['alias:kelurahan_id', 'exclude_if_null'],
+ 'zip': ['required'],
+ 'longtitude': '',
+ 'latitude': '',
+ 'address_map': [],
+
+ 'alamat_lengkap_text': []
+ })
if not params['valid']:
- return {'headers' : headers,'code': 400, 'description': params}
+ return {'headers': headers, 'code': 400, 'description': params}
partner = request.env['res.partner'].sudo().search([('id', '=', id)], limit=1)
if not partner:
- return {'headers' : headers,'code': 404, 'description': 'User not found'}
+ return {'headers': headers, 'code': 404, 'description': 'User not found'}
+ vals = dict(params['value'])
+ vals.pop('id', None)
+ use_pin = bool(request.jsonrequest.get('use_pin'))
+
+ if not use_pin:
+ vals.pop('address_map', None)
+ vals.pop('latitude', None)
+ vals.pop('longtitude', None)
+ else:
+ lat = vals.get('latitude')
+ lng = vals.get('longtitude')
+ if not lat or not lng or float(lat) == 0.0 or float(lng) == 0.0:
+ vals.pop('latitude', None)
+ vals.pop('longtitude', None)
+
+ partner.write(vals)
- partner.write(params['value'])
- return {'id': partner.id, 'headers' : headers}
+ return {'id': partner.id, 'headers': headers}
except Exception as e:
- return {'headers' : headers,'code': 500, 'description': f'Internal Error: {str(e)}'}
+ return {'headers': headers, 'code': 500, 'description': f'Internal Error: {str(e)}'}
@http.route(prefix + 'partner/address', auth='public', methods=['POST', 'OPTIONS'], csrf=False)
@controller.Controller.must_authorized()
diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py
index d199cd84..374b49a2 100644
--- a/indoteknik_api/controllers/api_v1/sale_order.py
+++ b/indoteknik_api/controllers/api_v1/sale_order.py
@@ -544,13 +544,76 @@ class SaleOrder(controller.Controller):
main_partner = partner_invoice.get_main_parent()
_logger.info(
f"Partner Info - Sales: {sales_partner.id}, Invoice: {partner_invoice.id}, Main: {main_partner.id}")
-
+
+ def _get_request_context(params, kw):
+ # 1) kw (querystring di route)
+ ctx = kw.get('context')
+ if ctx:
+ return str(ctx).strip().lower()
+
+ # 2) querystring langsung
+ ctx = request.httprequest.args.get('context')
+ if ctx:
+ return str(ctx).strip().lower()
+
+ # 3) form-encoded body
+ ctx = request.httprequest.form.get('context')
+ if ctx:
+ return str(ctx).strip().lower()
+
+ # 4) JSON body (kalau ada)
+ try:
+ js = request.httprequest.get_json(force=False, silent=True) or {}
+ ctx = js.get('context')
+ if ctx:
+ return str(ctx).strip().lower()
+ except Exception:
+ pass
+
+ # 5) fallback: dari hasil get_request_params kalau kamu tambahkan 'context' di mapping
+ try:
+ return str((params.get('value', {}) or {}).get('context') or '').strip().lower()
+ except Exception:
+ return ''
+
+ ctx = _get_request_context(params, kw)
+ _logger.info(f"[checkout] context sources -> kw={repr(kw.get('context'))}, "
+ f"args={repr(request.httprequest.args.get('context'))}, "
+ f"form={repr(request.httprequest.form.get('context'))}, "
+ f"json={(request.httprequest.get_json(force=False, silent=True) or {}).get('context') if hasattr(request.httprequest,'get_json') else None}, "
+ f"normalized={ctx}")
+
+ payment_term_id_value = 26
+
+ ctx = str((kw.get('context') or ''))
+ if ctx == 'quotation':
+ try:
+ creator_user_id = params['value'].get('user_id') or request.env.user.id
+ user = request.env['res.users'].sudo().browse(int(creator_user_id))
+
+ term_name = None
+ if user and user.exists() and user.partner_id:
+ prop = getattr(user.partner_id, 'property_payment_term_id', False)
+ # Kalau Many2one -> pakai .name, kalau string/selection -> cast ke str
+ if prop:
+ term_name = getattr(prop, 'name', False) or str(prop).strip()
+
+ if term_name:
+ term = request.env['account.payment.term'].sudo().search(
+ [('name', 'ilike', term_name)],
+ limit=1
+ )
+ if term:
+ payment_term_id_value = term.id
+ except Exception as e:
+ _logger.warning(f"Gagal resolve payment term dari user: {e}")
+
parameters = {
'warehouse_id': 8,
'carrier_id': 1,
'sales_tax_id': 23,
'pricelist_id': user_pricelist or product_pricelist_default_discount_id,
- 'payment_term_id': 26,
+ 'payment_term_id':payment_term_id_value,
'team_id': 2,
'company_id': 1,
'currency_id': 12,
@@ -633,6 +696,7 @@ class SaleOrder(controller.Controller):
_logger.info(f"Final is_has_disc: {is_has_disc}")
+
order_line = request.env['sale.order.line'].create({
'company_id': 1,
'order_id': sale_order.id,
diff --git a/indoteknik_api/controllers/api_v1/user.py b/indoteknik_api/controllers/api_v1/user.py
index b5b7e055..dde30fec 100644
--- a/indoteknik_api/controllers/api_v1/user.py
+++ b/indoteknik_api/controllers/api_v1/user.py
@@ -396,7 +396,7 @@ class User(controller.Controller):
'user': request.env['res.users'].api_single_response(user)
})
- @http.route(prefix + 'user/<id>', auth='public', methods=['PUT', 'OPTIONS'], csrf=False)
+ @http.route(prefix + 'user/<id>', auth='public', methods=['POST', 'OPTIONS'], csrf=False)
@controller.Controller.must_authorized()
def update_user(self, **kw):
id = kw.get('id')
@@ -446,7 +446,7 @@ class User(controller.Controller):
return self.response(address)
- @http.route(prefix + 'user/<id>/switch', auth='public', methods=['PUT', 'OPTIONS'], csrf=False)
+ @http.route(prefix + 'user/<id>/switch', auth='public', methods=['POST', 'OPTIONS'], csrf=False)
@controller.Controller.must_authorized()
def switch_account(self, **kw):
id = int(kw.get('id'))
@@ -489,7 +489,7 @@ class User(controller.Controller):
if type_acc == 'business':
parameter = [
- ('company_type', '=', 'company'),
+ ('is_company', '=', True),
('name', 'ilike', business_name)
]
if parent_id:
@@ -674,11 +674,11 @@ class User(controller.Controller):
# request_company.send_company_request_mail_switch()
request_company.send_company_request_mail_switch()
- response['switch'] = 'Pending'
+ response['switch'] = 'pending'
return self.response(response)
- @http.route(prefix + 'user/<id>/switch_progres', auth='public', methods=['GET', 'OPTIONS'], csrf=False)
- # @controller.Controller.must_authorized()
+ @http.route(prefix + 'user/<id>/switch_progres', auth='public', methods=['GET', 'OPTIONS'])
+ @controller.Controller.must_authorized()
def switch_account_progres(self, **kw):
id = int(kw.get('id'))
# user = request.env['res.partner'].search([('id', '=', id)], limit=1)
@@ -691,15 +691,28 @@ class User(controller.Controller):
new_company_request = request.env['user.company.request'].search(parameter, limit=1)
is_approve = ''
if new_company_request:
- # Mengambil nilai is_approve
- if new_company_request.is_approve == False:
+ if not new_company_request.is_approve: # None atau ''
response['status'] = 'pending'
else:
- response['status'] = new_company_request.is_approve
+ response['status'] = new_company_request.is_approve # 'approved' atau 'rejected'
else:
response['status'] = False
return self.response(response)
+ # @http.route(prefix + 'user/<id>/parent_status', auth='public', methods=['GET', 'OPTIONS'], csrf=False)
+ # # @controller.Controller.must_authorized()
+ # def parent_status(self, **kw):
+ # id = int(kw.get('id'))
+ # response = {
+ # 'parentId': None,
+ # 'parentName': None
+ # }
+ # partner = request.env['res.partner'].sudo().search([('id', '=', id)], limit=1)
+ # if partner and partner.parent_id:
+ # response['parentId'] = partner.parent_id.id
+ # response['parentName'] = partner.parent_id.name
+ # return self.response(response)
+
def get_user_by_email(self, email):
return request.env['res.users'].search([
('login', '=', email),
diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py
index 68759e5c..31685005 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/mrp_production.py b/indoteknik_custom/models/mrp_production.py
index 91da0597..b39995b5 100644
--- a/indoteknik_custom/models/mrp_production.py
+++ b/indoteknik_custom/models/mrp_production.py
@@ -263,7 +263,7 @@ class CheckBomProduct(models.Model):
"The product '%s' tidak ada di operations. "
) % record.product_id.display_name)
- total_qty_in_moves = sum(moves.mapped('product_uom_qty'))
+ total_qty_in_moves = sum(moves.mapped('quantity_done'))
# Find existing lines for the same product, excluding the current line
existing_lines = record.production_id.check_bom_product_lines.filtered(
@@ -273,15 +273,15 @@ class CheckBomProduct(models.Model):
if existing_lines:
total_quantity = sum(existing_lines.mapped('quantity'))
- if total_quantity > total_qty_in_moves:
+ if total_quantity != total_qty_in_moves:
raise UserError((
- "Quantity Product '%s' kurang dari quantity demand."
+ "Quantity Product '%s' harus sama dengan quantity consumed."
) % (record.product_id.display_name))
else:
# Check if the quantity exceeds the allowed total
- if record.quantity > total_qty_in_moves:
+ if record.quantity != total_qty_in_moves:
raise UserError((
- "Quantity Product '%s' kurang dari quantity demand."
+ "Quantity Product '%s' harus sama dengan quantity consumed."
) % (record.product_id.display_name))
# Set the quantity to the entered value
@@ -446,7 +446,7 @@ class CheckBomProduct(models.Model):
"The product '%s' tidak ada di operations. "
) % record.product_id.display_name)
- total_qty_in_moves = sum(moves.mapped('product_uom_qty'))
+ total_qty_in_moves = sum(moves.mapped('quantity_done'))
# Find existing lines for the same product, excluding the current line
existing_lines = record.production_id.check_bom_product_lines.filtered(
@@ -462,13 +462,13 @@ class CheckBomProduct(models.Model):
if total_quantity > total_qty_in_moves:
raise UserError((
- "Quantity Product '%s' sudah melebihi quantity demand."
+ "Quantity Product '%s' sudah melebihi quantity consumed."
) % (record.product_id.display_name))
else:
# Check if the quantity exceeds the allowed total
if record.quantity == total_qty_in_moves:
raise UserError((
- "Quantity Product '%s' sudah melebihi quantity demand."
+ "Quantity Product '%s' sudah melebihi quantity consumed."
) % (record.product_id.display_name))
# Set the quantity to the entered 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 cf9fbea4..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
@@ -199,16 +207,20 @@ class ResPartner(models.Model):
alamat_lengkap_text = fields.Text(string="Alamat Lengkap", required=False, tracking=3)
- def write(self, vals):
- res = super(ResPartner, self).write(vals)
+ # def write(self, vals):
+ # res = super(ResPartner, self).write(vals)
- for rec in self:
- if 'latitude' in vals or 'longtitude' in vals:
- rec._update_address_from_coords()
+ # use_pin = bool(self.env.context.get('use_pin'))
- # Sinkronisasi payment_difficulty ke semua anak jika partner ini adalah parent
- if not rec.parent_id and 'payment_difficulty' in vals:
- rec.child_ids.write({'payment_difficulty': vals['payment_difficulty']})
+ # for rec in self:
+ # if use_pin and 'latitude' in vals or 'longtitude' in vals:
+ # # rec._update_address_from_coords()
+ # rec.with_context(overwrite_street_from_pin=False)._update_address_from_coords()
+
+
+ # # Sinkronisasi payment_difficulty ke semua anak jika partner ini adalah parent
+ # if not rec.parent_id and 'payment_difficulty' in vals:
+ # rec.child_ids.write({'payment_difficulty': vals['payment_difficulty']})
#
# # if 'property_payment_term_id' in vals:
# # if not self.env.user.is_accounting and vals['property_payment_term_id'] != 26:
@@ -219,17 +231,47 @@ class ResPartner(models.Model):
# # if self.env.user.id not in users_in_group.mapped('id'):
# # raise UserError('You name it')
#
+ def write(self, vals):
+ res = super(ResPartner, self).write(vals)
+
+ use_pin = bool(self.env.context.get('use_pin'))
+ for rec in self:
+ # Hanya jalan kalau explicit use_pin DAN ada lat/lng di payload
+ if use_pin and (('latitude' in vals) or ('longtitude' in vals)):
+ # optional guard: pastikan bukan kosong/0
+ lat = rec.latitude
+ lng = rec.longtitude
+ if lat and lng and str(lat) not in ('0', '0.0') and str(lng) not in ('0', '0.0'):
+ rec.with_context(overwrite_street_from_pin=False)._update_address_from_coords()
+
+ # Sinkronisasi payment_difficulty ke anak
+ if not rec.parent_id and 'payment_difficulty' in vals:
+ rec.child_ids.write({'payment_difficulty': vals['payment_difficulty']})
return res
+
+ # @api.model
+ # def create(self, vals):
+ # records = super().create(vals)
+ # use_pin = bool(self.env.context.get('use_pin'))
+ # for rec in records:
+ # if use_pin and vals.get('latitude') and vals.get('longtitude'):
+ # rec._update_address_from_coords()
+ # if rec.parent_id and not vals.get('payment_difficulty'):
+ # rec.payment_difficulty = rec.parent_id.payment_difficulty
+ # return records
+
@api.model
def create(self, vals):
records = super().create(vals)
+ use_pin = bool(self.env.context.get('use_pin'))
for rec in records:
- if vals.get('latitude') and vals.get('longtitude'):
- rec._update_address_from_coords()
+ if use_pin and vals.get('latitude') and vals.get('longtitude'):
+ rec.with_context(overwrite_street_from_pin=False)._update_address_from_coords()
if rec.parent_id and not vals.get('payment_difficulty'):
- rec.payment_difficulty = rec.parent_id.payment_difficulty
+ rec.payment_difficulty = rec.parent_id.payment_difficulty
return records
+
@api.constrains('email')
def _check_duplicate_name(self):
@@ -241,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')
@@ -621,6 +663,44 @@ class ResPartner(models.Model):
else:
raise UserError("Permintaan ke Google Maps gagal. Periksa koneksi internet atau API Key.")
+ # def _update_address_from_coords(self):
+ # for rec in self:
+ # if rec.latitude and rec.longtitude:
+ # try:
+ # components, formatted, parsed = rec._reverse_geocode(rec.latitude, rec.longtitude)
+ # if not parsed:
+ # continue
+
+ # updates = {
+ # 'street': parsed.get('road') or '',
+ # 'zip': parsed.get('postcode') or '',
+ # 'address_map': formatted or '',
+ # }
+
+ # if self.env.context.get('overwrite_street_from_pin', False):
+ # updates['street'] = parsed.get('road') or rec.street
+
+ # state = self.env['res.country.state'].search([('name', 'ilike', parsed.get('state'))], limit=1)
+ # if state:
+ # updates['state_id'] = state.id
+
+ # kota = self.env['vit.kota'].search([('name', 'ilike', parsed.get('city'))], limit=1)
+ # if kota:
+ # updates['kota_id'] = kota.id
+
+ # kec = self.env['vit.kecamatan'].search([('name', 'ilike', parsed.get('district'))], limit=1)
+ # if kec:
+ # updates['kecamatan_id'] = kec.id
+
+ # kel = self.env['vit.kelurahan'].search([('name', 'ilike', parsed.get('suburb'))], limit=1)
+ # if kel:
+ # updates['kelurahan_id'] = kel.id
+
+ # rec.update(updates)
+
+ # except Exception as e:
+ # raise UserError(f"Gagal update alamat dari koordinat: {str(e)}")
+
def _update_address_from_coords(self):
for rec in self:
if rec.latitude and rec.longtitude:
@@ -630,28 +710,45 @@ class ResPartner(models.Model):
continue
updates = {
- 'street': parsed.get('road') or '',
- 'zip': parsed.get('postcode') or '',
- 'address_map': formatted or '',
+ 'address_map': formatted or rec.address_map or '',
}
- state = self.env['res.country.state'].search([('name', 'ilike', parsed.get('state'))], limit=1)
- if state:
- updates['state_id'] = state.id
-
- kota = self.env['vit.kota'].search([('name', 'ilike', parsed.get('city'))], limit=1)
- if kota:
- updates['kota_id'] = kota.id
-
- kec = self.env['vit.kecamatan'].search([('name', 'ilike', parsed.get('district'))], limit=1)
- if kec:
- updates['kecamatan_id'] = kec.id
-
- kel = self.env['vit.kelurahan'].search([('name', 'ilike', parsed.get('suburb'))], limit=1)
- if kel:
- updates['kelurahan_id'] = kel.id
-
- rec.update(updates)
+ # ❌ Jangan isi street kecuali diminta eksplisit via context
+ if self.env.context.get('overwrite_street_from_pin', False):
+ road = parsed.get('road')
+ if road:
+ updates['street'] = road
+
+ # (Opsional) admin area & zip hanya jika kosong agar tidak menimpa input user
+ if (not rec.zip) and parsed.get('postcode'):
+ updates['zip'] = parsed.get('postcode')
+
+ state_name = parsed.get('state')
+ if state_name and not rec.state_id:
+ state = self.env['res.country.state'].search([('name', 'ilike', state_name)], limit=1)
+ if state:
+ updates['state_id'] = state.id
+
+ city_name = parsed.get('city')
+ if city_name and not rec.kota_id:
+ kota = self.env['vit.kota'].search([('name', 'ilike', city_name)], limit=1)
+ if kota:
+ updates['kota_id'] = kota.id
+
+ dist_name = parsed.get('district')
+ if dist_name and not rec.kecamatan_id:
+ kec = self.env['vit.kecamatan'].search([('name', 'ilike', dist_name)], limit=1)
+ if kec:
+ updates['kecamatan_id'] = kec.id
+
+ sub_name = parsed.get('suburb')
+ if sub_name and not rec.kelurahan_id:
+ kel = self.env['vit.kelurahan'].search([('name', 'ilike', sub_name)], limit=1)
+ if kel:
+ updates['kelurahan_id'] = kel.id
+
+ if updates:
+ rec.update(updates)
except Exception as e:
raise UserError(f"Gagal update alamat dari koordinat: {str(e)}")
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index 998363ef..903f834b 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -375,6 +375,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 bf6834d0..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):
@@ -1060,7 +1060,7 @@ class StockPicking(models.Model):
self.sale_id.date_doc_kirim = self.date_doc_kirim
def action_assign(self):
- if self.env.context.get('default_picking_type_id') and self.sale_id:
+ if self.env.context.get('default_picking_type_id') and ('BU/INPUT' not in self.name or 'BU/PUT' not in self.name):
pickings_to_assign = self.filtered(
lambda p: not (p.sale_id and p.sale_id.hold_outgoing)
)
@@ -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/stock_picking_return.py b/indoteknik_custom/models/stock_picking_return.py
index 1fc8d088..88acf83c 100644
--- a/indoteknik_custom/models/stock_picking_return.py
+++ b/indoteknik_custom/models/stock_picking_return.py
@@ -110,7 +110,7 @@ class ReturnPicking(models.TransientModel):
if mapping_koli_vals:
context['default_mapping_koli_ids'] = mapping_koli_vals
- if picking.purchase_id or 'PO' in picking.origin:
+ if picking.purchase_id or 'PO' in (picking.origin or ''):
_logger.info("Redirect ke Tukar Guling PO via purchase_id / origin")
return {
'name': _('Tukar Guling PO'),
@@ -120,7 +120,7 @@ class ReturnPicking(models.TransientModel):
'target': 'current',
'context': context,
}
- else:
+ if picking.sale_id or 'SO' in (picking.origin or ''):
_logger.info("This picking is NOT from a PO, fallback to SO.")
return {
'name': _('Tukar Guling SO'),
@@ -130,6 +130,9 @@ class ReturnPicking(models.TransientModel):
'target': 'current',
'context': context,
}
+ else:
+ _logger.info("Bukan SO/PO → retur standar (create_returns)")
+ return super(ReturnPicking, self).create_returns()
class ReturnPickingLine(models.TransientModel):
diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py
index ff4edc84..94a0c663 100644
--- a/indoteknik_custom/models/tukar_guling.py
+++ b/indoteknik_custom/models/tukar_guling.py
@@ -425,8 +425,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/models/user_company_request.py b/indoteknik_custom/models/user_company_request.py
index 9216e8eb..284575aa 100644
--- a/indoteknik_custom/models/user_company_request.py
+++ b/indoteknik_custom/models/user_company_request.py
@@ -1,6 +1,9 @@
from odoo import models, fields, api
from odoo.exceptions import UserError
from odoo.http import request
+import logging
+
+_logger = logging.getLogger(__name__)
class UserCompanyRequest(models.Model):
_name = 'user.company.request'
@@ -65,8 +68,8 @@ class UserCompanyRequest(models.Model):
is_approve = vals.get('is_approve')
is_internal_input = vals.get('internal_input')
is_company_id = vals.get('user_company_id')
- if self.is_approve and is_approve:
- raise UserError('Tidak dapat mengubah approval yang sudah diisi')
+ # if self.is_approve and is_approve:
+ # raise UserError('Tidak dapat mengubah approval yang sudah diisi')
if is_internal_input:
if self.user_company_id.nama_wajib_pajak == self.user_company_id.name:
@@ -75,6 +78,7 @@ class UserCompanyRequest(models.Model):
user_company_id = []
if is_company_id:
user_company_id = request.env['res.partner'].search([('id', '=', is_company_id)], limit=1)
+ _logger.info('User Company ID: %s', user_company_id)
# self.user_company_id.customer_type = self.similar_company_ids.customer_type
# self.user_company_id.npwp = self.similar_company_ids.npwp
# self.user_company_id.sppkp = self.similar_company_ids.sppkp
@@ -101,6 +105,8 @@ class UserCompanyRequest(models.Model):
self.user_id.property_account_payable_id = user_company_id.property_account_payable_id if user_company_id else self.user_company_id.property_account_payable_id
self.user_id.property_payment_term_id = user_company_id.property_payment_term_id if user_company_id else self.user_company_id.property_payment_term_id
self.user_id.property_supplier_payment_term_id = user_company_id.property_supplier_payment_term_id if user_company_id else self.user_company_id.property_supplier_payment_term_id
+ # self.user_id.is_company = True
+ # self.user_id.active = True
self.user_company_id.active = True
user.send_company_switch_approve_mail() if vals.get('is_switch_account') == True else user.send_company_request_approve_mail()
else:
diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv
index 91d1e493..866a7140 100755
--- a/indoteknik_custom/security/ir.model.access.csv
+++ b/indoteknik_custom/security/ir.model.access.csv
@@ -192,4 +192,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 667a6b3e..1b477c6d 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 8f8e4288..156c48d7 100755
--- a/indoteknik_custom/views/sale_order.xml
+++ b/indoteknik_custom/views/sale_order.xml
@@ -288,6 +288,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"/>
@@ -475,6 +478,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;">