diff options
| author | it-fixcomart <it@fixcomart.co.id> | 2025-08-30 09:25:35 +0700 |
|---|---|---|
| committer | it-fixcomart <it@fixcomart.co.id> | 2025-08-30 09:25:35 +0700 |
| commit | 5ef97855847141eaa705be36a2aae17cdf928258 (patch) | |
| tree | 449d86549d69024e92032b2246f8481bf081302a | |
| parent | 0298605049e29ef436a5e6984b743f89fed712b3 (diff) | |
| parent | 63426e9de8700daff0c0f7cf0389d2be55e982fb (diff) | |
<hafid> fix conflict merge
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 & 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" <noreply@indoteknik.com></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;"> |
