diff options
| author | IT Fixcomart <it@fixcomart.co.id> | 2025-06-13 08:06:50 +0000 |
|---|---|---|
| committer | IT Fixcomart <it@fixcomart.co.id> | 2025-06-13 08:06:50 +0000 |
| commit | 655512eee7016f2e321b9994455f87a4c8e91884 (patch) | |
| tree | 361752527dde4be9c136f37a97ebec80f5dbfe4d | |
| parent | 8ac5d556a6686c6b81d5e9178bff5d308e8f176f (diff) | |
| parent | 80355d9de0d079ec2f1004efac0377b8c4bfa0eb (diff) | |
Merged in try-gmaps (pull request #333)
Try gmaps
| -rw-r--r-- | ab_openstreetmap/__manifest__.py | 2 | ||||
| -rw-r--r-- | ab_openstreetmap/static/src/js/googlemap_widget.js | 96 | ||||
| -rw-r--r-- | ab_openstreetmap/static/src/js/openstreetmap_widget.js | 106 | ||||
| -rw-r--r-- | ab_openstreetmap/static/src/xml/googlemap_template.xml | 7 | ||||
| -rw-r--r-- | ab_openstreetmap/static/src/xml/openstreetmap_template.xml | 8 | ||||
| -rw-r--r-- | ab_openstreetmap/views/templates.xml | 20 | ||||
| -rw-r--r-- | indoteknik_api/controllers/api_v1/partner.py | 2 | ||||
| -rw-r--r-- | indoteknik_custom/models/res_partner.py | 93 | ||||
| -rwxr-xr-x | indoteknik_custom/models/sale_order.py | 83 | ||||
| -rw-r--r-- | indoteknik_custom/views/res_partner.xml | 7 |
10 files changed, 258 insertions, 166 deletions
diff --git a/ab_openstreetmap/__manifest__.py b/ab_openstreetmap/__manifest__.py index 745e57ab..9d76c34c 100644 --- a/ab_openstreetmap/__manifest__.py +++ b/ab_openstreetmap/__manifest__.py @@ -12,6 +12,6 @@ 'category': 'Uncategorized', 'version': '0.1', 'depends': ['base'], - "qweb": ['static/src/xml/openstreetmap_template.xml'], + "qweb": ['static/src/xml/googlemap_template.xml'], 'data': ['views/templates.xml'], } diff --git a/ab_openstreetmap/static/src/js/googlemap_widget.js b/ab_openstreetmap/static/src/js/googlemap_widget.js new file mode 100644 index 00000000..4c48d564 --- /dev/null +++ b/ab_openstreetmap/static/src/js/googlemap_widget.js @@ -0,0 +1,96 @@ +odoo.define("ab_openstreetmap.googlemap_widget", function (require) { + "use strict"; + + const AbstractField = require("web.AbstractField"); + const fieldRegistry = require("web.field_registry"); + const rpc = require("web.rpc"); + + const GoogleMapWidget = AbstractField.extend({ + template: "googlemap_template", + + start: async function () { + await this._super(...arguments); + + this._waitForElement("#mapid", async () => { + const apiKey = await rpc.query({ + model: "ir.config_parameter", + method: "get_param", + args: ["google.maps.api_key"], + }); + const mapId = await rpc.query({ + model: "ir.config_parameter", + method: "get_param", + args: ["google.maps.map_id"], + }); + this._loadGoogleMaps(apiKey, mapId); + }); + }, + + _waitForElement: function (selector, callback) { + const el = document.querySelector(selector); + if (el && el.offsetHeight > 0 && el.offsetWidth > 0) { + callback(); + } else { + setTimeout(() => this._waitForElement(selector, callback), 100); + } + }, + + _loadGoogleMaps: function (apiKey, mapId) { + if (!window.google || !window.google.maps) { + const script = document.createElement("script"); + script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&v=weekly&libraries=marker`; + script.async = true; + script.defer = true; + script.onload = () => this._initMap(mapId); + document.head.appendChild(script); + } else { + this._initMap(mapId); + } + }, + + _initMap: async function (mapId) { + const lat = parseFloat(this.recordData.latitude) || -6.2; + const lng = parseFloat(this.recordData.longtitude) || 106.816666; + const edit = this.mode === "edit"; + + const mapEl = document.getElementById("mapid"); + if (!mapEl) return; + + const { Map } = await google.maps.importLibrary("maps"); + const { AdvancedMarkerElement } = await google.maps.importLibrary("marker"); + + const map = new Map(mapEl, { + center: { lat, lng }, + zoom: 15, + mapId: mapId, + }); + + const marker = new AdvancedMarkerElement({ + map, + position: { lat, lng }, + gmpDraggable: edit, + title: "Lokasi", + }); + + if (edit) { + marker.addListener("dragend", () => { + const pos = marker.position; + this.trigger_up("field_changed", { + dataPointID: this.dataPointID, + changes: { + latitude: pos.lat.toString(), + longtitude: pos.lng.toString(), + }, + viewType: this.viewType, + }); + }); + } + }, + + isSet: function () { + return true; + }, + }); + + fieldRegistry.add("googlemap", GoogleMapWidget); +}); diff --git a/ab_openstreetmap/static/src/js/openstreetmap_widget.js b/ab_openstreetmap/static/src/js/openstreetmap_widget.js deleted file mode 100644 index c84a2293..00000000 --- a/ab_openstreetmap/static/src/js/openstreetmap_widget.js +++ /dev/null @@ -1,106 +0,0 @@ -odoo.define("ab_openstreetmap.openstreetmap_widget", function (require) { - "use strict"; - - const fieldRegistry = require("web.field_registry"); - const AbstractField = require("web.AbstractField"); - - const OpenStreetMapWidget = AbstractField.extend({ - template: "openstreetmap_template", - - start: function () { - const self = this; - return this._super.apply(this, arguments).then(() => { - setTimeout(() => { - self._renderMapWhenReady(); - }, 100); - }); - }, - - _renderMapWhenReady: function () { - const self = this; - const check = () => { - const container = document.getElementById("mapid"); - if (container && container.offsetWidth > 0 && container.offsetHeight > 0) { - self._initMap(); - } else { - setTimeout(check, 100); - } - }; - check(); - }, - - _initMap: function () { - const self = this; - const container = document.getElementById("mapid"); - - // Bersihkan Leaflet instance sebelumnya jika ada - if (container && container._leaflet_id) { - container._leaflet_id = null; - } - - let lat = self.recordData.latitude; - let lng = self.recordData.longtitude; - - if (!lat && !lng) { - lat = -6.2349; - lng = 106.9896; - } - - const map = L.map("mapid").setView([lat, lng], 13); - - L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { - attribution: - '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', - }).addTo(map); - - const edit = self.mode === "edit"; - const marker = L.marker([lat, lng], { draggable: edit }).addTo(map); - - // Simpan koordinat saat marker digeser - marker.on("dragend", function (e) { - const latlng = e.target._latlng; - self.trigger_up("field_changed", { - dataPointID: self.dataPointID, - changes: { - latitude: latlng.lat.toString(), - longtitude: latlng.lng.toString(), - }, - viewType: self.viewType, - }); - }); - - // Fitur cari lokasi (geocoder) - if (edit) { - const geocode = L.Control.geocoder({ defaultMarkGeocode: false }).addTo(map); - geocode.on("markgeocode", function (e) { - const lat = e.geocode.center.lat; - const lng = e.geocode.center.lng; - map.flyTo([lat, lng]); - marker.setLatLng(new L.LatLng(lat, lng)); - self.trigger_up("field_changed", { - dataPointID: self.dataPointID, - changes: { - latitude: lat.toString(), - longtitude: lng.toString(), - }, - viewType: self.viewType, - }); - }); - } - - // Force render ulang map - const interval = setInterval(() => { - if (map && map._size.x > 0) { - clearInterval(interval); - } - window.dispatchEvent(new Event("resize")); - }, 500); - }, - - isSet: function () { - return true; - }, - }); - - fieldRegistry.add("openstreetmap", OpenStreetMapWidget); -}); diff --git a/ab_openstreetmap/static/src/xml/googlemap_template.xml b/ab_openstreetmap/static/src/xml/googlemap_template.xml new file mode 100644 index 00000000..53aef261 --- /dev/null +++ b/ab_openstreetmap/static/src/xml/googlemap_template.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + <t t-name="googlemap_template"> + <!-- <div id="mapid" style="height: 400px; width: 100%;"></div> --> + <div t-attf-id="mapid" style="height: 400px; width: 100%;" ></div> + </t> +</templates> diff --git a/ab_openstreetmap/static/src/xml/openstreetmap_template.xml b/ab_openstreetmap/static/src/xml/openstreetmap_template.xml deleted file mode 100644 index 82672748..00000000 --- a/ab_openstreetmap/static/src/xml/openstreetmap_template.xml +++ /dev/null @@ -1,8 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<templates id="template" xml:space="preserve"> - <t t-name="openstreetmap_template"> - <div style="width:100%"> - <div id="mapid" style="height: 500px;"/> - </div> - </t> -</templates>
\ No newline at end of file diff --git a/ab_openstreetmap/views/templates.xml b/ab_openstreetmap/views/templates.xml index ed82ae84..1f5a729b 100644 --- a/ab_openstreetmap/views/templates.xml +++ b/ab_openstreetmap/views/templates.xml @@ -1,13 +1,9 @@ <odoo> - <data> - <template id="assets_backend" inherit_id="web.assets_backend"> - <xpath expr="." position="inside"> - <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin=""/> - <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" crossorigin=""></script> - <link rel="stylesheet" href="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.css" /> - <script src="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.js"></script> - <script type="text/javascript" src="/ab_openstreetmap/static/src/js/openstreetmap_widget.js"/> - </xpath> - </template> - </data> -</odoo>
\ No newline at end of file + <data> + <template id="assets_backend" inherit_id="web.assets_backend"> + <xpath expr="." position="inside"> + <script type="text/javascript" src="/ab_openstreetmap/static/src/js/googlemap_widget.js"/> + </xpath> + </template> + </data> +</odoo> diff --git a/indoteknik_api/controllers/api_v1/partner.py b/indoteknik_api/controllers/api_v1/partner.py index c0f8f630..37ae8d5f 100644 --- a/indoteknik_api/controllers/api_v1/partner.py +++ b/indoteknik_api/controllers/api_v1/partner.py @@ -1,6 +1,6 @@ from .. import controller from odoo import http -from odoo.http import request +from odoo.http import request, Response from odoo import fields import json import base64 diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index eeb8b67d..6ef5698c 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -4,6 +4,8 @@ from datetime import datetime from odoo.http import request import re import requests +import logging +_logger = logging.getLogger(__name__) class GroupPartner(models.Model): _name = 'group.partner' @@ -186,6 +188,10 @@ class ResPartner(models.Model): def write(self, vals): res = super(ResPartner, self).write(vals) + + for rec in self: + if 'latitude' in vals or 'longtitude' in vals: + rec._update_address_from_coords() # # # if 'property_payment_term_id' in vals: # # if not self.env.user.is_accounting and vals['property_payment_term_id'] != 26: @@ -197,6 +203,14 @@ class ResPartner(models.Model): # # raise UserError('You name it') # return res + + @api.model + def create(self, vals): + records = super().create(vals) + for rec in records: + if vals.get('latitude') and vals.get('longtitude'): + rec._update_address_from_coords() + return records @api.constrains('name') def _check_duplicate_name(self): @@ -577,9 +591,88 @@ class ResPartner(models.Model): result = response.json() if result.get('results'): location = result['results'][0]['geometry']['location'] + formatted_address = result['results'][0].get('formatted_address', '') + rec.latitude = location['lat'] rec.longtitude = location['lng'] + rec.address_map = formatted_address # ✅ Simpan alamat lengkap else: raise UserError("Tidak ditemukan hasil geocode untuk alamat tersebut.") else: raise UserError("Permintaan ke Google Maps gagal. Periksa koneksi internet atau API Key.") + + def _update_address_from_coords(self): + for rec in self: + if rec.latitude and rec.longtitude: + try: + components, formatted, parsed = rec._reverse_geocode(rec.latitude, rec.longtitude) + if not parsed: + continue + + updates = { + 'street': parsed.get('road') or '', + 'zip': parsed.get('postcode') or '', + 'address_map': formatted or '', + } + + state = self.env['res.country.state'].search([('name', 'ilike', parsed.get('state'))], limit=1) + if state: + updates['state_id'] = state.id + + kota = self.env['vit.kota'].search([('name', 'ilike', parsed.get('city'))], limit=1) + if kota: + updates['kota_id'] = kota.id + + kec = self.env['vit.kecamatan'].search([('name', 'ilike', parsed.get('district'))], limit=1) + if kec: + updates['kecamatan_id'] = kec.id + + kel = self.env['vit.kelurahan'].search([('name', 'ilike', parsed.get('suburb'))], limit=1) + if kel: + updates['kelurahan_id'] = kel.id + + rec.update(updates) + + except Exception as e: + raise UserError(f"Gagal update alamat dari koordinat: {str(e)}") + + + def _reverse_geocode(self, lat, lng): + api_key = self.env['ir.config_parameter'].sudo().get_param('google.maps.api_key') + if not api_key: + raise UserError("API Key Google Maps belum dikonfigurasi.") + + url = f'https://maps.googleapis.com/maps/api/geocode/json?latlng={lat},{lng}&key={api_key}' + response = requests.get(url) + if response.ok: + result = response.json() + if result.get('results'): + components = result['results'][0]['address_components'] + formatted = result['results'][0]['formatted_address'] + return components, formatted, self._parse_google_address(components) + return {}, '', {} + + def _parse_google_address(self, components): + def get(types): + for comp in components: + if types in comp['types']: + return comp['long_name'] + return '' + + street_number = get('street_number') + route = get('route') + neighborhood = get('neighborhood') # Bisa jadi nama RW + subpremise = get('subpremise') # Bisa jadi no kamar/ruko + + # Gabungkan informasi jalan + road = " ".join(filter(None, [route, street_number, subpremise, neighborhood])) + + return { + 'road': road.strip(), + 'postcode': get('postal_code'), + 'state': get('administrative_area_level_1'), + 'city': get('administrative_area_level_2') or get('locality'), + 'district': get('administrative_area_level_3'), + 'suburb': get('administrative_area_level_4'), + 'formatted': get('formatted_address'), + } diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index f53d375b..e54053ff 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -421,39 +421,19 @@ class SaleOrder(models.Model): @api.onchange('carrier_id') def _onchange_carrier_id(self): - # ─────────────────────────────────────────────────────────────── - # 1. abaikan onchange kalau SO masih draft / belum tersimpan - # ─────────────────────────────────────────────────────────────── if not self._origin or not self._origin.id: return - sale_order_id = self._origin.id # id SO asli (sudah tersimpan) - - # ─────────────────────────────────────────────────────────────── - # 2. Jika SO BELUM mempunyai satupun shipping.option ⇒ - # jangan lakukan validasi apa-apa; cukup reset field & domain - # ─────────────────────────────────────────────────────────────── - total_so_options = self.env['shipping.option'].search_count( - [('sale_order_id', '=', sale_order_id)] - ) - if total_so_options == 0: - # belum pernah estimasi ongkir ⇒ biarkan user ganti carrier - self.shipping_option_id = False - return {'domain': {'shipping_option_id': [('id', '=', -1)]}} - - # ─────────────────────────────────────────────────────────────── - # 3. (kode lama) – mulai validasi hanya jika sudah ada option - # ─────────────────────────────────────────────────────────────── + sale_order_id = self._origin.id self.shipping_option_id = False if not self.carrier_id: return {'domain': {'shipping_option_id': [('id', '=', -1)]}} - # cari provider dari mapping rajaongkir_kurir + # Ambil provider dari mapping self.env.cr.execute(""" SELECT name FROM rajaongkir_kurir - WHERE delivery_carrier_id = %s - LIMIT 1 + WHERE delivery_carrier_id = %s LIMIT 1 """, (self.carrier_id.id,)) row = self.env.cr.fetchone() provider = row[0].lower() if row and row[0] else ( @@ -462,36 +442,69 @@ class SaleOrder(models.Model): _logger.info(f"[Carrier Changed] {self.carrier_id.name}, Detected Provider: {provider}") - # hitung berapa option yg match provider BARU - self.env.cr.execute(""" - SELECT COUNT(*) FROM shipping_option - WHERE LOWER(provider) LIKE %s AND sale_order_id = %s - """, (f'%{provider}%', sale_order_id)) - matched = self.env.cr.fetchone()[0] or 0 + # ─────────────────────────────────────────────────────────────── + # Validasi koordinat untuk kurir instan + # ─────────────────────────────────────────────────────────────── + instan_kurir = ['gojek', 'grab', 'lalamove', 'borzo', 'rara', 'deliveree'] + if provider in instan_kurir: + lat = self.real_shipping_id.latitude + lng = self.real_shipping_id.longtitude + def is_invalid(val): + try: + return not val or float(val) == 0.0 + except (ValueError, TypeError): + return True + + if is_invalid(lat) or is_invalid(lng): + self.carrier_id = self._origin.carrier_id + self.shipping_option_id = self._origin.shipping_option_id or False + return { + 'warning': { + 'title': "Alamat Belum Pin Point", + 'message': ( + "Kurir instan seperti Gojek, Grab, Lalamove, Borzo, Rara, dan Deliveree " + "membutuhkan alamat pengiriman yang sudah Pin Point.\n\n" + "Silakan tentukan lokasi dengan tepat pada Pin Point Location yang tersedia di kontak." + ) + }, + 'domain': {'shipping_option_id': [('id', '=', -1)]} + } + + # ─────────────────────────────────────────────────────────────── + # Baru cek apakah shipping option sudah ada + # ─────────────────────────────────────────────────────────────── + total_so_options = self.env['shipping.option'].search_count([ + ('sale_order_id', '=', sale_order_id) + ]) + if total_so_options == 0: + return {'domain': {'shipping_option_id': [('id', '=', -1)]}} + + # Validasi: apakah shipping option ada untuk provider ini? + matched = self.env['shipping.option'].search_count([ + ('sale_order_id', '=', sale_order_id), + ('provider', 'ilike', provider), + ]) if matched == 0: - # provider baru tidak ada di option yang SUDAH dibuat → kembalikan ke carrier lama - prev_carrier = self._origin.carrier_id - self.carrier_id = prev_carrier + self.carrier_id = self._origin.carrier_id self.shipping_option_id = self._origin.shipping_option_id or False return { 'warning': { 'title': "Shipping Option Tidak Ditemukan", 'message': ( - "Layanan kurir tidak tersedia untuk pengiriman ini.\n" + "Layanan kurir ini tidak tersedia pada pengiriman ini. " "Pilihan dikembalikan ke sebelumnya." ) }, 'domain': {'shipping_option_id': [('id', '=', -1)]} } - # kalau match ada → set domain normal (hanya option dengan provider itu) + # Kalau semua valid, kembalikan domain normal domain = [ '|', '&', ('sale_order_id', '=', sale_order_id), ('provider', 'ilike', f'%{provider}%'), '&', ('sale_order_id', '=', False), ('provider', 'ilike', f'%{provider}%') ] - return {'domain': {'shipping_option_id': domain}} @api.onchange('shipping_option_id') diff --git a/indoteknik_custom/views/res_partner.xml b/indoteknik_custom/views/res_partner.xml index 85ee5ef0..f739cf14 100644 --- a/indoteknik_custom/views/res_partner.xml +++ b/indoteknik_custom/views/res_partner.xml @@ -71,11 +71,12 @@ <button name="geocode_address" type="object" string="Get Pin Point Location" class="btn btn-primary"/> </group> <div style="margin: 16px 0;"> - <field name="map_view" widget="openstreetmap" nolabel="1" style="width: 100%;"/> + <field name="map_view" widget="googlemap" nolabel="1"/> </div> <group> - <field name="latitude" readonly="1"/> - <field name="longtitude" readonly="1"/> + <field name="address_map"/> + <field name="latitude" readonly="1" force_save="1"/> + <field name="longtitude" readonly="1" force_save="1"/> </group> </page> </xpath> |
