summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--indoteknik_api/controllers/api_v1/flash_sale.py2
-rw-r--r--indoteknik_api/models/product_pricelist.py12
-rw-r--r--indoteknik_custom/controllers/website.py7
-rw-r--r--indoteknik_custom/models/commision.py106
-rw-r--r--indoteknik_custom/models/manufacturing.py7
-rw-r--r--indoteknik_custom/models/mrp_production.py298
-rw-r--r--indoteknik_custom/models/product_pricelist.py1
-rwxr-xr-xindoteknik_custom/models/product_template.py18
-rwxr-xr-xindoteknik_custom/models/sale_order.py18
-rw-r--r--indoteknik_custom/models/solr/product_product.py5
-rw-r--r--indoteknik_custom/models/solr/product_template.py9
-rw-r--r--indoteknik_custom/models/stock_move.py15
-rw-r--r--indoteknik_custom/models/stock_picking.py13
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv2
-rw-r--r--indoteknik_custom/views/mrp_production.xml20
-rw-r--r--indoteknik_custom/views/product_pricelist.xml1
-rwxr-xr-xindoteknik_custom/views/product_template.xml15
-rw-r--r--indoteknik_custom/views/stock_picking.xml1
18 files changed, 490 insertions, 60 deletions
diff --git a/indoteknik_api/controllers/api_v1/flash_sale.py b/indoteknik_api/controllers/api_v1/flash_sale.py
index 6c4ad8c0..1038500c 100644
--- a/indoteknik_api/controllers/api_v1/flash_sale.py
+++ b/indoteknik_api/controllers/api_v1/flash_sale.py
@@ -14,7 +14,7 @@ class FlashSale(controller.Controller):
def _get_flash_sale_header(self, **kw):
try:
# base_url = request.env['ir.config_parameter'].get_param('web.base.url')
- active_flash_sale = request.env['product.pricelist'].get_is_show_program_flash_sale()
+ active_flash_sale = request.env['product.pricelist'].get_is_show_program_flash_sale(is_show_program=kw.get('is_show_program'))
data = []
for pricelist in active_flash_sale:
query = [
diff --git a/indoteknik_api/models/product_pricelist.py b/indoteknik_api/models/product_pricelist.py
index 6e88517c..e0debf38 100644
--- a/indoteknik_api/models/product_pricelist.py
+++ b/indoteknik_api/models/product_pricelist.py
@@ -95,18 +95,24 @@ class ProductPricelist(models.Model):
], limit=1, order='start_date asc')
return pricelist
- def get_is_show_program_flash_sale(self):
+ def get_is_show_program_flash_sale(self, is_show_program):
"""
Check whether have active flash sale in range of date
@return: returns pricelist: object
"""
+
+ if is_show_program == 'true':
+ is_show_program = True
+ else:
+ is_show_program = False
+
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
pricelist = self.search([
('is_flash_sale', '=', True),
- ('is_show_program', '=', True),
+ ('is_show_program', '=', is_show_program),
('start_date', '<=', current_time),
('end_date', '>=', current_time)
- ], order='start_date asc')
+ ], order='number asc')
return pricelist
def is_flash_sale_product(self, product_id: int):
diff --git a/indoteknik_custom/controllers/website.py b/indoteknik_custom/controllers/website.py
index 2e3df519..120dddad 100644
--- a/indoteknik_custom/controllers/website.py
+++ b/indoteknik_custom/controllers/website.py
@@ -1,7 +1,12 @@
-from odoo.http import request, Controller
+from odoo.http import request, Controller, route
from odoo import http, _
class Website(Controller):
+
+ @route(['/shop', '/shop/cart'], auth='public', website=True)
+ def shop(self, **kw):
+ return request.redirect('https://indoteknik.com/shop/promo', code=302)
+
@http.route('/content', auth='public')
def content(self, **kw):
url = kw.get('url', '')
diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py
index 6d832b85..32e81b9a 100644
--- a/indoteknik_custom/models/commision.py
+++ b/indoteknik_custom/models/commision.py
@@ -13,8 +13,10 @@ class CustomerRebate(models.Model):
_inherit = ['mail.thread']
partner_id = fields.Many2one('res.partner', string='Customer', required=True)
- date_from = fields.Date(string='Date From', required=True, help="Pastikan tanggal 1 januari, jika tidak, code akan break")
- date_to = fields.Date(string='Date To', required=True, help="Pastikan tanggal 31 desember, jika tidak, code akan break")
+ date_from = fields.Date(string='Date From', required=True,
+ help="Pastikan tanggal 1 januari, jika tidak, code akan break")
+ date_to = fields.Date(string='Date To', required=True,
+ help="Pastikan tanggal 31 desember, jika tidak, code akan break")
description = fields.Char(string='Description')
target_1st = fields.Float(string='Target/Quarter 1st')
target_2nd = fields.Float(string='Target/Quarter 2nd')
@@ -36,7 +38,7 @@ class CustomerRebate(models.Model):
line.dpp_q2 = line._get_current_dpp_q2(line)
line.dpp_q3 = line._get_current_dpp_q3(line)
line.dpp_q4 = line._get_current_dpp_q4(line)
-
+
def _compute_achievement(self):
for line in self:
line.status_q1 = line._check_achievement(line.target_1st, line.target_2nd, line.dpp_q1)
@@ -53,18 +55,18 @@ class CustomerRebate(models.Model):
else:
status = 'not achieve'
return status
-
+
def _get_current_dpp_q1(self, line):
sum_dpp = 0
brand = [10, 89, 122]
where = [
- ('move_id.move_type', '=', 'out_invoice'),
- ('move_id.state', '=', 'posted'),
- ('move_id.is_customer_commision', '=', False),
- ('move_id.partner_id.id', '=', line.partner_id.id),
- ('move_id.invoice_date', '>=', line.date_from),
- ('move_id.invoice_date', '<=', '2023-03-31'),
- ('product_id.x_manufacture', 'in', brand),
+ ('move_id.move_type', '=', 'out_invoice'),
+ ('move_id.state', '=', 'posted'),
+ ('move_id.is_customer_commision', '=', False),
+ ('move_id.partner_id.id', '=', line.partner_id.id),
+ ('move_id.invoice_date', '>=', line.date_from),
+ ('move_id.invoice_date', '<=', '2023-03-31'),
+ ('product_id.x_manufacture', 'in', brand),
]
invoice_lines = self.env['account.move.line'].search(where, order='id')
for invoice_line in invoice_lines:
@@ -75,13 +77,13 @@ class CustomerRebate(models.Model):
sum_dpp = 0
brand = [10, 89, 122]
where = [
- ('move_id.move_type', '=', 'out_invoice'),
- ('move_id.state', '=', 'posted'),
- ('move_id.is_customer_commision', '=', False),
- ('move_id.partner_id.id', '=', line.partner_id.id),
- ('move_id.invoice_date', '>=', '2023-04-01'),
- ('move_id.invoice_date', '<=', '2023-06-30'),
- ('product_id.x_manufacture', 'in', brand),
+ ('move_id.move_type', '=', 'out_invoice'),
+ ('move_id.state', '=', 'posted'),
+ ('move_id.is_customer_commision', '=', False),
+ ('move_id.partner_id.id', '=', line.partner_id.id),
+ ('move_id.invoice_date', '>=', '2023-04-01'),
+ ('move_id.invoice_date', '<=', '2023-06-30'),
+ ('product_id.x_manufacture', 'in', brand),
]
invoices = self.env['account.move.line'].search(where, order='id')
for invoice in invoices:
@@ -92,13 +94,13 @@ class CustomerRebate(models.Model):
sum_dpp = 0
brand = [10, 89, 122]
where = [
- ('move_id.move_type', '=', 'out_invoice'),
- ('move_id.state', '=', 'posted'),
- ('move_id.is_customer_commision', '=', False),
- ('move_id.partner_id.id', '=', line.partner_id.id),
- ('move_id.invoice_date', '>=', '2023-07-01'),
- ('move_id.invoice_date', '<=', '2023-09-30'),
- ('product_id.x_manufacture', 'in', brand),
+ ('move_id.move_type', '=', 'out_invoice'),
+ ('move_id.state', '=', 'posted'),
+ ('move_id.is_customer_commision', '=', False),
+ ('move_id.partner_id.id', '=', line.partner_id.id),
+ ('move_id.invoice_date', '>=', '2023-07-01'),
+ ('move_id.invoice_date', '<=', '2023-09-30'),
+ ('product_id.x_manufacture', 'in', brand),
]
invoices = self.env['account.move.line'].search(where, order='id')
for invoice in invoices:
@@ -109,19 +111,20 @@ class CustomerRebate(models.Model):
sum_dpp = 0
brand = [10, 89, 122]
where = [
- ('move_id.move_type', '=', 'out_invoice'),
- ('move_id.state', '=', 'posted'),
- ('move_id.is_customer_commision', '=', False),
- ('move_id.partner_id.id', '=', line.partner_id.id),
- ('move_id.invoice_date', '>=', '2023-10-01'),
- ('move_id.invoice_date', '<=', line.date_to),
- ('product_id.x_manufacture', 'in', brand),
+ ('move_id.move_type', '=', 'out_invoice'),
+ ('move_id.state', '=', 'posted'),
+ ('move_id.is_customer_commision', '=', False),
+ ('move_id.partner_id.id', '=', line.partner_id.id),
+ ('move_id.invoice_date', '>=', '2023-10-01'),
+ ('move_id.invoice_date', '<=', line.date_to),
+ ('product_id.x_manufacture', 'in', brand),
]
invoices = self.env['account.move.line'].search(where, order='id')
for invoice in invoices:
sum_dpp += invoice.price_subtotal
return sum_dpp
+
class RejectReasonCommision(models.TransientModel):
_name = 'reject.reason.commision'
_description = 'Wizard for Reject Reason Customer Commision'
@@ -150,16 +153,19 @@ class CustomerCommision(models.Model):
partner_ids = fields.Many2many('res.partner', String='Customer', required=True)
description = fields.Char(string='Description')
notification = fields.Char(string='Notification')
- commision_lines = fields.One2many('customer.commision.line', 'customer_commision_id', string='Lines', auto_join=True)
+ commision_lines = fields.One2many('customer.commision.line', 'customer_commision_id', string='Lines',
+ auto_join=True)
status = fields.Selection([
+ ('draft', 'Draft'),
('pengajuan1', 'Menunggu Approval Manager Sales'),
('pengajuan2', 'Menunggu Approval Marketing'),
('pengajuan3', 'Menunggu Approval Pimpinan'),
('pengajuan4', 'Menunggu Approval Accounting'),
('approved', 'Approved'),
('reject', 'Rejected'),
- ], string='Status', copy=False, readonly=True, tracking=3, index=True, track_visibility='onchange',default='draft')
+ ], string='Status', copy=False, readonly=True, tracking=3, index=True, track_visibility='onchange', default='draft')
last_status = fields.Selection([
+ ('draft', 'Draft'),
('pengajuan1', 'Menunggu Approval Manager Sales'),
('pengajuan2', 'Menunggu Approval Marketing'),
('pengajuan3', 'Menunggu Approval Pimpinan'),
@@ -192,7 +198,8 @@ class CustomerCommision(models.Model):
grouped_so_number = fields.Char(string='Group SO Number', compute='_compute_grouped_numbers')
grouped_invoice_number = fields.Char(string='Group Invoice Number', compute='_compute_grouped_numbers')
- sales_id = fields.Many2one('res.users', string="Sales", tracking=True)
+ sales_id = fields.Many2one('res.users', string="Sales", tracking=True, default=lambda self: self.env.user,
+ domain=lambda self: [('groups_id', 'in', self.env.ref('sales_team.group_sale_salesman').id)])
date_approved_sales = fields.Datetime(string="Date Approved Sales", tracking=True)
date_approved_marketing = fields.Datetime(string="Date Approved Marketing", tracking=True)
@@ -209,7 +216,7 @@ class CustomerCommision(models.Model):
for record in self:
res = ''
-
+
try:
if record.commision_amt > 0:
tb.parse(int(record.commision_amt))
@@ -222,15 +229,16 @@ class CustomerCommision(models.Model):
for rec in self:
so_numbers = set()
invoice_numbers = set()
-
+
for line in rec.commision_lines:
- if line.invoice_id:
+ if line.invoice_id:
if line.invoice_id.sale_id:
so_numbers.add(line.invoice_id.sale_id.name)
invoice_numbers.add(line.invoice_id.name)
-
+
rec.grouped_so_number = ', '.join(sorted(so_numbers))
rec.grouped_invoice_number = ', '.join(sorted(invoice_numbers))
+
# add status for type of commision, fee, rebate / cashback
# include child or not?
@@ -255,22 +263,22 @@ class CustomerCommision(models.Model):
self.commision_percent = achieve_2nd
else:
self.commision_percent = 0
-
+
self._onchange_commision_amt()
@api.constrains('commision_percent')
def _onchange_commision_percent(self):
if not self.env.context.get('_onchange_commision_percent', True):
return
-
+
if self.commision_amt == 0:
self.commision_amt = self.commision_percent * self.total_dpp // 100
-
+
@api.constrains('commision_amt')
def _onchange_commision_amt(self):
if not self.env.context.get('_onchange_commision_amt', True):
return
-
+
if self.total_dpp > 0 and self.commision_percent == 0:
self.commision_percent = (self.commision_amt / self.total_dpp) * 100
@@ -322,7 +330,7 @@ class CustomerCommision(models.Model):
raise UserError('Harus di approved oleh yang bersangkutan')
return
- def action_reject(self):#add 2 step approval
+ def action_reject(self): # add 2 step approval
return {
'type': 'ir.actions.act_window',
'name': _('Reject Reason'),
@@ -353,7 +361,7 @@ class CustomerCommision(models.Model):
def generate_customer_commision(self):
if self.commision_lines:
raise UserError('Line sudah ada, tidak bisa di generate ulang')
-
+
if self.commision_type == 'fee':
self._generate_customer_commision_fee()
else:
@@ -428,11 +436,13 @@ class CustomerCommision(models.Model):
}])
return
+
class CustomerCommisionLine(models.Model):
_name = 'customer.commision.line'
_order = 'id'
- customer_commision_id = fields.Many2one('customer.commision', string='Ref', required=True, ondelete='cascade', copy=False)
+ customer_commision_id = fields.Many2one('customer.commision', string='Ref', required=True, ondelete='cascade',
+ copy=False)
invoice_id = fields.Many2one('account.move', string='Invoice')
partner_id = fields.Many2one('res.partner', string='Customer')
state = fields.Char(string='InvStatus')
@@ -440,10 +450,12 @@ class CustomerCommisionLine(models.Model):
tax = fields.Float(string='TaxAmt')
total = fields.Float(string='Total')
total_percent_margin = fields.Float('Total Margin', related='invoice_id.sale_id.total_percent_margin')
- total_margin_excl_third_party = fields.Float('Before Margin', related='invoice_id.sale_id.total_margin_excl_third_party')
+ total_margin_excl_third_party = fields.Float('Before Margin',
+ related='invoice_id.sale_id.total_margin_excl_third_party')
product_id = fields.Many2one('product.product', string='Product')
sale_order_id = fields.Many2one('sale.order', string='Sale Order', related='invoice_id.sale_id')
+
class AccountMove(models.Model):
_inherit = 'account.move'
is_customer_commision = fields.Boolean(string='Customer Commision Used')
diff --git a/indoteknik_custom/models/manufacturing.py b/indoteknik_custom/models/manufacturing.py
index 24a8b8c3..715d8513 100644
--- a/indoteknik_custom/models/manufacturing.py
+++ b/indoteknik_custom/models/manufacturing.py
@@ -26,6 +26,13 @@ class Manufacturing(models.Model):
# Check product category
if self.product_id.categ_id.name != 'Finish Good':
raise UserError('Tidak bisa di complete karna product category bukan Unit / Finish Good')
+
+ if self.sale_order and self.sale_order.state != 'sale':
+ raise UserError(
+ ('Tidak bisa Mark as Done.\nSales Order "%s" (Nomor: %s) belum dikonfirmasi.')
+ % (self.sale_order.partner_id.name, self.sale_order.name)
+ )
+
for line in self.move_raw_ids:
# if line.quantity_done > 0 and line.quantity_done != self.product_uom_qty:
# raise UserError('Qty Consume per Line tidak sama dengan Qty to Produce')
diff --git a/indoteknik_custom/models/mrp_production.py b/indoteknik_custom/models/mrp_production.py
index d80df2ce..8179fe56 100644
--- a/indoteknik_custom/models/mrp_production.py
+++ b/indoteknik_custom/models/mrp_production.py
@@ -8,10 +8,43 @@ from odoo.exceptions import AccessError, UserError, ValidationError
class MrpProduction(models.Model):
_inherit = 'mrp.production'
+ check_bom_product_lines = fields.One2many('check.bom.product', 'production_id', string='Check Product', auto_join=True, copy=False)
desc = fields.Text(string='Description')
- sale_order = fields.Many2one('sale.order', string='Sale Order', required=True, copy=False)
+ sale_order = fields.Many2one('sale.order', string='Sale Order', copy=False)
production_purchase_match = fields.One2many('production.purchase.match', 'production_id', string='Purchase Matches', auto_join=True)
is_po = fields.Boolean(string='Is PO')
+ state_reserve = fields.Selection([
+ ('waiting', 'Waiting For Fullfilment'),
+ ('ready', 'Ready to Ship'),
+ ('done', 'Done'),
+ ('cancel', 'Cancelled'),
+ ], string='Status Reserve', tracking=True, copy=False, help="The current state of the stock picking.")
+ date_reserved = fields.Datetime(string="Date Reserved", help='Tanggal ter-reserved semua barang nya', copy=False)
+
+
+ @api.constrains('check_bom_product_lines')
+ def constrains_check_bom_product_lines(self):
+ for rec in self:
+ if len(rec.check_bom_product_lines) > 0:
+ rec.qty_producing = rec.product_qty
+
+ def button_mark_done(self):
+ """Override button_mark_done untuk mengirim pesan ke Sale Order jika state berubah menjadi 'confirmed'."""
+ if self._name != 'mrp.production':
+ return super(MrpProduction, self).button_mark_done()
+
+ result = super(MrpProduction, self).button_mark_done()
+
+ for record in self:
+ if len(record.check_bom_product_lines) < 1:
+ raise UserError("Check Product Tidak Boleh Kosong")
+ if not record.sale_order:
+ raise UserError("Sale Order Tidak Boleh Kosong")
+ if record.sale_order and record.state == 'confirmed':
+ message = _("Manufacturing order telah dibuat dengan nomor %s") % (record.name)
+ record.sale_order.message_post(body=message)
+
+ return result
def action_confirm(self):
"""Override action_confirm untuk mengirim pesan ke Sale Order jika state berubah menjadi 'confirmed'."""
@@ -21,6 +54,8 @@ class MrpProduction(models.Model):
result = super(MrpProduction, self).action_confirm()
for record in self:
+ # if len(record.check_bom_product_lines) < 1:
+ # raise UserError("Check Product Tidak Boleh Kosong")
if record.sale_order and record.state == 'confirmed':
message = _("Manufacturing order telah dibuat dengan nomor %s") % (record.name)
record.sale_order.message_post(body=message)
@@ -171,6 +206,267 @@ class MrpProduction(models.Model):
return price, taxes, vendor_id
+class CheckBomProduct(models.Model):
+ _name = 'check.bom.product'
+ _description = 'Check Product'
+ _order = 'production_id, id'
+
+ production_id = fields.Many2one(
+ 'mrp.production',
+ string='Bom Reference',
+ required=True,
+ ondelete='cascade',
+ index=True,
+ copy=False,
+ )
+ product_id = fields.Many2one('product.product', string='Product')
+ quantity = fields.Float(string='Quantity')
+ status = fields.Char(string='Status', compute='_compute_status')
+ code_product = fields.Char(string='Code Product')
+
+ @api.constrains('production_id')
+ def _check_missing_components(self):
+ for mo in self:
+ required = mo.production_id.move_raw_ids.mapped('product_id')
+ entered = mo.production_id.check_bom_product_lines.mapped('product_id')
+ missing = required - entered
+
+ # Jika HTML tidak bekerja sama sekali, gunakan format text biasa yang rapi
+ if missing:
+ product_list = "\n- " + "\n- ".join(p.display_name for p in missing)
+ raise UserError(
+ "⚠️ Komponen Wajib Diisi\n\n"
+ "Produk berikut harus ditambahkan:\n"
+ f"{product_list}\n\n"
+ "Silakan lengkapi terlebih dahulu."
+ )
+
+ @api.constrains('production_id', 'product_id')
+ def _check_product_bom_validation(self):
+ for record in self:
+ if not record.production_id or not record.product_id:
+ continue
+
+ moves = record.production_id.move_raw_ids.filtered(
+ lambda move: move.product_id.id == record.product_id.id
+ )
+
+ if not moves:
+ raise UserError((
+ "The product '%s' tidak ada di operations. "
+ ) % record.product_id.display_name)
+
+ total_qty_in_moves = sum(moves.mapped('product_uom_qty'))
+
+ # Find existing lines for the same product, excluding the current line
+ existing_lines = record.production_id.check_bom_product_lines.filtered(
+ lambda line: line.product_id == record.product_id
+ )
+
+ if existing_lines:
+ total_quantity = sum(existing_lines.mapped('quantity'))
+
+ if total_quantity < total_qty_in_moves:
+ raise UserError((
+ "Quantity Product '%s' kurang dari quantity demand."
+ ) % (record.product_id.display_name))
+ else:
+ # Check if the quantity exceeds the allowed total
+ if record.quantity < total_qty_in_moves:
+ raise UserError((
+ "Quantity Product '%s' kurang dari quantity demand."
+ ) % (record.product_id.display_name))
+
+ # Set the quantity to the entered value
+ record.quantity = record.quantity
+
+ @api.onchange('code_product')
+ def _onchange_code_product(self):
+ if not self.code_product:
+ return
+
+ # Cari product berdasarkan default_code, barcode, atau barcode_box
+ product = self.env['product.product'].search([
+ '|',
+ ('default_code', '=', self.code_product),
+ '|',
+ ('barcode', '=', self.code_product),
+ ('barcode_box', '=', self.code_product)
+ ], limit=1)
+
+ if not product:
+ raise UserError("Product tidak ditemukan")
+
+ # Jika scan barcode_box, set quantity sesuai qty_pcs_box
+ if product.barcode_box == self.code_product:
+ self.product_id = product.id
+ self.quantity = product.qty_pcs_box
+ self.code_product = product.default_code or product.barcode
+ # return {
+ # 'warning': {
+ # 'title': 'Info',8994175025871
+
+ # 'message': f'Product box terdeteksi. Quantity di-set ke {product.qty_pcs_box}'
+ # }
+ # }
+ else:
+ # Jika scan biasa
+ self.product_id = product.id
+ self.code_product = product.default_code or product.barcode
+ self.quantity = 1
+
+ def unlink(self):
+ # Get all affected pickings before deletion
+ productions = self.mapped('production_id')
+
+ # Store product_ids that will be deleted
+ deleted_product_ids = self.mapped('product_id')
+
+ # Perform the deletion
+ result = super(CheckBomProduct, self).unlink()
+
+ # After deletion, update moves for affected pickings
+ for production in productions:
+ # For products that were completely removed (no remaining check.bom.product lines)
+ remaining_product_ids = production.check_bom_product_lines.mapped('product_id')
+ removed_product_ids = deleted_product_ids - remaining_product_ids
+
+ # Set quantity_done to 0 for moves of completely removed products
+ moves_to_reset = production.move_raw_ids.filtered(
+ lambda move: move.product_id in removed_product_ids
+ )
+ for move in moves_to_reset:
+ move.quantity_done = 0.0
+
+ production.qty_producing = 0
+
+ # Also sync remaining products in case their totals changed
+ self._sync_check_product_to_moves(production)
+
+ return result
+
+ @api.depends('quantity')
+ def _compute_status(self):
+ for record in self:
+ moves = record.production_id.move_raw_ids.filtered(
+ lambda move: move.product_id.id == record.product_id.id
+ )
+ total_qty_in_moves = sum(moves.mapped('product_uom_qty'))
+
+ if record.quantity < total_qty_in_moves:
+ record.status = 'Pending'
+ else:
+ record.status = 'Done'
+
+
+ def create(self, vals):
+ # Create the record
+ record = super(CheckBomProduct, self).create(vals)
+ # Ensure uniqueness after creation
+ if not self.env.context.get('skip_consolidate'):
+ record.with_context(skip_consolidate=True)._consolidate_duplicate_lines()
+ return record
+
+ def write(self, vals):
+ # Write changes to the record
+ result = super(CheckBomProduct, self).write(vals)
+ # Ensure uniqueness after writing
+ if not self.env.context.get('skip_consolidate'):
+ self.with_context(skip_consolidate=True)._consolidate_duplicate_lines()
+ return result
+
+ def _sync_check_product_to_moves(self, production):
+ """
+ Sinkronisasi quantity_done di move_raw_ids
+ dengan total quantity dari check.bom.product berdasarkan product_id.
+ """
+ for product_id in production.check_bom_product_lines.mapped('product_id'):
+ # Totalkan quantity dari semua baris check.bom.product untuk product_id ini
+ total_quantity = sum(
+ line.quantity for line in production.check_bom_product_lines.filtered(lambda line: line.product_id == product_id)
+ )
+ # Update quantity_done di move yang relevan
+ moves = production.move_raw_ids.filtered(lambda move: move.product_id == product_id)
+ for move in moves:
+ move.quantity_done = total_quantity
+
+ def _consolidate_duplicate_lines(self):
+ """
+ Consolidate duplicate lines with the same product_id under the same production_id
+ and sync the total quantity to related moves.
+ """
+ for production in self.mapped('production_id'):
+ lines_to_remove = self.env['check.bom.product'] # Recordset untuk menyimpan baris yang akan dihapus
+ product_lines = production.check_bom_product_lines.filtered(lambda line: line.product_id)
+
+ # Group lines by product_id
+ product_groups = {}
+ for line in product_lines:
+ product_groups.setdefault(line.product_id.id, []).append(line)
+
+ for product_id, lines in product_groups.items():
+ if len(lines) > 1:
+ # Consolidate duplicate lines
+ first_line = lines[0]
+ total_quantity = sum(line.quantity for line in lines)
+
+ # Update the first line's quantity
+ first_line.with_context(skip_consolidate=True).write({'quantity': total_quantity})
+
+ # Add the remaining lines to the lines_to_remove recordset
+ lines_to_remove |= self.env['check.bom.product'].browse([line.id for line in lines[1:]])
+
+ # Perform unlink after consolidation
+ if lines_to_remove:
+ lines_to_remove.unlink()
+
+ # Sync total quantities to moves
+ self._sync_check_product_to_moves(production)
+
+ @api.onchange('product_id', 'quantity')
+ def check_product_validity(self):
+ for record in self:
+ if not record.production_id or not record.product_id:
+ continue
+
+ # Filter moves related to the selected product
+ moves = record.production_id.move_raw_ids.filtered(
+ lambda move: move.product_id.id == record.product_id.id
+ )
+
+ if not moves:
+ raise UserError((
+ "The product '%s' tidak ada di operations. "
+ ) % record.product_id.display_name)
+
+ total_qty_in_moves = sum(moves.mapped('product_uom_qty'))
+
+ # Find existing lines for the same product, excluding the current line
+ existing_lines = record.production_id.check_bom_product_lines.filtered(
+ lambda line: line.product_id == record.product_id
+ )
+
+ if existing_lines:
+ # Get the first existing line
+ first_line = existing_lines[0]
+
+ # Calculate the total quantity after addition
+ total_quantity = sum(existing_lines.mapped('quantity'))
+
+ if total_quantity > total_qty_in_moves:
+ raise UserError((
+ "Quantity Product '%s' sudah melebihi quantity demand."
+ ) % (record.product_id.display_name))
+ else:
+ # Check if the quantity exceeds the allowed total
+ if record.quantity == total_qty_in_moves:
+ raise UserError((
+ "Quantity Product '%s' sudah melebihi quantity demand."
+ ) % (record.product_id.display_name))
+
+ # Set the quantity to the entered value
+ record.quantity = record.quantity
+
class ProductionPurchaseMatch(models.Model):
_name = 'production.purchase.match'
diff --git a/indoteknik_custom/models/product_pricelist.py b/indoteknik_custom/models/product_pricelist.py
index c299ff2f..ea3ee6cf 100644
--- a/indoteknik_custom/models/product_pricelist.py
+++ b/indoteknik_custom/models/product_pricelist.py
@@ -17,6 +17,7 @@ class ProductPricelist(models.Model):
], string='Flash Sale Option')
banner_top = fields.Binary(string='Banner Top')
flashsale_tag = fields.Char(string='Flash Sale Tag')
+ number = fields.Integer(string='Sequence')
def _check_end_date_and_update_solr(self):
today = datetime.utcnow().date()
diff --git a/indoteknik_custom/models/product_template.py b/indoteknik_custom/models/product_template.py
index e6a01a04..a09570f4 100755
--- a/indoteknik_custom/models/product_template.py
+++ b/indoteknik_custom/models/product_template.py
@@ -15,6 +15,14 @@ _logger = logging.getLogger(__name__)
class ProductTemplate(models.Model):
_inherit = "product.template"
+
+ image_carousel_lines = fields.One2many(
+ comodel_name="image.carousel",
+ inverse_name="product_id",
+ string="Image Carousel",
+ auto_join=True,
+ copy=False
+ )
x_studio_field_tGhJR = fields.Many2many('x_product_tags', string="Product Tags")
x_manufacture = fields.Many2one(
comodel_name="x_manufactures",
@@ -246,7 +254,7 @@ class ProductTemplate(models.Model):
# product.default_code = 'ITV.'+str(product.id)
# _logger.info('Updated Variant %s' % product.name)
- @api.onchange('name','default_code','x_manufacture','product_rating','website_description','image_1920','weight','public_categ_ids')
+ @api.onchange('name','default_code','x_manufacture','product_rating','website_description','image_1920','weight','public_categ_ids','image_carousel_lines')
def update_solr_flag(self):
for tmpl in self:
if tmpl.solr_flag == 1:
@@ -734,3 +742,11 @@ class OutstandingMove(models.Model):
'partially_available'
)
""" % self._table)
+
+class ImageCarousel(models.Model):
+ _name = 'image.carousel'
+ _description = 'Image Carousel'
+ _order = 'product_id, id'
+
+ product_id = fields.Many2one('product.template', string='Product', required=True, ondelete='cascade', index=True, copy=False)
+ image = fields.Binary(string='Image')
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index d2c49bf0..4c48684d 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -297,6 +297,16 @@ class SaleOrder(models.Model):
nomor_so_pengganti = fields.Char(string='Nomor SO Pengganti', copy=False, tracking=3)
shipping_option_id = fields.Many2one("shipping.option", string="Selected Shipping Option", domain="['|', ('sale_order_id', '=', False), ('sale_order_id', '=', id)]")
+ def _validate_uniform_taxes(self):
+ for order in self:
+ tax_sets = set()
+ for line in order.order_line:
+ tax_ids = tuple(sorted(line.tax_id.ids))
+ if tax_ids:
+ tax_sets.add(tax_ids)
+ if len(tax_sets) > 1:
+ raise ValidationError("Semua produk dalam Sales Order harus memiliki kombinasi pajak yang sama.")
+
@api.constrains('fee_third_party', 'delivery_amt', 'biaya_lain_lain')
def _check_total_margin_excl_third_party(self):
for rec in self:
@@ -1085,6 +1095,7 @@ class SaleOrder(models.Model):
self._validate_order()
for order in self:
+ order._validate_uniform_taxes()
order.order_line.validate_line()
term_days = 0
@@ -1137,6 +1148,7 @@ class SaleOrder(models.Model):
self._validate_order()
for order in self:
+ order._validate_uniform_taxes()
order.order_line.validate_line()
order.check_data_real_delivery_address()
order._validate_order()
@@ -1368,6 +1380,7 @@ class SaleOrder(models.Model):
def action_confirm(self):
for order in self:
+ order._validate_uniform_taxes()
order.check_duplicate_product()
order.check_product_bom()
order.check_credit_limit()
@@ -1501,10 +1514,11 @@ class SaleOrder(models.Model):
return False
def _requires_approval_margin_leader(self):
- return self.total_percent_margin < 15 and not self.env.user.is_leader
+ return self.total_percent_margin <= 15 and not self.env.user.is_leader
def _requires_approval_margin_manager(self):
- return self.total_percent_margin >= 15 and not self.env.user.is_leader and not self.env.user.is_sales_manager
+ return 15 < self.total_percent_margin <= 24 and not self.env.user.is_sales_manager and not self.env.user.is_leader
+ # return self.total_percent_margin >= 15 and not self.env.user.is_leader and not self.env.user.is_sales_manager
def _create_approval_notification(self, approval_role):
title = 'Warning'
diff --git a/indoteknik_custom/models/solr/product_product.py b/indoteknik_custom/models/solr/product_product.py
index 667511b2..d8bc3973 100644
--- a/indoteknik_custom/models/solr/product_product.py
+++ b/indoteknik_custom/models/solr/product_product.py
@@ -57,6 +57,8 @@ class ProductProduct(models.Model):
is_in_bu = True if variant.qty_free_bandengan > 0 else False
document = solr_model.get_doc('variants', variant.id)
+
+ carousel = [ir_attachment.api_image('image.carousel', 'image', carousel.product_id.id) for carousel in variant.product_tmpl_id.image_carousel_lines],
document.update({
'id': variant.id,
@@ -67,10 +69,11 @@ class ProductProduct(models.Model):
'product_id_i': variant.id,
'template_id_i': variant.product_tmpl_id.id,
'image_s': ir_attachment.api_image('product.template', 'image_512', variant.product_tmpl_id.id),
+ 'image_carousel_s': [ir_attachment.api_image('image.carousel', 'image', carousel.id) for carousel in variant.product_tmpl_id.image_carousel_lines],
'image_mobile_s': ir_attachment.api_image('product.template', 'image_256', variant.product_tmpl_id.id),
'stock_total_f': variant.qty_stock_vendor,
'weight_f': variant.weight,
- 'manufacture_id_i': variant.product_tmpl_id.x_manufacture.id or 0,
+ 'manufacture_id_i': variant.product_tmpl_id.x_manufacture.id or 0,
'manufacture_name_s': variant.product_tmpl_id.x_manufacture.x_name or '',
'manufacture_name': variant.product_tmpl_id.x_manufacture.x_name or '',
'image_promotion_1_s': ir_attachment.api_image('x_manufactures', 'image_promotion_1', variant.product_tmpl_id.x_manufacture.id),
diff --git a/indoteknik_custom/models/solr/product_template.py b/indoteknik_custom/models/solr/product_template.py
index 8afff6e3..20402c84 100644
--- a/indoteknik_custom/models/solr/product_template.py
+++ b/indoteknik_custom/models/solr/product_template.py
@@ -26,7 +26,7 @@ class ProductTemplate(models.Model):
'function_name': function_name
})
- @api.constrains('name', 'default_code', 'weight', 'x_manufacture', 'public_categ_ids', 'search_rank', 'search_rank_weekly', 'image_1920', 'unpublished')
+ @api.constrains('name', 'default_code', 'weight', 'x_manufacture', 'public_categ_ids', 'search_rank', 'search_rank_weekly', 'image_1920', 'unpublished','image_carousel_lines')
def _create_solr_queue_sync_product_template(self):
self._create_solr_queue('_sync_product_template_to_solr')
@@ -85,6 +85,12 @@ class ProductTemplate(models.Model):
cleaned_desc = BeautifulSoup(template.website_description or '', "html.parser").get_text()
website_description = template.website_description if cleaned_desc else ''
+ # carousel_images = ', '.join([self.env['ir.attachment'].api_image('image.carousel', 'image', carousel.id) for carousel in template.image_carousel_lines])
+ carousel_images = []
+ for carousel in template.image_carousel_lines:
+ image_url = self.env['ir.attachment'].api_image('image.carousel', 'image', carousel.id)
+ if image_url: # Hanya tambahkan jika URL valid
+ carousel_images.append(image_url)
document = solr_model.get_doc('product', template.id)
document.update({
"id": template.id,
@@ -94,6 +100,7 @@ class ProductTemplate(models.Model):
"product_rating_f": template.virtual_rating,
"product_id_i": template.id,
"image_s": self.env['ir.attachment'].api_image('product.template', 'image_512', template.id),
+ "image_carousel_ss": carousel_images,
'image_mobile_s': self.env['ir.attachment'].api_image('product.template', 'image_256', template.id),
"variant_total_i": template.product_variant_count,
"stock_total_f": template.qty_stock_vendor,
diff --git a/indoteknik_custom/models/stock_move.py b/indoteknik_custom/models/stock_move.py
index 514acad0..3c765244 100644
--- a/indoteknik_custom/models/stock_move.py
+++ b/indoteknik_custom/models/stock_move.py
@@ -15,6 +15,21 @@ class StockMove(models.Model):
barcode = fields.Char(string='Barcode', related='product_id.barcode')
vendor_id = fields.Many2one('res.partner' ,string='Vendor')
+ # @api.model_create_multi
+ # def create(self, vals_list):
+ # moves = super(StockMove, self).create(vals_list)
+
+ # for move in moves:
+ # if move.product_id and move.location_id.id == 58 and move.location_dest_id.id == 57 and move.picking_type_id.id == 75:
+ # po_line = self.env['purchase.order.line'].search([
+ # ('product_id', '=', move.product_id.id),
+ # ('order_id.name', '=', move.origin)
+ # ], limit=1)
+ # if po_line:
+ # move.write({'purchase_line_id': po_line.id})
+
+ # return moves
+
@api.constrains('product_id')
def constrains_product_to_fill_vendor(self):
for rec in self:
diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py
index 64dc1499..1291737e 100644
--- a/indoteknik_custom/models/stock_picking.py
+++ b/indoteknik_custom/models/stock_picking.py
@@ -157,6 +157,15 @@ class StockPicking(models.Model):
so_lama = fields.Boolean('SO LAMA', copy=False)
linked_manual_bu_out = fields.Many2one('stock.picking', string='BU Out', copy=False)
+ area_name = fields.Char(string="Area", compute="_compute_area_name")
+ @api.depends('real_shipping_id.kecamatan_id', 'real_shipping_id.kota_id')
+ def _compute_area_name(self):
+ for record in self:
+ district = record.real_shipping_id.kecamatan_id.name or ''
+ city = record.real_shipping_id.kota_id.name or ''
+ record.area_name = f"{district}, {city}".strip(', ')
+
+
# def write(self, vals):
# if 'linked_manual_bu_out' in vals:
# for record in self:
@@ -1187,12 +1196,12 @@ class StockPicking(models.Model):
if picking.picking_type_code != 'outgoing' or 'BU/OUT/' not in picking.name or picking.partner_id.id == 96868:
continue
- invoice = self.env['account.move'].search([('sale_id', '=', picking.sale_id.id)], limit=1)
+ invoice = self.env['account.move'].search([('sale_id', '=', picking.sale_id.id), ('state','not in',['draft','cancel'])], limit=1)
if not invoice:
continue
- if not picking.date_doc_kirim or not invoice.invoice_date and not picking.so_lama:
+ if not picking.so_lama and (not picking.date_doc_kirim or not invoice.invoice_date):
raise UserError("Tanggal Kirim atau Tanggal Invoice belum diisi!")
picking_date = fields.Date.to_date(picking.date_doc_kirim)
diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv
index 30088680..7d7c98f4 100755
--- a/indoteknik_custom/security/ir.model.access.csv
+++ b/indoteknik_custom/security/ir.model.access.csv
@@ -154,6 +154,7 @@ access_va_multi_approve,access.va.multi.approve,model_va_multi_approve,,1,1,1,1
access_va_multi_reject,access.va.multi.reject,model_va_multi_reject,,1,1,1,1
access_vendor_sla,access.vendor_sla,model_vendor_sla,,1,1,1,1
access_check_product,access.check.product,model_check_product,,1,1,1,1
+access_check_bom_product,access.check.bom.product,model_check_bom_product,,1,1,1,1
access_check_koli,access.check.koli,model_check_koli,,1,1,1,1
access_scan_koli,access.scan.koli,model_scan_koli,,1,1,1,1
access_konfirm_koli,access.konfirm.koli,model_konfirm_koli,,1,1,1,1
@@ -178,3 +179,4 @@ access_cancel_reason_order,cancel.reason.order,model_cancel_reason_order,,1,1,1,
access_reject_reason_commision,reject.reason.commision,model_reject_reason_commision,,1,1,1,0
access_shipping_option,shipping.option,model_shipping_option,,1,1,1,1
access_production_purchase_match,access.production.purchase.match,model_production_purchase_match,,1,1,1,1
+access_image_carousel,access.image.carousel,model_image_carousel,,1,1,1,1
diff --git a/indoteknik_custom/views/mrp_production.xml b/indoteknik_custom/views/mrp_production.xml
index f8278f39..3de52a08 100644
--- a/indoteknik_custom/views/mrp_production.xml
+++ b/indoteknik_custom/views/mrp_production.xml
@@ -20,10 +20,26 @@
<page string="Purchase Match" name="purchase_order_lines_indent">
<field name="production_purchase_match"/>
</page>
+ <page string="Check Product" name="check_bom_product">
+ <field name="check_bom_product_lines"/>
+ </page>
</xpath>
</field>
</record>
+ <record id="check_bom_product_tree" model="ir.ui.view">
+ <field name="name">check.bom.product.tree</field>
+ <field name="model">check.bom.product</field>
+ <field name="arch" type="xml">
+ <tree editable="bottom" decoration-warning="status == 'Pending'" decoration-success="status == 'Done'">
+ <field name="code_product"/>
+ <field name="product_id"/>
+ <field name="quantity"/>
+ <field name="status" readonly="1"/>
+ </tree>
+ </field>
+ </record>
+
<record id="production_mrp_tree_view_inherited" model="ir.ui.view">
<field name="name">mrp.production.view.inherited</field>
<field name="model">mrp.production</field>
@@ -33,6 +49,10 @@
<field name="desc"/>
<field name="sale_order"/>
</field>
+ <field name="state" position="after">
+ <field name="state_reserve" optional="hide"/>
+ <field name="date_reserved" optional="hide"/>
+ </field>
</field>
</record>
diff --git a/indoteknik_custom/views/product_pricelist.xml b/indoteknik_custom/views/product_pricelist.xml
index 6eff0153..3c2b8b8d 100644
--- a/indoteknik_custom/views/product_pricelist.xml
+++ b/indoteknik_custom/views/product_pricelist.xml
@@ -20,6 +20,7 @@
<field name="banner_top" widget="image" />
<field name="start_date" attrs="{'required': [('is_flash_sale', '=', True)]}" />
<field name="end_date" attrs="{'required': [('is_flash_sale', '=', True)]}" />
+ <field name="number" required="1" />
</group>
</group>
</page>
diff --git a/indoteknik_custom/views/product_template.xml b/indoteknik_custom/views/product_template.xml
index 076a8082..8f9d1190 100755
--- a/indoteknik_custom/views/product_template.xml
+++ b/indoteknik_custom/views/product_template.xml
@@ -53,6 +53,21 @@
<field name="supplier_taxes_id" position="after">
<field name="supplier_url" widget="url"/>
</field>
+ <notebook position="inside">
+ <page string="Image Carousel">
+ <field name="image_carousel_lines"/>
+ </page>
+ </notebook>
+ </field>
+ </record>
+
+ <record id="image_carousel_tree" model="ir.ui.view">
+ <field name="name">image.carousel.tree</field>
+ <field name="model">image.carousel</field>
+ <field name="arch" type="xml">
+ <tree editable="bottom">
+ <field name="image" widget="image" width="80"/>
+ </tree>
</field>
</record>
diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml
index c916f2ef..b45debd0 100644
--- a/indoteknik_custom/views/stock_picking.xml
+++ b/indoteknik_custom/views/stock_picking.xml
@@ -26,6 +26,7 @@
<field name="countdown_ready_to_ship" /> -->
</field>
<field name="partner_id" position="after">
+ <field name="area_name" optional="hide"/>
<field name="purchase_representative_id"/>
<field name="status_printed"/>
</field>