diff options
| -rw-r--r-- | indoteknik_api/controllers/api_v1/flash_sale.py | 2 | ||||
| -rw-r--r-- | indoteknik_api/models/product_pricelist.py | 12 | ||||
| -rw-r--r-- | indoteknik_custom/controllers/website.py | 7 | ||||
| -rw-r--r-- | indoteknik_custom/models/commision.py | 106 | ||||
| -rw-r--r-- | indoteknik_custom/models/manufacturing.py | 7 | ||||
| -rw-r--r-- | indoteknik_custom/models/mrp_production.py | 298 | ||||
| -rw-r--r-- | indoteknik_custom/models/product_pricelist.py | 1 | ||||
| -rwxr-xr-x | indoteknik_custom/models/product_template.py | 18 | ||||
| -rwxr-xr-x | indoteknik_custom/models/sale_order.py | 18 | ||||
| -rw-r--r-- | indoteknik_custom/models/solr/product_product.py | 5 | ||||
| -rw-r--r-- | indoteknik_custom/models/solr/product_template.py | 9 | ||||
| -rw-r--r-- | indoteknik_custom/models/stock_move.py | 15 | ||||
| -rw-r--r-- | indoteknik_custom/models/stock_picking.py | 13 | ||||
| -rwxr-xr-x | indoteknik_custom/security/ir.model.access.csv | 2 | ||||
| -rw-r--r-- | indoteknik_custom/views/mrp_production.xml | 20 | ||||
| -rw-r--r-- | indoteknik_custom/views/product_pricelist.xml | 1 | ||||
| -rwxr-xr-x | indoteknik_custom/views/product_template.xml | 15 | ||||
| -rw-r--r-- | indoteknik_custom/views/stock_picking.xml | 1 |
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> |
