summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAzka Nathan <darizkyfaz@gmail.com>2025-04-29 10:00:15 +0700
committerAzka Nathan <darizkyfaz@gmail.com>2025-04-29 10:00:15 +0700
commit783b674e04dd123a5233fd01896925c73aa8143c (patch)
treea0b5dea1b38eb0141180396d621a76381a337f38
parentbac1744ce4e27d796fd2b52f5fbcd3d5cdabdc75 (diff)
check product on bom, view stock picking po and fix bug api flashsale header
-rw-r--r--indoteknik_api/controllers/api_v1/flash_sale.py2
-rw-r--r--indoteknik_api/models/product_pricelist.py9
-rw-r--r--indoteknik_custom/models/mrp_production.py284
-rw-r--r--indoteknik_custom/models/stock_move.py15
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv1
-rw-r--r--indoteknik_custom/views/mrp_production.xml16
6 files changed, 324 insertions, 3 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..2e825740 100644
--- a/indoteknik_api/models/product_pricelist.py
+++ b/indoteknik_api/models/product_pricelist.py
@@ -95,15 +95,20 @@ 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')
diff --git a/indoteknik_custom/models/mrp_production.py b/indoteknik_custom/models/mrp_production.py
index d80df2ce..ebbd1c24 100644
--- a/indoteknik_custom/models/mrp_production.py
+++ b/indoteknik_custom/models/mrp_production.py
@@ -8,11 +8,34 @@ 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)
production_purchase_match = fields.One2many('production.purchase.match', 'production_id', string='Purchase Matches', auto_join=True)
is_po = fields.Boolean(string='Is PO')
+ @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 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'."""
if self._name != 'mrp.production':
@@ -21,6 +44,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 +196,265 @@ 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
+
+ # 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/stock_move.py b/indoteknik_custom/models/stock_move.py
index 514acad0..e75c75f0 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/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv
index 30088680..24b8dfff 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
diff --git a/indoteknik_custom/views/mrp_production.xml b/indoteknik_custom/views/mrp_production.xml
index f8278f39..419737f9 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>