diff options
| author | it-fixcomart <it@fixcomart.co.id> | 2025-05-14 09:23:14 +0700 |
|---|---|---|
| committer | it-fixcomart <it@fixcomart.co.id> | 2025-05-14 09:23:14 +0700 |
| commit | f120c760c6a837681ebed26d9eea33a8961cd1aa (patch) | |
| tree | 728ecdd5ffd1530d97d58a9f18b850186b39aa67 | |
| parent | a571531bd8626f9bee25e89c62bbd9268ed30597 (diff) | |
| parent | 2469ee37cfe854f0419a8c3fbabed5bc32bcaa6e (diff) | |
Merge branch 'odoo-backup' into CR/form-merchant
# Conflicts:
# indoteknik_custom/models/__init__.py
# indoteknik_custom/security/ir.model.access.csv
101 files changed, 6161 insertions, 1317 deletions
diff --git a/indoteknik_api/controllers/api_v1/banner.py b/indoteknik_api/controllers/api_v1/banner.py index 308d2765..64a6167b 100644 --- a/indoteknik_api/controllers/api_v1/banner.py +++ b/indoteknik_api/controllers/api_v1/banner.py @@ -15,7 +15,8 @@ class Banner(controller.Controller): limit = int(kw.get('limit', 0)) offset = int(kw.get('offset', 0)) order = kw.get('order', 'write_date DESC') - + keyword = kw.get('keyword') + query = [('x_status_banner', '=', 'tayang')] if type: query += [('x_banner_category.x_studio_field_KKVl4', '=', type)] @@ -25,9 +26,27 @@ class Banner(controller.Controller): if manufacture_id: query += [('x_relasi_manufacture', '=', int(manufacture_id))] - - banners = request.env['x_banner.banner'].search(query, limit=limit, offset=offset, order=order) - + + banner_kumpulan = [] + banner_ids = set() # Set untuk menyimpan ID banner agar tidak duplikat + + if keyword: + keyword_list = [word.strip() for word in keyword.split() if word.strip()] # Pisahkan berdasarkan spasi + + for word in keyword_list: + keyword_query = query + [('x_keyword_banner', 'ilike', word)] # Buat query baru dengan keyword + banners = request.env['x_banner.banner'].search(keyword_query, limit=limit, offset=offset, order=order) + + for banner in banners: + if banner.id not in banner_ids: # Pastikan tidak ada duplikasi + banner_kumpulan.append(banner) + banner_ids.add(banner.id) + + if not keyword: + banners = request.env['x_banner.banner'].search(query, limit=limit, offset=offset, order=order) + else: + banners = banner_kumpulan if len(banner_kumpulan) > 0 else request.env['x_banner.banner'].search(query, limit=limit, offset=offset, order=order) + week_number = self.get_week_number_of_current_month() end_datas = [] @@ -41,7 +60,8 @@ class Banner(controller.Controller): 'group_by_week': banner.group_by_week, 'image': request.env['ir.attachment'].api_image('x_banner.banner', 'x_banner_image', banner.id), 'headline_banner': banner.x_headline_banner, - 'description_banner': banner.x_description_banner + 'description_banner': banner.x_description_banner, + 'keyword_banner': banner.x_keyword_banner } if banner.group_by_week and int(banner.group_by_week) < week_number and type == 'index-a-1': 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/controllers/api_v1/partner.py b/indoteknik_api/controllers/api_v1/partner.py index 307165b3..126fded4 100644 --- a/indoteknik_api/controllers/api_v1/partner.py +++ b/indoteknik_api/controllers/api_v1/partner.py @@ -64,37 +64,43 @@ class Partner(controller.Controller): @http.route(prefix + 'partner/<id>/address', auth='public', methods=['PUT', 'OPTIONS'], csrf=False) @controller.Controller.must_authorized() def write_partner_address_by_id(self, **kw): - params = self.get_request_params(kw, { - 'id': ['required', 'number'], - 'type': ['default:other'], - 'name': ['required'], - 'email': ['required'], - 'mobile': ['required'], - 'phone': [''], - 'street': ['required'], - 'state_id': ['required', 'number', 'alias:state_id'], - 'city_id': ['required', 'number', 'alias:kota_id'], - 'district_id': ['number', 'alias:kecamatan_id'], - 'sub_district_id': ['number', 'alias:kelurahan_id', 'exclude_if_null'], - 'zip': ['required'], - 'longtitude': [], - 'latitude': [], - 'address_map': [], - 'alamat_lengkap_text': [] - }) + try: + params = self.get_request_params(kw, { + 'id': ['required', 'number'], + 'type': ['default:other'], + 'name': ['required'], + 'email': ['required'], + 'mobile': ['required'], + 'phone': [''], + 'street': ['required'], + 'state_id': ['required', 'number', 'alias:state_id'], + 'city_id': ['required', 'number', 'alias:kota_id'], + 'district_id': ['number', 'alias:kecamatan_id'], + 'sub_district_id': ['number', 'alias:kelurahan_id', 'exclude_if_null'], + 'zip': ['required'], + 'longtitude': '', + 'latitude': '', + 'address_map': [], + 'alamat_lengkap_text': [] + }) - if not params['valid']: - return self.response(code=400, description=params) - - partner = request.env[self._name].search([('id', '=', params['value']['id'])], limit=1) - if not partner: - return self.response(code=404, description='User not found') - - partner.write(params['value']) + if not params['valid']: + return self.response(code=400, description=params) - return self.response({ - 'id': partner.id - }) + partner = request.env[self._name].sudo().search([('id', '=', params['value']['id'])], limit=1) + + if not partner: + return self.response(code=404, description='User not found') + + try: + partner.write(params['value']) + except Exception as e: + return self.response(code=500, description=f'Error writing partner data: {str(e)}') + + return self.response({'id': partner.id}) + + except Exception as e: + return self.response(code=500, description=f'Unexpected error: {str(e)}') @http.route(prefix + 'partner/address', auth='public', methods=['POST', 'OPTIONS'], csrf=False) @controller.Controller.must_authorized() @@ -111,8 +117,8 @@ class Partner(controller.Controller): 'city_id': ['required', 'number', 'alias:kota_id'], 'district_id': ['number', 'alias:kecamatan_id'], 'sub_district_id': ['number', 'alias:kelurahan_id', 'exclude_if_null'], - 'longtitude': [], - 'latitude': [], + 'longtitude': '', + 'latitude': '', 'address_map': [], 'zip': ['required'] }) @@ -303,270 +309,310 @@ class Partner(controller.Controller): @http.route(prefix + 'partner/pengajuan_tempo', auth='public', methods=['POST'], csrf=False) @controller.Controller.must_authorized() def write_pengajuan_tempo(self, **kw): - id = int(kw.get('partner_id')) - user_id = int(kw.get('user_id')) - tempo_request = True if kw.get('tempo_request') == 'true' else False - pengajuan_tempo = request.env['user.pengajuan.tempo'].search([('name_tempo', '=', user_id)], limit=1) - user = request.env['res.partner'].search([('id', '=', user_id)], limit=1) - company_name = kw.get('name', pengajuan_tempo.name_tempo.name) - partner_id = request.env['res.partner'].search([('name', 'like', company_name)], limit=1) - user_account = self.get_user_by_email(user.email) - - params = self.get_request_params(kw, { - - # informasi perusahaan - # 'name': ['required', 'alias:name_tempo'], - 'industryId': ['alias:industry_id_tempo'], - 'street': ['alias:street_tempo'], - 'state': ['alias:state_id_tempo'], - 'city': ['alias:city_id_tempo'], - 'district': ['alias:district_id_tempo'], - 'subDistrict': ['alias:subDistrict_id_tempo'], - 'zip': ['alias:zip_tempo'], - 'mobile': ['alias:mobile_tempo'], - 'bankName': ['alias:bank_name_tempo'], - 'accountName': ['alias:account_name_tempo'], - 'accountNumber': ['alias:account_number_tempo'], - 'website': ['alias:website_tempo'], - 'estimasi': ['alias:estimasi_tempo'], - 'portal': ['alias:portal'], - 'bersedia': ['alias:bersedia'], - 'tempoDuration': ['alias:tempo_duration'], - 'tempoLimit': ['alias:tempo_limit'], - - # informasi perusahaan - 'direkturTittle': ['alias:direktur_tittle'], - 'direkturName': ['alias:direktur_name'], - 'direkturMobile': ['alias:direktur_mobile'], - 'direkturEmail': ['alias:direktur_email'], - 'purchasingTittle': ['alias:purchasing_tittle'], - 'purchasingName': ['alias:purchasing_name'], - 'purchasingMobile': ['alias:purchasing_mobile'], - 'purchasingEmail': ['alias:purchasing_email'], - 'financeTittle': ['alias:finance_tittle'], - 'financeName': ['alias:finance_name'], - 'financeMobile': ['alias:finance_mobile'], - 'financeEmail': ['alias:finance_email'], - - # Pengiriman - 'PICTittle': ['alias:pic_tittle'], - 'PICName': ['alias:pic_name'], - 'streetPengiriman': ['alias:street_pengiriman'], - 'statePengiriman': ['alias:state_id_pengiriman'], - 'cityPengiriman': ['alias:city_id_pengiriman'], - 'districtPengiriman': ['alias:district_id_pengiriman'], - 'subDistrictPengiriman': ['alias:subDistrict_id_pengiriman'], - 'zipPengiriman': ['alias:zip_pengiriman'], - 'invoicePicTittle': ['alias:invoice_pic_tittle'], - 'invoicePic': ['alias:invoice_pic'], - 'streetInvoice': ['alias:street_invoice'], - 'stateInvoice': ['alias:state_id_invoice'], - 'cityInvoice': ['alias:city_id_invoice'], - 'districtInvoice': ['alias:district_id_invoice'], - 'subDistrictInvoice': ['alias:subDistrict_id_invoice'], - 'zipInvoice': ['alias:zip_invoice'], - 'isSameAddrees':['alias:is_same_address'], - 'isSameAddreesStreet':['alias:is_same_address_street'], - }) - - # # Konversi nilai 'true' ke boolean True - # is_same_address = kw.get('isSameAddrees', 'false').lower() == 'true' - # is_same_address_street = kw.get('isSameAddreesStreet', 'false').lower() == 'true' - # - # # Tambahkan nilai yang dikonversi ke params - # if 'isSameAddress' in kw: - # params['value']['is_same_address'] = is_same_address - # if 'is_same_address_street' in kw: - # params['value']['is_same_address_street'] = is_same_address_street + try: + id = int(kw.get('partner_id')) + user_id = int(kw.get('user_id')) + tempo_request = True if kw.get('tempo_request') == 'true' else False + pengajuan_tempo = request.env['user.pengajuan.tempo'].search([('name_tempo', '=', user_id)], limit=1) + user = request.env['res.partner'].search([('id', '=', user_id)], limit=1) + company_name = kw.get('name', pengajuan_tempo.name_tempo.name) + partner_id = request.env['res.partner'].search([('name', 'like', company_name)], limit=1) + user_account = self.get_user_by_email(user.email) + dokumen_prosedur = False + if kw.get('formDokumenProsedur') and kw.get('formDokumenProsedur') != 'false': + dokumen_prosedur = kw.get('formDokumenProsedur') + params = self.get_request_params(kw, { + + # informasi perusahaan + # 'name': ['required', 'alias:name_tempo'], + 'industryId': ['alias:industry_id_tempo'], + 'street': ['alias:street_tempo'], + 'state': ['alias:state_id_tempo'], + 'city': ['alias:city_id_tempo'], + 'district': ['alias:district_id_tempo'], + 'subDistrict': ['alias:subDistrict_id_tempo'], + 'zip': ['alias:zip_tempo'], + 'mobile': ['alias:mobile_tempo'], + 'bankName': ['alias:bank_name_tempo'], + 'accountName': ['alias:account_name_tempo'], + 'accountNumber': ['alias:account_number_tempo'], + 'website': ['alias:website_tempo'], + 'estimasi': ['alias:estimasi_tempo'], + 'portal': ['alias:portal'], + 'bersedia': ['alias:bersedia'], + 'tempoDuration': ['alias:tempo_duration'], + 'tempoLimit': ['alias:tempo_limit'], + + # informasi perusahaan + 'direkturTittle': ['alias:direktur_tittle'], + 'direkturName': ['alias:direktur_name'], + 'direkturMobile': ['alias:direktur_mobile'], + 'direkturEmail': ['alias:direktur_email'], + 'purchasingTittle': ['alias:purchasing_tittle'], + 'purchasingName': ['alias:purchasing_name'], + 'purchasingMobile': ['alias:purchasing_mobile'], + 'purchasingEmail': ['alias:purchasing_email'], + 'financeTittle': ['alias:finance_tittle'], + 'financeName': ['alias:finance_name'], + 'financeMobile': ['alias:finance_mobile'], + 'financeEmail': ['alias:finance_email'], + + # Pengiriman + 'PICTittle': ['alias:pic_tittle'], + 'PICBarangMobile': ['alias:pic_mobile'], + 'PICName': ['alias:pic_name'], + 'streetPengiriman': ['alias:street_pengiriman'], + 'statePengiriman': ['alias:state_id_pengiriman'], + 'cityPengiriman': ['alias:city_id_pengiriman'], + 'districtPengiriman': ['alias:district_id_pengiriman'], + 'subDistrictPengiriman': ['alias:subDistrict_id_pengiriman'], + 'zipPengiriman': ['alias:zip_pengiriman'], + 'invoicePicTittle': ['alias:invoice_pic_tittle'], + 'invoicePicMobile': ['alias:invoice_pic_mobile'], + 'invoicePic': ['alias:invoice_pic'], + 'streetInvoice': ['alias:street_invoice'], + 'stateInvoice': ['alias:state_id_invoice'], + 'cityInvoice': ['alias:city_id_invoice'], + 'districtInvoice': ['alias:district_id_invoice'], + 'subDistrictInvoice': ['alias:subDistrict_id_invoice'], + 'zipInvoice': ['alias:zip_invoice'], + 'isSameAddrees':['alias:is_same_address'], + 'isSameAddreesStreet':['alias:is_same_address_street'], + }) - if not params['valid']: - return self.response(code=400, description=params) - if params['value']['portal']: - if params['value']['portal'] == 'ada': - params['value']['portal'] = True + # # Konversi nilai 'true' ke boolean True + # is_same_address = kw.get('isSameAddrees', 'false').lower() == 'true' + # is_same_address_street = kw.get('isSameAddreesStreet', 'false').lower() == 'true' + # + # # Tambahkan nilai yang dikonversi ke params + # if 'isSameAddress' in kw: + # params['value']['is_same_address'] = is_same_address + # if 'is_same_address_street' in kw: + # params['value']['is_same_address_street'] = is_same_address_street + + if not params['valid']: + return self.response(code=400, description=params) + if params['value']['portal']: + if params['value']['portal'] == 'ada': + params['value']['portal'] = True + else: + params['value']['portal'] = False + # Filter data baru yang dikirim (non-kosong, boolean False tetap masuk) + new_data = {key: value for key, value in params['value'].items() if value != ''} + + if pengajuan_tempo: + try: + pengajuan_tempo.write(new_data) + except Exception as e: + return self.response(code=500, description=f'Error updating partner data: {str(e)}') else: - params['value']['portal'] = False - # Filter data baru yang dikirim (non-kosong, boolean False tetap masuk) - new_data = {key: value for key, value in params['value'].items() if value != ''} + try: + pengajuan_tempo = request.env['user.pengajuan.tempo'].create(new_data) + pengajuan_tempo.partner_id = user_id + except Exception as e: + return self.response(code=500, description=f'Error creating partner data: {str(e)}') + + if partner_id: + try: + pengajuan_tempo.name_tempo = partner_id + except Exception as e: + return self.response(code=500, description=f'Error updating partner data: {str(e)}') + + # Prosedur Pengiriman + if dokumen_prosedur: + dokumen_prosedur = json.loads(dokumen_prosedur) + mimetype, _ = mimetypes.guess_type(dokumen_prosedur['name']) + mimetype = mimetype or 'application/octet-stream' + data = base64.b64decode(dokumen_prosedur['base64']) + dokumen_prosedur_attachment = request.env['ir.attachment'].create({ + 'name': dokumen_prosedur['name'], + 'type': 'binary', + 'datas': base64.b64encode(data), + 'res_model': 'user.pengajuan.tempo', + 'res_id': pengajuan_tempo.id, + 'mimetype': mimetype + }) + pengajuan_tempo.dokumen_prosedur = [(6, 0, [dokumen_prosedur_attachment.id])] + pengajuan_tempo.message_post(body="Dokumen Prosedur", attachment_ids=[dokumen_prosedur_attachment.id]) + + + form_supplier_data = kw.get('formSupplier', False) + + if form_supplier_data: + try: + form_supplier_data = json.loads(form_supplier_data) + + supplier_ids_to_add = [] + for item in form_supplier_data: + supplier_name = item.get("supplier") + pic_name = item.get("pic") + phone = item.get("telepon") + tempo_duration = item.get("durasiTempo") + credit_limit = item.get("creditLimit") + + new_data = { + 'name_supplier': supplier_name, + 'pic_name': pic_name, + 'phone': phone, + 'tempo_duration': tempo_duration, + 'credit_limit': credit_limit, + } + new_supplier_data = request.env['user.pengajuan.tempo.line'].create(new_data) + + supplier_ids_to_add.append(new_supplier_data.id) + + pengajuan_tempo.write({'supplier_ids': [(6, 0, supplier_ids_to_add)]}) + + except json.JSONDecodeError: + return http.Response(status=400, json_body={'error': 'Invalid JSON format for formSupplier'}) + category_produk_ids = kw.get('categoryProduk', False) + category_ids = '' + if category_produk_ids: + try: + category_ids = list(map(int, category_produk_ids.split(','))) + pengajuan_tempo.category_produk_ids = [(6, 0, category_ids)] + except Exception as e: + return self.response(code=500, description=f'Unexpected error: {str(e)}') + + + tukar_invoice_input = kw.get('tukarInvoiceInput') + if tukar_invoice_input: + pengajuan_tempo.tukar_invoice = tukar_invoice_input + + tukar_invoice_input_pembayaran = kw.get('tukarInvoiceInputPembayaran') + if tukar_invoice_input_pembayaran: + pengajuan_tempo.jadwal_bayar = tukar_invoice_input_pembayaran + + dokumen_kirim = [ + 'Surat Tanda Terima Barang (STTB)', + 'Good Receipt (GR)', + 'Surat Terima Barang (STB)', + 'Lembar Penerimaan Barang (LPB)' + ] + + dokumen_kirim_barang_ids = kw.get('dokumenPengiriman') + dokumen_kirim_input = kw.get('dokumenKirimInput', '') + dokumen_kirim_barang_input = kw.get('dokumenPengirimanInput', '') + dokumen_kirim_barang = [] + + if dokumen_kirim_barang_ids: + dokumen_kirim_ids = list(map(int, dokumen_kirim_barang_ids.split(','))) + dokumen_kirim_barang = [dokumen_kirim[i] for i in dokumen_kirim_ids if 0 <= i < len(dokumen_kirim)] + if dokumen_kirim_input: + input_items = [item.strip() for item in dokumen_kirim_input.split(',')] + dokumen_kirim_barang.extend(item for item in input_items if item and item not in dokumen_kirim_barang) + pengajuan_tempo.dokumen_kirim_input = dokumen_kirim_input + if dokumen_kirim_barang: + pengajuan_tempo.dokumen_pengiriman = ', '.join(dokumen_kirim_barang) + if dokumen_kirim_barang_input: + pengajuan_tempo.dokumen_pengiriman_input = dokumen_kirim_barang_input + + dokumen = [ + 'Invoice Pembelian', + 'Surat Jalan', + 'Berita Acara Serah Terima (BAST)', + 'Faktur Pajak', + 'Good Receipt (GR)' + ] + + dokumen_invoice = kw.get('dokumenPengirimanInvoice', '') + if dokumen_invoice: + pengajuan_tempo.dokumen_invoice = dokumen_invoice + user_tempo_request = [] + if tempo_request: + user_tempo_request = request.env['user.pengajuan.tempo.request'].create({ + 'user_id': id, + 'pengajuan_tempo_id': pengajuan_tempo.id, + 'user_company_id': partner_id.id, + 'tempo_duration': pengajuan_tempo.tempo_duration.id, + 'tempo_limit': pengajuan_tempo.tempo_limit, + }) - if pengajuan_tempo: - # Jika pengajuan_tempo sudah ada, hanya write data baru yang non-kosong - pengajuan_tempo.write(new_data) - else: - # Jika belum ada, buat record baru - pengajuan_tempo = request.env['user.pengajuan.tempo'].create(new_data) - pengajuan_tempo.partner_id = user_id + form_dokumen_data = kw.get('formDocs', False) + if form_dokumen_data: + try: + form_dokumen = json.loads(form_dokumen_data) - if partner_id: - pengajuan_tempo.name_tempo = partner_id + for dokumen in form_dokumen: + if dokumen['details']['base64'] != '': + mimetype, _ = mimetypes.guess_type(dokumen['details']['name']) + mimetype = mimetype or 'application/octet-stream' + data = base64.b64decode(dokumen['details']['base64']) + sppkp_attachment = request.env['ir.attachment'].create({ + 'name': dokumen['details']['name'], + 'type': 'binary', + 'datas': base64.b64encode(data), + 'res_model': 'user.pengajuan.tempo', + 'res_id': pengajuan_tempo.id, + 'mimetype': mimetype + }) - form_supplier_data = kw.get('formSupplier', False) + if dokumen['documentName'] == 'dokumenNib': + pengajuan_tempo.dokumen_nib = [(6, 0, [sppkp_attachment.id])] - if form_supplier_data: - try: - form_supplier_data = json.loads(form_supplier_data) - - supplier_ids_to_add = [] - for item in form_supplier_data: - supplier_name = item.get("supplier") - pic_name = item.get("pic") - phone = item.get("telepon") - tempo_duration = item.get("durasiTempo") - credit_limit = item.get("creditLimit") - - new_data = { - 'name_supplier': supplier_name, - 'pic_name': pic_name, - 'phone': phone, - 'tempo_duration': tempo_duration, - 'credit_limit': credit_limit, - } - new_supplier_data = request.env['user.pengajuan.tempo.line'].create(new_data) - - supplier_ids_to_add.append(new_supplier_data.id) - - pengajuan_tempo.write({'supplier_ids': [(6, 0, supplier_ids_to_add)]}) - - except json.JSONDecodeError: - return http.Response(status=400, json_body={'error': 'Invalid JSON format for formSupplier'}) - category_produk_ids = kw.get('categoryProduk', False) - category_ids = '' - if category_produk_ids: - category_ids = list(map(int, category_produk_ids.split(','))) - pengajuan_tempo.category_produk_ids = [(6, 0, category_ids)] - - tukar_invoice_input = kw.get('tukarInvoiceInput') - if tukar_invoice_input: - pengajuan_tempo.tukar_invoice = tukar_invoice_input - - tukar_invoice_input_pembayaran = kw.get('tukarInvoiceInputPembayaran') - if tukar_invoice_input_pembayaran: - pengajuan_tempo.jadwal_bayar = tukar_invoice_input_pembayaran - - dokumen_kirim = [ - 'Surat Tanda Terima Barang (STTB)', - 'Good Receipt (GR)', - 'Surat Terima Barang (STB)', - 'Lembar Penerimaan Barang (LPB)' - ] - - dokumen_kirim_barang_ids = kw.get('dokumenPengiriman') - dokumen_kirim_input = kw.get('dokumenKirimInput', '') - dokumen_kirim_barang_input = kw.get('dokumenPengirimanInput', '') - dokumen_kirim_barang = [] - - if dokumen_kirim_barang_ids: - dokumen_kirim_ids = list(map(int, dokumen_kirim_barang_ids.split(','))) - dokumen_kirim_barang = [dokumen_kirim[i] for i in dokumen_kirim_ids if 0 <= i < len(dokumen_kirim)] - if dokumen_kirim_input: - input_items = [item.strip() for item in dokumen_kirim_input.split(',')] - dokumen_kirim_barang.extend(item for item in input_items if item and item not in dokumen_kirim_barang) - pengajuan_tempo.dokumen_kirim_input = dokumen_kirim_input - if dokumen_kirim_barang: - pengajuan_tempo.dokumen_pengiriman = ', '.join(dokumen_kirim_barang) - if dokumen_kirim_barang_input: - pengajuan_tempo.dokumen_pengiriman_input = dokumen_kirim_barang_input - - dokumen = [ - 'Invoice Pembelian', - 'Surat Jalan', - 'Berita Acara Serah Terima (BAST)', - 'Faktur Pajak', - 'Good Receipt (GR)' - ] - - dokumen_invoice = kw.get('dokumenPengirimanInvoice', '') - if dokumen_invoice: - pengajuan_tempo.dokumen_invoice = dokumen_invoice - user_tempo_request = [] - if tempo_request: - user_tempo_request = request.env['user.pengajuan.tempo.request'].create({ - 'user_id': id, - 'pengajuan_tempo_id': pengajuan_tempo.id, - 'user_company_id': partner_id.id, - 'tempo_duration': pengajuan_tempo.tempo_duration.id, - 'tempo_limit': pengajuan_tempo.tempo_limit, - }) + elif dokumen['documentName'] == 'dokumenSiup': + pengajuan_tempo.dokumen_siup = [(6, 0, [sppkp_attachment.id])] - form_dokumen_data = kw.get('formDocs', False) - if form_dokumen_data: - try: - form_dokumen = json.loads(form_dokumen_data) + elif dokumen['documentName'] == 'dokumenTdp': + pengajuan_tempo.dokumen_tdp = [(6, 0, [sppkp_attachment.id])] - for dokumen in form_dokumen: - if dokumen['details']['base64'] != '': - mimetype, _ = mimetypes.guess_type(dokumen['details']['name']) - mimetype = mimetype or 'application/octet-stream' - data = base64.b64decode(dokumen['details']['base64']) - sppkp_attachment = request.env['ir.attachment'].create({ - 'name': dokumen['details']['name'], - 'type': 'binary', - 'datas': base64.b64encode(data), - 'res_model': 'user.pengajuan.tempo', - 'res_id': pengajuan_tempo.id, - 'mimetype': mimetype - }) + elif dokumen['documentName'] == 'dokumenSkdp': + pengajuan_tempo.dokumen_skdp = [(6, 0, [sppkp_attachment.id])] - if dokumen['documentName'] == 'dokumenNib': - pengajuan_tempo.dokumen_nib = [(6, 0, [sppkp_attachment.id])] + elif dokumen['documentName'] == 'dokumenSkt': + pengajuan_tempo.dokumen_skt = [(6, 0, [sppkp_attachment.id])] - elif dokumen['documentName'] == 'dokumenSiup': - pengajuan_tempo.dokumen_siup = [(6, 0, [sppkp_attachment.id])] + elif dokumen['documentName'] == 'dokumenNpwp': + pengajuan_tempo.dokumen_npwp = [(6, 0, [sppkp_attachment.id])] - elif dokumen['documentName'] == 'dokumenTdp': - pengajuan_tempo.dokumen_tdp = [(6, 0, [sppkp_attachment.id])] + elif dokumen['documentName'] == 'dokumenSppkp': + pengajuan_tempo.dokumen_sppkp = [(6, 0, [sppkp_attachment.id])] - elif dokumen['documentName'] == 'dokumenSkdp': - pengajuan_tempo.dokumen_skdp = [(6, 0, [sppkp_attachment.id])] + elif dokumen['documentName'] == 'dokumenAktaPerubahan': + pengajuan_tempo.dokumen_akta_perubahan = [(6, 0, [sppkp_attachment.id])] - elif dokumen['documentName'] == 'dokumenSkt': - pengajuan_tempo.dokumen_skt = [(6, 0, [sppkp_attachment.id])] + elif dokumen['documentName'] == 'dokumenKtpDirut': + pengajuan_tempo.dokumen_ktp_dirut = [(6, 0, [sppkp_attachment.id])] - elif dokumen['documentName'] == 'dokumenNpwp': - pengajuan_tempo.dokumen_npwp = [(6, 0, [sppkp_attachment.id])] + elif dokumen['documentName'] == 'dokumenAktaPendirian': + pengajuan_tempo.dokumen_akta_pendirian = [(6, 0, [sppkp_attachment.id])] - elif dokumen['documentName'] == 'dokumenSppkp': - pengajuan_tempo.dokumen_sppkp = [(6, 0, [sppkp_attachment.id])] + elif dokumen['documentName'] == 'dokumenLaporanKeuangan': + pengajuan_tempo.dokumen_laporan_keuangan = [(6, 0, [sppkp_attachment.id])] - elif dokumen['documentName'] == 'dokumenAktaPerubahan': - pengajuan_tempo.dokumen_akta_perubahan = [(6, 0, [sppkp_attachment.id])] + elif dokumen['documentName'] == 'dokumenFotoKantor': + pengajuan_tempo.dokumen_foto_kantor = [(6, 0, [sppkp_attachment.id])] - elif dokumen['documentName'] == 'dokumenKtpDirut': - pengajuan_tempo.dokumen_ktp_dirut = [(6, 0, [sppkp_attachment.id])] + elif dokumen['documentName'] == 'dokumenTempatBekerja': + pengajuan_tempo.dokumen_tempat_bekerja = [(6, 0, [sppkp_attachment.id])] - elif dokumen['documentName'] == 'dokumenAktaPendirian': - pengajuan_tempo.dokumen_akta_pendirian = [(6, 0, [sppkp_attachment.id])] + formatted_text = ''.join([' ' + char if char.isupper() and i != 0 else char for i, char in + enumerate(dokumen['documentName'])]) + teks = formatted_text.strip().title() + pengajuan_tempo.message_post(body=teks, attachment_ids=[sppkp_attachment.id]) + if tempo_request: + user_tempo_request.message_post(body=teks, attachment_ids=[sppkp_attachment.id]) - elif dokumen['documentName'] == 'dokumenLaporanKeuangan': - pengajuan_tempo.dokumen_laporan_keuangan = [(6, 0, [sppkp_attachment.id])] - elif dokumen['documentName'] == 'dokumenFotoKantor': - pengajuan_tempo.dokumen_foto_kantor = [(6, 0, [sppkp_attachment.id])] + except json.JSONDecodeError: + return http.Response(status=400, json_body={'error': 'Invalid JSON format for formDokumen'}) - elif dokumen['documentName'] == 'dokumenTempatBekerja': - pengajuan_tempo.dokumen_tempat_bekerja = [(6, 0, [sppkp_attachment.id])] + if tempo_request: + # pengajuan_tempo.user_id = id + template = pengajuan_tempo.env.ref('indoteknik_custom.mail_template_res_user_company_request_tempo_review') + template.send_mail(pengajuan_tempo.id, force_send=True) + template2 = pengajuan_tempo.env.ref('indoteknik_custom.mail_template_res_user_company_new_tempo_to_sales') + template2.send_mail(pengajuan_tempo.id, force_send=True) + if not pengajuan_tempo: + return self.response(code=500, description="Failed to create or update pengajuan_tempo") - formatted_text = ''.join([' ' + char if char.isupper() and i != 0 else char for i, char in - enumerate(dokumen['documentName'])]) - teks = formatted_text.strip().title() - pengajuan_tempo.message_post(body=teks, attachment_ids=[sppkp_attachment.id]) - if tempo_request: - user_tempo_request.message_post(body=teks, attachment_ids=[sppkp_attachment.id]) - - - except json.JSONDecodeError: - return http.Response(status=400, json_body={'error': 'Invalid JSON format for formDokumen'}) + return self.response({ + 'id': pengajuan_tempo.id, + 'user_id': user_id, + }) + except Exception as e: + return self.response(code=500, description=f'Unexpected error: {str(e)}') - if tempo_request: - # pengajuan_tempo.user_id = id - template = pengajuan_tempo.env.ref('indoteknik_custom.mail_template_res_user_company_request_tempo_review') - template.send_mail(pengajuan_tempo.id, force_send=True) - template2 = pengajuan_tempo.env.ref('indoteknik_custom.mail_template_res_user_company_new_tempo_to_sales') - template2.send_mail(pengajuan_tempo.id, force_send=True) - return self.response({ - 'id': pengajuan_tempo.id, - 'user_id': user_id, - }) def get_user_by_email(self, email): return request.env['res.users'].search([ diff --git a/indoteknik_api/controllers/api_v1/product.py b/indoteknik_api/controllers/api_v1/product.py index 32362582..a88c3368 100644 --- a/indoteknik_api/controllers/api_v1/product.py +++ b/indoteknik_api/controllers/api_v1/product.py @@ -1,13 +1,13 @@ from .. import controller from odoo import http -from odoo.http import request +from odoo.http import request, Response from datetime import datetime, timedelta import ast import logging import math import json -_logger = logging.getLogger(__name__) +_logger = logging.getLogger(__name__) class Product(controller.Controller): @@ -33,9 +33,70 @@ class Product(controller.Controller): categories.reverse() return self.response(categories, headers=[('Cache-Control', 'max-age=3600, public')]) - - @http.route(prefix + 'product_variant/<id>/stock', auth='public', methods=['GET', 'OPTIONS']) + + @http.route(prefix + 'product/variants/sla', auth='public', methods=['POST', 'OPTIONS'] , csrf=False) @controller.Controller.must_authorized() + def get_product_template_sla_by_id(self, **kwargs): + + raw_data = kwargs.get('products', '[]') + product_data = json.loads(raw_data) + + product_ids = [int(item["id"]) for item in product_data] + products = request.env['purchase.pricelist'].search([ + ('product_id', 'in', product_ids), + ('is_winner', '=', True) + ]) + + start_date = datetime.today().date() + additional_days = request.env['sale.order'].get_days_until_next_business_day(start_date) + include_instant = True + + if(len(products) != len(product_ids)): + products_data_params = {product["id"] : product for product in product_data } + + all_fast_products = all( + product.product_id.qty_free_bandengan >= products_data_params.get(product.product_id.id, {}).get("quantity", 0) + for product in products + ) + + if all_fast_products: + return self.response({ + 'include_instant': include_instant, + 'sla_duration': 1, + 'sla_additional_days': additional_days, + 'sla_total' : int(1) + int(additional_days), + 'sla_unit': 'Hari' + }) + + max_slatime = 1 + + for vendor in products: + vendor_sla = request.env['vendor.sla'].search([('id_vendor', '=', vendor.vendor_id.id)], limit=1) + slatime = 15 + if vendor_sla: + if vendor_sla.unit == 'hari': + vendor_duration = vendor_sla.duration * 24 * 60 + include_instant = False + else : + vendor_duration = vendor_sla.duration * 60 + include_instant = True + + estimation_sla = (1 * 24 * 60) + vendor_duration + estimation_sla_days = estimation_sla / (24 * 60) + slatime = math.ceil(estimation_sla_days) + + max_slatime = max(max_slatime, slatime) + + return self.response({ + 'include_instant': include_instant, + 'sla_duration': max_slatime, + 'sla_additional_days': additional_days, + 'sla_total' : int(max_slatime) + int(additional_days), + 'sla_unit': 'Hari' + }) + + @http.route(prefix + 'product_variant/<id>/stock', auth='public', methods=['GET', 'OPTIONS']) + @controller.Controller.must_authorized() def get_product_template_stock_by_id(self, **kw): id = int(kw.get('id')) date_7_days_ago = datetime.now() - timedelta(days=7) @@ -49,10 +110,11 @@ class Product(controller.Controller): ], limit=1) qty_available = product.qty_free_bandengan - - if qty_available < 0: - qty_available = 0 - + + + if qty_available < 1 : + qty_available = 0 + qty = 0 sla_date = '-' @@ -74,24 +136,25 @@ class Product(controller.Controller): if qty_available > 0: qty = qty_available + total_adem + total_excell + sla_date = product_sla.sla or 1 elif qty_altama > 0 or qty_vendor > 0: qty = total_adem if qty_altama > 0 else total_excell - sla_date = '2-4 Hari' + sla_date = product_sla.sla else: - sla_date = '3-7 Hari' + sla_date = product_sla.sla except: print('error') else: if qty_available > 0: qty = qty_available - sla_date = product_sla.sla or '-' + sla_date = product_sla.sla or 'Indent' elif qty_vendor > 0: qty = total_excell sla_date = '2-4 Hari' data = { 'qty': qty, - 'sla_date': sla_date, + 'sla_date': sla_date } return self.response(data, headers=[('Cache-Control', 'max-age=600, private')]) diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py index 8b95ade8..98b13cad 100644 --- a/indoteknik_api/controllers/api_v1/sale_order.py +++ b/indoteknik_api/controllers/api_v1/sale_order.py @@ -386,7 +386,8 @@ class SaleOrder(controller.Controller): 'note_website': [], 'voucher': [], 'source': [], - 'estimated_arrival_days': ['number', 'default:0'] + 'estimated_arrival_days': ['number', 'default:0'], + 'estimated_arrival_days_start': ['number', 'default:0'] }) if not params['valid']: @@ -394,7 +395,8 @@ class SaleOrder(controller.Controller): # Fetch partner details sales_partner = request.env['res.partner'].browse(params['value']['partner_id']) - + partner_invoice = request.env['res.partner'].browse(params['value']['partner_invoice_id']) + main_partner = partner_invoice.get_main_parent() parameters = { 'warehouse_id': 8, 'carrier_id': 1, @@ -410,12 +412,13 @@ class SaleOrder(controller.Controller): 'partner_id': params['value']['partner_id'], 'partner_shipping_id': params['value']['partner_shipping_id'], 'real_shipping_id': params['value']['partner_shipping_id'], - 'partner_invoice_id': params['value']['partner_invoice_id'], + 'partner_invoice_id': main_partner.id, 'real_invoice_id': params['value']['partner_invoice_id'], 'partner_purchase_order_name': params['value']['po_number'], 'partner_purchase_order_file': params['value']['po_file'], 'delivery_amt': params['value']['delivery_amount'] * 1.10, 'estimated_arrival_days': params['value']['estimated_arrival_days'], + 'estimated_arrival_days_start': params['value']['estimated_arrival_days_start'], 'shipping_cost_covered': 'customer', 'shipping_paid_by': 'customer', 'carrier_id': params['value']['carrier_id'], @@ -426,7 +429,7 @@ class SaleOrder(controller.Controller): 'npwp': sales_partner.npwp or '0', # Get NPWP from partner 'sppkp': sales_partner.sppkp, # Get SPPKP from partner 'email': sales_partner.email, # Get Email from partner - 'user_id': 3222 # User ID: Nadia Rauhadatul Firdaus + 'user_id': 11314 # User ID: Boy Revandi } sales_partner = request.env['res.partner'].browse(parameters['partner_id']) @@ -463,6 +466,8 @@ class SaleOrder(controller.Controller): 'program_line_id': cart['id'], 'quantity': cart['quantity'] }) + + sale_order._compute_etrts_date() request.env['sale.order.promotion'].create(promotions) diff --git a/indoteknik_api/controllers/api_v1/stock_picking.py b/indoteknik_api/controllers/api_v1/stock_picking.py index 110cde8a..31706b99 100644 --- a/indoteknik_api/controllers/api_v1/stock_picking.py +++ b/indoteknik_api/controllers/api_v1/stock_picking.py @@ -57,7 +57,7 @@ class StockPicking(controller.Controller): if params['status'] == 'pending': domain += pending_domain elif params['status'] == 'shipment': - domain += shipment_domain + domain += shipment_domain + shipment_domain2 elif params['status'] == 'completed': domain += completed_domain @@ -101,7 +101,7 @@ class StockPicking(controller.Controller): picking = picking_model.browse(id) if not picking: return self.response(None) - + hostori = picking.get_tracking_detail() return self.response(picking.get_tracking_detail()) @http.route(prefix + 'stock-picking/<id>/tracking', auth='public', method=['GET', 'OPTIONS']) @@ -116,10 +116,10 @@ class StockPicking(controller.Controller): return self.response(picking.get_tracking_detail()) - @http.route(prefix + 'stock-picking/<picking_code>/documentation', auth='public', methods=['PUT', 'OPTIONS'], csrf=False) + @http.route(prefix + 'stock-picking/<scanid>/documentation', auth='public', methods=['PUT', 'OPTIONS'], csrf=False) @controller.Controller.must_authorized() def write_partner_stock_picking_documentation(self, **kw): - picking_code = int(kw.get('picking_code', 0)) + scanid = int(kw.get('scanid', 0)) sj_document = kw.get('sj_document', False) paket_document = kw.get('paket_document', False) @@ -128,7 +128,10 @@ class StockPicking(controller.Controller): 'driver_arrival_date': datetime.utcnow(), } - picking_data = request.env['stock.picking'].search([('picking_code', '=', picking_code)], limit=1) + picking_data = request.env['stock.picking'].search([('id', '=', scanid)], limit=1) + + if not picking_data: + picking_data = request.env['stock.picking'].search([('picking_code', '=', scanid)], limit=1) if not picking_data: return self.response(code=404, description='picking not found') @@ -136,4 +139,50 @@ class StockPicking(controller.Controller): return self.response({ 'name': picking_data.name - })
\ No newline at end of file + }) + + @http.route(prefix + 'webhook/biteship', type='json', auth='public', methods=['POST'], csrf=False) + def udpate_status_from_bitehsip(self, **kw): + try: + if not request.jsonrequest: + return "ok" + + data = request.jsonrequest # Ambil data JSON dari request + event = data.get('event') + + # Handle Event Berdasarkan Jenisnya + if event == "order.status": + self.process_order_status(data) + elif event == "order.price": + self.process_order_price(data) + elif event == "order.waybill_id": + self.process_order_waybill(data) + + return {'success': True, 'message': f'Webhook {event} received'} + except Exception as e: + return {'success': False, 'message': str(e)} + + def process_order_status(self, data): + picking_model = request.env['stock.picking'].sudo().search([('biteship_id', '=', data.get('order_id'))], limit=1) + if data.get('status') == 'picked': + picking_model.write({'driver_departure_date': datetime.utcnow()}) + elif data.get('status') == 'delivered': + picking_model.write({'driver_arrival_date': datetime.utcnow()}) + + def process_order_price(self, data): + picking_model = request.env['stock.picking'].sudo().search([('biteship_id', '=', data.get('order_id'))], limit=1) + order = request.env['sale.order'].sudo().search([('name', '=', picking_model.sale_id.name)], limit=1) + if order: + order.write({ + 'delivery_amt': data.get('price') + }) + + def process_order_waybill(self, data): + picking_model = request.env['stock.picking'].sudo().search([('biteship_id', '=', data.get('order_id'))], limit=1) + if picking_model: + picking_model.write({ + 'biteship_waybill_id': data.get('courier_waybill_id'), + 'delivery_tracking_no': data.get('courier_waybill_id'), + 'biteship_tracking_id':data.get('courier_tracking_id') + }) +
\ No newline at end of file diff --git a/indoteknik_api/controllers/api_v1/user.py b/indoteknik_api/controllers/api_v1/user.py index f71af89f..b5b7e055 100644 --- a/indoteknik_api/controllers/api_v1/user.py +++ b/indoteknik_api/controllers/api_v1/user.py @@ -98,7 +98,7 @@ class User(controller.Controller): user.partner_id.npwp = '00.000.000.0-000.000' user.partner_id.sppkp = '-' user.partner_id.nama_wajib_pajak = user.name - user.partner_id.user_id = 3222 + user.partner_id.user_id = 11314 user.partner_id.property_account_receivable_id = 395 user.partner_id.property_account_payable_id = 438 data = { @@ -131,6 +131,7 @@ class User(controller.Controller): nama_wajib_pajak = kw.get('nama_wajib_pajak', False) is_pkp = kw.get('is_pkp') is_terdaftar = kw.get('is_terdaftar', False) + is_terdaftar = False if is_terdaftar == 'false' else is_terdaftar type_acc = kw.get('type_acc', 'individu') or 'individu' if not name or not email or not password: @@ -162,16 +163,15 @@ class User(controller.Controller): 'sel_groups_1_9_10': 9 } - user = request.env['res.users'].create(user_data) - user.partner_id.email = email - user.partner_id.mobile = phone + if type_acc == 'business' and business_name: # Eksekusi query SQL menggunakan Levenshtein distance query = """ SELECT id, name, levenshtein(name::text, %s) AS distance FROM res_partner - WHERE levenshtein(name::text, %s) < 3 + WHERE is_company = true AND active = true + AND levenshtein(name::text, %s) < 3 ORDER BY distance ASC """ params = (business_name, business_name) @@ -181,7 +181,9 @@ class User(controller.Controller): if result and is_terdaftar: match_company_name = result[2] match_company_id = result[0] - + user = request.env['res.users'].create(user_data) + user.partner_id.email = email + user.partner_id.mobile = phone # Create a user company request request.env['user.company.request'].create({ 'user_id': user.partner_id.id, @@ -189,6 +191,9 @@ class User(controller.Controller): 'user_input': business_name }) else: + if not result and is_terdaftar: + response['reason'] = 'BISNIS_NOT_FOUND' + return self.response(response) if not nama_wajib_pajak and is_pkp == 'false': nama_wajib_pajak = business_name @@ -207,11 +212,15 @@ class User(controller.Controller): 'email': email_partner, 'street': alamat_bisnis, 'company_type': 'company', - 'user_id': 3222, + 'user_id': 11314, 'property_account_receivable_id': 395, 'property_account_payable_id': 438, 'active': False, } + + user = request.env['res.users'].create(user_data) + user.partner_id.email = email + user.partner_id.mobile = phone new_company = request.env['res.partner'].create(new_company_data) request.env['user.company.request'].create({ 'user_id': user.partner_id.id, @@ -246,13 +255,15 @@ class User(controller.Controller): 'mimetype': sppkp_mimetype }) new_company.message_post(body="SPPKP Uploaded", attachment_ids=[sppkp_attachment.id]) - if type_acc == 'individu': + user = request.env['res.users'].create(user_data) + user.partner_id.email = email + user.partner_id.mobile = phone user.partner_id.customer_type = 'nonpkp' user.partner_id.npwp = '00.000.000.0-000.000' user.partner_id.sppkp = '-' user.partner_id.nama_wajib_pajak = name - user.partner_id.user_id = 3222 + user.partner_id.user_id = 11314 user.partner_id.property_account_receivable_id= 395 user.partner_id.property_account_payable_id = 438 @@ -604,7 +615,7 @@ class User(controller.Controller): 'email': email_partner, 'street': alamat_bisnis, 'company_type': 'company', - 'user_id': 3222, + 'user_id': 11314, 'property_account_receivable_id': 395, 'property_account_payable_id': 438, 'active': False, diff --git a/indoteknik_api/controllers/api_v1/voucher.py b/indoteknik_api/controllers/api_v1/voucher.py index 910488d1..9ffeeace 100644 --- a/indoteknik_api/controllers/api_v1/voucher.py +++ b/indoteknik_api/controllers/api_v1/voucher.py @@ -22,9 +22,14 @@ class Voucher(controller.Controller): code = kw.get('code') type = kw.get('type') user_id = int(kw.get('user_id', 0)) + partner_id = int(kw.get('partner_id', 0)) source = kw.get('source') visibility = ['public'] + user = request.env['res.users'].search([('id', '=', user_id)], limit=1) + if not user: + return self.response([]) + domain = [] if code: visibility.append('private') @@ -37,14 +42,19 @@ class Voucher(controller.Controller): type = type.split(',') domain += [('apply_type', 'in', type)] + if partner_id: + partner = request.env['res.partner'].search([('id', '=', partner_id)], limit=1) + main_parent = partner.get_main_parent() + if main_parent and main_parent.company_type: + domain += [('account_type', 'in', ['all',main_parent.company_type])] + # domain += [('account_type', 'in', main_parent.company_type)] + + domain += [('visibility', 'in', visibility)] vouchers = request.env['voucher'].get_active_voucher(domain) checkout = cart.get_user_checkout(user_id, source=source) products = checkout['products'] - user = request.env['res.users'].search([('id', '=', user_id)], limit=1) - if not user: - return self.response([]) order_line = [] for product in products: @@ -89,3 +99,9 @@ class Voucher(controller.Controller): sorted_results = sorted(results, key=lambda x: x['can_apply'], reverse=True) return self.response(sorted_results) + + def get_user_by_email(self, email): + return request.env['res.users'].search([ + ('login', '=', email), + ('active', 'in', [True, False]) + ])
\ No newline at end of file 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_api/models/res_partner.py b/indoteknik_api/models/res_partner.py index 5569a877..2cebab83 100644 --- a/indoteknik_api/models/res_partner.py +++ b/indoteknik_api/models/res_partner.py @@ -59,6 +59,7 @@ class ResPartner(models.Model): # Pengiriman 'PIC_tittle' : pengajuan_tempo.pic_tittle if pengajuan_tempo.pic_tittle else '', + 'PICBarangMobile' : pengajuan_tempo.pic_mobile if pengajuan_tempo.pic_mobile else '', 'PIC_name' : pengajuan_tempo.pic_name if pengajuan_tempo.pic_name else '', 'street_pengiriman' : pengajuan_tempo.street_pengiriman if pengajuan_tempo.street_pengiriman else '', 'state_pengiriman' : pengajuan_tempo.state_id_pengiriman.id if pengajuan_tempo.state_id_pengiriman else '', @@ -67,6 +68,7 @@ class ResPartner(models.Model): 'subDistrict_pengiriman': pengajuan_tempo.subDistrict_id_pengiriman.id if pengajuan_tempo.subDistrict_id_pengiriman else '', 'zip_pengiriman' : pengajuan_tempo.zip_pengiriman if pengajuan_tempo.zip_pengiriman else '', 'invoice_pic_tittle' : pengajuan_tempo.invoice_pic_tittle if pengajuan_tempo.invoice_pic_tittle else '', + 'invoice_pic_mobile' : pengajuan_tempo.invoice_pic_mobile if pengajuan_tempo.invoice_pic_mobile else '', 'invoice_pic' : pengajuan_tempo.invoice_pic if pengajuan_tempo.invoice_pic else '', 'street_invoice' : pengajuan_tempo.street_invoice if pengajuan_tempo.street_invoice else '', 'state_invoice' : pengajuan_tempo.state_id_invoice.id if pengajuan_tempo.state_id_invoice else '', @@ -82,6 +84,12 @@ class ResPartner(models.Model): 'dokumen_pengiriman_invoice' : pengajuan_tempo.dokumen_invoice if pengajuan_tempo.dokumen_invoice else '', 'is_same_addrees': pengajuan_tempo.is_same_address if pengajuan_tempo.is_same_address else False, 'is_same_addrees_street': pengajuan_tempo.is_same_address_street if pengajuan_tempo.is_same_address_street else False, + 'dokumen_prosedur': + { + 'name': pengajuan_tempo.dokumen_prosedur.name, + 'base64': pengajuan_tempo.dokumen_prosedur.datas.decode('utf-8'), + 'format': pengajuan_tempo.dokumen_prosedur.mimetype, + } if pengajuan_tempo.dokumen_prosedur else '', 'supplier_ids': [ { 'id': supplier.id, diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index a63d7c89..d93db041 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -120,6 +120,7 @@ 'views/sale_monitoring_detail_v2.xml', 'views/purchase_order_multi_update.xml', 'views/purchase_order_multi_confirm.xml', + 'views/purchase_order_multi_ask_approval.xml', 'views/invoice_reklas_penjualan.xml', 'views/po_multi_cancel.xml', 'views/logbook_sj.xml', @@ -144,6 +145,7 @@ 'views/sale_order_multi_uangmuka_penjualan.xml', 'views/shipment_group.xml', 'views/approval_date_doc.xml', + 'views/approval_invoice_date.xml', 'views/partner_payment_term.xml', 'views/vendor_payment_term.xml', 'views/approval_unreserve.xml', @@ -153,6 +155,9 @@ 'views/form_vendor_approval_multi_approve.xml', 'views/form_vendor_approval_multi_reject.xml', 'views/user_pengajuan_tempo.xml', + 'views/stock_backorder_confirmation_views.xml', + 'views/barcoding_product.xml', + 'views/project_views.xml', 'report/report.xml', 'report/report_banner_banner.xml', 'report/report_banner_banner2.xml', @@ -160,7 +165,10 @@ 'report/report_invoice.xml', 'report/report_picking.xml', 'report/report_sale_order.xml', + 'views/vendor_sla.xml', 'views/coretax_faktur.xml', + 'views/public_holiday.xml', + 'views/stock_inventory.xml', ], 'demo': [], 'css': [], 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/__init__.py b/indoteknik_custom/models/__init__.py index 714d29d3..e17f68d1 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -106,6 +106,7 @@ from . import sale_monitoring_detail_v2 from . import purchase_order_multi_update from . import invoice_reklas_penjualan from . import purchase_order_multi_confirm +from . import purchase_order_multi_ask_approval from . import po_multi_cancel from . import logbook_sj from . import report_logbook_sj @@ -138,8 +139,16 @@ from . import user_pengajuan_tempo from . import approval_retur_picking from . import va_multi_approve from . import va_multi_reject +from . import vendor_sla from . import stock_immediate_transfer from . import coretax_fatur +from . import public_holiday from . import ir_actions_report from . import user_form_merchant from . import user_merchant_request +from . import barcoding_product +from . import sales_order_koli +from . import stock_backorder_confirmation +from . import account_payment_register +from . import stock_inventory +from . import approval_invoice_date diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 85ed1d54..30de67be 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -64,14 +64,37 @@ class AccountMove(models.Model): nomor_kwitansi = fields.Char(string="Nomor Kwitansi") other_subtotal = fields.Float(string="Other Subtotal", compute='compute_other_subtotal') other_taxes = fields.Float(string="Other Taxes", compute='compute_other_taxes') + is_hr = fields.Boolean(string="Is HR?", default=False) + purchase_order_id = fields.Many2one('purchase.order', string='Purchase Order') + length_of_payment = fields.Integer(string="Length of Payment", compute='compute_length_of_payment') + + def compute_length_of_payment(self): + for rec in self: + payment_term = rec.invoice_payment_term_id.line_ids[0].days + terima_faktur = rec.date_terima_tukar_faktur + payment = self.search([('ref', '=', rec.name), ('move_type', '=', 'entry')], limit=1) + + if payment and terima_faktur: + date_diff = terima_faktur - payment.date + rec.length_of_payment = date_diff.days + payment_term + else: + rec.length_of_payment = 0 + + def _update_line_name_from_ref(self): + """Update all account.move.line name fields with ref from account.move""" + for move in self: + if move.move_type == 'entry' and move.ref and move.line_ids: + for line in move.line_ids: + line.name = move.ref def compute_other_taxes(self): for rec in self: - rec.other_taxes = rec.other_subtotal * 0.12 + rec.other_taxes = round(rec.other_subtotal * 0.12, 2) + def compute_other_subtotal(self): for rec in self: - rec.other_subtotal = rec.amount_untaxed * (11 / 12) + rec.other_subtotal = round(rec.amount_untaxed * (11 / 12)) @api.model def generate_attachment(self, record): @@ -107,6 +130,7 @@ class AccountMove(models.Model): def create(self, vals): vals['nomor_kwitansi'] = self.env['ir.sequence'].next_by_code('nomor.kwitansi') or '0' result = super(AccountMove, self).create(vals) + # result._update_line_name_from_ref() return result def compute_so_shipping_paid_by(self): diff --git a/indoteknik_custom/models/account_move_line.py b/indoteknik_custom/models/account_move_line.py index a4b25109..7c95d4ef 100644 --- a/indoteknik_custom/models/account_move_line.py +++ b/indoteknik_custom/models/account_move_line.py @@ -7,6 +7,7 @@ class AccountMoveLine(models.Model): cost_centre_id = fields.Many2one('cost.centre', string='Cost Centre') is_required = fields.Boolean(string='Is Required', compute='_compute_is_required') analytic_account_ids = fields.Many2many('account.analytic.account', string='Analytic Account') + line_no = fields.Integer('No', default=0) @api.onchange('account_id') def _onchange_account_id(self): @@ -22,3 +23,11 @@ class AccountMoveLine(models.Model): else: account.is_required = False + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if 'move_id' in vals: + move = self.env['account.move'].browse(vals['move_id']) + if move.move_type == 'entry' and move.is_hr == True: + vals['name'] = move.ref + return super().create(vals_list)
\ No newline at end of file diff --git a/indoteknik_custom/models/account_payment_register.py b/indoteknik_custom/models/account_payment_register.py new file mode 100644 index 00000000..4ebb7e4e --- /dev/null +++ b/indoteknik_custom/models/account_payment_register.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + + +class AccountPaymentRegister(models.TransientModel): + _inherit = 'account.payment.register' + def _create_payment_vals_from_wizard(self): + payment_vals = { + 'date': self.payment_date, + 'amount': self.amount, + 'payment_type': self.payment_type, + 'partner_type': self.partner_type, + 'ref': self.communication, + 'journal_id': self.journal_id.id, + 'is_hr': True, + 'currency_id': self.currency_id.id, + 'partner_id': self.partner_id.id, + 'partner_bank_id': self.partner_bank_id.id, + 'payment_method_id': self.payment_method_id.id, + 'destination_account_id': self.line_ids[0].account_id.id + } + + if not self.currency_id.is_zero(self.payment_difference) and self.payment_difference_handling == 'reconcile': + payment_vals['write_off_line_vals'] = { + 'name': self.writeoff_label, + 'amount': self.payment_difference, + 'account_id': self.writeoff_account_id.id, + } + return payment_vals + + def _create_payment_vals_from_batch(self, batch_result): + batch_values = self._get_wizard_values_from_batch(batch_result) + return { + 'date': self.payment_date, + 'amount': batch_values['source_amount_currency'], + 'payment_type': batch_values['payment_type'], + 'partner_type': batch_values['partner_type'], + 'ref': self._get_batch_communication(batch_result), + 'journal_id': self.journal_id.id, + 'is_hr': True, + 'currency_id': batch_values['source_currency_id'], + 'partner_id': batch_values['partner_id'], + 'partner_bank_id': batch_result['key_values']['partner_bank_id'], + 'payment_method_id': self.payment_method_id.id, + 'destination_account_id': batch_result['lines'][0].account_id.id + } diff --git a/indoteknik_custom/models/approval_date_doc.py b/indoteknik_custom/models/approval_date_doc.py index 751bae82..638b44d7 100644 --- a/indoteknik_custom/models/approval_date_doc.py +++ b/indoteknik_custom/models/approval_date_doc.py @@ -39,12 +39,16 @@ class ApprovalDateDoc(models.Model): if not self.env.user.is_accounting: raise UserError("Hanya Accounting Yang Bisa Approve") self.check_invoice_so_picking - self.picking_id.driver_departure_date = self.driver_departure_date - self.picking_id.date_doc_kirim = self.driver_departure_date + # Tambahkan context saat mengupdate date_doc_kirim + self.picking_id.with_context(from_button_approve=True).write({ + 'driver_departure_date': self.driver_departure_date, + 'date_doc_kirim': self.driver_departure_date, + 'update_date_doc_kirim_add': True + }) self.state = 'done' self.approve_date = datetime.utcnow() self.approve_by = self.env.user.id - + def button_cancel(self): self.state = 'cancel' diff --git a/indoteknik_custom/models/approval_invoice_date.py b/indoteknik_custom/models/approval_invoice_date.py new file mode 100644 index 00000000..48546e55 --- /dev/null +++ b/indoteknik_custom/models/approval_invoice_date.py @@ -0,0 +1,46 @@ +from odoo import models, api, fields +from odoo.exceptions import AccessError, UserError, ValidationError +from datetime import timedelta, date, datetime +import logging + +_logger = logging.getLogger(__name__) + +class ApprovalInvoiceDate(models.Model): + _name = "approval.invoice.date" + _description = "Approval Invoice Date" + _rec_name = 'number' + + picking_id = fields.Many2one('stock.picking', string='Picking') + number = fields.Char(string='Document No', index=True, copy=False, readonly=True, tracking=True) + date_invoice = fields.Datetime( + string='Invoice Date', + copy=False + ) + date_doc_do = fields.Datetime( + string='Tanggal Kirim di SJ', + copy=False + ) + state = fields.Selection([('draft', 'Draft'), ('done', 'Done'), ('cancel', 'Cancel')], string='State', default='draft', tracking=True) + approve_date = fields.Datetime(string='Approve Date', copy=False) + approve_by = fields.Many2one('res.users', string='Approve By', copy=False) + sale_id = fields.Many2one('sale.order', string='Sale Order') + partner_id = fields.Many2one('res.partner', string='Partner') + move_id = fields.Many2one('account.move', string='Invoice') + note = fields.Char(string='Note') + + def button_approve(self): + if not self.env.user.is_accounting: + raise UserError("Hanya Accounting Yang Bisa Approve") + self.move_id.invoice_date = self.date_doc_do + self.state = 'done' + self.approve_date = datetime.utcnow() + self.approve_by = self.env.user.id + + def button_cancel(self): + self.state = 'cancel' + + @api.model + def create(self, vals): + vals['number'] = self.env['ir.sequence'].next_by_code('approval.invoice.date') or '0' + result = super(ApprovalInvoiceDate, self).create(vals) + return result diff --git a/indoteknik_custom/models/automatic_purchase.py b/indoteknik_custom/models/automatic_purchase.py index 09d283eb..b66121e1 100644 --- a/indoteknik_custom/models/automatic_purchase.py +++ b/indoteknik_custom/models/automatic_purchase.py @@ -1,4 +1,4 @@ -from odoo import models, api, fields +from odoo import models, api, fields, tools from odoo.exceptions import UserError from datetime import datetime import logging, math @@ -67,6 +67,15 @@ class AutomaticPurchase(models.Model): if count > 0: raise UserError('Ada sekitar %s SO Yang sudah create PO, berikut SO nya: %s' % (count, ', '.join(names))) + + def unlink_note_pj(self): + product = self.purchase_lines.mapped('product_id') + pj_state = self.env['purchasing.job.state'].search([ + ('purchasing_job_id', 'in', product.ids) + ]) + + for line in pj_state: + line.unlink() def create_po_from_automatic_purchase(self): if not self.purchase_lines: @@ -75,6 +84,7 @@ class AutomaticPurchase(models.Model): raise UserError('Sudah pernah di create PO') current_time = datetime.now() + self.unlink_note_pj() vendor_ids = self.env['automatic.purchase.line'].read_group( [('automatic_purchase_id', '=', self.id), ('partner_id', '!=', False)], fields=['partner_id'], @@ -183,89 +193,94 @@ class AutomaticPurchase(models.Model): def create_po_by_vendor(self, vendor_id): current_time = datetime.now() - if not self.apo_type =='reordering': - name = "/PJ/" - else: - name = "/A/" + name = "/PJ/" if not self.apo_type == 'reordering' else "/A/" PRODUCT_PER_PO = 20 - auto_purchase_line = self.env['automatic.purchase.line'] - last_po = self.env['purchase.order'].search([ - ('partner_id', '=', vendor_id), - ('state', '=', 'done'), - ], order='id desc', limit=1) - - param_header = { - 'partner_id': vendor_id, - 'currency_id': 12, - 'user_id': self.env.user.id, - 'company_id': 1, # indoteknik dotcom gemilang - 'picking_type_id': 28, # indoteknik bandengan receipts - 'date_order': current_time, - 'from_apo': True, - 'note_description': 'Automatic PO' - } - + # Domain untuk semua baris dengan vendor_id tertentu domain = [ ('automatic_purchase_id', '=', self.id), ('partner_id', '=', vendor_id), ('qty_purchase', '>', 0) ] - products_len = auto_purchase_line.search_count(domain) - page = math.ceil(products_len / PRODUCT_PER_PO) - - # i start from zero (0) - for i in range(page): - new_po = self.env['purchase.order'].create([param_header]) - new_po.payment_term_id = new_po.partner_id.property_supplier_payment_term_id - new_po.name = new_po.name + name + str(i + 1) + # Tambahkan domain khusus untuk brand_id 22 dan 564 + special_brand_domain = domain + [('brand_id', 'in', [22, 564])] + regular_domain = domain + [('brand_id', 'not in', [22, 564])] + + # Fungsi untuk membuat PO berdasarkan domain tertentu + def create_po_for_domain(domain, special_payment_term=False): + products_len = auto_purchase_line.search_count(domain) + page = math.ceil(products_len / PRODUCT_PER_PO) + + for i in range(page): + # Buat PO baru + param_header = { + 'partner_id': vendor_id, + 'currency_id': 12, + 'user_id': self.env.user.id, + 'company_id': 1, # indoteknik dotcom gemilang + 'picking_type_id': 28, # indoteknik bandengan receipts + 'date_order': current_time, + 'from_apo': True, + 'note_description': 'Automatic PO' + } - self.env['automatic.purchase.match'].create([{ - 'automatic_purchase_id': self.id, - 'order_id': new_po.id - }]) + new_po = self.env['purchase.order'].create([param_header]) - lines = auto_purchase_line.search( - domain, - offset=i * PRODUCT_PER_PO, - limit=PRODUCT_PER_PO - ) + # Set payment_term_id khusus jika diperlukan + if special_payment_term: + new_po.payment_term_id = 29 + else: + new_po.payment_term_id = new_po.partner_id.property_supplier_payment_term_id - lines = auto_purchase_line.search( - domain, - offset=i * PRODUCT_PER_PO, - limit=PRODUCT_PER_PO - ) + new_po.name = new_po.name + name + str(i + 1) + self.env['automatic.purchase.match'].create([{ + 'automatic_purchase_id': self.id, + 'order_id': new_po.id + }]) + + # Ambil baris sesuai halaman + lines = auto_purchase_line.search( + domain, + offset=i * PRODUCT_PER_PO, + limit=PRODUCT_PER_PO + ) + + for line in lines: + product = line.product_id + sales_match = self.env['automatic.purchase.sales.match'].search([ + ('automatic_purchase_id', '=', self.id), + ('product_id', '=', product.id), + ]) + param_line = { + 'order_id': new_po.id, + 'product_id': product.id, + 'product_qty': line.qty_purchase, + 'qty_available_store': product.qty_available_bandengan, + 'suggest': product._get_po_suggest(line.qty_purchase), + 'product_uom_qty': line.qty_purchase, + 'price_unit': line.last_price, + 'ending_price': line.last_price, + 'taxes_id': [line.taxes_id.id] if line.taxes_id else None, + 'so_line_id': sales_match[0].sale_line_id.id if sales_match else None, + 'so_id': sales_match[0].sale_id.id if sales_match else None + } + new_po_line = self.env['purchase.order.line'].create([param_line]) + line.current_po_id = new_po.id + line.current_po_line_id = new_po_line.id + + self.create_purchase_order_sales_match(new_po) + + # Buat PO untuk special brand + if vendor_id == 23: + create_po_for_domain(special_brand_domain, special_payment_term=True) + + # Buat PO untuk regular domain + create_po_for_domain(regular_domain, "") - for line in lines: - product = line.product_id - sales_match = self.env['automatic.purchase.sales.match'].search([ - ('automatic_purchase_id', '=', self.id), - ('product_id', '=', product.id), - ]) - param_line = { - 'order_id': new_po.id, - 'product_id': product.id, - 'product_qty': line.qty_purchase, - 'qty_available_store': product.qty_available_bandengan, - 'suggest': product._get_po_suggest(line.qty_purchase), - 'product_uom_qty': line.qty_purchase, - 'price_unit': line.last_price, - 'ending_price': line.last_price, - 'taxes_id': [line.taxes_id.id] if line.taxes_id else None, - 'so_line_id': sales_match[0].sale_line_id.id if sales_match else None, - 'so_id': sales_match[0].sale_id.id if sales_match else None - } - new_po_line = self.env['purchase.order.line'].create([param_line]) - line.current_po_id = new_po.id - line.current_po_line_id = new_po_line.id - # self.update_purchase_price_so_line(line) - - self.create_purchase_order_sales_match(new_po) def update_purchase_price_so_line(self, apo): sales_match = self.env['automatic.purchase.sales.match'].search([ @@ -279,7 +294,7 @@ class AutomaticPurchase(models.Model): def create_purchase_order_sales_match(self, purchase_order): matches_so_product_ids = [line.product_id.id for line in purchase_order.order_line] - matches_so = self.env['automatic.purchase.sales.match'].search([ + matches_so = self.env['v.sale.notin.matchpo'].search([ ('automatic_purchase_id', '=', self.id), ('sale_line_id.product_id', 'in', matches_so_product_ids), ]) @@ -287,6 +302,8 @@ class AutomaticPurchase(models.Model): sale_ids_set = set() sale_ids_name = set() for sale_order in matches_so: + # @stephan skip so line yang sudah pernah ada di purchase order sales match sebelumnya + salesperson_name = sale_order.sale_id.user_id.name sale_id_with_salesperson = f"{sale_order.sale_id.name} - {salesperson_name}" @@ -650,3 +667,50 @@ class SyncPurchasingJob(models.Model): outgoing = fields.Float(string="Outgoing") action = fields.Char(string="Status") date = fields.Datetime(string="Date Sync") + + +class SaleNotInMatchPO(models.Model): + # created by @stephan for speed up performance while create po from automatic purchase + _name = 'v.sale.notin.matchpo' + _auto = False + _rec_name = 'id' + + id = fields.Integer() + automatic_purchase_id = fields.Many2one('automatic.purchase', string='APO') + automatic_purchase_line_id = fields.Many2one('automatic.purchase.line', string='APO Line') + sale_id = fields.Many2one('sale.order', string='Sale') + sale_line_id = fields.Many2one('sale.order.line', string='Sale Line') + picking_id = fields.Many2one('stock.picking', string='Picking') + move_id = fields.Many2one('stock.move', string='Move') + partner_id = fields.Many2one('res.partner', string='Partner') + partner_invoice_id = fields.Many2one('res.partner', string='Partner Invoice') + salesperson_id = fields.Many2one('res.user', string='Salesperson') + product_id = fields.Many2one('product.product', string='Product') + qty_so = fields.Float(string='Qty SO') + qty_po = fields.Float(string='Qty PO') + create_uid = fields.Many2one('res.user', string='Created By') + create_date = fields.Datetime(string='Create Date') + write_uid = fields.Many2one('res.user', string='Updated By') + write_date = fields.Many2one(string='Updated') + purchase_price = fields.Many2one(string='Purchase Price') + purchase_tax_id = fields.Many2one('account.tax', string='Purchase Tax') + note_procurement = fields.Many2one(string='Note Procurement') + + def init(self): + tools.drop_view_if_exists(self.env.cr, self._table) + self.env.cr.execute(""" + CREATE OR REPLACE VIEW %s AS( + select apsm.id, apsm.automatic_purchase_id, apsm.automatic_purchase_line_id, apsm.sale_id, apsm.sale_line_id, + apsm.picking_id, apsm.move_id, apsm.partner_id, + apsm.partner_invoice_id, apsm.salesperson_id, apsm.product_id, apsm.qty_so, apsm.qty_po, apsm.create_uid, + apsm.create_date, apsm.write_uid, apsm.write_date, apsm.purchase_price, + apsm.purchase_tax_id, apsm.note_procurement + from automatic_purchase_sales_match apsm + where apsm.sale_line_id not in ( + select distinct coalesce(posm.sale_line_id,0) + from purchase_order_sales_match posm + join purchase_order po on po.id = posm.purchase_order_id + where po.state not in ('cancel') + ) + ) + """ % self._table) diff --git a/indoteknik_custom/models/barcoding_product.py b/indoteknik_custom/models/barcoding_product.py new file mode 100644 index 00000000..204c6128 --- /dev/null +++ b/indoteknik_custom/models/barcoding_product.py @@ -0,0 +1,62 @@ +from odoo import models, api, fields +from odoo.exceptions import AccessError, UserError, ValidationError +from datetime import timedelta, date, datetime +import logging + +_logger = logging.getLogger(__name__) + +class BarcodingProduct(models.Model): + _name = "barcoding.product" + _description = "Barcoding Product" + + barcoding_product_line = fields.One2many('barcoding.product.line', 'barcoding_product_id', string='Barcoding Product Lines', auto_join=True) + product_id = fields.Many2one('product.product', string="Product", tracking=3) + quantity = fields.Float(string="Quantity", tracking=3) + type = fields.Selection([('print', 'Print Barcode'), ('barcoding', 'Add Barcode To Product'), ('barcoding_box', 'Add Barcode Box To Product')], string='Type', default='print') + barcode = fields.Char(string="Barcode") + qty_pcs_box = fields.Char(string="Quantity Pcs Box") + + def check_duplicate_barcode(self): + if self.type in ['barcoding_box', 'barcoding']: + barcode_product = self.env['product.product'].search([('barcode', '=', self.barcode)]) + + if barcode_product: + raise UserError('Barcode sudah digunakan {}'.format(barcode_product.display_name)) + + barcode_box = self.env['product.product'].search([('barcode_box', '=', self.barcode)]) + + if barcode_box: + raise UserError('Barcode box sudah digunakan {}'.format(barcode_box.display_name)) + + @api.constrains('barcode') + def _send_barcode_to_product(self): + for record in self: + record.check_duplicate_barcode() + if record.type == 'barcoding_box': + record.product_id.barcode_box = record.barcode + record.product_id.qty_pcs_box = record.qty_pcs_box + else: + record.product_id.barcode = record.barcode + + @api.onchange('product_id', 'quantity') + def _onchange_product_or_quantity(self): + """Update barcoding_product_line based on product_id and quantity""" + if self.product_id and self.quantity > 0: + # Clear existing lines + self.barcoding_product_line = [(5, 0, 0)] + + # Add a new line with the current product and quantity + self.barcoding_product_line = [(0, 0, { + 'product_id': self.product_id.id, + 'barcoding_product_id': self.id, + }) for _ in range(int(self.quantity))] + + +class BarcodingProductLine(models.Model): + _name = 'barcoding.product.line' + _description = 'Barcoding Product Line' + _order = 'barcoding_product_id, id' + + barcoding_product_id = fields.Many2one('barcoding.product', string='Barcoding Product Ref', required=True, ondelete='cascade', index=True, copy=False) + product_id = fields.Many2one('product.product', string="Product") + qr_code_variant = fields.Binary("QR Code Variant", related='product_id.qr_code_variant')
\ No newline at end of file diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py index 48f1c7f6..eeaa8efc 100644 --- a/indoteknik_custom/models/commision.py +++ b/indoteknik_custom/models/commision.py @@ -1,7 +1,10 @@ -from odoo import models, api, fields +from odoo import models, api, fields, _ from odoo.exceptions import UserError from datetime import datetime +# import datetime import logging +from terbilang import Terbilang +import pytz _logger = logging.getLogger(__name__) @@ -12,8 +15,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') @@ -35,7 +40,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) @@ -52,18 +57,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: @@ -74,13 +79,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: @@ -91,13 +96,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: @@ -108,13 +113,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-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: @@ -122,6 +127,22 @@ class CustomerRebate(models.Model): return sum_dpp +class RejectReasonCommision(models.TransientModel): + _name = 'reject.reason.commision' + _description = 'Wizard for Reject Reason Customer Commision' + + request_id = fields.Many2one('customer.commision', string='Request') + reason_reject = fields.Text(string='Reason for Rejection', required=True, tracking=True) + + def confirm_reject(self): + commision = self.request_id + if commision: + commision.last_status = commision.status + commision.write({'reason_reject': self.reason_reject}) + commision.status = 'reject' + return {'type': 'ir.actions.act_window_close'} + + class CustomerCommision(models.Model): _name = 'customer.commision' _order = 'id desc' @@ -134,25 +155,132 @@ 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([ - ('pengajuan1', 'Menunggu Approval Marketing'), - ('pengajuan2', 'Menunggu Approval Pimpinan'), - ('approved', 'Approved') - ], string='Status', copy=False, readonly=True, tracking=3) + ('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') + last_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') commision_percent = fields.Float(string='Commision %', tracking=3) commision_amt = fields.Float(string='Commision Amount', tracking=3) + commision_amt_text = fields.Char(string='Commision Amount Text', compute='compute_delivery_amt_text') total_dpp = fields.Float(string='Total DPP', compute='_compute_total_dpp') commision_type = fields.Selection([ ('fee', 'Fee'), ('cashback', 'Cashback'), ('rebate', 'Rebate'), ], string='Commision Type', required=True) - bank_name = fields.Char(string='Bank', tracking=3) - account_name = fields.Char(string='Account Name', tracking=3) - bank_account = fields.Char(string='Account No', tracking=3) + bank_name = fields.Char(string='Bank', tracking=3, required=True) + account_name = fields.Char(string='Account Name', tracking=3, required=True) + bank_account = fields.Char(string='Account No', tracking=3, required=True) note_transfer = fields.Char(string='Keterangan') brand_ids = fields.Many2many('x_manufactures', string='Brands') + payment_status = fields.Selection([ + ('pending', 'Pending'), + ('payment', 'Payment'), + ], string='Payment Status', copy=False, readonly=True, tracking=3, default='pending') + note_finnance = fields.Text('Notes Finnance') + reason_reject = fields.Char(string='Reason Reaject', tracking=True, track_visibility='onchange') + approved_by = fields.Char(string='Approved By', tracking=True, track_visibility='always') + + 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, 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) + date_approved_pimpinan = fields.Datetime(string="Date Approved Pimpinan", tracking=True) + date_approved_accounting = fields.Datetime(string="Date Approved Accounting", tracking=True) + + position_sales = fields.Char(string="Position Sales", tracking=True) + position_marketing = fields.Char(string="Position Marketing", tracking=True) + position_pimpinan = fields.Char(string="Position Pimpinan", tracking=True) + position_accounting = fields.Char(string="Position Accounting", tracking=True) + + # get partner ids so it can be grouped by + @api.model + def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): + if 'partner_ids' in groupby: + # Get all records matching the domain + records = self.search(domain) + + # Create groups for each partner + groups = {} + for record in records: + for partner in record.partner_ids: + if partner.id not in groups: + groups[partner.id] = { + 'partner_ids': partner, + 'records': self.env['customer.commision'] + } + groups[partner.id]['records'] |= record + + # Format the result + result = [] + for partner_id, group_data in groups.items(): + partner = group_data['partner_ids'] + record_ids = group_data['records'].ids + + # Create the domain + group_domain = [('id', 'in', record_ids)] + if domain: + group_domain = ['&'] + domain + group_domain + + result.append({ + 'partner_ids': (partner.id, partner.display_name), + 'partner_ids_count': len(record_ids), + '__domain': group_domain, + '__count': len(record_ids), + }) + + return result + + return super(CustomerCommision, self).read_group(domain, fields, groupby, offset, limit, orderby, lazy) + + def compute_delivery_amt_text(self): + tb = Terbilang() + + for record in self: + res = '' + + try: + if record.commision_amt > 0: + tb.parse(int(record.commision_amt)) + res = tb.getresult().title() + record.commision_amt_text = res + ' Rupiah' + except: + record.commision_amt_text = res + + def _compute_grouped_numbers(self): + for rec in self: + so_numbers = set() + invoice_numbers = set() + + for line in rec.commision_lines: + 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? @@ -178,22 +306,25 @@ 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): + """ + Constrain to update commision percent from commision amount + """ 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 @@ -215,23 +346,72 @@ class CustomerCommision(models.Model): result = super(CustomerCommision, self).create(vals) return result - def action_confirm_customer_commision(self):#add 2 step approval - if not self.status: + def action_confirm_customer_commision(self): + jakarta_tz = pytz.timezone('Asia/Jakarta') + now = datetime.now(jakarta_tz) + + now_naive = now.replace(tzinfo=None) + + if not self.status or self.status == 'draft': self.status = 'pengajuan1' - elif self.status == 'pengajuan1' and self.env.user.id == 19: + elif self.status == 'pengajuan1' and self.env.user.is_sales_manager: self.status = 'pengajuan2' - elif self.status == 'pengajuan2' and self.env.user.is_leader: + self.approved_by = (self.approved_by + ', ' if self.approved_by else '') + self.env.user.name + self.date_approved_sales = now_naive + self.position_sales = 'Sales Manager' + elif self.status == 'pengajuan2' and self.env.user.id == 19: + self.status = 'pengajuan3' + self.approved_by = (self.approved_by + ', ' if self.approved_by else '') + self.env.user.name + self.date_approved_marketing = now_naive + self.position_marketing = 'Marketing Manager' + elif self.status == 'pengajuan3' and self.env.user.is_leader: + self.status = 'pengajuan4' + self.approved_by = (self.approved_by + ', ' if self.approved_by else '') + self.env.user.name + self.date_approved_pimpinan = now_naive + self.position_pimpinan = 'Pimpinan' + elif self.status == 'pengajuan4' and self.env.user.id == 1272: for line in self.commision_lines: line.invoice_id.is_customer_commision = True self.status = 'approved' + self.approved_by = (self.approved_by + ', ' if self.approved_by else '') + self.env.user.name + self.date_approved_accounting = now_naive + self.position_accounting = 'Accounting' else: raise UserError('Harus di approved oleh yang bersangkutan') return + def action_reject(self): # add 2 step approval + return { + 'type': 'ir.actions.act_window', + 'name': _('Reject Reason'), + 'res_model': 'reject.reason.commision', + 'view_mode': 'form', + 'target': 'new', + 'context': {'default_request_id': self.id}, + } + + def button_draft(self): + for commision in self: + commision.status = commision.last_status if commision.last_status else 'draft' + + def action_confirm_customer_payment(self): + if self.status != 'approved': + raise UserError('Commision harus di approve terlebih dahulu sebelum di konfirmasi pembayarannya') + + if self.payment_status == 'payment': + raise UserError('Customer Commision sudah berstatus Payment') + group_id = self.env.ref('indoteknik_custom.group_role_fat').id + users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) + if self.env.user.id not in users_in_group.mapped('id'): + raise UserError('Hanya bisa dikonfirmasi oleh FAT') + else: + self.payment_status = 'payment' + return + 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: @@ -306,11 +486,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') @@ -318,7 +500,11 @@ 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') 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' diff --git a/indoteknik_custom/models/coretax_fatur.py b/indoteknik_custom/models/coretax_fatur.py index ae6dd2ae..b4bffbd2 100644 --- a/indoteknik_custom/models/coretax_fatur.py +++ b/indoteknik_custom/models/coretax_fatur.py @@ -59,6 +59,7 @@ class CoretaxFaktur(models.Model): ET.SubElement(tax_invoice, 'TrxCode').text = '04' ET.SubElement(tax_invoice, 'AddInfo') ET.SubElement(tax_invoice, 'CustomDoc') + ET.SubElement(tax_invoice, 'CustomDocMonthYear') ET.SubElement(tax_invoice, 'RefDesc').text = invoice.name ET.SubElement(tax_invoice, 'FacilityStamp') ET.SubElement(tax_invoice, 'SellerIDTKU').text = '0742260227086000000000' @@ -77,7 +78,7 @@ class CoretaxFaktur(models.Model): otherTaxBase = round(line.price_subtotal * (11/12)) if line.price_subtotal else 0 good_service = ET.SubElement(list_of_good_service, 'GoodService') ET.SubElement(good_service, 'Opt').text = 'A' - ET.SubElement(good_service, 'Code') + ET.SubElement(good_service, 'Code').text = '000000' ET.SubElement(good_service, 'Name').text = line.name ET.SubElement(good_service, 'Unit').text = 'UM.0018' ET.SubElement(good_service, 'Price').text = str(round(line.price_subtotal/line.quantity, 2)) if line.price_subtotal else '0' diff --git a/indoteknik_custom/models/delivery_order.py b/indoteknik_custom/models/delivery_order.py index 3473197b..2dd0c802 100644 --- a/indoteknik_custom/models/delivery_order.py +++ b/indoteknik_custom/models/delivery_order.py @@ -25,7 +25,8 @@ class DeliveryOrder(models.TransientModel): picking = False if delivery_order_line[2]['name']: picking = self.env['stock.picking'].search([('picking_code', '=', delivery_order_line[2]['name'])], limit=1) - + if not picking: + picking = self.env['stock.picking'].search([('out_code', '=', delivery_order_line[2]['name'])], limit=1) if picking: line_tracking_no = delivery_order_line[2]['tracking_no'] @@ -86,6 +87,10 @@ class DeliveryOrderLine(models.TransientModel): if len(self.name) == 13: self.name = self.name[:-1] picking = self.env['stock.picking'].search([('picking_code', '=', self.name)], limit=1) + + if not picking: + picking = self.env['stock.picking'].search([('out_code', '=', self.name)], limit=1) + if picking: if picking.driver_id: self.driver_id = picking.driver_id diff --git a/indoteknik_custom/models/invoice_reklas.py b/indoteknik_custom/models/invoice_reklas.py index 30da02d1..d10d4c31 100644 --- a/indoteknik_custom/models/invoice_reklas.py +++ b/indoteknik_custom/models/invoice_reklas.py @@ -18,6 +18,12 @@ class InvoiceReklas(models.TransientModel): ('pembelian', 'Pembelian'), ], string='Reklas Tipe') + @api.onchange('reklas_type') + def _onchange_reklas_type(self): + if self.reklas_type == 'penjualan': + invoices = self.env['account.move'].browse(self._context.get('active_ids', [])) + self.pay_amt = invoices.amount_total + def create_reklas(self): if not self.reklas_type: raise UserError('Reklas Tipe harus diisi') @@ -49,7 +55,7 @@ class InvoiceReklas(models.TransientModel): if self.reklas_type == 'penjualan': parameter_debit = { 'move_id': account_move.id, - 'account_id': 449, # uang muka penjualan + 'account_id': 668, # penerimaan belum alokasi 'partner_id': invoice.partner_id.id, 'currency_id': 12, 'debit': self.pay_amt, @@ -77,7 +83,7 @@ class InvoiceReklas(models.TransientModel): } parameter_credit = { 'move_id': account_move.id, - 'account_id': 401, + 'account_id': 669, 'partner_id': invoice.partner_id.id, 'currency_id': 12, 'debit': 0, diff --git a/indoteknik_custom/models/invoice_reklas_penjualan.py b/indoteknik_custom/models/invoice_reklas_penjualan.py index 5027c8af..80c3ed43 100644 --- a/indoteknik_custom/models/invoice_reklas_penjualan.py +++ b/indoteknik_custom/models/invoice_reklas_penjualan.py @@ -33,7 +33,7 @@ class InvoiceReklasPenjualan(models.TransientModel): parameter_debit = { 'move_id': account_move.id, - 'account_id': 449, # uang muka penjualan + 'account_id': 668, # uang muka penjualan 'partner_id': invoice.partner_id.id, 'currency_id': 12, 'debit': invoice.pay_amt, diff --git a/indoteknik_custom/models/ir_actions_report.py b/indoteknik_custom/models/ir_actions_report.py index 28adcf74..83636945 100644 --- a/indoteknik_custom/models/ir_actions_report.py +++ b/indoteknik_custom/models/ir_actions_report.py @@ -17,13 +17,13 @@ class IrActionsReport(models.Model): return # ci_vita 7751529082:AAE9XsZa_Pj2Pi2IN1grX98WkwTaIt32pbI & 5081411103 # iman 7094158106:AAHpWtYOMnA3Yqm_Fvrr3Vw7MrB45vLV9AY & 6592318498 - # bot_name_iman = '7094158106:AAHpWtYOMnA3Yqm_Fvrr3Vw7MrB45vLV9AY' - # chat_id_iman = '6592318498' + bot_name_iman = '7094158106:AAHpWtYOMnA3Yqm_Fvrr3Vw7MrB45vLV9AY' + chat_id_iman = '-1002493002821' bot_name_vita = '7751529082:AAE9XsZa_Pj2Pi2IN1grX98WkwTaIt32pbI' chat_id_vita = '5081411103' - apiURL = f'https://api.telegram.org/bot{bot_name_vita}/sendMessage' + apiURL = f'https://api.telegram.org/bot{bot_name_iman}/sendMessage' try: - requests.post(apiURL, json={'chat_id': chat_id_vita, 'text': sale_order.name + " senilai Rp" + self.format_currency(sale_order.amount_total) + ' untuk customer ' + sale_order.partner_id.name + ' telah dibuat oleh sales ' +sale_order.user_id.name}) + requests.post(apiURL, json={'chat_id': chat_id_iman, 'text': sale_order.name + " senilai Rp" + self.format_currency(sale_order.amount_total) + ' untuk customer ' + sale_order.partner_id.name + ' telah dibuat oleh sales ' +sale_order.user_id.name}) except Exception as e: print(e) diff --git a/indoteknik_custom/models/logbook_sj.py b/indoteknik_custom/models/logbook_sj.py index 9f349882..75b2622f 100644 --- a/indoteknik_custom/models/logbook_sj.py +++ b/indoteknik_custom/models/logbook_sj.py @@ -26,6 +26,8 @@ class LogbookSJ(models.TransientModel): report_logbook = self.env['report.logbook.sj'].create([parameters_header]) for line in logbook_line: picking = self.env['stock.picking'].search([('picking_code', '=', line.name)], limit=1) + if not picking: + picking = self.env['stock.picking'].search([('out_code', '=', line.name)], limit=1) stock = picking parent_id = stock.partner_id.parent_id.id parent_id = parent_id if parent_id else stock.partner_id.id @@ -80,6 +82,9 @@ class LogbookSJLine(models.TransientModel): if len(self.name) == 13: self.name = self.name[:-1] picking = self.env['stock.picking'].search([('picking_code', '=', self.name)], limit=1) + + if not picking: + picking = self.env['stock.picking'].search([('out_code', '=', self.name)], limit=1) if picking: if picking.driver_id: self.driver_id = picking.driver_id 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 54d90256..8179fe56 100644 --- a/indoteknik_custom/models/mrp_production.py +++ b/indoteknik_custom/models/mrp_production.py @@ -1,10 +1,484 @@ -from odoo import fields, models, api, _ +from odoo import models, fields, api, tools, _ +from datetime import datetime, timedelta +import math +import logging 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', 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'.""" + if self._name != 'mrp.production': + return super(MrpProduction, self).action_confirm() + + 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) + + return result + + + def create_po_from_manufacturing(self): + if not self.state == 'confirmed': + raise UserError('Harus Di Approve oleh Merchandiser') + + if self.is_po == True: + raise UserError('Sudah pernah di buat PO') + + if not self.move_raw_ids: + raise UserError('Tidak ada Lines, belum bisa create PO') + # if self.is_po: + # raise UserError('Sudah pernah di create PO') + + vendor_ids = self.env['stock.move'].read_group([ + ('raw_material_production_id', '=', self.id), + ('vendor_id', '!=', False) + ], fields=['vendor_id'], groupby=['vendor_id']) + + po_ids = [] + for vendor in vendor_ids: + result_po = self.create_po_by_vendor(vendor['vendor_id'][0]) + po_ids += result_po + return { + 'name': _('Purchase Order'), + 'view_mode': 'tree,form', + 'res_model': 'purchase.order', + 'target': 'current', + 'type': 'ir.actions.act_window', + 'domain': [('id', 'in', po_ids)], + } -
\ No newline at end of file + + def create_po_by_vendor(self, vendor_id): + current_time = datetime.now() + + PRODUCT_PER_PO = 20 + + stock_move = self.env['stock.move'] + + param_header = { + 'partner_id': vendor_id, + # 'partner_ref': self.sale_order_id.name, + 'currency_id': 12, + 'user_id': self.env.user.id, + 'company_id': 1, # indoteknik dotcom gemilang + 'picking_type_id': 28, # indoteknik bandengan receipts + 'date_order': current_time, + 'product_bom_id': self.product_id.id, + # 'sale_order_id': self.sale_order_id.id, + 'note_description': 'from Manufacturing Order' + } + + domain = [ + ('raw_material_production_id', '=', self.id), + ('vendor_id', '=', vendor_id), + ('state', 'in', ['waiting','confirmed','partially_available']) + ] + + products_len = stock_move.search_count(domain) + page = math.ceil(products_len / PRODUCT_PER_PO) + po_ids = [] + # i start from zero (0) + for i in range(page): + new_po = self.env['purchase.order'].create([param_header]) + new_po.name = new_po.name + "/MO/" + str(i + 1) + po_ids.append(new_po.id) + lines = stock_move.search( + domain, + offset=i * PRODUCT_PER_PO, + limit=PRODUCT_PER_PO + ) + tax = [22] + + for line in lines: + product = line.product_id + price, taxes, vendor = self._get_purchase_price(product) + + param_line = { + 'order_id' : new_po.id, + 'product_id': product.id, + 'product_qty': line.product_uom_qty if line.state in ['confirmed', 'waiting'] else line.product_uom_qty - line.forecast_availability, + 'product_uom_qty': line.product_uom_qty if line.state in ['confirmed', 'waiting'] else line.product_uom_qty - line.forecast_availability, + 'name': product.display_name, + 'price_unit': price if price else 0.0, + 'taxes_id': [taxes] if taxes else [], + } + new_po_line = self.env['purchase.order.line'].create([param_line]) + + self.env['production.purchase.match'].create([{ + 'production_id': self.id, + 'order_id': new_po.id + }]) + + self.is_po = True + + return po_ids + + def _get_purchase_price(self, product_id): + override_vendor = product_id.x_manufacture.override_vendor_id + query = [('product_id', '=', product_id.id), + ('vendor_id', '=', override_vendor.id)] + purchase_price = self.env['purchase.pricelist'].search(query, limit=1) + if purchase_price: + return self._get_valid_purchase_price(purchase_price) + else: + purchase_price = self.env['purchase.pricelist'].search( + [('product_id', '=', product_id.id), + ('is_winner', '=', True)], + limit=1) + + return self._get_valid_purchase_price(purchase_price) + + def _get_valid_purchase_price(self, purchase_price): + current_time = datetime.now() + delta_time = current_time - timedelta(days=365) + # delta_time = delta_time.strftime('%Y-%m-%d %H:%M:%S') + + price = 0 + taxes = '' + vendor_id = '' + human_last_update = purchase_price.human_last_update or datetime.min + system_last_update = purchase_price.system_last_update or datetime.min + + if purchase_price.taxes_product_id.type_tax_use == 'purchase': + price = purchase_price.product_price + taxes = purchase_price.taxes_product_id.id + vendor_id = purchase_price.vendor_id.id + if delta_time > human_last_update: + price = 0 + taxes = '' + vendor_id = '' + + if system_last_update > human_last_update: + if purchase_price.taxes_system_id.type_tax_use == 'purchase': + price = purchase_price.system_price + taxes = purchase_price.taxes_system_id.id + vendor_id = purchase_price.vendor_id.id + if delta_time > system_last_update: + price = 0 + taxes = '' + vendor_id = '' + + 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' + _order = 'production_id, id' + + production_id = fields.Many2one('mrp.production', string='Ref', required=True, ondelete='cascade', index=True, copy=False) + order_id = fields.Many2one('purchase.order', string='Purchase Order') + vendor = fields.Char(string='Vendor', compute='_compute_info_po') + total = fields.Float(string='Total', compute='_compute_info_po') + + def _compute_info_po(self): + for match in self: + match.vendor = match.order_id.partner_id.name + match.total = match.order_id.amount_total + 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_sla.py b/indoteknik_custom/models/product_sla.py index 2e663d30..04ad2ffd 100644 --- a/indoteknik_custom/models/product_sla.py +++ b/indoteknik_custom/models/product_sla.py @@ -12,73 +12,110 @@ class ProductSla(models.Model): _rec_name = 'product_variant_id' product_variant_id = fields.Many2one('product.product',string='Product') - avg_leadtime = fields.Char(string='AVG Leadtime', readonly=True) - leadtime = fields.Char(string='Leadtime', readonly=True) + sla_vendor_id = fields.Many2one('vendor.sla',string='Vendor', readonly=True) + sla_vendor_duration = fields.Char(string='AVG Leadtime', related='sla_vendor_id.duration_unit') + sla_logistic = fields.Char(string='SLA Logistic', readonly=True) + sla_logistic_unit = fields.Selection( + [('jam', 'Jam'),('hari', 'Hari')], + string="SLA Logistic Time" + ) + sla_logistic_duration_unit = fields.Char(string="SLA Logistic Duration (Unit)") sla = fields.Char(string='SLA', readonly=True) version = fields.Integer(string="Version", compute="_compute_version") def _compute_version(self): for sla in self: sla.version = sla.product_variant_id.sla_version + + - def generate_product_variant_id_sla(self, limit=5000): - # Filter produk non-Altama + def generate_product_variant_id_sla(self, limit=500): + offset = 0 + # while True: products = self.env['product.product'].search([ - ('x_manufacture', 'not in', [10, 122, 89]), - ('location_id', '=', 57), - ('stock_move_ids', '!=', False), - ], order='sla_version asc', limit=limit) + ('active', '=', True), + ('sale_ok', '=', True), + ], order='sla_version asc', limit=limit, offset=offset) + + # if not products: + # break - i = 1 for product in products: - _logger.info(f'Product SLA: {i}/{len(products)}') - i += 1 - product.sla_version += 1 + _logger.info(f'Memproses SLA untuk produk ID {product.id}, versi {product.sla_version}') product_sla = self.search([('product_variant_id', '=', product.id)], limit=1) if not product_sla: - product_sla = self.env['product.sla'].create({ - 'product_variant_id': product.id, - }) - + product_sla = self.create({'product_variant_id': product.id}) + product_sla.generate_product_sla() + # Tandai produk sebagai sudah diproses + product.sla_version += 1 + + offset += limit + + def generate_product_sla(self): - self.avg_leadtime = '-' - self.sla = '-' + # self.sla_logistic = '-' + # self.sla_logistic_duration_unit = '-' + # self.sla = '-' product = self.product_variant_id - - qty_available = 0 - qty_available = product.qty_onhand_bandengan + + q_vendor = [ + ('product_id', '=', product.id), + ('is_winner', '=', True) + ] + + vendor = self.env['purchase.pricelist'].search(q_vendor) + vendor_duration = 0 - if qty_available > 0: - self.sla = '1 Hari' + #SLA Vendor + if vendor: + vendor_sla = self.env['vendor.sla'].search([('id_vendor', '=', vendor.vendor_id.id)], limit=1) + sla_vendor = int(vendor_sla.duration) if vendor_sla else 0 + if sla_vendor > 0: + if vendor_sla.unit == 'hari': + vendor_duration = vendor_sla.duration * 24 * 60 + else : + vendor_duration = vendor_sla.duration * 60 + + self.sla_vendor_id = vendor_sla.id if vendor_sla else False + #SLA Logistik selalu 1 hari + estimation_sla = (1 * 24 * 60) + vendor_duration + estimation_sla_days = estimation_sla / (24 * 60) + self.sla = math.ceil(estimation_sla_days) + self.sla_logistic = int(1) + self.sla_logistic_unit = 'hari' + self.sla_logistic_duration_unit = '1 hari' + else: + self.unlink() + else: + self.unlink() + - query = [ - ('product_id', '=', product.id), - ('picking_id', '!=', False), - ('picking_id.location_id', '=', 57), - ('picking_id.state', 'not in', ['cancel']) - ] - picking = self.env['stock.move.line'].search(query) - leadtimes=[] - for stock in picking: - date_delivered = stock.picking_id.driver_departure_date - date_so_confirmed = stock.picking_id.sale_id.date_order - if date_delivered and date_so_confirmed: - leadtime = date_delivered - date_so_confirmed - leadtime_in_days = leadtime.days - leadtimes.append(leadtime_in_days) + # query = [ + # ('product_id', '=', product.id), + # ('picking_id', '!=', False), + # ('picking_id.location_id', '=', 57), + # ('picking_id.state', 'not in', ['cancel']) + # ] + # picking = self.env['stock.move.line'].search(query) + + # leadtimes=[] + # for stock in picking: + # date_delivered = stock.picking_id.driver_departure_date + # date_do_ready = stock.picking_id.date_reserved + # if date_delivered and date_do_ready: + # leadtime = date_delivered - date_do_ready + # leadtime_in_days = leadtime.days + # leadtimes.append(leadtime_in_days) - if len(leadtimes) > 0: - avg_leadtime = sum(leadtimes) / len(leadtimes) - rounded_leadtime = math.ceil(avg_leadtime) - self.avg_leadtime = rounded_leadtime - if rounded_leadtime >= 1 and rounded_leadtime <= 5: - self.sla = '3-7 Hari' - elif rounded_leadtime >= 6 and rounded_leadtime <= 10: - self.sla = '4-12 Hari' - elif rounded_leadtime >= 11: - self.sla = 'Indent'
\ No newline at end of file + # if len(leadtimes) > 0: + # avg_leadtime = sum(leadtimes) / len(leadtimes) + # rounded_leadtime = math.ceil(avg_leadtime) + # estimation_sla = ((rounded_leadtime * 24 * 60) + vendor_duration)/2 + # estimation_sla_days = estimation_sla / (24 * 60) + # self.sla = math.ceil(estimation_sla_days) + # self.avg_leadtime = int(rounded_leadtime)
\ No newline at end of file diff --git a/indoteknik_custom/models/product_template.py b/indoteknik_custom/models/product_template.py index 5bedae13..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", @@ -67,6 +75,25 @@ class ProductTemplate(models.Model): print_barcode = fields.Boolean(string='Print Barcode', default=True) # qr_code = fields.Binary("QR Code", compute='_compute_qr_code') + @api.model + def create(self, vals): + group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id + users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) + active_model = self.env.context.get('active_model') + if self.env.user.id not in users_in_group.mapped('id') and active_model == None: + raise UserError('Hanya MD yang bisa membuat Product') + result = super(ProductTemplate, self).create(vals) + return result + + # def write(self, values): + # group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id + # users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) + # active_model = self.env.context.get('active_model') + # if self.env.user.id not in users_in_group.mapped('id') and active_model == None: + # raise UserError('Hanya MD yang bisa mengedit Product') + # result = super(ProductTemplate, self).write(values) + # return result + # def _compute_qr_code(self): # for rec in self.product_variant_ids: # qr = qrcode.QRCode( @@ -227,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: @@ -402,6 +429,39 @@ class ProductProduct(models.Model): plafon_qty = fields.Float(string='Max Plafon', compute='_get_plafon_qty_product') merchandise_ok = fields.Boolean(string='Product Promotion') qr_code_variant = fields.Binary("QR Code Variant", compute='_compute_qr_code_variant') + qty_pcs_box = fields.Float("Pcs Box") + barcode_box = fields.Char("Barcode Box") + + def generate_product_sla(self): + product_variant_ids = self.env.context.get('active_ids', []) + product_variant = self.search([('id', 'in', product_variant_ids)]) + sla_record = self.env['product.sla'].search([('product_variant_id', '=', product_variant.id)], limit=1) + + if sla_record: + sla_record.generate_product_sla() + else: + new_sla_record = self.env['product.sla'].create({ + 'product_variant_id': product_variant.id, + }) + new_sla_record.generate_product_sla() + @api.model + def create(self, vals): + group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id + active_model = self.env.context.get('active_model') + users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) + if self.env.user.id not in users_in_group.mapped('id') and active_model == None: + raise UserError('Hanya MD yang bisa membuat Product') + result = super(ProductProduct, self).create(vals) + return result + + # def write(self, values): + # group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id + # active_model = self.env.context.get('active_model') + # users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) + # if self.env.user.id not in users_in_group.mapped('id') and active_model == None: + # raise UserError('Hanya MD yang bisa mengedit Product') + # result = super(ProductProduct, self).write(values) + # return result def _compute_qr_code_variant(self): for rec in self: @@ -416,7 +476,7 @@ class ProductProduct(models.Model): box_size=5, border=4, ) - qr.add_data(rec.default_code) + qr.add_data(rec.barcode if rec.barcode else rec.default_code) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") @@ -682,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/public_holiday.py b/indoteknik_custom/models/public_holiday.py new file mode 100644 index 00000000..851d9080 --- /dev/null +++ b/indoteknik_custom/models/public_holiday.py @@ -0,0 +1,11 @@ +from odoo import api, fields, models +from datetime import timedelta, datetime + +class PublicHoliday(models.Model): + _name = 'hr.public.holiday' + _description = 'Public Holidays' + + name = fields.Char(string='Holiday Name', required=True) + start_date = fields.Date('Date Holiday', required=True) + # end_date = fields.Date('End Holiday Date', required=True) + # company_id = fields.Many2one('res.company', 'Company') diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index d487ada3..98b367d0 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -74,6 +74,7 @@ class PurchaseOrder(models.Model): date_done_picking = fields.Datetime(string='Date Done Picking', compute='get_date_done') bills_dp_id = fields.Many2one('account.move', string='Bills DP') bills_pelunasan_id = fields.Many2one('account.move', string='Bills Pelunasan') + product_bom_id = fields.Many2one('product.product', string='Product Bom') grand_total = fields.Monetary(string='Grand Total', help='Amount total + amount delivery', compute='_compute_grand_total') total_margin_match = fields.Float(string='Total Margin Match', compute='_compute_total_margin_match') approve_by = fields.Many2one('res.users', string='Approve By') @@ -86,6 +87,113 @@ class PurchaseOrder(models.Model): total_cost_service = fields.Float(string='Total Cost Service') total_delivery_amt = fields.Float(string='Total Delivery Amt') store_name = fields.Char(string='Nama Toko') + purchase_order_count = fields.Integer('Purchase Order Count', related='partner_id.purchase_order_count') + + # cek payment term + def _check_payment_term(self): + _logger.info("Check Payment Term Terpanggil") + + cbd_term = self.env['account.payment.term'].search([ + ('name', 'ilike', 'Cash Before Delivery') + ], limit=1) + + for order in self: + if not order.partner_id or not order.partner_id.minimum_amount: + continue + + if not order.order_line or order.amount_total == 0: + continue + + if order.amount_total < order.partner_id.minimum_amount: + if cbd_term and order.payment_term_id != cbd_term: + order.payment_term_id = cbd_term.id + self.env.user.notify_info( + message="Total belanja PO belum mencapai minimum yang ditentukan vendor. " + "Payment Term telah otomatis diubah menjadi Cash Before Delivery (C.B.D).", + title="Payment Term Diperbarui" + ) + else: + vendor_term = order.partner_id.property_supplier_payment_term_id + if vendor_term and order.payment_term_id != vendor_term: + order.payment_term_id = vendor_term.id + self.env.user.notify_info( + message=f"Total belanja PO telah memenuhi jumlah minimum vendor. " + f"Payment Term otomatis dikembalikan ke pengaturan vendor awal: *{vendor_term.name}*.", + title="Payment Term Diperbarui" + ) + + def _check_tax_rule(self): + _logger.info("Check Tax Rule Terpanggil") + + # Pajak 11% + tax_11 = self.env['account.tax'].search([ + ('type_tax_use', '=', 'purchase'), + ('name', 'ilike', '11%') + ], limit=1) + + # Pajak "No Tax" + no_tax = self.env['account.tax'].search([ + ('type_tax_use', '=', 'purchase'), + ('name', 'ilike', 'no tax') + ], limit=1) + + if not tax_11: + raise UserError("Pajak 11% tidak ditemukan. Mohon pastikan pajak 11% tersedia.") + + if not no_tax: + raise UserError("Pajak 'No Tax' tidak ditemukan. Harap buat tax dengan nama 'No Tax' dan tipe 'Purchase'.") + + for order in self: + partner = order.partner_id + minimum_tax = partner.minimum_amount_tax + + _logger.info("Partner ID: %s, Minimum Tax: %s, Untaxed Total: %s", partner.id, minimum_tax, order.amount_untaxed) + + if not minimum_tax or not order.order_line: + continue + + if order.amount_total < minimum_tax: + _logger.info(">>> Total di bawah minimum → apply No Tax") + for line in order.order_line: + line.taxes_id = [(6, 0, [no_tax.id])] + + if self.env.context.get('notify_tax'): + self.env.user.notify_info( + message="Total belanja PO belum mencapai minimum pajak vendor. " + "Pajak diganti menjadi 'No Tax'.", + title="Pajak Diperbarui", + ) + else: + _logger.info(">>> Total memenuhi minimum → apply Pajak 11%") + for line in order.order_line: + line.taxes_id = [(6, 0, [tax_11.id])] + + if self.env.context.get('notify_tax'): + self.env.user.notify_info( + message="Total belanja sebelum pajak telah memenuhi minimum. " + "Pajak 11%% diterapkan", + title="Pajak Diperbarui", + ) + + # set default no_tax pada order line + # @api.onchange('order_line') + # def _onchange_order_line_tax_default(self): + # _logger.info("Onchange Order Line Tax Default Terpanggil") + + # no_tax = self.env['account.tax'].search([ + # ('type_tax_use', '=', 'purchase'), + # ('name', 'ilike', 'no tax') + # ], limit=1) + + # if not no_tax: + # _logger.info("No Tax tidak ditemukan") + # return + + # for order in self: + # for line in order.order_line: + # if not line.taxes_id: + # line.taxes_id = [(6, 0, [no_tax.id])] + # _logger.info("Auto-set No tax ke baris product: %s", line.product_id.name) @api.onchange('total_cost_service') def _onchange_total_cost_service(self): @@ -154,6 +262,7 @@ class PurchaseOrder(models.Model): 'invoice_date': current_date, 'date': current_date, 'invoice_origin': self.name, + 'purchase_order_id': self.id, 'move_type': 'in_invoice' } @@ -178,7 +287,7 @@ class PurchaseOrder(models.Model): 'move_id': bills.id, 'product_id': product_dp.id, # product down payment 'name': '[IT.121456] Down Payment', # product down payment - 'account_id': 401, # Uang Muka persediaan barang dagang + 'account_id': 669, # Uang Muka persediaan barang dagang # 'price_unit': move_line.price_unit, 'quantity': -1, 'product_uom_id': 1, @@ -230,6 +339,7 @@ class PurchaseOrder(models.Model): 'invoice_date': current_date, 'date': current_date, 'invoice_origin': self.name, + 'purchase_order_id': self.id, 'move_type': 'in_invoice' } @@ -240,7 +350,7 @@ class PurchaseOrder(models.Model): data_line_bills = { 'move_id': bills.id, 'product_id': product_dp.id, # product down payment - 'account_id': 401, # Uang Muka persediaan barang dagang + 'account_id': 669, # Uang Muka persediaan barang dagang 'quantity': 1, 'product_uom_id': 1, 'tax_ids': [line[0].taxes_id.id for line in self.order_line], @@ -306,6 +416,7 @@ class PurchaseOrder(models.Model): invoice_vals = { 'ref': self.partner_ref or '', 'move_type': move_type, + 'purchase_order_id': self.id, 'narration': self.notes, 'currency_id': self.currency_id.id, 'invoice_user_id': self.user_id and self.user_id.id or self.env.user.id, @@ -387,6 +498,13 @@ class PurchaseOrder(models.Model): } return action + def open_form_multi_ask_approval_po(self): + action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_purchase_order_multi_ask_approval') + action['context'] = { + 'po_ids': [x.id for x in self] + } + return action + def open_form_multi_create_uang_muka(self): action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_purchase_order_multi_uangmuka') action['context'] = { @@ -414,6 +532,13 @@ class PurchaseOrder(models.Model): purchase.button_confirm() + def action_multi_ask_approval_po(self): + for purchase in self: + if purchase.state != 'draft': + continue + + purchase.po_approve() + def open_form_multi_update_paid_status(self): action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_purchase_order_multi_update') action['context'] = { @@ -653,6 +778,7 @@ class PurchaseOrder(models.Model): raise UserError("Produk "+line.product_id.name+" memiliki vendor berbeda dengan SO (Vendor PO: "+str(self.partner_id.name)+", Vendor SO: "+str(line.so_line_id.vendor_id.name)+")") def button_confirm(self): + # self._check_payment_term() # check payment term res = super(PurchaseOrder, self).button_confirm() current_time = datetime.now() self.check_ppn_mix() @@ -708,9 +834,25 @@ class PurchaseOrder(models.Model): self.unlink_purchasing_job_state() self._check_qty_plafon_product() + if self.product_bom_id: + self._remove_product_bom() return res + def _remove_product_bom(self): + pj = self.env['v.purchasing.job'].search([ + ('product_id', '=', self.product_bom_id.id) + ]) + + if pj: + pj_state = self.env['purchasing.job.state'].search([ + ('purchasing_job_id', '=', pj.id) + ]) + + if pj_state: + pj_state.note = 'Product BOM Sudah Di PO' + pj_state.date_po = datetime.utcnow() + def check_ppn_mix(self): reference_taxes = self.order_line[0].taxes_id @@ -1044,6 +1186,19 @@ class PurchaseOrder(models.Model): return super(PurchaseOrder, self).button_unlock() + @api.model #override custom create & write for check payment term + def create(self, vals): + order = super().create(vals) + # order.with_context(skip_check_payment=True)._check_payment_term() + # order.with_context(notify_tax=True)._check_tax_rule() + return order + + def write(self, vals): + res = super().write(vals) + if not self.env.context.get('skip_check_payment'): + self.with_context(skip_check_payment=True)._check_payment_term() + self.with_context(notify_tax=True)._check_tax_rule() + return res class PurchaseOrderUnlockWizard(models.TransientModel): _name = 'purchase.order.unlock.wizard' diff --git a/indoteknik_custom/models/purchase_order_line.py b/indoteknik_custom/models/purchase_order_line.py index 587a09a1..033469b8 100755 --- a/indoteknik_custom/models/purchase_order_line.py +++ b/indoteknik_custom/models/purchase_order_line.py @@ -321,32 +321,60 @@ class PurchaseOrderLine(models.Model): def compute_item_margin(self): sum_so_margin = sum_sales_price = sum_margin = 0 + for line in self: - if not line.product_id or line.product_id.type == 'service' or not self.order_id.sale_order_id: + product = line.product_id + order = line.order_id + + # Skip jika tidak ada product_id, produk adalah service, atau tidak ada purchase order terkait + if not product or product.type == 'service' or not order: line.so_item_margin = 0 line.so_item_percent_margin = 0 line.item_margin = 0 line.item_percent_margin = 0 continue - sale_order_line = self.env['sale.order.line'].search( - [('product_id', '=', line.product_id.id), - ('order_id', '=', line.order_id.sale_order_id.id)], limit=1, order='price_reduce_taxexcl') - line.so_item_margin = sale_order_line.item_margin - line.so_item_percent_margin = sale_order_line.item_percent_margin - sum_so_margin += sale_order_line.item_margin - sales_price = sale_order_line.price_reduce_taxexcl * sale_order_line.product_uom_qty - if sale_order_line.order_id.shipping_cost_covered == 'indoteknik': - sales_price -= sale_order_line.delivery_amt_line - if sale_order_line.order_id.fee_third_party > 0: - sales_price -= sale_order_line.fee_third_party_line - sum_sales_price += sales_price + + # Cari semua sale.order.line terkait dengan purchase.order melalui tabel purchase.order.sales.match + sales_matches = self.env['purchase.order.sales.match'].search([ + ('purchase_order_id', '=', order.id), + ('product_id', '=', product.id) + ]) + + total_sales_price = total_margin = total_qty_so = 0 + for match in sales_matches: + sale_order_line = match.sale_line_id + + # Hitung harga jual setelah mempertimbangkan biaya tambahan + sales_price = sale_order_line.price_reduce_taxexcl * match.qty_so + if sale_order_line.order_id.shipping_cost_covered == 'indoteknik': + sales_price -= sale_order_line.delivery_amt_line + if sale_order_line.order_id.fee_third_party > 0: + sales_price -= sale_order_line.fee_third_party_line + + total_sales_price += sales_price + total_margin += sale_order_line.item_margin + total_qty_so += match.qty_so + + # Set margin berdasarkan total dari semua sales order yang terkait + line.so_item_margin = total_margin + line.so_item_percent_margin = (total_margin / total_sales_price) * 100 if total_sales_price else 0 + + sum_so_margin += total_margin + sum_sales_price += total_sales_price + + # Hitung harga pembelian dengan mempertimbangkan biaya pengiriman purchase_price = line.price_subtotal - if line.order_id.delivery_amount > 0: + if order.delivery_amount > 0: purchase_price += line.delivery_amt_line - real_item_margin = sales_price - purchase_price - real_item_percent_margin = round((real_item_margin/sales_price), 2) * 100 + + # Hitung margin dan persentase margin + real_item_margin = total_sales_price - purchase_price + real_item_percent_margin = (real_item_margin / total_sales_price) * 100 if total_sales_price else 0 + + # Set nilai margin ke dalam line line.item_margin = real_item_margin line.item_percent_margin = real_item_percent_margin + sum_margin += real_item_margin def compute_delivery_amt_line(self): diff --git a/indoteknik_custom/models/purchase_order_multi_ask_approval.py b/indoteknik_custom/models/purchase_order_multi_ask_approval.py new file mode 100644 index 00000000..69f8e5a8 --- /dev/null +++ b/indoteknik_custom/models/purchase_order_multi_ask_approval.py @@ -0,0 +1,22 @@ +from odoo import models, fields +import logging + +_logger = logging.getLogger(__name__) + + +class PurchaseOrderMultiUpdate(models.TransientModel): + _name = 'purchase.order.multi_ask_approval' + + def save_multi_ask_approval_po(self): + po_ids = self._context['po_ids'] + purchase = self.env['purchase.order'].browse(po_ids) + purchase.action_multi_ask_approval_po() + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Notification', + 'message': 'PO berhasil di ask_approval', + 'next': {'type': 'ir.actions.act_window_close'}, + } + }
\ No newline at end of file diff --git a/indoteknik_custom/models/purchase_order_multi_uangmuka.py b/indoteknik_custom/models/purchase_order_multi_uangmuka.py index dd63e698..0570efd9 100644 --- a/indoteknik_custom/models/purchase_order_multi_uangmuka.py +++ b/indoteknik_custom/models/purchase_order_multi_uangmuka.py @@ -76,7 +76,7 @@ class PurchaseOrderMultiUangmuka(models.TransientModel): param_debit = { 'move_id': account_move.id, - 'account_id': 401, # uang muka persediaan barang dagang + 'account_id': 669, # uang muka persediaan barang dagang 'partner_id': partner_id, 'currency_id': 12, 'debit': order.amount_total, diff --git a/indoteknik_custom/models/purchase_order_sales_match.py b/indoteknik_custom/models/purchase_order_sales_match.py index d1d929d3..0bd0092b 100644 --- a/indoteknik_custom/models/purchase_order_sales_match.py +++ b/indoteknik_custom/models/purchase_order_sales_match.py @@ -24,6 +24,19 @@ class PurchaseOrderSalesMatch(models.Model): margin_item = fields.Float(string='Margin') delivery_amt = fields.Float(string='Delivery Amount', compute='_compute_delivery_amt') margin_deduct = fields.Float(string='After Deduct', compute='_compute_delivery_amt') + purchase_price_so = fields.Float(string='Purchase Price Sale Order', related='sale_line_id.purchase_price') + purchase_price_po = fields.Float('Purchase Price PO', compute='_compute_purchase_price_po') + purchase_line_id = fields.Many2one('purchase.order.line', string='Purchase Line', compute='_compute_purchase_line_id') + hold_outgoing_so = fields.Boolean(string='Hold Outgoing SO', related='sale_id.hold_outgoing') + + def _compute_purchase_line_id(self): + for line in self: + line.purchase_line_id = self.env['purchase.order.line'].search([('order_id', '=', line.purchase_order_id.id), ('product_id', '=', line.product_id.id)]) + + def _compute_purchase_price_po(self): + for line in self: + purchase_price = self.env['purchase.order.line'].search([('order_id', '=', line.purchase_order_id.id), ('product_id', '=', line.product_id.id)]) + line.purchase_price_po = purchase_price.price_unit def _compute_delivery_amt(self): for line in self: diff --git a/indoteknik_custom/models/purchase_pricelist.py b/indoteknik_custom/models/purchase_pricelist.py index e5b35d7f..dd680f4d 100755 --- a/indoteknik_custom/models/purchase_pricelist.py +++ b/indoteknik_custom/models/purchase_pricelist.py @@ -83,6 +83,15 @@ class PurchasePricelist(models.Model): massage="Ada duplikat product dan vendor, berikut data yang anda duplikat : \n" + str(existing_purchase.product_id.name) + " - " + str(existing_purchase.vendor_id.name) + " - " + str(existing_purchase.product_price) if existing_purchase: raise UserError(massage) + + def sync_pricelist_item_promo(self, product): + pricelist_product = self.env['product.pricelist.item'].search([('product_id', '=', product.id), ('pricelist_id', '=', 17022)]) + for pricelist in pricelist_product: + if pricelist.fixed_price == 0: + flashsale = self.env['product.pricelist.item'].search([('product_id', '=', product.id), ('pricelist_id.is_flash_sale', '=', True)]) + if flashsale: + flashsale.fixed_price = 0 + return def action_calculate_pricelist(self): MAX_PRICELIST = 10 @@ -94,6 +103,8 @@ class PurchasePricelist(models.Model): records = self.env['purchase.pricelist'].browse(active_ids) price_group = self.env['price.group'].collect_price_group() for rec in records: + if rec.include_price == 0: + rec.sync_pricelist_item_promo(rec.product_id) product_group = rec.product_id.product_tmpl_id.x_manufacture.pricing_group or None price_incl = rec.include_price diff --git a/indoteknik_custom/models/purchasing_job.py b/indoteknik_custom/models/purchasing_job.py index 74b5134e..ea2f46cb 100644 --- a/indoteknik_custom/models/purchasing_job.py +++ b/indoteknik_custom/models/purchasing_job.py @@ -25,6 +25,15 @@ class PurchasingJob(models.Model): ], string='APO?') purchase_representative_id = fields.Many2one('res.users', string="Purchase Representative", readonly=True) note = fields.Char(string="Note Detail") + date_po = fields.Datetime(string='Date PO', copy=False) + + def unlink(self): + # Example: Delete related records from the underlying model + underlying_records = self.env['purchasing.job'].search([ + ('product_id', 'in', self.mapped('product_id').ids) + ]) + underlying_records.unlink() + return super(PurchasingJob, self).unlink() def redirect_to_pjs(self): states = self.env['purchasing.job.state'].search([ @@ -56,8 +65,9 @@ class PurchasingJob(models.Model): pmp.action, max(pjs.status_apo::text) AS status_apo, max(pjs.note::text) AS note, + max(pjs.date_po::text) AS date_po, CASE - WHEN pmp.brand IN ('Tekiro', 'RYU', 'Rexco') THEN 27 + WHEN pmp.brand IN ('Tekiro', 'RYU', 'Rexco', 'RYU (Sparepart)') THEN 27 WHEN sub.vendor_id = 9688 THEN 397 WHEN sub.vendor_id = 35475 THEN 397 WHEN sub.vendor_id = 29712 THEN 397 @@ -182,7 +192,8 @@ class OutstandingSales(models.Model): join product_product pp on pp.id = sm.product_id join product_template pt on pt.id = pp.product_tmpl_id left join x_manufactures xm on xm.id = pt.x_manufacture - where sp.state in ('draft', 'waiting', 'confirmed') + where sp.state in ('draft', 'waiting', 'confirmed', 'assigned') + and sm.state in ('draft', 'waiting', 'confirmed', 'partially_available') and sp.name like '%OUT%' ) """) diff --git a/indoteknik_custom/models/purchasing_job_multi_update.py b/indoteknik_custom/models/purchasing_job_multi_update.py index deba960a..80a43e45 100644 --- a/indoteknik_custom/models/purchasing_job_multi_update.py +++ b/indoteknik_custom/models/purchasing_job_multi_update.py @@ -18,7 +18,7 @@ class PurchasingJobMultiUpdate(models.TransientModel): ('purchasing_job_id', '=', product.id) ]) - purchasing_job_state.unlink() + # purchasing_job_state.unlink() purchasing_job_state.create({ 'purchasing_job_id': product.id, diff --git a/indoteknik_custom/models/purchasing_job_state.py b/indoteknik_custom/models/purchasing_job_state.py index 1838a496..d014edfe 100644 --- a/indoteknik_custom/models/purchasing_job_state.py +++ b/indoteknik_custom/models/purchasing_job_state.py @@ -14,4 +14,5 @@ class PurchasingJobState(models.Model): ('not_apo', 'Belum APO'), ('apo', 'APO') ], string='APO?', copy=False) - note = fields.Char(string="Note Detail") + note = fields.Char(string="Note Detail", copy=False) + date_po = fields.Datetime(string='Date PO', copy=False) diff --git a/indoteknik_custom/models/report_logbook_sj.py b/indoteknik_custom/models/report_logbook_sj.py index a1b6299c..17119c12 100644 --- a/indoteknik_custom/models/report_logbook_sj.py +++ b/indoteknik_custom/models/report_logbook_sj.py @@ -29,6 +29,8 @@ class ReportLogbookSJ(models.Model): string='Status', tracking=True, ) + + sj_number = fields.Char(string='Picking', related='report_logbook_sj_line.name') count_line = fields.Char(string='Count Line', compute='_compute_count_line') diff --git a/indoteknik_custom/models/requisition.py b/indoteknik_custom/models/requisition.py index c972b485..1d350929 100644 --- a/indoteknik_custom/models/requisition.py +++ b/indoteknik_custom/models/requisition.py @@ -276,10 +276,11 @@ class RequisitionLine(models.Model): _name = 'requisition.line' _description = 'Requisition Line' _order = 'requisition_id, id' + _inherit = ['mail.thread'] requisition_id = fields.Many2one('requisition', string='Ref', required=True, ondelete='cascade', index=True, copy=False) brand_id = fields.Many2one('x_manufactures', string='Brand') - product_id = fields.Many2one('product.product', string='Product') + product_id = fields.Many2one('product.product', string='Product', tracking=3,) partner_id = fields.Many2one('res.partner', string='Vendor') qty_purchase = fields.Float(string='Qty Purchase') price_unit = fields.Float(string='Price') @@ -331,6 +332,45 @@ class RequisitionLine(models.Model): line.taxes_id = taxes line.partner_id = purchase_pricelist.vendor_id.id + @api.model + def create(self, vals): + record = super(RequisitionLine, self).create(vals) + record._track_changes('Tambah') + return record + + def write(self, vals): + for record in self: + old_values = {field: record[field] for field in vals if field in record} + + result = super(RequisitionLine, self).write(vals) + + for record in self: + record._track_changes('Updated', old_values) + + return result + + def unlink(self): + for record in self: + record._track_changes('Hapus') + return super(RequisitionLine, self).unlink() + + def _track_changes(self, action, old_values=None): + message = f"Produk telah di-{action} : <br/>" + if action == 'Tambah': + # message += f"<br/> Product: {self.product_id.name}" + message += f"Product: {self.product_id.name} <br/> Vendor: {self.partner_id.name} <br/> Qty: {self.qty_purchase} <br/> Price: {self.price_unit} <br/> Tax: {self.tax_id.name} <br/> Subtotal: {self.subtotal} <br/> Brand: {self.brand_id.x_name}" + elif action == 'Hapus': + # message += f"<br/> Deleted Product: {self.product_id.name}" + message += f"<br/> Deleted Product: {self.product_id.name} <br/> Vendor: {self.partner_id.name} Qty: {self.qty_purchase} <br/> Price: {self.price_unit} <br/> Tax: {self.tax_id.name} <br/> Subtotal: {self.subtotal} <br/> Brand: {self.brand_id.x_name}" + else: # Updated + for field, old_value in old_values.items(): + new_value = self[field] + if old_value != new_value: + field_label = self._fields[field].string # Ambil nama label field + message += f"{field_label}: {old_value} -> {new_value}<br/>" + + if self.requisition_id: + self.requisition_id.message_post(body=message) class RequisitionPurchaseMatch(models.Model): _name = 'requisition.purchase.match' diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index 7852682f..ff07c94c 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -2,6 +2,7 @@ from odoo import models, fields, api from odoo.exceptions import UserError, ValidationError from datetime import datetime from odoo.http import request +import re class GroupPartner(models.Model): _name = 'group.partner' @@ -39,6 +40,14 @@ class ResPartner(models.Model): estimasi_tempo = fields.Char(string='Estimasi Pembelian Pertahun') tempo_duration = fields.Many2one('account.payment.term', string='Durasi Tempo') tempo_limit = fields.Char(string='Limit Tempo') + minimum_amount = fields.Float( + string="Minimum Order", + help="Jika total belanja kurang dari ini, maka payment term akan otomatis menjadi CBD." + ) + minimum_amount_tax = fields.Float( + string="Minimum Amount Tax", + help="Jika total belanja kurang dari ini, maka tax akan otomatis menjadi 0%." + ) category_produk_ids = fields.Many2many('product.public.category', string='Kategori Produk yang Digunakan', domain=lambda self: self._get_default_category_domain()) @api.model @@ -58,6 +67,7 @@ class ResPartner(models.Model): # Pengiriman pic_name = fields.Char(string='Nama PIC Penerimaan Barang') + pic_mobile = fields.Char(string='Nomor HP PIC Penerimaan Barang') street_pengiriman = fields.Char(string="Alamat Perusahaan") state_id_pengiriman = fields.Many2one('res.country.state', string='State') city_id_pengiriman = fields.Many2one('vit.kota', string='City') @@ -65,6 +75,7 @@ class ResPartner(models.Model): subDistrict_id_pengiriman = fields.Many2one('vit.kelurahan', string='Kelurahan') zip_pengiriman = fields.Char(string="Zip") invoice_pic = fields.Char(string='Nama PIC Penerimaan Invoice') + invoice_pic_mobile = fields.Char(string='Nomor HP PIC Penerimaan Invoice') street_invoice = fields.Char(string="Alamat Perusahaan") state_id_invoice = fields.Many2one('res.country.state', string='State') city_id_invoice = fields.Many2one('vit.kota', string='City') @@ -73,6 +84,7 @@ class ResPartner(models.Model): zip_invoice = fields.Char(string="Zip") tukar_invoice = fields.Char(string='Jadwal Penukaran Invoice') jadwal_bayar = fields.Char(string='Jadwal Pembayaran') + dokumen_prosedur = fields.Many2one('ir.attachment', string="Dokumen Pengiriman", tracking=3, readonly=True) dokumen_pengiriman = fields.Char(string='Dokumen Tanda Terima yang Diberikan Pada Saat Pengiriman Barang') dokumen_pengiriman_input = fields.Char(string='Dokumen yang Dibawa Saat Pengiriman Barang') dokumen_invoice = fields.Char(string='Dokumen yang dilampirkan saat Pengiriman Invoice') @@ -124,8 +136,8 @@ class ResPartner(models.Model): ('PNR', 'Pareto Non Repeating'), ('NP', 'Non Pareto') ]) - email_finance = fields.Char(string='Email Finance') - email_sales = fields.Char(string='Email Sales') + email_finance = fields.Char(string='Email Finance Vendor') + email_sales = fields.Char(string='Email Sales Vendor') user_payment_terms_sales = fields.Many2one('res.users', string='Users Update Payment Terms') date_payment_terms_sales = fields.Datetime(string='Date Update Payment Terms') @@ -263,6 +275,45 @@ class ResPartner(models.Model): # # raise UserError('You name it') # return res + + @api.constrains('name') + def _check_duplicate_name(self): + for record in self: + if record.name: + existing_partner = self.env['res.partner'].search([ + ('id', '!=', record.id), + ('name', '=', record.name), + ('email', '=', record.email) + ], limit=1) + + if existing_partner: + raise ValidationError(f"Nama '{record.name}' sudah digunakan oleh partner lain!") + + @api.constrains('npwp') + def _check_npwp(self): + for record in self: + npwp = record.npwp.strip() if record.npwp else '' + # Abaikan validasi jika NPWP kosong atau diisi "0" + if not npwp or npwp == '0' or npwp == '00.000.000.0-000.000': + continue + + # Validasi untuk NPWP 15 digit (format: 99.999.999.9-999.999) + if len(npwp) == 20: + # Regex untuk 15 digit dengan format titik dan tanda hubung + pattern_15_digit = r'^\d{2}\.\d{3}\.\d{3}\.\d{1}-\d{3}\.\d{3}$' + if not re.match(pattern_15_digit, npwp): + raise ValidationError("Format NPWP 15 digit yang dimasukkan salah. Pastikan format yang benar adalah: 99.999.999.9-999.999") + + # Validasi untuk NPWP 16 digit (hanya angka tanpa titik atau tanda hubung) + elif len(npwp) == 16: + pattern_16_digit = r'^\d{16}$' + if not re.match(pattern_16_digit, npwp): + raise ValidationError("Format NPWP 16 digit yang dimasukkan salah. Format yang benar adalah 16 digit angka tanpa titik atau tanda hubung.") + + # Validasi panjang NPWP jika lebih atau kurang dari 15 atau 16 digit + else: + raise ValidationError("Digit NPWP yang dimasukkan tidak sesuai. Pastikan NPWP memiliki 15 digit dengan format tertentu (99.999.999.9-999.999) atau 16 digit tanpa tanda hubung.") + def write(self, vals): # Fungsi rekursif untuk meng-update semua child, termasuk child dari child @@ -321,6 +372,7 @@ class ResPartner(models.Model): # Pengiriman vals['pic_name'] = vals.get('pic_name', self.pic_name) + vals['pic_mobile'] = vals.get('pic_mobile', self.pic_mobile) vals['street_pengiriman'] = vals.get('street_pengiriman', self.street_pengiriman) vals['state_id_pengiriman'] = vals.get('state_id_pengiriman', self.state_id_pengiriman) vals['city_id_pengiriman'] = vals.get('city_id_pengiriman', self.city_id_pengiriman) @@ -328,6 +380,7 @@ class ResPartner(models.Model): vals['subDistrict_id_pengiriman'] = vals.get('subDistrict_id_pengiriman', self.subDistrict_id_pengiriman) vals['zip_pengiriman'] = vals.get('zip_pengiriman', self.zip_pengiriman) vals['invoice_pic'] = vals.get('invoice_pic', self.invoice_pic) + vals['invoice_pic_mobile'] = vals.get('invoice_pic_mobile', self.invoice_pic_mobile) vals['street_invoice'] = vals.get('street_invoice', self.street_invoice) vals['state_id_invoice'] = vals.get('state_id_invoice', self.state_id_invoice) vals['city_id_invoice'] = vals.get('city_id_invoice', self.city_id_invoice) @@ -336,6 +389,7 @@ class ResPartner(models.Model): vals['zip_invoice'] = vals.get('zip_invoice', self.zip_invoice) vals['tukar_invoice'] = vals.get('tukar_invoice', self.tukar_invoice) vals['jadwal_bayar'] = vals.get('jadwal_bayar', self.jadwal_bayar) + vals['dokumen_prosedur'] = vals.get('dokumen_prosedur', self.dokumen_prosedur) vals['dokumen_pengiriman'] = vals.get('dokumen_pengiriman', self.dokumen_pengiriman) vals['dokumen_pengiriman_input'] = vals.get('dokumen_pengiriman_input', self.dokumen_pengiriman_input) vals['dokumen_invoice'] = vals.get('dokumen_invoice', self.dokumen_invoice) @@ -444,6 +498,7 @@ class ResPartner(models.Model): 'finance_mobile': vals.get('finance_mobile'), 'finance_email': vals.get('finance_email'), 'pic_name': vals.get('pic_name'), + 'pic_mobile': vals.get('pic_mobile'), 'street_pengiriman': vals.get('street_pengiriman'), 'state_id_pengiriman': vals.get('state_id_pengiriman'), 'city_id_pengiriman': vals.get('city_id_pengiriman'), @@ -451,6 +506,7 @@ class ResPartner(models.Model): 'subDistrict_id_pengiriman': vals.get('subDistrict_id_pengiriman'), 'zip_pengiriman': vals.get('zip_pengiriman'), 'invoice_pic': vals.get('invoice_pic'), + 'invoice_pic_mobile': vals.get('invoice_pic_mobile'), 'street_invoice': vals.get('street_invoice'), 'state_id_invoice': vals.get('state_id_invoice'), 'city_id_invoice': vals.get('city_id_invoice'), @@ -459,6 +515,7 @@ class ResPartner(models.Model): 'zip_invoice': vals.get('zip_invoice'), 'tukar_invoice': vals.get('tukar_invoice'), 'jadwal_bayar': vals.get('jadwal_bayar'), + 'dokumen_prosedur': vals.get('dokumen_prosedur'), 'dokumen_pengiriman': vals.get('dokumen_pengiriman'), 'dokumen_pengiriman_input': vals.get('dokumen_pengiriman_input'), 'dokumen_invoice': vals.get('dokumen_invoice'), @@ -625,6 +682,8 @@ class ResPartner(models.Model): def _onchange_customer_type(self): if self.customer_type == 'nonpkp': self.npwp = '00.000.000.0-000.000' + elif self.customer_type == 'pkp': + self.npwp = '00.000.000.0-000.000' def get_check_payment_term(self): self.ensure_one() @@ -639,4 +698,9 @@ class ResPartner(models.Model): if not self.nitku.isdigit(): raise UserError("NITKU harus berupa angka.") if len(self.nitku) != 22: - raise UserError("NITKU harus memiliki tepat 22 angka.")
\ No newline at end of file + raise UserError("NITKU harus memiliki tepat 22 angka.") + + @api.onchange('name') + def _onchange_name(self): + if self.company_type == 'person': + self.nama_wajib_pajak = self.name
\ No newline at end of file diff --git a/indoteknik_custom/models/res_users.py b/indoteknik_custom/models/res_users.py index 31b84ae3..b0864f2c 100755 --- a/indoteknik_custom/models/res_users.py +++ b/indoteknik_custom/models/res_users.py @@ -70,6 +70,10 @@ class ResUsers(models.Model): vouchers = self.env['voucher'].get_active_voucher([('show_on_email', '=', 'user_activation')]) if not vouchers: return None return ', '.join(x.code for x in vouchers) + if type == 'switch_account': + vouchers = self.env['voucher'].get_active_voucher([('excl_pricelist_ids', 'not in', [1]), ('apply_type', 'in', ['all', 'brand']), ('account_type', 'in', ['all', 'company']), ('visibility', 'in', ['public'])]) + if not vouchers: return None + return ', '.join(x.code for x in vouchers) return None def check_access(self, model, mode): diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 48195b77..0d4fc6c3 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -1,3 +1,5 @@ +from re import search + from odoo import fields, models, api, _ from odoo.exceptions import UserError, ValidationError from datetime import datetime, timedelta @@ -7,16 +9,133 @@ from collections import defaultdict _logger = logging.getLogger(__name__) +class CancelReasonOrder(models.TransientModel): + _name = 'cancel.reason.order' + _description = 'Wizard for Cancel Reason order' + + request_id = fields.Many2one('sale.order', string='Request') + reason_cancel = fields.Selection([ + ('harga_terlalu_mahal', 'Harga barang terlalu mahal'), + ('harga_web_tidak_valid', 'Harga web tidak valid'), + ('stok_kosong', 'Stock kosong'), + ('tidak_mau_indent', 'Customer tidak mau indent'), + ('batal_rencana_pembelian', 'Customer membatalkan rencana pembelian'), + ('vendor_tidak_support_demo', 'Vendor tidak support demo/trial product'), + ('product_knowledge_kurang', 'Product knowledge kurang baik'), + ('barang_tidak_sesuai', 'Barang tidak sesuai/tepat'), + ('tidak_sepakat_pembayaran', 'Tidak menemukan kesepakatan untuk pembayaran'), + ('dokumen_tidak_support', 'Indoteknik tidak bisa support document yang dibutuhkan (Ex: TKDN, COO, SNI)'), + ('ganti_quotation', 'Ganti Quotation'), + ('testing_internal', 'Testing Internal'), + ('revisi_data', 'Revisi Data'), + ], string='Reason for Cancel', required=True, copy=False, index=True, tracking=3) + attachment_bukti = fields.Many2many( + 'ir.attachment', + string="Attachment Bukti", readonly=False, + tracking=3, required=True + ) + nomor_so_pengganti = fields.Char(string='Nomor SO Pengganti', copy=False, tracking=3) + + def confirm_reject(self): + order = self.request_id + if order: + order.write({'reason_cancel': self.reason_cancel}) + if not self.attachment_bukti: + raise UserError('Attachment bukti wajib disertakan') + order.write({'attachment_bukti': self.attachment_bukti}) + order.message_post(body='Attachment Bukti Cancel', + attachment_ids=[self.attachment_bukti.id]) + if self.reason_cancel == 'ganti_quotation': + if self.nomor_so_pengganti: + order.write({'nomor_so_pengganti': self.nomor_so_pengganti}) + else: + raise UserError('Nomor SO pengganti wajib disertakan') + order.confirm_cancel_order() + + return {'type': 'ir.actions.act_window_close'} + +class ShippingOption(models.Model): + _name = "shipping.option" + _description = "Shipping Option" + + name = fields.Char(string="Option Name", required=True) + price = fields.Float(string="Price", required=True) + provider = fields.Char(string="Provider") + etd = fields.Char(string="Estimated Delivery Time") + sale_order_id = fields.Many2one('sale.order', string="Sale Order", ondelete="cascade") + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + def unlink(self): + lines_to_reject = [] + for line in self: + if line.order_id: + now = fields.Datetime.now() + + initial_reason="Product Rejected" + + # Buat lognote untuk product yang di delete + log_note = (f"<li>Product '{line.product_id.name}' rejected. </li>" + f"<li>Quantity: {line.product_uom_qty}, </li>" + f"<li>Date: {now.strftime('%d-%m-%Y')}, </li>" + f"<li>Time: {now.strftime('%H:%M:%S')} </li>" + f"<li>Reason reject: {initial_reason} </li>") + + lines_to_reject.append({ + 'sale_order_id': line.order_id.id, + 'product_id': line.product_id.id, + 'qty_reject': line.product_uom_qty, + 'reason_reject': initial_reason, # pesan reason reject + 'message_body': log_note, + 'order_id': line.order_id, + }) + + # Call the original unlink method + result = super(SaleOrderLine, self).unlink() + + # After deletion, create reject lines and post messages + SalesOrderReject = self.env['sales.order.reject'] + for reject_data in lines_to_reject: + # Buat line baru di reject line + SalesOrderReject.create({ + 'sale_order_id': reject_data['sale_order_id'], + 'product_id': reject_data['product_id'], + 'qty_reject': reject_data['qty_reject'], + 'reason_reject': reject_data['reason_reject'], + }) + + # Post to chatter with a more prominent message + reject_data['order_id'].message_post( + body=reject_data['message_body'], + author_id=self.env.user.partner_id.id, # menampilkan pesan di lognote sebagai current user + ) + + return result class SaleOrder(models.Model): _inherit = "sale.order" + ongkir_ke_xpdc = fields.Float(string='Ongkir ke Ekspedisi', help='Biaya ongkir ekspedisi', copy=False, index=True, tracking=3) + + metode_kirim_ke_xpdc = fields.Selection([ + ('indoteknik_deliv', 'Indoteknik Delivery'), + ('lalamove', 'Lalamove'), + ('grab', 'Grab'), + ('gojek', 'Gojek'), + ('deliveree', 'Deliveree'), + ('other', 'Other'), + ], string='Metode Kirim Ke Ekspedisi', copy=False, index=True, tracking=3) + + koli_lines = fields.One2many('sales.order.koli', 'sale_order_id', string='Sales Order Koli', auto_join=True) fulfillment_line_v2 = fields.One2many('sales.order.fulfillment.v2', 'sale_order_id', string='Fullfillment2') fullfillment_line = fields.One2many('sales.order.fullfillment', 'sales_order_id', string='Fullfillment') reject_line = fields.One2many('sales.order.reject', 'sale_order_id', string='Reject Lines') order_sales_match_line = fields.One2many('sales.order.purchase.match', 'sales_order_id', string='Purchase Match Lines', states={'cancel': [('readonly', True)], 'done': [('readonly', True)]}, copy=True) total_margin = fields.Float('Total Margin', compute='_compute_total_margin', help="Total Margin in Sales Order Header") + total_before_margin = fields.Float('Total Before Margin', compute='_compute_total_before_margin', help="Total Margin in Sales Order Header") total_percent_margin = fields.Float('Total Percent Margin', compute='_compute_total_percent_margin', help="Total % Margin in Sales Order Header") + total_margin_excl_third_party = fields.Float('Before Margin', help="Before Margin in Sales Order Header", compute='_compute_total_margin_excl_third_party') approval_status = fields.Selection([ ('pengajuan1', 'Approval Manager'), ('pengajuan2', 'Approval Pimpinan'), @@ -28,11 +147,11 @@ class SaleOrder(models.Model): shipping_cost_covered = fields.Selection([ ('indoteknik', 'Indoteknik'), ('customer', 'Customer') - ], string='Shipping Covered by', help='Siapa yang menanggung biaya ekspedisi?', copy=False) + ], string='Shipping Covered by', help='Siapa yang menanggung biaya ekspedisi?', copy=False, tracking=3) shipping_paid_by = fields.Selection([ ('indoteknik', 'Indoteknik'), ('customer', 'Customer') - ], string='Shipping Paid by', help='Siapa yang talangin dulu Biaya ekspedisi-nya?', copy=False) + ], string='Shipping Paid by', help='Siapa yang talangin dulu Biaya ekspedisi-nya?', copy=False, tracking=3) sales_tax_id = fields.Many2one('account.tax', string='Tax', domain=['|', ('active', '=', False), ('active', '=', True)]) have_outstanding_invoice = fields.Boolean('Have Outstanding Invoice', compute='_have_outstanding_invoice') have_outstanding_picking = fields.Boolean('Have Outstanding Picking', compute='_have_outstanding_picking') @@ -48,6 +167,7 @@ class SaleOrder(models.Model): domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", help="Dipakai untuk alamat tempel", tracking=True) fee_third_party = fields.Float('Fee Pihak Ketiga') + biaya_lain_lain = fields.Float('Biaya Lain Lain') so_status = fields.Selection([ ('terproses', 'Terproses'), ('sebagian', 'Sebagian Diproses'), @@ -83,9 +203,9 @@ class SaleOrder(models.Model): customer_type = fields.Selection([ ('pkp', 'PKP'), ('nonpkp', 'Non PKP') - ], required=True) - sppkp = fields.Char(string="SPPKP", required=True, tracking=True) - npwp = fields.Char(string="NPWP", required=True, tracking=True) + ], required=True, compute='_compute_partner_field') + sppkp = fields.Char(string="SPPKP", required=True, tracking=True, compute='_compute_partner_field') + npwp = fields.Char(string="NPWP", required=True, tracking=True, compute='_compute_partner_field') purchase_total = fields.Monetary(string='Purchase Total', compute='_compute_purchase_total') voucher_id = fields.Many2one(comodel_name='voucher', string='Voucher', copy=False) applied_voucher_id = fields.Many2one(comodel_name='voucher', string='Applied Voucher', copy=False) @@ -93,11 +213,13 @@ class SaleOrder(models.Model): applied_voucher_shipping_id = fields.Many2one(comodel_name='voucher', string='Applied Voucher', copy=False) amount_voucher_shipping_disc = fields.Float(string='Voucher Discount') source_id = fields.Many2one('utm.source', 'Source', domain="[('id', 'in', [32, 59, 60, 61])]", required=True) - estimated_arrival_days = fields.Integer('Estimated Arrival Days', default=0) + estimated_arrival_days = fields.Integer('Estimated Arrival To', default=0) + estimated_arrival_days_start = fields.Integer('Estimated Arrival From', default=0) email = fields.Char(string='Email') picking_iu_id = fields.Many2one('stock.picking', 'Picking IU') helper_by_id = fields.Many2one('res.users', 'Helper By') - eta_date = fields.Datetime(string='ETA Date', copy=False, compute='_compute_eta_date') + eta_date_start = fields.Datetime(string='ETA Date start', copy=False, compute='_compute_eta_date') + eta_date = fields.Datetime(string='ETA Date end', copy=False, compute='_compute_eta_date') flash_sale = fields.Boolean(string='Flash Sale', help='Data dari web') is_continue_transaction = fields.Boolean(string='Button Transaction', help='Data dari web') web_approval = fields.Selection([ @@ -144,6 +266,121 @@ class SaleOrder(models.Model): ('PNR', 'Pareto Non Repeating'), ('NP', 'Non Pareto') ]) + # estimated_ready_ship_date = fields.Datetime( + # string='ET Ready to Ship compute', + # compute='_compute_etrts_date' + # ) + expected_ready_to_ship = fields.Datetime( + string='ET Ready to Ship', + copy=False + ) + shipping_method_picking = fields.Char(string='Shipping Method Picking', compute='_compute_shipping_method_picking') + + reason_cancel = fields.Selection([ + ('harga_terlalu_mahal', 'Harga barang terlalu mahal'), + ('harga_web_tidak_valid', 'Harga web tidak valid'), + ('stok_kosong', 'Stock kosong'), + ('tidak_mau_indent', 'Customer tidak mau indent'), + ('batal_rencana_pembelian', 'Customer membatalkan rencana pembelian'), + ('vendor_tidak_support_demo', 'Vendor tidak support demo/trial product'), + ('product_knowledge_kurang', 'Product knowledge kurang baik'), + ('barang_tidak_sesuai', 'Barang tidak sesuai/tepat'), + ('tidak_sepakat_pembayaran', 'Tidak menemukan kesepakatan untuk pembayaran'), + ('dokumen_tidak_support', 'Indoteknik tidak bisa support document yang dibutuhkan (Ex: TKDN, COO, SNI)'), + ('ganti_quotation', 'Ganti Quotation'), + ('testing_internal', 'Testing Internal'), + ('revisi_data', 'Revisi Data'), + ], string='Reason for Cancel', copy=False, index=True, tracking=3) + attachment_bukti = fields.Many2one( + 'ir.attachment', + string="Attachment Bukti Cancel", readonly=False, + ) + 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)]") + hold_outgoing = fields.Boolean('Hold Outgoing SO', tracking=3) + state_ask_cancel = fields.Selection([ + ('hold', 'Hold'), + ('approve', 'Approve') + ], tracking=True, string='State Cancel', copy=False) + + def _compute_total_margin_excl_third_party(self): + for order in self: + if order.amount_untaxed == 0: + order.total_margin_excl_third_party = 0 + continue + + # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2) + order.total_margin_excl_third_party = round((order.total_before_margin / (order.amount_untaxed)) * 100, 2) + # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed)) * 100, 2) + + def ask_retur_cancel_purchasing(self): + for rec in self: + if self.env.user.has_group('indoteknik_custom.group_role_purchasing'): + rec.state_ask_cancel = 'approve' + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Persetujuan Diberikan', + 'message': 'Proses cancel sudah disetujui', + 'type': 'success', + 'sticky': True + } + } + else: + rec.state_ask_cancel = 'hold' + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Menunggu Persetujuan', + 'message': 'Tim Purchasing akan memproses permintaan Anda', + 'type': 'warning', + 'sticky': False + } + } + + def hold_unhold_qty_outgoing_so(self): + if self.hold_outgoing == True: + self.hold_outgoing = False + else: + self.hold_outgoing = True + + 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: + # if rec.fee_third_party == 0 and rec.total_margin_excl_third_party != rec.total_percent_margin: + # # Gunakan direct SQL atau flag context untuk menghindari rekursi + # self.env.cr.execute(""" + # UPDATE sale_order + # SET total_margin_excl_third_party = %s + # WHERE id = %s + # """, (rec.total_percent_margin, rec.id)) + # self.invalidate_cache() + + @api.constrains('shipping_option_id') + def _check_shipping_option(self): + for rec in self: + if rec.shipping_option_id: + rec.delivery_amt = rec.shipping_option_id.price + + def _compute_shipping_method_picking(self): + for order in self: + if order.picking_ids: + carrier_names = order.picking_ids.mapped('carrier_id.name') + order.shipping_method_picking = ', '.join(filter(None, carrier_names)) + else: + order.shipping_method_picking = False @api.onchange('payment_status') def _is_continue_transaction(self): @@ -183,15 +420,36 @@ class SaleOrder(models.Model): if total_weight == 0: raise UserError("Tidak dapat mengestimasi ongkir tanpa berat yang valid.") - + if total_weight < 10: total_weight = 10 + self.delivery_amt = total_weight * 3000 - + + shipping_option = self.env["shipping.option"].create({ + "name": "Indoteknik Delivery", + "price": self.delivery_amt, + "provider": "Indoteknik", + "etd": "1-2 Hari", + "sale_order_id": self.id, + }) + self.shipping_option_id = shipping_option.id + self.message_post( + body=( + f"<b>Estimasi pengiriman Indoteknik berhasil:</b><br/>" + f"Layanan: {shipping_option.name}<br/>" + f"ETD: {shipping_option.etd}<br/>" + f"Biaya: Rp {shipping_option.price:,}<br/>" + f"Provider: {shipping_option.provider}" + ), + message_type="comment", + ) + def action_estimate_shipping(self): if self.carrier_id.id in [1, 151]: self.action_indoteknik_estimate_shipping() return + total_weight = 0 missing_weight_products = [] @@ -209,35 +467,50 @@ class SaleOrder(models.Model): if total_weight == 0: raise UserError("Tidak dapat mengestimasi ongkir tanpa berat yang valid.") - # Mendapatkan city_id berdasarkan nama kota - origin_city_name = self.warehouse_id.partner_id.kota_id.name destination_subsdistrict_id = self.real_shipping_id.kecamatan_id.rajaongkir_id - if not destination_subsdistrict_id: - raise UserError("Gagal mendapatkan ID kota asal atau tujuan.") + raise UserError("Gagal mendapatkan ID kota tujuan.") result = self._call_rajaongkir_api(total_weight, destination_subsdistrict_id) if result: - estimated_cost = result['rajaongkir']['results'][0]['costs'][0]['cost'][0]['value'] - self.delivery_amt = estimated_cost - - shipping_info = [] + shipping_options = [] for courier in result['rajaongkir']['results']: for cost_detail in courier['costs']: service = cost_detail['service'] description = cost_detail['description'] etd = cost_detail['cost'][0]['etd'] value = cost_detail['cost'][0]['value'] - shipping_info.append(f"Service: {service}, Description: {description}, ETD: {etd} hari, Cost: Rp {value}") + shipping_options.append((service, description, etd, value, courier['code'])) + + self.env["shipping.option"].search([('sale_order_id', '=', self.id)]).unlink() + + _logger.info(f"Shipping options: {shipping_options}") + + for service, description, etd, value, provider in shipping_options: + self.env["shipping.option"].create({ + "name": service, + "price": value, + "provider": provider, + "etd": etd, + "sale_order_id": self.id, + }) + - log_message = "<br/>".join(shipping_info) + self.shipping_option_id = self.env["shipping.option"].search([('sale_order_id', '=', self.id)], limit=1).id + + _logger.info(f"Shipping option SO ID: {self.shipping_option_id}") + + self.message_post( + body=f"Estimasi Ongkos Kirim: Rp{self.delivery_amt}<br/>Detail Lain:<br/>" + f"{'<br/>'.join([f'Service: {s[0]}, Description: {s[1]}, ETD: {s[2]} hari, Cost: Rp {s[3]}' for s in shipping_options])}", + message_type="comment" + ) + + # self.message_post(body=f"Estimasi Ongkos Kirim: Rp{self.delivery_amt}<br/>Detail Lain:<br/>{'<br/>'.join([f'Service: {s[0]}, Description: {s[1]}, ETD: {s[2]} hari, Cost: Rp {s[3]}' for s in shipping_options])}", message_type="comment") - description_ongkir = result['rajaongkir']['results'][0]['costs'][0]['description'] - etd_ongkir = result['rajaongkir']['results'][0]['costs'][0]['cost'][0]['etd'] - service_ongkir = result['rajaongkir']['results'][0]['costs'][0]['service'] - self.message_post(body=f"Estimasi Ongkos Kirim: Rp{self.delivery_amt}<br/>Service: {service_ongkir}<br/>Description: {description_ongkir}<br/>ETD: {etd_ongkir}<br/>Detail Lain:<br/>{log_message}") else: raise UserError("Gagal mendapatkan estimasi ongkir.") + def _call_rajaongkir_api(self, total_weight, destination_subsdistrict_id): url = 'https://pro.rajaongkir.com/api/cost' @@ -337,11 +610,11 @@ class SaleOrder(models.Model): delivery_amt = order.delivery_amt else: delivery_amt = 0 - order.percent_margin_after_delivery_purchase = round((order.margin_after_delivery_purchase / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2) + order.percent_margin_after_delivery_purchase = round((order.margin_after_delivery_purchase / (order.amount_untaxed-delivery_amt-order.fee_third_party-order.biaya_lain_lain)) * 100, 2) def _compute_date_kirim(self): for rec in self: - picking = self.env['stock.picking'].search([('sale_id', '=', rec.id), ('state', 'not in', ['cancel'])], order='date_doc_kirim desc', limit=1) + picking = self.env['stock.picking'].search([('sale_id', '=', rec.id), ('state', 'not in', ['cancel']), ('name', 'not ilike', 'BU/PICK/%')], order='date_doc_kirim desc', limit=1) rec.date_kirim_ril = picking.date_doc_kirim rec.date_status_done = picking.date_done rec.date_driver_arrival = picking.driver_arrival_date @@ -363,19 +636,131 @@ class SaleOrder(models.Model): rec.compute_fullfillment = True + @api.depends('date_order', 'estimated_arrival_days', 'state', 'estimated_arrival_days_start') def _compute_eta_date(self): - max_leadtime = 0 + current_date = datetime.now().date() + for rec in self: + if rec.date_order and rec.state not in ['cancel'] and rec.estimated_arrival_days and rec.estimated_arrival_days_start: + rec.eta_date = current_date + timedelta(days=rec.estimated_arrival_days) + rec.eta_date_start = current_date + timedelta(days=rec.estimated_arrival_days_start) + else: + rec.eta_date = False + rec.eta_date_start = False + + + def get_days_until_next_business_day(self,start_date=None, *args, **kwargs): + today = start_date or datetime.today().date() + offset = 0 # Counter jumlah hari yang ditambahkan + holiday = self.env['hr.public.holiday'] + + while True : + today += timedelta(days=1) + offset += 1 + + if today.weekday() >= 5: + continue - for line in self.order_line: - leadtime = line.vendor_id.leadtime - max_leadtime = max(max_leadtime, leadtime) + is_holiday = holiday.search([("start_date", "=", today)]) + if is_holiday: + continue + + break + + return offset + + def calculate_sla_by_vendor(self, products): + product_ids = products.mapped('product_id.id') # Kumpulkan semua ID produk + include_instant = True # Default True, tetapi bisa menjadi False + + # Cek apakah SEMUA produk memiliki qty_free_bandengan >= qty_needed + all_fast_products = all(product.product_id.qty_free_bandengan >= product.product_uom_qty for product in products) + if all_fast_products: + return {'slatime': 1, 'include_instant': include_instant} + + # Cari semua vendor pemenang untuk produk yang diberikan + vendors = self.env['purchase.pricelist'].search([ + ('product_id', 'in', product_ids), + ('is_winner', '=', True) + ]) + max_slatime = 1 + + for vendor in vendors: + vendor_sla = self.env['vendor.sla'].search([('id_vendor', '=', vendor.vendor_id.id)], limit=1) + slatime = 15 + if vendor_sla: + if vendor_sla.unit == 'hari': + vendor_duration = vendor_sla.duration * 24 * 60 + include_instant = False + else : + vendor_duration = vendor_sla.duration * 60 + include_instant = True + + estimation_sla = (1 * 24 * 60) + vendor_duration + estimation_sla_days = estimation_sla / (24 * 60) + slatime = math.ceil(estimation_sla_days) + + max_slatime = max(max_slatime, slatime) + + return {'slatime': max_slatime, 'include_instant': include_instant} + + def _calculate_etrts_date(self): for rec in self: - if rec.date_order and rec.state not in ['cancel', 'draft']: - eta_date = datetime.now() + timedelta(days=max_leadtime) - rec.eta_date = eta_date - else: - rec.eta_date = False + if not rec.date_order: + rec.expected_ready_to_ship = False + return + + current_date = datetime.now().date() + + max_slatime = 1 # Default SLA jika tidak ada + slatime = self.calculate_sla_by_vendor(rec.order_line) + max_slatime = max(max_slatime, slatime['slatime']) + + sum_days = max_slatime + self.get_days_until_next_business_day(current_date) - 1 + if not rec.estimated_arrival_days: + rec.estimated_arrival_days = sum_days + + eta_date = current_date + timedelta(days=sum_days) + rec.commitment_date = eta_date + rec.expected_ready_to_ship = eta_date + + @api.depends("order_line.product_id", "date_order") + def _compute_etrts_date(self): #Function to calculate Estimated Ready To Ship Date + self._calculate_etrts_date() + + + def _validate_expected_ready_ship_date(self): + for rec in self: + if rec.expected_ready_to_ship and rec.commitment_date: + current_date = datetime.now().date() + # Hanya membandingkan tanggal saja, tanpa jam + expected_date = rec.expected_ready_to_ship.date() + + max_slatime = 1 # Default SLA jika tidak ada + slatime = self.calculate_sla_by_vendor(rec.order_line) + max_slatime = max(max_slatime, slatime['slatime']) + sum_days = max_slatime + self.get_days_until_next_business_day(current_date) - 1 + eta_minimum = current_date + timedelta(days=sum_days) + + if expected_date < eta_minimum: + rec.expected_ready_to_ship = eta_minimum + raise ValidationError( + "Tanggal 'Expected Ready to Ship' tidak boleh lebih kecil dari {}. Mohon pilih tanggal minimal {}." + .format(eta_minimum.strftime('%d-%m-%Y'), eta_minimum.strftime('%d-%m-%Y')) + ) + else: + rec.commitment_date = rec.expected_ready_to_ship + + + @api.onchange('expected_ready_to_ship') #Hangle Onchange form Expected Ready to Ship + def _onchange_expected_ready_ship_date(self): + self._validate_expected_ready_ship_date() + + def _set_etrts_date(self): + for order in self: + if order.state in ('done', 'cancel', 'sale'): + raise UserError(_("You cannot change the Estimated Ready To Ship Date on a done, sale or cancelled order.")) + # order.move_lines.write({'estimated_ready_ship_date': order.estimated_ready_ship_date}) def _prepare_invoice(self): """ @@ -426,6 +811,33 @@ class SaleOrder(models.Model): if self.email and not re.match(pattern, self.email): raise UserError('Email yang anda input kurang valid') + # @api.constrains('delivery_amt', 'carrier_id', 'shipping_cost_covered') + def _validate_delivery_amt(self): + is_indoteknik = self.carrier_id.id == 1 or self.shipping_cost_covered == 'indoteknik' + is_active_id = not self.env.context.get('active_id', []) + + if is_indoteknik and is_active_id: + if self.delivery_amt == 0: + if self.carrier_id.id == 1: + raise UserError('Untuk Kurir Indoteknik Delivery, estimasi ongkos kirim belum diisi.') + else: + raise UserError('Untuk Shipping Covered Indoteknik, estimasi ongkos kirim belum diisi.') + + if self.delivery_amt < 100: + if self.carrier_id.id == 1: + raise UserError('Untuk Kurir Indoteknik Delivery, estimasi ongkos kirim belum memenuhi tarif minimum.') + else: + raise UserError('Untuk Shipping Covered Indoteknik, estimasi ongkos kirim belum memenuhi tarif minimum.') + + + # if self.delivery_amt < 5000: + # if (self.carrier_id.id == 1 or self.shipping_cost_covered == 'indoteknik') and not self.env.context.get('active_id', []): + # if self.carrier_id.id == 1: + # raise UserError('Untuk Kurir Indoteknik Delivery, estimasi ongkos kirim belum memenuhi jumlah minimum.') + # else: + # raise UserError('Untuk Shipping Covered Indoteknik, estimasi ongkos kirim belum memenuhi jumlah minimum.') + + def override_allow_create_invoice(self): if not self.env.user.is_accounting: raise UserError('Hanya Finance Accounting yang dapat klik tombol ini') @@ -540,22 +952,22 @@ class SaleOrder(models.Model): redirect_url = json.loads(lookup_json)['redirect_url'] self.payment_link_midtrans = str(redirect_url) - # Generate QR code - qr = qrcode.QRCode( - version=1, - error_correction=qrcode.constants.ERROR_CORRECT_L, - box_size=10, - border=4, - ) - qr.add_data(redirect_url) - qr.make(fit=True) - img = qr.make_image(fill_color="black", back_color="white") + if 'redirect_url' in response: + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=10, + border=4, + ) + qr.add_data(redirect_url) + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") - buffer = BytesIO() - img.save(buffer, format="PNG") - qr_code_img = base64.b64encode(buffer.getvalue()).decode() + buffer = BytesIO() + img.save(buffer, format="PNG") + qr_code_img = base64.b64encode(buffer.getvalue()).decode() - self.payment_qr_code = qr_code_img + self.payment_qr_code = qr_code_img @api.model def _generate_so_access_token(self, limit=50): @@ -570,17 +982,14 @@ class SaleOrder(models.Model): if line.product_id.type == 'product': line_no += 1 line.line_no = line_no + def write(self, vals): - res = super(SaleOrder, self).write(vals) - if 'carrier_id' in vals: for picking in self.picking_ids: if picking.state == 'assigned': picking.carrier_id = self.carrier_id - return res - def calculate_so_status(self): so_state = ['sale'] sales = self.search([ @@ -634,6 +1043,13 @@ class SaleOrder(models.Model): # return [('id', 'not in', order_ids)] # return ['&', ('order_line.invoice_lines.move_id.move_type', 'in', ('out_invoice', 'out_refund')), ('order_line.invoice_lines.move_id', operator, value)] + @api.depends('partner_id') + def _compute_partner_field(self): + for order in self: + partner = order.partner_id.parent_id or order.partner_id + order.npwp = partner.npwp + order.sppkp = partner.sppkp + order.customer_type = partner.customer_type @api.onchange('partner_id') def onchange_partner_contact(self): @@ -735,6 +1151,7 @@ class SaleOrder(models.Model): self._validate_order() for order in self: + order._validate_uniform_taxes() order.order_line.validate_line() term_days = 0 @@ -756,15 +1173,38 @@ class SaleOrder(models.Model): raise UserError("Customer Reference kosong, di isi dengan NO PO jika PO tidak ada mohon ditulis Tanpa PO") if not order.user_id.active: raise UserError("Salesperson sudah tidak aktif, mohon diisi yang benar pada data SO dan Contact") - + + def check_product_bom(self): + for order in self: + for line in order.order_line: + if 'bom-it' in line.name.lower() or 'bom' in line.product_id.default_code.lower() if line.product_id.default_code else False: + search_bom = self.env['mrp.production'].search([('product_id', '=', line.product_id.id)],order='name desc') + if search_bom: + confirmed_bom = search_bom.filtered(lambda x: x.state == 'confirmed') + if not confirmed_bom: + raise UserError("Product BOM belum dikonfirmasi di Manufacturing Orders. Silakan hubungi MD.") + else: + raise UserError("Product BOM tidak di temukan di manufacturing orders, silahkan hubungi MD") + + def check_duplicate_product(self): + for order in self: + for line in order.order_line: + search_product = self.env['sale.order.line'].search([('product_id', '=', line.product_id.id), ('order_id', '=', order.id)]) + if len(search_product) > 1: + raise UserError("Terdapat DUPLIKASI data pada Product {}".format(line.product_id.display_name)) + def sale_order_approve(self): + self.check_duplicate_product() + self.check_product_bom() self.check_credit_limit() + self.check_limit_so_to_invoice() if self.validate_different_vendor() and not self.vendor_approval: return self._create_notification_action('Notification', 'Terdapat Vendor yang berbeda dengan MD Vendor') self.check_due() 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() @@ -816,7 +1256,9 @@ class SaleOrder(models.Model): order.approval_status = 'pengajuan2' return self._create_approval_notification('Pimpinan') elif order._requires_approval_margin_manager(): + self.check_product_bom() self.check_credit_limit() + self.check_limit_so_to_invoice() order.approval_status = 'pengajuan1' return self._create_approval_notification('Sales Manager') @@ -923,6 +1365,29 @@ class SaleOrder(models.Model): raise UserError(_("%s is in Blocking Stage, Remaining credit limit is %s, from %s and outstanding %s") % (rec.partner_id.name, remaining_credit_limit, block_stage, outstanding_amount)) + def check_limit_so_to_invoice(self): + for rec in self: + # Ambil jumlah outstanding_amount dan rec.amount_total sebagai current_total + outstanding_amount = rec.outstanding_amount + current_total = rec.amount_total + outstanding_amount + + # Ambil blocking stage dari partner + block_stage = rec.partner_id.parent_id.blocking_stage if rec.partner_id.parent_id else rec.partner_id.blocking_stage or 0 + is_cbd = rec.partner_id.parent_id.property_payment_term_id.id == 26 if rec.partner_id.parent_id else rec.partner_id.property_payment_term_id.id == 26 or False + + # Ambil jumlah nilai dari SO yang invoice_status masih 'to invoice' + so_to_invoice = 0 + for sale in rec.partner_id.sale_order_ids: + if sale.invoice_status == 'to invoice': + so_to_invoice = so_to_invoice + sale.amount_total + # Hitung remaining credit limit + remaining_credit_limit = block_stage - current_total - so_to_invoice + + # Validasi limit + if remaining_credit_limit <= 0 and block_stage > 0 and not is_cbd: + raise UserError(_("The credit limit for %s will exceed the Blocking Stage if the Sale Order is confirmed. The remaining credit limit is %s, from %s and the outstanding amount is %s.") + % (rec.partner_id.name, block_stage - current_total, block_stage, outstanding_amount)) + def validate_different_vendor(self): if self.vendor_approval_id.filtered(lambda v: v.state == 'draft'): draft_names = ", ".join(self.vendor_approval_id.filtered(lambda v: v.state == 'draft').mapped('number')) @@ -971,6 +1436,11 @@ 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() + order.check_limit_so_to_invoice() if self.validate_different_vendor() and not self.vendor_approval: return self._create_notification_action('Notification', 'Terdapat Vendor yang berbeda dengan MD Vendor') @@ -1009,6 +1479,7 @@ class SaleOrder(models.Model): order._set_sppkp_npwp_contact() order.calculate_line_no() order.send_notif_to_salesperson() + # order._compute_etrts_date() # order.order_line.get_reserved_from() res = super(SaleOrder, self).action_confirm() @@ -1026,15 +1497,22 @@ class SaleOrder(models.Model): def action_cancel(self): # TODO stephan prevent cancel if have invoice, do, and po + if self.state_ask_cancel != 'approve' and self.state not in ['draft', 'sent']: + raise UserError("Anda harus approval purchasing terlebih dahulu") main_parent = self.partner_id.get_main_parent() if self._name != 'sale.order': return super(SaleOrder, self).action_cancel() if self.have_outstanding_invoice: raise UserError("Invoice harus di Cancel dahulu") + + disallow_states = ['draft', 'waiting', 'confirmed', 'assigned'] + for picking in self.picking_ids: + if picking.state in disallow_states: + raise UserError("DO yang draft, waiting, confirmed, atau assigned harus di-cancel oleh Logistik") for line in self.order_line: if line.qty_delivered > 0: - raise UserError("DO harus di-cancel terlebih dahulu.") + raise UserError("DO yang done harus di-Return oleh Logistik") if not self.web_approval: self.web_approval = 'company' @@ -1045,8 +1523,24 @@ class SaleOrder(models.Model): self.due_id = False if main_parent.use_so_approval: self.send_notif_to_salesperson(cancel=True) + for order in self: + if order.amount_total > 30000000: + return { + 'type': 'ir.actions.act_window', + 'name': _('Cancel Reason'), + 'res_model': 'cancel.reason.order', + 'view_mode': 'form', + 'target': 'new', + 'context': {'default_request_id': self.id}, + } return super(SaleOrder, self).action_cancel() - + + def confirm_cancel_order(self): + """Fungsi ini akan dipanggil oleh wizard setelah alasan pembatalan dipilih""" + if self.state != 'cancel': + self.state = 'cancel' + return super(SaleOrder, self).action_cancel() + def validate_partner_invoice_due(self): parent_id = self.partner_id.parent_id.id parent_id = parent_id if parent_id else self.partner_id.id @@ -1078,10 +1572,11 @@ class SaleOrder(models.Model): return False def _requires_approval_margin_leader(self): - return self.total_percent_margin <= 13 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 <= 20 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' @@ -1118,8 +1613,16 @@ class SaleOrder(models.Model): def _compute_total_margin(self): for order in self: total_margin = sum(line.item_margin for line in order.order_line if line.product_id) + if order.ongkir_ke_xpdc: + total_margin -= order.ongkir_ke_xpdc + order.total_margin = total_margin + def _compute_total_before_margin(self): + for order in self: + total_before_margin = sum(line.item_before_margin for line in order.order_line if line.product_id) + order.total_before_margin = total_before_margin + def _compute_total_percent_margin(self): for order in self: if order.amount_untaxed == 0: @@ -1129,7 +1632,9 @@ class SaleOrder(models.Model): delivery_amt = order.delivery_amt else: delivery_amt = 0 - order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2) + + # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2) + order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-order.fee_third_party-order.biaya_lain_lain)) * 100, 2) # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed)) * 100, 2) @api.onchange('sales_tax_id') @@ -1383,18 +1888,22 @@ class SaleOrder(models.Model): def create(self, vals): # Ensure partner details are updated when a sale order is created order = super(SaleOrder, self).create(vals) + order._compute_etrts_date() + order._validate_expected_ready_ship_date() + order._validate_delivery_amt() + # order._check_total_margin_excl_third_party() # order._update_partner_details() return order - def write(self, vals): + # def write(self, vals): # Call the super method to handle the write operation - res = super(SaleOrder, self).write(vals) - + # res = super(SaleOrder, self).write(vals) + # self._compute_etrts_date() # Check if the update is coming from a save operation # if any(field in vals for field in ['sppkp', 'npwp', 'email', 'customer_type']): # self._update_partner_details() - return res + # return res def _update_partner_details(self): for order in self: @@ -1423,5 +1932,11 @@ class SaleOrder(models.Model): if command[0] == 0: # A new line is being added raise UserError( "SO tidak dapat ditambahkan produk baru karena SO sudah menjadi sale order.") + res = super(SaleOrder, self).write(vals) + # self._check_total_margin_excl_third_party() + if any(fields in vals for fields in ['delivery_amt', 'carrier_id', 'shipping_cost_covered']): + self._validate_delivery_amt() + if any(field in vals for field in ["order_line", "client_order_ref"]): + self._calculate_etrts_date() return res
\ No newline at end of file diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index aed95aab..2450abd4 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta class SaleOrderLine(models.Model): _inherit = 'sale.order.line' item_margin = fields.Float('Margin', compute='compute_item_margin', help="Total Margin in Sales Order Header") + item_before_margin = fields.Float('Before Margin', compute='compute_item_before_margin', help="Total Margin in Sales Order Header") item_percent_margin = fields.Float('%Margin', compute='compute_item_margin', help="Total % Margin in Sales Order Header") initial_discount = fields.Float('Initial Discount') vendor_id = fields.Many2one( @@ -146,6 +147,24 @@ class SaleOrderLine(models.Model): if not line.margin_md: line.margin_md = line.item_percent_margin + def compute_item_before_margin(self): + for line in self: + if not line.product_id or line.product_id.type == 'service' \ + or line.price_unit <= 0 or line.product_uom_qty <= 0 \ + or not line.vendor_id: + line.item_before_margin = 0 + continue + # calculate margin without tax + sales_price = line.price_reduce_taxexcl * line.product_uom_qty + + purchase_price = line.purchase_price + if line.purchase_tax_id.price_include: + purchase_price = line.purchase_price / 1.11 + + purchase_price = purchase_price * line.product_uom_qty + margin_per_item = sales_price - purchase_price + line.item_before_margin = margin_per_item + @api.onchange('vendor_id') def onchange_vendor_id(self): # TODO : need to change this logic @stephan diff --git a/indoteknik_custom/models/sale_order_multi_uangmuka_penjualan.py b/indoteknik_custom/models/sale_order_multi_uangmuka_penjualan.py index f9120290..96c2f676 100644 --- a/indoteknik_custom/models/sale_order_multi_uangmuka_penjualan.py +++ b/indoteknik_custom/models/sale_order_multi_uangmuka_penjualan.py @@ -68,7 +68,7 @@ class PurchaseOrderMultiUangmukaPenjualan(models.TransientModel): param_credit = { 'move_id': account_move.id, - 'account_id': 449, # uang muka penjualan + 'account_id': 668, # penerimaan belum alokasi 'partner_id': partner_id, 'currency_id': 12, 'debit': 0, diff --git a/indoteknik_custom/models/sales_order_koli.py b/indoteknik_custom/models/sales_order_koli.py new file mode 100644 index 00000000..c782a40e --- /dev/null +++ b/indoteknik_custom/models/sales_order_koli.py @@ -0,0 +1,26 @@ +from odoo import fields, models, api, _ +from odoo.exceptions import AccessError, UserError, ValidationError +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT +import logging + +_logger = logging.getLogger(__name__) + + +class SalesOrderKoli(models.Model): + _name = 'sales.order.koli' + _description = 'Sales Order Koli' + _order = 'sale_order_id, id' + _rec_name = 'koli_id' + + sale_order_id = fields.Many2one( + 'sale.order', + string='Sale Order Reference', + required=True, + ondelete='cascade', + index=True, + copy=False, + ) + koli_id = fields.Many2one('check.koli', string='Koli') + picking_id = fields.Many2one('stock.picking', string='Picking') + state = fields.Selection([('not_delivered', 'Not Delivered'), ('delivered', 'Delivered')], string='Status', default='not_delivered') + diff --git a/indoteknik_custom/models/sales_order_reject.py b/indoteknik_custom/models/sales_order_reject.py index 9983c64e..b180fad6 100644 --- a/indoteknik_custom/models/sales_order_reject.py +++ b/indoteknik_custom/models/sales_order_reject.py @@ -13,3 +13,30 @@ class SalesOrderReject(models.Model): product_id = fields.Many2one('product.product', string='Product') qty_reject = fields.Float(string='Qty') reason_reject = fields.Char(string='Reason Reject') + + def write(self, vals): + # Check if reason_reject is being updated + if 'reason_reject' in vals: + for record in self: + old_reason = record.reason_reject + new_reason = vals['reason_reject'] + + # Only post a message if the reason actually changed + if old_reason != new_reason: + now = fields.Datetime.now() + + # Create the log note for the updated reason + log_note = (f"<li>Product '{record.product_id.name}' rejection reason updated:</li>" + f"<li>From: {old_reason}</li>" + f"<li>To: {new_reason}</li>" + f"<li>Updated on: {now.strftime('%d-%m-%Y')} at {now.strftime('%H:%M:%S')}</li>") + + # Post ke lognote + if record.sale_order_id: + record.sale_order_id.message_post( + body=log_note, + author_id=self.env.user.partner_id.id, + ) + + # Call the original write method + return super(SalesOrderReject, self).write(vals)
\ No newline at end of file diff --git a/indoteknik_custom/models/shipment_group.py b/indoteknik_custom/models/shipment_group.py index df3f1bb4..b7d7ac12 100644 --- a/indoteknik_custom/models/shipment_group.py +++ b/indoteknik_custom/models/shipment_group.py @@ -14,6 +14,13 @@ class ShipmentGroup(models.Model): number = fields.Char(string='Document No', index=True, copy=False, readonly=True, tracking=True) shipment_line = fields.One2many('shipment.group.line', 'shipment_id', string='Shipment Group Lines', auto_join=True) partner_id = fields.Many2one('res.partner', string='Customer') + carrier_id = fields.Many2one('delivery.carrier', string='Ekspedisi') + total_colly_line = fields.Float(string='Total Colly', compute='_compute_total_colly_line') + + @api.depends('shipment_line.total_colly') + def _compute_total_colly_line(self): + for rec in self: + rec.total_colly_line = sum(rec.shipment_line.mapped('total_colly')) @api.model def create(self, vals): @@ -35,6 +42,26 @@ class ShipmentGroupLine(models.Model): ('indoteknik', 'Indoteknik'), ('customer', 'Customer') ], string='Shipping Paid by', copy=False) + total_colly = fields.Float(string='Total Colly') + carrier_id = fields.Many2one('delivery.carrier', string='Ekspedisi') + + @api.constrains('picking_id') + def _check_picking_id(self): + for rec in self: + if not rec.picking_id: + continue + + duplicates = self.env['shipment.group.line'].search([ + ('picking_id', '=', rec.picking_id.id), + ('id', '!=', rec.id) + ]) + + if duplicates: + shipment_numbers = duplicates.mapped('shipment_id.number') + raise UserError( + f"Picking {rec.picking_id.name} sudah discan dalam shipment group berikut: {', '.join(shipment_numbers)}! " + "Satu picking hanya boleh dimasukkan dalam satu shipment group." + ) @api.depends('picking_id.state') def _compute_state(self): @@ -63,12 +90,20 @@ class ShipmentGroupLine(models.Model): if self.shipment_id.partner_id and self.shipment_id.partner_id != picking.partner_id: raise UserError('Partner must be same as shipment group') + if self.shipment_id.carrier_id and self.shipment_id.carrier_id != picking.carrier_id: + raise UserError('carrier must be same as shipment group') + self.partner_id = picking.partner_id self.shipping_paid_by = picking.sale_id.shipping_paid_by + self.carrier_id = picking.carrier_id.id + self.total_colly = picking.total_mapping_koli if not self.shipment_id.partner_id: self.shipment_id.partner_id = picking.partner_id + if not self.shipment_id.carrier_id: + self.shipment_id.carrier_id = picking.carrier_id + self.sale_id = picking.sale_id @api.model 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 87e8370f..c4aefe19 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') @@ -76,12 +76,21 @@ class ProductTemplate(models.Model): ('product_id', 'in', template.product_variant_ids.ids), ('location_id', 'in', target_locations), ]) - - is_in_bu = any(quant.available_quantity > 0 for quant in stock_quant) + is_in_bu = False + for quant in stock_quant: + if quant.product_id.qty_free_bandengan > 0: + is_in_bu = True + break 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, @@ -91,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 if carousel_images else [], '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/solr/x_banner_banner.py b/indoteknik_custom/models/solr/x_banner_banner.py index 8452644c..aa6e0c2a 100644 --- a/indoteknik_custom/models/solr/x_banner_banner.py +++ b/indoteknik_custom/models/solr/x_banner_banner.py @@ -23,7 +23,7 @@ class XBannerBanner(models.Model): 'function_name': function_name }) - @api.constrains('x_name', 'x_url_banner', 'background_color', 'x_banner_image', 'x_banner_category', 'x_relasi_manufacture', 'x_sequence_banner', 'x_status_banner', 'sequence', 'group_by_week', 'headline_banner_s', 'description_banner_s') + @api.constrains('x_name', 'x_url_banner', 'background_color', 'x_banner_image', 'x_banner_category', 'x_relasi_manufacture', 'x_sequence_banner', 'x_status_banner', 'sequence', 'group_by_week', 'headline_banner_s', 'description_banner_s', 'x_keyword_banner') def _create_solr_queue_sync_brands(self): self._create_solr_queue('_sync_banners_to_solr') @@ -51,6 +51,7 @@ class XBannerBanner(models.Model): 'group_by_week': banners.group_by_week or '', 'headline_banner_s': banners.x_headline_banner or '', 'description_banner_s': banners.x_description_banner or '', + 'keyword_banner_s': banners.x_keyword_banner or '', }) self.solr().add([document]) banners.update_last_update_solr() diff --git a/indoteknik_custom/models/stock_backorder_confirmation.py b/indoteknik_custom/models/stock_backorder_confirmation.py new file mode 100644 index 00000000..d8a41f54 --- /dev/null +++ b/indoteknik_custom/models/stock_backorder_confirmation.py @@ -0,0 +1,33 @@ +from odoo import models, fields, api +from odoo.tools.float_utils import float_compare + +class StockBackorderConfirmation(models.TransientModel): + _inherit = 'stock.backorder.confirmation' + + def process(self): + pickings_to_do = self.env['stock.picking'] + pickings_not_to_do = self.env['stock.picking'] + for line in self.backorder_confirmation_line_ids: + line.picking_id.send_mail_bills() + # line.picking_id.send_koli_to_so() + if line.to_backorder is True: + pickings_to_do |= line.picking_id + else: + pickings_not_to_do |= line.picking_id + + for pick_id in pickings_not_to_do: + moves_to_log = {} + for move in pick_id.move_lines: + if float_compare(move.product_uom_qty, + move.quantity_done, + precision_rounding=move.product_uom.rounding) > 0: + moves_to_log[move] = (move.quantity_done, move.product_uom_qty) + pick_id._log_less_quantities_than_expected(moves_to_log) + + pickings_to_validate = self.env.context.get('button_validate_picking_ids') + if pickings_to_validate: + pickings_to_validate = self.env['stock.picking'].browse(pickings_to_validate).with_context(skip_backorder=True) + if pickings_not_to_do: + pickings_to_validate = pickings_to_validate.with_context(picking_ids_not_to_backorder=pickings_not_to_do.ids) + return pickings_to_validate.button_validate() + return True diff --git a/indoteknik_custom/models/stock_immediate_transfer.py b/indoteknik_custom/models/stock_immediate_transfer.py index 4be0dff2..c2a293f9 100644 --- a/indoteknik_custom/models/stock_immediate_transfer.py +++ b/indoteknik_custom/models/stock_immediate_transfer.py @@ -5,18 +5,20 @@ class StockImmediateTransfer(models.TransientModel): _inherit = 'stock.immediate.transfer' def process(self): - """Override process method to add send_mail_bills logic.""" pickings_to_do = self.env['stock.picking'] pickings_not_to_do = self.env['stock.picking'] for line in self.immediate_transfer_line_ids: + line.picking_id.send_mail_bills() + line.picking_id.send_koli_to_so() if line.to_immediate is True: pickings_to_do |= line.picking_id else: pickings_not_to_do |= line.picking_id for picking in pickings_to_do: - picking.send_mail_bills() + # picking.send_mail_bills() + # picking.send_koli_to_so() # If still in draft => confirm and assign if picking.state == 'draft': picking.action_confirm() @@ -24,6 +26,7 @@ class StockImmediateTransfer(models.TransientModel): picking.action_assign() if picking.state != 'assigned': raise UserError(_("Could not reserve all requested products. Please use the 'Mark as Todo' button to handle the reservation manually.")) + for move in picking.move_lines.filtered(lambda m: m.state not in ['done', 'cancel']): for move_line in move.move_line_ids: move_line.qty_done = move_line.product_uom_qty @@ -33,4 +36,6 @@ class StockImmediateTransfer(models.TransientModel): pickings_to_validate = self.env['stock.picking'].browse(pickings_to_validate) pickings_to_validate = pickings_to_validate - pickings_not_to_do return pickings_to_validate.with_context(skip_immediate=True).button_validate() + return True + diff --git a/indoteknik_custom/models/stock_inventory.py b/indoteknik_custom/models/stock_inventory.py new file mode 100644 index 00000000..69cca5bc --- /dev/null +++ b/indoteknik_custom/models/stock_inventory.py @@ -0,0 +1,79 @@ +from odoo import models, api, fields +from odoo.exceptions import UserError +from datetime import datetime +import logging + +_logger = logging.getLogger(__name__) + + +class StockInventory(models.Model): + _inherit = ['stock.inventory'] + _order = 'id desc' + _rec_name = 'number' + + number = fields.Char(string='Document No', index=True, copy=False, readonly=True) + adjusment_type = fields.Selection([ + ('in', 'Adjusment In'), + ('out', 'Adjusment Out'), + ], string='Adjusments Type', required=True) + + def _generate_number_stock_inventory(self): + """Men-generate nomor untuk semua stock inventory yang belum memiliki number.""" + stock_records = self.env['stock.inventory'].search([('number', '=', False)], order='id asc') + for record in stock_records: + self._assign_number(record) + + _logger.info('Generate Number Done') + + def _assign_number(self, record): + """Menentukan nomor berdasarkan kategori Adjust-In atau Adjust-Out.""" + name_upper = record.name.upper() if record.name else "" + + if self.adjusment_type == 'out': + last_number = self._get_last_sequence("ADJUST/OUT/") + record.number = f"ADJUST/OUT/{last_number}" + elif self.adjusment_type == 'in': + last_number = self._get_last_sequence("ADJUST/IN/") + record.number = f"ADJUST/IN/{last_number}" + else: + record.number = "UNKNOWN" # Jika tidak termasuk kategori + + def _get_last_sequence(self, prefix): + """Mengambil nomor terakhir berdasarkan prefix (ADJUST/OUT/ atau ADJUST/IN/) dengan format 00001, 00002, dst.""" + last_record = self.env['stock.inventory'].search( + [("number", "like", f"{prefix}%")], order="number desc", limit=1 + ) + + if last_record and last_record.number: + try: + last_number = int(last_record.number.split("/")[-1]) # Ambil angka terakhir + return str(last_number + 1).zfill(5) # Format jadi 00001, 00002, dst. + except ValueError: + return "00001" # Jika format tidak valid, mulai dari 00001 + return "00001" # Jika belum ada data, mulai dari 00001 + + @api.model + def create(self, vals): + """Pastikan nomor hanya dibuat saat penyimpanan.""" + if 'adjusment_type' in vals and not vals.get('number'): + vals['number'] = False # Jangan buat number otomatis dulu + + order = super(StockInventory, self).create(vals) + + if order.adjusment_type: + self._assign_number(order) # Generate number setelah save + + return order + + def write(self, vals): + """Jika adjusment_type diubah, generate ulang nomor.""" + res = super(StockInventory, self).write(vals) + if 'adjusment_type' in vals: + for record in self: + self._assign_number(record) + return res + + def copy(self, default=None): + """Saat duplikasi, adjusment_type dikosongkan dan number tidak ikut terduplikasi.""" + default = dict(default or {}, adjusment_type=False, number=False) + return super(StockInventory, self).copy(default=default) diff --git a/indoteknik_custom/models/stock_move.py b/indoteknik_custom/models/stock_move.py index e1d4e74c..90ab30a4 100644 --- a/indoteknik_custom/models/stock_move.py +++ b/indoteknik_custom/models/stock_move.py @@ -12,9 +12,44 @@ class StockMove(models.Model): default=lambda self: self.product_id.print_barcode, ) qr_code_variant = fields.Binary("QR Code Variant", compute='_compute_qr_code_variant') + barcode = fields.Char(string='Barcode', related='product_id.barcode') + vendor_id = fields.Many2one('res.partner' ,string='Vendor') + hold_outgoingg = fields.Boolean('Hold Outgoing', default=False) + + # @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: + if rec.product_id and rec.bom_line_id: + if rec.product_id.x_manufacture.override_vendor_id: + rec.vendor_id = rec.product_id.x_manufacture.override_vendor_id.id + else: + purchase_pricelist = self.env['purchase.pricelist'].search( + [('product_id', '=', rec.product_id.id), + ('is_winner', '=', True)], + limit=1) + if purchase_pricelist: + rec.vendor_id = purchase_pricelist.vendor_id.id def _compute_qr_code_variant(self): for rec in self: + if rec.picking_id.picking_type_code == 'outgoing' and rec.picking_id and rec.picking_id.origin and rec.picking_id.origin.startswith('SO/'): + rec.qr_code_variant = rec.product_id.qr_code_variant + rec.print_barcode = True if rec.print_barcode and rec.print_barcode == True and rec.product_id and rec.product_id.qr_code_variant: rec.qr_code_variant = rec.product_id.qr_code_variant else: diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index cd330aeb..6a6fe352 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1,7 +1,9 @@ from odoo import fields, models, api, _ from odoo.exceptions import AccessError, UserError, ValidationError from odoo.tools.float_utils import float_is_zero +from collections import defaultdict from datetime import timedelta, datetime +from datetime import timedelta, datetime as waktu from itertools import groupby import pytz, requests, json, requests from dateutil import parser @@ -12,11 +14,28 @@ import base64 import requests import time import logging +import re + _logger = logging.getLogger(__name__) +_biteship_url = "https://api.biteship.com/v1" +_biteship_api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA" + + +# _biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo" + + class StockPicking(models.Model): _inherit = 'stock.picking' - # check_product_lines = fields.One2many('check.product', 'picking_id', string='Check Product', auto_join=True) + _order = 'final_seq ASC' + konfirm_koli_lines = fields.One2many('konfirm.koli', 'picking_id', string='Konfirm Koli', auto_join=True, + copy=False) + scan_koli_lines = fields.One2many('scan.koli', 'picking_id', string='Scan Koli', auto_join=True, copy=False) + check_koli_lines = fields.One2many('check.koli', 'picking_id', string='Check Koli', auto_join=True, copy=False) + + check_product_lines = fields.One2many('check.product', 'picking_id', string='Check Product', auto_join=True, + copy=False) + barcode_product_lines = fields.One2many('barcode.product', 'picking_id', string='Barcode Product', auto_join=True) is_internal_use = fields.Boolean('Internal Use', help='flag which is internal use or not') account_id = fields.Many2one('account.account', string='Account') efaktur_id = fields.Many2one('vit.efaktur', string='Faktur Pajak') @@ -61,39 +80,50 @@ class StockPicking(models.Model): readonly=True, copy=False ) + out_code = fields.Integer( + string="Out Code", + readonly=True, + related="id", + ) sj_documentation = fields.Binary(string="Dokumentasi Surat Jalan", ) paket_documentation = fields.Binary(string="Dokumentasi Paket", ) - sj_return_date = fields.Datetime(string="SJ Return Date", ) + sj_return_date = fields.Datetime(string="SJ Return Date", copy=False) responsible = fields.Many2one('res.users', string='Responsible', tracking=True) approval_status = fields.Selection([ ('pengajuan1', 'Approval Accounting'), ('approved', 'Approved'), - ], string='Approval Status', readonly=True, copy=False, index=True, tracking=3, help="Approval Status untuk Internal Use") + ], string='Approval Status', readonly=True, copy=False, index=True, tracking=3, + help="Approval Status untuk Internal Use") approval_receipt_status = fields.Selection([ ('pengajuan1', 'Approval Logistic'), ('approved', 'Approved'), - ], string='Approval Receipt Status', readonly=True, copy=False, index=True, tracking=3, help="Approval Status untuk Receipt") + ], string='Approval Receipt Status', readonly=True, copy=False, index=True, tracking=3, + help="Approval Status untuk Receipt") approval_return_status = fields.Selection([ ('pengajuan1', 'Approval Finance'), ('approved', 'Approved'), - ], string='Approval Return Status', readonly=True, copy=False, index=True, tracking=3, help="Approval Status untuk Return") - date_doc_kirim = fields.Datetime(string='Tanggal Kirim di SJ', help="Tanggal Kirim di cetakan SJ, tidak berpengaruh ke Accounting", tracking=True) + ], string='Approval Return Status', readonly=True, copy=False, index=True, tracking=3, + help="Approval Status untuk Return") + date_doc_kirim = fields.Datetime(string='Tanggal Kirim di SJ', + help="Tanggal Kirim di cetakan SJ, tidak berpengaruh ke Accounting", tracking=True, + copy=False) note_logistic = fields.Selection([ - ('hold', 'Hold by Sales'), + ('wait_so_together', 'Tunggu SO Barengan'), ('not_paid', 'Customer belum bayar'), - ('partial', 'Kirim Parsial'), - ('indent', 'Indent'), + ('reserve_stock', 'Reserve Stock'), + ('waiting_schedule', 'Menunggu Jadwal Kirim'), ('self_pickup', 'Barang belum di pickup Customer'), ('expedition_closed', 'Eskpedisi belum buka') ], string='Note Logistic', help='jika field ini diisi maka tidak akan dihitung ke lead time') waybill_id = fields.One2many(comodel_name='airway.bill', inverse_name='do_id', string='Airway Bill') - purchase_representative_id = fields.Many2one('res.users', related='move_lines.purchase_line_id.order_id.user_id', string="Purchase Representative") + purchase_representative_id = fields.Many2one('res.users', related='move_lines.purchase_line_id.order_id.user_id', + string="Purchase Representative") carrier_id = fields.Many2one('delivery.carrier', string='Shipping Method') shipping_status = fields.Char(string='Shipping Status', compute="_compute_shipping_status") - date_reserved = fields.Datetime(string="Date Reserved", help='Tanggal ter-reserved semua barang nya') + date_reserved = fields.Datetime(string="Date Reserved", help='Tanggal ter-reserved semua barang nya', copy=False) status_printed = fields.Selection([ ('not_printed', 'Belum Print'), ('printed', 'Printed') @@ -111,16 +141,74 @@ class StockPicking(models.Model): ('invoiced', 'Fully Invoiced'), ('to invoice', 'To Invoice'), ('no', 'Nothing to Invoice') - ], string='Invoice Status', related="sale_id.invoice_status") + ], string='Invoice Status', related="sale_id.invoice_status") note_return = fields.Text(string="Note Return", help="Catatan untuk kirim barang kembali") - + state_reserve = fields.Selection([ ('waiting', 'Waiting For Fullfilment'), ('ready', 'Ready to Ship'), ('done', 'Done'), ('cancel', 'Cancelled'), - ], string='Status Reserve', readonly=True, tracking=True, help="The current state of the stock picking.") - notee = fields.Text(string="Note") + ], string='Status Reserve', tracking=True, copy=False, help="The current state of the stock picking.") + notee = fields.Text(string="Note SJ", help="Catatan untuk kirim barang") + note_info = fields.Text(string="Note Logistix (Text)", help="Catatan untuk pengiriman") + state_approve_md = fields.Selection([ + ('waiting', 'Waiting For Approve by MD'), + ('pending', 'Pending (perlu koordinasi dengan MD)'), + ('done', 'Approve by MD'), + ], string='Approval MD Gudang Selisih', tracking=True, copy=False, + help="The current state of the MD Approval transfer barang from gudang selisih.") + # show_state_approve_md = fields.Boolean(compute="_compute_show_state_approve_md") + + # def _compute_show_state_approve_md(self): + # for record in self: + # record.show_state_approve_md = record.location_id.id == 47 or record.location_id.complete_name == "Virtual Locations/Gudang Selisih" + quantity_koli = fields.Float(string="Quantity Koli", copy=False) + total_mapping_koli = fields.Float(string="Total Mapping Koli", compute='_compute_total_mapping_koli') + 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: + # if (record.picking_type_code == 'internal' + # and 'BU/PICK/' in record.name): + # # Jika menghapus referensi (nilai di-set False/None) + # if record.linked_manual_bu_out and not vals['linked_manual_bu_out']: + # record.linked_manual_bu_out.state_packing = 'not_packing' + # # Jika menambahkan referensi baru + # elif vals['linked_manual_bu_out']: + # new_picking = self.env['stock.picking'].browse(vals['linked_manual_bu_out']) + # new_picking.state_packing = 'packing_done' + # return super().write(vals) + + # @api.model + # def create(self, vals): + # record = super().create(vals) + # if (record.picking_type_code == 'internal' + # and 'BU/PICK/' in record.name + # and vals.get('linked_manual_bu_out')): + # picking = self.env['stock.picking'].browse(vals['linked_manual_bu_out']) + # picking.state_packing = 'packing_done' + # return record + + @api.depends('konfirm_koli_lines', 'konfirm_koli_lines.pick_id', 'konfirm_koli_lines.pick_id.quantity_koli') + def _compute_total_mapping_koli(self): + for record in self: + total = 0.0 + for line in record.konfirm_koli_lines: + if line.pick_id and line.pick_id.quantity_koli: + total += line.pick_id.quantity_koli + record.total_mapping_koli = total @api.model def _compute_dokumen_tanda_terima(self): @@ -132,8 +220,10 @@ class StockPicking(models.Model): for picking in self: picking.dokumen_pengiriman = picking.partner_id.dokumen_pengiriman_input - dokumen_tanda_terima = fields.Char(string='Dokumen Tanda Terima yang Diberikan Pada Saat Pengiriman Barang', readonly=True, compute=_compute_dokumen_tanda_terima) - dokumen_pengiriman = fields.Char(string='Dokumen yang Dibawa Saat Pengiriman Barang', readonly=True, compute=_compute_dokumen_pengiriman) + dokumen_tanda_terima = fields.Char(string='Dokumen Tanda Terima yang Diberikan Pada Saat Pengiriman Barang', + readonly=True, compute=_compute_dokumen_tanda_terima) + dokumen_pengiriman = fields.Char(string='Dokumen yang Dibawa Saat Pengiriman Barang', readonly=True, + compute=_compute_dokumen_pengiriman) # Envio Tracking Section envio_id = fields.Char(string="Envio ID", readonly=True) @@ -165,6 +255,265 @@ class StockPicking(models.Model): lalamove_image_url = fields.Char(string="Lalamove Image URL") lalamove_image_html = fields.Html(string="Lalamove Image", compute="_compute_lalamove_image_html") + # KGX Section + kgx_pod_photo_url = fields.Char('KGX Photo URL') + kgx_pod_photo = fields.Html('KGX Photo', compute='_compute_kgx_image_html') + kgx_pod_signature = fields.Char('KGX Signature URL') + kgx_pod_receive_time = fields.Datetime('KGX Ata Date') + kgx_pod_receiver = fields.Char('KGX Receiver') + + total_koli = fields.Integer(compute='_compute_total_koli', string="Total Koli") + total_koli_display = fields.Char(compute='_compute_total_koli_display', string="Total Koli Display") + linked_out_picking_id = fields.Many2one('stock.picking', string="Linked BU/OUT", copy=False) + total_so_koli = fields.Integer(compute='_compute_total_so_koli', string="Total SO Koli") + + # Biteship Section + biteship_id = fields.Char(string="Biteship Respon ID") + biteship_tracking_id = fields.Char(string="Biteship Trackcking ID") + biteship_waybill_id = fields.Char(string="Biteship Waybill ID") + # estimated_ready_ship_date = fields.Datetime(string='ET Ready to Ship', copy=False, related='sale_id.estimated_ready_ship_date') + # countdown_hours = fields.Float(string='Countdown in Hours', compute='_callculate_sequance', default=False, store=False, compute_sudo=False) + # countdown_ready_to_ship = fields.Char(string='Countdown Ready to Ship', compute='_callculate_sequance', store=False, compute_sudo=False) + final_seq = fields.Float(string='Remaining Time') + shipping_method_so_id = fields.Many2one('delivery.carrier', string='Shipping Method SO', + related='sale_id.carrier_id') + state_packing = fields.Selection([('not_packing', 'Belum Packing'), ('packing_done', 'Sudah Packing')], + string='Packing Status') + approval_invoice_date_id = fields.Many2one('approval.invoice.date', string='Approval Invoice Date') + last_update_date_doc_kirim = fields.Datetime(string='Last Update Tanggal Kirim') + update_date_doc_kirim_add = fields.Boolean(string='Update Tanggal Kirim Lewat ADD') + + def _get_kgx_awb_number(self): + """Menggabungkan name dan origin untuk membuat AWB Number""" + self.ensure_one() + if not self.name or not self.origin: + return False + return f"{self.name} {self.origin}" + + def _download_pod_photo(self, url): + """Mengunduh foto POD dari URL""" + try: + response = requests.get(url, timeout=10) + response.raise_for_status() + return base64.b64encode(response.content) + except Exception as e: + raise UserError(f"Gagal mengunduh foto POD: {str(e)}") + + def _parse_datetime(self, dt_str): + """Parse datetime string dari format KGX""" + try: + from datetime import datetime + # Hilangkan timezone jika ada masalah parsing + if '+' in dt_str: + dt_str = dt_str.split('+')[0] + return datetime.strptime(dt_str, '%Y-%m-%dT%H:%M:%S') + except ValueError: + return False + + def action_get_kgx_pod(self): + self.ensure_one() + + awb_number = self._get_kgx_awb_number() + if not awb_number: + raise UserError("Nomor AWB tidak dapat dibuat, pastikan picking memiliki name dan origin") + + url = "https://kgx.co.id/get_detail_awb" + headers = {'Content-Type': 'application/json'} + payload = {"params" : {'awb_number': awb_number}} + + try: + response = requests.post(url, headers=headers, data=json.dumps(payload)) + response.raise_for_status() + data = response.json() + + if data.get('result', {}).get('data', []): + pod_data = data['result']['data'][0].get('connote_pod', {}) + photo_url = pod_data.get('photo') + + self.kgx_pod_photo_url = photo_url + self.kgx_pod_signature = pod_data.get('signature') + self.kgx_pod_receiver = pod_data.get('receiver') + self.kgx_pod_receive_time = self._parse_datetime(pod_data.get('timeReceive')) + self.driver_arrival_date = self._parse_datetime(pod_data.get('timeReceive')) + + return data + else: + raise UserError(f"Tidak ditemukan data untuk AWB: {awb_number}") + + except requests.exceptions.RequestException as e: + raise UserError(f"Gagal mengambil data POD: {str(e)}") + + @api.constrains('sj_return_date') + def _check_sj_return_date(self): + for record in self: + if not record.driver_arrival_date: + if record.sj_return_date: + raise ValidationError( + _("Anda tidak dapat mengubah Tanggal Pengembalian setelah Tanggal Pengiriman!") + ) + + def _check_date_doc_kirim_modification(self): + for record in self: + if record.last_update_date_doc_kirim and not self.env.context.get('from_button_approve'): + kirim_date = fields.Datetime.from_string(record.last_update_date_doc_kirim) + now = fields.Datetime.now() + + deadline = kirim_date + timedelta(days=1) + deadline = deadline.replace(hour=10, minute=0, second=0) + + if now > deadline: + raise ValidationError( + _("Anda tidak dapat mengubah Tanggal Kirim setelah jam 10:00 pada hari berikutnya!") + ) + + @api.constrains('date_doc_kirim') + def _constrains_date_doc_kirim(self): + for rec in self: + rec.calculate_line_no() + + if rec.picking_type_code == 'outgoing' and 'BU/OUT/' in rec.name and rec.partner_id.id != 96868: + invoice = self.env['account.move'].search( + [('sale_id', '=', rec.sale_id.id), ('move_type', '=', 'out_invoice'), ('state', '=', 'posted')], + limit=1, order='create_date desc') + + if invoice and not self.env.context.get('active_model') == 'stock.picking': + rec._check_date_doc_kirim_modification() + if rec.date_doc_kirim != invoice.invoice_date and not self.env.context.get('from_button_approve'): + get_approval_invoice_date = self.env['approval.invoice.date'].search( + [('picking_id', '=', rec.id), ('state', '=', 'draft')], limit=1) + + if get_approval_invoice_date and get_approval_invoice_date.state == 'draft': + get_approval_invoice_date.date_doc_do = rec.date_doc_kirim + else: + approval_invoice_date = self.env['approval.invoice.date'].create({ + 'picking_id': rec.id, + 'date_invoice': invoice.invoice_date, + 'date_doc_do': rec.date_doc_kirim, + 'sale_id': rec.sale_id.id, + 'move_id': invoice.id, + 'partner_id': rec.partner_id.id + }) + + rec.approval_invoice_date_id = approval_invoice_date.id + + if approval_invoice_date: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': {'title': 'Notification', + 'message': 'Invoice Date Tidak Sesuai, Document Approval Invoice Date Terbuat', + 'next': {'type': 'ir.actions.act_window_close'}}, + } + + rec.last_update_date_doc_kirim = datetime.datetime.utcnow() + + @api.constrains('scan_koli_lines') + def _constrains_scan_koli_lines(self): + now = datetime.datetime.utcnow() + for picking in self: + if len(picking.scan_koli_lines) > 0: + if len(picking.scan_koli_lines) != picking.total_mapping_koli: + raise UserError("Scan Koli Tidak Sesuai Dengan Total Mapping Koli") + + picking.driver_departure_date = now + + @api.depends('total_so_koli') + def _compute_total_so_koli(self): + for picking in self: + if picking.state == 'done': + picking.total_so_koli = self.env['sales.order.koli'].search_count( + [('picking_id.linked_out_picking_id', '=', picking.id), ('state', '=', 'delivered')]) + else: + picking.total_so_koli = self.env['sales.order.koli'].search_count( + [('picking_id.linked_out_picking_id', '=', picking.id), ('state', '!=', 'delivered')]) + + @api.depends('total_koli') + def _compute_total_koli(self): + for picking in self: + picking.total_koli = self.env['scan.koli'].search_count([('picking_id', '=', picking.id)]) + + @api.depends('total_koli', 'total_so_koli') + def _compute_total_koli_display(self): + for picking in self: + picking.total_koli_display = f"{picking.total_koli} / {picking.total_so_koli}" + + @api.constrains('quantity_koli') + def _constrains_quantity_koli(self): + for picking in self: + if not picking.linked_out_picking_id: + so_koli = self.env['sales.order.koli'].search([('picking_id', '=', picking.id)]) + + if so_koli: + so_koli.unlink() + + for rec in picking.check_koli_lines: + self.env['sales.order.koli'].create({ + 'sale_order_id': picking.sale_id.id, + 'picking_id': picking.id, + 'koli_id': rec.id, + }) + else: + raise UserError( + 'Tidak Bisa Mengubah Quantity Koli Karena Koli Dari Picking Ini Sudah Dipakai Di BU/OUT!') + + @api.onchange('quantity_koli') + def _onchange_quantity_koli(self): + self.check_koli_lines = [(5, 0, 0)] + self.check_koli_lines = [(0, 0, { + 'koli': f"{self.name}/{str(i + 1).zfill(3)}", + 'picking_id': self.id, + }) for i in range(int(self.quantity_koli))] + + def schduled_update_sequance(self): + query = "SELECT update_sequance_stock_picking();" + self.env.cr.execute(query) + + # @api.depends('estimated_ready_ship_date', 'state') + # def _callculate_sequance(self): + # for record in self: + # try : + # if record.estimated_ready_ship_date and record.state not in ('cancel', 'done'): + # rts = record.estimated_ready_ship_date - waktu.now() + # rts_days = rts.days + # rts_hours = divmod(rts.seconds, 3600) + + # estimated_by_erts = rts.total_seconds() / 3600 + + # record.countdown_ready_to_ship = f"{rts_days} days, {rts_hours} hours" + # record.countdown_hours = estimated_by_erts + # else: + # record.countdown_hours = 999999999999 + # record.countdown_ready_to_ship = False + # except Exception as e : + # _logger.error(f"Error calculating sequance {record.id}: {str(e)}") + # print(str(e)) + # return { 'error': str(e) } + + # @api.depends('estimated_ready_ship_date', 'state') + # def _compute_countdown_hours(self): + # for record in self: + # if record.state in ('cancel', 'done') or not record.estimated_ready_ship_date: + # # Gunakan nilai yang sangat besar sebagai placeholder + # record.countdown_hours = 999999 + # else: + # delta = record.estimated_ready_ship_date - waktu.now() + # record.countdown_hours = delta.total_seconds() / 3600 + + # @api.depends('estimated_ready_ship_date', 'state') + # def _compute_countdown_ready_to_ship(self): + # for record in self: + # if record.state in ('cancel', 'done'): + # record.countdown_ready_to_ship = False + # else: + # if record.estimated_ready_ship_date: + # delta = record.estimated_ready_ship_date - waktu.now() + # days = delta.days + # hours, remainder = divmod(delta.seconds, 3600) + # record.countdown_ready_to_ship = f"{days} days, {hours} hours" + # record.countdown_hours = delta.total_seconds() / 3600 + # else: + # record.countdown_ready_to_ship = False + def _compute_lalamove_image_html(self): for record in self: if record.lalamove_image_url: @@ -172,12 +521,20 @@ class StockPicking(models.Model): else: record.lalamove_image_html = "No image available." + def _compute_kgx_image_html(self): + for record in self: + if record.kgx_pod_photo_url: + record.kgx_pod_photo = f'<img src="{record.kgx_pod_photo_url}" width="300" height="300"/>' + else: + record.kgx_pod_photo = "No image available." + def action_fetch_lalamove_order(self): pickings = self.env['stock.picking'].search([ ('picking_type_code', '=', 'outgoing'), ('state', '=', 'done'), - ('carrier_id', '=', 9) - ]) + ('carrier_id', '=', 9), + ('lalamove_order_id', '!=', False) + ]) for picking in pickings: try: order_id = picking.lalamove_order_id @@ -212,7 +569,7 @@ class StockPicking(models.Model): for stop in stops: pod = stop.get("POD", {}) if pod.get("status") == "DELIVERED": - image_url = pod.get("image") # Sesuaikan jika key berbeda + image_url = pod.get("image") # Sesuaikan jika key berbeda self.lalamove_image_url = image_url address = stop.get("address") @@ -233,7 +590,6 @@ class StockPicking(models.Model): else: raise UserError(f"Error {response.status_code}: {response.text}") - def _convert_to_wib(self, date_str): """ Mengonversi string waktu ISO 8601 ke format waktu Indonesia (WIB) @@ -330,8 +686,9 @@ class StockPicking(models.Model): raise UserError(f"Kesalahan tidak terduga: {str(e)}") def action_send_to_biteship(self): - url = "https://api.biteship.com/v1/orders" - api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA" + + if self.biteship_tracking_id: + raise UserError(f"Order ini sudah dikirim ke Biteship. Dengan Tracking Id: {self.biteship_tracking_id}") # Mencari data sale.order.line berdasarkan sale_id products = self.env['sale.order.line'].search([('order_id', '=', self.sale_id.id)]) @@ -357,17 +714,18 @@ class StockPicking(models.Model): ('order_id', '=', self.sale_id.id), ('product_id', '=', move_line.product_id.id) ], limit=1) - + if order_line: items_data_instant.append({ "name": order_line.product_id.name, "description": order_line.name, "value": order_line.price_unit, - "quantity": move_line.qty_done, # Menggunakan qty_done dari move_line + "quantity": move_line.qty_done, "weight": order_line.weight }) payload = { + "reference_id ": self.sale_id.name, "shipper_contact_name": self.carrier_id.pic_name or '', "shipper_contact_phone": self.carrier_id.pic_phone or '', "shipper_organization": self.carrier_id.name, @@ -379,7 +737,8 @@ class StockPicking(models.Model): "destination_contact_phone": self.real_shipping_id.phone or self.real_shipping_id.mobile, "destination_address": self.real_shipping_id.street, "destination_postal_code": self.real_shipping_id.zip, - "courier_type": "reg", + "origin_note": "BELAKANG INDOMARET", + "courier_type": self.sale_id.delivery_service_type or "reg", "courier_company": self.carrier_id.name.lower(), "delivery_type": "now", "destination_postal_code": self.real_shipping_id.zip, @@ -387,31 +746,57 @@ class StockPicking(models.Model): } # Cek jika pengiriman instant atau same_day - if "instant" in self.sale_id.delivery_service_type or "same_day" in self.sale_id.delivery_service_type: + if self.sale_id.delivery_service_type and ( + "instant" in self.sale_id.delivery_service_type or "same_day" in self.sale_id.delivery_service_type): payload.update({ - "origin_note": "BELAKANG INDOMARET", - "courier_company": self.carrier_id.name.lower(), - "courier_type": self.sale_id.delivery_service_type, - "delivery_type": "now", - "items": items_data_instant # Gunakan items untuk instant + "origin_coordinate": { + "latitude": -6.3031123, + "longitude": 106.7794934999 + }, + "destination_coordinate": { + "latitude": self.real_shipping_id.latitude, + "longitude": self.real_shipping_id.longtitude, + }, + "items": items_data_instant }) + api_key = _biteship_api_key headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } # Kirim request ke Biteship - response = requests.post(url, headers=headers, json=payload) + response = requests.post(_biteship_url + '/orders', headers=headers, json=payload) - if response.status_code == 201: - return response.json() + if response.status_code == 200: + data = response.json() + + self.biteship_id = data.get("id", "") + self.biteship_tracking_id = data.get("courier", {}).get("tracking_id", "") + self.biteship_waybill_id = data.get("courier", {}).get("waybill_id", "") + self.delivery_tracking_no = data.get("courier", {}).get("waybill_id", "") + + waybill_id = data.get("courier", {}).get("waybill_id", "") + + message = f"✅ Berhasil Order ke Biteship! Resi: {waybill_id}" if waybill_id else "⚠️ Order berhasil, tetapi tidak ada nomor resi." + + return { + 'effect': { + 'fadeout': 'slow', # Efek menghilang perlahan + 'message': message, # Pesan sukses + 'type': 'rainbow_man', # Efek animasi lucu Odoo + } + } else: - raise UserError(f"Error saat mengirim ke Biteship: {response.content}") - + error_data = response.json() + error_message = error_data.get("error", "Unknown error") + error_code = error_data.get("code", "No code provided") + raise UserError(f"Error saat mengirim ke Biteship: {error_message} (Code: {error_code})") + @api.constrains('driver_departure_date') - def constrains_driver_departure_date(self): - if not self.date_doc_kirim: + def constrains_driver_departure_date(self): + if not self.date_doc_kirim: self.date_doc_kirim = self.driver_departure_date @api.constrains('arrival_time') @@ -439,58 +824,64 @@ class StockPicking(models.Model): if not self._context.get('darimana') == 'sale.order' and self.env.user.id not in users_in_group.mapped('id'): self.sale_id.unreserve_id = self.id return self._create_approval_notification('Logistic') - + res = super(StockPicking, self).do_unreserve() current_time = datetime.datetime.utcnow() self.date_unreserve = current_time - # self.check_state_reserve() - + return res - - # def check_state_reserve(self): - # do = self.search([ - # ('state', 'not in', ['cancel', 'draft', 'done']), - # ('picking_type_code', '=', 'outgoing') - # ]) - - # for rec in do: - # rec.state_reserve = 'ready' - # rec.date_reserved = datetime.datetime.utcnow() - - # for line in rec.move_ids_without_package: - # if line.product_uom_qty > line.reserved_availability: - # rec.state_reserve = 'waiting' - # rec.date_reserved = '' - # break def check_state_reserve(self): pickings = self.search([ ('state', 'not in', ['cancel', 'draft', 'done']), - ('picking_type_code', '=', 'outgoing') + ('picking_type_code', '=', 'internal'), + ('name', 'ilike', 'BU/PICK/'), ]) - + for picking in pickings: - fullfillments = self.env['sales.order.fullfillment'].search([ - ('sales_order_id', '=', picking.sale_id.id) + fullfillments = self.env['sales.order.fulfillment.v2'].search([ + ('sale_order_id', '=', picking.sale_id.id) ]) - + picking.state_reserve = 'ready' picking.date_reserved = picking.date_reserved or datetime.datetime.utcnow() - - if any(rec.reserved_from not in ['Inventory On Hand', 'Reserved from stock', 'Free Stock'] for rec in fullfillments): + + if any(rec.so_qty != rec.reserved_stock_qty for rec in fullfillments): picking.state_reserve = 'waiting' picking.date_reserved = '' - + + self.check_state_reserve_backorder() + + def check_state_reserve_backorder(self): + pickings = self.search([ + ('backorder_id', '!=', False), + ('name', 'ilike', 'BU/PICK/'), + ('picking_type_code', '=', 'internal'), + ('state', 'not in', ['cancel', 'draft', 'done']) + ]) + + for picking in pickings: + fullfillments = self.env['sales.order.fulfillment.v2'].search([ + ('sale_order_id', '=', picking.sale_id.id) + ]) + + picking.state_reserve = 'ready' + picking.date_reserved = picking.date_reserved or datetime.datetime.utcnow() + + if any(rec.so_qty != rec.reserved_stock_qty for rec in fullfillments): + picking.state_reserve = 'waiting' + picking.date_reserved = '' + def _create_approval_notification(self, approval_role): title = 'Warning' message = f'Butuh approval sales untuk unreserved' return self._create_notification_action(title, message) - + def _create_notification_action(self, title, message): return { 'type': 'ir.actions.client', 'tag': 'display_notification', - 'params': { 'title': title, 'message': message, 'next': {'type': 'ir.actions.act_window_close'} }, + 'params': {'title': title, 'message': message, 'next': {'type': 'ir.actions.act_window_close'}}, } def _compute_shipping_status(self): @@ -500,7 +891,7 @@ class StockPicking(models.Model): status = 'shipment' elif rec.driver_departure_date and (rec.sj_return_date or rec.driver_arrival_date): status = 'completed' - + rec.shipping_status = status def action_create_invoice_from_mr(self): @@ -508,10 +899,10 @@ class StockPicking(models.Model): """ if not self.env.user.is_accounting: raise UserError('Hanya Accounting yang bisa membuat Bill') - + precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') - #custom here + # custom here po = self.env['purchase.order'].search([ ('name', '=', self.group_id.name) ]) @@ -528,24 +919,29 @@ class StockPicking(models.Model): invoice_vals = order._prepare_invoice() # Invoice line values (keep only necessary sections). for line in self.move_ids_without_package: - po_line = self.env['purchase.order.line'].search([('order_id', '=', po.id), ('product_id', '=', line.product_id.id)], limit=1) + po_line = self.env['purchase.order.line'].search( + [('order_id', '=', po.id), ('product_id', '=', line.product_id.id)], limit=1) qty = line.product_uom_qty if po_line.display_type == 'line_section': pending_section = line continue if not float_is_zero(po_line.qty_to_invoice, precision_digits=precision): if pending_section: - invoice_vals['invoice_line_ids'].append((0, 0, pending_section._prepare_account_move_line_from_mr(po_line, qty))) + invoice_vals['invoice_line_ids'].append( + (0, 0, pending_section._prepare_account_move_line_from_mr(po_line, qty))) pending_section = None - invoice_vals['invoice_line_ids'].append((0, 0, line._prepare_account_move_line_from_mr(po_line, qty))) + invoice_vals['invoice_line_ids'].append( + (0, 0, line._prepare_account_move_line_from_mr(po_line, qty))) invoice_vals_list.append(invoice_vals) if not invoice_vals_list: - raise UserError(_('There is no invoiceable line. If a product has a control policy based on received quantity, please make sure that a quantity has been received.')) + raise UserError( + _('There is no invoiceable line. If a product has a control policy based on received quantity, please make sure that a quantity has been received.')) # 2) group by (company_id, partner_id, currency_id) for batch creation new_invoice_vals_list = [] - for grouping_keys, invoices in groupby(invoice_vals_list, key=lambda x: (x.get('company_id'), x.get('partner_id'), x.get('currency_id'))): + for grouping_keys, invoices in groupby(invoice_vals_list, key=lambda x: ( + x.get('company_id'), x.get('partner_id'), x.get('currency_id'))): origins = set() payment_refs = set() refs = set() @@ -575,7 +971,8 @@ class StockPicking(models.Model): # 4) Some moves might actually be refunds: convert them if the total amount is negative # We do this after the moves have been created since we need taxes, etc. to know if the total # is actually negative or not - moves.filtered(lambda m: m.currency_id.round(m.amount_total) < 0).action_switch_invoice_into_refund_credit_note() + moves.filtered( + lambda m: m.currency_id.round(m.amount_total) < 0).action_switch_invoice_into_refund_credit_note() return self.action_view_invoice_from_mr(moves) @@ -638,7 +1035,7 @@ class StockPicking(models.Model): # for stock_move_line in stock_move_lines: # if stock_move_line.picking_id.state not in list_state: # continue - # raise UserError('Sudah pernah dikirim kalender') + # raise UserError('Sudah pernah dikirim kalender') for pick in self: if not pick.is_internal_use: @@ -659,23 +1056,27 @@ class StockPicking(models.Model): if self.env.user.is_accounting: pick.approval_return_status = 'approved' continue + else: + pick.approval_return_status = 'pengajuan1' action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_stock_return_note_wizard') if self.picking_type_code == 'outgoing': if self.env.user.id in [3988, 3401, 20] or ( - self.env.user.has_group('indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin + self.env.user.has_group( + 'indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin ): action['context'] = {'picking_ids': [x.id for x in self]} return action - elif not self.env.user.has_group('indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin: + elif not self.env.user.has_group( + 'indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin: raise UserError('Harus Purchasing yang Ask Return') else: raise UserError('Harus Sales Admin yang Ask Return') elif self.picking_type_code == 'incoming': if self.env.user.has_group('indoteknik_custom.group_role_purchasing') or ( - self.env.user.id in [3988, 3401, 20] and 'Return of' in self.origin + self.env.user.id in [3988, 3401, 20] and 'Return of' in self.origin ): action['context'] = {'picking_ids': [x.id for x in self]} return action @@ -685,7 +1086,7 @@ class StockPicking(models.Model): raise UserError('Harus Purchasing yang Ask Return') def calculate_line_no(self): - + for picking in self: name = picking.group_id.name for move in picking.move_ids_without_package: @@ -708,10 +1109,10 @@ class StockPicking(models.Model): def _compute_summary_qty(self): for picking in self: sum_qty_detail = sum_qty_operation = count_line_detail = count_line_operation = 0 - for detail in picking.move_line_ids_without_package: # detailed operations + for detail in picking.move_line_ids_without_package: # detailed operations sum_qty_detail += detail.qty_done count_line_detail += 1 - for operation in picking.move_ids_without_package: # operations + for operation in picking.move_ids_without_package: # operations sum_qty_operation += operation.product_uom_qty count_line_operation += 1 picking.summary_qty_detail = sum_qty_detail @@ -733,13 +1134,13 @@ class StockPicking(models.Model): ]) if ( - self.picking_type_id.id == 29 - and quant - and line.location_id.id == bu_location_id - and quant.inventory_quantity < line.product_uom_qty + self.picking_type_id.id == 29 + and quant + and line.location_id.id == bu_location_id + and quant.inventory_quantity < line.product_uom_qty ): raise UserError('Quantity reserved lebih besar dari quantity onhand di product') - + def check_qty_done_stock(self): for line in self.move_line_ids_without_package: def check_qty_per_inventory(self, product, location): @@ -752,12 +1153,74 @@ class StockPicking(models.Model): return quant.quantity return 0 - + qty_onhand = check_qty_per_inventory(self, line.product_id, line.location_id) if line.qty_done > qty_onhand: raise UserError('Quantity Done melebihi Quantity Onhand') + def button_state_approve_md(self): + group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id + users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) + active_model = self.env.context.get('active_model') + if self.env.user.id in users_in_group.mapped('id'): + self.state_approve_md = 'done' + else: + raise UserError('Hanya MD yang bisa Approve') + + def button_state_pending_md(self): + group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id + users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) + active_model = self.env.context.get('active_model') + if self.env.user.id in users_in_group.mapped('id'): + self.state_approve_md = 'pending' + else: + raise UserError('Hanya MD yang bisa Approve') + def button_validate(self): + self.check_invoice_date() + threshold_datetime = waktu(2025, 4, 11, 6, 26) + group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id + users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) + active_model = self.env.context.get('active_model') + if self.location_id.id == 47 and self.env.user.id not in users_in_group.mapped( + 'id') and self.state_approve_md != 'done': + self.state_approve_md = 'waiting' if self.state_approve_md != 'pending' else 'pending' + self.env.cr.commit() + raise UserError("Transfer dari gudang selisih harus di approve MD, Hubungi MD agar bisa di Validate") + else: + if self.location_id.id == 47 and self.env.user.id in users_in_group.mapped('id'): + self.state_approve_md = 'done' + + if (len(self.konfirm_koli_lines) == 0 + and 'BU/OUT/' in self.name + and self.picking_type_code == 'outgoing' + and self.create_date > threshold_datetime + and not self.so_lama): + raise UserError(_("Tidak ada Mapping koli! Harap periksa kembali.")) + + if (len(self.scan_koli_lines) == 0 + and 'BU/OUT/' in self.name + and self.picking_type_code == 'outgoing' + and self.create_date > threshold_datetime + and not self.so_lama): + raise UserError(_("Tidak ada scan koli! Harap periksa kembali.")) + + # if self.driver_departure_date == False and 'BU/OUT/' in self.name and self.picking_type_code == 'outgoing': + # raise UserError(_("Isi Driver Departure Date dulu sebelum validate")) + + if len(self.check_koli_lines) == 0 and 'BU/PICK/' in self.name: + raise UserError(_("Tidak ada koli! Harap periksa kembali.")) + + if not self.linked_manual_bu_out and 'BU/PICK/' in self.name: + raise UserError(_("Isi BU Out terlebih dahulu!")) + + if len(self.check_product_lines) == 0 and 'BU/PICK/' in self.name: + raise UserError(_("Tidak ada Check Product! Harap periksa kembali.")) + + if self.total_koli > self.total_so_koli: + raise UserError(_("Total Koli (%s) dan Total SO Koli (%s) tidak sama! Harap periksa kembali.") + % (self.total_koli, self.t1otal_so_koli)) + if not self.env.user.is_logistic_approver and self.env.context.get('active_model') == 'stock.picking': if self.origin and 'Return of' in self.origin: raise UserError("Button ini hanya untuk Logistik") @@ -779,10 +1242,10 @@ class StockPicking(models.Model): if self.is_internal_use and not self.env.user.is_accounting: raise UserError("Harus di Approve oleh Accounting") - + if self.picking_type_id.id == 28 and not self.env.user.is_logistic_approver: raise UserError("Harus di Approve oleh Logistik") - + if self.location_dest_id.id == 47 and not self.env.user.is_purchasing_manager: raise UserError("Transfer ke gudang selisih harus di approve Rafly Hanggara") @@ -805,12 +1268,147 @@ class StockPicking(models.Model): self.validation_minus_onhand_quantity() self.responsible = self.env.user.id + # self.send_koli_to_so() + if self.picking_type_code == 'outgoing' and 'BU/OUT/' in self.name: + self.check_koli() res = super(StockPicking, self).button_validate() - self.calculate_line_no() self.date_done = datetime.datetime.utcnow() self.state_reserve = 'done' + self.final_seq = 0 + self.set_picking_code_out() + self.send_koli_to_so() + + if (self.state_reserve == 'done' and self.picking_type_code == 'internal' and 'BU/PICK/' in self.name + and self.linked_manual_bu_out): + if not self.linked_manual_bu_out.date_reserved: + current_datetime = datetime.datetime.utcnow() + self.linked_manual_bu_out.date_reserved = current_datetime + self.linked_manual_bu_out.message_post( + body=f"Date Reserved diisi secara otomatis dari validasi BU/PICK {self.name}" + ) + + if not self.env.context.get('skip_koli_check'): + for picking in self: + if picking.sale_id: + all_koli_ids = picking.sale_id.koli_lines.filtered(lambda k: k.state != 'delivered').ids + scanned_koli_ids = picking.scan_koli_lines.mapped('koli_id.id') + + missing_koli_ids = set(all_koli_ids) - set(scanned_koli_ids) + + if len(missing_koli_ids) > 0 and picking.picking_type_code == 'outgoing' and 'BU/OUT/' in picking.name: + missing_koli_names = picking.sale_id.koli_lines.filtered( + lambda k: k.id in missing_koli_ids and k.state != 'delivered').mapped('display_name') + missing_koli_list = "\n".join(f"- {name}" for name in missing_koli_names) + + # Buat wizard modal warning + wizard = self.env['warning.modal.wizard'].create({ + 'message': f"Berikut Koli yang belum discan:\n{missing_koli_list}", + 'picking_id': picking.id, + }) + + return { + 'type': 'ir.actions.act_window', + 'res_model': 'warning.modal.wizard', + 'view_mode': 'form', + 'res_id': wizard.id, + 'target': 'new', + } + self.send_mail_bills() return res - + + def check_invoice_date(self): + for picking in self: + 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), ('state', 'not in', ['draft', 'cancel']), ('move_type', '=', 'out_invoice')], limit=1) + + if not invoice: + continue + + if not picking.so_lama and invoice 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) + invoice_date = fields.Date.to_date(invoice.invoice_date) + + if picking_date != invoice_date and picking.update_date_doc_kirim_add: + raise UserError("Tanggal Kirim (%s) tidak sesuai dengan Tanggal Invoice (%s)!" % ( + picking_date.strftime('%d-%m-%Y'), + invoice_date.strftime('%d-%m-%Y') + )) + + def set_picking_code_out(self): + for picking in self: + # Check if picking meets criteria + is_bu_pick = picking.picking_type_code == 'internal' and 'BU/PICK/' in picking.name + if not is_bu_pick: + continue + + # Find matching outgoing transfers + bu_out_transfers = self.search([ + ('name', 'like', 'BU/OUT/%'), + ('sale_id', '=', picking.sale_id.id), + ('picking_type_code', '=', 'outgoing'), + ('picking_code', '=', False), + ('state', 'not in', ['done', 'cancel']) + ]) + + # Assign sequence code to each matching transfer + for transfer in bu_out_transfers: + transfer.picking_code = self.env['ir.sequence'].next_by_code('stock.picking.code') + + def check_koli(self): + for picking in self: + sale_id = picking.sale_id + for koli_lines in picking.scan_koli_lines: + if koli_lines.koli_id.sale_order_id != sale_id: + raise UserError('Koli tidak sesuai') + + def send_koli_to_so(self): + for picking in self: + if picking.picking_type_code == 'internal' and 'BU/PICK/' in picking.name: + for koli_line in picking.check_koli_lines: + existing_koli = self.env['sales.order.koli'].search([ + ('sale_order_id', '=', picking.sale_id.id), + ('picking_id', '=', picking.id), + ('koli_id', '=', koli_line.id) + ], limit=1) + + if not existing_koli: + self.env['sales.order.koli'].create({ + 'sale_order_id': picking.sale_id.id, + 'picking_id': picking.id, + 'koli_id': koli_line.id + }) + + if picking.picking_type_code == 'outgoing' and 'BU/OUT/' in picking.name: + if picking.state == 'done': + for koli_line in picking.scan_koli_lines: + existing_koli = self.env['sales.order.koli'].search([ + ('sale_order_id', '=', picking.sale_id.id), + ('koli_id', '=', koli_line.koli_id.koli_id.id) + ], limit=1) + + existing_koli.state = 'delivered' + + def check_qty_done_stock(self): + for line in self.move_line_ids_without_package: + def check_qty_per_inventory(self, product, location): + quant = self.env['stock.quant'].search([ + ('product_id', '=', product.id), + ('location_id', '=', location.id), + ]) + + if quant: + return quant.quantity + + return 0 + + qty_onhand = check_qty_per_inventory(self, line.product_id, line.location_id) + if line.qty_done > qty_onhand: + raise UserError('Quantity Done melebihi Quantity Onhand') def send_mail_bills(self): if self.picking_type_code == 'incoming' and self.purchase_id: @@ -864,21 +1462,75 @@ class StockPicking(models.Model): return True def action_cancel(self): - if not self.env.user.is_logistic_approver and self.env.context.get('active_model') == 'stock.picking': + if not self.env.user.is_logistic_approver and ( + self.env.context.get('active_model') == 'stock.picking' or self.env.context.get( + 'active_model') == 'stock.picking.type'): if self.origin and 'Return of' in self.origin: raise UserError("Button ini hanya untuk Logistik") + if not self.env.user.has_group('indoteknik_custom.group_role_it') and not self.env.user.has_group( + 'indoteknik_custom.group_role_logistic') and self.picking_type_code == 'outgoing': + raise UserError("Button ini hanya untuk Logistik") + res = super(StockPicking, self).action_cancel() return res - @api.model def create(self, vals): self._use_faktur(vals) - return super(StockPicking, self).create(vals) + records = super(StockPicking, self).create(vals) + + # Panggil sync_sale_line setelah record dibuat + # records.sync_sale_line(vals) + return records + + def sync_sale_line(self, vals): + # Pastikan kita bekerja dengan record yang sudah ada + for picking in self: + if picking.picking_type_code == 'internal' and 'BU/PICK/' in picking.name: + for line in picking.move_ids_without_package: + if line.product_id and picking.sale_id: + sale_line = self.env['sale.order.line'].search([ + ('product_id', '=', line.product_id.id), + ('order_id', '=', picking.sale_id.id) + ], limit=1) # Tambahkan limit=1 untuk efisiensi + + if sale_line: + line.sale_line_id = sale_line.id def write(self, vals): + if 'linked_manual_bu_out' in vals: + for record in self: + if (record.picking_type_code == 'internal' + and 'BU/PICK/' in record.name): + # Jika menghapus referensi (nilai di-set False/None) + if record.linked_manual_bu_out and not vals['linked_manual_bu_out']: + record.linked_manual_bu_out.state_packing = 'not_packing' + # Jika menambahkan referensi baru + elif vals['linked_manual_bu_out']: + new_picking = self.env['stock.picking'].browse(vals['linked_manual_bu_out']) + new_picking.state_packing = 'packing_done' self._use_faktur(vals) + self.sync_sale_line(vals) + for picking in self: + # Periksa apakah kondisi terpenuhi saat data diubah + if (vals.get('picking_type_code', picking.picking_type_code) == 'incoming' and + vals.get('location_dest_id', picking.location_dest_id.id) == 58): + if 'name' in vals or picking.name.startswith('BU/IN/'): + name_to_modify = vals.get('name', picking.name) + if name_to_modify.startswith('BU/IN/'): + vals['name'] = name_to_modify.replace('BU/IN/', 'BU/INPUT/', 1) + + if (vals.get('picking_type_code', picking.picking_type_code) == 'internal' and + vals.get('location_id', picking.location_id.id) == 58): + name_to_modify = vals.get('name', picking.name) + if name_to_modify.startswith('BU/INT'): + new_name = name_to_modify.replace('BU/INT', 'BU/IN', 1) + # Periksa apakah nama sudah ada + if self.env['stock.picking'].search_count( + [('name', '=', new_name), ('company_id', '=', picking.company_id.id)]) > 0: + new_name = f"{new_name}-DUP" + vals['name'] = new_name return super(StockPicking, self).write(vals) def _use_faktur(self, vals): @@ -920,7 +1572,7 @@ class StockPicking(models.Model): def get_manifests(self): if self.waybill_id and len(self.waybill_id.manifest_ids) > 0: return [self.create_manifest_data(x.description, x.datetime) for x in self.waybill_id.manifest_ids] - + status_mapping = { 'pickup': { 'arrival': 'Sudah diambil', @@ -945,7 +1597,7 @@ class StockPicking(models.Model): if not status: return manifest_datas - + if arrival_date or self.sj_return_date: manifest_datas.append(self.create_manifest_data(status['arrival'], arrival_date)) if departure_date: @@ -957,10 +1609,13 @@ class StockPicking(models.Model): def get_tracking_detail(self): self.ensure_one() + order = self.env['sale.order'].search([('name', '=', self.sale_id.name)], limit=1) + response = { 'delivery_order': { 'name': self.name, 'carrier': self.carrier_id.name or '', + 'service': order.delivery_service_type or '', 'receiver_name': '', 'receiver_city': '' }, @@ -969,20 +1624,105 @@ class StockPicking(models.Model): 'waybill_number': self.delivery_tracking_no or '', 'delivery_status': None, 'eta': self.generate_eta_delivery(), + 'is_biteship': True if self.biteship_id else False, 'manifests': self.get_manifests() } + if self.biteship_id: + histori = self.get_manifest_biteship() + eta_start = order.date_order + timedelta(days=order.estimated_arrival_days_start) + eta_end = order.date_order + timedelta(days=order.estimated_arrival_days) + formatted_eta = f"{eta_start.strftime('%d %b')} - {eta_end.strftime('%d %b %Y')}" + response['eta'] = formatted_eta + response['manifests'] = histori.get("manifests", []) + response['delivered'] = histori.get("delivered", + False) or self.sj_return_date != False or self.driver_arrival_date != False + response['status'] = self._map_status_biteship(histori.get("delivered")) + + return response + if not self.waybill_id or len(self.waybill_id.manifest_ids) == 0: response['delivered'] = self.sj_return_date != False or self.driver_arrival_date != False return response - + response['delivery_order']['receiver_name'] = self.waybill_id.receiver_name response['delivery_order']['receiver_city'] = self.waybill_id.receiver_city response['delivery_status'] = self.waybill_id._get_history('delivery_status') response['delivered'] = self.waybill_id.delivered return response - + + def get_manifest_biteship(self): + api_key = _biteship_api_key + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + + manifests = [] + + try: + # Kirim request ke Biteship + response = requests.get(_biteship_url + '/trackings/' + self.biteship_tracking_id, headers=headers, + json=manifests) + result = response.json() + description = { + 'confirmed': 'Indoteknik telah melakukan permintaan pick-up', + 'allocated': 'Kurir akan melakukan pick-up pesanan', + 'picking_up': 'Kurir sedang dalam perjalanan menuju lokasi pick-up', + 'picked': 'Pesanan sudah di pick-up kurir ' + result.get("courier", {}).get("name", ""), + 'on_hold': 'Pesanan ditahan sementara karena masalah pengiriman', + 'dropping_off': 'Kurir sudah ditugaskan dan pesanan akan segera diantar ke pembeli', + 'delivered': 'Pesanan telah sampai dan diterima oleh ' + result.get("destination", {}).get( + "contact_name", "") + } + if (result.get('success') == True): + history = result.get("history", []) + status = result.get("status", "") + + for entry in reversed(history): + manifests.append({ + "status": re.sub(r'[^a-zA-Z0-9\s]', ' ', entry["status"]).lower().capitalize(), + "datetime": self._convert_to_local_time(entry["updated_at"]), + "description": description[entry["status"]], + }) + + return { + "manifests": manifests, + "delivered": status + } + + return manifests + except Exception as e: + _logger.error(f"Error fetching Biteship order for picking {self.id}: {str(e)}") + return {'error': str(e)} + + def _convert_to_local_time(self, iso_date): + try: + dt_with_tz = waktu.fromisoformat(iso_date) + utc_dt = dt_with_tz.astimezone(pytz.utc) + + local_tz = pytz.timezone("Asia/Jakarta") + local_dt = utc_dt.astimezone(local_tz) + + return local_dt.strftime("%Y-%m-%d %H:%M:%S") + except Exception as e: + return str(e) + + def _map_status_biteship(self, status): + status_mapping = { + "confirmed": "pending", + "scheduled": "pending", + "allocated": "pending", + "picking_up": "pending", + "picked": "shipment", + "cancelled": "cancelled", + "on_hold": "on_hold", + "dropping_off": "shipment", + "delivered": "completed" + } + return status_mapping.get(status, "Hubungi Admin") + def generate_eta_delivery(self): current_date = datetime.datetime.now() prepare_days = 3 @@ -996,7 +1736,7 @@ class StockPicking(models.Model): fastest_eta = start_date + ead_datetime if not self.driver_departure_date and fastest_eta < current_date: fastest_eta = current_date + ead_datetime - + longest_days = 3 longest_eta = fastest_eta + datetime.timedelta(days=longest_days) @@ -1005,51 +1745,510 @@ class StockPicking(models.Model): formatted_fastest_eta = fastest_eta.strftime(format_time_fastest) formatted_longest_eta = longest_eta.strftime(format_time) - + return f'{formatted_fastest_eta} - {formatted_longest_eta}' - -# class CheckProduct(models.Model): -# _name = 'check.product' -# _description = 'Check Product' -# _order = 'picking_id, id' - -# picking_id = fields.Many2one('stock.picking', string='Picking Reference', required=True, ondelete='cascade', index=True, copy=False) -# product_id = fields.Many2one('product.product', string='Product') - - -# @api.constrains('product_id') -# def check_product_validity(self): -# """ -# Validate if the product exists in the related stock.picking's move_ids_without_package -# and ensure that the product's quantity does not exceed the available product_uom_qty. -# """ -# for record in self: -# if not record.picking_id or not record.product_id: -# continue - -# # Filter move lines in the related picking for the selected product -# moves = record.picking_id.move_ids_without_package.filtered( -# lambda move: move.product_id.id == record.product_id.id -# ) - -# if not moves: -# raise UserError(( -# "The product '%s' is not available in the related stock picking's moves. " -# "Please check and try again." -# ) % record.product_id.display_name) - -# # Calculate the total entries for the product in check.product for the same picking -# product_entries_count = self.search_count([ -# ('picking_id', '=', record.picking_id.id), -# ('product_id', '=', record.product_id.id) -# ]) - -# # Sum the product_uom_qty for all relevant moves -# total_qty_in_moves = sum(moves.mapped('product_uom_qty')) - -# # Compare the count of entries against the available quantity -# if product_entries_count > total_qty_in_moves: -# raise UserError(( -# "The product '%s' exceeds the allowable quantity (%s) in the related stock picking's moves. " -# "You can only add it %s times." -# ) % (record.product_id.display_name, total_qty_in_moves, total_qty_in_moves)) + + +class CheckProduct(models.Model): + _name = 'check.product' + _description = 'Check Product' + _order = 'picking_id, id' + + picking_id = fields.Many2one( + 'stock.picking', + string='Picking 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.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 + pickings = self.mapped('picking_id') + + # Store product_ids that will be deleted + deleted_product_ids = self.mapped('product_id') + + # Perform the deletion + result = super(CheckProduct, self).unlink() + + # After deletion, update moves for affected pickings + for picking in pickings: + # For products that were completely removed (no remaining check.product lines) + remaining_product_ids = picking.check_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 = picking.move_ids_without_package.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(picking) + + return result + + @api.depends('quantity') + def _compute_status(self): + for record in self: + moves = record.picking_id.move_ids_without_package.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(CheckProduct, 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(CheckProduct, 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, picking): + """ + Sinkronisasi quantity_done di move_ids_without_package + dengan total quantity dari check.product berdasarkan product_id. + """ + for product_id in picking.check_product_lines.mapped('product_id'): + # Totalkan quantity dari semua baris check.product untuk product_id ini + total_quantity = sum( + line.quantity for line in + picking.check_product_lines.filtered(lambda line: line.product_id == product_id) + ) + # Update quantity_done di move yang relevan + moves = picking.move_ids_without_package.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 picking_id + and sync the total quantity to related moves. + """ + for picking in self.mapped('picking_id'): + lines_to_remove = self.env['check.product'] # Recordset untuk menyimpan baris yang akan dihapus + product_lines = picking.check_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.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(picking) + + @api.onchange('product_id', 'quantity') + def check_product_validity(self): + for record in self: + if not record.picking_id or not record.product_id: + continue + + # Filter moves related to the selected product + moves = record.picking_id.move_ids_without_package.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.picking_id.check_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 BarcodeProduct(models.Model): + _name = 'barcode.product' + _description = 'Barcode Product' + _order = 'picking_id, id' + + picking_id = fields.Many2one( + 'stock.picking', + string='Picking Reference', + required=True, + ondelete='cascade', + index=True, + copy=False, + ) + product_id = fields.Many2one('product.product', string='Product', required=True) + barcode = fields.Char(string='Barcode') + + def check_duplicate_barcode(self): + barcode_product = self.env['product.product'].search([('barcode', '=', self.barcode)]) + + if barcode_product: + raise UserError('Barcode sudah digunakan {}'.format(barcode_product.display_name)) + + barcode_box = self.env['product.product'].search([('barcode_box', '=', self.barcode)]) + + if barcode_box: + raise UserError('Barcode box sudah digunakan {}'.format(barcode_box.display_name)) + + @api.constrains('barcode') + def send_barcode_to_product(self): + for record in self: + record.check_duplicate_barcode() + if record.barcode and not record.product_id.barcode: + record.product_id.barcode = record.barcode + else: + raise UserError('Barcode sudah terisi') + + +class CheckKoli(models.Model): + _name = 'check.koli' + _description = 'Check Koli' + _order = 'picking_id, id' + _rec_name = 'koli' + + picking_id = fields.Many2one( + 'stock.picking', + string='Picking Reference', + required=True, + ondelete='cascade', + index=True, + copy=False, + ) + koli = fields.Char(string='Koli') + reserved_id = fields.Many2one('stock.picking', string='Reserved Picking') + check_koli_progress = fields.Char( + string="Progress Check Koli" + ) + + @api.constrains('koli') + def _check_koli_progress(self): + for check in self: + if check.picking_id: + all_checks = self.env['check.koli'].search([('picking_id', '=', check.picking_id.id)], order='id') + if all_checks: + check_index = list(all_checks).index(check) + 1 # Nomor urut check + total_so_koli = len(all_checks) + check.check_koli_progress = f"{check_index}/{total_so_koli}" if total_so_koli else "0/0" + + +class ScanKoli(models.Model): + _name = 'scan.koli' + _description = 'Scan Koli' + _order = 'picking_id, id' + _rec_name = 'koli_id' + + picking_id = fields.Many2one( + 'stock.picking', + string='Picking Reference', + required=True, + ondelete='cascade', + index=True, + copy=False, + ) + koli_id = fields.Many2one('sales.order.koli', string='Koli') + scan_koli_progress = fields.Char( + string="Progress Scan Koli", + compute="_compute_scan_koli_progress" + ) + code_koli = fields.Char(string='Code Koli') + + @api.onchange('code_koli') + def _onchange_code_koli(self): + if self.code_koli: + koli = self.env['sales.order.koli'].search([('koli_id.koli', '=', self.code_koli)], limit=1) + if koli: + self.write({'koli_id': koli.id}) + else: + raise UserError('Koli tidak ditemukan') + + # def _compute_scan_koli_progress(self): + # for scan in self: + # if scan.picking_id: + # all_scans = self.env['scan.koli'].search([('picking_id', '=', scan.picking_id.id)], order='id') + # if all_scans: + # scan_index = list(all_scans).index(scan) + 1 # Nomor urut scan + # total_so_koli = scan.picking_id.total_so_koli + # scan.scan_koli_progress = f"{scan_index}/{total_so_koli}" if total_so_koli else "0/0" + + @api.onchange('koli_id') + def _onchange_koli_compare_with_konfirm_koli(self): + if not self.koli_id: + return + + if not self.picking_id.konfirm_koli_lines: + raise UserError(_('Mapping Koli Harus Diisi!')) + + koli_picking = self.koli_id.picking_id._origin + + konfirm_pick_ids = [ + line.pick_id._origin + for line in self.picking_id.konfirm_koli_lines + if line.pick_id + ] + + if koli_picking not in konfirm_pick_ids: + raise UserError(_('Koli tidak sesuai dengan mapping koli, pastikan picking terkait benar!')) + + @api.constrains('picking_id', 'koli_id') + def _check_duplicate_koli(self): + for record in self: + if record.koli_id: + existing_koli = self.search([ + ('picking_id', '=', record.picking_id.id), + ('koli_id', '=', record.koli_id.id), + ('id', '!=', record.id) + ]) + if existing_koli: + raise ValidationError(f"⚠️ Koli '{record.koli_id.display_name}' sudah discan untuk picking ini!") + + def unlink(self): + picking_ids = set(self.mapped('koli_id.picking_id.id')) + for scan in self: + koli = scan.koli_id.koli_id + if koli: + koli.reserved_id = False + + for picking_id in picking_ids: + remaining_scans = self.env['sales.order.koli'].search_count([ + ('koli_id.picking_id', '=', picking_id) + ]) + + delete_koli = len(self.filtered(lambda rec: rec.koli_id.picking_id.id == picking_id)) + + if remaining_scans == delete_koli: + picking = self.env['stock.picking'].browse(picking_id) + picking.linked_out_picking_id = False + else: + raise UserError( + _("Tidak dapat menghapus scan koli, karena masih ada scan koli lain yang tersisa untuk picking ini.")) + + for picking_id in picking_ids: + self._reset_qty_done_if_no_scan(picking_id) + + # self.check_koli_not_balance() + + return super(ScanKoli, self).unlink() + + @api.onchange('koli_id', 'scan_koli_progress') + def onchange_koli_id(self): + if not self.koli_id: + return + + for scan in self: + if scan.koli_id.koli_id.picking_id.group_id.id != scan.picking_id.group_id.id: + scan.koli_id.koli_id.reserved_id = scan.picking_id.id.origin + scan.koli_id.koli_id.picking_id.linked_out_picking_id = scan.picking_id.id.origin + + def _compute_scan_koli_progress(self): + for scan in self: + if not scan.picking_id: + scan.scan_koli_progress = "0/0" + continue + + try: + all_scans = self.env['scan.koli'].search([('picking_id', '=', scan.picking_id.id)], order='id') + if all_scans: + scan_index = list(all_scans).index(scan) + 1 + total_so_koli = scan.picking_id.total_so_koli or 0 + scan.scan_koli_progress = f"{scan_index}/{total_so_koli}" + else: + scan.scan_koli_progress = "0/0" + except Exception: + # Fallback in case of any error + scan.scan_koli_progress = "0/0" + + @api.constrains('picking_id', 'picking_id.total_so_koli') + def _check_koli_validation(self): + for scan in self.picking_id.scan_koli_lines: + scan.koli_id.koli_id.reserved_id = scan.picking_id.id + scan.koli_id.koli_id.picking_id.linked_out_picking_id = scan.picking_id.id + + total_scans = len(self.picking_id.scan_koli_lines) + if total_scans != self.picking_id.total_so_koli: + raise UserError(_("Jumlah scan koli tidak sama dengan total SO koli!")) + + # def check_koli_not_balance(self): + # for scan in self: + # total_scancs = self.env['scan.koli'].search_count([('picking_id', '=', scan.picking_id.id), ('id', '!=', scan.id)]) + # if total_scancs != scan.picking_id.total_so_koli: + # raise UserError(_("Jumlah scan koli tidak sama dengan total SO koli!")) + + @api.onchange('koli_id') + def _onchange_koli_id(self): + if not self.koli_id: + return + + source_koli_so = self.picking_id.group_id.id + source_koli = self.koli_id.picking_id.group_id.id + + if source_koli_so != source_koli: + raise UserError(_('Koli tidak sesuai, pastikan picking terkait benar!')) + + @api.constrains('koli_id') + def _send_product_from_koli_id(self): + if not self.koli_id: + return + + koli_count_by_picking = defaultdict(int) + for scan in self: + koli_count_by_picking[scan.koli_id.picking_id.id] += 1 + + for picking_id, total_koli in koli_count_by_picking.items(): + picking = self.env['stock.picking'].browse(picking_id) + + if total_koli == picking.quantity_koli: + pick_moves = self.env['stock.move.line'].search([('picking_id', '=', picking_id)]) + out_moves = self.env['stock.move.line'].search([('picking_id', '=', picking.linked_out_picking_id.id)]) + + for pick_move in pick_moves: + corresponding_out_move = out_moves.filtered(lambda m: m.product_id == pick_move.product_id) + if corresponding_out_move: + corresponding_out_move.qty_done += pick_move.qty_done + + def _reset_qty_done_if_no_scan(self, picking_id): + product_bu_pick = self.env['stock.move.line'].search([('picking_id', '=', picking_id)]) + + for move in product_bu_pick: + product_bu_out = self.env['stock.move.line'].search( + [('picking_id', '=', self.picking_id.id), ('product_id', '=', move.product_id.id)]) + for bu_out in product_bu_out: + bu_out.qty_done -= move.qty_done + # if remaining_scans == 0: + # picking = self.env['stock.picking'].browse(picking_id) + # picking.move_line_ids_without_package.write({'qty_done': 0}) + # picking.message_post(body=f"⚠ qty_done direset ke 0 untuk Picking {picking.name} karena tidak ada scan.koli yang tersisa.") + + # return remaining_scans + + +class KonfirmKoli(models.Model): + _name = 'konfirm.koli' + _description = 'Konfirm Koli' + _order = 'picking_id, id' + _rec_name = 'pick_id' + + picking_id = fields.Many2one( + 'stock.picking', + string='Picking Reference', + required=True, + ondelete='cascade', + index=True, + copy=False, + ) + pick_id = fields.Many2one('stock.picking', string='Pick') + + @api.constrains('pick_id') + def _check_duplicate_pick_id(self): + for rec in self: + exist = self.search([ + ('pick_id', '=', rec.pick_id.id), + ('picking_id', '=', rec.picking_id.id), + ('id', '!=', rec.id), + ]) + + if exist: + raise UserError(f"⚠️ '{rec.pick_id.display_name}' sudah discan untuk picking ini!") + + +class WarningModalWizard(models.TransientModel): + _name = 'warning.modal.wizard' + _description = 'Peringatan Koli Belum Diperiksa' + + name = fields.Char(default="⚠️ Perhatian!") + message = fields.Text() + picking_id = fields.Many2one('stock.picking') + + def action_continue(self): + if self.picking_id: + return self.picking_id.with_context(skip_koli_check=True).button_validate() + return {'type': 'ir.actions.act_window_close'} diff --git a/indoteknik_custom/models/stock_picking_return.py b/indoteknik_custom/models/stock_picking_return.py index d4347235..a683d80e 100644 --- a/indoteknik_custom/models/stock_picking_return.py +++ b/indoteknik_custom/models/stock_picking_return.py @@ -24,4 +24,15 @@ class ReturnPicking(models.TransientModel): # if not stock_picking.approval_return_status == 'approved' and purchase.invoice_ids: # raise UserError('Harus Approval Accounting AP untuk melakukan Retur') - return res
\ No newline at end of file + return res + +class ReturnPickingLine(models.TransientModel): + _inherit = 'stock.return.picking.line' + + @api.onchange('quantity') + def _onchange_quantity(self): + for rec in self: + qty_done = rec.move_id.quantity_done + + if rec.quantity > qty_done: + raise UserError(f"Quantity yang Anda masukkan tidak boleh melebihi quantity done yaitu: {qty_done}")
\ No newline at end of file diff --git a/indoteknik_custom/models/uangmuka_pembelian.py b/indoteknik_custom/models/uangmuka_pembelian.py index 204855d3..ba41f814 100644 --- a/indoteknik_custom/models/uangmuka_pembelian.py +++ b/indoteknik_custom/models/uangmuka_pembelian.py @@ -64,7 +64,7 @@ class UangmukaPembelian(models.TransientModel): param_debit = { 'move_id': account_move.id, - 'account_id': 401, # uang muka persediaan barang dagang + 'account_id': 669, # uang muka persediaan barang dagang 'partner_id': partner_id, 'currency_id': 12, 'debit': self.pay_amt, diff --git a/indoteknik_custom/models/uangmuka_penjualan.py b/indoteknik_custom/models/uangmuka_penjualan.py index 5acf604d..a3e95ecd 100644 --- a/indoteknik_custom/models/uangmuka_penjualan.py +++ b/indoteknik_custom/models/uangmuka_penjualan.py @@ -74,7 +74,7 @@ class UangmukaPenjualan(models.TransientModel): # sisa di credit untuk uang muka penjualan, diluar ongkir dan selisih param_credit = { 'move_id': account_move.id, - 'account_id': 449, # uang muka penjualan + 'account_id': 668, # penerimaan belum alokasi 'partner_id': partner_id, 'currency_id': 12, 'debit': 0, diff --git a/indoteknik_custom/models/user_company_request.py b/indoteknik_custom/models/user_company_request.py index af8a86ba..9216e8eb 100644 --- a/indoteknik_custom/models/user_company_request.py +++ b/indoteknik_custom/models/user_company_request.py @@ -104,9 +104,9 @@ class UserCompanyRequest(models.Model): self.user_company_id.active = True user.send_company_switch_approve_mail() if vals.get('is_switch_account') == True else user.send_company_request_approve_mail() else: - new_company = self.env['res.partner'].create({ - 'name': self.user_input - }) + # new_company = self.env['res.partner'].create({ + # 'name': self.user_input + # }) # self.user_id.parent_id = new_company.id user.send_company_request_reject_mail() return super(UserCompanyRequest, self).write(vals) diff --git a/indoteknik_custom/models/user_pengajuan_tempo.py b/indoteknik_custom/models/user_pengajuan_tempo.py index 0fdcdbeb..0b3ab63d 100644 --- a/indoteknik_custom/models/user_pengajuan_tempo.py +++ b/indoteknik_custom/models/user_pengajuan_tempo.py @@ -74,6 +74,7 @@ class UserPengajuanTempo(models.Model): # Pengiriman pic_tittle = fields.Char(string='Tittle PIC Penerimaan Barang') + pic_mobile = fields.Char(string='Nomor HP PIC Penerimaan Barang') pic_name = fields.Char(string='Nama PIC Penerimaan Barang') street_pengiriman = fields.Char(string="Alamat Perusahaan") state_id_pengiriman = fields.Many2one('res.country.state', string='State') @@ -83,6 +84,7 @@ class UserPengajuanTempo(models.Model): zip_pengiriman = fields.Char(string="Zip") invoice_pic_tittle = fields.Char(string='Tittle PIC Penerimaan Invoice') invoice_pic = fields.Char(string='Nama PIC Penerimaan Invoice') + invoice_pic_mobile = fields.Char(string='Nomor HP PIC Penerimaan Invoice') street_invoice = fields.Char(string="Alamat Perusahaan") state_id_invoice = fields.Many2one('res.country.state', string='State') city_id_invoice = fields.Many2one('vit.kota', string='City') @@ -97,6 +99,7 @@ class UserPengajuanTempo(models.Model): dokumen_invoice = fields.Char(string='Dokumen yang dilampirkan saat Pengiriman Invoice') is_same_address = fields.Boolean(string="Same Address pengiriman invoicr dan alamat pengiriman barang") is_same_address_street = fields.Boolean(string="Same Address pengiriman barang dan alamat bisnis") + dokumen_prosedur = fields.Many2many('ir.attachment', 'dokumen_prosedur_rel', string="Dokumen Prosedur", tracking=True) # Referensi supplier_ids = fields.Many2many('user.pengajuan.tempo.line', string="Suppliers") diff --git a/indoteknik_custom/models/user_pengajuan_tempo_request.py b/indoteknik_custom/models/user_pengajuan_tempo_request.py index b43f56ac..565b0315 100644 --- a/indoteknik_custom/models/user_pengajuan_tempo_request.py +++ b/indoteknik_custom/models/user_pengajuan_tempo_request.py @@ -7,7 +7,7 @@ class RejectReasonWizard(models.TransientModel): _description = 'Wizard for Reject Reason' request_id = fields.Many2one('user.pengajuan.tempo.request', string='Request') - reason_reject = fields.Text(string='Reason for Rejection', required=True) + reason_reject = fields.Text(string='Reason for Rejection', required=True, tracking=True) def confirm_reject(self): tempo = self.request_id @@ -55,7 +55,27 @@ class UserPengajuanTempoRequest(models.Model): ('approval_director', 'Approved by Director'), ('reject', 'Rejected'), ], string='Status', readonly=True, copy=False, index=True, track_visibility='onchange', default='draft') - reason_reject = fields.Char(string='Limit Tempo') + last_state_tempo = fields.Selection([ + ('draft', 'Pengajuan Tempo'), + ('approval_sales', 'Approved by Sales Manager'), + ('approval_finance', 'Approved by Finance'), + ('approval_director', 'Approved by Director'), + ('reject', 'Rejected'), + ], string='Status',) + reason_reject = fields.Char(string='Reason Reaject', tracking=True, track_visibility='onchange') + state_tempo_text = fields.Char(string="Status", compute="_compute_state_tempo_text") + + @api.depends('state_tempo') + def _compute_state_tempo_text(self): + for record in self: + status_mapping = { + 'draft': "Menunggu Approve Manager", + 'approval_sales': "Menunggu Approve Finance", + 'approval_finance': "Menunggu Approve Direktur", + 'approval_director': "Approved", + 'reject': "Rejected", + } + record.state_tempo_text = status_mapping.get(record.state_tempo, "Unknown") # informasi perusahaan name_tempo = fields.Many2one('res.partner', string='Nama Perusahaan', related='pengajuan_tempo_id.name_tempo', store=True, tracking=True, readonly=False) @@ -71,7 +91,7 @@ class UserPengajuanTempoRequest(models.Model): website_tempo = fields.Char(string='Website', related='pengajuan_tempo_id.website_tempo', store=True, tracking=True, readonly=False) portal = fields.Boolean(string='Portal Website', related='pengajuan_tempo_id.portal', store=True, tracking=True, readonly=False) estimasi_tempo = fields.Char(string='Estimasi Pembelian Pertahun', related='pengajuan_tempo_id.estimasi_tempo', store=True, tracking=True, readonly=False) - tempo_duration_origin = fields.Many2one('account.payment.term', string='Durasi Tempo', related='pengajuan_tempo_id.tempo_duration', store=True, tracking=True, readonly=False, domain=[('id', 'in', [24, 25, 29, 32])]) + tempo_duration_origin = fields.Many2one('account.payment.term', string='Durasi Tempo', related='tempo_duration', store=True, tracking=True, readonly=False, domain=[('id', 'in', [24, 25, 29, 32])]) tempo_limit_origin = fields.Char(string='Limit Tempo', related='pengajuan_tempo_id.tempo_limit' , store=True, tracking=True, readonly=False) category_produk_ids = fields.Many2many('product.public.category', string='Kategori Produk yang Digunakan', related='pengajuan_tempo_id.category_produk_ids', readonly=False) @@ -88,16 +108,18 @@ class UserPengajuanTempoRequest(models.Model): # Pengiriman pic_tittle = fields.Char(string='Tittle PIC Penerimaan Barang', related='pengajuan_tempo_id.pic_tittle', store=True, readonly=False) + pic_mobile = fields.Char(string='Nomor HP PIC Penerimaan Barang', related='pengajuan_tempo_id.pic_mobile', store=True, readonly=False) pic_name = fields.Char(string='Nama PIC Penerimaan Barang', related='pengajuan_tempo_id.pic_name', store=True, readonly=False) - street_pengiriman = fields.Char(string="Alamat Perusahaan", related='pengajuan_tempo_id.street_pengiriman', store=True, readonly=False) + street_pengiriman = fields.Char(string="Alamat Pengiriman Barang", related='pengajuan_tempo_id.street_pengiriman', store=True, readonly=False) state_id_pengiriman = fields.Many2one('res.country.state', string='State', related='pengajuan_tempo_id.state_id_pengiriman', store=True, readonly=False) city_id_pengiriman = fields.Many2one('vit.kota', string='City', related='pengajuan_tempo_id.city_id_pengiriman', store=True, readonly=False) district_id_pengiriman = fields.Many2one('vit.kecamatan', string='Kecamatan',related='pengajuan_tempo_id.district_id_pengiriman', store=True, readonly=False) subDistrict_id_pengiriman = fields.Many2one('vit.kelurahan', string='Kelurahan', related='pengajuan_tempo_id.subDistrict_id_pengiriman', store=True, readonly=False) zip_pengiriman = fields.Char(string="Zip", related='pengajuan_tempo_id.zip_pengiriman', store=True, readonly=False) invoice_pic_tittle = fields.Char(string='Tittle PIC Penerimaan Invoice', related='pengajuan_tempo_id.invoice_pic_tittle', store=True, readonly=False) + invoice_pic_mobile = fields.Char(string='Nomor HP PIC Penerimaan Invoice', related='pengajuan_tempo_id.invoice_pic_mobile', store=True, readonly=False) invoice_pic = fields.Char(string='Nama PIC Penerimaan Invoice', related='pengajuan_tempo_id.invoice_pic', store=True, readonly=False) - street_invoice = fields.Char(string="Alamat Perusahaan", related='pengajuan_tempo_id.street_invoice', store=True, readonly=False) + street_invoice = fields.Char(string="Alamat Pengiriman Invoice", related='pengajuan_tempo_id.street_invoice', store=True, readonly=False) state_id_invoice = fields.Many2one('res.country.state', string='State', related='pengajuan_tempo_id.state_id_invoice', store=True, readonly=False) city_id_invoice = fields.Many2one('vit.kota', string='City', related='pengajuan_tempo_id.city_id_invoice', store=True, readonly=False) district_id_invoice = fields.Many2one('vit.kecamatan', string='Kecamatan', related='pengajuan_tempo_id.district_id_invoice', store=True, readonly=False) @@ -110,7 +132,14 @@ class UserPengajuanTempoRequest(models.Model): dokumen_invoice = fields.Char(string='Dokumen yang dilampirkan saat Pengiriman Invoice', related='pengajuan_tempo_id.dokumen_invoice', store=True, readonly=False) is_same_address = fields.Boolean(string="Same Address pengiriman invoicr dan alamat pengiriman barang", related='pengajuan_tempo_id.is_same_address', store=True, readonly=False) is_same_address_street = fields.Boolean(string="Same Address pengiriman barang dan alamat bisnis", related='pengajuan_tempo_id.is_same_address_street', store=True, readonly=False) - + dokumen_prosedur = fields.Many2many( + 'ir.attachment', + 'dokumen_prosedur_rel', + string="Dokumen Prosedur", + related='pengajuan_tempo_id.dokumen_prosedur', + readonly=False, + tracking=3 + ) #Referensi supplier_ids = fields.Many2many('user.pengajuan.tempo.line',related='pengajuan_tempo_id.supplier_ids', string="Suppliers", readonly=False) @@ -272,16 +301,17 @@ class UserPengajuanTempoRequest(models.Model): self.pengajuan_tempo_id.finance_mobile = self.finance_mobile self.pengajuan_tempo_id.finance_email = self.finance_email - @api.onchange('pic_tittle', 'pic_name', 'street_pengiriman', 'state_id_pengiriman', 'city_id_pengiriman', + @api.onchange('pic_tittle','pic_mobile', 'pic_name', 'street_pengiriman', 'state_id_pengiriman', 'city_id_pengiriman', 'zip_pengiriman', 'district_id_pengiriman', 'subDistrict_id_pengiriman' - 'invoice_pic_tittle', 'invoice_pic', 'street_invoice', 'state_id_invoice', 'city_id_invoice', + 'invoice_pic_tittle','invoice_pic_mobile', 'invoice_pic', 'street_invoice', 'state_id_invoice', 'city_id_invoice', 'district_id_invoice', 'subDistrict_id_invoice', 'zip_invoice', 'tukar_invoice', 'jadwal_bayar', 'dokumen_pengiriman', 'dokumen_pengiriman_input', 'dokumen_invoice', - 'is_same_address', 'is_same_address_street') + 'is_same_address', 'is_same_address_street','dokumen_prosedur') def _onchange_related_fields_pengiriman(self): if self.pengajuan_tempo_id: # Perbarui nilai di pengajuan_tempo_id self.pengajuan_tempo_id.pic_tittle = self.pic_tittle + self.pengajuan_tempo_id.pic_mobile = self.pic_mobile self.pengajuan_tempo_id.pic_name = self.pic_name self.pengajuan_tempo_id.street_pengiriman = self.street_pengiriman self.pengajuan_tempo_id.state_id_pengiriman = self.state_id_pengiriman @@ -290,6 +320,7 @@ class UserPengajuanTempoRequest(models.Model): self.pengajuan_tempo_id.subDistrict_id_pengiriman = self.subDistrict_id_pengiriman self.pengajuan_tempo_id.zip_pengiriman = self.zip_pengiriman self.pengajuan_tempo_id.invoice_pic_tittle = self.invoice_pic_tittle + self.pengajuan_tempo_id.invoice_pic_mobile = self.invoice_pic_mobile self.pengajuan_tempo_id.invoice_pic = self.invoice_pic self.pengajuan_tempo_id.street_invoice = self.street_invoice self.pengajuan_tempo_id.state_id_invoice = self.state_id_invoice @@ -304,6 +335,7 @@ class UserPengajuanTempoRequest(models.Model): self.pengajuan_tempo_id.dokumen_invoice = self.dokumen_invoice self.pengajuan_tempo_id.is_same_address = self.is_same_address self.pengajuan_tempo_id.is_same_address_street = self.is_same_address_street + self.pengajuan_tempo_id.dokumen_prosedur = self.dokumen_prosedur @api.onchange('supplier_ids') def _onchange_supplier_ids(self): @@ -317,7 +349,6 @@ class UserPengajuanTempoRequest(models.Model): def _onchange_related_fields_dokumen(self): if self.pengajuan_tempo_id: # Perbarui nilai di pengajuan_tempo_id - self.pengajuan_tempo_id.dokumen_nib = self.dokumen_nib self.pengajuan_tempo_id.dokumen_siup = self.dokumen_siup self.pengajuan_tempo_id.dokumen_tdp = self.dokumen_tdp self.pengajuan_tempo_id.dokumen_skdp = self.dokumen_skdp @@ -334,13 +365,13 @@ class UserPengajuanTempoRequest(models.Model): @api.onchange('tempo_duration') def _tempo_duration_change(self): for tempo in self: - if tempo.env.user.id not in (7, 377, 12182): + if tempo.env.user.id not in (7, 688, 28, 377, 12182): raise UserError("Durasi tempo hanya bisa di ubah oleh Sales Manager atau Direktur") @api.onchange('tempo_limit') def _onchange_tempo_limit(self): for tempo in self: - if tempo.env.user.id not in (7, 377, 12182): + if tempo.env.user.id not in (7, 688, 28, 377, 12182): raise UserError("Limit tempo hanya bisa diubah oleh Sales Manager atau Direktur") def button_approve(self): for tempo in self: @@ -409,6 +440,9 @@ class UserPengajuanTempoRequest(models.Model): 'target': 'new', 'context': {'default_request_id': self.id}, } + def button_draft(self): + for tempo in self: + tempo.state_tempo = tempo.last_state_tempo if tempo.last_state_tempo else 'draft' def write(self, vals): is_approve = True if self.state_tempo == 'approval_director' or vals.get('state_tempo') == 'approval_director' else False @@ -486,6 +520,7 @@ class UserPengajuanTempoRequest(models.Model): { "type": "delivery", "name": self.pengajuan_tempo_id.pic_name, + "phone": self.pengajuan_tempo_id.pic_mobile, "street": self.pengajuan_tempo_id.street_pengiriman, "state_id": self.pengajuan_tempo_id.state_id_pengiriman.id, "kota_id": self.pengajuan_tempo_id.city_id_pengiriman.id, @@ -496,6 +531,7 @@ class UserPengajuanTempoRequest(models.Model): { "type": "invoice", "name": self.pengajuan_tempo_id.invoice_pic, + "phone": self.pengajuan_tempo_id.invoice_pic_mobile, "street": self.pengajuan_tempo_id.street_invoice, "state_id": self.pengajuan_tempo_id.state_id_invoice.id, "kota_id": self.pengajuan_tempo_id.city_id_invoice.id, @@ -507,10 +543,25 @@ class UserPengajuanTempoRequest(models.Model): # Buat kontak baru untuk company_id for contact_data in contacts_data: - self.env['res.partner'].create({ - "parent_id": self.user_company_id.id, # Hubungkan ke perusahaan - **contact_data, # Tambahkan data kontak - }) + existing_contact = self.env['res.partner'].search([ + ('parent_id', '=', self.user_company_id.id), + ('name', '=', contact_data['name']) + ], limit=1) + + if not existing_contact: + # Pastikan tidak ada partner lain dengan nama yang sama sebelum membuat baru + duplicate_check = self.env['res.partner'].search([ + ('name', '=', contact_data['name']) + ], limit=1) + + if duplicate_check: + # Jika nama sudah ada tetapi di perusahaan lain, tambahkan nama perusahaan + contact_data['name'] = f"{contact_data['name']} ({self.user_company_id.name})" + + self.env['res.partner'].create({ + "parent_id": self.user_company_id.id, # Hubungkan ke perusahaan + **contact_data, # Tambahkan data kontak + }) # Pengiriman self.user_company_id.pic_name = self.pengajuan_tempo_id.pic_name @@ -532,6 +583,10 @@ class UserPengajuanTempoRequest(models.Model): self.user_company_id.dokumen_pengiriman = self.pengajuan_tempo_id.dokumen_pengiriman self.user_company_id.dokumen_pengiriman_input = self.pengajuan_tempo_id.dokumen_pengiriman_input self.user_company_id.dokumen_invoice = self.pengajuan_tempo_id.dokumen_invoice + self.user_company_id.dokumen_prosedur = self.pengajuan_tempo_id.dokumen_prosedur[0] if self.pengajuan_tempo_id.dokumen_prosedur else [] + if self.user_company_id.dokumen_prosedur: + self.user_company_id.message_post(body='Dokumen Prosedur', + attachment_ids=[self.user_company_id.dokumen_prosedur.id]) # Referensi self.user_company_id.supplier_ids = self.pengajuan_tempo_id.supplier_ids @@ -541,6 +596,8 @@ class UserPengajuanTempoRequest(models.Model): if self.user_company_id.dokumen_npwp: self.user_company_id.message_post(body='Dokumen NPWP', attachment_ids=[self.user_company_id.dokumen_npwp.id]) + + self.user_company_id.dokumen_sppkp = self.pengajuan_tempo_id.dokumen_sppkp[0] if self.pengajuan_tempo_id.dokumen_sppkp else [] if self.user_company_id.dokumen_sppkp: self.user_company_id.message_post(body='Dokumen SPPKP', attachment_ids=[self.user_company_id.dokumen_sppkp.id]) @@ -596,10 +653,10 @@ class UserPengajuanTempoRequest(models.Model): attachment_ids=[self.user_company_id.dokumen_tempat_bekerja.id]) # self.user_company_id.active = True # user.send_company_request_approve_mail() - self.user_company_id.property_payment_term_id = self.pengajuan_tempo_id.tempo_duration.id + self.user_company_id.property_payment_term_id = self.tempo_duration.id self.user_company_id.active_limit = True - self.user_company_id.warning_stage = float(limit_tempo) - (float(limit_tempo)/2) self.user_company_id.blocking_stage = limit_tempo + self.user_company_id.warning_stage = float(limit_tempo) - (float(limit_tempo)/2) # Internal Notes comment = [] diff --git a/indoteknik_custom/models/vendor_sla.py b/indoteknik_custom/models/vendor_sla.py new file mode 100644 index 00000000..b052e6cb --- /dev/null +++ b/indoteknik_custom/models/vendor_sla.py @@ -0,0 +1,102 @@ +from odoo import models, fields, api +import logging +import math +_logger = logging.getLogger(__name__) + +class VendorSLA(models.Model): + _name = 'vendor.sla' + _description = 'Vendor SLA' + _rec_name = 'id_vendor' + + id_vendor = fields.Many2one('res.partner', string='Name', domain="[('industry_id', '=', 46)]") + duration = fields.Integer(string='Duration', description='SLA Duration') + unit = fields.Selection( + [('jam', 'Jam'),('hari', 'Hari')], + string="SLA Time" + ) + duration_unit = fields.Char(string="Duration (Unit)", compute="_compute_duration_unit") + + # pertama, lakukan group by vendor pada modul purchase.order + # kedua, pada setiap Purchase order pada group by vendor tersebut, lakukan penghitungan penjumlahan setiap nilai datetime field date_planed dikurangi date_approve purchase order + # dibagi jumlah data dari setiap Purchase order pada group by vendor tersebut. hasilnya lalu di gunakan untuk mengset nilai duration + def generate_vendor_id_sla(self): + # Step 1: Group stock pickings by vendor based on operation type + stock_picking_env = self.env['stock.picking'] + stock_moves = stock_picking_env.read_group( + domain=[ + ('state', '=', 'done'), + ('location_id', '=', 4), # Partner Locations/Vendors + ('location_dest_id', '=', 57) # BU/Stock + ], + fields=['partner_id', 'date_done', 'scheduled_date'], + groupby=['partner_id'], + lazy=False + ) + + for group in stock_moves: + partner_id = group['partner_id'][0] + total_duration = 0 + count = 0 + + # Step 2: Calculate the average duration for each vendor + pos_for_vendor = stock_picking_env.search([ + ('partner_id', '=', partner_id), + ('state', '=', 'done'), + ]) + + for po in pos_for_vendor: + if po.date_done and po.purchase_id.date_approve: + date_of_transfer = fields.Datetime.to_datetime(po.date_done) + po_confirmation_date = fields.Datetime.to_datetime(po.purchase_id.date_approve) + if date_of_transfer < po_confirmation_date: continue + + days_difference = (date_of_transfer - po_confirmation_date).days + if days_difference > 14: + continue + duration = (date_of_transfer - po_confirmation_date).total_seconds() / 3600 # Convert to hours + total_duration += duration + count += 1 + + if count > 0: + average_duration = total_duration / count + + # Step 3: Update the duration field in the corresponding res.partner record + vendor_sla = self.search([('id_vendor', '=', partner_id)], limit=1) + + # Konversi jam ke hari jika diperlukan + if average_duration >= 24: + days = average_duration / 24 + if days - int(days) > 0.5: # Jika sisa lebih dari 0,5, bulatkan ke atas + days = int(days) + 1 + else: # Jika sisa 0,5 atau kurang, bulatkan ke bawah + days = int(days) + duration_to_save = days + unit_to_save = 'hari' + else: + duration_to_save = round(average_duration) + unit_to_save = 'jam' + + # Update atau create vendor SLA record + if vendor_sla: + vendor_sla.write({ + 'duration': duration_to_save, + 'unit': unit_to_save + }) + else: + self.create({ + 'id_vendor': partner_id, + 'duration': duration_to_save, + 'unit': unit_to_save + }) + _logger.info(f'Proses SLA untuk Vendor selesai dilakukan') + + @api.depends('duration', 'unit') + def _compute_duration_unit(self): + for record in self: + if record.duration and record.unit: + record.duration_unit = f"{record.duration} {record.unit}" + else: + record.duration_unit = "-" + + +
\ No newline at end of file diff --git a/indoteknik_custom/models/voucher.py b/indoteknik_custom/models/voucher.py index 37c97338..b213a039 100644 --- a/indoteknik_custom/models/voucher.py +++ b/indoteknik_custom/models/voucher.py @@ -11,43 +11,47 @@ class Voucher(models.Model): name = fields.Char(string='Name') image = fields.Binary(string='Image') code = fields.Char(string='Code', help='Kode voucher yang akan berlaku untuk pengguna') + voucher_category = fields.Many2many('product.public.category', string='Category Voucher', + help='Kategori Produk yang dapat menggunakan voucher ini') description = fields.Text(string='Description') discount_amount = fields.Float(string='Discount Amount') - discount_type = fields.Selection(string='Discount Type', - selection=[ - ('percentage', 'Percentage'), - ('fixed_price', 'Fixed Price'), - ], - help='Select the type of discount:\n' - '- Percentage: Persentase dari total harga.\n' - '- Fixed Price: Jumlah tetap yang dikurangi dari harga total.' - ) - visibility = fields.Selection(string='Visibility', - selection=[ - ('public', 'Public'), - ('private', 'Private') - ], - help='Select the visibility:\n' - '- Public: Ditampilkan kepada seluruh pengguna.\n' - '- Private: Tidak ditampilkan kepada seluruh pengguna.' - ) + discount_type = fields.Selection(string='Discount Type', + selection=[ + ('percentage', 'Percentage'), + ('fixed_price', 'Fixed Price'), + ], + help='Select the type of discount:\n' + '- Percentage: Persentase dari total harga.\n' + '- Fixed Price: Jumlah tetap yang dikurangi dari harga total.' + ) + visibility = fields.Selection(string='Visibility', + selection=[ + ('public', 'Public'), + ('private', 'Private') + ], + help='Select the visibility:\n' + '- Public: Ditampilkan kepada seluruh pengguna.\n' + '- Private: Tidak ditampilkan kepada seluruh pengguna.' + ) start_time = fields.Datetime(string='Start Time') end_time = fields.Datetime(string='End Time') - min_purchase_amount = fields.Integer(string='Min. Purchase Amount', help='Nominal minimum untuk dapat menggunakan voucher. Isi 0 jika tidak ada minimum purchase amount') + min_purchase_amount = fields.Integer(string='Min. Purchase Amount', + help='Nominal minimum untuk dapat menggunakan voucher. Isi 0 jika tidak ada minimum purchase amount') max_discount_amount = fields.Integer(string='Max. Discount Amount', help='Max nominal terhadap persentase diskon') order_ids = fields.One2many('sale.order', 'applied_voucher_id', string='Order') limit = fields.Integer( - string='Limit', + string='Limit', default=0, help='Batas penggunaan voucher keseluruhan. Isi dengan angka 0 untuk penggunaan tanpa batas' ) limit_user = fields.Integer( - string='Limit User', + string='Limit User', default=0, help='Batas penggunaan voucher per pengguna. Misalnya, jika diisi dengan angka 1, maka setiap pengguna hanya dapat menggunakan voucher ini satu kali. Isi dengan angka 0 untuk penggunaan tanpa batas' ) manufacture_ids = fields.Many2many('x_manufactures', string='Brands', help='Voucher appplied only for brand') - excl_pricelist_ids = fields.Many2many('product.pricelist', string='Excluded Pricelists', help='Hide voucher from selected exclude pricelist') + excl_pricelist_ids = fields.Many2many('product.pricelist', string='Excluded Pricelists', + help='Hide voucher from selected exclude pricelist') voucher_line = fields.One2many('voucher.line', 'voucher_id', 'Voucher Line') terms_conditions = fields.Html('Terms and Conditions') apply_type = fields.Selection(string='Apply Type', default="all", selection=[ @@ -59,13 +63,45 @@ class Voucher(models.Model): show_on_email = fields.Selection([ ('user_activation', 'User Activation') ], 'Show on Email') + account_type = fields.Selection(string='Account Type', default="all", selection=[ + ('all', "All Account"), + ('person', "Account Individu"), + ('company', "Account Company"), + ]) + + def is_voucher_applicable(self, product_id): + if not self.voucher_category: + return True + + public_categories = product_id.public_categ_ids + + return bool(set(public_categories.ids) & set(self.voucher_category.ids)) + + def is_voucher_applicable_for_category(self, category): + if not self.voucher_category: + return True + + if category.id in self.voucher_category.ids: + return True + + category_path = [] + current_cat = category + while current_cat: + category_path.append(current_cat.id) + current_cat = current_cat.parent_id + + for voucher_cat in self.voucher_category: + if voucher_cat.id in category_path: + return True + + return False @api.constrains('description') def _check_description_length(self): for record in self: if record.description and len(record.description) > 120: raise ValidationError('Deskripsi tidak boleh lebih dari 120 karakter') - + @api.constrains('limit', 'limit_user') def _check_limit(self): for rec in self: @@ -83,7 +119,7 @@ class Voucher(models.Model): def res_format(self): datas = [voucher.format() for voucher in self] return datas - + def format(self): ir_attachment = self.env['ir.attachment'] data = { @@ -96,7 +132,7 @@ class Voucher(models.Model): 'remaining_time': self._res_remaining_time(), } return data - + def _res_remaining_time(self): seconds = self._get_remaining_time() remaining_time = timedelta(seconds=seconds) @@ -112,14 +148,31 @@ class Voucher(models.Model): time = minutes unit = 'menit' return f'{time} {unit}' - + def _get_remaining_time(self): calculate_time = self.end_time - datetime.now() return round(calculate_time.total_seconds()) - + def filter_order_line(self, order_line): + # import logging + # _logger = logging.getLogger(__name__) + voucher_manufacture_ids = self.collect_manufacture_ids() results = [] + + if self.voucher_category and len(order_line) > 0: + for line in order_line: + category_applicable = False + for category in line['product_id'].public_categ_ids: + if self.is_voucher_applicable_for_category(category): + category_applicable = True + break + + if not category_applicable: + # _logger.info("Cart contains product %s with non-applicable category - voucher %s cannot be used", + # line['product_id'].name, self.code) + return [] + for line in order_line: manufacture_id = line['product_id'].x_manufacture.id or None if self.apply_type == 'brand' and manufacture_id not in voucher_manufacture_ids: @@ -128,35 +181,36 @@ class Voucher(models.Model): product_flashsale = line['product_id']._get_active_flash_sale() if len(product_flashsale) > 0: continue - + results.append(line) - + return results - + def calc_total_order_line(self, order_line): - result = { 'all': 0, 'brand': {} } + result = {'all': 0, 'brand': {}} for line in order_line: manufacture_id = line['product_id'].x_manufacture.id or None manufacture_total = result['brand'].get(manufacture_id, 0) result['brand'][manufacture_id] = manufacture_total + line['subtotal'] result['all'] += line['subtotal'] - + return result - + def calc_discount_amount(self, total): - result = { 'all': 0, 'brand': {} } + result = {'all': 0, 'brand': {}} if self.apply_type in ['all', 'shipping']: if total['all'] < self.min_purchase_amount: return result - + if self.discount_type == 'percentage': decimal_discount = self.discount_amount / 100 discount_all = total['all'] * decimal_discount - result['all'] = min(discount_all, self.max_discount_amount) if self.max_discount_amount > 0 else discount_all + result['all'] = min(discount_all, + self.max_discount_amount) if self.max_discount_amount > 0 else discount_all else: result['all'] = min(self.discount_amount, total['all']) - + return result for line in self.voucher_line: @@ -169,95 +223,141 @@ class Voucher(models.Model): elif line.discount_type == 'percentage': decimal_discount = line.discount_amount / 100 discount_brand = total_brand * decimal_discount - discount_brand = min(discount_brand, line.max_discount_amount) if line.max_discount_amount > 0 else discount_brand + discount_brand = min(discount_brand, + line.max_discount_amount) if line.max_discount_amount > 0 else discount_brand else: discount_brand = min(line.discount_amount, total_brand) - + result['brand'][manufacture_id] = round(discount_brand, 2) result['all'] += discount_brand - + result['all'] = round(result['all'], 2) return result def apply(self, order_line): - order_line = self.filter_order_line(order_line) - amount_total = self.calc_total_order_line(order_line) + + filtered_order_line = self.filter_order_line(order_line) + + amount_total = self.calc_total_order_line(filtered_order_line) + discount = self.calc_discount_amount(amount_total) + return { 'discount': discount, 'total': amount_total, 'type': self.apply_type, - 'valid_order': order_line + 'valid_order': filtered_order_line, } - + def collect_manufacture_ids(self): return [x.manufacture_id.id for x in self.voucher_line] - + def calculate_discount(self, price): if price < self.min_purchase_amount: return 0 - + if self.discount_type == 'fixed_price': return self.discount_amount - + if self.discount_type == 'percentage': discount = price * self.discount_amount / 100 max_disc = self.max_discount_amount return discount if max_disc == 0 else min(discount, max_disc) - + return 0 - + def get_active_voucher(self, domain): current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') domain += [ ('start_time', '<=', current_time), ('end_time', '>=', current_time), ] - vouchers = self.search(domain, order='min_purchase_amount ASC') + vouchers = self.search(domain, order='min_purchase_amount ASC') return vouchers - + def generate_tnc(self): + def format_currency(amount): + formatted_number = '{:,.0f}'.format(amount).replace(',', '.') + return f'Rp{formatted_number}' + tnc = [] tnc.append('<ol>') - tnc.append('<li>Voucher hanya berlaku apabila pembelian Pengguna sudah memenuhi syarat dan ketentuan yang tertera pada voucher</li>') + tnc.append( + '<li>Voucher hanya berlaku apabila pembelian Pengguna sudah memenuhi syarat dan ketentuan yang tertera pada voucher</li>') tnc.append(f'<li>Voucher berlaku {self._res_remaining_time()} lagi</li>') tnc.append(f'<li>Voucher tidak bisa digunakan apabila terdapat produk flash sale</li>') - if len(self.voucher_line) > 0: - brand_names = ', '.join([x.manufacture_id.x_name or '' for x in self.voucher_line]) - tnc.append(f'<li>Voucher berlaku untuk produk dari brand {brand_names}</li>') - tnc.append(self.generate_detail_tnc()) + + if self.apply_type == 'brand': + tnc.append(f'<li>Voucher berlaku untuk produk dari brand terpilih</li>') + tnc.append( + f'<li>Nominal potongan produk yang bisa didapatkan hingga 10 Juta dengan minimum pembelian 10 Ribu.</li>') + elif self.apply_type == 'all' or self.apply_type == 'shipping': + if self.voucher_category: + category_names = ', '.join([cat.name for cat in self.voucher_category]) + tnc.append( + f'<li>Voucher hanya berlaku untuk produk dalam kategori {category_names} dan sub-kategorinya</li>') + tnc.append( + f'<li>Voucher tidak dapat digunakan jika ada produk di keranjang yang tidak termasuk dalam kategori tersebut</li>') + + if self.discount_type == 'percentage' and self.apply_type != 'brand': + tnc.append( + f'<li>Nominal potongan produk yang bisa didapatkan sebesar {self.max_discount_amount}% dengan minimum pembelian {self.min_purchase_amount}</li>') + elif self.discount_type == 'percentage' and self.apply_type != 'brand': + tnc.append( + f'<li>Nominal potongan produk yang bisa didapatkan sebesar {format_currency(self.discount_amount)} dengan minimum pembelian {format_currency(self.min_purchase_amount)}</li>') + tnc.append('</ol>') - - return ' '.join(tnc) - - def generate_detail_tnc(self): - def format_currency(amount): - formatted_number = '{:,.0f}'.format(amount).replace(',', '.') - return f'Rp{formatted_number}' - - tnc = [] - if self.apply_type == 'all': - tnc.append('<li>') - tnc.append('Nominal potongan yang bisa didapatkan sebesar') - tnc.append(f'{self.discount_amount}%' if self.discount_type == 'percentage' else format_currency(self.discount_amount)) - - if self.discount_type == 'percentage' and self.max_discount_amount > 0: - tnc.append(f'hingga {format_currency(self.max_discount_amount)}') - - tnc.append(f'dengan minimum pembelian {format_currency(self.min_purchase_amount)}' if self.min_purchase_amount > 0 else 'tanpa minimum pembelian') - tnc.append('</li>') - else: - for line in self.voucher_line: - line_tnc = [] - line_tnc.append(f'Nominal potongan produk {line.manufacture_id.x_name} yang bisa didapatkan sebesar') - line_tnc.append(f'{line.discount_amount}%' if line.discount_type == 'percentage' else format_currency(line.discount_amount)) - - if line.discount_type == 'percentage' and line.max_discount_amount > 0: - line_tnc.append(f'hingga {format_currency(line.max_discount_amount)}') - - line_tnc.append(f'dengan minimum pembelian {format_currency(line.min_purchase_amount)}' if line.min_purchase_amount > 0 else 'tanpa minimum pembelian') - line_tnc = ' '.join(line_tnc) - tnc.append(f'<li>{line_tnc}</li>') + # tnc.append(self.generate_detail_tnc()) return ' '.join(tnc) + # def generate_detail_tnc(self): + # def format_currency(amount): + # formatted_number = '{:,.0f}'.format(amount).replace(',', '.') + # return f'Rp{formatted_number}' + # + # tnc = [] + # if self.apply_type == 'all': + # tnc.append('<li>') + # tnc.append('Nominal potongan yang bisa didapatkan sebesar') + # tnc.append(f'{self.discount_amount}%' if self.discount_type == 'percentage' else format_currency( + # self.discount_amount)) + # + # if self.discount_type == 'percentage' and self.max_discount_amount > 0: + # tnc.append(f'hingga {format_currency(self.max_discount_amount)}') + # + # tnc.append( + # f'dengan minimum pembelian {format_currency(self.min_purchase_amount)}' if self.min_purchase_amount > 0 else 'tanpa minimum pembelian') + # tnc.append('</li>') + # else: + # for line in self.voucher_line: + # line_tnc = [] + # line_tnc.append(f'Nominal potongan produk {line.manufacture_id.x_name} yang bisa didapatkan sebesar') + # line_tnc.append(f'{line.discount_amount}%' if line.discount_type == 'percentage' else format_currency( + # line.discount_amount)) + # + # if line.discount_type == 'percentage' and line.max_discount_amount > 0: + # line_tnc.append(f'hingga {format_currency(line.max_discount_amount)}') + # + # line_tnc.append( + # f'dengan minimum pembelian {format_currency(line.min_purchase_amount)}' if line.min_purchase_amount > 0 else 'tanpa minimum pembelian') + # line_tnc = ' '.join(line_tnc) + # tnc.append(f'<li>{line_tnc}</li>') + # return ' '.join(tnc) + + # copy semua data kalau diduplicate + def copy(self, default=None): + default = dict(default or {}) + voucher_lines = [] + + for line in self.voucher_line: + voucher_lines.append((0, 0, { + 'manufacture_id': line.manufacture_id.id, + 'discount_amount': line.discount_amount, + 'discount_type': line.discount_type, + 'min_purchase_amount': line.min_purchase_amount, + 'max_discount_amount': line.max_discount_amount, + })) + + default['voucher_line'] = voucher_lines + return super(Voucher, self).copy(default) diff --git a/indoteknik_custom/models/website_user_cart.py b/indoteknik_custom/models/website_user_cart.py index 44393cf1..a6d08949 100644 --- a/indoteknik_custom/models/website_user_cart.py +++ b/indoteknik_custom/models/website_user_cart.py @@ -1,10 +1,11 @@ from odoo import fields, models, api from datetime import datetime, timedelta + class WebsiteUserCart(models.Model): _name = 'website.user.cart' _rec_name = 'user_id' - + user_id = fields.Many2one('res.users', string='User') product_id = fields.Many2one('product.product', string='Product') program_line_id = fields.Many2one('promotion.program.line', string='Program', help="Apply program") @@ -18,7 +19,8 @@ class WebsiteUserCart(models.Model): is_reminder = fields.Boolean(string='Reminder?') phone_user = fields.Char(string='Phone', related='user_id.mobile') price = fields.Float(string='Price', compute='_compute_price') - program_product_id = fields.Many2one('product.product', string='Program Products', compute='_compute_program_product_ids') + program_product_id = fields.Many2one('product.product', string='Program Products', + compute='_compute_program_product_ids') @api.depends('program_line_id') def _compute_program_product_ids(self): @@ -55,6 +57,12 @@ class WebsiteUserCart(models.Model): product = self.product_id.v2_api_single_response(self.product_id) res.update(product) + # Add category information + res['categories'] = [{ + 'id': cat.id, + 'name': cat.name + } for cat in self.product_id.public_categ_ids] + # Check if the product's inventory location is in ID 57 or 83 target_locations = [57, 83] stock_quant = self.env['stock.quant'].search([ @@ -90,7 +98,14 @@ class WebsiteUserCart(models.Model): def get_products(self): products = [x.get_product() for x in self] - + + for i, cart_item in enumerate(self): + if cart_item.product_id and i < len(products): + products[i]['categories'] = [{ + 'id': cat.id, + 'name': cat.name + } for cat in cart_item.product_id.public_categ_ids] + return products def get_product_by_user(self, user_id, selected=False, source=False): @@ -121,10 +136,10 @@ class WebsiteUserCart(models.Model): products = products_active.get_products() return products - + def get_user_checkout(self, user_id, voucher=False, voucher_shipping=False, source=False): products = self.get_product_by_user(user_id=user_id, selected=True, source=source) - + total_purchase = 0 total_discount = 0 for product in products: @@ -132,9 +147,9 @@ class WebsiteUserCart(models.Model): price = product['package_price'] * product['quantity'] else: price = product['price']['price'] * product['quantity'] - + discount_price = price - product['price']['price_discount'] * product['quantity'] - + total_purchase += price total_discount += discount_price @@ -142,7 +157,7 @@ class WebsiteUserCart(models.Model): discount_voucher = 0 discount_voucher_shipping = 0 order_line = [] - + if voucher or voucher_shipping: for product in products: if product['cart_type'] == 'promotion': continue @@ -153,16 +168,16 @@ class WebsiteUserCart(models.Model): 'qty': product['quantity'], 'subtotal': product['subtotal'] }) - + if voucher: voucher_info = voucher.apply(order_line) discount_voucher = voucher_info['discount']['all'] subtotal -= discount_voucher - + if voucher_shipping: voucher_shipping_info = voucher_shipping.apply(order_line) - discount_voucher_shipping = voucher_shipping_info['discount']['all'] - + discount_voucher_shipping = voucher_shipping_info['discount']['all'] + tax = round(subtotal * 0.11) grand_total = subtotal + tax total_weight = sum(x['weight'] * x['quantity'] for x in products) @@ -179,28 +194,31 @@ class WebsiteUserCart(models.Model): 'kg': total_weight, 'g': total_weight * 1000 }, - 'has_product_without_weight': any(not product.get('weight') or product.get('weight') == 0 for product in products), + 'has_product_without_weight': any( + not product.get('weight') or product.get('weight') == 0 for product in products), 'products': products } return result - + def action_mail_reminder_to_checkout(self, limit=200): user_ids = self.search([('is_reminder', '=', False)]).mapped('user_id')[:limit] - + for user in user_ids: latest_cart = self.search([('user_id', '=', user.id)], order='create_date desc', limit=1) - + carts_to_remind = self.search([('user_id', '=', user.id)]) - + if latest_cart and not latest_cart.is_reminder: for cart in carts_to_remind: check = cart.check_product_flashsale(cart.product_id.id) - if not cart.program_line_id and cart.product_id.default_code and not 'BOM' in cart.product_id.default_code and check['is_flashsale'] == False: + if not cart.program_line_id and cart.product_id.default_code and not 'BOM' in cart.product_id.default_code and \ + check['is_flashsale'] == False: cart.is_selected = True - if cart.program_line_id or check['is_flashsale'] or cart.product_id.default_code and 'BOM' in cart.product_id.default_code: + if cart.program_line_id or check[ + 'is_flashsale'] or cart.product_id.default_code and 'BOM' in cart.product_id.default_code: cart.is_selected = False cart.is_reminder = True - + template = self.env.ref('indoteknik_custom.mail_template_user_cart_reminder_to_checkout') template.send_mail(latest_cart.id, force_send=True) @@ -234,8 +252,9 @@ class WebsiteUserCart(models.Model): break product_discount = subtotal_promo if cart.program_line_id or check['is_flashsale'] else subtotal - total_discount += product_discount - if check['is_flashsale'] == False and cart.product_id.default_code and not 'BOM' in cart.product_id.default_code: + total_discount += product_discount + if check[ + 'is_flashsale'] == False and cart.product_id.default_code and not 'BOM' in cart.product_id.default_code: voucher_product = subtotal * (discount_amount / 100.0) total_voucher += voucher_product @@ -253,14 +272,15 @@ class WebsiteUserCart(models.Model): def check_product_flashsale(self, product_id): product = product_id current_time = datetime.utcnow() - found_product = self.env['product.pricelist.item'].search([('product_id', '=', product_id), ('pricelist_id.is_flash_sale', '=', True)]) + found_product = self.env['product.pricelist.item'].search( + [('product_id', '=', product_id), ('pricelist_id.is_flash_sale', '=', True)]) if found_product: for found in found_product: pricelist_flashsale = found.pricelist_id if pricelist_flashsale.start_date <= current_time <= pricelist_flashsale.end_date: - return { + return { 'is_flashsale': True, 'price': found.fixed_price } @@ -269,10 +289,9 @@ class WebsiteUserCart(models.Model): 'is_flashsale': False } - return { + return { 'is_flashsale': False } - # if found_product: # start_date = found_product.pricelist_id.start_date @@ -291,26 +310,26 @@ class WebsiteUserCart(models.Model): # return { # 'is_flashsale': False # } - + def get_data_promo(self, program_line_id): program_line_product = self.env['promotion.product'].search([ ('program_line_id', '=', program_line_id) - ]) - + ]) + program_free_product = self.env['promotion.free_product'].search([ ('program_line_id', '=', program_line_id) - ]) + ]) return program_line_product, program_free_product - + def get_weight_product(self, program_line_id): program_line_product = self.env['promotion.product'].search([ ('program_line_id', '=', program_line_id) - ]) - + ]) + program_free_product = self.env['promotion.free_product'].search([ ('program_line_id', '=', program_line_id) - ]) - + ]) + real_weight = 0.0 if program_line_product: for product in program_line_product: @@ -321,16 +340,16 @@ class WebsiteUserCart(models.Model): real_weight += product.product_id.weight return real_weight - + def get_price_coret(self, program_line_id): program_line_product = self.env['promotion.product'].search([ ('program_line_id', '=', program_line_id) - ]) - + ]) + program_free_product = self.env['promotion.free_product'].search([ ('program_line_id', '=', program_line_id) - ]) - + ]) + price_coret = 0.0 for product in program_line_product: price = self.get_price_website(product.product_id.id) @@ -340,20 +359,22 @@ class WebsiteUserCart(models.Model): price = self.get_price_website(product.product_id.id) price_coret += price['price'] * product.qty - return price_coret - + return price_coret + def get_price_website(self, product_id): - price_website = self.env['product.pricelist.item'].search([('product_id', '=', product_id), ('pricelist_id', '=', 17022)], limit=1) - - price_tier = self.env['product.pricelist.item'].search([('product_id', '=', product_id), ('pricelist_id', '=', 17023)], limit=1) - + price_website = self.env['product.pricelist.item'].search( + [('product_id', '=', product_id), ('pricelist_id', '=', 17022)], limit=1) + + price_tier = self.env['product.pricelist.item'].search( + [('product_id', '=', product_id), ('pricelist_id', '=', 17023)], limit=1) + fixed_price = price_website.fixed_price if price_website else 0.0 discount = price_tier.price_discount if price_tier else 0.0 - + discounted_price = fixed_price - (fixed_price * discount / 100) - + final_price = discounted_price / 1.11 - + return { 'price': final_price, 'web_price': discounted_price @@ -365,4 +386,4 @@ class WebsiteUserCart(models.Model): def format_currency(self, number): number = int(number) - return "{:,}".format(number).replace(',', '.')
\ No newline at end of file + return "{:,}".format(number).replace(',', '.') diff --git a/indoteknik_custom/models/x_banner_banner.py b/indoteknik_custom/models/x_banner_banner.py index 810bdf39..16d54b02 100755 --- a/indoteknik_custom/models/x_banner_banner.py +++ b/indoteknik_custom/models/x_banner_banner.py @@ -25,4 +25,5 @@ class XBannerBanner(models.Model): ('4', '4') ], string='Group by Week') x_headline_banner = fields.Text(string="Headline Banner") - x_description_banner = fields.Text(string="Description Banner")
\ No newline at end of file + x_description_banner = fields.Text(string="Description Banner") + x_keyword_banner = fields.Text(string="Keyword Banner")
\ No newline at end of file diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index 036e28fc..28fc760d 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -105,6 +105,7 @@ access_purchase_order_multi_update,access.purchase.order.multi_update,model_purc access_invoice_reklas_penjualan,access.invoice.reklas.penjualan,model_invoice_reklas_penjualan,,1,1,1,1 access_invoice_reklas_penjualan_line,access.invoice.reklas.penjualan.line,model_invoice_reklas_penjualan_line,,1,1,1,1 access_purchase_order_multi_confirm,access.purchase.order.multi_confirm,model_purchase_order_multi_confirm,,1,1,1,1 +access_purchase_order_multi_ask_approval,access.purchase.order.multi_ask_approval,model_purchase_order_multi_ask_approval,,1,1,1,1 access_po_multi_cancel,access.po.multi.cancel,model_po_multi_cancel,,1,1,1,1 access_logbook_sj,access.logbook.sj,model_logbook_sj,,1,1,1,1 access_logbook_sj_line,access.logbook.sj.line,model_logbook_sj_line,,1,1,1,1 @@ -137,6 +138,7 @@ access_shipment_group,access.shipment.group,model_shipment_group,,1,1,1,1 access_shipment_group_line,access.shipment.group.line,model_shipment_group_line,,1,1,1,1 access_sales_order_reject,access.sales.order.reject,model_sales_order_reject,,1,1,1,1 access_approval_date_doc,access.approval.date.doc,model_approval_date_doc,,1,1,1,1 +access_approval_invoice_date,access.approval.invoice.date,model_approval_invoice_date,,1,1,1,1 access_account_tax,access.account.tax,model_account_tax,,1,1,1,1 access_approval_unreserve,access.approval.unreserve,model_approval_unreserve,,1,1,1,1 access_approval_unreserve_line,access.approval.unreserve.line,model_approval_unreserve_line,,1,1,1,1 @@ -150,9 +152,18 @@ access_sales_order_fulfillment_v2,access.sales.order.fulfillment.v2,model_sales_ access_v_move_outstanding,access.v.move.outstanding,model_v_move_outstanding,,1,1,1,1 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 access_stock_immediate_transfer,access.stock.immediate.transfer,model_stock_immediate_transfer,,1,1,1,1 access_coretax_faktur,access.coretax.faktur,model_coretax_faktur,,1,1,1,1 access_purchase_order_unlock_wizard,access.purchase.order.unlock.wizard,model_purchase_order_unlock_wizard,,1,1,1,1 +access_sales_order_koli,access.sales.order.koli,model_sales_order_koli,,1,1,1,1 +access_stock_backorder_confirmation,access.stock.backorder.confirmation,model_stock_backorder_confirmation,,1,1,1,1 +access_warning_modal_wizard,access.warning.modal.wizard,model_warning_modal_wizard,,1,1,1,1 access_User_pengajuan_tempo_line,access.user.pengajuan.tempo.line,model_user_pengajuan_tempo_line,,1,1,1,1 access_user_pengajuan_tempo,access.user.pengajuan.tempo,model_user_pengajuan_tempo,,1,1,1,1 @@ -162,3 +173,15 @@ access_user_form_merchant,access.user.form.merchant,model_user_form_merchant,,1, access_user_merchant_request,access.user.merchant.request,model_user_merchant_request,,1,1,1,1 access_reject_reason_wizard_merchant,reject.reason.wizard.merchant,model_reject_reason_wizard_merchant,,1,1,1,0 access_confirm_approval_wizard_merchant,confirm.approval.wizard.merchant,model_confirm_approval_wizard_merchant,,1,1,1,0 +access_hr_public_holiday,confirm.hr.public.holiday,model_hr_public_holiday,,1,1,1,0 +access_barcode_product,access.barcode.product,model_barcode_product,,1,1,1,1 +access_barcoding_product,access.barcoding.product,model_barcoding_product,,1,1,1,1 +access_barcoding_product_line,access.barcoding.product.line,model_barcoding_product_line,,1,1,1,1 +access_account_payment_register,access.account.payment.register,model_account_payment_register,,1,1,1,1 +access_stock_inventory,access.stock.inventory,model_stock_inventory,,1,1,1,1 +access_cancel_reason_order,cancel.reason.order,model_cancel_reason_order,,1,1,1,0 +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 +access_v_sale_notin_matchpo,access.v.sale.notin.matchpo,model_v_sale_notin_matchpo,,1,1,1,1 diff --git a/indoteknik_custom/views/account_move.xml b/indoteknik_custom/views/account_move.xml index 36b292e8..46737a40 100644 --- a/indoteknik_custom/views/account_move.xml +++ b/indoteknik_custom/views/account_move.xml @@ -27,6 +27,7 @@ </field> <field name="invoice_date" position="after"> <field name="sale_id" readonly="1" attrs="{'invisible': [('move_type', '!=', 'out_invoice')]}"/> + <field name="purchase_order_id" readonly="1" attrs="{'invisible': [('move_type', '!=', 'in_invoice')]}"/> </field> <field name="ref" position="after"> <field name="sale_id" readonly="1" attrs="{'invisible': [('move_type', '!=', 'entry')]}"/> @@ -91,6 +92,7 @@ <field name="is_efaktur_exported" optional="hide"/> <field name="invoice_day_to_due" attrs="{'invisible': [['payment_state', 'in', ('paid', 'in_payment', 'reversed')]]}" optional="hide"/> <field name="new_invoice_day_to_due" attrs="{'invisible': [['payment_state', 'in', ('paid', 'in_payment', 'reversed')]]}" optional="hide"/> + <field name="length_of_payment" optional="hide"/> <field name="mark_upload_efaktur" optional="hide" widget="badge" decoration-danger="mark_upload_efaktur == 'belum_upload'" decoration-success="mark_upload_efaktur == 'sudah_upload'" /> @@ -137,6 +139,9 @@ {'column_invisible': [('parent.move_type', '!=', 'in_invoice')]} </attribute> </field> + <field name="product_id" position="before"> + <field name="line_no" optional="hide"/> + </field> <field name="invoice_incoterm_id" position="after"> <field name="date_terima_tukar_faktur"/> </field> diff --git a/indoteknik_custom/views/approval_invoice_date.xml b/indoteknik_custom/views/approval_invoice_date.xml new file mode 100644 index 00000000..31f346e7 --- /dev/null +++ b/indoteknik_custom/views/approval_invoice_date.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<odoo> + <record id="approval_invoice_date_tree" model="ir.ui.view"> + <field name="name">approval.invoice.date.tree</field> + <field name="model">approval.invoice.date</field> + <field name="arch" type="xml"> + <tree> + <field name="number"/> + <field name="picking_id"/> + <field name="partner_id"/> + <field name="sale_id"/> + <field name="date_doc_do"/> + <field name="date_invoice"/> + <field name="state" widget="badge" decoration-danger="state == 'cancel'" + decoration-success="state == 'done'" + decoration-info="state == 'draft'"/> + <field name="approve_date"/> + <field name="approve_by"/> + <field name="create_uid"/> + </tree> + </field> + </record> + + <record id="approval_invoice_date_form" model="ir.ui.view"> + <field name="name">approval.invoice.date.form</field> + <field name="model">approval.invoice.date</field> + <field name="arch" type="xml"> + <form> + <header> + <button name="button_approve" + string="Approve" + type="object" + attrs="{'invisible': [('state', '=', 'done')]}" + /> + <button name="button_cancel" + string="Cancel" + type="object" + attrs="{'invisible': [('state', '=', 'cancel')]}" + /> + <field name="state" widget="statusbar" + statusbar_visible="draft,cancel,done" + statusbar_colors='{"cancel":"red", "done":"green"}'/> + </header> + <sheet string="Approval Invoice Date"> + <group> + <group> + <field name="number"/> + <field name="picking_id"/> + <field name="partner_id"/> + <field name="sale_id"/> + <field name="move_id"/> + <field name="date_doc_do"/> + <field name="date_invoice"/> + <field name="approve_date"/> + <field name="approve_by"/> + <field name="create_uid"/> + <field name="note" attrs="{'invisible': [('state', '!=', 'cancel')]}"/> + </group> + </group> + </sheet> + </form> + </field> + </record> + + <record id="view_approval_invoice_date_filter" model="ir.ui.view"> + <field name="name">approval.invoice.date.list.select</field> + <field name="model">approval.invoice.date</field> + <field name="priority" eval="15"/> + <field name="arch" type="xml"> + <search string="Search Approval Invoice Date"> + <field name="number"/> + <field name="partner_id"/> + <field name="picking_id"/> + <field name="sale_id"/> + </search> + </field> + </record> + + <record id="approval_invoice_date_action" model="ir.actions.act_window"> + <field name="name">Approval Invoice Date</field> + <field name="type">ir.actions.act_window</field> + <field name="res_model">approval.invoice.date</field> + <field name="view_mode">tree,form</field> + </record> + + <menuitem id="menu_approval_invoice_date" name="Approval Invoice Date" + parent="account.menu_finance_receivables" + action="approval_invoice_date_action" + sequence="100" + /> + +</odoo>
\ No newline at end of file diff --git a/indoteknik_custom/views/barcoding_product.xml b/indoteknik_custom/views/barcoding_product.xml new file mode 100644 index 00000000..55876580 --- /dev/null +++ b/indoteknik_custom/views/barcoding_product.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="UTF-8"?> +<odoo> + <data> + <record id="barcoding_product_tree" model="ir.ui.view"> + <field name="name">barcoding.product.tree</field> + <field name="model">barcoding.product</field> + <field name="arch" type="xml"> + <tree default_order="create_date desc"> + <field name="product_id"/> + <field name="quantity"/> + <field name="type"/> + </tree> + </field> + </record> + + <record id="barcoding_product_line_tree" model="ir.ui.view"> + <field name="name">barcoding.product.line.tree</field> + <field name="model">barcoding.product.line</field> + <field name="arch" type="xml"> + <tree> + <field name="product_id"/> + <field name="qr_code_variant" widget="image"/> + </tree> + </field> + </record> + + <record id="barcoding_product_form" model="ir.ui.view"> + <field name="name">barcoding.product.form</field> + <field name="model">barcoding.product</field> + <field name="arch" type="xml"> + <form > + <sheet> + <group> + <group> + <field name="product_id" required="1"/> + <field name="type" required="1"/> + <field name="quantity" attrs="{'invisible': [['type', 'in', ('barcoding','barcoding_box')]], 'required': [['type', 'not in', ('barcoding')]]}"/> + <field name="barcode" attrs="{'invisible': [['type', 'in', ('print')]], 'required': [['type', 'not in', ('print')]]}"/> + <field name="qty_pcs_box" attrs="{'invisible': [['type', 'in', ('print','barcoding')]], 'required': [['type', 'not in', ('print', 'barcoding')]]}"/> + </group> + </group> + <notebook> + <page string="Line" attrs="{'invisible': [['type', 'in', ('barcoding','barcoding_box')]]}"> + <field name="barcoding_product_line"/> + </page> + </notebook> + </sheet> + </form> + </field> + </record> + + <record id="barcoding_product_view_search" model="ir.ui.view"> + <field name="name">barcoding.product.search.view</field> + <field name="model">barcoding.product</field> + <field name="arch" type="xml"> + <search string="Search Barcoding Product"> + <field name="product_id"/> + </search> + </field> + </record> + + <record id="barcoding_product_action" model="ir.actions.act_window"> + <field name="name">Barcoding Product</field> + <field name="type">ir.actions.act_window</field> + <field name="res_model">barcoding.product</field> + <field name="view_mode">tree,form</field> + </record> + + <menuitem + id="menu_barcoding_product" + name="Barcoding Product" + parent="stock.menu_stock_warehouse_mgmt" + sequence="4" + action="barcoding_product_action" + /> + </data> +</odoo> diff --git a/indoteknik_custom/views/customer_commision.xml b/indoteknik_custom/views/customer_commision.xml index 0b72587e..37df16ff 100644 --- a/indoteknik_custom/views/customer_commision.xml +++ b/indoteknik_custom/views/customer_commision.xml @@ -11,8 +11,15 @@ <field name="partner_ids" widget="many2many_tags"/> <field name="commision_percent"/> <field name="commision_amt" readonly="1"/> - <field name="status" readonly="1"/> + <field name="status" readonly="1" decoration-success="status == 'approved'" widget="badge" + optional="show"/> + <field name="payment_status" readonly="1" + decoration-success="payment_status == 'payment'" + decoration-danger="payment_status == 'pending'" + widget="badge"/> <field name="brand_ids" widget="many2many_tags"/> + <field name="grouped_so_number" readonly="1" optional="hide"/> + <field name="grouped_invoice_number" readonly="1" optional="hide"/> </tree> </field> </record> @@ -24,10 +31,12 @@ <tree editable="top" create="false"> <field name="partner_id" readonly="1"/> <field name="invoice_id" readonly="1"/> + <field name="sale_order_id" readonly="1"/> <field name="state" readonly="1"/> <field name="product_id" readonly="1" optional="hide"/> <field name="dpp" readonly="1"/> <field name="total_percent_margin" readonly="1"/> + <field name="total_margin_excl_third_party" readonly="1"/> <field name="tax" readonly="1" optional="hide"/> <field name="total" readonly="1" optional="hide"/> </tree> @@ -39,60 +48,112 @@ <field name="model">customer.commision</field> <field name="arch" type="xml"> <form> + <!-- attrs="{'invisible': [('status', 'in', ['draft','pengajuan1','pengajuan2','pengajuan3','pengajuan4'])]}"--> <header> - <button name="action_confirm_customer_commision" - string="Confirm" type="object" - options="{}"/> + <button name="action_confirm_customer_commision" + string="Confirm" type="object" + attrs="{'invisible': [('status', 'in', ['approved','reject'])]}" + options="{}"/> + <button name="action_reject" + string="Reject" + attrs="{'invisible': [('status', 'in', ['approved','reject'])]}" + type="object"/> + <button name="button_draft" + string="Reset to Draft" + attrs="{'invisible': [('status', '!=', 'reject')]}" + type="object"/> + <button name="action_confirm_customer_payment" + string="Konfirmasi Pembayaran" type="object" + options="{}" + attrs="{'invisible': [('payment_status', '==', 'payment')], 'readonly': [('payment_status', '=', 'payment')]}"/> + <field name="status" widget="statusbar" + statusbar_visible="draft,pengajuan1,pengajuan2,pengajuan3,pengajuan4,approved" + statusbar_colors='{"reject":"red"}'/> </header> <sheet string="Customer Commision"> - <div class="oe_button_box" name="button_box"/> + <div class="oe_button_box" name="button_box"/> + <group> <group> + <field name="number"/> + <field name="date_from"/> + <field name="partner_ids" widget="many2many_tags"/> + <field name="description"/> + <field name="commision_percent"/> + <field name="commision_amt"/> + <field name="commision_amt_text"/> + <field name="grouped_so_number" readonly="1"/> + <field name="grouped_invoice_number" readonly="1"/> + <field name="approved_by" readonly="1"/> + </group> + <group> + <div> + <button name="generate_customer_commision" + string="Generate Line" + type="object" + class="mr-2 oe_highlight" + /> + </div> + <field name="date_to"/> + <field name="sales_id"/> + <field name="commision_type"/> + <field name="brand_ids" widget="many2many_tags"/> + <field name="notification" readonly="1"/> + <!-- <field name="status" readonly="1"/>--> + <field name="payment_status" readonly="1"/> + <field name="total_dpp"/> + </group> + </group> + <notebook> + <page string="Lines"> + <field name="commision_lines"/> + </page> + <page string="Other Info" name="customer_commision_info"> <group> - <field name="number"/> - <field name="date_from"/> - <field name="partner_ids" widget="many2many_tags"/> - <field name="description"/> - <field name="commision_percent"/> - <field name="commision_amt"/> + <field name="bank_name"/> + <field name="account_name"/> + <field name="bank_account"/> + <field name="note_transfer"/> </group> + </page> + <page string="Finance Notes"> <group> - <div> - <button name="generate_customer_commision" - string="Generate Line" - type="object" - class="mr-2 oe_highlight" - /> - </div> - <field name="date_to"/> - <field name="commision_type"/> - <field name="brand_ids" widget="many2many_tags"/> - <field name="notification" readonly="1"/> - <field name="status" readonly="1"/> - <field name="total_dpp"/> + <field name="note_finnance"/> </group> - </group> - <notebook> - <page string="Lines"> - <field name="commision_lines"/> - </page> - <page string="Other Info" name="customer_commision_info"> - <group> - <field name="bank_name"/> - <field name="account_name"/> - <field name="bank_account"/> - <field name="note_transfer"/> - </group> - </page> - </notebook> - </sheet> - <div class="oe_chatter"> - <field name="message_follower_ids" widget="mail_followers"/> - <field name="message_ids" widget="mail_thread"/> - </div> + </page> + </notebook> + </sheet> + <div class="oe_chatter"> + <field name="message_follower_ids" widget="mail_followers"/> + <field name="message_ids" widget="mail_thread"/> + </div> </form> </field> </record> + <!-- Wizard for Reject Reason --> + <record id="view_reject_reason_wizard_form" model="ir.ui.view"> + <field name="name">reject.reason.commision.form</field> + <field name="model">reject.reason.commision</field> + <field name="arch" type="xml"> + <form string="Reject Reason"> + <group> + <field name="reason_reject" widget="text"/> + </group> + <footer> + <button string="Confirm" type="object" name="confirm_reject" class="btn-primary"/> + <button string="Cancel" class="btn-secondary" special="cancel"/> + </footer> + </form> + </field> + </record> + + <record id="action_reject_reason_wizard" model="ir.actions.act_window"> + <field name="name">Reject Reason</field> + <field name="res_model">reject.reason.commision</field> + <field name="view_mode">form</field> + <field name="target">new</field> + </record> + <record id="view_customer_commision_filter" model="ir.ui.view"> <field name="name">customer.commision.list.select</field> <field name="model">customer.commision</field> @@ -100,7 +161,12 @@ <field name="arch" type="xml"> <search string="Search Customer Commision"> <field name="partner_ids"/> - </search> + <group expand="0" string="Group By"> + <filter string="Partner" name="group_partner" + domain="[]" + context="{'group_by':'partner_ids'}"/> + </group> + </search> </field> </record> @@ -113,17 +179,17 @@ </record> <menuitem id="menu_customer_commision_acct" - name="Customer Commision" - action="customer_commision_action" - parent="account.menu_finance_entries" - sequence="113" + name="Customer Commision" + action="customer_commision_action" + parent="account.menu_finance_entries" + sequence="113" /> <menuitem id="menu_customer_commision_sales" - name="Customer Commision" - action="customer_commision_action" - parent="sale.product_menu_catalog" - sequence="101" + name="Customer Commision" + action="customer_commision_action" + parent="sale.product_menu_catalog" + sequence="101" /> <record id="customer_rebate_tree" model="ir.ui.view"> @@ -157,34 +223,34 @@ <field name="arch" type="xml"> <form> <sheet string="Customer Rebate"> - <div class="oe_button_box" name="button_box"/> + <div class="oe_button_box" name="button_box"/> + <group> <group> - <group> - <field name="date_from"/> - <field name="partner_id"/> - <field name="target_1st"/> - <field name="target_2nd"/> - <field name="dpp_q1"/> - <field name="dpp_q2"/> - <field name="dpp_q3"/> - <field name="dpp_q4"/> - </group> - <group> - <field name="date_to"/> - <field name="description"/> - <field name="achieve_1"/> - <field name="achieve_2"/> - <field name="status_q1"/> - <field name="status_q2"/> - <field name="status_q3"/> - <field name="status_q4"/> - </group> + <field name="date_from"/> + <field name="partner_id"/> + <field name="target_1st"/> + <field name="target_2nd"/> + <field name="dpp_q1"/> + <field name="dpp_q2"/> + <field name="dpp_q3"/> + <field name="dpp_q4"/> + </group> + <group> + <field name="date_to"/> + <field name="description"/> + <field name="achieve_1"/> + <field name="achieve_2"/> + <field name="status_q1"/> + <field name="status_q2"/> + <field name="status_q3"/> + <field name="status_q4"/> </group> - </sheet> - <div class="oe_chatter"> - <field name="message_follower_ids" widget="mail_followers"/> - <field name="message_ids" widget="mail_thread"/> - </div> + </group> + </sheet> + <div class="oe_chatter"> + <field name="message_follower_ids" widget="mail_followers"/> + <field name="message_ids" widget="mail_thread"/> + </div> </form> </field> </record> @@ -197,16 +263,16 @@ </record> <menuitem id="menu_customer_rebate_acct" - name="Customer Rebate" - action="customer_rebate_action" - parent="account.menu_finance_entries" - sequence="114" + name="Customer Rebate" + action="customer_rebate_action" + parent="account.menu_finance_entries" + sequence="114" /> <menuitem id="menu_customer_rebate_sales" - name="Customer Rebate" - action="customer_rebate_action" - parent="sale.product_menu_catalog" - sequence="102" + name="Customer Rebate" + action="customer_rebate_action" + parent="sale.product_menu_catalog" + sequence="102" /> </odoo>
\ No newline at end of file diff --git a/indoteknik_custom/views/ir_sequence.xml b/indoteknik_custom/views/ir_sequence.xml index dfb56100..97bf40bb 100644 --- a/indoteknik_custom/views/ir_sequence.xml +++ b/indoteknik_custom/views/ir_sequence.xml @@ -21,6 +21,16 @@ <field name="number_increment">1</field> </record> + <record id="sequence_invoice_date" model="ir.sequence"> + <field name="name">Approval Invoice Date</field> + <field name="code">approval.invoice.date</field> + <field name="active">TRUE</field> + <field name="prefix">AID/%(year)s/</field> + <field name="padding">5</field> + <field name="number_next">1</field> + <field name="number_increment">1</field> + </record> + <record id="sequence_vendor_approval" model="ir.sequence"> <field name="name">Vendor Approval</field> <field name="code">vendor.approval</field> @@ -55,7 +65,7 @@ <field name="name">Shipment Group</field> <field name="code">shipment.group</field> <field name="active">TRUE</field> - <field name="prefix">SG/%(year)s/</field> + <field name="prefix">SGR/%(year)s/</field> <field name="padding">5</field> <field name="number_next">1</field> <field name="number_increment">1</field> diff --git a/indoteknik_custom/views/mrp_production.xml b/indoteknik_custom/views/mrp_production.xml index f81d65e8..3de52a08 100644 --- a/indoteknik_custom/views/mrp_production.xml +++ b/indoteknik_custom/views/mrp_production.xml @@ -5,9 +5,38 @@ <field name="model">mrp.production</field> <field name="inherit_id" ref="mrp.mrp_production_form_view" /> <field name="arch" type="xml"> + <button name="button_mark_done" position="after"> + <button name="create_po_from_manufacturing" type="object" string="Create PO" class="oe_highlight" attrs="{'invisible': ['|', ('state', '!=', 'confirmed'), ('is_po', '=', True)]}"/> + </button> <field name="bom_id" position="after"> <field name="desc"/> + <field name="sale_order"/> + <field name="is_po"/> </field> + <xpath expr="//form/sheet/notebook/page/field[@name='move_raw_ids']/tree/field[@name='product_uom_qty']" position="before"> + <field name="vendor_id"/> + </xpath> + <xpath expr="//form/sheet/notebook/page[@name='miscellaneous']" position="after"> + <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> @@ -18,7 +47,24 @@ <field name="arch" type="xml"> <field name="product_id" position="after"> <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> + + <record id="production_purchase_match_tree" model="ir.ui.view"> + <field name="name">production.purchase.match.tree</field> + <field name="model">production.purchase.match</field> + <field name="arch" type="xml"> + <tree> + <field name="order_id" readonly="1"/> + <field name="vendor" readonly="1"/> + <field name="total" readonly="1"/> + </tree> + </field> + </record> </odoo> 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_product.xml b/indoteknik_custom/views/product_product.xml index 71748e44..b214dc87 100644 --- a/indoteknik_custom/views/product_product.xml +++ b/indoteknik_custom/views/product_product.xml @@ -31,6 +31,14 @@ <field name="code">model.action_sync_to_solr()</field> </record> + <record id="ir_actions_server_product_sla_generate" model="ir.actions.server"> + <field name="name">Generate Product SLA</field> + <field name="model_id" ref="product.model_product_product"/> + <field name="binding_model_id" ref="product.model_product_product"/> + <field name="state">code</field> + <field name="code">model.generate_product_sla()</field> + </record> + <data noupdate="1"> <record id="cron_variant_solr_flag_solr" model="ir.cron"> <field name="name">Sync Variant To Solr: Solr Flag 2</field> diff --git a/indoteknik_custom/views/product_sla.xml b/indoteknik_custom/views/product_sla.xml index 8b0e874b..9179730f 100644 --- a/indoteknik_custom/views/product_sla.xml +++ b/indoteknik_custom/views/product_sla.xml @@ -6,7 +6,9 @@ <field name="arch" type="xml"> <tree create="false"> <field name="product_variant_id"/> - <field name="avg_leadtime"/> + <field name="sla_vendor_id" string="Name Vendor"/> + <field name="sla_vendor_duration" string="SLA Vendor"/> + <field name="sla_logistic_duration_unit" string="SLA Logistic"/> <field name="sla"/> </tree> </field> @@ -21,7 +23,8 @@ <group> <group> <field name="product_variant_id"/> - <field name="avg_leadtime"/> + <field name="sla_logistic"/> + <field name="sla_logistic_unit"/> <field name="sla"/> <field name="version"/> </group> diff --git a/indoteknik_custom/views/product_template.xml b/indoteknik_custom/views/product_template.xml index af21984a..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> @@ -62,6 +77,8 @@ <field name="inherit_id" ref="product.product_normal_form_view"/> <field name="arch" type="xml"> <field name="last_update_solr" position="after"> + <field name="barcode_box" /> + <field name="qty_pcs_box" /> <field name="clean_website_description" /> <field name="qr_code_variant" widget="image" readonly="True"/> </field> diff --git a/indoteknik_custom/views/project_views.xml b/indoteknik_custom/views/project_views.xml new file mode 100644 index 00000000..3023fa18 --- /dev/null +++ b/indoteknik_custom/views/project_views.xml @@ -0,0 +1,13 @@ +<odoo> + <record id="view_task_kanban_inherit" model="ir.ui.view"> + <field name="name">project.task.kanban.inherit</field> + <field name="model">project.task</field> + <field name="inherit_id" ref="project.view_task_kanban"/> + <field name="arch" type="xml"> + <!-- Target field user_id di bagian kanban_bottom_right --> + <xpath expr="//div[@class='oe_kanban_bottom_right']/field[@name='user_id']" position="attributes"> + <attribute name="widget" remove="many2one_avatar_user" /> + </xpath> + </field> + </record> +</odoo>
\ No newline at end of file diff --git a/indoteknik_custom/views/public_holiday.xml b/indoteknik_custom/views/public_holiday.xml new file mode 100644 index 00000000..146c5b0b --- /dev/null +++ b/indoteknik_custom/views/public_holiday.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<odoo> + <data> + <!-- Security Access Rights --> + <record id="model_hr_public_holiday_access" model="ir.model.access"> + <field name="name">hr.public.holiday access</field> + <field name="model_id" ref="model_hr_public_holiday"/> + <field name="group_id" eval="False"/> + <field name="perm_read" eval="True"/> + <field name="perm_write" eval="True"/> + <field name="perm_create" eval="True"/> + <field name="perm_unlink" eval="True"/> + </record> + + <!-- Public Holiday Form View --> + <record id="view_hr_public_holiday_form" model="ir.ui.view"> + <field name="name">hr.public.holiday.form</field> + <field name="model">hr.public.holiday</field> + <field name="arch" type="xml"> + <form string="Public Holiday"> + <sheet> + <group> + <field name="name"/> + <field name="start_date"/> + </group> + </sheet> + </form> + </field> + </record> + + <record id="view_hr_public_holiday_tree" model="ir.ui.view"> + <field name="name">hr.public.holiday.tree</field> + <field name="model">hr.public.holiday</field> + <field name="arch" type="xml"> + <tree string="Public Holidays"> + <field name="name"/> + <field name="start_date"/> + </tree> + </field> + </record> + <record id="action_hr_public_holiday" model="ir.actions.act_window"> + <field name="name">Public Holidays</field> + <field name="res_model">hr.public.holiday</field> + <field name="view_mode">tree,form</field> + </record> + + <menuitem + id="hr_public_holiday" + name="Public Holiday" + parent="website_sale.menu_orders" + sequence="1" + action="action_hr_public_holiday" + /> + </data> +</odoo> diff --git a/indoteknik_custom/views/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml index 022937f4..b58139c6 100755 --- a/indoteknik_custom/views/purchase_order.xml +++ b/indoteknik_custom/views/purchase_order.xml @@ -51,6 +51,9 @@ <field name="approval_status" position="after"> <field name="approval_status_unlock" invisible="True"/> </field> + <field name="partner_id" position="after"> + <field name="purchase_order_count"/> + </field> <field name="incoterm_id" position="after"> <field name="amount_total_without_service"/> <field name="delivery_amt"/> @@ -62,6 +65,7 @@ <field name="payment_term_id"/> <field name="total_cost_service" attrs="{'required': [('partner_id', 'in', [9688, 29712])]}"/> <field name="total_delivery_amt" attrs="{'required': [('partner_id', 'in', [9688, 29712])]}"/> + <field name="product_bom_id"/> </field> <field name="amount_total" position="after"> <field name="total_margin"/> @@ -78,6 +82,9 @@ <field name="product_id" position="attributes"> <attribute name="options">{'no_create': True}</attribute> </field> + <field name="date_planned" position="attributes"> + <attribute name="invisible">1</attribute> + </field> <field name="product_qty" position="before"> <field name="is_edit_product_qty" readonly="1" optional="hide"/> <field name="qty_onhand" readonly="1" optional="hide"/> @@ -103,6 +110,10 @@ <field name="price_vendor" attrs="{'readonly': 1}" optional="hide"/> </field> <field name="price_subtotal" position="after"> + <field name="so_item_margin" attrs="{'readonly': 1}" optional="hide"/> + <field name="so_item_percent_margin" attrs="{'readonly': 1}" optional="hide"/> + <field name="item_margin" attrs="{'readonly': 1}" optional="hide"/> + <field name="item_percent_margin" attrs="{'readonly': 1}" optional="hide"/> <field name="so_line_id" attrs="{'readonly': 1}" optional="hide"/> <field name="so_id" attrs="{'readonly': 1}" optional="hide"/> <field name="indent" optional="hide"/> @@ -129,7 +140,7 @@ </field> <field name="order_line" position="attributes"> - <attribute name="attrs">{'readonly': ['|', ('state', 'in', ['done', 'cancel']), ('has_active_invoice', '=', True)]}</attribute> + <attribute name="attrs">{'readonly': ['|', ('state', 'in', ['purchase', 'done', 'cancel']), ('has_active_invoice', '=', True)]}</attribute> </field> <xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='price_unit']" position="attributes"> @@ -290,11 +301,14 @@ <field name="partner_invoice_id"/> <field name="salesperson_id"/> <field name="product_id"/> + <field name="purchase_price_so"/> + <field name="purchase_price_po"/> <field name="qty_so"/> <field name="qty_po"/> <field name="margin_item" optional="hide"/> <field name="delivery_amt" optional="hide"/> <field name="margin_deduct" optional="hide"/> + <field name="hold_outgoing_so" optional="hide"/> <field name="margin_so"/> </tree> </field> @@ -319,6 +333,15 @@ </record> </data> <data> + <record id="purchase_order_multi_ask_approval_ir_actions_server" model="ir.actions.server"> + <field name="name">Ask Approval PO</field> + <field name="model_id" ref="purchase.model_purchase_order"/> + <field name="binding_model_id" ref="purchase.model_purchase_order"/> + <field name="state">code</field> + <field name="code">action = records.open_form_multi_ask_approval_po()</field> + </record> + </data> + <data> <record id="purchase_order_multi_create_uangmuka_ir_actions_server" model="ir.actions.server"> <field name="name">Uang Muka</field> <field name="model_id" ref="purchase.model_purchase_order"/> diff --git a/indoteknik_custom/views/purchase_order_multi_ask_approval.xml b/indoteknik_custom/views/purchase_order_multi_ask_approval.xml new file mode 100644 index 00000000..887ac0bd --- /dev/null +++ b/indoteknik_custom/views/purchase_order_multi_ask_approval.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<odoo> + <data> + <record id="view_purchase_order_multi_ask_approval_po_form" model="ir.ui.view"> + <field name="name">Purchase Order Multi ask_approval</field> + <field name="model">purchase.order.multi_ask_approval</field> + <field name="arch" type="xml"> + <form> + <sheet> + <group> + <span>Apakah Anda Yakin Ingin Ask Approval PO Tersebut?</span> + </group> + </sheet> + <footer> + <button name="save_multi_ask_approval_po" string="Ask Approval PO" type="object" default_focus="1" class="oe_highlight"/> + <button string="Cancel" class="btn btn-secondary" special="cancel" /> + </footer> + </form> + </field> + </record> + + <record id="action_purchase_order_multi_ask_approval" model="ir.actions.act_window"> + <field name="name">Purchase Order Multi ask_approval</field> + <field name="res_model">purchase.order.multi_ask_approval</field> + <field name="type">ir.actions.act_window</field> + <field name="view_mode">form</field> + <field name="view_id" ref="view_purchase_order_multi_ask_approval_po_form"/> + <field name="target">new</field> + </record> + </data> +</odoo>
\ No newline at end of file diff --git a/indoteknik_custom/views/purchasing_job.xml b/indoteknik_custom/views/purchasing_job.xml index 16f1bedd..bb1c7643 100644 --- a/indoteknik_custom/views/purchasing_job.xml +++ b/indoteknik_custom/views/purchasing_job.xml @@ -17,6 +17,7 @@ <field name="status_apo" invisible="1"/> <field name="action"/> <field name="note"/> + <field name="date_po"/> </tree> </field> </record> diff --git a/indoteknik_custom/views/report_logbook_sj.xml b/indoteknik_custom/views/report_logbook_sj.xml index 896594bb..94f6c2ab 100644 --- a/indoteknik_custom/views/report_logbook_sj.xml +++ b/indoteknik_custom/views/report_logbook_sj.xml @@ -77,6 +77,16 @@ </field> </record> + <record id="report_logbook_sj_view_search" model="ir.ui.view"> + <field name="name">report.logbook.sj.search.view</field> <!-- Made the name more descriptive --> + <field name="model">report.logbook.sj</field> + <field name="arch" type="xml"> + <search string="Search Report"> + <field name="sj_number"/> + </search> + </field> + </record> + <record id="report_logbook_sj_action" model="ir.actions.act_window"> <field name="name">Report Logbook SJ</field> <field name="type">ir.actions.act_window</field> diff --git a/indoteknik_custom/views/res_partner.xml b/indoteknik_custom/views/res_partner.xml index 19d4b940..5160523f 100644 --- a/indoteknik_custom/views/res_partner.xml +++ b/indoteknik_custom/views/res_partner.xml @@ -131,6 +131,7 @@ <group string="Pengiriman" colspan="4"> <group> <field name="pic_name"/> + <field name="pic_mobile"/> <field name="street_pengiriman"/> <field name="state_id_pengiriman"/> <field name="city_id_pengiriman"/> @@ -140,6 +141,7 @@ </group> <group> <field name="invoice_pic"/> + <field name="invoice_pic_mobile"/> <field name="street_invoice"/> <field name="state_id_invoice"/> <field name="city_id_invoice"/> @@ -150,6 +152,7 @@ <group> <field name="tukar_invoice"/> <field name="jadwal_bayar"/> + <field name="dokumen_prosedur" /> <field name="dokumen_pengiriman"/> <field name="dokumen_pengiriman_input"/> <field name="dokumen_invoice"/> diff --git a/indoteknik_custom/views/res_users.xml b/indoteknik_custom/views/res_users.xml index 9553bb91..5b35f9c4 100644 --- a/indoteknik_custom/views/res_users.xml +++ b/indoteknik_custom/views/res_users.xml @@ -236,6 +236,7 @@ <tr><td style="padding-bottom: 16px;"><b>Pembayaran Lengkap:</b> Pilih metode pembayaran yang sesuai dengan kebutuhan Anda, baik transfer bank, VA, kartu kredit, atau pembayaran tempo.</td></tr> <tr><td style="padding-bottom: 16px;"><b>Pelacakan Pengiriman:</b> Lacak status pengiriman pesanan Anda secara real-time.</td></tr> <tr><td style="padding-bottom: 16px;">Untuk memulai transaksi, silakan login Kembali menggunakan akun Anda di <a href="https://indoteknik.com/login">Indoteknik.com</a></td></tr> + <tr><td style="padding-bottom: 16px;">Gunakan kode voucher berikut untuk mendapatkan diskon belanja hingga 10 juta: <strong>${object.get_voucher_code('switch_account')}</strong></td></tr> <tr><td style="padding-bottom: 16px;">Kami sangat senang dapat melayani Anda. Jika ada pertanyaan atau membutuhkan bantuan, jangan ragu untuk menghubungi tim layanan Customer kami di Whatsapp <a href="https://wa.me/6281717181922">0817-1718-1922</a> atau <a href="mailto:sales@indoteknik.com">sales@indoteknik.com</a></td></tr> <tr><td style="padding-bottom: 2px;"><b>Hormat kami,</b></td></tr> @@ -299,6 +300,7 @@ <tr><td style="padding-bottom: 16px;">Selamat! Akun bisnis Anda di indoteknik.com sekarang sudah resmi menjadi PKP dari yang sebelumnya Non-PKP.</td></tr> <tr><td style="padding-bottom: 16px;">Jangan lupa untuk mengecek kembali semua data perusahaan kamu, ya. Pastikan NPWP dan informasi Perusahaan lainnya sudah kamu isi dengan benar.</td></tr> + <tr><td style="padding-bottom: 16px;">Gunakan kode voucher berikut untuk mendapatkan diskon belanja hingga 10 juta: <strong>${object.get_voucher_code('switch_account')}</strong></td></tr> <tr><td style="padding-bottom: 16px;">Kamu juga dapat mengubah informasi perusahaan dengan mengunjungi profil atau <a href="https://indoteknik.com/my/profile">klik disini</a></td></tr> <tr><td style="padding-bottom: 2px;">Industrial Supply & Solutions</td></tr> diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml index 008a04ed..e0085eeb 100755 --- a/indoteknik_custom/views/sale_order.xml +++ b/indoteknik_custom/views/sale_order.xml @@ -1,174 +1,225 @@ -<?xml version="1.0" encoding="UTF-8" ?> +<?xml version="1.0" encoding="UTF-8"?> <odoo> <data> <record id="sale_order_form_view_inherit" model="ir.ui.view"> <field name="name">Sale Order</field> <field name="model">sale.order</field> - <field name="inherit_id" ref="sale.view_order_form"/> + <field name="inherit_id" ref="sale.view_order_form" /> <field name="arch" type="xml"> <button id="action_confirm" position="after"> <button name="calculate_line_no" - string="Create No" - type="object" + string="Create No" + type="object" /> <button name="sale_order_approve" - string="Ask Approval" - type="object" - attrs="{'invisible': [('approval_status', '=', ['approved'])]}" + string="Ask Approval" + type="object" + attrs="{'invisible': [('approval_status', '=', ['approved'])]}" + /> + <button name="hold_unhold_qty_outgoing_so" + string="Hold/Unhold Outgoing" + type="object" + attrs="{'invisible': [('state', 'in', ['cancel'])]}" + /> + <button name="ask_retur_cancel_purchasing" + string="Ask Cancel Purchasing" + type="object" + attrs="{'invisible': [('state', 'in', ['cancel'])]}" /> <button name="action_web_approve" - string="Web Approve" - type="object" - attrs="{'invisible': ['|', '|', ('create_uid', '!=', 25), ('web_approval', '!=', False), ('state', '!=', 'draft')]}" + string="Web Approve" + type="object" + attrs="{'invisible': ['|', '|', ('create_uid', '!=', 25), ('web_approval', '!=', False), ('state', '!=', 'draft')]}" /> - <button name="indoteknik_custom.action_view_uangmuka_penjualan" string="UangMuka" - type="action" attrs="{'invisible': [('approval_status', '!=', 'approved')]}"/> + <button name="indoteknik_custom.action_view_uangmuka_penjualan" + string="UangMuka" + type="action" attrs="{'invisible': [('approval_status', '!=', 'approved')]}" /> </button> <field name="payment_term_id" position="after"> - <field name="create_uid" invisible="1"/> - <field name="create_date" invisible="1"/> - <field name="shipping_cost_covered" attrs="{'required': ['|', ('create_date', '>', '2023-06-15'), ('create_date', '=', False)]}"/> - <field name="shipping_paid_by" attrs="{'required': ['|', ('create_date', '>', '2023-06-15'), ('create_date', '=', False)]}"/> - <field name="delivery_amt"/> - <field name="fee_third_party"/> - <field name="total_percent_margin"/> - <field name="type_promotion"/> - <label for="voucher_id"/> + <field name="create_uid" invisible="1" /> + <field name="create_date" invisible="1" /> + <field name="shipping_cost_covered" + attrs="{'required': ['|', ('create_date', '>', '2023-06-15'), ('create_date', '=', False)]}" /> + <field name="shipping_paid_by" + attrs="{'required': ['|', ('create_date', '>', '2023-06-15'), ('create_date', '=', False)]}" /> + <field name="delivery_amt" /> + <field name="ongkir_ke_xpdc" /> + <field name="metode_kirim_ke_xpdc" /> + <field name="fee_third_party" /> + <field name="biaya_lain_lain" /> + <field name="total_percent_margin" /> + <field name="total_margin_excl_third_party" readonly="1" /> + <field name="type_promotion" /> + <label for="voucher_id" /> <div class="o_row"> - <field name="voucher_id" id="voucher_id" attrs="{'readonly': ['|', ('state', 'not in', ['draft', 'sent']), ('applied_voucher_id', '!=', False)]}"/> + <field name="voucher_id" id="voucher_id" + attrs="{'readonly': ['|', ('state', 'not in', ['draft', 'sent']), ('applied_voucher_id', '!=', False)]}" /> <field name="applied_voucher_id" invisible="1" /> - <button name="action_apply_voucher" type="object" string="Apply" confirm="Anda yakin untuk menggunakan voucher?" help="Apply the selected voucher" class="btn-link mb-1 px-0" icon="fa-plus" + <button name="action_apply_voucher" type="object" string="Apply" + confirm="Anda yakin untuk menggunakan voucher?" + help="Apply the selected voucher" class="btn-link mb-1 px-0" + icon="fa-plus" attrs="{'invisible': ['|', '|', ('voucher_id', '=', False), ('state', 'not in', ['draft', 'sent']), ('applied_voucher_id', '!=', False)]}" /> - <button name="cancel_voucher" type="object" string="Cancel" confirm="Anda yakin untuk membatalkan penggunaan voucher?" help="Cancel applied voucher" class="btn-link mb-1 px-0" icon="fa-times" + <button name="cancel_voucher" type="object" string="Cancel" + confirm="Anda yakin untuk membatalkan penggunaan voucher?" + help="Cancel applied voucher" class="btn-link mb-1 px-0" icon="fa-times" attrs="{'invisible': ['|', ('applied_voucher_id', '=', False), ('state', 'not in', ['draft','sent'])]}" /> </div> - <label for="voucher_shipping_id"/> + <label for="voucher_shipping_id" /> <div class="o_row"> - <field name="voucher_shipping_id" id="voucher_shipping_id" attrs="{'readonly': ['|', ('state', 'not in', ['draft', 'sent']), ('applied_voucher_shipping_id', '!=', False)]}"/> + <field name="voucher_shipping_id" id="voucher_shipping_id" + attrs="{'readonly': ['|', ('state', 'not in', ['draft', 'sent']), ('applied_voucher_shipping_id', '!=', False)]}" /> <field name="applied_voucher_shipping_id" invisible="1" /> - <button name="action_apply_voucher_shipping" type="object" string="Apply" confirm="Anda yakin untuk menggunakan voucher?" help="Apply the selected voucher" class="btn-link mb-1 px-0" icon="fa-plus" + <button name="action_apply_voucher_shipping" type="object" string="Apply" + confirm="Anda yakin untuk menggunakan voucher?" + help="Apply the selected voucher" class="btn-link mb-1 px-0" + icon="fa-plus" attrs="{'invisible': ['|', '|', ('voucher_id', '=', False), ('state', 'not in', ['draft', 'sent']), ('applied_voucher_shipping_id', '!=', False)]}" /> - <button name="cancel_voucher_shipping" type="object" string="Cancel" confirm="Anda yakin untuk membatalkan penggunaan voucher?" help="Cancel applied voucher" class="btn-link mb-1 px-0" icon="fa-times" + <button name="cancel_voucher_shipping" type="object" string="Cancel" + confirm="Anda yakin untuk membatalkan penggunaan voucher?" + help="Cancel applied voucher" class="btn-link mb-1 px-0" icon="fa-times" attrs="{'invisible': ['|', ('applied_voucher_shipping_id', '=', False), ('state', 'not in', ['draft','sent'])]}" /> </div> <button name="calculate_selling_price" - string="Calculate Selling Price" - type="object" + string="Calculate Selling Price" + type="object" /> </field> <field name="source_id" position="attributes"> <attribute name="invisible">1</attribute> </field> <field name="user_id" position="after"> - <field name="helper_by_id" readonly="1"/> - <field name="compute_fullfillment" invisible="1"/> + <field name="hold_outgoing" readonly="1" /> + <field name="helper_by_id" readonly="1" /> + <field name="compute_fullfillment" invisible="1" /> </field> <field name="tag_ids" position="after"> - <field name="eta_date" readonly="1"/> - <field name="flash_sale"/> - <field name="margin_after_delivery_purchase"/> - <field name="percent_margin_after_delivery_purchase"/> - <field name="total_weight"/> - <field name="pareto_status"/> + <field name="eta_date_start" /> + <t t-esc="' to '" /> + <field name="eta_date" readonly="1" /> + <field name="expected_ready_to_ship" /> + <field name="flash_sale" /> + <field name="margin_after_delivery_purchase" /> + <field name="percent_margin_after_delivery_purchase" /> + <field name="total_weight" /> + <field name="pareto_status" /> </field> <field name="analytic_account_id" position="after"> - <field name="customer_type" required="1"/> - <field name="npwp" placeholder='99.999.999.9-999.999' required="1"/> - <field name="sppkp" attrs="{'required': [('customer_type', '=', 'pkp')]}"/> - <field name="email" required="1"/> - <field name="unreserve_id"/> - <field name="due_id" readonly="1"/> - <field name="vendor_approval_id" readonly="1" widget="many2many_tags"/> - <field name="source_id" domain="[('id', 'in', [32, 59, 60, 61])]" required="1"/> + <field name="customer_type" readonly="1" /> + <field name="npwp" placeholder='99.999.999.9-999.999' readonly="1" /> + <field name="sppkp" attrs="{'required': [('customer_type', '=', 'pkp')]}" readonly="1" /> + <field name="email" required="1" /> + <field name="unreserve_id" /> + <field name="due_id" readonly="1" /> + <field name="vendor_approval_id" readonly="1" widget="many2many_tags" /> + <field name="source_id" domain="[('id', 'in', [32, 59, 60, 61])]" required="1" /> <button name="override_allow_create_invoice" - string="Override Create Invoice" - type="object" + string="Override Create Invoice" + type="object" /> - <button string="Estimate Shipping" type="object" name="action_estimate_shipping"/> + <button string="Estimate Shipping" type="object" name="action_estimate_shipping" /> </field> <field name="partner_shipping_id" position="after"> - <field name="real_shipping_id"/> - <field name="real_invoice_id"/> + <field name="real_shipping_id" /> + <field name="real_invoice_id" /> <field name="approval_status" /> - <field name="sales_tax_id" domain="[('type_tax_use','=','sale'), ('active', '=', True)]" required="1"/> - <field name="carrier_id" required="1"/> - <field name="delivery_service_type" readonly="1"/> + <field name="sales_tax_id" + domain="[('type_tax_use','=','sale'), ('active', '=', True)]" required="1" /> + <field name="carrier_id" required="1" /> + <field name="delivery_service_type" readonly="1" /> + <field name="shipping_option_id" /> </field> <field name="medium_id" position="after"> - <field name="date_doc_kirim" readonly="1"/> - <field name="notification" readonly="1"/> + <field name="date_doc_kirim" readonly="1" /> + <field name="notification" readonly="1" /> </field> - <xpath expr="//form/sheet/notebook/page/field[@name='order_line']" position="attributes"> + <xpath expr="//form/sheet/notebook/page/field[@name='order_line']" + position="attributes"> <attribute name="attrs"> {'readonly': [('state', 'in', ('done','cancel'))]} </attribute> </xpath> - <xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree" position="inside"> - <field name="desc_updatable" invisible="1"/> + <xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree" + position="inside"> + <field name="desc_updatable" invisible="1" /> </xpath> - <xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='name']" position="attributes"> - <attribute name="attrs"> + <xpath + expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='name']" + position="attributes"> + <attribute name="modifiers"> {'readonly': [('desc_updatable', '=', False)]} </attribute> </xpath> - <xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='price_unit']" position="attributes"> + <xpath + expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='price_unit']" + position="attributes"> <attribute name="attrs"> { - 'readonly': [ - '|', - ('qty_invoiced', '>', 0), - ('parent.approval_status', '!=', False) - ] + 'readonly': [ + '|', + ('qty_invoiced', '>', 0), + ('parent.approval_status', '!=', False) + ] } </attribute> </xpath> <div name="invoice_lines" position="before"> - <div name="vendor_id" groups="base.group_no_one" attrs="{'invisible': [('display_type', '!=', False)]}"> - <label for="vendor_id"/> + <div name="vendor_id" groups="base.group_no_one" + attrs="{'invisible': [('display_type', '!=', False)]}"> + <label for="vendor_id" /> <div name="vendor_id"> - <field name="vendor_id" - attrs="{'readonly': [('parent.approval_status', '=', 'approved')]}" - domain="[('parent_id', '=', False)]" - options="{'no_create': True}" class="oe_inline" /> + <field name="vendor_id" + attrs="{'readonly': [('parent.approval_status', '=', 'approved')]}" + domain="[('parent_id', '=', False)]" + options="{'no_create': True}" class="oe_inline" /> </div> </div> </div> - + <div name="invoice_lines" position="before"> - <div name="purchase_price" groups="base.group_no_one" attrs="{'invisible': [('display_type', '!=', False)]}"> - <label for="purchase_price"/> - <field name="purchase_price"/> + <div name="purchase_price" groups="base.group_no_one" + attrs="{'invisible': [('display_type', '!=', False)]}"> + <label for="purchase_price" /> + <field name="purchase_price" /> </div> </div> <div name="invoice_lines" position="before"> - <div name="purchase_tax_id" groups="base.group_no_one" attrs="{'invisible': [('display_type', '!=', False)]}"> - <label for="purchase_tax_id"/> + <div name="purchase_tax_id" groups="base.group_no_one" + attrs="{'invisible': [('display_type', '!=', False)]}"> + <label for="purchase_tax_id" /> <div name="purchase_tax_id"> - <field name="purchase_tax_id"/> + <field name="purchase_tax_id" /> </div> </div> </div> <div name="invoice_lines" position="before"> - <div name="item_percent_margin" groups="base.group_no_one" attrs="{'invisible': [('display_type', '!=', False)]}"> - <label for="item_percent_margin"/> - <field name="item_percent_margin"/> + <div name="item_percent_margin" groups="base.group_no_one" + attrs="{'invisible': [('display_type', '!=', False)]}"> + <label for="item_percent_margin" /> + <field name="item_percent_margin" /> </div> </div> <div name="invoice_lines" position="before"> - <div name="price_subtotal" groups="base.group_no_one" attrs="{'invisible': [('display_type', '!=', False)]}"> - <label for="price_subtotal"/> - <field name="price_subtotal"/> + <div name="price_subtotal" groups="base.group_no_one" + attrs="{'invisible': [('display_type', '!=', False)]}"> + <label for="price_subtotal" /> + <field name="price_subtotal" /> </div> </div> - <xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='price_total']" position="after"> - <field name="qty_free_bu" optional="hide"/> - <field name="vendor_id" attrs="{'readonly': [('parent.approval_status', '=', 'approved')], 'invisible': [('display_type', '!=', False)]}" domain="[('parent_id', '=', False)]" options="{'no_create':True}"/> - <field name="vendor_md_id" optional="hide"/> - <field name="purchase_price" attrs=" + <xpath + expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='price_total']" + position="after"> + <field name="qty_free_bu" optional="hide" /> + <field name="vendor_id" + attrs="{'readonly': [('parent.approval_status', '=', 'approved')], 'invisible': [('display_type', '!=', False)]}" + domain="[('parent_id', '=', False)]" options="{'no_create':True}" /> + <field name="vendor_md_id" optional="hide" /> + <field name="purchase_price" + attrs=" { 'readonly': [ '|', @@ -176,56 +227,79 @@ ('parent.approval_status', '!=', False) ] } - "/> - <field name="purchase_price_md" optional="hide"/> - <field name="purchase_tax_id" attrs="{'readonly': [('parent.approval_status', '!=', False)]}" domain="[('type_tax_use','=','purchase')]" options="{'no_create':True}"/> - <field name="item_percent_margin"/> - <field name="item_margin" optional="hide"/> - <field name="margin_md" optional="hide"/> - <field name="note" optional="hide"/> - <field name="note_procurement" optional="hide"/> - <field name="vendor_subtotal" optional="hide"/> - <field name="weight" optional="hide"/> - <field name="amount_voucher_disc" string="Voucher" readonly="1" optional="hide"/> - <field name="order_promotion_id" string="Promotion" readonly="1" optional="hide"/> + " /> + <field name="purchase_price_md" optional="hide" /> + <field name="purchase_tax_id" + attrs="{'readonly': [('parent.approval_status', '!=', False)]}" + domain="[('type_tax_use','=','purchase')]" options="{'no_create':True}" /> + <field name="item_percent_margin" /> + <field name="item_margin" optional="hide" /> + <field name="margin_md" optional="hide" /> + <field name="note" optional="hide" /> + <field name="note_procurement" optional="hide" /> + <field name="vendor_subtotal" optional="hide" /> + <field name="weight" optional="hide" /> + <field name="amount_voucher_disc" string="Voucher" readonly="1" optional="hide" /> + <field name="order_promotion_id" string="Promotion" readonly="1" optional="hide" /> </xpath> - <xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='product_id']" position="before"> - <field name="line_no" readonly="1" optional="hide"/> + <xpath + expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='product_id']" + position="before"> + <field name="line_no" readonly="1" optional="hide" /> </xpath> - <xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='qty_delivered']" position="before"> - <field name="qty_reserved" invisible="1"/> - <field name="reserved_from" readonly="1" optional="hide"/> + <xpath + expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='qty_delivered']" + position="before"> + <field name="qty_reserved" invisible="1" /> + <field name="reserved_from" readonly="1" optional="hide" /> </xpath> - <xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='product_id']" position="attributes"> + <xpath + expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='product_id']" + position="attributes"> <attribute name="options">{'no_create': True}</attribute> </xpath> - <!-- <xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='tax_id']" position="attributes"> + <!-- <xpath + expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='tax_id']" + position="attributes"> <attribute name="required">1</attribute> </xpath> --> <field name="amount_total" position="after"> - <field name="grand_total"/> + <field name="grand_total" /> <label for="amount_voucher_disc" string="Voucher" /> <div> - <field class="mb-0" name="amount_voucher_disc" string="Voucher" readonly="1"/> - <div class="text-right mb-2"><small>*Hanya informasi</small></div> + <field class="mb-0" name="amount_voucher_disc" string="Voucher" readonly="1" /> + <div class="text-right mb-2"> + <small>*Hanya informasi</small> + </div> </div> <label for="amount_voucher_shipping_disc" string="Voucher Shipping" /> <div> - <field class="mb-0" name="amount_voucher_shipping_disc" string="Voucher Shipping" readonly="1"/> - <div class="text-right mb-2"><small>*Hanya informasi</small></div> + <field class="mb-0" name="amount_voucher_shipping_disc" + string="Voucher Shipping" readonly="1" /> + <div class="text-right mb-2"> + <small>*Hanya informasi</small> + </div> </div> - <field name="total_margin"/> - <field name="total_percent_margin"/> + <field name="total_margin" /> + <field name="total_percent_margin" /> + <field name="total_before_margin" /> </field> <field name="effective_date" position="after"> - <field name="carrier_id"/> - <field name="estimated_arrival_days"/> - <field name="picking_iu_id"/> - <field name="note_ekspedisi"/> + <field name="carrier_id" /> + <field name="estimated_arrival_days" /> + <field name="picking_iu_id" /> + <field name="note_ekspedisi" /> </field> <field name="carrier_id" position="attributes"> <attribute name="attrs"> - {'readonly': [('approval_status', '=', 'approved'), ('state', 'not in', ['cancel','draft'])]} + {'readonly': [('approval_status', '=', 'approved'), ('state', 'not in', + ['cancel','draft'])]} + </attribute> + </field> + <field name="payment_term_id" position="attributes"> + <attribute name="attrs"> + {'readonly': [('approval_status', '=', 'approved'), ('state', 'not in', + ['cancel','draft'])]} </attribute> </field> @@ -233,22 +307,22 @@ <page string="Website" name="customer_purchase_order"> <group> <group> - <field name="partner_purchase_order_name" readonly="True"/> - <field name="partner_purchase_order_description" readonly="True"/> - <field name="partner_purchase_order_file" readonly="True"/> - <field name="note_website" readonly="True"/> - <field name="web_approval" readonly="True"/> + <field name="partner_purchase_order_name" readonly="True" /> + <field name="partner_purchase_order_description" readonly="True" /> + <field name="partner_purchase_order_file" readonly="True" /> + <field name="note_website" readonly="True" /> + <field name="web_approval" readonly="True" /> </group> <group> <button name="generate_payment_link_midtrans_sales_order" - string="Create Payment Link" - type="object" + string="Create Payment Link" + type="object" /> - <field name="payment_link_midtrans" readonly="True" widget="url"/> - <field name="gross_amount" readonly="True"/> - <field name="payment_type" readonly="True"/> - <field name="payment_status" readonly="True"/> - <field name="payment_qr_code" widget="image" readonly="True"/> + <field name="payment_link_midtrans" readonly="True" widget="url" /> + <field name="gross_amount" readonly="True" /> + <field name="payment_type" readonly="True" /> + <field name="payment_status" readonly="True" /> + <field name="payment_qr_code" widget="image" readonly="True" /> </group> </group> </page> @@ -269,60 +343,92 @@ </field> </page> <page string="Matches PO" name="page_matches_po" invisible="1"> - <field name="order_sales_match_line" readonly="1"/> + <field name="order_sales_match_line" readonly="1" /> </page> <!-- <page string="Fullfillment" name="page_sale_order_fullfillment"> <field name="fullfillment_line" readonly="1"/> </page> --> <page string="Fulfillment v2" name="page_sale_order_fullfillment2"> - <field name="fulfillment_line_v2" readonly="1"/> + <field name="fulfillment_line_v2" readonly="1" /> </page> <page string="Reject Line" name="page_sale_order_reject_line"> - <field name="reject_line" readonly="1"/> + <field name="reject_line" readonly="0" /> + </page> + <page string="Koli" name="page_sales_order_koli_line"> + <field name="koli_lines" readonly="1" /> </page> </page> </field> </record> + <!-- Wizard for Reject Reason --> + <record id="view_cancel_reason_order_form" model="ir.ui.view"> + <field name="name">cancel.reason.order.form</field> + <field name="model">cancel.reason.order</field> + <field name="arch" type="xml"> + <form string="Cancel Reason"> + <group> + <field name="reason_cancel" widget="selection" /> + <field name="attachment_bukti" widget="many2many_binary" required="1" /> + <field name="nomor_so_pengganti" + attrs="{'invisible': [('reason_cancel', '!=', 'ganti_quotation')]}" /> + </group> + <footer> + <button string="Confirm" type="object" name="confirm_reject" + class="btn-primary" /> + <button string="Cancel" class="btn-secondary" special="cancel" /> + </footer> + </form> + </field> + </record> + + <record id="action_cancel_reason_order" model="ir.actions.act_window"> + <field name="name">Cancel Reason</field> + <field name="res_model">cancel.reason.order</field> + <field name="view_mode">form</field> + <field name="target">new</field> + </record> </data> <data> <record id="sale_order_tree_view_inherit" model="ir.ui.view"> <field name="name">Sale Order</field> <field name="model">sale.order</field> - <field name="inherit_id" ref="sale.view_quotation_tree_with_onboarding"/> + <field name="inherit_id" ref="sale.view_quotation_tree_with_onboarding" /> <field name="arch" type="xml"> <field name="state" position="after"> <field name="approval_status" /> - <field name="client_order_ref"/> - <field name="payment_type" optional="hide"/> - <field name="payment_status" optional="hide"/> - <field name="pareto_status" optional="hide"/> + <field name="client_order_ref" /> + <field name="payment_type" optional="hide" /> + <field name="payment_status" optional="hide" /> + <field name="pareto_status" optional="hide" /> + <field name="shipping_method_picking" optional="hide" /> + <field name="hold_outgoing" optional="hide" /> </field> </field> </record> <record id="sales_order_tree_view_inherit" model="ir.ui.view"> <field name="name">Sale Order</field> <field name="model">sale.order</field> - <field name="inherit_id" ref="sale.view_order_tree"/> + <field name="inherit_id" ref="sale.view_order_tree" /> <field name="arch" type="xml"> <field name="state" position="after"> <field name="approval_status" /> - <field name="client_order_ref"/> - <field name="so_status"/> - <field name="date_status_done"/> - <field name="date_kirim_ril"/> - <field name="date_driver_departure"/> - <field name="date_driver_arrival"/> - <field name="payment_type" optional="hide"/> - <field name="payment_status" optional="hide"/> - <field name="pareto_status" optional="hide"/> + <field name="client_order_ref" /> + <field name="so_status" /> + <field name="date_status_done" /> + <field name="date_kirim_ril" /> + <field name="date_driver_departure" /> + <field name="date_driver_arrival" /> + <field name="payment_type" optional="hide" /> + <field name="payment_status" optional="hide" /> + <field name="pareto_status" optional="hide" /> </field> </field> </record> <record id="sale_order_multi_update_ir_actions_server" model="ir.actions.server"> <field name="name">Mark As Cancel</field> - <field name="model_id" ref="sale.model_sale_order"/> - <field name="binding_model_id" ref="sale.model_sale_order"/> + <field name="model_id" ref="sale.model_sale_order" /> + <field name="binding_model_id" ref="sale.model_sale_order" /> <field name="binding_view_types">form,list</field> <field name="state">code</field> <field name="code">action = records.open_form_multi_update_state()</field> @@ -330,46 +436,81 @@ <record id="sale_order_update_multi_actions_server" model="ir.actions.server"> <field name="name">Mark As Completed</field> - <field name="model_id" ref="sale.model_sale_order"/> - <field name="binding_model_id" ref="sale.model_sale_order"/> + <field name="model_id" ref="sale.model_sale_order" /> + <field name="binding_model_id" ref="sale.model_sale_order" /> <field name="state">code</field> <field name="code">action = records.open_form_multi_update_status()</field> </record> <record id="mail_template_sale_order_web_approve_notification" model="mail.template"> <field name="name">Sale Order: Web Approve Notification</field> - <field name="model_id" ref="indoteknik_custom.model_sale_order"/> + <field name="model_id" ref="indoteknik_custom.model_sale_order" /> <field name="subject">Permintaan Persetujuan Pesanan ${object.name} di Indoteknik.com</field> <field name="email_from">sales@indoteknik.com</field> <field name="email_to">${object.partner_id.email | safe}</field> <field name="email_cc">${object.partner_id.get_approve_partner_ids("email_comma_sep")}</field> <field name="body_html" type="html"> - <table border="0" cellpadding="0" cellspacing="0" style="padding: 16px 0; background-color: #F1F1F1; font-family:Inter, Helvetica, Verdana, Arial,sans-serif; line-height: 24px; color: #454748; width: 100%; border-collapse:separate;"> - <tr><td align="center"> - <table border="0" cellpadding="0" cellspacing="0" width="590" style="font-size: 13px; padding: 16px; background-color: white; color: #454748; border-collapse:separate;"> - <tbody> - <tr> - <td align="center" style="min-width: 590px;"> - <table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;"> - <tr><td style="padding-bottom: 24px;">Dear ${(object.partner_id.get_main_parent()).name},</td></tr> + <table border="0" cellpadding="0" cellspacing="0" + style="padding: 16px 0; background-color: #F1F1F1; font-family:Inter, Helvetica, Verdana, Arial,sans-serif; line-height: 24px; color: #454748; width: 100%; border-collapse:separate;"> + <tr> + <td align="center"> + <table border="0" cellpadding="0" cellspacing="0" width="590" + style="font-size: 13px; padding: 16px; background-color: white; color: #454748; border-collapse:separate;"> + <tbody> + <tr> + <td align="center" style="min-width: 590px;"> + <table border="0" cellpadding="0" cellspacing="0" + width="590" + style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;"> + <tr> + <td style="padding-bottom: 24px;"> + Dear + ${(object.partner_id.get_main_parent()).name},</td> + </tr> - <tr><td style="padding-bottom: 16px;">Ini adalah konfirmasi pesanan dari ${object.partner_id.name | safe} untuk nomor pesanan ${object.name} yang memerlukan persetujuan agar dapat diproses.</td></tr> - <tr><td style="padding-bottom: 16px;"> - <a href="https://indoteknik.com/my/quotations/${object.id}" style="color: white; background-color: #C53030; border: none; border-radius: 6px; padding: 4px 8px; width: fit-content; display: block;"> - Lihat Pesanan - </a> - </td></tr> - <tr><td style="padding-bottom: 16px;">Mohon segera melakukan tinjauan terhadap pesanan ini dan memberikan persetujuan. Terima kasih atas perhatian dan kerjasama Anda. Kami berharap dapat segera melanjutkan proses pesanan ini setelah mendapatkan persetujuan Anda.</td></tr> + <tr> + <td style="padding-bottom: 16px;">Ini adalah + konfirmasi pesanan dari + ${object.partner_id.name | safe} untuk nomor + pesanan ${object.name} yang memerlukan + persetujuan agar dapat diproses.</td> + </tr> + <tr> + <td style="padding-bottom: 16px;"> + <a + href="https://indoteknik.com/my/quotations/${object.id}" + style="color: white; background-color: #C53030; border: none; border-radius: 6px; padding: 4px 8px; width: fit-content; display: block;"> + Lihat Pesanan + </a> + </td> + </tr> + <tr> + <td style="padding-bottom: 16px;">Mohon segera + melakukan tinjauan terhadap pesanan ini dan + memberikan persetujuan. Terima kasih atas + perhatian dan kerjasama Anda. Kami berharap + dapat segera melanjutkan proses pesanan ini + setelah mendapatkan persetujuan Anda.</td> + </tr> - <tr><td style="padding-bottom: 2px;">Hormat kami,</td></tr> - <tr><td style="padding-bottom: 2px;">PT. Indoteknik Dotcom Gemilang</td></tr> - <tr><td style="padding-bottom: 2px;">sales@indoteknik.com</td></tr> - </table> - </td> - </tr> - </tbody> - </table> - </td></tr> + <tr> + <td style="padding-bottom: 2px;">Hormat kami,</td> + </tr> + <tr> + <td style="padding-bottom: 2px;">PT. Indoteknik + Dotcom Gemilang</td> + </tr> + <tr> + <td style="padding-bottom: 2px;"> + sales@indoteknik.com</td> + </tr> + </table> + </td> + </tr> + </tbody> + </table> + </td> + </tr> </table> </field> </record> @@ -381,45 +522,59 @@ <field name="model">sales.order.purchase.match</field> <field name="arch" type="xml"> <tree editable="top" create="false" delete="false"> - <field name="purchase_order_id" readonly="1"/> - <field name="purchase_line_id" readonly="1"/> - <field name="product_id" readonly="1"/> - <field name="qty_so" readonly="1"/> - <field name="qty_po" readonly="1"/> + <field name="purchase_order_id" readonly="1" /> + <field name="purchase_line_id" readonly="1" /> + <field name="product_id" readonly="1" /> + <field name="qty_so" readonly="1" /> + <field name="qty_po" readonly="1" /> </tree> </field> </record> </data> <data> - - </data> - <record id="sales_order_fulfillment_v2_tree" model="ir.ui.view"> - <field name="name">sales.order.fulfillment.v2.tree</field> - <field name="model">sales.order.fulfillment.v2</field> + <record id="sales_order_koli_tree" model="ir.ui.view"> + <field name="name">sales.order.koli.tree</field> + <field name="model">sales.order.koli</field> <field name="arch" type="xml"> - <tree editable="top" create="false"> - <field name="product_id" readonly="1"/> - <field name="so_qty" readonly="1" optional="show"/> - <field name="reserved_stock_qty" readonly="1" optional="show"/> - <field name="delivered_qty" readonly="1" optional="hide"/> - <field name="po_ids" widget="many2many_tags" readonly="1" optional="show"/> - <field name="po_qty" readonly="1" optional="show"/> - <field name="received_qty" readonly="1" optional="show"/> - <field name="purchaser" readonly="1" optional="hide"/> + <tree editable="top" create="false" delete="false"> + <field name="koli_id" readonly="1" /> + <field name="picking_id" readonly="1" /> + <field name="state" readonly="1" /> </tree> </field> </record> + </data> + + <data> + + </data> + <record id="sales_order_fulfillment_v2_tree" model="ir.ui.view"> + <field name="name">sales.order.fulfillment.v2.tree</field> + <field name="model">sales.order.fulfillment.v2</field> + <field name="arch" type="xml"> + <tree editable="top" create="false"> + <field name="product_id" readonly="1" /> + <field name="so_qty" readonly="1" optional="show" /> + <field name="reserved_stock_qty" readonly="1" optional="show" /> + <field name="delivered_qty" readonly="1" optional="hide" /> + <field name="po_ids" widget="many2many_tags" readonly="1" optional="show" /> + <field name="po_qty" readonly="1" optional="show" /> + <field name="received_qty" readonly="1" optional="show" /> + <field name="purchaser" readonly="1" optional="hide" /> + </tree> + </field> + </record> <data> <record id="sales_order_fullfillmet_tree" model="ir.ui.view"> <field name="name">sales.order.fullfillment.tree</field> <field name="model">sales.order.fullfillment</field> <field name="arch" type="xml"> <tree editable="top" create="false"> - <field name="product_id" readonly="1"/> - <field name="reserved_from" readonly="1"/> - <field name="qty_fullfillment" readonly="1"/> - <field name="user_id" readonly="1"/> + <field name="product_id" readonly="1" /> + <field name="reserved_from" readonly="1" /> + <field name="qty_fullfillment" readonly="1" /> + <field name="user_id" readonly="1" /> </tree> </field> </record> @@ -431,9 +586,9 @@ <field name="model">sales.order.reject</field> <field name="arch" type="xml"> <tree editable="top" create="false"> - <field name="product_id" readonly="1"/> - <field name="qty_reject" readonly="1"/> - <field name="reason_reject" readonly="1"/> + <field name="product_id" readonly="1" /> + <field name="qty_reject" readonly="1" /> + <field name="reason_reject" readonly="0" /> </tree> </field> </record> @@ -442,8 +597,8 @@ <data> <record id="sale_order_multi_create_uangmuka_ir_actions_server" model="ir.actions.server"> <field name="name">Uang Muka</field> - <field name="model_id" ref="sale.model_sale_order"/> - <field name="binding_model_id" ref="sale.model_sale_order"/> + <field name="model_id" ref="sale.model_sale_order" /> + <field name="binding_model_id" ref="sale.model_sale_order" /> <field name="state">code</field> <field name="code">action = records.open_form_multi_create_uang_muka()</field> </record> @@ -452,66 +607,84 @@ <data> <record id="mail_template_sale_order_notification_to_salesperson" model="mail.template"> <field name="name">Sale Order: Notification to Salesperson</field> - <field name="model_id" ref="sale.model_sale_order"/> + <field name="model_id" ref="sale.model_sale_order" /> <field name="subject">Konsolidasi Pengiriman</field> <field name="email_from">sales@indoteknik.com</field> <field name="email_to">${object.user_id.login | safe}</field> <field name="body_html" type="html"> - <table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Inter, Helvetica, Verdana, Arial,sans-serif; line-height: 24px; color: #454748; width: 100%; border-collapse:separate;"> - <tr><td align="center"> - <table border="0" cellpadding="0" cellspacing="0" width="590" style="font-size: 13px; padding: 16px; background-color: white; color: #454748; border-collapse:separate;"> - <!-- HEADER --> - <tbody> - <tr> - <td align="center" style="min-width: 590px;"> - <table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;"> - <tr> - <td valign="middle"> - <span></span> - </td> - </tr> - </table> - </td> - </tr> - <!-- CONTENT --> - <tr> - <td align="center" style="min-width: 590px;"> - <table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;"> - <tr><td style="padding-bottom: 24px;">Dear ${salesperson_name},</td></tr> - - <tr><td style="padding-bottom: 16px;">Terdapat pesanan dari BP ${business_partner} untuk site ${site} dengan total belanja ${sum_total_amount} dari list SO dibawah ini:</td></tr> - - <tr> - <td> - <table border="1" cellpadding="5" cellspacing="0"> - <thead> - <tr> - <th>Nama Pesanan</th> - <th>Nama Perusahaan Induk</th> - <th>Nama Situs</th> - <th>Total Pembelian</th> - </tr> - </thead> - <tbody> - ${table_content} - </tbody> - </table> - </td> - </tr> - - <tr> - <td style="text-align:center;"> - <hr width="100%" - style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;" /> - </td> - </tr> - </table> - </td> - </tr> - <!-- CONTENT --> - </tbody> - </table> - </td></tr> + <table border="0" cellpadding="0" cellspacing="0" + style="padding-top: 16px; background-color: #F1F1F1; font-family:Inter, Helvetica, Verdana, Arial,sans-serif; line-height: 24px; color: #454748; width: 100%; border-collapse:separate;"> + <tr> + <td align="center"> + <table border="0" cellpadding="0" cellspacing="0" width="590" + style="font-size: 13px; padding: 16px; background-color: white; color: #454748; border-collapse:separate;"> + <!-- HEADER --> + <tbody> + <tr> + <td align="center" style="min-width: 590px;"> + <table border="0" cellpadding="0" cellspacing="0" + width="590" + style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;"> + <tr> + <td valign="middle"> + <span></span> + </td> + </tr> + </table> + </td> + </tr> + <!-- CONTENT --> + <tr> + <td align="center" style="min-width: 590px;"> + <table border="0" cellpadding="0" cellspacing="0" + width="590" + style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;"> + <tr> + <td style="padding-bottom: 24px;">Dear + ${salesperson_name},</td> + </tr> + + <tr> + <td style="padding-bottom: 16px;">Terdapat + pesanan dari BP ${business_partner} untuk + site ${site} dengan total belanja + ${sum_total_amount} dari list SO dibawah + ini:</td> + </tr> + + <tr> + <td> + <table border="1" cellpadding="5" + cellspacing="0"> + <thead> + <tr> + <th>Nama Pesanan</th> + <th>Nama Perusahaan Induk</th> + <th>Nama Situs</th> + <th>Total Pembelian</th> + </tr> + </thead> + <tbody> + ${table_content} + </tbody> + </table> + </td> + </tr> + + <tr> + <td style="text-align:center;"> + <hr width="100%" + style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;" /> + </td> + </tr> + </table> + </td> + </tr> + <!-- CONTENT --> + </tbody> + </table> + </td> + </tr> </table> </field> </record> diff --git a/indoteknik_custom/views/shipment_group.xml b/indoteknik_custom/views/shipment_group.xml index e9eec41b..d2c661ba 100644 --- a/indoteknik_custom/views/shipment_group.xml +++ b/indoteknik_custom/views/shipment_group.xml @@ -7,6 +7,8 @@ <tree default_order="create_date desc"> <field name="number"/> <field name="partner_id"/> + <field name="carrier_id"/> + <field name="total_colly_line"/> </tree> </field> </record> @@ -16,11 +18,9 @@ <field name="model">shipment.group.line</field> <field name="arch" type="xml"> <tree editable="bottom"> - <field name="picking_id" required="1"/> - <field name="partner_id" readonly="1"/> <field name="sale_id" readonly="1"/> - <field name="shipping_paid_by" readonly="1"/> - <field name="state" readonly="1"/> + <field name="picking_id" required="1"/> + <field name="total_colly" readonly="1"/> </tree> </field> </record> @@ -37,6 +37,8 @@ </group> <group> <field name="partner_id" readonly="1"/> + <field name="carrier_id" readonly="1"/> + <field name="total_colly_line" readonly="1"/> </group> </group> <notebook> diff --git a/indoteknik_custom/views/stock_backorder_confirmation_views.xml b/indoteknik_custom/views/stock_backorder_confirmation_views.xml new file mode 100644 index 00000000..a622899e --- /dev/null +++ b/indoteknik_custom/views/stock_backorder_confirmation_views.xml @@ -0,0 +1,14 @@ +<odoo> + <record id="view_backorder_confirmation_inherit" model="ir.ui.view"> + <field name="name">stock_backorder_confirmation_inherit</field> + <field name="model">stock.backorder.confirmation</field> + <field name="inherit_id" ref="stock.view_backorder_confirmation"/> + <field name="arch" type="xml"> + <xpath expr="//group/p[1]" position="replace"> + <p> + Ada product yang quantity done nya kurang dari quantity demand. + </p> + </xpath> + </field> + </record> +</odoo> diff --git a/indoteknik_custom/views/stock_inventory.xml b/indoteknik_custom/views/stock_inventory.xml new file mode 100644 index 00000000..db85f05c --- /dev/null +++ b/indoteknik_custom/views/stock_inventory.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<odoo> + <!-- Form View: Tambahkan field 'number' setelah lokasi --> + <record id="view_stock_inventory_form_inherit" model="ir.ui.view"> + <field name="name">stock.inventory.form.inherit</field> + <field name="model">stock.inventory</field> + <field name="inherit_id" ref="stock.view_inventory_form"/> + <field name="arch" type="xml"> + <xpath expr="//field[@name='location_ids']" position="after"> + <field name="number" readonly="1"/> + <field name="adjusment_type" /> + </xpath> + </field> + </record> + + <!-- Tree View: Tambahkan field 'number' setelah tanggal --> + <record id="view_stock_inventory_tree_inherit" model="ir.ui.view"> + <field name="name">stock.inventory.tree.inherit</field> + <field name="model">stock.inventory</field> + <field name="inherit_id" ref="stock.view_inventory_tree"/> + <field name="arch" type="xml"> + <xpath expr="//field[@name='date']" position="after"> + <field name="number"/> + </xpath> + </field> + </record> + +</odoo> diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml index 8acba608..ae77ab9a 100644 --- a/indoteknik_custom/views/stock_picking.xml +++ b/indoteknik_custom/views/stock_picking.xml @@ -7,7 +7,8 @@ <field name="inherit_id" ref="stock.vpicktree"/> <field name="arch" type="xml"> <tree position="attributes"> - <attribute name="default_order">create_date desc</attribute> + <attribute name="default_order">final_seq asc</attribute> + <!-- <attribute name="default_order">create_date desc</attribute> --> </tree> <field name="json_popover" position="after"> <field name="date_done" optional="hide"/> @@ -18,8 +19,14 @@ <field name="note" optional="hide"/> <field name="date_reserved" optional="hide"/> <field name="state_reserve" optional="hide"/> + <field name="state_packing" widget="badge" decoration-success="state_packing == 'packing_done'" decoration-danger="state_packing == 'not_packing'" optional="hide"/> + <field name="final_seq"/> + <field name="state_approve_md" widget="badge" decoration-success="state_approve_md == 'done'" decoration-warning="state_approve_md == 'pending'" optional="hide"/> + <!-- <field name="countdown_hours" optional="hide"/> + <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> @@ -66,12 +73,33 @@ type="object" attrs="{'invisible': [('carrier_id', '!=', 9)]}" /> + <button name="action_get_kgx_pod" + string="Tracking KGX" + type="object" + attrs="{'invisible': [('carrier_id', '!=', 173)]}" + /> + <button name="button_state_approve_md" + string="Approve MD Gudang Selisih" + type="object" + attrs="{'invisible': [('state_approve_md', 'not in', ['waiting', 'pending'])]}" + /> + + <button name="button_state_pending_md" + string="Pending MD Gudang Selisih" + type="object" + attrs="{'invisible': [('state_approve_md', 'not in', ['waiting'])]}" + /> </button> <field name="backorder_id" position="after"> + <field name="shipping_method_so_id"/> <field name="summary_qty_detail"/> <field name="count_line_detail"/> <field name="dokumen_tanda_terima"/> <field name="dokumen_pengiriman"/> + <field name="quantity_koli" attrs="{'invisible': [('location_dest_id', '!=', 60)], 'required': [('location_dest_id', '=', 60)]}"/> + <field name="total_mapping_koli" attrs="{'invisible': [('location_id', '!=', 60)]}"/> + <field name="total_koli_display" readonly="1" attrs="{'invisible': [('location_id', '!=', 60)]}"/> + <field name="linked_out_picking_id" readonly="1" attrs="{'invisible': [('location_id', '=', 60)]}"/> </field> <field name="weight_uom_name" position="after"> <group> @@ -86,19 +114,22 @@ <field name="partner_id" position="after"> <field name="real_shipping_id"/> </field> - <field name="product_uom_qty" position="attributes"> + <!-- <field name="product_uom_qty" position="attributes"> <attribute name="attrs">{'readonly': [('parent.picking_type_code', '=', 'incoming')]}</attribute> - </field> + </field> --> <field name="date_done" position="after"> <field name="arrival_time"/> </field> <field name="origin" position="after"> +<!-- <field name="show_state_approve_md" invisible="1" optional="hide"/>--> + <field name="state_approve_md" widget="badge"/> <field name="purchase_id"/> <field name="sale_order"/> <field name="invoice_status"/> <field name="date_doc_kirim" attrs="{'readonly':[('invoice_status', '=', 'invoiced')]}"/> <field name="summary_qty_operation"/> <field name="count_line_operation"/> + <field name="linked_manual_bu_out" attrs="{'invisible': [('location_id', '=', 60)]}" domain="[('picking_type_code', '=', 'outgoing'),('state', 'not in', ['done','cancel']), ('group_id', '=', group_id)]"/> <field name="account_id" attrs="{ 'readonly': [['state', 'in', ['done', 'cancel']]], @@ -124,6 +155,7 @@ <field name="approval_status"/> <field name="approval_receipt_status"/> <field name="approval_return_status"/> + <field name="so_lama"/> </field> <field name="product_id" position="before"> <field name="line_no" attrs="{'readonly': 1}" optional="hide"/> @@ -132,6 +164,7 @@ <field name="sale_id" attrs="{'readonly': 1}" optional="hide"/> <field name="print_barcode" optional="hide"/> <field name="qr_code_variant" widget="image" optional="hide"/> + <field name="barcode" optional="hide"/> </field> <page name="note" position="after"> <page string="E-Faktur" name="efaktur" attrs="{'invisible': [['is_internal_use', '=', False]]}"> @@ -143,13 +176,15 @@ </group> </group> </page> - <page string="Delivery" name="delivery_order"> + <page string="Delivery" name="delivery_order" attrs="{'invisible': [('location_dest_id', '=', 60)]}"> <group> <group> <field name="notee"/> <field name="note_logistic"/> + <field name="note_info"/> <field name="responsible" /> <field name="carrier_id"/> + <field name="out_code" attrs="{'invisible': [['out_code', '=', False]]}"/> <field name="picking_code" attrs="{'invisible': [['picking_code', '=', False]]}"/> <field name="picking_code" string="Picking code (akan digenerate ketika sudah di-validate)" attrs="{'invisible': [['picking_code', '!=', False]]}"/> <field name="driver_departure_date" attrs="{'readonly':[('invoice_status', '=', 'invoiced')]}"/> @@ -189,27 +224,92 @@ <field name="lalamove_image_url" invisible="1"/> <field name="lalamove_image_html"/> </group> + <group attrs="{'invisible': [('carrier_id', '!=', 173)]}"> + <field name="kgx_pod_photo_url" invisible="1"/> + <field name="kgx_pod_photo"/> + <field name="kgx_pod_signature" invisible="1"/> + <field name="kgx_pod_receive_time"/> + <field name="kgx_pod_receiver"/> + </group> </group> </page> - <!-- <page string="Check Product" name="check_product"> + <page string="Check Product" name="check_product" attrs="{'invisible': [('picking_type_code', '=', 'outgoing')]}"> <field name="check_product_lines"/> - </page> --> + </page> + <page string="Barcode Product" name="barcode_product" attrs="{'invisible': [('picking_type_code', '!=', 'incoming')]}"> + <field name="barcode_product_lines"/> + </page> + <page string="Check Koli" name="check_koli" attrs="{'invisible': [('location_dest_id', '!=', 60)]}"> + <field name="check_koli_lines"/> + </page> + <page string="Mapping Koli" name="konfirm_koli" attrs="{'invisible': [('picking_type_code', '!=', 'outgoing')]}"> + <field name="konfirm_koli_lines"/> + </page> + <page string="Konfirm Koli" name="scan_koli" attrs="{'invisible': [('picking_type_code', '!=', 'outgoing')]}"> + <field name="scan_koli_lines"/> + </page> </page> </field> </record> - <!-- <record id="check_product_tree" model="ir.ui.view"> + <record id="scan_koli_tree" model="ir.ui.view"> + <field name="name">scan.koli.tree</field> + <field name="model">scan.koli</field> + <field name="arch" type="xml"> + <tree editable="bottom"> + <field name="code_koli"/> + <field name="koli_id" options="{'no_create': True}" domain="[('state', '=', 'not_delivered')]"/> + <field name="scan_koli_progress"/> + </tree> + </field> + </record> + + <record id="konfirm_koli_tree" model="ir.ui.view"> + <field name="name">konfirm.koli.tree</field> + <field name="model">konfirm.koli</field> + <field name="arch" type="xml"> + <tree editable="bottom"> + <field name="pick_id" options="{'no_create': True}" required="1" domain="[('picking_type_code', '=', 'internal'), ('group_id', '=', parent.group_id), ('linked_manual_bu_out', '=', parent.id)]"/> + </tree> + </field> + </record> + + <record id="check_koli_tree" model="ir.ui.view"> + <field name="name">check.koli.tree</field> + <field name="model">check.koli</field> + <field name="arch" type="xml"> + <tree editable="bottom"> + <field name="koli"/> + <field name="reserved_id"/> + <field name="check_koli_progress"/> + </tree> + </field> + </record> + + <record id="check_product_tree" model="ir.ui.view"> <field name="name">check.product.tree</field> <field name="model">check.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"/> + <field name="status" readonly="1"/> + </tree> + </field> + </record> + + <record id="barcode_product_tree" model="ir.ui.view"> + <field name="name">barcode.product.tree</field> + <field name="model">barcode.product</field> + <field name="arch" type="xml"> + <tree editable="bottom"> + <field name="product_id"/> + <field name="barcode"/> </tree> </field> - </record> --> + </record> <record id="view_stock_move_line_detailed_operation_tree_inherit" model="ir.ui.view"> <field name="name">stock.move.line.operations.tree.inherit</field> @@ -232,6 +332,35 @@ <field name="purchase_representative_id"/> </field> </field> - </record> + </record> + + <record id="view_warning_modal_wizard_form" model="ir.ui.view"> + <field name="name">warning.modal.wizard.form</field> + <field name="model">warning.modal.wizard</field> + <field name="arch" type="xml"> + <form string="Peringatan Koli Belum Diperiksa"> + <sheet> + <div class="oe_title"> + <h2><span>⚠️ Perhatian!</span></h2> + </div> + <group> + <field name="message" readonly="1" nolabel="1" widget="text"/> + </group> + </sheet> + <footer> + <button name="action_continue" type="object" string="Lanjutkan" class="btn-primary"/> + <button string="Tutup" class="btn-secondary" special="cancel"/> + </footer> + </form> + </field> + </record> + + <record id="action_warning_modal_wizard" model="ir.actions.act_window"> + <field name="name">Peringatan Koli</field> + <field name="res_model">warning.modal.wizard</field> + <field name="view_mode">form</field> + <field name="view_id" ref="view_warning_modal_wizard_form"/> + <field name="target">new</field> + </record> </data> </odoo>
\ No newline at end of file diff --git a/indoteknik_custom/views/user_pengajuan_tempo.xml b/indoteknik_custom/views/user_pengajuan_tempo.xml index 33ad91cf..4eebe9e4 100644 --- a/indoteknik_custom/views/user_pengajuan_tempo.xml +++ b/indoteknik_custom/views/user_pengajuan_tempo.xml @@ -53,6 +53,7 @@ <group string="Pengiriman" colspan="4"> <group> <field name="pic_name"/> + <field name="pic_mobile"/> <field name="street_pengiriman"/> <field name="state_id_pengiriman"/> <field name="city_id_pengiriman"/> @@ -62,6 +63,7 @@ </group> <group> <field name="invoice_pic"/> + <field name="invoice_pic_mobile"/> <field name="street_invoice"/> <field name="state_id_invoice"/> <field name="city_id_invoice"/> @@ -269,7 +271,7 @@ <field name="subject">Pengajuan Tempo Harus di Periksa!</field> <field name="email_from">"Indoteknik.com" <noreply@indoteknik.com></field> <field name="reply_to">sales@indoteknik.com</field> - <field name="email_to">widyariyanti97@gmail.com, stephan@indoteknik.co.id</field> + <field name="email_to">finance@indoteknik.co.id, stephan@indoteknik.co.id, sapiabon768@gmail.com</field> <!-- <field name="email_to">sapiabon768@gmail.com</field>--> <field name="body_html" type="html"> <table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Inter, Helvetica, Verdana, Arial,sans-serif; line-height: 24px; color: #454748; width: 100%; border-collapse:separate;"> diff --git a/indoteknik_custom/views/user_pengajuan_tempo_request.xml b/indoteknik_custom/views/user_pengajuan_tempo_request.xml index bb8262c9..339ce8db 100644 --- a/indoteknik_custom/views/user_pengajuan_tempo_request.xml +++ b/indoteknik_custom/views/user_pengajuan_tempo_request.xml @@ -10,7 +10,17 @@ <field name="create_date"/> <field name="tempo_duration" attrs="{'readonly': [('state_tempo', '=', 'approval_director')]}"/> <field name="tempo_limit" attrs="{'readonly': [('state_tempo', '=', 'approval_director')]}" placeholder="Contoh format, misalnya '10000000'" widget="monetary" options="{'currency_field': 'currency_id'}"/> - <field name="state_tempo" decoration-success="state_tempo == 'approval_director'" decoration-danger="state_tempo == 'reject'" widget="badge" optional="show"/> +<!-- <field name="state_tempo"--> +<!-- decoration-danger="state_tempo == 'reject'"--> +<!-- decoration-warning="state_tempo in ('draft', 'approval_sales', 'approval_finance')"--> +<!-- decoration-success="state_tempo == 'approval_director'">--> +<!-- <tree>--> +<!-- <field name="state_tempo" invisible="1"/>--> +<!-- <field name="state_tempo_text" string="Status"/>--> +<!-- </tree>--> +<!-- </field>--> + <field name="state_tempo" invisible="1"/> + <field name="state_tempo_text" decoration-success="state_tempo_text == 'Approved'" decoration-danger="state_tempo_text == 'Rejected'" widget="badge" optional="show"/> </tree> </field> </record> @@ -30,7 +40,11 @@ string="Reject" attrs="{'invisible': [('state_tempo', 'in', ['approval_director','reject'])]}" type="object" - groups="purchase.group_purchase_manager" + class="oe_highlight"/> + <button name="button_draft" + string="Reset to Draft" + attrs="{'invisible': [('state_tempo', '!=', 'reject')]}" + type="object" class="oe_highlight"/> <field name="state_tempo" widget="statusbar" statusbar_visible="draft,approval_sales,approval_finance,approval_director" @@ -88,6 +102,7 @@ <Page string="Pengiriman"> <group> <field name="pic_name"/> + <field name="pic_mobile"/> <field name="street_pengiriman"/> <field name="state_id_pengiriman"/> <field name="city_id_pengiriman"/> @@ -97,6 +112,7 @@ </group> <group> <field name="invoice_pic"/> + <field name="invoice_pic_mobile"/> <field name="street_invoice"/> <field name="state_id_invoice"/> <field name="city_id_invoice"/> @@ -107,6 +123,7 @@ <group> <field name="tukar_invoice"/> <field name="jadwal_bayar"/> + <field name="dokumen_prosedur" widget="many2many_binary"/> <field name="dokumen_pengiriman"/> <field name="dokumen_pengiriman_input"/> <field name="dokumen_invoice"/> diff --git a/indoteknik_custom/views/vendor_payment_term.xml b/indoteknik_custom/views/vendor_payment_term.xml index e0e96388..7d16b129 100644 --- a/indoteknik_custom/views/vendor_payment_term.xml +++ b/indoteknik_custom/views/vendor_payment_term.xml @@ -8,6 +8,8 @@ <field name="display_name"/> <field name="name"/> <field name="parent_id"/> + <field name="minimum_amount"/> + <field name="minimum_amount_tax"/> <field name="property_supplier_payment_term_id"/> </tree> </field> @@ -23,6 +25,8 @@ <group> <field name="name"/> <field name="parent_id" readonly="1"/> + <field name="minimum_amount"/> + <field name="minimum_amount_tax"/> <field name="property_supplier_payment_term_id"/> </group> </group> diff --git a/indoteknik_custom/views/vendor_sla.xml b/indoteknik_custom/views/vendor_sla.xml new file mode 100644 index 00000000..cf4425eb --- /dev/null +++ b/indoteknik_custom/views/vendor_sla.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8" ?> +<odoo> + <record id="vendor_action" model="ir.actions.act_window"> + <field name="name">Vendor SLA</field> + <field name="res_model">vendor.sla</field> + <field name="view_mode">tree,form</field> + </record> + + <record id="vendor_tree" model="ir.ui.view"> + <field name="name">Vendor SLA</field> + <field name="model">vendor.sla</field> + <field name="arch" type="xml"> + <tree> + <field name="id_vendor" string="Vendor Name" /> + <field name="duration_unit" string="Duration" /> + </tree> + </field> + </record> + + <record id="vendor_sla_view" model="ir.ui.view"> + <field name="name">Vendor SLA</field> + <field name="model">vendor.sla</field> + <field name="arch" type="xml"> + <form> + <sheet> + <group> + <field name="id_vendor" string="Vendor Name" /> + <field name="duration" string="SLA Duration" /> + <field name="unit" string="SLA Time" /> + </group> + </sheet> + </form> + </field> + </record> + + <menuitem id="menu_vendor_sla" + name="Vendor SLA" + parent="menu_monitoring_in_purchase" + sequence="1" + action="vendor_action" + /> +</odoo>
\ No newline at end of file diff --git a/indoteknik_custom/views/voucher.xml b/indoteknik_custom/views/voucher.xml index 29d0ad4b..78e42969 100755 --- a/indoteknik_custom/views/voucher.xml +++ b/indoteknik_custom/views/voucher.xml @@ -27,62 +27,71 @@ <group> <group> <field name="image" widget="image" width="120"/> - <field name="name" required="1" /> - <field name="code" required="1" /> - <field name="visibility" required="1" /> + <field name="name" required="1"/> + <field name="code" required="1"/> + <field name="voucher_category" widget="many2many"/> + <field name="visibility" required="1"/> <field name="start_time" required="1"/> <field name="end_time" required="1"/> <field name="limit" required="1"/> <field name="limit_user" required="1"/> - <field name="apply_type" required="1" /> - <field name="show_on_email" /> - <field name="excl_pricelist_ids" widget="many2many_tags" domain="[('id', 'in', [4, 15037, 15038, 15039, 17023, 17024, 17025, 17026,17027])]"/> + <field name="apply_type" required="1"/> + <field name="account_type" required="1"/> + <field name="show_on_email"/> + <field name="excl_pricelist_ids" widget="many2many_tags" + domain="[('id', 'in', [4, 15037, 15038, 15039, 17023, 17024, 17025, 17026,17027])]"/> </group> - <group string="Discount Settings" attrs="{'invisible': [('apply_type', 'not in', ['all', 'shipping'])]}"> - <field name="min_purchase_amount" widget="monetary" required="1" /> - <field name="discount_type" attrs="{'invisible': [('apply_type','not in', ['all', 'shipping'])], 'required': [('apply_type', 'in', ['all', 'shipping'])]}" /> + <group string="Discount Settings" + attrs="{'invisible': [('apply_type', 'not in', ['all', 'shipping'])]}"> + <field name="min_purchase_amount" widget="monetary" required="1"/> + <field name="discount_type" + attrs="{'invisible': [('apply_type','not in', ['all', 'shipping'])], 'required': [('apply_type', 'in', ['all', 'shipping'])]}"/> - <label for="max_discount_amount" string="Discount Amount" /> + <label for="max_discount_amount" string="Discount Amount"/> <div class="d-flex align-items-center"> - <span - class="mr-1 font-weight-bold" - attrs="{'invisible': [('discount_type', '!=', 'fixed_price')]}" + <span + class="mr-1 font-weight-bold" + attrs="{'invisible': [('discount_type', '!=', 'fixed_price')]}" > Rp </span> - <field class="mb-0" name="discount_amount" required="1" /> - <span - class="ml-1 font-weight-bold" - attrs="{'invisible': [('discount_type', '!=', 'percentage')]}" + <field class="mb-0" name="discount_amount" required="1"/> + <span + class="ml-1 font-weight-bold" + attrs="{'invisible': [('discount_type', '!=', 'percentage')]}" > % </span> </div> - <field name="max_discount_amount" widget="monetary" required="1" attrs="{'invisible': [('discount_type', '!=', 'percentage')]}"/> + <field name="max_discount_amount" widget="monetary" required="1" + attrs="{'invisible': [('discount_type', '!=', 'percentage')]}"/> </group> </group> <notebook> - <page name="voucher_line" string="Voucher Line" attrs="{'invisible': [('apply_type', '!=', 'brand')]}"> + <page name="voucher_line" string="Voucher Line" + attrs="{'invisible': [('apply_type', '!=', 'brand')]}"> <field name="voucher_line"> <tree editable="bottom"> - <field name="manufacture_id" required="1" /> - <field name="min_purchase_amount" required="1" /> - <field name="discount_type" required="1" /> - <field name="discount_amount" required="1" /> - <field name="max_discount_amount" required="1" attrs="{'readonly': [('discount_type', '!=', 'percentage')]}" /> + <field name="manufacture_id" required="1"/> + <field name="min_purchase_amount" required="1"/> + <field name="discount_type" required="1"/> + <field name="discount_amount" required="1"/> + <field name="max_discount_amount" required="1" + attrs="{'readonly': [('discount_type', '!=', 'percentage')]}"/> </tree> </field> </page> <page name="description" string="Description"> - <label for="description" string="Max 120 characters:" class="font-weight-normal mb-2 oe_edit_only"/> - <field name="description" placeholder="Insert short description..." /> + <label for="description" string="Max 120 characters:" + class="font-weight-normal mb-2 oe_edit_only"/> + <field name="description" placeholder="Insert short description..."/> </page> <page name="terms_conditions" string="Terms and Conditions"> - <field name="terms_conditions" /> + <field name="terms_conditions"/> </page> <page name="order_page" string="Orders"> - <field name="order_ids" readonly="1" /> + <field name="order_ids" readonly="1"/> </page> </notebook> </sheet> @@ -91,10 +100,10 @@ </record> <menuitem id="voucher" - name="Voucher" - parent="website_sale.menu_catalog" - sequence="1" - action="voucher_action" + name="Voucher" + parent="website_sale.menu_catalog" + sequence="1" + action="voucher_action" /> </data> </odoo>
\ No newline at end of file diff --git a/indoteknik_custom/views/x_banner_banner.xml b/indoteknik_custom/views/x_banner_banner.xml index ec1e38a5..e40568cc 100755 --- a/indoteknik_custom/views/x_banner_banner.xml +++ b/indoteknik_custom/views/x_banner_banner.xml @@ -33,6 +33,7 @@ <field name="group_by_week" /> <field name="x_headline_banner" /> <field name="x_description_banner" /> + <field name="x_keyword_banner" /> <field name="last_update_solr" readonly="1"/> </group> <group> diff --git a/indoteknik_custom/views/x_banner_category.xml b/indoteknik_custom/views/x_banner_category.xml index 11feb207..a83c4129 100755 --- a/indoteknik_custom/views/x_banner_category.xml +++ b/indoteknik_custom/views/x_banner_category.xml @@ -23,7 +23,7 @@ <group> <field name="x_name"/> <field name="x_banner_subtitle"/> - <field name="x_studio_field_KKVl4"/> + <field name="x_studio_field_KKVl4"/> <field name="last_update_solr" readonly="1"/> </group> <group></group> |
