summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHafidBuroiroh <hafidburoiroh09@gmail.com>2026-02-28 09:56:26 +0700
committerHafidBuroiroh <hafidburoiroh09@gmail.com>2026-02-28 09:56:26 +0700
commit8d953f913aceb97faa026253b65d6159759f5a62 (patch)
tree0ff82faa484cae7c87f75df556966e0da49ee088
parent7bfc92fdb73a89c5bc0b4c711315cbd5ea3ff268 (diff)
parent7cd31c8dab49c59f8c6e67528d528514cc13932c (diff)
<hafid> change request sourcing job order
-rw-r--r--indoteknik_api/controllers/api_v1/sale_order.py4
-rw-r--r--indoteknik_api/models/sale_order.py2
-rwxr-xr-xindoteknik_custom/__manifest__.py5
-rwxr-xr-xindoteknik_custom/models/__init__.py3
-rw-r--r--indoteknik_custom/models/advance_payment_request.py96
-rw-r--r--indoteknik_custom/models/automatic_purchase.py73
-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.py43
-rw-r--r--indoteknik_custom/models/price_group.py20
-rwxr-xr-xindoteknik_custom/models/product_template.py13
-rwxr-xr-xindoteknik_custom/models/purchase_order.py310
-rwxr-xr-xindoteknik_custom/models/purchase_order_line.py17
-rwxr-xr-xindoteknik_custom/models/purchase_pricelist.py7
-rw-r--r--indoteknik_custom/models/res_partner.py34
-rwxr-xr-xindoteknik_custom/models/sale_order.py64
-rw-r--r--indoteknik_custom/models/stock_picking.py1
-rw-r--r--indoteknik_custom/models/token_log.py17
-rw-r--r--indoteknik_custom/models/tukar_guling.py61
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv5
-rw-r--r--indoteknik_custom/views/account_move_line.xml3
-rw-r--r--indoteknik_custom/views/advance_payment_request.xml24
-rw-r--r--indoteknik_custom/views/advance_payment_settlement.xml14
-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
-rwxr-xr-xindoteknik_custom/views/purchase_order.xml22
-rwxr-xr-xindoteknik_custom/views/sale_order.xml2
-rw-r--r--indoteknik_custom/views/stock_picking.xml1
-rw-r--r--indoteknik_custom/views/token_log.xml33
30 files changed, 1394 insertions, 75 deletions
diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py
index cff1921d..f4a2a9d4 100644
--- a/indoteknik_api/controllers/api_v1/sale_order.py
+++ b/indoteknik_api/controllers/api_v1/sale_order.py
@@ -154,13 +154,13 @@ class SaleOrder(controller.Controller):
elif status == 'belum_bayar':
domain += [
('state', '=', 'draft'),
- ('approval_status', 'in', ['pengajuan1', 'pengajuan2']),
+ ('approval_status', 'in', ['pengajuan0','pengajuan1', 'pengajuan2']),
('payment_status', 'in', [False, None, '', 'pending', 'expire'])
]
elif status == 'diproses':
domain += [
('state', '=', 'draft'),
- ('approval_status', 'in', ['pengajuan1', 'pengajuan2']),
+ ('approval_status', 'in', ['pengajuan0','pengajuan1', 'pengajuan2']),
('payment_status', '!=', False),
('payment_status', 'not in', ['', 'pending', 'expire']),
]
diff --git a/indoteknik_api/models/sale_order.py b/indoteknik_api/models/sale_order.py
index 23be358a..7951de45 100644
--- a/indoteknik_api/models/sale_order.py
+++ b/indoteknik_api/models/sale_order.py
@@ -66,7 +66,7 @@ class SaleOrder(models.Model):
elif sale_order.state == 'draft':
if not sale_order.approval_status:
data['status'] = 'draft'
- elif sale_order.approval_status in ('pengajuan1', 'pengajuan2'):
+ elif sale_order.approval_status in ('pengajuan0', 'pengajuan1', 'pengajuan2'):
if sale_order.payment_status in ('', 'pending', False, None, 'expire'):
data['status'] = 'belum_bayar'
elif sale_order.payment_status not in ['', 'pending', False, None, 'expire']:
diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py
index 66cbd995..9458cc41 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',
@@ -193,6 +193,9 @@
'views/commission_internal.xml',
'views/keywords.xml',
'views/sourcing.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 9d9c0ec5..fb93e0a3 100755
--- a/indoteknik_custom/models/__init__.py
+++ b/indoteknik_custom/models/__init__.py
@@ -165,6 +165,9 @@ 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 sourcing_job_order
+from . import token_log
+from . import kartu_stock
diff --git a/indoteknik_custom/models/advance_payment_request.py b/indoteknik_custom/models/advance_payment_request.py
index 8cadb1b6..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':
@@ -751,7 +763,7 @@ class AdvancePaymentRequest(models.Model):
pum_ids = self.search([
('user_id', '=', user.id),
- ('status', '!=', 'reject'),
+ ('status', '!=', 'cancel'),
('type_request', '=', 'pum')
])
@@ -911,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
@@ -1144,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]
@@ -1612,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 0b2f7d1b..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
@@ -740,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
index 4ab649dc..3fa9dd72 100644
--- a/indoteknik_custom/models/keywords.py
+++ b/indoteknik_custom/models/keywords.py
@@ -133,7 +133,7 @@ class Keywords(models.Model):
def _onchange_solr_flag(self):
"""Set solr_flag=2 when tracked fields change to trigger queue sync"""
for record in self:
- if not record.skip:
+ if len(record.product_ids) > 0:
record.solr_flag = 2
def solr_flag_to_queue(self, limit=500):
@@ -161,10 +161,18 @@ class Keywords(models.Model):
return True
def _sync_keywords_queue_callback(self):
- """Callback method executed by apache.solr.queue - syncs keyword data to Solr"""
- documents = []
+ 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,
@@ -173,13 +181,19 @@ class Keywords(models.Model):
'url_s': keyword.url,
'product_ids_is': [p.product_tmpl_id.id for p in keyword.product_ids],
}
- documents.append(doc)
+
+ solr.add([doc])
+
+ success_keywords |= keyword
+
except Exception as e:
- _logger.error('failed %s', e)
- _logger.error('doc data: %s', doc)
+ _logger.error(
+ "Solr sync failed for keyword ID %s: %s",
+ keyword.id, e
+ )
- if documents:
- solr.add(documents)
+ if success_keywords:
+ success_keywords.write({'solr_flag': 0})
return True
@@ -194,6 +208,11 @@ class Keywords(models.Model):
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 = {
@@ -223,10 +242,12 @@ class Keywords(models.Model):
def write(self, vals):
result = super().write(vals)
- tracked_fields = ['keywords', 'category_id', 'product_ids']
+ tracked_fields = ['keywords', 'category_id', 'product_ids', 'skip', 'name']
neded_sync = any(field in vals for field in tracked_fields)
- if neded_sync and self.skip == False:
+ if neded_sync:
for record in self:
- record.solr_flag = 2
+ # 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 969c9765..56022d06 100755
--- a/indoteknik_custom/models/product_template.py
+++ b/indoteknik_custom/models/product_template.py
@@ -94,7 +94,6 @@ class ProductTemplate(models.Model):
if self.env.user.id not in users_in_group.mapped('id') and active_model == None:
raise UserError('Hanya MD yang bisa membuat Product')
result = super(ProductTemplate, self).create(vals)
- self.env['product.product']._add_product_to_keywords(result)
return result
# def write(self, values):
@@ -895,11 +894,6 @@ class ProductTemplate(models.Model):
# Log changes
self._log_field_changes_product(vals, old_values)
- # Add product to keywords
- keyword_trigger = ['name', 'website_description', 'unpublished']
- if any(field in vals for field in keyword_trigger):
- for product in self:
- self.env['product.product']._add_product_to_keywords(product)
return result
# def write(self, vals):
@@ -952,6 +946,7 @@ class ProductProduct(models.Model):
string="Sourcing Job",
readonly=True,
)
+ has_magento = fields.Boolean(string='Has Magento?', default=False, readonly=True)
def _add_product_to_keywords(self,product):
keywords_model = self.env['keywords']
@@ -971,8 +966,6 @@ class ProductProduct(models.Model):
return True
- has_magento = fields.Boolean(string='Has Magento?', default=False, readonly=True)
-
def generate_product_sla(self):
product_variant_ids = self.env.context.get('active_ids', [])
product_variant = self.search([('id', 'in', product_variant_ids)])
@@ -993,7 +986,6 @@ class ProductProduct(models.Model):
if self.env.user.id not in users_in_group.mapped('id') and active_model == None:
raise UserError('Hanya MD yang bisa membuat Product')
result = super(ProductProduct, self).create(vals)
- self._add_product_to_keywords(result)
return result
# def write(self, values):
@@ -1360,9 +1352,6 @@ class ProductProduct(models.Model):
]
# pake ini kalau mau Cek semua field
# if vals:
- trigger_fields = ['name', 'website_description', 'unpublished']
- if any(f in vals for f in trigger_fields):
- self._add_product_to_keywords(vals)
if any(field in vals for field in tracked_fields):
old_values = self._collect_old_values(vals)
result = super().write(vals)
diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py
index b3ecca56..a345b96b 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
@@ -125,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()
@@ -632,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:
@@ -1142,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.")
@@ -1159,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)
diff --git a/indoteknik_custom/models/purchase_order_line.py b/indoteknik_custom/models/purchase_order_line.py
index 76dcc09e..c6a49481 100755
--- a/indoteknik_custom/models/purchase_order_line.py
+++ b/indoteknik_custom/models/purchase_order_line.py
@@ -55,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):
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/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 3382d95b..92efa3bd 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','ninja','lion','rara','sicepat','jnt','pos','idexpress','rpx','wahana','jdl','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')
@@ -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(
@@ -2601,12 +2616,22 @@ class SaleOrder(models.Model):
def check_archived_product(self):
for order in self:
for line in order.order_line:
- if line.product_id.active == False:
- raise UserError("Terdapat Product yang sudah di Archive pada Product: {}".format(line.product_id.display_name))
+ # 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))
@@ -2657,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()
@@ -2778,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
@@ -3397,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.")
diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py
index 065b1484..ab366fd6 100644
--- a/indoteknik_custom/models/stock_picking.py
+++ b/indoteknik_custom/models/stock_picking.py
@@ -459,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"""
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/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv
index 24604eeb..fed6193a 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,7 +216,10 @@ 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_sourcing_job_order,access.sourcing_job_order,model_sourcing_job_order,base.group_user,1,1,1,1
access_sourcing_job_order_line_user,sourcing.job.order.line,model_sourcing_job_order_line,base.group_user,1,1,1,1
@@ -224,3 +228,4 @@ access_wizard_export_sjo_to_so,wizard.export.sjo.to.so,model_wizard_export_sjo_t
access_sourcing_job_order_line_import_wizard,sourcing.job.order.line.import.wizard,model_sourcing_job_order_line_import_wizard,base.group_user,1,1,1,1
access_sourcing_job_order_line_export_wizard,sourcing.job.order.line.export.wizard,model_sourcing_job_order_line_export_wizard,base.group_user,1,1,1,1
access_sourcing_job_order_line_template_wizard,sourcing.job.order.line.template.wizard,model_sourcing_job_order_line_template_wizard,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 838596c8..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>
diff --git a/indoteknik_custom/views/advance_payment_request.xml b/indoteknik_custom/views/advance_payment_request.xml
index 340e0caf..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"/>
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/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/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml
index 59e317d2..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">
@@ -153,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/sale_order.xml b/indoteknik_custom/views/sale_order.xml
index ac871ead..33cd51e4 100755
--- a/indoteknik_custom/views/sale_order.xml
+++ b/indoteknik_custom/views/sale_order.xml
@@ -391,7 +391,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/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml
index 9cd63e25..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>
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>