summaryrefslogtreecommitdiff
path: root/indoteknik_custom
diff options
context:
space:
mode:
Diffstat (limited to 'indoteknik_custom')
-rwxr-xr-xindoteknik_custom/__manifest__.py16
-rwxr-xr-xindoteknik_custom/models/__init__.py5
-rw-r--r--indoteknik_custom/models/account_asset.py119
-rw-r--r--indoteknik_custom/models/account_move.py17
-rw-r--r--indoteknik_custom/models/account_move_line.py65
-rw-r--r--indoteknik_custom/models/advance_payment_request.py110
-rw-r--r--indoteknik_custom/models/automatic_purchase.py76
-rw-r--r--indoteknik_custom/models/gudang_service.py273
-rw-r--r--indoteknik_custom/models/kartu_stock.py186
-rw-r--r--indoteknik_custom/models/keywords.py253
-rw-r--r--indoteknik_custom/models/price_group.py20
-rwxr-xr-xindoteknik_custom/models/product_template.py175
-rwxr-xr-xindoteknik_custom/models/purchase_order.py352
-rwxr-xr-xindoteknik_custom/models/purchase_order_line.py63
-rwxr-xr-xindoteknik_custom/models/purchase_pricelist.py7
-rw-r--r--indoteknik_custom/models/refund_sale_order.py57
-rw-r--r--indoteknik_custom/models/res_partner.py34
-rwxr-xr-xindoteknik_custom/models/sale_order.py78
-rw-r--r--indoteknik_custom/models/sale_order_line.py53
-rw-r--r--indoteknik_custom/models/shipment_group.py24
-rw-r--r--indoteknik_custom/models/stock_inventory.py44
-rw-r--r--indoteknik_custom/models/stock_picking.py14
-rw-r--r--indoteknik_custom/models/token_log.py17
-rw-r--r--indoteknik_custom/models/tukar_guling.py61
-rw-r--r--indoteknik_custom/models/tukar_guling_po.py40
-rw-r--r--indoteknik_custom/models/update_depreciation_move_wizard.py48
-rwxr-xr-xindoteknik_custom/models/x_manufactures.py2
-rw-r--r--indoteknik_custom/report/purchase_report.xml2
-rw-r--r--indoteknik_custom/report/purchase_report_internal.xml201
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv7
-rw-r--r--indoteknik_custom/views/account_move_line.xml15
-rw-r--r--indoteknik_custom/views/advance_payment_request.xml25
-rw-r--r--indoteknik_custom/views/advance_payment_settlement.xml14
-rw-r--r--indoteknik_custom/views/approval_date_doc.xml2
-rw-r--r--indoteknik_custom/views/find_page.xml13
-rw-r--r--indoteknik_custom/views/gudang_service.xml110
-rw-r--r--indoteknik_custom/views/ir_sequence.xml9
-rw-r--r--indoteknik_custom/views/kartu_stock.xml17
-rw-r--r--indoteknik_custom/views/keywords.xml96
-rw-r--r--indoteknik_custom/views/mail_template_efaktur.xml2
-rwxr-xr-xindoteknik_custom/views/purchase_order.xml23
-rw-r--r--indoteknik_custom/views/purchasing_job.xml28
-rwxr-xr-xindoteknik_custom/views/sale_order.xml3
-rw-r--r--indoteknik_custom/views/shipment_group.xml6
-rw-r--r--indoteknik_custom/views/stock_inventory.xml3
-rw-r--r--indoteknik_custom/views/stock_picking.xml2
-rw-r--r--indoteknik_custom/views/token_log.xml33
-rw-r--r--indoteknik_custom/views/tukar_guling.xml24
-rw-r--r--indoteknik_custom/views/update_depreciation_move_wizard_view.xml35
-rwxr-xr-xindoteknik_custom/views/x_manufactures.xml2
50 files changed, 2669 insertions, 212 deletions
diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py
index 66962a24..8c427579 100755
--- a/indoteknik_custom/__manifest__.py
+++ b/indoteknik_custom/__manifest__.py
@@ -8,7 +8,7 @@
'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', 'proweb_kartu_stok'],
'data': [
'views/assets.xml',
'security/ir.model.access.csv',
@@ -90,7 +90,7 @@
'views/product_sla.xml',
'views/voucher.xml',
'views/bill_receipt.xml',
- 'views/account_financial_report_view.xml',
+ 'views/account_financial_report_view.xml',
'views/account_report_general_ledger_view.xml',
'views/account_move_multi_update.xml',
'views/airway_bill.xml',
@@ -169,6 +169,7 @@
'report/report_surat_piutang.xml',
'report/report_tutup_tempo.xml',
'report/purchase_report.xml',
+ 'report/purchase_report_internal.xml',
'views/vendor_sla.xml',
'views/coretax_faktur.xml',
'views/public_holiday.xml',
@@ -177,22 +178,23 @@
'views/refund_sale_order.xml',
'views/advance_payment_request.xml',
'views/advance_payment_settlement.xml',
- # 'views/refund_sale_order.xml',
'views/tukar_guling.xml',
- # 'views/tukar_guling_return_views.xml'
'views/tukar_guling_po.xml',
- # 'views/refund_sale_order.xml',
'views/update_date_planned_po_wizard_view.xml',
'views/unpaid_invoice_view.xml',
'views/letter_receivable.xml',
'views/letter_receivable_mail_template.xml',
'views/mail_template_pum.xml',
- # 'views/reimburse.xml',
'views/sj_tele.xml',
'views/close_tempo_mail_template.xml',
'views/domain_apo.xml',
'views/uom_uom.xml',
- 'views/commission_internal.xml'
+ 'views/update_depreciation_move_wizard_view.xml',
+ 'views/commission_internal.xml',
+ 'views/keywords.xml',
+ 'views/token_log.xml',
+ 'views/gudang_service.xml',
+ 'views/kartu_stock.xml',
],
'demo': [],
'css': [],
diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py
index a14c766e..31ee5108 100755
--- a/indoteknik_custom/models/__init__.py
+++ b/indoteknik_custom/models/__init__.py
@@ -165,3 +165,8 @@ from . import partial_delivery
from . import domain_apo
from . import uom_uom
from . import commission_internal
+from . import gudang_service
+from . import update_depreciation_move_wizard
+from . import keywords
+from . import token_log
+from . import kartu_stock
diff --git a/indoteknik_custom/models/account_asset.py b/indoteknik_custom/models/account_asset.py
index 211ab229..dbcb6ba4 100644
--- a/indoteknik_custom/models/account_asset.py
+++ b/indoteknik_custom/models/account_asset.py
@@ -1,5 +1,7 @@
from odoo import fields, models, api, _
from odoo.exceptions import AccessError, UserError, ValidationError
+from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DF
+from odoo.tools import float_compare, float_is_zero
class AccountAsset(models.Model):
@@ -14,3 +16,120 @@ class AccountAsset(models.Model):
if asset.value > 0:
raise UserError("Asset masih mempunyai Value")
asset.state = 'close'
+
+
+class AccountAssetDepreciationLine(models.Model):
+ _inherit = 'account.asset.depreciation.line'
+
+ def create_move(self, post_move=True):
+ created_moves = self.env['account.move']
+ prec = self.env['decimal.precision'].precision_get('Account')
+ if self.mapped('move_id'):
+ raise UserError(_(
+ 'This depreciation is already linked to a journal entry! Please post or delete it.'))
+ for line in self:
+ category_id = line.asset_id.category_id
+ depreciation_date = self.env.context.get(
+ 'depreciation_date') or line.depreciation_date or fields.Date.context_today(
+ self)
+ company_currency = line.asset_id.company_id.currency_id
+ current_currency = line.asset_id.currency_id
+ amount = current_currency.with_context(
+ date=depreciation_date).compute(line.amount, company_currency)
+ asset_name = line.asset_id.name + ' (%s/%s)' % (
+ line.sequence, len(line.asset_id.depreciation_line_ids))
+ partner = self.env['res.partner']._find_accounting_partner(
+ line.asset_id.partner_id)
+ move_line_1 = {
+ 'name': asset_name,
+ 'account_id': category_id.account_depreciation_id.id,
+ 'debit': 0.0 if float_compare(amount, 0.0,
+ precision_digits=prec) > 0 else -amount,
+ 'credit': amount if float_compare(amount, 0.0,
+ precision_digits=prec) > 0 else 0.0,
+ 'journal_id': category_id.journal_id.id,
+ 'partner_id': partner.id,
+ 'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'sale' else False,
+ 'currency_id': company_currency != current_currency and current_currency.id or False,
+ 'amount_currency': company_currency != current_currency and - 1.0 * line.amount or 0.0,
+ }
+ move_line_2 = {
+ 'name': asset_name,
+ 'account_id': category_id.account_depreciation_expense_id.id,
+ 'credit': 0.0 if float_compare(amount, 0.0,
+ precision_digits=prec) > 0 else -amount,
+ 'debit': amount if float_compare(amount, 0.0,
+ precision_digits=prec) > 0 else 0.0,
+ 'journal_id': category_id.journal_id.id,
+ 'partner_id': partner.id,
+ 'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'purchase' else False,
+ 'currency_id': company_currency != current_currency and current_currency.id or False,
+ 'amount_currency': company_currency != current_currency and line.amount or 0.0,
+ }
+ move_vals = {
+ 'ref': line.asset_id.code,
+ 'date': depreciation_date or False,
+ 'journal_id': category_id.journal_id.id,
+ 'line_ids': [(0, 0, move_line_1), (0, 0, move_line_2)],
+ }
+ move = self.env['account.move'].create(move_vals)
+ line.write({'move_id': move.id, 'move_check': True})
+ created_moves |= move
+
+ if post_move and created_moves:
+ created_moves.filtered(lambda m: any(
+ m.asset_depreciation_ids.mapped(
+ 'asset_id.category_id.open_asset'))).post()
+
+ for move in created_moves:
+ move.action_post()
+ return [x.id for x in created_moves]
+
+ def create_grouped_move(self, post_move=True):
+ if not self.exists():
+ return []
+
+ created_moves = self.env['account.move']
+ category_id = self[
+ 0].asset_id.category_id # we can suppose that all lines have the same category
+ depreciation_date = self.env.context.get(
+ 'depreciation_date') or fields.Date.context_today(self)
+ amount = 0.0
+ for line in self:
+ # Sum amount of all depreciation lines
+ company_currency = line.asset_id.company_id.currency_id
+ current_currency = line.asset_id.currency_id
+ amount += current_currency.compute(line.amount, company_currency)
+
+ name = category_id.name + _(' (grouped)')
+ move_line_1 = {
+ 'name': name,
+ 'account_id': category_id.account_depreciation_id.id,
+ 'debit': 0.0,
+ 'credit': amount,
+ 'journal_id': category_id.journal_id.id,
+ 'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'sale' else False,
+ }
+ move_line_2 = {
+ 'name': name,
+ 'account_id': category_id.account_depreciation_expense_id.id,
+ 'credit': 0.0,
+ 'debit': amount,
+ 'journal_id': category_id.journal_id.id,
+ 'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'purchase' else False,
+ }
+ move_vals = {
+ 'ref': category_id.name,
+ 'date': depreciation_date or False,
+ 'journal_id': category_id.journal_id.id,
+ 'line_ids': [(0, 0, move_line_1), (0, 0, move_line_2)],
+ }
+ move = self.env['account.move'].create(move_vals)
+ self.write({'move_id': move.id, 'move_check': True})
+ created_moves |= move
+
+ if post_move and created_moves:
+ self.post_lines_and_close_asset()
+ created_moves.post()
+ created_moves.action_post()
+ return [x.id for x in created_moves] \ No newline at end of file
diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py
index e1360cfa..a317dccc 100644
--- a/indoteknik_custom/models/account_move.py
+++ b/indoteknik_custom/models/account_move.py
@@ -587,6 +587,10 @@ class AccountMove(models.Model):
records = self.search([('id', 'in', self.ids)])
template = self.env.ref('indoteknik_custom.mail_template_efaktur_document')
+ ICP = self.env['ir.config_parameter'].sudo()
+ special_partner_ids = set(
+ int(x) for x in (ICP.get_param('efaktur.special_partner_ids') or '').split(',') if x
+ )
for record in records:
if record.invoice_payment_term_id.id == 26:
@@ -595,6 +599,19 @@ class AccountMove(models.Model):
'attachment_ids': [(4, attachment.id)]
}
template.send_mail(record.id, email_values=email_values, force_send=True)
+
+ elif record.partner_id.id in special_partner_ids:
+ cust_ref = record.sale_id.client_order_ref if record.sale_id and record.sale_id.client_order_ref else ''
+ attachment = self.generate_attachment(record)
+ email_list = [record.partner_id.email] if record.partner_id.email else []
+ if record.real_invoice_id and record.real_invoice_id.email:
+ email_list.append(record.real_invoice_id.email)
+
+ email_values = {
+ 'email_to': ",".join(set(email_list)),
+ 'attachment_ids': [(4, attachment.id)]
+ }
+ template.with_context(cust_ref=cust_ref).send_mail(record.id, email_values=email_values, force_send=True)
# @api.model
# def create(self, vals):
diff --git a/indoteknik_custom/models/account_move_line.py b/indoteknik_custom/models/account_move_line.py
index 7c95d4ef..5edea25f 100644
--- a/indoteknik_custom/models/account_move_line.py
+++ b/indoteknik_custom/models/account_move_line.py
@@ -1,5 +1,5 @@
from odoo import models, api, fields
-
+from odoo.exceptions import AccessError, UserError, ValidationError
class AccountMoveLine(models.Model):
_inherit = "account.move.line"
@@ -9,6 +9,69 @@ class AccountMoveLine(models.Model):
analytic_account_ids = fields.Many2many('account.analytic.account', string='Analytic Account')
line_no = fields.Integer('No', default=0)
+ def action_gl_reconcile(self):
+ lines = self
+
+ journal = self.env['account.journal'].search([
+ ('suspense_account_id', '=', lines[0].account_id.id)
+ ], limit=1)
+
+ if not journal:
+ raise UserError('Journal dengan suspense account ini tidak ditemukan!')
+
+ statement = self.env['account.bank.statement'].create({
+ 'journal_id': journal.id,
+ 'name': f'REKONSIL {journal.name} {lines[0].date.strftime("%d-%m-%Y")}',
+ 'company_id': self.env.company.id,
+ 'date': lines[0].date,
+ })
+
+ widget_vals = []
+ st_line_ids = []
+
+ for line in lines:
+ amount = line.debit - line.credit
+
+ st_line = self.env['account.bank.statement.line'].create({
+ 'statement_id': statement.id,
+ 'date': line.date or fields.Date.today(),
+ 'payment_ref': line.name,
+ 'partner_id': line.partner_id.id,
+ 'amount': amount,
+ 'ref': line.name,
+ })
+
+ st_line_ids.append(st_line.id)
+
+ widget_vals.append({
+ 'partner_id': st_line.partner_id.id,
+ 'counterpart_aml_dicts': [{
+ 'counterpart_aml_id': line.id,
+ 'debit': abs(amount) if amount < 0 else 0,
+ 'credit': abs(amount) if amount > 0 else 0,
+ 'name': line.name or '/',
+ }],
+ 'payment_aml_ids': [],
+ 'new_aml_dicts': [],
+ 'to_check': False,
+ })
+
+ statement.button_post()
+
+ self.env['account.reconciliation.widget'].process_bank_statement_line(
+ st_line_ids,
+ widget_vals
+ )
+ # statement.button_validate_or_action()
+
+ return {
+ 'effect': {
+ 'fadeout': 'slow',
+ 'message': 'Statement + Auto Reconcile sukses besar 😎🔥',
+ 'type': 'rainbow_man',
+ }
+ }
+
@api.onchange('account_id')
def _onchange_account_id(self):
for account in self:
diff --git a/indoteknik_custom/models/advance_payment_request.py b/indoteknik_custom/models/advance_payment_request.py
index ed0b0809..039d18a5 100644
--- a/indoteknik_custom/models/advance_payment_request.py
+++ b/indoteknik_custom/models/advance_payment_request.py
@@ -155,6 +155,18 @@ class AdvancePaymentRequest(models.Model):
compute='_compute_is_current_user_ap'
)
+ estimate_line_ids = fields.One2many('advance.payment.request.estimate.line', 'request_id', string='Rincian Estimasi')
+
+ @api.constrains('nominal', 'estimate_line_ids')
+ def _check_nominal_vs_estimate_total(self):
+ for rec in self:
+ if rec.type_request == 'pum':
+ if not rec.estimate_line_ids:
+ raise UserError("Rincian estimasi wajib diisi untuk PUM. Silakan tambahkan rincian estimasi.")
+ total_estimate = sum(line.nominal for line in rec.estimate_line_ids)
+ if round(total_estimate, 2) != round(rec.nominal, 2):
+ raise UserError("Total estimasi harus sama dengan nominal PUM. Silakan sesuaikan rincian estimasi atau nominal PUM.")
+
@api.onchange('grand_total_reimburse', 'type_request')
def _onchange_reimburse_line_update_nominal(self):
if self.type_request == 'reimburse':
@@ -641,10 +653,16 @@ class AdvancePaymentRequest(models.Model):
today = date.today()
for rec in self:
- current_days = rec.days_remaining or 0
- current_due_date = rec.estimated_return_date or False
- if rec.type_request == 'pum':
- is_settlement_approved = any(s.status == 'approved' for s in rec.settlement_ids)
+ # current_days = rec.days_remaining or 0
+ # current_due_date = rec.estimated_return_date or False
+ current_days = 0
+ current_due_date = False
+
+ is_settlement_approved = any(s.status == 'approved' for s in rec.settlement_ids)
+ is_pum_canceled = (rec.status == 'cancel')
+
+ if rec.type_request == 'pum' and not is_pum_canceled and not is_settlement_approved:
+
if not is_settlement_approved:
due_date = False
@@ -745,7 +763,7 @@ class AdvancePaymentRequest(models.Model):
pum_ids = self.search([
('user_id', '=', user.id),
- ('status', '!=', 'reject'),
+ ('status', '!=', 'cancel'),
('type_request', '=', 'pum')
])
@@ -905,6 +923,35 @@ class AdvancePaymentUsageLine(models.Model):
compute='_compute_is_current_user_ap'
)
+ category_usage = fields.Selection([
+ ('parkir', 'Parkir'),
+ ('tol', 'Tol'),
+ ('bbm', 'BBM'),
+ ('kuli', 'Kuli'),
+ ('konsumsi', 'Konsumsi'),
+ ('lain_lain', 'Lain-lain'),
+ ], string='Kategori System', compute='_compute_category_usage')
+
+ @api.depends('account_id')
+ def _compute_category_usage(self):
+ for rec in self:
+ if not rec.account_id:
+ rec.category_usage = False
+ continue
+ name = rec.account_id.name.lower()
+ if 'bbm' in name or 'bahan bakar' in name:
+ rec.category_usage = 'bbm'
+ elif 'tol' in name:
+ rec.category_usage = 'tol'
+ elif 'parkir' in name:
+ rec.category_usage = 'parkir'
+ elif 'kuli' in name or 'bongkar' in name:
+ rec.category_usage = 'kuli'
+ elif 'konsumsi' in name or 'makan' in name or 'minum' in name:
+ rec.category_usage = 'konsumsi'
+ else:
+ rec.category_usage = 'lain_lain'
+
def _compute_is_current_user_ap(self):
ap_user_ids = [23, 9468, 16729]
is_ap = self.env.user.id in ap_user_ids
@@ -1138,6 +1185,11 @@ class AdvancePaymentSettlement(models.Model):
string="Is Current User AP",
compute='_compute_is_current_user_ap'
)
+ pum_estimate_line_ids = fields.One2many(
+ related='pum_id.estimate_line_ids',
+ string='Rincian Estimasi PUM',
+ readonly=True
+ )
def _compute_is_current_user_ap(self):
ap_user_ids = [23, 9468, 16729]
@@ -1606,4 +1658,50 @@ class CreateReimburseCabWizard(models.TransientModel):
'type': 'ir.actions.act_window',
'res_id': move.id,
'target': 'current',
- } \ No newline at end of file
+ }
+
+class AdvancePaymentRequestEstimateLine(models.Model):
+ _name = 'advance.payment.request.estimate.line'
+ _description = 'Advance Payment Request Estimate Line'
+
+ request_id = fields.Many2one('advance.payment.request', string='Request')
+ category_estimate = fields.Selection([
+ ('parkir', 'Parkir'),
+ ('tol', 'Tol'),
+ ('bbm', 'BBM'),
+ ('kuli', 'Kuli'),
+ ('konsumsi', 'Konsumsi'),
+ ('lain_lain', 'Lain-lain'),
+ ], string='Kategori Estimasi', required=True)
+ description = fields.Text(string='Description', help='Deskripsi tambahan untuk estimasi biaya yang diperlukan.')
+ nominal = fields.Float(string='Nominal Estimasi', required=True, help='Masukkan nominal estimasi untuk kategori ini (tidak mesti akurat, hanya untuk gambaran umum).')
+ currency_id = fields.Many2one(related='request_id.currency_id')
+
+ total_actual = fields.Float(string='Nominal Realisasi', compute='_compute_actual_data')
+ frequency = fields.Integer(string='Qty Realisasi', compute='_compute_actual_data')
+
+ @api.depends('request_id.settlement_ids.penggunaan_line_ids.nominal',
+ 'request_id.settlement_ids.penggunaan_line_ids.account_id')
+ def _compute_actual_data(self):
+ for rec in self:
+ total_act = 0
+ freq = 0
+ if rec.request_id and rec.request_id.settlement_ids:
+ all_usage_lines = rec.request_id.settlement_ids.mapped('penggunaan_line_ids')
+ valid_lines = all_usage_lines.filtered(lambda l: l.account_id and l.category_usage)
+ planned_category = rec.request_id.estimate_line_ids.mapped('category_estimate')
+ if rec.category_estimate == 'lain_lain':
+ matched_lines = valid_lines.filtered(
+ lambda l: l.category_usage == 'lain_lain' or \
+ l.category_usage not in planned_category
+ )
+ else:
+ matched_lines = valid_lines.filtered(
+ lambda l: l.category_usage == rec.category_estimate
+ )
+
+ total_act = sum(matched_lines.mapped('nominal'))
+ freq = len(matched_lines)
+ rec.total_actual = total_act
+ rec.frequency = freq
+
diff --git a/indoteknik_custom/models/automatic_purchase.py b/indoteknik_custom/models/automatic_purchase.py
index 4b0ce325..f7c0d75e 100644
--- a/indoteknik_custom/models/automatic_purchase.py
+++ b/indoteknik_custom/models/automatic_purchase.py
@@ -314,7 +314,74 @@ class AutomaticPurchase(models.Model):
sale_ids_set = set()
sale_ids_name = set()
+ retur_cache = {}
+ incoming_cache = {}
for sale_order in matches_so:
+ exist = self.env['purchase.order.sales.match'].search([
+ ('product_id', '=', sale_order.product_id.id),
+ ('sale_line_id', '=', sale_order.sale_line_id.id),
+ ('sale_id', '=', sale_order.sale_id.id),
+ ('purchase_order_id.state', '!=', 'cancel'),
+ ])
+
+ skip_line = False
+
+ sale_line_id = sale_order.sale_line_id.id
+
+ if sale_line_id not in incoming_cache:
+
+ qty_incoming = 0
+
+ for existing in exist:
+ if existing.purchase_order_id.state in ['done', 'purchase']:
+
+ incoming_moves = self.env['stock.move'].search([
+ ('reference', 'ilike', 'BU/INPUT'),
+ ('state', 'not in', ['done','cancel']),
+ ('product_id', '=', existing.product_id.id),
+ ('purchase_line_id', '=', existing.purchase_line_id.id),
+ ])
+
+ qty_incoming += sum(incoming_moves.mapped('product_uom_qty'))
+
+ incoming_cache[sale_line_id] = qty_incoming
+
+
+ qty_need = sale_order.sale_line_id.product_uom_qty
+
+ if incoming_cache[sale_line_id] >= qty_need:
+ skip_line = True
+
+ sale_line_id = sale_order.sale_line_id.id
+
+ if sale_line_id not in retur_cache:
+
+ fully_received = True
+
+ for existing in exist:
+ if existing.purchase_order_id.state in ['done', 'purchase']:
+
+ if existing.purchase_line_id.qty_received != existing.purchase_line_id.product_qty:
+ fully_received = False
+ break
+
+ retur_cache[sale_line_id] = fully_received
+
+
+ if retur_cache[sale_line_id] and exist:
+ skip_line = True
+
+ if skip_line:
+ continue
+
+ stock_move = self.env['stock.move'].search([
+ ('reference', 'ilike', 'BU/PICK'),
+ ('state', 'in', ['confirmed','waiting','partially_available']),
+ ('product_id', '=', sale_order.product_id.id),
+ ('sale_line_id', '=', sale_order.sale_line_id.id),
+ ])
+ if not stock_move:
+ continue
# @stephan skip so line yang sudah pernah ada di purchase order sales match sebelumnya
salesperson_name = sale_order.sale_id.user_id.name
@@ -499,7 +566,8 @@ class AutomaticPurchase(models.Model):
# _logger.info('test %s' % point.product_id.name)
if point.product_id.qty_available_bandengan > point.product_min_qty:
continue
- qty_purchase = point.product_max_qty - point.product_id.qty_incoming_bandengan - point.product_id.qty_available_bandengan
+ # qty_purchase = point.product_max_qty - point.product_id.qty_incoming_bandengan - point.product_id.qty_available_bandengan
+ qty_purchase = point.product_max_qty - point.product_id.qty_available_bandengan
po_line = self.env['purchase.order.line'].search([('product_id', '=', point.product_id.id), ('order_id.state', '=', 'done')], order='id desc', limit=1)
if self.vendor_id:
@@ -739,11 +807,5 @@ class SaleNotInMatchPO(models.Model):
apsm.create_date, apsm.write_uid, apsm.write_date, apsm.purchase_price,
apsm.purchase_tax_id, apsm.note_procurement
from automatic_purchase_sales_match apsm
- where apsm.sale_line_id not in (
- select distinct coalesce(posm.sale_line_id,0)
- from purchase_order_sales_match posm
- join purchase_order po on po.id = posm.purchase_order_id
- where po.state not in ('cancel')
- )
)
""" % self._table) \ No newline at end of file
diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py
new file mode 100644
index 00000000..d699ccf4
--- /dev/null
+++ b/indoteknik_custom/models/gudang_service.py
@@ -0,0 +1,273 @@
+from odoo import models, fields, api, _
+from odoo.exceptions import UserError, ValidationError
+import logging
+from datetime import datetime
+from collections import defaultdict
+
+
+class GudangService(models.Model):
+ _name = "gudang.service"
+ _description = "Gudang Service"
+ _inherit = ["mail.thread", "mail.activity.mixin"]
+ _order = "id asc"
+
+ name = fields.Char("Name", readonly=True)
+ partner_id = fields.Many2one("res.partner", string="Customer", readonly=True)
+ vendor_id = fields.Many2one("res.partner", string="Vendor Service", required=True)
+ origin = fields.Many2one(
+ "sale.order",
+ string="Origin SO",
+ required=True,
+ domain=[("state", "in", ["done", "sale"])],
+ )
+ schedule_date = fields.Date(string="Schedule Date", required=True, tracking=True)
+ start_date = fields.Datetime(string="Date Processed", copy=False, tracking=True)
+ create_date = fields.Datetime(
+ string="Create Date", copy=False, tracking=True, default=fields.Datetime.now()
+ )
+ done_date = fields.Datetime(string="Date Done", copy=False, tracking=True)
+ gudang_service_lines = fields.One2many(
+ "gudang.service.line", "gudang_service_id", string="Gudang Service Lines"
+ )
+ # unprocessed_date = fields.Char(
+ # string='Unprocessed Since',
+ # compute='_compute_unprocessed_date'
+ # )
+ remaining_date = fields.Char(
+ compute="_compute_remaining_date", string="Date Status"
+ )
+ state = fields.Selection(
+ [
+ ("draft", "Backlog"),
+ ("received_from_cust", "Received From Customer"),
+ ("sent_to_vendor", "Sent to Service Vendor"),
+ ("received_from_vendor", "Received From Service Vendor"),
+ ("delivered_to_cust", "Delivered to Customer"),
+ ("cancel", "Cancel"),
+ ],
+ default="draft",
+ tracking=True,
+ )
+ cancel_reason = fields.Text("Cancel Reason", tracking=True)
+
+ def check_duplicate_docs(self):
+ for rec in self:
+ found = self.env["gudang.service"].search(
+ [
+ ("id", "!=", self.id),
+ ("origin.id", "=", self.origin.id),
+ ("partner_id.id", "=", rec.partner_id.id),
+ ("vendor_id.id", "=", rec.vendor_id.id),
+ ("schedule_date", "=", rec.schedule_date),
+ (
+ "gudang_service_lines.product_id.name",
+ "=",
+ rec.gudang_service_lines.product_id.name,
+ ),
+ (
+ "gudang_service_lines.quantity",
+ "=",
+ rec.gudang_service_lines.quantity,
+ ),
+ ]
+ )
+ if found:
+ raise UserError("This Document has duplicate with %s" % found.name)
+
+ def _send_logistic_notification(self):
+ group = self.env.ref(
+ "indoteknik_custom.group_role_logistic", raise_if_not_found=False
+ )
+ if not group:
+ return
+
+ users = group.users
+ # MD
+ md = self.env["res.users"].browse([3425, 4801, 1036])
+ # send to logistic and MD
+ users = users | md
+
+ if not users:
+ return
+
+ # Logistic users to be excluded
+ excluded_users = [7, 17098, 216, 28, 15710]
+
+ for rec in self:
+ for user in users:
+ if user.id in excluded_users:
+ continue
+ self.env["mail.activity"].create(
+ {
+ "res_model_id": self.env["ir.model"]._get_id("gudang.service"),
+ "res_id": rec.id,
+ "activity_type_id": self.env.ref(
+ "mail.mail_activity_data_todo"
+ ).id,
+ "user_id": user.id,
+ "summary": "Gudang Service On Progress",
+ "note": _(
+ "Ada Jadwal Service Barang di Document <b>%s</b> Jadwal Service 📅 <b>%s</b>"
+ )
+ % (rec.name, rec.schedule_date),
+ # 'date_deadline': fields.Date.today(),
+ }
+ )
+
+ # kirim ke private message odoo
+ channel = (
+ self.env["mail.channel"]
+ .channel_get([self.env.user.partner_id.id, user.partner_id.id])
+ .get("id")
+ )
+ if not channel:
+ continue
+ res = self.env["mail.channel"].browse(channel)
+ res.with_user(self.env.user.browse(25)).message_post(
+ body=_(
+ "Ada Jadwal Service Barang di Document <b>%s</b> Jadwal Service 📅 <b>%s</b>"
+ )
+ % (rec.name, rec.schedule_date),
+ message_type="comment",
+ subtype_xmlid="mail.mt_comment",
+ )
+
+ @api.model
+ def cron_notify_onprogress_gudang_service(self):
+ records = self.search(
+ [
+ ("state", "in", ["draft", "received_from_customer"]),
+ ]
+ )
+
+ if records:
+ records._send_logistic_notification()
+
+ @api.depends("schedule_date", "create_date")
+ def _compute_remaining_date(self):
+ today = fields.Date.today()
+
+ for rec in self:
+ if not rec.schedule_date:
+ rec.remaining_date = "-"
+ continue
+
+ base_date = rec.create_date.date() if rec.create_date else today
+
+ schedule = rec.schedule_date
+ days = (schedule - base_date).days
+
+ if days > 0:
+ rec.remaining_date = _("In %s days") % days
+ elif days == 0:
+ rec.remaining_date = _("Today")
+ else:
+ rec.remaining_date = _("Overdue %s days") % abs(days)
+
+ def action_submit(self):
+ self.ensure_one()
+ self.check_duplicate_docs()
+ for rec in self:
+ if rec.state == "draft":
+ rec.state = "received_from_cust"
+ elif rec.state == "received_from_cust":
+ rec.state = "sent_to_vendor"
+ rec.start_date = fields.Datetime.now()
+ elif rec.state == "sent_to_vendor":
+ rec.state = "received_from_vendor"
+
+ def action_done(self):
+ for rec in self:
+ if rec.state != "received_from_vendor":
+ raise UserError("Only 'Received From Vendor' state can be set to Done")
+
+ rec.activity_ids.unlink()
+
+ rec.write(
+ {"state": "delivered_to_cust", "done_date": fields.Datetime.now()}
+ )
+
+ def action_draft(self):
+ """Reset to draft state"""
+ for rec in self:
+ rec.cancel_reason = False
+ if rec.state == "cancel":
+ rec.write({"state": "draft"})
+ else:
+ raise UserError("Only Canceled Record Can Be Reset To Draft")
+
+ def action_cancel(self):
+ for rec in self:
+ activities = self.env["mail.activity"].search(
+ [
+ ("res_id", "=", rec.id),
+ ("res_model", "=", "gudang.service"),
+ ]
+ )
+ activities.unlink()
+ if rec.state == "delivered_to_cust":
+ raise UserError("You cannot cancel a done record")
+ if not rec.cancel_reason:
+ raise UserError("Cancel Reason must be filled")
+ rec.start_date = False
+ rec.done_date = False
+ rec.state = "cancel"
+
+ @api.model
+ def create(self, vals):
+ # sequence
+ if not vals.get("name") or vals["name"] == "New":
+ vals["name"] = self.env["ir.sequence"].next_by_code("gudang.service")
+
+ # partner dari SO
+ so = self.env["sale.order"].browse(vals["origin"])
+ vals["partner_id"] = so.partner_id.id
+
+ res = super(GudangService, self).create(vals)
+
+ res.check_duplicate_docs()
+ res._send_logistic_notification()
+ return res
+
+ def write(self, vals):
+ if vals.get("origin"):
+ so = self.env["sale.order"].browse(vals["origin"])
+ vals["partner_id"] = so.partner_id.id
+ vals.check_duplicate_docs()
+
+ return super(GudangService, self).write(vals)
+
+ @api.onchange("origin")
+ def _onchange_origin(self):
+ if not self.origin:
+ self.gudang_service_lines = [(5, 0, 0)]
+ return
+
+ self.partner_id = self.origin.partner_id
+
+ lines = []
+ for line in self.origin.order_line:
+ lines.append(
+ (
+ 0,
+ 0,
+ {
+ "product_id": line.product_id.id,
+ "quantity": line.product_uom_qty,
+ "origin_so": self.origin.id,
+ },
+ )
+ )
+
+ # hapus line lama lalu isi baru
+ self.gudang_service_lines = [(5, 0, 0)] + lines
+
+
+class GudangServiceLine(models.Model):
+ _name = "gudang.service.line"
+ _inherit = ["mail.thread", "mail.activity.mixin"]
+
+ product_id = fields.Many2one("product.product", string="Product")
+ quantity = fields.Float(string="Quantity")
+ origin_so = fields.Many2one("sale.order", string="Origin SO")
+ gudang_service_id = fields.Many2one("gudang.service", string="Gudang Service ID")
diff --git a/indoteknik_custom/models/kartu_stock.py b/indoteknik_custom/models/kartu_stock.py
new file mode 100644
index 00000000..22f90df0
--- /dev/null
+++ b/indoteknik_custom/models/kartu_stock.py
@@ -0,0 +1,186 @@
+from odoo import models
+from io import BytesIO
+import datetime
+from base64 import encodebytes
+import xlsxwriter
+
+
+class KartuStokWizardInherit(models.TransientModel):
+ _inherit = 'kartu.stok.wizard'
+
+
+ def action_kartu_stok_excel_single_sheet(self):
+
+ active_ids_tmp = self.env.context.get('active_ids')
+ active_model = self.env.context.get('active_model')
+
+ if active_model == 'product.template':
+ active_ids = self.env['product.product'].search(
+ [('product_tmpl_id', 'in', active_ids_tmp),
+ ('active', '=', True)]).ids
+ else:
+ active_ids = active_ids_tmp
+
+ data = {
+ 'location_id': self.location_id.id,
+ 'day_date': self.day_date,
+ 'previous_number_days': self.previous_number_days,
+ 'date_from': self.date_from,
+ 'date_to': self.date_to,
+ 'ids': active_ids,
+ 'context': {'active_model': active_model}
+ }
+
+ file_io = BytesIO()
+ workbook = xlsxwriter.Workbook(file_io)
+
+ self.generate_xlsx_single_sheet(workbook, data)
+
+ workbook.close()
+
+ fout = encodebytes(file_io.getvalue())
+ datetime_string = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
+ filename = f'Kartu_Stok_Single_{datetime_string}.xlsx'
+
+ self.write({
+ 'fileout': fout,
+ 'fileout_filename': filename
+ })
+
+ file_io.close()
+
+ return {
+ 'type': 'ir.actions.act_url',
+ 'target': 'new',
+ 'url': 'web/content/?model=' + self._name +
+ '&id=' + str(self.id) +
+ '&field=fileout&download=true&filename=' + filename,
+ }
+
+ def generate_xlsx_single_sheet(self, workbook, data):
+
+ bold = workbook.add_format({'bold': True})
+ border_date_right = workbook.add_format({'border':1, 'num_format': 'DD-MM', 'bg_color': '#dddddd', 'align': 'right'})
+ border_int_right = workbook.add_format({'border':1, 'num_format': '#,##0', 'bg_color': '#dddddd', 'align': 'right'})
+ border_int_right_bold = workbook.add_format({'border':1, 'num_format': '#,##0', 'bg_color': '#dddddd', 'align': 'right', 'bold': True})
+ border_text_center = workbook.add_format({'border':1, 'bg_color': '#dddddd', 'align': 'center'})
+ header_format = workbook.add_format({'bold': True, 'border':1, 'bg_color': '#808080', 'align': 'center'})
+
+ sheet = workbook.add_worksheet('Kartu Stok')
+
+ docs = self.env['product.product'].browse(data['ids'])
+ location = self.env['stock.location'].browse(data['location_id'])
+ location_name = location.display_name.split('/')[0]
+
+ row = 0
+
+ for doc in docs:
+
+ # =========================
+ # HEADER PRODUCT
+ # =========================
+ sheet.write(row, 0, doc.display_name, bold)
+ row += 1
+ sheet.write(row, 0, location_name, bold)
+ row += 2
+
+ # =========================
+ # TABLE HEADER
+ # =========================
+ sheet.write(row, 0, 'Date', header_format)
+ sheet.write(row, 1, 'In', header_format)
+ sheet.write(row, 2, 'Out', header_format)
+ sheet.write(row, 3, 'Stock', header_format)
+ sheet.write(row, 4, 'Distributor', header_format)
+ sheet.write(row, 5, 'Buyer', header_format)
+ sheet.write(row, 6, 'Document', header_format)
+ sheet.write(row, 7, 'Source Document', header_format)
+ row += 1
+
+ stock_total = 0
+ stock_show_initial = False
+
+ # =========================
+ # MOVE LOOP (SAMA LOGIC ASLI)
+ # =========================
+ for move in doc.stock_move_ids.sorted(key=lambda sm: sm.date):
+ for line in move.move_line_ids:
+
+ if line.state != 'done':
+ continue
+
+ if line.location_id.id != data['location_id'] and \
+ line.location_dest_id.id != data['location_id']:
+ continue
+
+ if not stock_show_initial:
+ sheet.write(row, 3, stock_total, border_int_right_bold)
+ sheet.write(row, 4, 'Initial Stock', border_text_center)
+ stock_show_initial = True
+ row += 1
+
+ qty_in = 0
+ qty_out = 0
+
+ if line.location_dest_id.id == data['location_id']:
+ qty_in = line.qty_done
+ stock_total += qty_in
+
+ if line.location_id.id == data['location_id']:
+ qty_out = line.qty_done
+ stock_total -= qty_out
+
+ sheet.write(row, 0, line.date, border_date_right)
+ sheet.write(row, 1, qty_in, border_int_right)
+ sheet.write(row, 2, qty_out, border_int_right)
+ sheet.write(row, 3, stock_total, border_int_right)
+ # Distributor
+ col = 4
+ if line.location_dest_id.id == data['location_id']:
+ if line.picking_id and line.picking_id.origin:
+ sheet.write(row, col, line.picking_id.partner_id.display_name, border_text_center)
+ else:
+ if line.location_id:
+ if line.location_id.name == 'Inventory adjustment':
+ sheet.write(row, col, 'Adjust *', border_text_center)
+ else:
+ sheet.write(row, col, line.location_id.location_id.name + ' *', border_text_center)
+ else:
+ sheet.write(row, col, doc.seller_ids[0].name + ' *' if doc.seller_ids else '', border_text_center)
+ else:
+ sheet.write(row, col, '', border_text_center)
+ # Buyer
+ col = 5
+ if line.location_id.id == data['location_id']:
+ if line.picking_id and line.picking_id.origin:
+ sheet.write(row, col, line.picking_id.partner_id.display_name, border_text_center)
+ else:
+ if line.location_dest_id:
+ if line.location_dest_id.name == 'Inventory adjustment':
+ sheet.write(row, col, 'Adjust *', border_text_center)
+ else:
+ sheet.write(row, col, line.location_dest_id.location_id.name + ' *', border_text_center)
+ else:
+ sheet.write(row, col, doc.seller_ids[0].name + ' *' if doc.seller_ids else '', border_text_center)
+ else:
+ sheet.write(row, col, '', border_text_center)
+ # Document
+ col = 6
+ if line.picking_id and line.picking_id.origin:
+ sheet.write(row, col, line.picking_id.name, border_text_center)
+ else:
+ sheet.write(row, col, line.reference or '', border_text_center)
+ # Source Document
+ col = 7
+ if line.picking_id and line.picking_id.origin:
+ sheet.write(row, col, line.picking_id.origin, border_text_center)
+ else:
+ sheet.write(row, col, line.reference or '', border_text_center)
+
+ row += 1
+
+ row += 3 # jarak antar product
+
+
+
+
diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py
new file mode 100644
index 00000000..3fa9dd72
--- /dev/null
+++ b/indoteknik_custom/models/keywords.py
@@ -0,0 +1,253 @@
+from itertools import product
+from multiprocessing import Condition
+from odoo import fields, models, api, tools, _
+import logging
+import re
+import pysolr
+from odoo.exceptions import UserError
+import base64
+import xlrd, xlwt
+import io
+
+
+_logger = logging.getLogger(__name__)
+solr = pysolr.Solr('http://10.148.0.5:8983/solr/searchkey/', always_commit=True, timeout=30)
+# solr = pysolr.Solr('http://127.0.0.1:8983/solr/searchkey/', always_commit=True, timeout=30)
+
+class Keywords(models.Model):
+ _name = 'keywords'
+ _order= 'id desc'
+
+ category_id = fields.Many2one('product.public.category', string='Category', required=True, help="Category to filter products when generating products for this keyword and to throw to solr")
+ keywords = fields.Char('Keywords', required=True)
+ product_ids = fields.Many2many(
+ 'product.product',
+ 'keywords_product_rel',
+ 'keyword_id',
+ 'product_id',
+ string='Products'
+ )
+ name = fields.Char('Name', compute="_compute_name")
+ skip = fields.Boolean('Skip Generate Product', default=False, help="If checked, the system will not generate products for this keyword")
+ url = fields.Char('Website URL', compute="_compute_url", help="Generated website url based on keywords")
+ sum = fields.Integer('Total Product', compute="_compute_total_product", readonly=True, help="Total products found for this keyword including variants")
+ solr_flag = fields.Integer(string='Solr Flag', default=0, help="0=no sync needed, 2=needs sync, 1=queued")
+
+ @api.depends('product_ids')
+ def _compute_total_product(self):
+ for record in self:
+ record.sum = len(record.product_ids)
+
+ @api.depends('keywords')
+ def _compute_url(self):
+ prefix = "https://indoteknik.com/searchkey/"
+ for record in self:
+ if record.keywords:
+ slug = re.sub(r'[^a-zA-Z0-9]+', '-', record.keywords.strip().lower())
+ slug = slug.strip('-')
+ record.url = prefix + slug
+ else:
+ record.url = False
+
+ def _compute_name(self):
+ for record in self:
+ if not record.name:
+ record.name = record.keywords
+
+ @api.constrains('keywords', 'category_id')
+ def check_already_exist(self):
+ model = self.env['keywords']
+ for record in self:
+ match = model.search([
+ ('keywords', '=', record.keywords),
+ ('category_id.id', '=', record.category_id.id),
+ ('id', '!=', record.id)
+ ])
+ if match:
+ raise UserError("Tidak bisa create karena keywords sudah dipakai")
+
+ def copy(self):
+ raise UserError("Duplicate Record not allowed")
+
+
+ def clear_products(self):
+ for record in self:
+ record.product_ids = [(5, 0, 0)]
+
+ def generate_products(self):
+ for record in self:
+ if not record.keywords or record.skip:
+ continue
+
+ keyword = f"%{record.keywords.strip()}%"
+
+ # AND (pt.unpublished IS FALSE OR pt.unpublished IS NULL)
+ sql = """
+ SELECT DISTINCT pp.id
+ FROM product_product pp
+ JOIN product_template pt ON pt.id = pp.product_tmpl_id
+ JOIN product_public_category_product_template_rel rel
+ ON rel.product_template_id = pt.id
+ WHERE
+ pt.product_rating >= 8
+ AND pp.active IS TRUE
+ AND (
+ pt.name ILIKE %s
+ OR pt.website_description ILIKE %s
+ )
+ """
+
+ params = [
+ keyword,
+ keyword,
+ ]
+
+ if record.category_id:
+ child_categs = self.env['product.public.category'].search([
+ ('id', 'child_of', record.category_id.id)
+ ])
+ sql += " AND rel.product_public_category_id = ANY(%s)"
+ params.append(child_categs.ids)
+
+ self.env.cr.execute(sql, params)
+
+ rows = self.env.cr.fetchall()
+ product_ids = [r[0] for r in rows]
+
+ if not product_ids:
+ raise UserError(
+ f"Tidak berhasil menemukan barang untuk keyword '{record.keywords}'"
+ )
+
+ record.with_context(skip_generate=True).write({
+ 'product_ids': [(6, 0, product_ids)]
+ })
+
+ _logger.info(
+ "Product Found: Found %s products for keyword '%s'",
+ len(product_ids),
+ record.keywords
+ )
+
+ @api.onchange('keywords', 'category_id', 'product_ids')
+ def _onchange_solr_flag(self):
+ """Set solr_flag=2 when tracked fields change to trigger queue sync"""
+ for record in self:
+ if len(record.product_ids) > 0:
+ record.solr_flag = 2
+
+ def solr_flag_to_queue(self, limit=500):
+ """Find keywords with solr_flag=2 and create apache.solr.queue entries"""
+ keywords = self.search([('solr_flag', '=', 2)], limit=limit)
+
+ for keyword in keywords:
+ # Create unique queue entry
+ queue_obj = self.env['apache.solr.queue']
+ queue_obj.create_unique({
+ 'res_model': 'keywords',
+ 'res_id': keyword.id,
+ 'function_name': '_sync_keywords_queue_callback'
+ })
+
+ # Set flag to indicate queued
+ keyword.solr_flag = 1
+
+ if keywords:
+ _logger.info(
+ 'Queued %s keywords for Solr synchronization',
+ len(keywords)
+ )
+
+ return True
+
+ def _sync_keywords_queue_callback(self):
+ success_keywords = self.browse()
+
+ for keyword in self:
+ if not keyword.product_ids:
+ _logger.info(
+ 'Skipping Solr sync for keyword "%s" - no products found',
+ keyword.keywords
+ )
+ continue
+
+ searchkey = (keyword.keywords or '').strip().lower().replace(' ', '-')
+
+ try:
+ doc = {
+ 'id': keyword.id,
+ 'category_id_i': keyword.category_id.id,
+ 'keywords_s': searchkey,
+ 'url_s': keyword.url,
+ 'product_ids_is': [p.product_tmpl_id.id for p in keyword.product_ids],
+ }
+
+ solr.add([doc])
+
+ success_keywords |= keyword
+
+ except Exception as e:
+ _logger.error(
+ "Solr sync failed for keyword ID %s: %s",
+ keyword.id, e
+ )
+
+ if success_keywords:
+ success_keywords.write({'solr_flag': 0})
+
+ return True
+
+ def sync_solr(self):
+ """Manual sync method for active_ids context (backward compatibility)"""
+ active_ids = self.env.context.get('active_ids', [])
+ if not active_ids:
+ _logger.warning("No active_ids found, nothing to sync")
+ return True
+
+ keywords = self.browse(active_ids)
+
+ documents = []
+ for keyword in keywords:
+ # Skip syncing if product count is 0
+ if len(keyword.product_ids) == 0:
+ _logger.info('Skipping Solr sync for keyword "%s" - no products found', keyword.keywords)
+ continue
+
+ searchkey = (keyword.keywords or '').strip().lower().replace(' ', '-')
+ try:
+ doc = {
+ 'id': keyword.id,
+ 'category_id_i': keyword.category_id.id,
+ 'keywords_s': searchkey,
+ 'url_s': keyword.url,
+ 'product_ids_is': [p.product_tmpl_id.id for p in keyword.product_ids],
+ }
+ documents.append(doc)
+ except Exception as e:
+ _logger.error('failed %s', e)
+ _logger.error('doc data: %s', doc)
+
+ if documents:
+ solr.add(documents)
+
+ return True
+
+ @api.model
+ def create(self, vals):
+ record = super().create(vals)
+ # self.check_already_exist()
+ # record.generate_products()
+ return record
+
+ def write(self, vals):
+ result = super().write(vals)
+
+ tracked_fields = ['keywords', 'category_id', 'product_ids', 'skip', 'name']
+ neded_sync = any(field in vals for field in tracked_fields)
+ if neded_sync:
+ for record in self:
+ # Only flag for sync if there are products
+ if len(record.product_ids) > 0:
+ record.solr_flag = 2
+
+ return result
diff --git a/indoteknik_custom/models/price_group.py b/indoteknik_custom/models/price_group.py
index fce78fff..1a4f1bd6 100644
--- a/indoteknik_custom/models/price_group.py
+++ b/indoteknik_custom/models/price_group.py
@@ -10,16 +10,16 @@ class PriceGroup(models.Model):
name = fields.Char(string='Name')
pricelist_id = fields.Many2one('product.pricelist', string='Price List')
- group1 = fields.Float(string='Kelompok 1 (%)')
- group2 = fields.Float(string='Kelompok 2 (%)')
- group3 = fields.Float(string='Kelompok 3 (%)')
- group4 = fields.Float(string='Kelompok 4 (%)')
- group5 = fields.Float(string='Kelompok 5 (%)')
- group6 = fields.Float(string='Kelompok 6 (%)')
- group7 = fields.Float(string='Kelompok 7 (%)')
- group8 = fields.Float(string='Kelompok 8 (%)')
- group9 = fields.Float(string='Kelompok 9 (%)')
- group10 = fields.Float(string='Kelompok 10 (%)')
+ group1 = fields.Float(string='Kelompok 1 (%)', digits=(16, 12))
+ group2 = fields.Float(string='Kelompok 2 (%)', digits=(16, 12))
+ group3 = fields.Float(string='Kelompok 3 (%)', digits=(16, 12))
+ group4 = fields.Float(string='Kelompok 4 (%)', digits=(16, 12))
+ group5 = fields.Float(string='Kelompok 5 (%)', digits=(16, 12))
+ group6 = fields.Float(string='Kelompok 6 (%)', digits=(16, 12))
+ group7 = fields.Float(string='Kelompok 7 (%)', digits=(16, 12))
+ group8 = fields.Float(string='Kelompok 8 (%)', digits=(16, 12))
+ group9 = fields.Float(string='Kelompok 9 (%)', digits=(16, 12))
+ group10 = fields.Float(string='Kelompok 10 (%)', digits=(16, 12))
def collect_price_group(self):
PRICE_GROUP_ID = {
diff --git a/indoteknik_custom/models/product_template.py b/indoteknik_custom/models/product_template.py
index 2c798f43..e10b4de2 100755
--- a/indoteknik_custom/models/product_template.py
+++ b/indoteknik_custom/models/product_template.py
@@ -1,3 +1,4 @@
+from this import s
from odoo import fields, models, api, tools, _
from datetime import datetime, timedelta, date
from odoo.exceptions import UserError
@@ -78,7 +79,7 @@ class ProductTemplate(models.Model):
('sp', 'Spare Part'),
('acc', 'Accessories')
], string='Kind of', copy=False)
- sni = fields.Boolean(string='SNI')
+ sni = fields.Boolean(string='SNI')
tkdn = fields.Boolean(string='TKDN')
short_spesification = fields.Char(string='Short Spesification')
merchandise_ok = fields.Boolean(string='Product Promotion')
@@ -94,7 +95,7 @@ class ProductTemplate(models.Model):
raise UserError('Hanya MD yang bisa membuat Product')
result = super(ProductTemplate, self).create(vals)
return result
-
+
# def write(self, values):
# group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id
# users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])])
@@ -121,7 +122,7 @@ class ProductTemplate(models.Model):
# qr_code_img = base64.b64encode(buffer.getvalue()).decode()
# rec.qr_code = qr_code_img
-
+
@api.constrains('name', 'internal_reference', 'x_manufacture')
def required_public_categ_ids(self):
for rec in self:
@@ -134,7 +135,7 @@ class ProductTemplate(models.Model):
def day_product_to_edit(self):
day_products = []
-
+
for product in self:
day_product = (product.write_date - product.create_date).days
day_products.append(day_product)
@@ -147,25 +148,25 @@ class ProductTemplate(models.Model):
variants = product.product_variant_ids
names = [x.name for x in variants] if variants else [product.name]
default_codes = [x.default_code for x in variants] if variants else [product.default_code]
-
+
domain = [
- ('default_code', '!=', False),
+ ('default_code', '!=', False),
('id', '!=', product.id),
- '|',
- ('name', 'in', names),
+ '|',
+ ('name', 'in', names),
('default_code', 'in', default_codes)
]
-
+
product_exist = self.search(domain, limit=1)
if len(product_exist) > 0:
raise UserError('Name atau Internal Reference sudah digunakan pada produk lain')
-
+
if self.env.user.is_purchasing_manager or self.env.user.is_editor_product or self.env.user.id in [1, 25]:
continue
-
+
if sum(product.day_product_to_edit()) > 0:
raise UserError('Produk ini tidak dapat diubah')
-
+
@api.constrains('name')
def _validate_name(self):
rule_regex = self.env['ir.config_parameter'].sudo().get_param('product.product.rule_name_regex') or ''
@@ -174,7 +175,7 @@ class ProductTemplate(models.Model):
pattern_suggest = rf"{rule_regex}"
suggest = ''.join(re.findall(pattern_suggest, self.name))
raise UserError(f'Contoh yang benar adalah {suggest}')
-
+
# def write(self, vals):
# if 'solr_flag' not in vals and self.solr_flag == 1:
# vals['solr_flag'] = 2
@@ -213,7 +214,7 @@ class ProductTemplate(models.Model):
def unlink(self):
if self._name == 'product.template':
raise UserError('Maaf anda tidak bisa delete product')
-
+
def update_new_product(self):
current_time = datetime.now()
delta_time = current_time - timedelta(days=30)
@@ -244,7 +245,7 @@ class ProductTemplate(models.Model):
for template in templates:
if not template.default_code:
template.default_code = 'IT.'+str(template.id)
-
+
for variant in template.product_variant_ids:
if not variant.default_code:
variant.default_code = 'ITV.%s' % str(variant.id)
@@ -336,7 +337,7 @@ class ProductTemplate(models.Model):
def _get_stock_website(self):
qty = self._get_stock_altama()
print(qty)
-
+
def get_stock_altama(self, item_code):
current_time = datetime.now()
current_time = current_time.strftime('%Y-%m-%d %H:%M:%S')
@@ -347,7 +348,7 @@ class ProductTemplate(models.Model):
token = token_data['access_token']
else:
token = token_data.access_token
-
+
url = "https://erpapi.altama.co.id/erp/api/stock/buffer/btob"
auth = "Bearer "+token
headers = {
@@ -361,7 +362,7 @@ class ProductTemplate(models.Model):
response = requests.post(url, headers=headers, json=json_data)
if response.status_code != 200:
return 0
-
+
datas = json.loads(response.text)['data']
qty = 0
for data in datas:
@@ -380,12 +381,12 @@ class ProductTemplate(models.Model):
data = {
'grant_type': 'client_credentials',
}
-
+
response = requests.post(url, headers=headers, data=data).json()
lookup_json = json.dumps(response, indent=4, sort_keys=True)
token = json.loads(lookup_json)['access_token']
expires_in = json.loads(lookup_json)['expires_in']
-
+
current_time = datetime.now()
delta_time = current_time + timedelta(seconds=int(expires_in))
@@ -401,18 +402,18 @@ class ProductTemplate(models.Model):
self.env['token.storage'].create([values])
return values
- # ==============================
+ # ==============================
def get_vendor_name(self, rec):
"""Get formatted name for vendor/supplier"""
return rec.name.name if rec.name else f"ID {rec.id}"
-
+
def get_attribute_line_name(self, rec):
"""Get formatted name for attribute line"""
if rec.attribute_id and rec.value_ids:
values = ", ".join(rec.value_ids.mapped('name'))
return f"{rec.attribute_id.name}: {values}"
return f"ID {rec.id}"
-
+
def _get_vendor_field_label(self, field_name):
"""Get human-readable label for vendor fields"""
field_labels = {
@@ -428,10 +429,10 @@ class ProductTemplate(models.Model):
'date_end': 'End Date',
'min_qty': 'Quantity'
}
- return field_labels.get(field_name, field_name.replace('_', ' ').title())
+ return field_labels.get(field_name, field_name.replace('_', ' ').title())
# ==============================
-
+
def _collect_old_values(self, vals):
"""Collect old values before write"""
return {
@@ -442,7 +443,7 @@ class ProductTemplate(models.Model):
}
for record in self
}
-
+
def _prepare_attribute_line_info(self):
"""Prepare attribute line info for logging and update comparison"""
line_info = {}
@@ -455,7 +456,7 @@ class ProductTemplate(models.Model):
'value_names': ", ".join(line.value_ids.mapped('name'))
}
return line_info
-
+
def _prepare_vendor_info(self):
"""Prepare vendor info for logging before they are deleted"""
vendor_info = {}
@@ -474,13 +475,13 @@ class ProductTemplate(models.Model):
'date_end': seller.date_end,
}
return vendor_info
-
+
# ==========================
-
+
def _get_context_with_all_info(self, vals):
"""Get context with all necessary info (attributes and vendors)"""
context = dict(self.env.context)
-
+
# Check for attribute line changes
if 'attribute_line_ids' in vals:
attribute_line_info = {}
@@ -488,7 +489,7 @@ class ProductTemplate(models.Model):
product_line_info = product._prepare_attribute_line_info()
attribute_line_info.update(product_line_info)
context['attribute_line_info'] = attribute_line_info
-
+
# Check for vendor changes - store both for deletion and for comparing old values
if 'seller_ids' in vals:
vendor_info = {}
@@ -497,18 +498,18 @@ class ProductTemplate(models.Model):
# For deletion logging
product_vendor_info = product._prepare_vendor_info()
vendor_info.update(product_vendor_info)
-
+
# For update comparison
product_vendor_old = product._prepare_vendor_info()
vendor_old_values.update(product_vendor_old)
-
+
context['vendor_info'] = vendor_info
context['vendor_old_values'] = vendor_old_values
-
+
return context
-
+
# ========================
-
+
def _log_image_changes(self, field_name, old_val, new_val):
"""Log image field changes"""
label_map = {
@@ -526,18 +527,18 @@ class ProductTemplate(models.Model):
elif old_val != new_val:
return f"<li><b>{label}</b>: image updated</li>"
return None
-
+
def _log_attribute_line_changes(self, commands):
"""Log changes to attribute lines with complete information"""
# Get stored info from context
stored_info = self.env.context.get('attribute_line_info', {})
-
+
for cmd in commands:
if cmd[0] == 0: # Add
new = self.env['product.template.attribute.line'].new(cmd[2])
attribute_name = new.attribute_id.name if new.attribute_id else 'Attribute'
values = ", ".join(new.value_ids.mapped('name')) if new.value_ids else ''
-
+
message = f"<b>Product Attribute</b>:<br/>{attribute_name} added<br/>"
if values:
message += f"Values: '{values}'"
@@ -546,7 +547,7 @@ class ProductTemplate(models.Model):
elif cmd[0] == 1: # Update
rec_id = cmd[1]
vals = cmd[2]
-
+
# Get old values from context
old_data = stored_info.get(rec_id, {})
if not old_data:
@@ -561,10 +562,10 @@ class ProductTemplate(models.Model):
'value_ids': [(v.id, v.name) for v in rec.value_ids],
'value_names': ", ".join(rec.value_ids.mapped('name'))
}
-
+
changes = []
attribute_name = old_data.get('attribute_name', 'Attribute')
-
+
# Check for attribute change
if 'attribute_id' in vals:
old_attr = old_data.get('attribute_name', '-')
@@ -576,7 +577,7 @@ class ProductTemplate(models.Model):
# Check for value changes
if 'value_ids' in vals:
old_vals = old_data.get('value_names', '')
-
+
# Parse the command for value_ids
new_value_ids = []
for value_cmd in vals['value_ids']:
@@ -588,14 +589,14 @@ class ProductTemplate(models.Model):
elif value_cmd[0] == 3: # Remove
# This is more complex, would need current state
pass
-
+
# Get new value names
if new_value_ids:
new_values = self.env['product.attribute.value'].browse(new_value_ids)
new_vals = ", ".join(new_values.mapped('name'))
else:
new_vals = ""
-
+
if old_vals != new_vals:
changes.append(f"Values: '{old_vals}' → '{new_vals}'")
@@ -605,7 +606,7 @@ class ProductTemplate(models.Model):
message += "<br/>".join(changes)
self.message_post(body=message)
- elif cmd[0] in (2, 3): # Remove
+ elif cmd[0] in (2, 3): # Remove
# Use info from stored data
line_data = stored_info.get(cmd[1])
if line_data:
@@ -619,7 +620,7 @@ class ProductTemplate(models.Model):
else:
attribute_name = 'Attribute'
values = f"ID {cmd[1]}"
-
+
message = f"<b>Product Attribute</b>:<br/>{attribute_name} removed<br/>"
if values:
message += f"Values: '{values}'"
@@ -627,25 +628,25 @@ class ProductTemplate(models.Model):
elif cmd[0] == 5: # Clear all
self.message_post(body=f"<b>Product Attribute</b>:<br/>All attributes removed")
-
+
def _log_vendor_pricelist_changes(self, commands):
"""Log changes to vendor pricelist with complete information"""
# Get stored info from context
stored_info = self.env.context.get('vendor_info', {})
old_values_info = self.env.context.get('vendor_old_values', {})
-
+
for cmd in commands:
if cmd[0] == 0: # Add
vals = cmd[2]
-
+
# Create temporary record to get proper display values
temp_values = vals.copy()
temp_values['product_tmpl_id'] = self.id
new = self.env['product.supplierinfo'].new(temp_values)
-
+
name = self.get_vendor_name(new)
details = []
-
+
if 'price' in vals and vals['price'] is not None:
details.append(f"<li>Price: {vals['price']}</li>")
if 'min_qty' in vals and vals['min_qty'] is not None:
@@ -662,18 +663,18 @@ class ProductTemplate(models.Model):
if 'product_uom' in vals and vals['product_uom']:
uom = self.env['uom.uom'].browse(vals['product_uom'])
details.append(f"<li>Unit of Measure: {uom.name}</li>")
-
+
if details:
detail_str = f" with:<ul>{''.join(details)}</ul>"
else:
detail_str = ""
-
+
self.message_post(body=f"<b>Vendor Pricelist</b>: added '{name}'{detail_str}")
-
+
elif cmd[0] == 1: # Update
rec_id = cmd[1]
vals = cmd[2]
-
+
# Get old values from context
old_data = old_values_info.get(rec_id, {})
if not old_data:
@@ -694,10 +695,10 @@ class ProductTemplate(models.Model):
'date_start': rec.date_start,
'date_end': rec.date_end,
}
-
+
name = old_data.get('name', f'ID {rec_id}')
changes = []
-
+
# Check each field in vals for changes
for field, new_value in vals.items():
if field == 'name':
@@ -707,9 +708,9 @@ class ProductTemplate(models.Model):
new_name = self.env['res.partner'].browse(new_value).name if new_value else 'None'
changes.append(f"<li>Vendor: {old_name} → {new_name}</li>")
continue
-
+
old_value = old_data.get(field)
-
+
# Format values based on field type
if field == 'currency_id':
if old_value != new_value:
@@ -741,14 +742,14 @@ class ProductTemplate(models.Model):
# Compare numeric values properly
old_num = float(old_value) if old_value is not None else 0.0
new_num = float(new_value) if new_value is not None else 0.0
-
+
if field == 'delay': # Integer field
old_num = int(old_num)
new_num = int(new_num)
-
+
if old_num == new_num:
continue
-
+
old_str = str(old_value) if old_value is not None else 'None'
new_str = str(new_value) if new_value is not None else 'None'
else:
@@ -757,20 +758,20 @@ class ProductTemplate(models.Model):
continue
old_str = str(old_value) if old_value is not None else 'None'
new_str = str(new_value) if new_value is not None else 'None'
-
+
label = self._get_vendor_field_label(field)
changes.append(f"<li>{label}: {old_str} → {new_str}</li>")
-
+
if changes:
changes_str = f"<ul>{''.join(changes)}</ul>"
self.message_post(body=f"<b>Vendor Pricelist</b>: updated '{name}':{changes_str}")
-
+
elif cmd[0] in (2, 3): # Remove
vendor_data = stored_info.get(cmd[1])
if vendor_data:
name = vendor_data['name']
details = []
-
+
if vendor_data.get('price'):
details.append(f"<li>Price: {vendor_data['price']}</li>")
if vendor_data.get('min_qty'):
@@ -779,7 +780,7 @@ class ProductTemplate(models.Model):
details.append(f"<li>Product Name: {vendor_data['product_name']}</li>")
if vendor_data.get('delay'):
details.append(f"<li>Delivery Lead Time: {vendor_data['delay']}</li>")
-
+
if details:
detail_str = f"<ul>{''.join(details)}</ul>"
else:
@@ -795,7 +796,7 @@ class ProductTemplate(models.Model):
details.append(f"<li>Quantity: {rec.min_qty}</li>")
if rec.product_name:
details.append(f"<li>Product Name: {rec.product_name}</li>")
-
+
if details:
detail_str = f"<ul>{''.join(details)}</ul>"
else:
@@ -803,12 +804,12 @@ class ProductTemplate(models.Model):
else:
name = f"ID {cmd[1]}"
detail_str = ""
-
+
self.message_post(body=f"<b>Vendor Pricelist</b>: removed '{name}'{detail_str}")
-
+
elif cmd[0] == 5: # Clear all
self.message_post(body=f"<b>Vendor Pricelist</b>: all removed")
-
+
def _log_field_changes_product(self, vals, old_values):
"""Revised - Log general field changes for product template without posting to variants"""
exclude_fields = ['solr_flag', 'desc_update_solr', 'last_update_solr', 'is_edited']
@@ -892,6 +893,7 @@ class ProductTemplate(models.Model):
result = super().write(vals)
# Log changes
self._log_field_changes_product(vals, old_values)
+
return result
# def write(self, vals):
@@ -900,7 +902,7 @@ class ProductTemplate(models.Model):
# # raise UserError('Tidak dapat mengubah produk sementara')
# self._log_field_changes(vals)
# return super(ProductTemplate, self).write(vals)
-
+
class ProductProduct(models.Model):
_inherit = "product.product"
web_price = fields.Float(
@@ -938,7 +940,7 @@ class ProductProduct(models.Model):
qr_code_variant = fields.Binary("QR Code Variant", compute='_compute_qr_code_variant')
qty_pcs_box = fields.Float("Pcs Box")
barcode_box = fields.Char("Barcode Box")
-
+ # keyword_id = fields.Many2one('keywords', string='Keyword')
has_magento = fields.Boolean(string='Has Magento?', default=False, readonly=True)
def generate_product_sla(self):
@@ -962,7 +964,7 @@ class ProductProduct(models.Model):
raise UserError('Hanya MD yang bisa membuat Product')
result = super(ProductProduct, self).create(vals)
return result
-
+
# def write(self, values):
# group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id
# active_model = self.env.context.get('active_model')
@@ -978,7 +980,7 @@ class ProductProduct(models.Model):
if not rec.active:
rec.qr_code_variant = False # Clear the QR Code for archived variants
continue
-
+
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
@@ -1066,11 +1068,11 @@ class ProductProduct(models.Model):
if self.qty_available_bandengan < qty_purchase:
return 'harus beli'
return 'masih cukup'
-
+
def _get_qty_upcoming(self):
for product in self:
product.qty_upcoming = product.incoming_qty + product.qty_available
-
+
def _get_qty_sold(self):
for product in self:
order_line = self.env['sale.order.line'].search([
@@ -1081,13 +1083,13 @@ class ProductProduct(models.Model):
def day_product_to_edit(self):
day_products = []
-
+
for product in self:
day_product = (product.write_date - product.create_date).days
day_products.append(day_product)
return day_products
-
+
@api.constrains('name')
def _validate_name(self):
rule_regex = self.env['ir.config_parameter'].sudo().get_param('product.product.rule_name_regex') or ''
@@ -1096,7 +1098,7 @@ class ProductProduct(models.Model):
pattern_suggest = rf"{rule_regex}"
suggest = ''.join(re.findall(pattern_suggest, self.name))
raise UserError(f'Contoh yang benar adalah {suggest}')
-
+
def _get_qty_incoming_bandengan(self):
for product in self:
qty = self.env['v.move.outstanding'].read_group(
@@ -1218,11 +1220,11 @@ class ProductProduct(models.Model):
for product in self:
stock_vendor = self.env['stock.vendor'].search([('product_variant_id', '=', product.id)], limit=1)
product.qty_stock_vendor = stock_vendor.quantity + product.qty_available
-
+
def unlink(self):
if self._name == 'product.product':
raise UserError('Maaf anda tidak bisa delete product')
-
+
def _get_active_flash_sale(self):
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
pricelist = self.env['product.pricelist'].search([
@@ -1248,7 +1250,7 @@ class ProductProduct(models.Model):
def _log_field_changes_product_variants(self, vals, old_values):
"""Revised - Log field changes for variants without posting to template"""
exclude_fields = ['solr_flag', 'desc_update_solr', 'last_update_solr', 'is_edited']
-
+
# Custom labels for image fields
custom_labels = {
'image_1920': 'Main Image',
@@ -1325,7 +1327,8 @@ class ProductProduct(models.Model):
'public_categ_ids', 'search_rank', 'search_rank_weekly',
'image_1920', 'unpublished', 'image_carousel_lines'
]
-
+ # pake ini kalau mau Cek semua field
+ # if vals:
if any(field in vals for field in tracked_fields):
old_values = self._collect_old_values(vals)
result = super().write(vals)
@@ -1354,7 +1357,7 @@ class OutstandingMove(models.Model):
tools.drop_view_if_exists(self.env.cr, self._table)
self.env.cr.execute("""
CREATE OR REPLACE VIEW %s AS
- select sm.id, sm.reference, sm.product_id,
+ select sm.id, sm.reference, sm.product_id,
sm.product_uom_qty as qty_need,
sm.location_id, sm.location_dest_id,
sm.raw_material_production_id as mo_id,
diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py
index 35fa79a8..244575ae 100755
--- a/indoteknik_custom/models/purchase_order.py
+++ b/indoteknik_custom/models/purchase_order.py
@@ -8,6 +8,8 @@ import io
import base64
from odoo.tools import lazy_property
import socket
+import requests
+import json
try:
from odoo.tools.misc import xlsxwriter
@@ -53,6 +55,7 @@ class PurchaseOrder(models.Model):
total_so_percent_margin = fields.Float(
'SO Margin%', compute='compute_total_margin',
help="Total % Margin in Sales Order Header")
+ amount_cashback = fields.Float('Cashback', compute = 'compute_total_margin', help = 'Total Cashback brand Altama')
amount_total_without_service = fields.Float('AmtTotalWithoutService', compute='compute_amt_total_without_service')
summary_qty_po = fields.Float('Total Qty', compute='_compute_summary_qty')
summary_qty_receipt = fields.Float('Summary Qty Receipt', compute='_compute_summary_qty')
@@ -124,7 +127,308 @@ class PurchaseOrder(models.Model):
)
overseas_po = fields.Boolean(string='PO Luar Negeri?', tracking=3, help='Centang jika PO untuk pembelian luar negeri')
+ order_altama_id = fields.Integer('Req Order Altama', copy=False)
+ soo_number = fields.Char('SOO Number', copy=False)
+ soo_price = fields.Float('SOO Price', copy=False)
+ soo_discount = fields.Float('SOO Discount', copy=False)
+ soo_tax = fields.Float('SOO Tax', copy=False)
+
+ def _get_altama_token(self, source='auto'):
+ ICP = self.env['ir.config_parameter'].sudo()
+ TokenLog = self.env['token.log'].sudo()
+
+ token_url = ICP.get_param('token.adempiere.altama')
+ client_id = ICP.get_param('client.adempiere.altama')
+ client_secret = ICP.get_param('secret_key.adempiere.altama')
+
+ active_token = TokenLog.search([
+ ('is_active', '=', True),
+ ('token_from', '=', 'Adempiere Altama'),
+ ('expires_at', '>', datetime.utcnow()),
+ ], limit=1, order='id desc')
+
+ if active_token:
+ return active_token.token
+
+ headers = {
+ "Authorization": "Basic " + base64.b64encode(f"{client_id}:{client_secret}".encode()).decode(),
+ "Content-Type": "application/x-www-form-urlencoded",
+ }
+ data = {"grant_type": "client_credentials"}
+
+ response = requests.post(token_url, data=data, headers=headers, timeout=15)
+ if response.status_code == 200:
+ result = response.json()
+ token = result.get("access_token")
+ expires_in = result.get("expires_in", 3600)
+ expiry_time = datetime.utcnow() + timedelta(seconds=expires_in - 60)
+
+ TokenLog.search([
+ ('token_from', '=', 'Adempiere Altama'),
+ ('is_active', '=', True),
+ ]).write({'is_active': False})
+
+ TokenLog.create({
+ 'token': token,
+ 'expires_at': expiry_time,
+ 'is_active': True,
+ 'created_by': self.env.user.id if self.env.user else None,
+ 'source': source,
+ 'token_from': 'Adempiere Altama',
+ })
+
+ return token
+
+ else:
+ raise Exception(f"Gagal ambil token: {response.status_code} - {response.text}")
+
+ def action_create_order_altama(self):
+ ICP = self.env['ir.config_parameter'].sudo()
+ for order in self:
+ try:
+ token = self._get_altama_token(source='manual')
+ url = ICP.get_param('endpoint.create.order.adempiere.altama')
+ headers = {
+ "Authorization": f"Bearer {token}",
+ "Content-Type": "application/json",
+ }
+
+ payload = {
+ "date_po": order.date_approve.strftime("%Y%m%d%H%M%S"),
+ "no_po": order.name,
+ "details": [
+ {
+ "item_code": line.product_id.default_code or "",
+ "price": line.price_unit,
+ "qty": line.product_qty,
+ }
+ for line in order.order_line
+ ],
+ }
+
+ response = requests.post(url, json=payload, headers=headers, timeout=20)
+
+ try:
+ result = response.json()
+ except json.JSONDecodeError:
+ raise Exception(f"Response bukan JSON valid: {response.text}")
+
+ if response.status_code == 200 and result.get("code") == "00":
+ contents = result.get("contents", {})
+ if isinstance(contents, dict):
+ order.order_altama_id = contents.get("req_id")
+ else:
+ order.order_altama_id = contents.get("req_id")
+
+ elif response.status_code == 404:
+ raise Exception("URL endpoint gak ditemukan (404). Pastikan path-nya benar di Altama API.")
+ elif response.status_code == 401:
+ token = self._get_altama_token(source='auto')
+ headers["Authorization"] = f"Bearer {token}"
+ response = requests.post(url, json=payload, headers=headers, timeout=20)
+ elif response.status_code not in (200, 201):
+ raise Exception(f"Gagal kirim ke Altama: {response.status_code} - {response.text}")
+
+ self.message_post(body=f"✅ PO berhasil dikirim ke Altama!\nResponse: {json.dumps(result, indent=2)}")
+
+ except Exception as e:
+ self.message_post(body=f"❌ Gagal kirim ke Altama:<br/><pre>{str(e)}</pre>")
+
+
+ def action_get_order_altama(self):
+ ICP = self.env['ir.config_parameter'].sudo()
+
+ for order in self:
+ try:
+ # ============================
+ # Get Token
+ # ============================
+ token = self._get_altama_token(source='manual')
+
+ url = ICP.get_param('endpoint.get.order.adempiere.altama')
+ if not url:
+ raise Exception("Parameter 'endpoint.adempiere.altama' belum diset di System Parameters.")
+
+ headers = {
+ "Authorization": f"Bearer {token}",
+ "Content-Type": "application/json",
+ }
+
+ params = {
+ "orderreq_id": order.order_altama_id or 0,
+ }
+
+ # ============================
+ # Request ke API
+ # ============================
+ response = requests.get(url, headers=headers, params=params, timeout=20)
+
+ if response.status_code == 401:
+ token = self._get_altama_token(source='auto')
+ headers["Authorization"] = f"Bearer {token}"
+ response = requests.get(url, headers=headers, params=params, timeout=20)
+ if response.status_code not in (200, 201):
+ raise Exception(f"Gagal ambil data dari Altama: {response.status_code} - {response.text}")
+
+ data = response.json()
+
+ # ============================
+ # Extract Data
+ # ============================
+ contents_root = data.get("contents", {})
+ contents_list = contents_root.get("contents", [])
+
+ if not isinstance(contents_list, list):
+ raise Exception("Format data contents dari Altama tidak sesuai (expected list).")
+
+ order.message_post(
+ body=f"✅ Berhasil ambil data dari Altama. Ditemukan {len(contents_list)} record."
+ )
+
+ # =====================================================
+ # LOOP MAIN DATA
+ # =====================================================
+ for item in contents_list:
+
+ req_id = item.get("req_id")
+ no_po = item.get("no_po")
+ list_item_po = item.get("list_Item_po", [])
+ list_soo = item.get("list_soo", [])
+
+ # ============================
+ # Isi Data SOO Ke Order
+ # ============================
+ soo_numbers = [s.get("no_soo") for s in list_soo if s.get("no_soo")]
+ unique_soo = list(set(soo_numbers))
+ if len(unique_soo) == 1:
+ order.soo_number = unique_soo[0]
+ if not order.picking_ids.number_soo:
+ # order.picking_ids[0].number_soo = unique_soo[0]
+ for picking in order.picking_ids:
+ picking.number_soo = unique_soo[0]
+ elif len(unique_soo) > 1:
+ order.soo_number = ", ".join(unique_soo)
+ if not order.picking_ids.number_soo:
+ # order.picking_ids[0].number_soo = ", ".join(unique_soo)
+ for picking in order.picking_ids:
+ picking.number_soo = ", ".join(unique_soo)
+ else:
+ order.soo_number = False
+
+ if list_soo:
+ first_soo = list_soo[0]
+ order.soo_price = first_soo.get("totalprice")
+ order.soo_discount = first_soo.get("diskon")
+ order.soo_tax = first_soo.get("ppn")
+
+ order.order_altama_id = req_id
+
+ # ============================
+ # Update Order Lines
+ # ============================
+ for item_line in list_item_po:
+
+ line = order.order_line.filtered(
+ lambda l: l.product_id.default_code == item_line.get("item_code")
+ )
+
+ if line:
+ line.write({
+ "description": item_line.get("description", ""),
+ "altama_ordered": item_line.get("qtyordered", 0),
+ "altama_delivered": item_line.get("qtydelivered", 0),
+ "altama_invoiced": item_line.get("qtyinvoiced", 0),
+ "docstatus_altama": item_line.get("docstatus", ""),
+ })
+
+ # =====================================================
+ # BUILD HTML TABLES FOR CHATTER
+ # =====================================================
+
+ # ---- SOO TABLE ----
+ soo_rows = ""
+ for s in list_soo:
+ soo_rows += f"""
+ <tr>
+ <td>{s.get('no_soo')}</td>
+ <td>{s.get('totalprice')}</td>
+ <td>{s.get('diskon')}</td>
+ <td>{s.get('ppn')}</td>
+ </tr>
+ """
+
+ soo_table = f"""
+ <table style="width:100%; border-collapse: collapse; margin-top: 10px;">
+ <thead>
+ <tr style="background:#f1f1f1;">
+ <th style="border:1px solid #ccc; padding:6px;">SOO Number</th>
+ <th style="border:1px solid #ccc; padding:6px;">Total Price</th>
+ <th style="border:1px solid #ccc; padding:6px;">Diskon</th>
+ <th style="border:1px solid #ccc; padding:6px;">PPN</th>
+ </tr>
+ </thead>
+ <tbody>
+ {soo_rows or '<tr><td colspan="4">Tidak ada data SOO</td></tr>'}
+ </tbody>
+ </table>
+ """
+
+ # ---- ITEM PO TABLE ----
+ po_rows = ""
+ for l in list_item_po:
+
+ desc = l.get("description") or ""
+
+ # Flag: row error kalau description tidak mulai dengan SOO/
+ is_error = desc and not desc.startswith("SOO/")
+
+ # Style row merah
+ row_style = "color:red; font-weight:bold;" if is_error else ""
+
+ po_rows += f"""
+ <tr style="{row_style}">
+ <td>{l.get('item_code')}</td>
+ <td>{desc}</td>
+ <td>{l.get('qtyordered')}</td>
+ <td>{l.get('qtydelivered')}</td>
+ <td>{l.get('qtyinvoiced')}</td>
+ <td>{l.get('docstatus')}</td>
+ </tr>
+ """
+
+
+ po_table = f"""
+ <table style="width:100%; border-collapse: collapse; margin-top: 10px;">
+ <thead>
+ <tr style="background:#f1f1f1;">
+ <th style="border:1px solid #ccc; padding:6px;">Item Code</th>
+ <th style="border:1px solid #ccc; padding:6px;">Description</th>
+ <th style="border:1px solid #ccc; padding:6px;">Ordered</th>
+ <th style="border:1px solid #ccc; padding:6px;">Delivered</th>
+ <th style="border:1px solid #ccc; padding:6px;">Invoiced</th>
+ <th style="border:1px solid #ccc; padding:6px;">Status</th>
+ </tr>
+ </thead>
+ <tbody>
+ {po_rows or '<tr><td colspan="6">Tidak ada item PO</td></tr>'}
+ </tbody>
+ </table>
+ """
+
+ # ---- POST TO CHATTER ----
+ order.message_post(
+ body=f"""
+ <b>📦 Data SOO</b><br/>{soo_table}
+ <br/><br/>
+ <b>📦 Data Item PO</b><br/>{po_table}
+ """
+ )
+
+ except Exception as e:
+ order.message_post(
+ body=f"❌ Gagal ambil data dari Altama:<br/><pre>{str(e)}</pre>"
+ )
@staticmethod
def is_local_env():
hostname = socket.gethostname().lower()
@@ -631,7 +935,8 @@ class PurchaseOrder(models.Model):
date_done = self.date_approve
- day_extension = int(self.payment_term_id.line_ids.days)
+ # day_extension = int(self.payment_term_id.line_ids.days)
+ day_extension = int(max(self.payment_term_id.line_ids.mapped('days'), default=0))
payment_schedule = date_done + timedelta(days=day_extension)
if payment_schedule.weekday() == 0:
@@ -1086,17 +1391,6 @@ class PurchaseOrder(models.Model):
if self.order_sales_match_line:
if self.total_percent_margin <= 15.0:
raise UserError("Approval Pimpinan diperlukan jika terdapat perubahan Unit Price pada PO Line dan Memiliki Margin <= 15%")
- else:
- low_margin_match_so = self.order_sales_match_line.filtered(
- lambda match: match.so_header_margin <= 15.0
- )
- if low_margin_match_so:
- raise UserError("Approval Pimpinan diperlukan jika pada PO Line yang Matches SO item memiliki header margin SO <= 15%")
- # else:
- # is_po_manual = '/A/' not in self.name and '/MO/' not in self.name
- # if is_po_manual:
- # if not self.order_sales_match_line:
- # raise UserError("Tidak ada matches SO, Approval Pimpinan diperlukan.")
self._check_assets_note()
# self._check_payment_term() # check payment term
@@ -1152,6 +1446,9 @@ class PurchaseOrder(models.Model):
send_email = True
break
+ # if self.partner_id.id == 5571 and not self.revisi_po:
+ # self.action_create_order_altama()
+
if send_email:
if self.is_local_env():
_logger.warning("📪 Local environment detected — skip sending email reminders.")
@@ -1169,6 +1466,7 @@ class PurchaseOrder(models.Model):
self.calculate_line_no()
self.approve_by = self.env.user.id
+
# override date planned added with two days
# leadtime = self.partner_id.leadtime
# delta_time = current_time + timedelta(days=leadtime)
@@ -1186,6 +1484,8 @@ class PurchaseOrder(models.Model):
# if len(self) == 1:
# _logger.info("Redirecting ke BU")
# return self.action_view_related_bu()
+ if self.partner_id.id == 5571 and not self.revisi_po:
+ self.action_create_order_altama()
return res
@@ -1421,19 +1721,36 @@ class PurchaseOrder(models.Model):
real_item_margin = sales_price - purchase_price
sum_margin += real_item_margin
+ cashback_amount = 0
+ if self.partner_id.id == 5571:
+ cashback_percent = line.product_id.x_manufacture.cashback_percent or 0.0
+ if cashback_percent > 0:
+ cashback_amount = purchase_price * cashback_percent
+ purchase_price -= cashback_amount
+
+ # line.amount_cashback = cashback_amount
+
if sum_so_margin != 0 and sum_sales_price != 0 and sum_margin != 0:
self.total_so_margin = sum_so_margin
self.total_so_percent_margin = round((sum_so_margin / sum_sales_price), 2) * 100
self.total_margin = sum_margin
self.total_percent_margin = round((sum_margin / sum_sales_price), 2) * 100
+ self.amount_cashback = 0
+ elif self.partner_id.id == 5571 and sum_so_margin != 0 and sum_sales_price != 0 and sum_margin != 0 and cashback_amount != 0:
+ self.total_so_margin = sum_so_margin
+ self.total_so_percent_margin = round((sum_so_margin / sum_sales_price), 2) * 100
+ self.total_margin = sum_margin
+ self.total_percent_margin = round((sum_margin / sum_sales_price), 2) * 100
+ self.amount_cashback = cashback_amount
else:
self.total_margin = 0
self.total_percent_margin = 0
self.total_so_margin = 0
self.total_so_percent_margin = 0
+ self.amount_cashback = 0
def compute_total_margin_from_apo(self):
- sum_so_margin = sum_sales_price = sum_margin = 0
+ sum_so_margin = sum_sales_price = sum_margin = cashback_amount = 0
for line in self.order_sales_match_line:
po_line = self.env['purchase.order.line'].search([
('product_id', '=', line.product_id.id),
@@ -1470,18 +1787,27 @@ class PurchaseOrder(models.Model):
if line.purchase_order_id.delivery_amt > 0:
purchase_price += line.purchase_order_id.delivery_amt
+ if self.partner_id.id == 5571:
+ cashback_percent = line.product_id.x_manufacture.cashback_percent or 0.0
+ if cashback_percent > 0:
+ cashback_amount = purchase_price * cashback_percent
+ purchase_price -= cashback_amount
+
real_item_margin = sales_price - purchase_price
sum_margin += real_item_margin
+ self.amount_cashback = cashback_amount
# Akumulasi hasil akhir
if sum_sales_price != 0:
self.total_so_margin = sum_so_margin
self.total_so_percent_margin = round((sum_so_margin / sum_sales_price), 2) * 100
self.total_margin = sum_margin
self.total_percent_margin = round((sum_margin / sum_sales_price), 2) * 100
+ self.amount_cashback = cashback_amount
else:
self.total_margin = self.total_percent_margin = 0
self.total_so_margin = self.total_so_percent_margin = 0
+ self.amount_cashback = 0
def compute_amt_total_without_service(self):
diff --git a/indoteknik_custom/models/purchase_order_line.py b/indoteknik_custom/models/purchase_order_line.py
index 8c72887d..c6a49481 100755
--- a/indoteknik_custom/models/purchase_order_line.py
+++ b/indoteknik_custom/models/purchase_order_line.py
@@ -23,6 +23,9 @@ class PurchaseOrderLine(models.Model):
so_item_percent_margin = fields.Float(
'SO Margin%', compute='compute_item_margin',
help="Total % Margin in Sales Order Header")
+ amount_cashback = fields.Float(
+ 'SO Margin%', compute='_compute_cashback_brand',
+ help="Total % Margin in Sales Order Header")
delivery_amt_line = fields.Float('DeliveryAmtLine', compute='compute_delivery_amt_line')
line_no = fields.Integer('No', default=0)
qty_available = fields.Float('Qty Available', compute='_compute_qty_stock')
@@ -52,6 +55,23 @@ class PurchaseOrderLine(models.Model):
ending_price = fields.Float(string='Ending Price', compute='_compute_doc_delivery_amt')
show_description = fields.Boolean(string='Show Description', help="Show Description when print po", default=True)
price_unit_before = fields.Float(string='Unit Price Before', help="Harga awal yang sebelumnya telah diinputkan")
+ altama_ordered = fields.Float(
+ string='Altama Ordered',
+ default=0.0,
+ copy=False
+ )
+ altama_delivered = fields.Float(
+ string='Altama Delivered',
+ default=0.0,
+ copy=False
+ )
+ altama_invoiced = fields.Float(
+ string='Altama Invoiced',
+ default=0.0,
+ copy=False
+ )
+ description = fields.Text(string='Description', readonly=True, copy=False)
+ docstatus_altama = fields.Text(string='Status Altama', readonly=True, copy=False)
@api.onchange('price_unit')
def _onchange_price_unit_before(self):
@@ -373,6 +393,9 @@ class PurchaseOrderLine(models.Model):
purchase_price = line.price_subtotal
if order.delivery_amount > 0:
purchase_price += line.delivery_amt_line
+
+ if line.amount_cashback > 0:
+ purchase_price = purchase_price - line.amount_cashback
# Hitung margin dan persentase margin
real_item_margin = total_sales_price - purchase_price
@@ -384,6 +407,46 @@ class PurchaseOrderLine(models.Model):
sum_margin += real_item_margin
+ def _compute_cashback_brand(self):
+ start_date = datetime(2026, 2, 1, 0, 0, 0)
+
+ for line in self:
+ line.amount_cashback = 0.0
+
+ product = line.product_id
+ order = line.order_id
+
+ if not product or not order:
+ continue
+
+ if order.partner_id.id != 5571:
+ continue
+
+ sales_matches = self.env['purchase.order.sales.match'].search([
+ ('purchase_order_id', '=', order.id),
+ ('product_id', '=', product.id)
+ ])
+
+ total_cashback = 0.0
+
+ for match in sales_matches:
+ so_line = match.sale_line_id
+ so_order = so_line.order_id
+
+ if not so_order.date_order or so_order.date_order < start_date:
+ continue
+
+ cashback_percent = product.x_manufacture.cashback_percent or 0.0
+ if cashback_percent <= 0:
+ continue
+ sales_price = so_line.price_reduce_taxexcl * match.qty_so
+
+ cashback = sales_price * cashback_percent
+ total_cashback += cashback
+
+ line.amount_cashback = total_cashback
+
+
def compute_delivery_amt_line(self):
for line in self:
if line.product_id.type == 'product':
diff --git a/indoteknik_custom/models/purchase_pricelist.py b/indoteknik_custom/models/purchase_pricelist.py
index b3a473b6..83e06f55 100755
--- a/indoteknik_custom/models/purchase_pricelist.py
+++ b/indoteknik_custom/models/purchase_pricelist.py
@@ -118,15 +118,16 @@ class PurchasePricelist(models.Model):
product_domain = [('product_id', '=', rec.product_id.id)]
markup_pricelist = price_group['markup'].pricelist_id
- base_price = price_incl + (price_incl * markup_percentage / 100)
+ # base_price = price_incl + (price_incl * markup_percentage / 100)
+ base_price = round(price_incl + (price_incl * markup_percentage / 100), 12)
base_prod_pricelist = self.env['product.pricelist.item'].search(product_domain + [('pricelist_id', '=', markup_pricelist.id)], limit=1)
base_prod_pricelist.fixed_price = base_price
tier_percentages = [price_group[f'tier_{i}'][product_group] for i in range(1, 6)]
for i, tier_percentage in enumerate(tier_percentages):
tier_pricelist = price_group[f'tier_{i + 1}'].pricelist_id
- tier_price = price_incl + (price_incl * tier_percentage / 100)
- tier_perc = (base_price - tier_price) / base_price * 100
+ tier_price = round(price_incl + (price_incl * tier_percentage / 100), 12)
+ tier_perc = round((base_price - tier_price) / base_price * 100, 12)
tier_prod_pricelist = self.env['product.pricelist.item'].search(product_domain + [('pricelist_id', '=', tier_pricelist.id)], limit=1)
tier_prod_pricelist.price_discount = tier_perc
diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py
index 28f88d0e..6acd0b59 100644
--- a/indoteknik_custom/models/refund_sale_order.py
+++ b/indoteknik_custom/models/refund_sale_order.py
@@ -62,7 +62,8 @@ class RefundSaleOrder(models.Model):
('uang', 'Refund Lebih Bayar'),
('retur_half', 'Refund Retur Sebagian'),
('retur', 'Refund Retur Full'),
- ('salah_transfer', 'Salah Transfer')
+ ('salah_transfer', 'Salah Transfer'),
+ ('berita_acara', 'Kebutuhan Berita Acara')
], string='Refund Type', required=True)
tukar_guling_ids = fields.One2many(
@@ -242,7 +243,7 @@ class RefundSaleOrder(models.Model):
)
invoices = sale_orders.mapped('invoice_ids').filtered(
- lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.payment_state == 'paid'
+ lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.state == 'posted'
)
if invoices:
vals['invoice_ids'] = [(6, 0, invoices.ids)]
@@ -251,7 +252,7 @@ class RefundSaleOrder(models.Model):
invoice_ids_data = vals.get('invoice_ids', [])
invoice_ids = invoice_ids_data[0][2] if invoice_ids_data and invoice_ids_data[0][0] == 6 else []
invoices = self.env['account.move'].browse(invoice_ids)
- if invoice_ids and refund_type and refund_type not in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half']:
+ if invoice_ids and refund_type and refund_type not in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half', 'berita_acara']:
raise UserError("Refund type Hanya Bisa Lebih Bayar, Barang Kosong Sebagian, atau Retur jika ada invoice")
if not invoice_ids and refund_type and refund_type in ['uang', 'barang_kosong_sebagian', 'retur_half']:
@@ -434,13 +435,17 @@ class RefundSaleOrder(models.Model):
total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total_signed')) if invoice_ids else 0.0
vals['total_invoice'] = total_invoice
amount_refund = vals.get('amount_refund', 0.0)
- can_refund = sisa_uang_masuk - total_invoice
-
- if amount_refund > can_refund or can_refund == 0.0:
- raise ValidationError(
- _("Maksimal refund yang bisa dilakukan adalah sebesar %s. "
- "Silakan sesuaikan jumlah refund.") % (can_refund)
- )
+ can_refund = 0.0
+ if refund_type == 'berita_acara':
+ can_refund = sisa_uang_masuk
+ else:
+ can_refund = sisa_uang_masuk - total_invoice
+ if refund_type != 'berita_acara':
+ if amount_refund > can_refund or can_refund == 0.0:
+ raise ValidationError(
+ _("Maksimal refund yang bisa dilakukan adalah sebesar %s. "
+ "Silakan sesuaikan jumlah refund.") % (can_refund)
+ )
if amount_refund <= 0.00:
raise ValidationError('Total Refund harus lebih dari 0 jika ingin mengajukan refund')
@@ -492,7 +497,7 @@ class RefundSaleOrder(models.Model):
valid_invoices = sale_orders.mapped('invoice_ids').filtered(
- lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.payment_state == 'paid'
+ lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.state == 'posted'
)
vals['invoice_ids'] = [(6, 0, valid_invoices.ids)]
vals['ongkir'] = sum(so.delivery_amt or 0.0 for so in sale_orders)
@@ -535,7 +540,7 @@ class RefundSaleOrder(models.Model):
else:
invoice_ids = rec.invoice_ids.ids
- if invoice_ids and vals.get('refund_type', rec.refund_type) not in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half', 'retur']:
+ if invoice_ids and vals.get('refund_type', rec.refund_type) not in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half', 'retur', 'berita_acara']:
raise UserError("Refund type Hanya Bisa Lebih Bayar, Barang Kosong Sebagian, atau Retur jika ada invoice")
if not invoice_ids and vals.get('refund_type', rec.refund_type) in ['uang', 'barang_kosong_sebagian', 'retur_half']:
@@ -548,10 +553,28 @@ class RefundSaleOrder(models.Model):
if any(field in vals for field in ['uang_masuk', 'invoice_ids', 'ongkir', 'sale_order_ids', 'amount_refund']):
total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total_signed'))
vals['total_invoice'] = total_invoice
- uang_masuk = rec.uang_masuk
- can_refund = uang_masuk - total_invoice
-
+ uang_masuk = vals.get('uang_masuk', rec.uang_masuk)
amount_refund = vals.get('amount_refund', rec.amount_refund)
+ can_refund = 0.0
+ total_refunded = 0.0
+
+ if refund_type == 'berita_acara':
+ can_refund = uang_masuk
+ remaining = uang_masuk - amount_refund
+ else:
+ can_refund = uang_masuk - total_invoice
+
+ existing_refunds = self.search([
+ ('sale_order_ids', 'in', so_ids),
+ ('id', '!=', rec.id)
+ ])
+ total_refunded = sum(existing_refunds.mapped('amount_refund'))
+
+ if existing_refunds:
+ remaining = uang_masuk - total_refunded
+ else:
+ remaining = uang_masuk - amount_refund
+
if amount_refund > can_refund:
raise ValidationError(
@@ -601,7 +624,7 @@ class RefundSaleOrder(models.Model):
for rec in self:
move_links = []
- invoice_ids = rec.sale_order_ids.mapped('invoice_ids')
+ invoice_ids = rec.sale_order_ids.mapped('invoice_ids').filtered(lambda m: m.state == 'posted')
moves = self.env['account.move'].search([
('sale_id', 'in', rec.sale_order_ids.ids),
@@ -722,7 +745,7 @@ class RefundSaleOrder(models.Model):
for so in self.sale_order_ids:
self.ongkir += so.delivery_amt or 0.0
valid_invoices = so.invoice_ids.filtered(
- lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.payment_state == 'paid'
+ lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.state == 'posted'
)
all_invoices |= valid_invoices
total_invoice += sum(valid_invoices.mapped('amount_total_signed'))
diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py
index 7f4feb75..5bbaaf1c 100644
--- a/indoteknik_custom/models/res_partner.py
+++ b/indoteknik_custom/models/res_partner.py
@@ -298,26 +298,36 @@ class ResPartner(models.Model):
def _check_npwp(self):
for record in self:
npwp = record.npwp.strip() if record.npwp else ''
- # Abaikan validasi jika NPWP kosong atau diisi "0"
+
if not npwp or npwp == '0' or npwp == '00.000.000.0-000.000':
continue
-
- # Validasi untuk NPWP 15 digit (format: 99.999.999.9-999.999)
- if len(npwp) == 20:
- # Regex untuk 15 digit dengan format titik dan tanda hubung
+
+ elif len(npwp) == 15 and npwp.isdigit():
+ formatted = f"{npwp[0:2]}.{npwp[2:5]}.{npwp[5:8]}.{npwp[8]}-{npwp[9:12]}.{npwp[12:15]}"
+ record.npwp = formatted
+
+ elif len(npwp) == 20:
pattern_15_digit = r'^\d{2}\.\d{3}\.\d{3}\.\d{1}-\d{3}\.\d{3}$'
if not re.match(pattern_15_digit, npwp):
- raise ValidationError("Format NPWP 15 digit yang dimasukkan salah. Pastikan format yang benar adalah: 99.999.999.9-999.999")
-
- # Validasi untuk NPWP 16 digit (hanya angka tanpa titik atau tanda hubung)
+ raise ValidationError(
+ "Format NPWP 15 digit yang dimasukkan salah. "
+ "Pastikan format yang benar adalah: 99.999.999.9-999.999"
+ )
+
elif len(npwp) == 16:
pattern_16_digit = r'^\d{16}$'
if not re.match(pattern_16_digit, npwp):
- raise ValidationError("Format NPWP 16 digit yang dimasukkan salah. Format yang benar adalah 16 digit angka tanpa titik atau tanda hubung.")
-
- # Validasi panjang NPWP jika lebih atau kurang dari 15 atau 16 digit
+ raise ValidationError(
+ "Format NPWP 16 digit yang dimasukkan salah. "
+ "Format yang benar adalah 16 digit angka tanpa titik atau tanda hubung."
+ )
+
else:
- raise ValidationError("Digit NPWP yang dimasukkan tidak sesuai. Pastikan NPWP memiliki 15 digit dengan format tertentu (99.999.999.9-999.999) atau 16 digit tanpa tanda hubung.")
+ raise ValidationError(
+ "Digit NPWP yang dimasukkan tidak sesuai. "
+ "Pastikan NPWP memiliki 15 digit dengan format tertentu "
+ "(99.999.999.9-999.999) atau 16 digit tanpa tanda hubung."
+ )
# def write(self, vals):
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index 469509d4..0cb6670e 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -156,6 +156,7 @@ class SaleOrder(models.Model):
total_margin_excl_third_party = fields.Float('Before Margin', help="Before Margin in Sales Order Header")
approval_status = fields.Selection([
+ ('pengajuan0', 'Approval Team Sales'),
('pengajuan1', 'Approval Manager'),
('pengajuan2', 'Approval Pimpinan'),
('approved', 'Approved'),
@@ -647,7 +648,7 @@ class SaleOrder(models.Model):
def _get_biteship_courier_codes(self):
return [
- 'gojek','grab','deliveree','lalamove','jne','tiki','ninja','lion','rara','sicepat','jnt','pos','idexpress','rpx','wahana','jdl','pos','anteraja','sap','paxel','borzo'
+ 'gojek','grab','deliveree','lalamove','jne','ninja','lion','rara','sicepat','jnt','idexpress','rpx','wahana','jdl','anteraja','sap','paxel','borzo'
]
@api.onchange('carrier_id')
@@ -2316,7 +2317,7 @@ class SaleOrder(models.Model):
for order in self:
for line in order.order_line:
search_product = self.env['sale.order.line'].search(
- [('product_id', '=', line.product_id.id), ('order_id', '=', order.id)])
+ [('product_id', '=', line.product_id.id), ('order_id', '=', order.id), ('display_type', '=', False)])
if len(search_product) > 1:
raise UserError("Terdapat DUPLIKASI data pada Product {}".format(line.product_id.display_name))
@@ -2384,8 +2385,15 @@ class SaleOrder(models.Model):
# if order.validate_partner_invoice_due():
# return self._create_notification_action('Notification',
# 'Terdapat invoice yang telah melewati batas waktu, mohon perbarui pada dokumen Due Extension')
-
- if order._requires_approval_margin_leader():
+ value_trigger = order._requires_approval_by_value()
+ if value_trigger:
+ self.check_product_bom()
+ self.check_credit_limit()
+ self.check_limit_so_to_invoice()
+ order.approval_status = 'pengajuan0'
+ order.message_post(body="Mengajukan approval ke Team Sales_")
+ return self._create_approval_notification('Team Sales')
+ elif order._requires_approval_margin_leader():
order.approval_status = 'pengajuan2'
order.message_post(body="Mengajukan approval ke Pimpinan")
return self._create_approval_notification('Pimpinan')
@@ -2400,9 +2408,16 @@ class SaleOrder(models.Model):
self.check_product_bom()
self.check_credit_limit()
self.check_limit_so_to_invoice()
- order.approval_status = 'pengajuan1'
+ order.approval_status = 'pengajuan0'
order.message_post(body="Mengajukan approval ke Team Sales")
return self._create_approval_notification('Team Sales')
+ # elif value_trigger:
+ # self.check_product_bom()
+ # self.check_credit_limit()
+ # self.check_limit_so_to_invoice()
+ # order.approval_status = 'pengajuan0'
+ # order.message_post(body="Mengajukan approval ke Team Sales_")
+ # return self._create_approval_notification('Team Sales')
if not order.with_context(ask_approval=True)._is_request_to_own_team_leader():
return self._create_notification_action(
@@ -2598,6 +2613,28 @@ class SaleOrder(models.Model):
else:
return False
+ def check_archived_product(self):
+ for order in self:
+ for line in order.order_line:
+ # Skip section & note
+ if line.display_type:
+ continue
+
+ if line.product_id and not line.product_id.active:
+ raise UserError(
+ "Terdapat Product yang sudah di Archive pada Product: {}".format(
+ line.product_id.display_name
+ )
+ )
+
+ def check_archived_uom(self):
+ for order in self:
+ for line in order.order_line:
+ if line.display_type:
+ continue
+ if line.product_uom.active == False:
+ raise UserError("Terdapat UoM yang sudah di Archive pada UoM {} di Product {}".format(line.product_uom.name, line.product_id.display_name))
+
def action_confirm(self):
for order in self:
order._validate_delivery_amt()
@@ -2614,6 +2651,8 @@ class SaleOrder(models.Model):
order._validate_order()
order._validate_npwp()
order.order_line.validate_line()
+ order.check_archived_product()
+ order.check_archived_uom()
main_parent = order.partner_id.get_main_parent()
SYSTEM_UID = 25
@@ -2643,16 +2682,25 @@ class SaleOrder(models.Model):
'Warning',
'Hanya bisa konfirmasi SO tim Anda.'
)
- if order._requires_approval_margin_leader():
+ value_trigger = order._requires_approval_by_value()
+ if value_trigger:
+ order.approval_status = 'pengajuan0'
+ order.message_post(body="Mengajukan approval ke Team Sales")
+ return self._create_approval_notification('Team Sales')
+ elif order._requires_approval_margin_leader():
order.approval_status = 'pengajuan2'
return self._create_approval_notification('Pimpinan')
elif order._requires_approval_margin_manager():
order.approval_status = 'pengajuan1'
return self._create_approval_notification('Sales Manager')
- elif order._requires_approval_team_sales():
- order.approval_status = 'pengajuan1'
+ elif value_trigger or order._requires_approval_team_sales():
+ order.approval_status = 'pengajuan0'
order.message_post(body="Mengajukan approval ke Team Sales")
return self._create_approval_notification('Team Sales')
+ # elif value_trigger:
+ # order.approval_status = 'pengajuan0'
+ # order.message_post(body="Mengajukan approval ke Team Sales (Total SO > 50jt)")
+ # return self._create_approval_notification('Team Sales')
order.approval_status = 'approved'
order._set_sppkp_npwp_contact()
@@ -2764,7 +2812,15 @@ class SaleOrder(models.Model):
and not self.env.user.is_leader
)
-
+ def _requires_approval_by_value(self):
+ # LIMIT_VALUE = 50000000
+ LIMIT_VALUE = float(self.env['ir.config_parameter'].sudo().get_param('so.limit_value_approve', default='50000000'))
+ return (
+ self.amount_total >= LIMIT_VALUE
+ and self.env.user.id not in [11, 9, 375] # Eko, Ade, Putra
+ and not self.env.user.is_sales_manager
+ and not self.env.user.is_leader
+ )
def _is_request_to_own_team_leader(self):
user = self.env.user
@@ -3383,7 +3439,7 @@ class SaleOrder(models.Model):
#payment term vals
if 'payment_term_id' in vals and any(
- order.approval_status in ['pengajuan1', 'pengajuan2', 'approved'] for order in self):
+ order.approval_status in ['pengajuan0','pengajuan1', 'pengajuan2', 'approved'] for order in self):
raise UserError(
"Payment Term tidak dapat diubah karena Sales Order sedang dalam proses approval atau sudah diapprove.")
@@ -3438,7 +3494,7 @@ class SaleOrder(models.Model):
def button_refund(self):
self.ensure_one()
- invoice_ids = self.invoice_ids.filtered(lambda inv: inv.payment_state == 'paid')
+ invoice_ids = self.invoice_ids.filtered(lambda inv: inv.state == 'posted')
moves = self.env['account.move'].search([
('sale_id', '=', self.id),
diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py
index 1df1a058..dd44f84a 100644
--- a/indoteknik_custom/models/sale_order_line.py
+++ b/indoteknik_custom/models/sale_order_line.py
@@ -17,6 +17,7 @@ class SaleOrderLine(models.Model):
help="Total % Margin in Sales Order Header")
item_percent_margin_before = fields.Float('%Margin Before', compute='_compute_item_percent_margin_before',
help="Total % Margin excluding third party in Sales Order Header")
+ amount_cashback = fields.Float('Cashback Brand', compute='_compute_cashback_brand', help='Cashback from product who has cashback percent in manufacture')
initial_discount = fields.Float('Initial Discount')
vendor_id = fields.Many2one(
'res.partner', string='Vendor', readonly=True,
@@ -164,7 +165,10 @@ class SaleOrderLine(models.Model):
purchase_price = line.purchase_price
if line.purchase_tax_id.price_include:
- purchase_price = line.purchase_price / 1.11
+ purchase_price = line.purchase_price / (1 + (line.purchase_tax_id.amount / 100))
+
+ if line.amount_cashback > 0:
+ purchase_price = purchase_price - line.amount_cashback
purchase_price = purchase_price * line.product_uom_qty
margin_per_item = sales_price - purchase_price
@@ -186,7 +190,10 @@ class SaleOrderLine(models.Model):
purchase_price = line.purchase_price
if line.purchase_tax_id and line.purchase_tax_id.price_include:
- purchase_price = line.purchase_price / 1.11
+ purchase_price = line.purchase_price / (1 + (line.purchase_tax_id.amount / 100))
+
+ if line.amount_cashback > 0:
+ purchase_price = purchase_price - line.amount_cashback
purchase_price = purchase_price * line.product_uom_qty
@@ -215,7 +222,10 @@ class SaleOrderLine(models.Model):
purchase_price = line.purchase_price
if line.purchase_tax_id.price_include:
- purchase_price = line.purchase_price / 1.11
+ purchase_price = line.purchase_price / (1 + (line.purchase_tax_id.amount / 100))
+
+ if line.amount_cashback > 0:
+ purchase_price = purchase_price - line.amount_cashback
purchase_price = purchase_price * line.product_uom_qty
margin_per_item = sales_price - purchase_price
@@ -241,12 +251,47 @@ class SaleOrderLine(models.Model):
purchase_price = line.purchase_price
if line.purchase_tax_id.price_include:
- purchase_price = line.purchase_price / 1.11
+ purchase_price = line.purchase_price / (1 + (line.purchase_tax_id.amount / 100))
+
+ if line.amount_cashback > 0:
+ purchase_price = purchase_price - line.amount_cashback
purchase_price = purchase_price * line.product_uom_qty
margin_per_item = sales_price - purchase_price
line.item_before_margin = margin_per_item
+ def _compute_cashback_brand(self):
+ start_date = datetime(2026, 2, 1, 0, 0, 0)
+ for line in self:
+ line.amount_cashback = 0
+
+ if not line.product_id:
+ continue
+
+ if line.order_id.date_order < start_date:
+ continue
+
+ price, taxes, vendor_id = self._get_purchase_price(line.product_id)
+
+ cashback_percent = line.product_id.x_manufacture.cashback_percent or 0
+ if cashback_percent <= 0:
+ continue
+
+
+ if line.vendor_id.id != 5571:
+ continue
+
+ price_tax_excl = price
+
+ if taxes:
+ tax = self.env['account.tax'].browse(taxes)
+ if tax.price_include:
+ price_tax_excl = price / (1 + (tax.amount / 100))
+ else:
+ price_tax_excl = price
+
+ line.amount_cashback = price_tax_excl * cashback_percent
+
# @api.onchange('vendor_id')
# def onchange_vendor_id(self):
# # TODO : need to change this logic @stephan
diff --git a/indoteknik_custom/models/shipment_group.py b/indoteknik_custom/models/shipment_group.py
index 7203b566..5f5f2d79 100644
--- a/indoteknik_custom/models/shipment_group.py
+++ b/indoteknik_custom/models/shipment_group.py
@@ -2,6 +2,7 @@ from odoo import models, api, fields
from odoo.exceptions import AccessError, UserError, ValidationError
from datetime import timedelta, date
import logging
+from markupsafe import escape as html_escape
_logger = logging.getLogger(__name__)
@@ -16,7 +17,21 @@ class ShipmentGroup(models.Model):
partner_id = fields.Many2one('res.partner', string='Customer')
carrier_id = fields.Many2one('delivery.carrier', string='Ekspedisi')
total_colly_line = fields.Float(string='Total Colly', compute='_compute_total_colly_line')
+ is_multi_partner = fields.Boolean(string='Is Multi Partner', compute='_compute_is_multi_partner')
+ partner_ids = fields.Many2many('res.partner', string='Customers', compute='_compute_partner_ids')
+ driver = fields.Text(string='Driver')
+ @api.depends('shipment_line.partner_id')
+ def _compute_partner_ids(self):
+ for rec in self:
+ rec.partner_ids = rec.shipment_line.mapped('partner_id').ids
+
+ @api.depends('shipment_line.partner_id')
+ def _compute_is_multi_partner(self):
+ for rec in self:
+ partners = rec.shipment_line.mapped('partner_id')
+ rec.is_multi_partner = len(partners) > 1
+
def sync_api_shipping(self):
for rec in self.shipment_line:
picking_names = [lines.picking_id.name for lines in self.shipment_line]
@@ -97,14 +112,14 @@ class ShipmentGroupLine(models.Model):
@api.onchange('picking_id')
def onchange_picking_id(self):
if self.picking_id:
- picking = self.env['stock.picking'].browse(self.picking_id.id)
+ picking = self.picking_id
if self.shipment_id.carrier_id and self.shipment_id.carrier_id != picking.carrier_id:
raise UserError('carrier must be same as shipment group')
-
+
if picking.total_mapping_koli == 0:
raise UserError(f'Picking {picking.name} tidak memiliki mapping koli')
-
+
self.partner_id = picking.partner_id
self.shipping_paid_by = picking.sale_id.shipping_paid_by
self.carrier_id = picking.carrier_id.id
@@ -115,6 +130,9 @@ class ShipmentGroupLine(models.Model):
self.sale_id = picking.sale_id
+ if self.shipment_id:
+ self.shipment_id._compute_is_multi_partner()
+
@api.model
def create(self, vals):
record = super(ShipmentGroupLine, self).create(vals)
diff --git a/indoteknik_custom/models/stock_inventory.py b/indoteknik_custom/models/stock_inventory.py
index 84eb5a17..efd52e5c 100644
--- a/indoteknik_custom/models/stock_inventory.py
+++ b/indoteknik_custom/models/stock_inventory.py
@@ -17,6 +17,35 @@ class StockInventory(models.Model):
('out', 'Adjusment Out'),
], string='Adjusments Type', required=True)
+ approval_state = fields.Selection([
+ ('logistic', 'Logistic'),
+ ('accounting', 'Accounting'),
+ ('approved', 'Approved'),
+ ], tracking=True, readonly=True)
+
+ def action_validate(self):
+ if self.adjusment_type == 'out':
+
+ if self.approval_state != 'approved':
+
+ if self.approval_state == 'logistic':
+ if not self.env.user.has_group('indoteknik_custom.group_role_logistic'):
+ raise UserError("Adjustment Out harus dilakukan oleh Logistic")
+ self.approval_state = 'accounting'
+ return True
+
+ elif self.approval_state == 'accounting':
+ if not self.env.user.has_group('indoteknik_custom.group_role_fat'):
+ raise UserError("Adjustment Out harus dilakukan oleh Accounting")
+ self.approval_state = 'approved'
+ return super(StockInventory, self).action_validate()
+
+ else:
+ raise UserError("Adjustment Out harus melalui approval terlebih dahulu.")
+
+ return super(StockInventory, self).action_validate()
+
+
def _generate_number_stock_inventory(self):
"""Men-generate nomor untuk semua stock inventory yang belum memiliki number."""
stock_records = self.env['stock.inventory'].search([('number', '=', False)], order='id asc')
@@ -53,13 +82,19 @@ class StockInventory(models.Model):
return "00001" # Jika belum ada data, mulai dari 00001
def action_start(self):
- if self.env.user.id not in [21, 17, 571, 28]:
- raise UserError("Hanya Rafly, Denise, Iqmal, dan Stephan yang bisa start inventory")
+ if self.env.user.id not in [21, 17, 571, 28, 25]:
+ raise UserError("Hanya Rafly, Denise, Iqmal, dan Stephan yang bisa start inventory")
return super(StockInventory, self).action_start()
@api.model
def create(self, vals):
"""Pastikan nomor hanya dibuat saat penyimpanan."""
+
+ if vals.get('adjusment_type') == 'in':
+ vals['approval_state'] = False
+ elif vals.get('adjusment_type') == 'out':
+ vals['approval_state'] = 'logistic'
+
if 'adjusment_type' in vals and not vals.get('number'):
vals['number'] = False # Jangan buat number otomatis dulu
@@ -69,12 +104,17 @@ class StockInventory(models.Model):
self._assign_number(order) # Generate number setelah save
return order
+
def write(self, vals):
"""Jika adjusment_type diubah, generate ulang nomor."""
res = super(StockInventory, self).write(vals)
if 'adjusment_type' in vals:
for record in self:
+ if record.adjusment_type == 'in':
+ record.approval_state = False
+ elif record.adjusment_type == 'out' and record.approval_state == False:
+ record.approval_state = 'logistic'
self._assign_number(record)
return res
diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py
index 2465fa96..ab366fd6 100644
--- a/indoteknik_custom/models/stock_picking.py
+++ b/indoteknik_custom/models/stock_picking.py
@@ -203,6 +203,16 @@ class StockPicking(models.Model):
is_so_fiktif = fields.Boolean('SO Fiktif?', compute='_compute_is_so_fiktif', tracking=3)
payment_term = fields.Char('Payment Term', compute='_get_partner_payment_term')
is_rev_tg = fields.Boolean('Administrasi')
+ shipment_group_id = fields.Many2one('shipment.group', string='Shipment Group', compute='_compute_shipment_group_id')
+
+ @api.depends('shipment_group_id')
+ def _compute_shipment_group_id(self):
+ for record in self:
+ shipment_line = self.env['shipment.group.line'].search([('picking_id', '=', record.id)], limit=1)
+ if shipment_line:
+ record.shipment_group_id = shipment_line.shipment_id.id
+ else:
+ record.shipment_group_id = False
@api.depends('sale_id.payment_term_id')
def _get_partner_payment_term(self):
@@ -449,6 +459,7 @@ class StockPicking(models.Model):
('urgent', 'Urgent Delivery'),
], string='Reason Change Date Planned', tracking=True)
delivery_date = fields.Datetime(string='Delivery Date', copy=False)
+ number_soo = fields.Char(string='Number SOO Altama')
def _get_kgx_awb_number(self):
"""Menggabungkan name dan origin untuk membuat AWB Number"""
@@ -1387,7 +1398,8 @@ class StockPicking(models.Model):
])
if quant:
- return quant.quantity
+ return sum(quant.mapped('quantity'))
+ # return quant.quantity
return 0
diff --git a/indoteknik_custom/models/token_log.py b/indoteknik_custom/models/token_log.py
new file mode 100644
index 00000000..fdc0c03e
--- /dev/null
+++ b/indoteknik_custom/models/token_log.py
@@ -0,0 +1,17 @@
+from odoo import models, fields, api
+
+class FixcoTokenLog(models.Model):
+ _name = 'token.log'
+ _description = 'Log Token Fixco'
+ _order = 'create_date desc'
+
+ token = fields.Text(string="Access Token", readonly=True)
+ expires_at = fields.Datetime(string="Expires At", readonly=True)
+ created_at = fields.Datetime(string="Created At", default=lambda self: fields.Datetime.now(), readonly=True)
+ created_by = fields.Many2one('res.users', string="Generated By", readonly=True)
+ source = fields.Selection([
+ ('manual', 'Manual Request'),
+ ('auto', 'Auto Refresh'),
+ ], string="Token Source", default='auto', readonly=True)
+ token_from = fields.Char(string="From", readonly=True)
+ is_active = fields.Boolean("Active", default=True) \ No newline at end of file
diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py
index 682c478a..619e7c99 100644
--- a/indoteknik_custom/models/tukar_guling.py
+++ b/indoteknik_custom/models/tukar_guling.py
@@ -62,6 +62,7 @@ class TukarGuling(models.Model):
notes = fields.Text('Notes')
return_type = fields.Selection(String='Return Type', selection=[
('tukar_guling', 'Tukar Guling'), # -> barang yang sama
+ # ('service', 'Service'), # -> barang yang sama
('retur_so', 'Retur SO')], required=True, tracking=3, help='Retur SO (ORT-SRT),\n Tukar Guling (ORT-SRT-PICK-OUT)')
state = fields.Selection(string='Status', selection=[
('draft', 'Draft'),
@@ -931,6 +932,66 @@ class TukarGuling(models.Model):
_logger.info(f"✅ BU/PICK Baru dari ORT created: {new_pick.name}")
record.message_post(
body=f"📦 <b>{new_pick.name}</b> created by <b>{self.env.user.name}</b> (state: <b>{new_pick.state}</b>)")
+
+ # if record.return_type == 'service':
+ # GUDANG_SERVICE_LOCATION_ID = 98
+ # # From STOCK to OUTPUT
+ # done_service = self.env['stock.picking'].create({
+ # 'group_id': bu_out.group_id.id,
+ # 'tukar_guling_id': record.id,
+ # 'sale_order': record.origin,
+ # 'note': record.notes,
+ # 'picking_type_id': 32,
+ # 'location_id': GUDANG_SERVICE_LOCATION_ID,
+ # 'location_dest_id': BU_STOCK_LOCATION_ID,
+ # 'partner_id': bu_out.partner_id.id,
+ # 'move_ids_without_package': [(0, 0, {
+ # 'product_id': line.product_id.id,
+ # 'product_uom_qty': line.product_uom_qty,
+ # 'product_uom': line.product_uom.id,
+ # 'name': line.product_id.display_name,
+ # 'location_id': GUDANG_SERVICE_LOCATION_ID,
+ # 'location_dest_id': BU_STOCK_LOCATION_ID,
+ # }) for line in record.line_ids],
+ # })
+ # if done_service:
+ # done_service.action_confirm()
+ # done_service.action_assign()
+ # else:
+ # raise UserError("Gagal membuat picking service")
+
+ # service_to_output = self.env['stock.picking'].create({
+ # 'group_id': bu_out.group_id.id,
+ # 'tukar_guling_id': record.id,
+ # 'sale_order': record.origin,
+ # 'note': record.notes,
+ # 'picking_type_id': 32,
+ # 'location_id': BU_STOCK_LOCATION_ID,
+ # 'location_dest_id': BU_OUTPUT_LOCATION_ID,
+ # 'partner_id': bu_out.partner_id.id,
+ # 'move_lines': [(0, 0, {
+ # 'product_id': line.product_id.id,
+ # 'product_uom_qty': line.product_uom_qty,
+ # 'product_uom': line.product_uom.id,
+ # 'name': line.product_id.display_name,
+ # 'location_id':BU_STOCK_LOCATION_ID,
+ # 'location_dest_id': BU_STOCK_LOCATION_ID,
+ # }) for line in record.line_ids],
+ # 'move_ids_without_package': [(0, 0, {
+ # 'product_id': line.product_id.id,
+ # 'product_uom_qty': line.product_uom_qty,
+ # 'product_uom': line.product_uom.id,
+ # 'name': line.product_id.display_name,
+ # 'location_id': BU_STOCK_LOCATION_ID,
+ # 'location_dest_id': BU_OUTPUT_LOCATION_ID,
+ # }) for line in record.line_ids],
+ # })
+ # if service_to_output:
+ # service_to_output.action_confirm()
+ # service_to_output.action_assign()
+ # else:
+ # raise UserError("Gagal membuat picking service")
+
# BU/OUT Baru dari SRT
if srt_picking:
diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py
index ae58d509..9038dd28 100644
--- a/indoteknik_custom/models/tukar_guling_po.py
+++ b/indoteknik_custom/models/tukar_guling_po.py
@@ -533,7 +533,7 @@ class TukarGulingPO(models.Model):
('state', '=', 'done'),
('picking_type_id.id', '=', 75)
])
- if self.state == 'aproved' and total_put > 0 and put == total_put:
+ if self.state == 'approved' and total_put > 0 and put == total_put:
self.state = 'done'
@@ -582,7 +582,23 @@ class TukarGulingPO(models.Model):
('group_id', '=', group.id),
('state', '=', 'done')
])
- bu_inputs = po_pickings.filtered(lambda p: p.picking_type_id.id == 28)
+
+ product_ids = set(record.line_ids.mapped("product_id").ids)
+
+ _logger.info("TG product_ids: %s", product_ids)
+
+ def _get_moves(picking):
+ return picking.move_ids_without_package if picking.move_ids_without_package else picking.move_lines
+
+ bu_inputs = po_pickings.filtered(
+ lambda p: p.picking_type_id.id == 28 and any(
+ m.product_id.id in product_ids
+ for m in _get_moves(p)
+ )
+ )
+
+ _logger.info("BU INPUT dengan product sama: %s", bu_inputs.mapped("name"))
+
bu_puts = po_pickings.filtered(lambda p: p.picking_type_id.id == 75)
else:
raise UserError("Group ID tidak ditemukan pada BU Operations.")
@@ -711,12 +727,26 @@ class TukarGulingPO(models.Model):
# Ambil pasangannya di BU INPUT (asumsi urutan sejajar)
sorted_bu_puts = sorted(bu_puts, key=lambda p: p.name)
+ # sorted_bu_inputs = sorted(bu_inputs, key=lambda p: p.name)
+
+ # if bu_put_index >= len(sorted_bu_inputs):
+ # raise UserError("Tidak ditemukan pasangan BU INPUT untuk BU PUT yang dipilih.")
+
+ # paired = [(sorted_bu_puts[bu_put_index], sorted_bu_inputs[bu_put_index])]
sorted_bu_inputs = sorted(bu_inputs, key=lambda p: p.name)
- if bu_put_index >= len(sorted_bu_inputs):
- raise UserError("Tidak ditemukan pasangan BU INPUT untuk BU PUT yang dipilih.")
+ if not sorted_bu_inputs:
+ raise UserError(
+ "Tidak ditemukan BU INPUT yang memiliki product TG."
+ )
- paired = [(sorted_bu_puts[bu_put_index], sorted_bu_inputs[bu_put_index])]
+ paired = [(record.operations, sorted_bu_inputs[0])]
+
+ _logger.info(
+ "🔗 Pairing BU PUT %s dengan BU INPUT %s",
+ record.operations.name,
+ sorted_bu_inputs[0].name
+ )
for bu_put, bu_input in paired:
vrt = _create_return_from_picking(bu_put, bu_put_qty_map)
diff --git a/indoteknik_custom/models/update_depreciation_move_wizard.py b/indoteknik_custom/models/update_depreciation_move_wizard.py
new file mode 100644
index 00000000..7d465f1d
--- /dev/null
+++ b/indoteknik_custom/models/update_depreciation_move_wizard.py
@@ -0,0 +1,48 @@
+from odoo import models, fields, api
+from odoo.exceptions import UserError
+
+class UpdateDepreciationMoveWizard(models.TransientModel):
+ _name = 'update.depreciation.move.wizard'
+ _description = 'Wizard untuk Update Move Check Depreciation Line'
+
+ target_date = fields.Date(string="Tanggal Depresiasi", required=True)
+
+ # def action_update_move_check(self):
+ # lines = self.env['account.asset.depreciation.line'].search([
+ # ('depreciation_date', '=', self.target_date),
+ # ])
+ # if not lines:
+ # raise UserError("Tidak ada baris depresiasi dengan tanggal tersebut.")
+
+ # updated_count = 0
+ # for line in lines:
+ # if not line.move_check:
+ # line.move_check = True
+ # line.move_posted_check = True
+ # updated_count += 1
+
+ # return {
+ # 'type': 'ir.actions.client',
+ # 'tag': 'display_notification',
+ # 'params': {
+ # 'title': 'Update Selesai',
+ # 'message': f'{updated_count} baris berhasil di-update.',
+ # 'type': 'success',
+ # 'sticky': False,
+ # }
+ # }
+
+ def action_update_move_check(self):
+ assets = self.env['account.asset.asset']
+ assets.compute_generated_entries(self.target_date)
+
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': 'Update Selesai',
+ 'message': 'Depresiasi berhasil di-update.',
+ 'type': 'success',
+ 'sticky': False,
+ }
+ } \ No newline at end of file
diff --git a/indoteknik_custom/models/x_manufactures.py b/indoteknik_custom/models/x_manufactures.py
index 9e214d92..0c3bfa3b 100755
--- a/indoteknik_custom/models/x_manufactures.py
+++ b/indoteknik_custom/models/x_manufactures.py
@@ -50,7 +50,7 @@ class XManufactures(models.Model):
# user_id = fields.Many2one('res.users', string='Responsible', domain="['|'('id', '=', 19), ('id', '=', 6)]", help="Siapa yang bertanggung jawab")
user_id = fields.Many2one('res.users', string='Responsible', help="Siapa yang bertanggung jawab")
override_vendor_id = fields.Many2one('res.partner', string='Override Vendor')
- # cashback_percent = fields.Float(string='Cashback Percent')
+ cashback_percent = fields.Float(string='Cashback Percent', default=0)
def _compute_vendor_ids(self):
for manufacture in self:
diff --git a/indoteknik_custom/report/purchase_report.xml b/indoteknik_custom/report/purchase_report.xml
index a6804ca4..81129dde 100644
--- a/indoteknik_custom/report/purchase_report.xml
+++ b/indoteknik_custom/report/purchase_report.xml
@@ -67,7 +67,7 @@
ADM. JAKARTA UTARA, DKI JAKARTA
</t>
<t t-else="">
- Jl. Bandengan Utara Komp A 8 B RT.<br/>
+ Jl. Bandengan Utara Komp A &amp; B RT.<br/>
Penjaringan, Kec. Penjaringan, Jakarta<br/>
(BELAKANG INDOMARET)
</t>
diff --git a/indoteknik_custom/report/purchase_report_internal.xml b/indoteknik_custom/report/purchase_report_internal.xml
new file mode 100644
index 00000000..7df847de
--- /dev/null
+++ b/indoteknik_custom/report/purchase_report_internal.xml
@@ -0,0 +1,201 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+ <!-- Report Action -->
+ <record id="action_report_purchaseorder_internal" model="ir.actions.report">
+ <field name="name">Purchase Order (Internal)</field>
+ <field name="model">purchase.order</field>
+ <field name="report_type">qweb-pdf</field>
+ <field name="report_name">indoteknik_custom.report_purchaseorder_internal</field>
+ <field name="report_file">indoteknik_custom.report_purchaseorder_internal</field>
+ <field name="print_report_name">
+ ('%s - %s' % (object.name, object.partner_id.name))
+ </field>
+ <field name="binding_model_id" ref="purchase.model_purchase_order"/>
+ <field name="binding_type">report</field>
+ </record>
+ </data>
+
+ <!-- Wrapper Template -->
+ <template id="report_purchaseorder_internal">
+ <t t-call="web.html_container">
+ <t t-foreach="docs" t-as="doc">
+ <t t-call="indoteknik_custom.report_purchaseorder_internal_document" t-lang="doc.partner_id.lang"/>
+ </t>
+ </t>
+ </template>
+
+ <template id="report_purchaseorder_internal_document">
+ <t t-call="web.html_container">
+ <t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)" />
+
+ <!-- HEADER -->
+ <div class="header">
+ <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2498521"
+ style="width:100%; display:block;"/>
+ </div>
+
+ <!-- PAGE CONTENT -->
+ <div class="article" style="margin: 0 1.5cm 0 1.5cm; font-family:Arial, sans-serif; font-size:14px; color:#333;">
+
+ <!-- TITLE -->
+ <h2 style="text-align:center; margin:8px 0 0 0; color:#d32f2f; font-weight:800; letter-spacing:1px;">
+ PURCHASE ORDER
+ </h2>
+ <h4 style="text-align:center; margin:4px 0 20px 0; font-weight:normal; color:#555;">
+ No. <span t-field="doc.name"/>
+ </h4>
+
+ <!-- TOP INFO -->
+ <table style="width:100%; margin-bottom:20px; border-radius:8px; box-shadow:0 1px 4px rgba(0,0,0,0.1); overflow:hidden; border:1px solid #ddd;">
+ <tr style="background:#fafafa;">
+ <td style="padding:10px 12px;"><strong>Term Of Payment:</strong> <span t-field="doc.payment_term_id.name"/></td>
+ <td style="padding:10px 12px;"><strong>Order Date:</strong> <span t-field="doc.date_order" t-options='{"widget": "date"}'/></td>
+ <td style="padding:10px 12px;"><strong>Responsible:</strong> <span t-field="doc.user_id"/></td>
+ </tr>
+ </table>
+
+ <!-- VENDOR & DELIVERY -->
+ <table style="width:100%; margin-bottom:24px; border-spacing:16px 0;">
+ <tr>
+ <td style="width:50%; border:1px solid #ccc; border-radius:8px; padding:10px; background:#fcfcfc; vertical-align:top;">
+ <strong style="color:#d32f2f;">Alamat Pengiriman</strong><br/>
+ PT. Indoteknik Dotcom Gemilang<br/>
+ <t t-if="doc.overseas_po == True">
+ JALAN BANDENGAN UTARA 85A NO.8-9 RT. 003<br/>
+ RW. 016, PENJARINGAN, PENJARINGAN, KOTA<br/>
+ ADM. JAKARTA UTARA, DKI JAKARTA
+ </t>
+ <t t-else="">
+ Jl. Bandengan Utara Komp A &amp; B RT.<br/>
+ Penjaringan, Kec. Penjaringan, Jakarta<br/>
+ (BELAKANG INDOMARET)
+ </t>
+ <br/>
+ Daerah Khusus Ibukota Jakarta 14440
+ </td>
+ <td style="width:50%; border:1px solid #ccc; border-radius:8px; padding:10px; background:#fcfcfc; vertical-align:top;">
+ <strong style="color:#d32f2f;">Nama Vendor</strong><br/>
+ <span t-field="doc.partner_id.name"/><br/>
+ <span t-field="doc.partner_id.street"/><br/>
+ <span t-field="doc.partner_id.city"/> - <span t-field="doc.partner_id.zip"/>
+ </td>
+ </tr>
+ </table>
+
+ <!-- ORDER LINES -->
+ <table style="border-collapse:collapse; width:100%; margin-top:16px; font-size:14px;">
+ <tbody>
+ <!-- HEADER -->
+ <tr style="background:#e53935; color:white;">
+ <th style="border:1px solid #ccc; padding:8px; text-align:left;">No. &amp; Description</th>
+ <th style="border:1px solid #ccc; padding:8px; text-align:left;">Image</th>
+ <th style="border:1px solid #ccc; padding:8px; text-align:center;">Quantity</th>
+ <th style="border:1px solid #ccc; padding:8px; text-align:center;">Unit Price</th>
+ <th style="border:1px solid #ccc; padding:8px; text-align:center;">Taxes</th>
+ <th style="border:1px solid #ccc; padding:8px; text-align:center;">Subtotal</th>
+ </tr>
+
+ <!-- ISI ORDER LINE -->
+ <t t-foreach="doc.order_line" t-as="line" t-index="line_index">
+ <tr t-attf-style="background-color: #{ '#fafafa' if line_index % 2 == 0 else 'white' };">
+
+ <!-- NO & DESCRIPTION + IMAGE -->
+ <td style="border:1px solid #ccc; padding: 6px; display:flex; align-items:center; gap:10px;">
+ <!-- TEKS -->
+ <div style="display:flex; flex-direction:column; flex:1;">
+ <span style="font-weight:bold; margin-bottom:2px;">
+ <t t-esc="line_index + 1"/>.
+ <t t-if="line.product_id.id in [114360, 595346, 610166, 420315]">
+ <t t-esc="line.name"/>
+ </t>
+ <t t-else="">
+ <t t-esc="line.product_id.display_name"/>
+ </t>
+ </span>
+ </div>
+
+ </td>
+
+ <td style="border:1px solid #ccc; padding:6px; text-align:center;">
+ <t t-if="line.image_small">
+ <img t-att-src="image_data_uri(line.image_small)"
+ style="width:100px; height:100px; object-fit:contain; border:1px solid #ddd; border-radius:6px; background:#fff;"/>
+ </t>
+ </td>
+ <!-- QTY -->
+ <td style="border:1px solid #ccc; padding:6px; text-align:center;">
+ <span t-field="line.product_qty"/> <span t-field="line.product_uom"/>
+ </td>
+
+ <!-- UNIT PRICE -->
+ <td style="border:1px solid #ccc; padding:6px; text-align:center;">
+ <span t-field="line.price_unit"/>
+ </td>
+
+ <!-- TAXES -->
+ <td style="border:1px solid #ccc; padding:6px; text-align:center;">
+ <span t-esc="', '.join(map(lambda x: (x.description or x.name), line.taxes_id))"/>
+ </td>
+
+ <!-- SUBTOTAL -->
+ <td style="border:1px solid #ccc; padding:6px; text-align:right; font-weight:bold;">
+ <span t-field="line.price_subtotal"/>
+ </td>
+ </tr>
+
+ <!-- WEBSITE DESCRIPTION -->
+ <t t-if="line.show_description == True">
+ <tr t-attf-style="background-color: #{ '#fef5f5' if line_index % 2 == 0 else '#fffafa' }; ">
+ <td colspan="6" style="padding: 10px 14px; font-size:10px; line-height:1.3; font-style:italic; color:#555; border-left:1px solid #ccc; border-right:1px solid #ccc; border-bottom:1px solid #ccc;">
+ <div t-raw="line.product_id.website_description"/>
+ </td>
+ </tr>
+ </t>
+ </t>
+ </tbody>
+ </table>
+
+
+ <!-- TOTALS -->
+ <table style="margin-top:24px; margin-left:auto; width:40%; font-size:14px; border:1px solid #ddd; border-radius:6px; box-shadow:0 1px 3px rgba(0,0,0,0.08);">
+ <tr style="background:#fafafa;">
+ <td style="padding:8px;"><strong>Subtotal</strong></td>
+ <td style="text-align:right; padding:8px;"><span t-field="doc.amount_untaxed"/></td>
+ </tr>
+ <tr>
+ <td style="padding:8px;">Taxes</td>
+ <td style="text-align:right; padding:8px;"><span t-field="doc.amount_tax"/></td>
+ </tr>
+ <tr style="background:#fbe9e7; font-weight:bold; color:#d32f2f;">
+ <td style="padding:8px;">Total</td>
+ <td style="text-align:right; padding:8px;"><span t-field="doc.amount_total"/></td>
+ </tr>
+ <tr>
+ <td style="padding:8px;">Margin PO %</td>
+ <td style="text-align:right; padding:8px;"><span t-field="doc.total_percent_margin"/></td>
+ </tr>
+ <tr>
+ <td style="padding:8px;">Margin SO %</td>
+ <td style="text-align:right; padding:8px;"><span t-field="doc.total_so_percent_margin"/></td>
+ </tr>
+ </table>
+
+ <!-- NOTES -->
+ <div style="margin-top:24px; padding:12px; border-top:1px solid #ddd; font-style:italic; color:#555;">
+ <p t-field="doc.notes"/>
+ </div>
+ </div>
+
+ <!-- STATIC FOOTER -->
+ <div class="footer">
+ <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2859765"
+ style="width:100%; display:block;"/>
+ </div>
+
+ </t>
+ </template>
+
+
+
+</odoo>
diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv
index d501de1a..42c68e80 100755
--- a/indoteknik_custom/security/ir.model.access.csv
+++ b/indoteknik_custom/security/ir.model.access.csv
@@ -200,6 +200,7 @@ access_advance_payment_usage_line,access.advance.payment.usage.line,model_advanc
access_advance_payment_create_bill,access.advance.payment.create.bill,model_advance_payment_create_bill,,1,1,1,1
access_create_reimburse_cab_wizard_user,create.reimburse.cab.wizard user,model_create_reimburse_cab_wizard,,1,1,1,1
access_advance_payment_cancel_wizard,advance.payment.cancel.wizard,model_advance_payment_cancel_wizard,,1,1,1,1
+access_advance_payment_request_estimate_line,advance.payment.request.estimate.line,model_advance_payment_request_estimate_line,,1,1,1,1
access_purchasing_job_seen,purchasing.job.seen,model_purchasing_job_seen,,1,1,1,1
access_tukar_guling_all_users,tukar.guling.all.users,model_tukar_guling,base.group_user,1,1,1,1
@@ -215,3 +216,9 @@ access_surat_piutang_user,surat.piutang user,model_surat_piutang,,1,1,1,1
access_surat_piutang_line_user,surat.piutang.line user,model_surat_piutang_line,,1,1,1,1
access_sj_tele,access.sj.tele,model_sj_tele,base.group_system,1,1,1,1
access_stock_picking_sj_document,stock.picking.sj.document,model_stock_picking_sj_document,base.group_user,1,1,1,1
+access_gudang_service,gudang.service,model_gudang_service,base.group_user,1,1,1,1
+access_gudang_service_line,gudang.service.line,model_gudang_service_line,base.group_user,1,1,1,1
+access_update_depreciation_move_wizard,access.update.depreciation.move.wizard,model_update_depreciation_move_wizard,,1,1,1,1
+
+access_keywords,keywords,model_keywords,base.group_user,1,1,1,1
+access_token_log,access.token.log,model_token_log,,1,1,1,1
diff --git a/indoteknik_custom/views/account_move_line.xml b/indoteknik_custom/views/account_move_line.xml
index cb24a0f0..346494f3 100644
--- a/indoteknik_custom/views/account_move_line.xml
+++ b/indoteknik_custom/views/account_move_line.xml
@@ -9,6 +9,9 @@
<!-- <xpath expr="//page[@id='aml_tab']/field[@name='line_ids']" position="attributes">
<attribute name="attrs">{'readonly': [('refund_id','!=',False)]}</attribute>
</xpath> -->
+ <xpath expr="//field[@name='line_ids']/tree/field[@name='credit']" position="after">
+ <field name="date_maturity" optional="hide"/>
+ </xpath>
<xpath expr="//page[@id='aml_tab']/field[@name='line_ids']/tree/field[@name='currency_id']" position="before">
<field name="is_required" invisible="1"/>
</xpath>
@@ -22,4 +25,16 @@
</field>
</record>
</data>
+ <data>
+ <record id="action_gl_reconcile_server" model="ir.actions.server">
+ <field name="name">Reconcile Selected</field>
+ <field name="model_id" ref="account.model_account_move_line"/>
+ <field name="binding_model_id" ref="account.model_account_move_line"/>
+ <field name="binding_view_types">list</field>
+ <field name="state">code</field>
+ <field name="code">
+ action = records.action_gl_reconcile()
+ </field>
+ </record>
+ </data>
</odoo>
diff --git a/indoteknik_custom/views/advance_payment_request.xml b/indoteknik_custom/views/advance_payment_request.xml
index 7f422aa9..4faf905e 100644
--- a/indoteknik_custom/views/advance_payment_request.xml
+++ b/indoteknik_custom/views/advance_payment_request.xml
@@ -134,8 +134,28 @@
<br/>
</group>
</group>
- <notebook attrs="{'invisible': [('type_request', '!=', 'reimburse')]}">
- <page string="Rincian Reimburse">
+ <notebook>
+ <page string="Rincian Estimasi PUM" attrs="{'invisible': [('type_request', '!=', 'pum')]}">
+ <p style="font-size: 12px; color: grey; font-style: italic">*Masukkan estimasi alokasi biaya sebagai gambaran rencana penggunaan dana, tidak harus diisi dengan nominal yang akurat</p>
+ <field name="estimate_line_ids">
+ <tree>
+ <field name="category_estimate"/>
+ <field name="description"/>
+ <field name="nominal" sum="Total"/>
+ <field name="currency_id" invisible="1"/>
+ </tree>
+ <form>
+ <group col="2">
+ <field name="request_id" invisible="1"/>
+ <field name="category_estimate"/>
+ <field name="description" placeholder="Deskripsi tambahan untuk rincian estimasi..."/>
+ <field name="nominal"/>
+ <field name="currency_id" invisible="1"/>
+ </group>
+ </form>
+ </field>
+ </page>
+ <page string="Rincian Reimburse" attrs="{'invisible': [('type_request', '!=', 'reimburse')]}">
<field name="reimburse_line_ids">
<tree>
<field name="sequence" widget="handle"/>
@@ -236,6 +256,7 @@
<filter string="PUM" name="filter_pum" domain="[('type_request','=','pum')]"/>
<filter string="Reimburse" name="filter_reimburse" domain="[('type_request','=','reimburse')]"/>
<separator/>
+ <filter string="Cancelled" name="filter_cancelled" domain="[('status','=','cancel')]"/>
<filter string="Waiting for Approval" name="filter_waiting_approval" domain="[('status','in',['pengajuan1','pengajuan2','pengajuan3'])]"/>
<filter string="Approved" name="filter_approved" domain="[('status','=','approved')]"/>
<separator/>
diff --git a/indoteknik_custom/views/advance_payment_settlement.xml b/indoteknik_custom/views/advance_payment_settlement.xml
index a8bf1de7..352c5b96 100644
--- a/indoteknik_custom/views/advance_payment_settlement.xml
+++ b/indoteknik_custom/views/advance_payment_settlement.xml
@@ -118,12 +118,26 @@
<group string="Finance">
<field name="is_current_user_ap" invisible="1"/>
<field name="account_id" attrs="{'readonly': [('is_current_user_ap', '=', False)]}"/>
+ <field name="category_usage" invisible="1"/>
<field name="done_attachment" attrs="{'readonly': [('is_current_user_ap', '=', False)]}"/>
</group>
</group>
</form>
</field>
</page>
+ <page string="Rincian Estimasi PUM">
+ <p style="font-size: 12px; color: grey; font-style: italic">*Rincian estimasi PUM ini hanya sebagai gambaran umum untuk realisasi yang dilakukan, tidak harus diisi dengan nominal yang akurat.</p>
+ <field name="pum_estimate_line_ids" nolabel="1">
+ <tree>
+ <field name="category_estimate"/>
+ <field name="description"/>
+ <field name="nominal" sum="Total Estimasi"/>
+ <field name="frequency"/>
+ <field name="total_actual" sum="Total Actual"/>
+ <field name="currency_id" invisible="1"/>
+ </tree>
+ </field>
+ </page>
</notebook>
<div style="text-align:right;">
diff --git a/indoteknik_custom/views/approval_date_doc.xml b/indoteknik_custom/views/approval_date_doc.xml
index 3d597aa8..a3aae3b4 100644
--- a/indoteknik_custom/views/approval_date_doc.xml
+++ b/indoteknik_custom/views/approval_date_doc.xml
@@ -14,6 +14,7 @@
<field name="approve_date"/>
<field name="approve_by"/>
<field name="create_uid"/>
+ <field name="create_date"/>
</tree>
</field>
</record>
@@ -46,6 +47,7 @@
<field name="approve_date"/>
<field name="approve_by"/>
<field name="create_uid"/>
+ <field name="create_date"/>
<field name="note" attrs="{'invisible': [('state', '!=', 'cancel')]}"/>
<field name="state" readonly="1"/>
</group>
diff --git a/indoteknik_custom/views/find_page.xml b/indoteknik_custom/views/find_page.xml
index c752aa98..fc9bddbb 100644
--- a/indoteknik_custom/views/find_page.xml
+++ b/indoteknik_custom/views/find_page.xml
@@ -25,7 +25,7 @@
<group>
<field name="category_id"/>
<field name="brand_id"/>
- <field name="url"/>
+ <field name="url" />
</group>
<group>
<field name="create_uid"/>
@@ -62,9 +62,18 @@
<field name="view_mode">tree,form</field>
</record>
+
+ <record id="ir_actions_server_find_page_sync_to_solr" model="ir.actions.server">
+ <field name="name">Sync to solr</field>
+ <field name="model_id" ref="indoteknik_custom.model_web_find_page"/>
+ <field name="binding_model_id" ref="indoteknik_custom.model_web_find_page"/>
+ <field name="state">code</field>
+ <field name="code">model._sync_to_solr()</field>
+ </record>
+
<menuitem id="menu_web_find_page"
name="Web Find Page"
action="web_find_page_action"
parent="website_sale.menu_orders"
sequence="8"/>
-</odoo> \ No newline at end of file
+</odoo>
diff --git a/indoteknik_custom/views/gudang_service.xml b/indoteknik_custom/views/gudang_service.xml
new file mode 100644
index 00000000..769664c5
--- /dev/null
+++ b/indoteknik_custom/views/gudang_service.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<odoo>
+ <data>
+ <!-- Tree -->
+ <record id="view_gudang_service_tree" model="ir.ui.view">
+ <field name="name">gudang.serivice.tree</field>
+ <field name="model">gudang.service</field>
+ <field name="arch" type="xml">
+ <tree string="Monitoring Barang Service" decoration-danger="state in ('draft', 'received_from_cust')" decoration-warning="state in ('sent_to_vendor', 'received_from_vendor')"
+ decoration-success="state == 'delivered_to_cust'" decoration-muted="state == 'cancel'" >
+ <field name="name"/>
+ <field name="partner_id"/>
+ <field name="vendor_id"/>
+ <field name="origin"/>
+ <field name="schedule_date"/>
+ <field name="start_date" optional="hide"/>
+ <field name="remaining_date"/>
+ <field name="state" widget="badge" decoration-danger="state in ('draft', 'received_from_cust')" decoration-warning="state in ('sent_to_vendor', 'received_from_vendor')"
+ decoration-success="state == 'delivered_to_cust'" decoration-muted="state == 'cancel'" />
+ <field name="cancel_reason" optional="hide"/>
+ <field name="create_date" optional="hide"/>
+ </tree>
+ </field>
+ </record>
+ <!-- Form -->
+ <record id="view_gudang_service_form" model="ir.ui.view">
+ <field name="name">gudang.service.form</field>
+ <field name="model">gudang.service</field>
+ <field name="arch" type="xml">
+ <form>
+ <header>
+ <button name="action_submit" string="Proceed" type="object"
+ class="btn-primary"
+ attrs="{'invisible': [('state', 'in', ['cancel', 'done', 'received_from_vendor', 'delivered_to_cust'])]}"/>
+ <button name="action_done" string="Set Done" type="object"
+ class="btn-primary"
+ attrs="{'invisible': [('state', 'not in', ['received_from_vendor'])]}"/>
+ <button name="action_cancel" string="Cancel" type="object"
+ class="btn-secondary"
+ attrs="{'invisible': [('state', 'in', ['cancel', 'delivered_to_cust'])]}"/>
+ <button name="action_draft" string="Set to Backlog" type="object"
+ class="btn-secondary"
+ attrs="{'invisible': [('state', 'not in', ['cancel'])]}"/>
+ <field name="state" widget="statusbar" readonly="1"/>
+ </header>
+ <sheet>
+ <div class="oe_title">
+ <h1>
+ <field name="name" readonly="1" class="oe_inline"/>
+ </h1>
+ </div>
+ <group>
+ <field name="origin" attrs="{'readonly': [('state', 'not in', ['draft'])]}"/>
+ <field name="partner_id"/>
+ <field name="vendor_id"/>
+ <field name="remaining_date"/>
+ <field name="schedule_date" attrs="{'readonly': [('state', 'not in', ['draft', 'reveived_from_cust'])]}"/>
+ <field name="start_date" readonly="1"/>
+ <field name="done_date" attrs="{'invisible': [('state', 'not in', ['delivered_to_cust'])]}"/>
+ <field name="create_uid"/>
+ <field name="cancel_reason"
+ attrs="{'invisible': [('state', 'in', ['delivered_to_cust', 'draft'])]}"/>
+ </group>
+ <notebook>
+ <page string="Product Lines" name="product_lines">
+ <field name="gudang_service_lines">
+ <tree string="Product Lines" editable="top" create="0" delete="1">
+ <field name="product_id"/>
+ <field name="quantity"/>
+ </tree>
+ </field>
+ </page>
+ </notebook>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
+ </form>
+ </field>
+ </record>
+ <!-- Action -->
+ <record id="action_gudang_service" model="ir.actions.act_window">
+ <field name="name">Monitoring Barang Service</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">gudang.service</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ <!-- Menu -->
+ <menuitem
+ id="menu_gudang_service"
+ name="Monitoring Barang Service"
+ parent="indoteknik_custom.menu_monitoring_in_sale"
+ sequence="10"
+ action="action_gudang_service"
+ />
+ </data>
+ <!-- Cron -->
+ <record id="ir_cron_gudang_service_logistik_notify" model="ir.cron">
+ <field name="name">Gudang Service Daily Notification</field>
+ <field name="model_id" ref="model_gudang_service"/>
+ <field name="state">code</field>
+ <field name="code">model.cron_notify_onprogress_gudang_service()</field>
+ <field name="interval_number">1</field>
+ <field name="interval_type">days</field>
+ <field name="numbercall">-1</field>
+ <field name="active">False</field>
+ </record>
+</odoo>
diff --git a/indoteknik_custom/views/ir_sequence.xml b/indoteknik_custom/views/ir_sequence.xml
index 46148606..55e48300 100644
--- a/indoteknik_custom/views/ir_sequence.xml
+++ b/indoteknik_custom/views/ir_sequence.xml
@@ -260,5 +260,14 @@
<field name="number_next">1</field>
<field name="number_increment">1</field>
</record>
+
+ <record id="seq_gudang_service" model="ir.sequence">
+ <field name="name">Gudang Service</field>
+ <field name="code">gudang.service</field>
+ <field name="prefix">MGS/%(year)s/%(month)s/</field>
+ <field name="padding">4</field>
+ <field name="number_next">1</field>
+ <field name="number_increment">1</field>
+ </record>
</data>
</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/kartu_stock.xml b/indoteknik_custom/views/kartu_stock.xml
new file mode 100644
index 00000000..705d86a2
--- /dev/null
+++ b/indoteknik_custom/views/kartu_stock.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+ <data>
+ <record id="kartu_stok_wizard_form_inherit_single_excel" model="ir.ui.view">
+ <field name="name">kartu.stok.wizard.form.inherit.single.excel</field>
+ <field name="model">kartu.stok.wizard</field>
+ <field name="inherit_id" ref="proweb_kartu_stok.print_kartu_stok_view_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//footer" position="inside">
+
+ <button name="action_kartu_stok_excel_single_sheet" type="object" string="Print Excel (Single Sheet)" class="btn-primary"/>
+
+ </xpath>
+ </field>
+ </record>
+ </data>
+</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/keywords.xml b/indoteknik_custom/views/keywords.xml
new file mode 100644
index 00000000..a33edae0
--- /dev/null
+++ b/indoteknik_custom/views/keywords.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<odoo>
+ <record id="keywords_tree" model="ir.ui.view">
+ <field name="name">keywords.tree</field>
+ <field name="model">keywords</field>
+ <field name="arch" type="xml">
+ <tree >
+ <field name="category_id" />
+ <field name="keywords" />
+ <field name="url" />
+ <field name="sum" />
+ <field name="solr_flag" readonly="1"/>
+ <field name="product_ids" widget="many2many_tags" />
+ </tree>
+ </field>
+ </record>
+
+ <record id="keywords_form" model="ir.ui.view">
+ <field name="name">keywords.form</field>
+ <field name="model">keywords</field>
+ <field name="arch" type="xml">
+ <form>
+ <header>
+ <button name="generate_products" string="Generate Product Manual" type="object" class="oe_highlight"/>
+ <button name="clear_products" string="Clear Generated Products" type="object" />
+ </header>
+ <sheet>
+ <div class="oe_title">
+ <h1>
+ <field name="name" readonly="1" class="oe_inline"/>
+ </h1>
+ </div>
+ <group>
+ <field name="category_id" />
+ <field name="keywords" />
+ <field name="url" />
+ <field name="sum" />
+ <field name="product_ids" widget="many2many_tags" />
+ <field name="skip" />
+ <field name="solr_flag" readonly="1"/>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="view_keywords_filter" model="ir.ui.view">
+ <field name="name">keywords.list.select</field>
+ <field name="model">keywords</field>
+ <field name="priority" eval="15"/>
+ <field name="arch" type="xml">
+ <search string="Search Keywords">
+ <field name="category_id"/>
+ <field name="keywords"/>
+ <field name="sum"/>
+ <field name="solr_flag" readonly="1"/>
+ <field name="product_ids" widget="many2many_tags"/>
+ </search>
+ </field>
+ </record>
+ <record id="action_keywords" model="ir.actions.act_window">
+ <field name="name">Keywords</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">keywords</field>
+ <field name="search_view_id" ref="view_keywords_filter"/>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ <record id="ir_actions_server_keywords_sync_to_solr" model="ir.actions.server">
+ <field name="name">Sync to solr</field>
+ <field name="model_id" ref="indoteknik_custom.model_keywords"/>
+ <field name="binding_model_id" ref="indoteknik_custom.model_keywords"/>
+ <field name="state">code</field>
+ <field name="code">model.sync_solr()</field>
+ </record>
+ <menuitem id="menu_keywords"
+ name="Keywords"
+ parent="website_sale.menu_orders"
+ action="action_keywords"
+ sequence="100"/>
+
+ <data noupdate="1">
+ <record id="cron_keywords_solr_flag_queue" model="ir.cron">
+ <field name="name">Sync Keywords To Solr: Queue Solr Flag 2</field>
+ <field name="interval_number">1</field>
+ <field name="interval_type">hours</field>
+ <field name="numbercall">-1</field>
+ <field name="doall" eval="False"/>
+ <field name="model_id" ref="model_keywords"/>
+ <field name="code">model.solr_flag_to_queue(limit=500)</field>
+ <field name="state">code</field>
+ <field name="priority">55</field>
+ <field name="active">False</field>
+ </record>
+ </data>
+</odoo>
diff --git a/indoteknik_custom/views/mail_template_efaktur.xml b/indoteknik_custom/views/mail_template_efaktur.xml
index b0637691..ea473364 100644
--- a/indoteknik_custom/views/mail_template_efaktur.xml
+++ b/indoteknik_custom/views/mail_template_efaktur.xml
@@ -11,7 +11,7 @@
<field name="email_to">${object.partner_id.email|safe}</field>
<field name="body_html" type="html">
<p>Dengan Hormat Bpk/Ibu ${object.partner_id.name},</p>
- <p>Terlampir Faktur Pajak atas Invoice ${object.name}.</p>
+ <p>Terlampir Faktur Pajak atas Invoice ${object.name}${ ' (%s)' % ctx.get('cust_ref') if ctx.get('cust_ref') else '' }</p>
<p><strong>Keterangan:</strong></p>
<p>Mohon dicek langsung faktur pajak terlampir, terutama informasi nomor NPWP dan alamat NPWP serta nama pembelian barang. Jika ada yang tidak sesuai, mohon segera menginformasikan kepada kami paling lambat 1 (satu) minggu dari tanggal email ini. Revisi faktur pajak tidak dapat kami proses apabila sudah melewati 1 (satu) minggu. Harap maklum.</p>
<p>Mohon balas email ini jika sudah menerima, terima kasih.</p>
diff --git a/indoteknik_custom/views/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml
index 16b8bd44..9651cdd6 100755
--- a/indoteknik_custom/views/purchase_order.xml
+++ b/indoteknik_custom/views/purchase_order.xml
@@ -46,6 +46,19 @@
</button>
</xpath>
<button id="draft_confirm" position="after">
+ <button name="action_create_order_altama"
+ type="object"
+ string="Create Order Altama"
+ class="oe_highlight"
+ icon="fa-cloud-upload"
+ attrs="{'invisible': [('partner_id', '!=', 5571)]}"
+ />
+ <button name="action_get_order_altama"
+ type="object"
+ string="Get Order Altama"
+ class="oe_highlight"
+ attrs="{'invisible': [('partner_id', '!=', 5571)]}"
+ icon="fa-cloud-download"/>
<button name="po_approve"
string="Ask Approval"
type="object"
@@ -76,6 +89,10 @@
</field>
<field name="approval_status" position="after">
<field name="revisi_po"/>
+ <field name="soo_number" attrs="{'invisible': [('partner_id', '!=', 5571)]}"/>
+ <field name="soo_price" attrs="{'invisible': [('partner_id', '!=', 5571)]}"/>
+ <field name="soo_discount" attrs="{'invisible': [('partner_id', '!=', 5571)]}"/>
+ <field name="soo_tax" attrs="{'invisible': [('partner_id', '!=', 5571)]}"/>
<field name="not_update_purchasepricelist"/>
</field>
<field name="approval_status" position="after">
@@ -105,6 +122,7 @@
<field name="amount_total" position="after">
<field name="total_margin"/>
<field name="total_so_margin"/>
+ <field name="amount_cashback"/>
<field name="total_percent_margin"/>
<field name="total_so_percent_margin"/>
</field>
@@ -152,8 +170,13 @@
</field>
<field name="price_unit" position="after">
<field name="price_vendor" attrs="{'readonly': 1}" optional="hide"/>
+ <field name="description" optional="hide"/>
+ <field name="docstatus_altama" optional="hide"/>
</field>
<field name="price_subtotal" position="after">
+ <field name="altama_ordered" optional="hide" readonly="1"/>
+ <field name="altama_delivered" optional="hide" readonly="1"/>
+ <field name="altama_invoiced" optional="hide" readonly="1"/>
<field name="so_item_margin" attrs="{'readonly': 1}" optional="hide"/>
<field name="so_item_percent_margin" attrs="{'readonly': 1}" optional="hide"/>
<field name="item_margin" attrs="{'readonly': 1}" optional="hide"/>
diff --git a/indoteknik_custom/views/purchasing_job.xml b/indoteknik_custom/views/purchasing_job.xml
index d303e5ea..16fbb01a 100644
--- a/indoteknik_custom/views/purchasing_job.xml
+++ b/indoteknik_custom/views/purchasing_job.xml
@@ -7,20 +7,20 @@
<tree decoration-info="(check_pj == False)" create="false" multi_edit="1">
<field name="product_id"/>
<field name="vendor_id"/>
- <field name="purchase_representative_id"/>
- <field name="brand"/>
- <field name="item_code"/>
- <field name="product"/>
- <field name="onhand"/>
- <field name="incoming"/>
- <field name="outgoing"/>
- <field name="status_apo" invisible="1"/>
- <field name="action"/>
- <field name="note"/>
- <field name="note_detail"/>
+ <field name="purchase_representative_id" optional="hide"/>
+ <field name="brand" optional="hide"/>
+ <field name="item_code" optional="hide"/>
+ <field name="product" optional="hide"/>
+ <field name="onhand" optional="hide"/>
+ <field name="incoming" optional="hide"/>
+ <field name="outgoing" optional="hide"/>
+ <field name="status_apo" optional="hide" invisible="1"/>
+ <field name="action" optional="hide"/>
+ <field name="note" optional="hide"/>
+ <field name="note_detail" optional="hide"/>
<field name="date_po" optional="hide"/>
- <field name="so_number"/>
- <field name="check_pj" invisible="1"/>
+ <field name="so_number" optional="hide"/>
+ <field name="check_pj" optional="hide" invisible="1"/>
<button name="action_open_job_detail"
string="📄"
type="object"
@@ -59,7 +59,7 @@
<field name="outgoing"/>
<field name="purchase_representative_id"/>
<field name="vendor_id"/>
- <field name="note"/>
+ <!-- <field name="note"/> -->
<field name="note_detail"/>
</group>
</group>
diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml
index 23fbe155..2d4488ab 100755
--- a/indoteknik_custom/views/sale_order.xml
+++ b/indoteknik_custom/views/sale_order.xml
@@ -302,6 +302,7 @@
]
}
"/>
+ <field name="amount_cashback"/>
<field name="purchase_price_md" optional="hide"/>
<field name="purchase_tax_id"
attrs="{'readonly': [('parent.approval_status', '!=', False)]}"
@@ -384,7 +385,7 @@
{
'readonly': [
'|',
- ('approval_status', 'in', ['pengajuan1', 'pengajuan2', 'approved']),
+ ('approval_status', 'in', ['pengajuan0','pengajuan1', 'pengajuan2', 'approved']),
('state', 'not in', ['cancel', 'draft'])
]
}
diff --git a/indoteknik_custom/views/shipment_group.xml b/indoteknik_custom/views/shipment_group.xml
index c3f79bda..e348867c 100644
--- a/indoteknik_custom/views/shipment_group.xml
+++ b/indoteknik_custom/views/shipment_group.xml
@@ -7,6 +7,7 @@
<tree default_order="create_date desc">
<field name="number"/>
<field name="partner_id"/>
+ <field name="partner_ids" widget="many2many_tags" optional="hide"/>
<field name="carrier_id"/>
<field name="total_colly_line"/>
</tree>
@@ -42,9 +43,12 @@
<group>
<group>
<field name="number" readonly="1"/>
+ <field name="is_multi_partner" readonly="1"/>
</group>
<group>
- <field name="partner_id" readonly="1"/>
+ <field name="partner_id" readonly="1" attrs="{'invisible': [('is_multi_partner', '=', True)]}"/>
+ <field name="partner_ids" readonly="1" widget="many2many_tags" attrs="{'invisible': [('is_multi_partner', '=', False)]}"/>
+ <field name="driver" attrs="{'invisible': [('carrier_id', '!=', 1)]}"/>
<field name="carrier_id" readonly="1"/>
<field name="total_colly_line" readonly="1"/>
</group>
diff --git a/indoteknik_custom/views/stock_inventory.xml b/indoteknik_custom/views/stock_inventory.xml
index db85f05c..ab1b6eec 100644
--- a/indoteknik_custom/views/stock_inventory.xml
+++ b/indoteknik_custom/views/stock_inventory.xml
@@ -9,6 +9,7 @@
<xpath expr="//field[@name='location_ids']" position="after">
<field name="number" readonly="1"/>
<field name="adjusment_type" />
+ <field name="approval_state" />
</xpath>
</field>
</record>
@@ -21,6 +22,8 @@
<field name="arch" type="xml">
<xpath expr="//field[@name='date']" position="after">
<field name="number"/>
+ <field name="adjusment_type" />
+ <field name="approval_state"/>
</xpath>
</field>
</record>
diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml
index 9aa0581c..29a95626 100644
--- a/indoteknik_custom/views/stock_picking.xml
+++ b/indoteknik_custom/views/stock_picking.xml
@@ -133,6 +133,7 @@
<field name="total_mapping_koli" attrs="{'invisible': [('location_id', '!=', 60)]}"/>
<field name="total_koli_display" readonly="1" attrs="{'invisible': [('location_id', '!=', 60)]}"/>
<field name="linked_out_picking_id" readonly="1" attrs="{'invisible': [('location_id', '=', 60)]}"/>
+ <field name="number_soo" attrs="{'invisible': [('picking_type_id', 'in', [29,30])]}"/>
</field>
<field name="weight_uom_name" position="after">
<group>
@@ -245,6 +246,7 @@
<field name="responsible"/>
<field name="carrier_id"
attrs="{'invisible': [('select_shipping_option_so', '=', 'biteship')]}"/>
+ <field name="shipment_group_id"/>
<field name="biteship_id" invisible="1"/>
<field name="out_code" attrs="{'invisible': [['out_code', '=', False]]}"/>
<field name="picking_code" attrs="{'invisible': [['picking_code', '=', False]]}"/>
diff --git a/indoteknik_custom/views/token_log.xml b/indoteknik_custom/views/token_log.xml
new file mode 100644
index 00000000..77e6dd48
--- /dev/null
+++ b/indoteknik_custom/views/token_log.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+ <record id="token_log_tree" model="ir.ui.view">
+ <field name="name">token.log.tree</field>
+ <field name="model">token.log</field>
+ <field name="arch" type="xml">
+ <tree editable="top" default_order="create_date desc">
+ <field name="token"/>
+ <field name="expires_at"/>
+ <field name="token_from"/>
+ <field name="created_at"/>
+ <field name="created_by"/>
+ <field name="source"/>
+ <field name="is_active" widget="boolean_toggle"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="token_log_action" model="ir.actions.act_window">
+ <field name="name">Token Log</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">token.log</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ <menuitem
+ action="token_log_action"
+ id="token_log"
+ parent="base.menu_users"
+ name="Token Log"
+ sequence="1"
+ />
+</odoo>
diff --git a/indoteknik_custom/views/tukar_guling.xml b/indoteknik_custom/views/tukar_guling.xml
index 8cfb5680..609dea15 100644
--- a/indoteknik_custom/views/tukar_guling.xml
+++ b/indoteknik_custom/views/tukar_guling.xml
@@ -133,5 +133,29 @@
</form>
</field>
</record>
+ <record id="view_tukar_guling_filter" model="ir.ui.view">
+ <field name="name">tukar.guling.filter</field>
+ <field name="model">tukar.guling</field>
+ <field name="arch" type="xml">
+ <search string="Tukar Guling">
+ <field name="name" string="No. Dokumen"/>
+ <field name="partner_id" string="Customer"/>
+ <field name="origin" string="SO Number"/>
+ <field name="operations" string="Operations"/>
+ <!-- <filter string="Pengajuan Saya" name="my_tukar_guling" domain="[('create_uid', '=', uid)]"/> -->
+ <separator/>
+ <filter string="Tukar Guling" name="tukar_guling" domain="[('return_type', '=', 'tukar_guling')]"/>
+ <filter string="Return SO" name="return_so" domain="[('return_type', '=', 'retur_so')]"/>
+ <separator/>
+ <filter string="Approval Sales" name="approval_sales" domain="[('state', '=', 'approval_sales')]"/>
+ <filter string="Approval Logistic" name="approval_logistic" domain="[('state', '=', 'approval_logistic')]"/>
+ <filter string="Approval Finance" name="approval_finance" domain="[('state', '=', 'approval_finance')]"/>
+ <filter string="Approved" name="approved" domain="[('state', '=', 'approved')]"/>
+ <separator/>
+ <filter string="Done" name="done" domain="[('state', '=', 'done')]"/>
+ <filter string="Cancelled" name="cancel" domain="[('state', '=', 'cancel')]"/>
+ </search>
+ </field>
+ </record>
</data>
</odoo>
diff --git a/indoteknik_custom/views/update_depreciation_move_wizard_view.xml b/indoteknik_custom/views/update_depreciation_move_wizard_view.xml
new file mode 100644
index 00000000..ff128a71
--- /dev/null
+++ b/indoteknik_custom/views/update_depreciation_move_wizard_view.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<odoo>
+ <record id="view_update_depreciation_move_wizard_form" model="ir.ui.view">
+ <field name="name">update.depreciation.move.wizard.form</field>
+ <field name="model">update.depreciation.move.wizard</field>
+ <field name="arch" type="xml">
+ <form string="Update Move Check">
+ <group>
+ <field name="target_date"/>
+ </group>
+ <footer>
+ <button string="Update" type="object" name="action_update_move_check" class="btn-primary"/>
+ <button string="Batal" special="cancel" class="btn-secondary"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <record id="update_depreciation_move_wizard_action" model="ir.actions.act_window">
+ <field name="name">Update Depreciation Asset</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">update.depreciation.move.wizard</field>
+ <field name="view_mode">form</field>
+ <field name="target">new</field>
+ </record>
+
+ <menuitem
+ id="menu_update_depreciation_move_wizard"
+ name="Update Depreciation Asset"
+ parent="account.menu_finance_entries_management"
+ sequence="4"
+ action="update_depreciation_move_wizard_action"
+ />
+</odoo>
+ \ No newline at end of file
diff --git a/indoteknik_custom/views/x_manufactures.xml b/indoteknik_custom/views/x_manufactures.xml
index d5cec350..b52fe795 100755
--- a/indoteknik_custom/views/x_manufactures.xml
+++ b/indoteknik_custom/views/x_manufactures.xml
@@ -82,7 +82,7 @@
<field name="x_negara_asal"/>
<field name="x_short_desc"/>
<field name="x_manufacture_level"/>
- <!-- <field name="cashback_percent" widget="percentage"/> -->
+ <field name="cashback_percent" widget="percentage"/>
<field name="x_produk_aksesoris_sparepart"/>
<field name="cache_reset_status"/>
<field name="parent_id"/>