diff options
| author | Azka Nathan <darizkyfaz@gmail.com> | 2025-04-29 10:00:15 +0700 |
|---|---|---|
| committer | Azka Nathan <darizkyfaz@gmail.com> | 2025-04-29 10:00:15 +0700 |
| commit | 783b674e04dd123a5233fd01896925c73aa8143c (patch) | |
| tree | a0b5dea1b38eb0141180396d621a76381a337f38 | |
| parent | bac1744ce4e27d796fd2b52f5fbcd3d5cdabdc75 (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.py | 2 | ||||
| -rw-r--r-- | indoteknik_api/models/product_pricelist.py | 9 | ||||
| -rw-r--r-- | indoteknik_custom/models/mrp_production.py | 284 | ||||
| -rw-r--r-- | indoteknik_custom/models/stock_move.py | 15 | ||||
| -rwxr-xr-x | indoteknik_custom/security/ir.model.access.csv | 1 | ||||
| -rw-r--r-- | indoteknik_custom/views/mrp_production.xml | 16 |
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> |
