diff --git a/.gitignore b/.gitignore index 88355f1..2ba42d7 100644 --- a/.gitignore +++ b/.gitignore @@ -24,8 +24,19 @@ exports/ # OS .DS_Store +**/.DS_Store Thumbs.db +# Generated invoice/quote previews (output of setup_invoice_print_format.py +# + test_jinja_render.py). Keep sources (*.jinja) and final references +# (docs/assets/*.pdf when added intentionally), never ephemeral output. +invoice_preview*.pdf +scripts/migration/invoice_preview*.pdf +scripts/migration/invoice_preview*.html +scripts/migration/rendered_jinja_invoice* +scripts/migration/SINV-*.pdf +scripts/migration/ref_invoice.pdf + # IDE .vscode/ .idea/ diff --git a/README.md b/README.md index 32d46fd..792f544 100644 --- a/README.md +++ b/README.md @@ -130,15 +130,9 @@ Authentik SSO protects staff apps via Traefik `forwardAuth`. The ops app reads ` | Document | Content | |----------|---------| -| [ARCHITECTURE.md](docs/ARCHITECTURE.md) | Data model, tech stack, authentication flow, doctype reference | -| [INFRASTRUCTURE.md](docs/INFRASTRUCTURE.md) | Server, DNS, Traefik, Authentik, Docker, n8n, gotchas | -| [MIGRATION-PLAN.md](docs/MIGRATION-PLAN.md) | Legacy system portrait, field mapping, phases, risks | -| [CHANGELOG.md](docs/CHANGELOG.md) | Detailed migration log with volumes and methods | -| [ROADMAP.md](docs/ROADMAP.md) | 5-phase implementation plan | -| [ECOSYSTEM-OVERVIEW.md](docs/ECOSYSTEM-OVERVIEW.md) | Full platform ecosystem and integration map | -| [PLATFORM-STRATEGY.md](docs/PLATFORM-STRATEGY.md) | Platform strategy and product direction | -| [CUSTOMER-360-FLOWS.md](docs/CUSTOMER-360-FLOWS.md) | Customer lifecycle flows and 360 view design | -| [DESIGN_GUIDELINES.md](docs/DESIGN_GUIDELINES.md) | UI/UX design guidelines for ops apps | -| [COMPETITIVE-ANALYSIS.md](docs/COMPETITIVE-ANALYSIS.md) | Comparison with Gaiia, Odoo, Zuper, Salesforce, ServiceTitan | -| [TR069-TO-TR369-MIGRATION.md](docs/TR069-TO-TR369-MIGRATION.md) | CPE management protocol migration plan | -| [scripts/migration/MIGRATION_MAP.md](scripts/migration/MIGRATION_MAP.md) | Field-level mapping from legacy tables to ERPNext | +| [ARCHITECTURE.md](docs/ARCHITECTURE.md) | Ecosystem overview, remote Docker infrastructure, platform strategy | +| [DATA_AND_FLOWS.md](docs/DATA_AND_FLOWS.md) | ERPNext data models, atomic order creation, customer flows | +| [CPE_MANAGEMENT.md](docs/CPE_MANAGEMENT.md) | Hardware management, XX230v diagnostics, TR-069/TR-369 | +| [APP_DESIGN_GUIDELINES.md](docs/APP_DESIGN_GUIDELINES.md) | Frontend framework architecture rules, UI/UX Wizard guidelines | +| [ROADMAP.md](docs/ROADMAP.md) | Implementation phases and current remote transition tasks | +| [archive/](docs/archive/) | Completed legacy migration analyses and accounting audits | diff --git a/apps/field/src/composables/useScanner.js b/apps/field/src/composables/useScanner.js index a3e3872..d2c1ab8 100644 --- a/apps/field/src/composables/useScanner.js +++ b/apps/field/src/composables/useScanner.js @@ -1,5 +1,8 @@ -import { ref } from 'vue' +import { ref, watch } from 'vue' import { scanBarcodes } from 'src/api/ocr' +import { useOfflineStore } from 'src/stores/offline' + +const SCAN_TIMEOUT_MS = 8000 /** * Barcode scanner using device camera photo capture + Gemini Vision AI. @@ -8,59 +11,99 @@ import { scanBarcodes } from 'src/api/ocr' * the native camera app — this gives proper autofocus, tap-to-focus, * and high-res photos. Then send to Gemini Vision for barcode extraction. * - * Also keeps a thumbnail of each captured photo for reference. + * Resilience: if Gemini doesn't answer within SCAN_TIMEOUT_MS (weak LTE), + * the photo is queued in IndexedDB via the offline store and retried when + * the signal comes back. The tech gets a "scan en attente" indicator and + * can keep working; late results are delivered via onNewCode(). + * + * @param {object} options + * @param {(code: string) => void} [options.onNewCode] — called for each + * newly detected code, whether the scan was synchronous or delivered + * later from the offline queue. Typically used to trigger lookup + notify. */ -export function useScanner () { +export function useScanner (options = {}) { + const onNewCode = options.onNewCode || (() => {}) const barcodes = ref([]) // Array of { value, region } — max 3 const scanning = ref(false) // true while Gemini is processing const error = ref(null) const lastPhoto = ref(null) // data URI of last captured photo (thumbnail) const photos = ref([]) // all captured photo thumbnails + const offline = useOfflineStore() + + // Pick up any scans that completed while the page was unmounted (e.g. tech + // queued a photo, locked phone, walked out of the basement, signal returns). + for (const result of offline.scanResults) { + mergeCodes(result.barcodes || [], 'queued') + offline.consumeScanResult(result.id) + } + + // Watch for sync completions during the lifetime of this scanner. + // Vue auto-disposes the watcher when the host component unmounts. + watch( + () => offline.scanResults.length, + () => { + for (const result of [...offline.scanResults]) { + mergeCodes(result.barcodes || [], 'queued') + offline.consumeScanResult(result.id) + } + } + ) + + function addCode (code, region) { + if (barcodes.value.length >= 3) return false + if (barcodes.value.find(b => b.value === code)) return false + barcodes.value.push({ value: code, region }) + onNewCode(code) + return true + } + + function mergeCodes (codes, region) { + const added = [] + for (const code of codes) { + if (addCode(code, region)) added.push(code) + } + return added + } + /** * Process a photo file from camera input. - * Resizes for AI, keeps thumbnail, sends to Gemini. - * @param {File} file - image file from camera - * @returns {string[]} newly found barcode values + * Resizes for AI, keeps thumbnail, sends to Gemini with an 8s timeout. + * On timeout/failure, the photo is queued for background retry. */ async function processPhoto (file) { if (!file) return [] error.value = null scanning.value = true - const found = [] + let aiImage = null + const photoIdx = photos.value.length + let found = [] try { // Create thumbnail for display (small) const thumbUrl = await resizeImage(file, 400) lastPhoto.value = thumbUrl - photos.value.push({ url: thumbUrl, ts: Date.now(), codes: [] }) + photos.value.push({ url: thumbUrl, ts: Date.now(), codes: [], queued: false }) // Create optimized image for AI — keep high res for text readability - const aiImage = await resizeImage(file, 1600, 0.92) + aiImage = await resizeImage(file, 1600, 0.92) - // Send to Gemini Vision - const result = await scanBarcodes(aiImage) - const existing = new Set(barcodes.value.map(b => b.value)) - - for (const code of (result.barcodes || [])) { - if (barcodes.value.length >= 3) break - if (!existing.has(code)) { - existing.add(code) - barcodes.value.push({ value: code, region: 'photo' }) - found.push(code) - } - } - - // Tag the photo with found codes - const lastIdx = photos.value.length - 1 - if (lastIdx >= 0) photos.value[lastIdx].codes = found + const result = await scanBarcodesWithTimeout(aiImage, SCAN_TIMEOUT_MS) + found = mergeCodes(result.barcodes || [], 'photo') + photos.value[photoIdx].codes = found if (found.length === 0) { error.value = 'Aucun code détecté — rapprochez-vous ou améliorez la mise au point' } } catch (e) { - error.value = e.message || 'Erreur' + if (aiImage && isRetryable(e)) { + await offline.enqueueVisionScan({ image: aiImage }) + if (photos.value[photoIdx]) photos.value[photoIdx].queued = true + error.value = 'Réseau faible — scan en attente. Reprise automatique au retour du signal.' + } else { + error.value = e.message || 'Erreur' + } } finally { scanning.value = false } @@ -68,6 +111,25 @@ export function useScanner () { return found } + async function scanBarcodesWithTimeout (image, ms) { + return await Promise.race([ + scanBarcodes(image), + new Promise((_, reject) => setTimeout( + () => reject(new Error('ScanTimeout')), + ms, + )), + ]) + } + + function isRetryable (e) { + const msg = (e?.message || '').toLowerCase() + return msg.includes('scantimeout') + || msg.includes('failed to fetch') + || msg.includes('networkerror') + || msg.includes('load failed') + || e?.name === 'TypeError' // fetch throws TypeError on network error + } + /** * Resize an image file to a max dimension, return as base64 data URI. */ diff --git a/apps/field/src/pages/ScanPage.vue b/apps/field/src/pages/ScanPage.vue index 08932f8..107ba65 100644 --- a/apps/field/src/pages/ScanPage.vue +++ b/apps/field/src/pages/ScanPage.vue @@ -29,6 +29,13 @@ + +
| + | Nom | +Catégorie | +Applique à | +Trigger | +Étapes | +Actif | +Actions | +
|---|---|---|---|---|---|---|---|
|
+ {{ r.template_name }}
+
+ {{ r.name }}
+
+ |
+ {{ r.applies_to || '—' }} | +{{ triggerLabel(r.trigger_event) || '—' }} | +{{ r.step_count || 0 }} | +
+ |
+
+ |
+
{{ formatted(v.path) }}
+ | Nom | IP | MAC | Bail | Type |
|---|---|---|---|---|
| {{ l.hostname || '—' }} | +{{ l.ip }} |
+ {{ l.mac }} |
+ {{ l.expiry != null ? formatLease(l.expiry) : '—' }} | +
+ |
+
|
-
- {{ c.signal > 0 ? c.signal : '?' }}
+
+
+ {{ c.signal > 0 ? c.signal : '?' }}
+
+ |
{{ c.hostname || '—' }}
- {{ c.ip }}
+ {{ c.ip || 'hors ligne' }}
|
{{ c.meshNode || '—' }} |
@@ -387,7 +451,7 @@ import { ref, computed, onMounted, watch } from 'vue'
import { useQuasar } from 'quasar'
import InlineField from 'src/components/shared/InlineField.vue'
import { useDeviceStatus } from 'src/composables/useDeviceStatus'
-import { useWifiDiagnostic } from 'src/composables/useWifiDiagnostic'
+import { useModemDiagnostic } from 'src/composables/useModemDiagnostic'
import { deleteDoc } from 'src/api/erp'
const props = defineProps({ doc: { type: Object, required: true }, docName: String })
@@ -395,7 +459,7 @@ const emit = defineEmits(['deleted'])
const $q = useQuasar()
const { fetchStatus, fetchOltStatus, fetchPortContext, getDevice, isOnline, combinedStatus, signalQuality, refreshDeviceParams, fetchHosts, loading: deviceLoading } = useDeviceStatus()
-const { fetchDiagnostic, loading: wifiDiagLoading, error: wifiDiagError, data: wifiDiag } = useWifiDiagnostic()
+const { fetchDiagnostic, fetchDiagnosticAuto, loading: wifiDiagLoading, error: wifiDiagError, data: wifiDiag } = useModemDiagnostic()
const refreshing = ref(false)
const deleting = ref(false)
const portCtx = ref(null)
@@ -405,20 +469,44 @@ const managementIp = computed(() => {
return iface?.ip || null
})
-function runWifiDiagnostic() {
- if (!managementIp.value) return
+async function runWifiDiagnostic() {
+ const serial = props.doc?.serial_number
+ // Try auto-fetch first (credentials resolved server-side)
+ if (serial) {
+ const result = await fetchDiagnosticAuto(serial)
+ if (result) return
+ }
+ // Fallback: manual password entry on any auto-fetch failure
+ const ip = managementIp.value || props.doc?.ip_address
+ if (!ip) return
+ const errMsg = wifiDiagError.value || ''
$q.dialog({
title: 'Mot de passe modem',
- message: `Entrer le mot de passe superadmin pour ${managementIp.value}`,
+ message: errMsg
+ ? `Echec auto (${errMsg.substring(0, 80)}) — entrer le mot de passe pour ${ip}`
+ : `Entrer le mot de passe superadmin pour ${ip}`,
prompt: { model: '', type: 'password', filled: true },
cancel: { flat: true, label: 'Annuler' },
ok: { label: 'Lancer', color: 'primary' },
persistent: false,
}).onOk(pass => {
- if (pass) fetchDiagnostic(managementIp.value, pass)
+ if (pass) fetchDiagnostic(ip, pass)
})
}
+const MODEM_TYPE_LABELS = {
+ tplink_xx230v: 'TP-Link',
+ raisecom_boa: 'Raisecom 803-W',
+ raisecom_php: 'Raisecom 803-WS2',
+}
+const MODEM_TYPE_COLORS = {
+ tplink_xx230v: 'teal',
+ raisecom_boa: 'orange',
+ raisecom_php: 'deep-purple',
+}
+const modemTypeLabel = (type) => MODEM_TYPE_LABELS[type] || type
+const modemTypeColor = (type) => MODEM_TYPE_COLORS[type] || 'grey'
+
const wanRoleLabel = (role) => ROLE_LABELS[role] || role
function maskToCidr(mask) {
@@ -438,6 +526,13 @@ function backhaulColor(signal) {
return '#f87171'
}
+function formatBytes(bytes) {
+ if (!bytes || bytes <= 0) return '0 B'
+ const units = ['B', 'KB', 'MB', 'GB']
+ const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
+ return (bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0) + ' ' + units[i]
+}
+
function confirmDelete () {
$q.dialog({
title: 'Supprimer cet équipement ?',
@@ -683,4 +778,5 @@ watch(() => props.doc.serial_number, sn => {
.adv-signal-bar { width: 40px; height: 6px; background: #e5e7eb; border-radius: 3px; display: inline-block; vertical-align: middle; margin-right: 4px; overflow: hidden; }
.adv-signal-fill { height: 100%; border-radius: 3px; transition: width 0.3s; }
.adv-clients-table td:first-child { white-space: nowrap; }
+.adv-inactive-row { opacity: 0.5; font-style: italic; }
diff --git a/apps/ops/src/components/shared/detail-sections/InvoiceDetail.vue b/apps/ops/src/components/shared/detail-sections/InvoiceDetail.vue
index 67c0b1d..527eef6 100644
--- a/apps/ops/src/components/shared/detail-sections/InvoiceDetail.vue
+++ b/apps/ops/src/components/shared/detail-sections/InvoiceDetail.vue
@@ -1,4 +1,25 @@
+
+
+
+
+
+
+
+
+
@@ -82,8 +103,31 @@
+
+
+
+
+
+{{ mc.content }}
+
+
+
+
+
+
+ {{ job.subject || 'Travail' }}
+ •
+ {{ job.duration || 1 }}h
+ •
+ {{ job.address }}
+
+ Recherche des meilleures plages…
+ {{ errorMsg }}
+
+
+ {{ relativeDate(grp.date) }}
+
+
+
+
+
+
+ Aucun contrat de service
+
+
+
+
+
+ Aucune soumission
@@ -836,6 +906,9 @@
+
+
+
+ {{ total.toLocaleString() }} tickets
+
La validation de l'adresse fibre n'a pas été faite. Recommence. ");
+
+ $q_fibre_info = "SELECT `terrain`, `rue`, `ville` FROM `fibre` WHERE `id` = '{$_POST['fibre_id']}'";
+ $res_fibre_info = $sql->query($q_fibre_info);
+ $row_fibre_info = $res_fibre_info->fetch_array();
+
+ if($res_fibre_info->num_rows != 1) die("Adresse fibre introuvé ou non unique ");
+
+ }
+
+
+ $q_delivery = "UPDATE `$mydbName`.`delivery` SET `email` = '{$_POST['email']}' WHERE `id` = '{$_POST['delivery_id']}'";
+ $sql->query($q_delivery);
+
+ #print_r($_POST); echo ""; + + $date = mktime(0,0,0,date('n'),date('d'),date('Y')); + $time_now = time(); + + $subject = change_quote($_POST['city'] . " | " . $_POST['customer_name']); + if($_POST['select_install'] == 584) $subject = change_quote("Lac des pins | " . $_POST['customer_name']); + + $tmp = str_split($_POST['customer_id'],5); + $customer_id = (is_numeric($_POST['customer_id'])) ? implode(' ',$tmp) : $_POST['customer_id']; + + $ticket_msg = "Nom: ".$_POST['customer_name']."\nID: {$customer_id}\nEmail: {$_POST['email']}\n\n"; + $ticket_msg .= "Adresse: " . "{$_POST['address']} {$_POST['city']}" . "\nTéléphone: {$_POST['phone']}\n\n"; + + + $d = explode('-',$_POST['date_install']); + $date = mktime(0,0,0,$d[1],$d[0],$d[2]); + + + $d = explode('-',$_POST['date_invoice']); + $date_invoice = mktime(0,0,0,$d[1],$d[0],$d[2]); + + + ## block install + + if(isset($_POST['chk_install'])){ + + + $ticket_assign = 3301; + + if($_POST['fibre'] == 0) + $ticket_dept = 12; + else + $ticket_dept = 27; + + if($_POST['select_install'] == 584) $ticket_dept = 27; //camping + + $ticket_msg .= "Installation: {$_POST['install_price']}\n"; + if($_POST['install_credit'] != '') $ticket_msg .= "Crédit: {$_POST['install_credit']}$\n"; + + $ticket_msg .= "\n"; + + } + + $aCombo = array(); + + ## block service + + $service_list = ""; + + if(isset($_POST['chk_product'])){ + + $sku = $_POST['sku']; //product id... not sku -_-' + $desc = $_POST['desc']; + $amount = $_POST['amount']; + $quota_day = $_POST['quota_day']; + $quota_night = $_POST['quota_night']; + $contrat = $_POST['contrat']; + $duration = $_POST['duration']; + + + $ticket_msg .= "Service: \n"; + + $count_service = 0; + $fi_count = 0; + + $premier_fi_id = 0; + + $set_comm = 0; + + foreach($sku AS $i=>$value){ + + //$sku[$i] -- $desc[$i] -- $amount[$i] -- $contrat[$i] -- $duration[$i] + + if($sku[$i] == 'default') continue; + + $res_prod_comm = $sql->query("SELECT * FROM `product` WHERE `id` = '{$sku[$i]}'"); + $row_prod_comm = $res_prod_comm->fetch_array(); + if($row_prod_comm['commercial'] == 1) $set_comm = 1; + + if($row_prod_comm['combo_ready'] AND $amount[$i] > 0) $aCombo['internet'] = 1; + + + $result_pID = $sql->query("SHOW TABLE STATUS LIKE 'service';"); + $row_pID = $result_pID->fetch_array(); + $predic_service_id = $row_pID['Auto_increment']; + + $fi = ($sku[$i] == '7' OR $sku[$i] == '8' OR $sku[$i] == '88' OR $sku[$i] == '117') ? 0 : 1; + + $price = ($sku[$i] == '7' OR $sku[$i] == '88') ? abs($amount[$i]) * -1 : $amount[$i]; + + if($contrat[$i] != ''){ + $contract_month = $contrat[$i]; + $date_end_contract = date("U", strtotime("+$contract_month months",$date_invoice)); + } + else + $date_end_contract = 'NULL'; + + if($duration[$i] != ''){ + $actif_month = $duration[$i]; + $date_actif_until = mktime(0,0,0,date("n", strtotime("+$actif_month months",$date_invoice)),1,date("Y", strtotime("+$actif_month months",$date_invoice))); + } + else + $date_actif_until = 'NULL'; + + $day = $quota_day[$i] * 1073741824; + $night = $quota_night[$i] * 1073741824; + + $hijack = $_POST['hij'][$i]; + + $raduser = $radpwd = ''; + $radconso = 0; + if($price >= 0){ + if($row_prod_comm['type'] == 1 or $row_prod_comm['type'] == 2){ + $raduser = "tci$predic_service_id"; + for($p=1; $p<=8; $p++){ + $radpwd.=rand(0,9); + } + } + } + + if($raduser != ''){ + $q_radcheck = "INSERT INTO `radcheck` (`username`,`attribute`,`op`,`value`) VALUES ('$raduser','Cleartext-Password',':=','$radpwd')"; + $q_radgroup = "INSERT INTO `radusergroup` (`username`,`groupname`,`priority`) VALUES ('$raduser','residentiel','1')"; + + + $sql_vpnradius = new mysqli('10.5.2.25', 'facturation', 'N0HAk4u$', 'radiusdb'); + $sql_vpnradius->query($q_radcheck); + $sql_vpnradius->query($q_radgroup); + $sql_vpnradius->close(); + + $radconso = 1; + } + + $recurrence = ($_POST['select_install'] == 584) ? 5 : 2; + + $q_prod = "INSERT INTO `$mydbName`.`service` (`date_orig`, `date_next_invoice`, `delivery_id`, `product_id`, `payment_recurrence`, `status`, `hijack`, `hijack_price`, `hijack_desc`, `hijack_quota_day`, `hijack_quota_night`, `date_end_contract`, `actif_until`, `forfait_internet`, `radius_user`, `radius_pwd`, `radius_conso`) VALUES "; + $q_prod .= "('$date','$date_invoice','{$_POST['delivery_id']}','{$sku[$i]}','$recurrence','0','$hijack','$price','".change_quote($desc[$i])."','$day','$night',$date_end_contract,$date_actif_until,'$fi','$raduser','$radpwd','$radconso')"; + $sql->query($q_prod); + #echo "$q_prod "; + + if($fi){ + + $snap_date = mktime(0,0,0,date('n'),1,date('Y')); + $q_insert = "INSERT INTO `$mydbName`.`service_snapshot` (`date`, `account_id`, `service_id`, `quota_day`, `quota_night`) VALUES ('$snap_date','{$_POST['account_id']}','$predic_service_id','$day','$night');"; + $sql->query($q_insert); + + if($fi_count == 0){ + + if($_POST['client_password'] == ''){ + $cli_password = enchsetenev("targo$predic_service_id",10000); + + $q_update_password = "UPDATE `$mydbName`.`account` SET `password` = '$cli_password' WHERE `id` = '{$_POST['account_id']}'"; + $sql->query($q_update_password); + } + + $premier_fi_id = $predic_service_id; + $fi_count++; + + } + } + + $service_list .= "$predic_service_id;"; + + $ticket_msg .= "ID: $predic_service_id, {$desc[$i]}, $price $, {$quota_day[$i]} / {$quota_night[$i]} go."; + + if($contrat[$i] != '') $ticket_msg .= "Fin du contrat: " . date("d-m-Y", $date_end_contract) . ". "; + if($duration[$i] != '') $ticket_msg .= "Actif jusqu'au: " . date("d-m-Y", $date_actif_until) . ". "; + if($raduser != '') $ticket_msg .= "\n - Radius: $raduser / $radpwd"; + + $ticket_msg .= "\n"; + + + if($count_service == 0){ + + $sql_cp = new mysqli($cpHost, $cpUser, $cpPass, $cpName); + + $query_cp = "INSERT INTO `cportal`.`valid_user` (`customer_id`, `service_id`) VALUES ('{$_POST['customer_id']}', '$predic_service_id');"; + $sql_cp->query($query_cp); + + $sql_cp->close(); + + $count_service++; + } + + + } + + if($set_comm == 1) $sql->query("UPDATE `account` SET `commercial` = 1 WHERE `id` = '{$_POST['account_id']}'"); + + $ticket_msg .= "\n"; + + } + + + ## block phone + + if(isset($_POST['chk_phone'])){ + + $aCombo['phone'] = 1; + + $q_tel = "SELECT `name` FROM `$mydbName`.`product_translate` WHERE `product_id` = '54' AND `language_id` = 'francais'"; + $res_tel = $sql->query($q_tel); + $row_tel = $res_tel->fetch_array(); + + $result_pID = $sql->query("SHOW TABLE STATUS LIKE 'service';"); + $row_pID = $result_pID->fetch_array(); + $predic_service_id = $row_pID['Auto_increment']; + $service_list .= "$predic_service_id;"; + + $q_prod = "INSERT INTO `$mydbName`.`service` (`date_orig`, `date_next_invoice`, `delivery_id`, `product_id`, `payment_recurrence`, `status`, `hijack`, `hijack_price`, `hijack_desc`, `hijack_quota_day`, `hijack_quota_night`, `date_end_contract`, `actif_until`, `forfait_internet`) VALUES "; + $q_prod .= "('$date','$date_invoice','{$_POST['delivery_id']}','54','2','0','1','{$_POST['price_tel_men']}','{$row_tel['name']}','0','0',NULL,NULL,'0')"; + $sql->query($q_prod); + + + + #echo "$q_prod "; + + if(isset($_POST['chk_tel_trans'])){ + + $trans_num = str_replace('-','',$_POST['trans_number']); + $trans_num = str_replace(' ','',$trans_num); + $trans_num = str_replace('(','',$trans_num); + $trans_num = str_replace(')','',$trans_num); + + $q_serv_tel = "INSERT INTO `$mydbName`.`phone` (`service_id`, `phone_num`) VALUES ('$predic_service_id','$trans_num');"; + $sql->query($q_serv_tel); + } + + $ticket_msg .= "Téléphone:\n"; + $ticket_msg .= "Prix: {$_POST['price_tel_men']}$ "; + + if($_POST['credit_telepmens'] != 0){ + $price = abs($_POST['credit_telepmens']) * -1; + + if($_POST['credit_telepmens_time'] != ''){ + $actif_month = $_POST['credit_telepmens_time']; + $date_actif_until = mktime(0,0,0,date("n", strtotime("+$actif_month months",$date_invoice)),1,date("Y", strtotime("+$actif_month months",$date_invoice))); + } + else + $date_actif_until = 'NULL'; + + $result_pID = $sql->query("SHOW TABLE STATUS LIKE 'service';"); + $row_pID = $result_pID->fetch_array(); + $predic_service_id = $row_pID['Auto_increment']; + $service_list .= "$predic_service_id;"; + + $q_prod = "INSERT INTO `$mydbName`.`service` (`date_orig`, `date_next_invoice`, `delivery_id`, `product_id`, `payment_recurrence`, `status`, `hijack`, `hijack_price`, `hijack_desc`, `hijack_quota_day`, `hijack_quota_night`, `date_end_contract`, `actif_until`, `forfait_internet`) VALUES "; + $q_prod .= "('$date','$date_invoice','{$_POST['delivery_id']}','54','2','0','1','$price','Crédit','0','0',NULL,$date_actif_until,'0')"; + $sql->query($q_prod); + #echo "$q_prod "; + + $ticket_msg .= " Crédit de " . abs($_POST['credit_telepmens']) . "$ durant {$_POST['credit_telepmens_time']} mois."; + + } + + $q_prod_911 = "INSERT INTO `$mydbName`.`service` (`date_orig`, `date_next_invoice`, `delivery_id`, `product_id`, `payment_recurrence`, `status`, `date_end_contract`, `actif_until`, `forfait_internet`) VALUES "; + $q_prod_911 .= "('$date','$date_invoice','{$_POST['delivery_id']}','52','2','0',NULL,NULL,'0')"; + $sql->query($q_prod_911); + $id911 = $sql->insert_id; + $service_list .= "$id911;"; + + $ticket_msg .= "\n"; + + if(isset($_POST['chk_tel_act'])) $ticket_msg .= "Activation: {$_POST['price_tel_act']}$ \n"; + + if($_POST['opt_num'] == 'nouveau') + $ticket_msg .= "Nouveau numéro - préférence: {$_POST['opt_pref']} (ne pas oublier d'associer le numéro avec le service lorsque connu)\n"; + else + $ticket_msg .= "Porter le numéro ({$_POST['trans_number']}): {$_POST['price_tel_trans']}$ \n"; + + + $ticket_msg .= "\n"; + } + + + ## block tele + + if(isset($_POST['chk_tele'])){ + + + $sku_tele = $_POST['t_sku']; //product id... not sku -_-' + $desc_tele = $_POST['t_desc']; + $amount_tele = $_POST['t_amount']; + $contrat_tele = $_POST['t_contrat']; + $duration_tele = $_POST['t_duration']; + + $ticket_msg .= "Tele: \n"; + + foreach($sku_tele AS $i=>$value){ + + if($sku_tele[$i] == 'default') continue; + + $q_prod_tele = "SELECT * FROM `product` WHERE `id` = {$sku_tele[$i]}"; + $res_prod_tele = $sql->query($q_prod_tele); + $row_prod_tele = $res_prod_tele->fetch_array(); + + if($row_prod_tele['combo_ready']) $aCombo['tele'] = 1; + + $result_pID = $sql->query("SHOW TABLE STATUS LIKE 'service';"); + $row_pID = $result_pID->fetch_array(); + $predic_service_id = $row_pID['Auto_increment']; + + + + $price = $amount_tele[$i]; + + if($contrat_tele[$i] != ''){ + $contract_month = $contrat_tele[$i]; + $date_end_contract = date("U", strtotime("+$contract_month months",$date_invoice)); + } + else + $date_end_contract = 'NULL'; + + if($duration_tele[$i] != ''){ + $actif_month = $duration_tele[$i]; + $date_actif_until = mktime(0,0,0,date("n", strtotime("+$actif_month months",$date_invoice)),1,date("Y", strtotime("+$actif_month months",$date_invoice))); + } + else + $date_actif_until = 'NULL'; + + $hijack = $_POST['t_hij'][$i]; + + $q_prod = "INSERT INTO `$mydbName`.`service` (`date_orig`, `date_next_invoice`, `delivery_id`, `product_id`, `payment_recurrence`, `status`, `hijack`, `hijack_price`, `hijack_desc`, `hijack_quota_day`, `hijack_quota_night`, `date_end_contract`, `actif_until`, `forfait_internet`, `radius_user`, `radius_pwd`) VALUES "; + $q_prod .= "('$date','$date_invoice','{$_POST['delivery_id']}','{$sku_tele[$i]}','2','0','$hijack','$price','".change_quote($desc_tele[$i])."','0','0',$date_end_contract,$date_actif_until,'0','','')"; + $sql->query($q_prod); + #echo "$q_prod "; + + + + $service_list .= "$predic_service_id;"; + + $ticket_msg .= "ID: $predic_service_id, {$desc_tele[$i]}, $price $. "; + + if($contrat_tele[$i] != '') $ticket_msg .= "Fin du contrat: " . date("d-m-Y", $date_end_contract) . ". "; + if($duration_tele[$i] != '') $ticket_msg .= "Actif jusqu'au: " . date("d-m-Y", $date_actif_until) . ". "; + + $ticket_msg .= "\n"; + + } + $ticket_msg .= "\n"; + } + + + ## add combo prod + + switch(count($aCombo)){ + case 2: $combo_id = '556'; $combo_sku = 'RAB2X'; break; + case 3: $combo_id = '557'; $combo_sku = 'RAB3X'; break; + case 4: $combo_id = '558'; $combo_sku = 'RAB4X'; break; + default: $combo_id = ''; + } + + if($combo_id != ''){ + + $result_pID = $sql->query("SHOW TABLE STATUS LIKE 'service';"); + $row_pID = $result_pID->fetch_array(); + $predic_service_id = $row_pID['Auto_increment']; + + $q_combo = "INSERT INTO `$mydbName`.`service` (`date_orig`, `date_next_invoice`, `delivery_id`, `product_id`, `payment_recurrence`, `status`) VALUES "; + $q_combo .= "('$date','$date_invoice','{$_POST['delivery_id']}','$combo_id','2','0')"; + $sql->query($q_combo); + + $service_list .= "$predic_service_id;"; + $ticket_msg .= "Ajout de $combo_sku \n"; + + } + + $ticket_msg .= "\n" . $_POST['info_supp']; + + $service_list = substr($service_list,0,-1); + + $result_aID = $sql->query("SHOW TABLE STATUS LIKE 'bon_travail';"); + $row_aID = $result_aID->fetch_array(); + $bon_id = $row_aID['Auto_increment']; + + $q_bon = "INSERT INTO `$mydbName`.`bon_travail` (`date`, `account_id`) VALUES ('$date', '{$_POST['account_id']}');"; + $sql->query($q_bon); + + $wiz_fibre = ""; + $info_fibre = ""; + if($_POST['fibre'] == 1){ + $wiz_fibre = "{$_POST['delivery_id']}|{$_POST['fibre_id']}|$premier_fi_id"; + + $q_fibre_info = "SELECT * FROM `fibre` WHERE `id` = '{$_POST['fibre_id']}'"; + $res_fibre_info = $sql->query($q_fibre_info); + $row_fibre_info = $res_fibre_info->fetch_array(); + + switch($row_fibre_info['tech']){ + case 2: $tech_fibre = "Raisecom"; break; + case 3: $tech_fibre = "TP-link"; break; + default: $tech_fibre = "tech inconnu"; + } + + $info_fibre = "\n\n Info pour fibre:\n$tech_fibre\nOLT: {$row_fibre_info['info_connect']}\nVille: {$row_fibre_info['ville']}\nRue: {$row_fibre_info['rue']}\nCivique: {$row_fibre_info['terrain']} "; + + //ticket tech pour installer le boitier + if($row_fibre_info['boitier_pas_install'] == 1){ + + $date_boitier = date('U',strtotime('+1 day')); + $q_ticket_boitier = "INSERT INTO `ticket` (`account_id`, `subject`, `dept_id`, `open_by`, `assign_to`, `status`, `date_create`, `last_update`, `due_date`) VALUES "; + $q_ticket_boitier .= "('{$_POST['account_id']}', '{$row_fibre_info['ville']} | Installation boitier', '27', '$userid', '3301', 'open', $time_now, $time_now, $date_boitier)"; + $sql->query($q_ticket_boitier); + $ticket_boitier = $sql->insert_id; + $msg_boitier = "Installation pour un client au {$row_fibre_info['terrain']} {$row_fibre_info['rue']} {$row_fibre_info['ville']} prevu pour le " . date('d-m-Y', $date); + $msg_boitier .= "\nLe boitier a besoin d'être fait au préalable.\nNe pas oublier de mettre a jour la map pour les adresses affectées par cette intervention"; + $msg_boitier = $sql->real_escape_string($msg_boitier); + $q_boitier_msg = "INSERT INTO `ticket_msg` (`ticket_id`, `staff_id`, `msg`, `date_orig`) VALUES ('$ticket_boitier','$userid','$msg_boitier','$time_now');"; + $sql->query($q_boitier_msg); + } + } + + $ticket_msg .= $info_fibre; + + $result_aID = $sql->query("SHOW TABLE STATUS LIKE 'ticket';"); + $row_aID = $result_aID->fetch_array(); + $ticket_id = $row_aID['Auto_increment']; + + $q_follow = "SELECT * FROM `ticket_dept` WHERE `id` = $ticket_dept"; + $res_follow = $sql->query($q_follow); + $row_follow = $res_follow->fetch_assoc(); + + if($row_follow['default_follow'] == 0) + $follow = "[]"; + else{ + $thisFollower[$row_follow['default_follow']]['child'] = 0; + $follow = json_encode($thisFollower); + } + + + if($_POST['fibre'] > 0){ + + $q_ticket_fact = "INSERT INTO `ticket` (`account_id`, `subject`, `dept_id`, `open_by`, `assign_to`, `status`, `date_create`, `last_update`) VALUES "; + $q_ticket_fact .= "('{$_POST['account_id']}', 'validation paiement installation', '2', '$userid', '0', 'open', $time_now, $time_now)"; + $sql->query($q_ticket_fact); + $ticket_fact = $sql->insert_id; + + $msg_fact = $sql->real_escape_string("Fermer le ticket pour activer le ticket d'installation"); + $q_fac_msg = "INSERT INTO `ticket_msg` (`ticket_id`, `staff_id`, `msg`, `date_orig`) VALUES ('$ticket_fact','$userid','$msg_fact','$time_now');"; + $sql->query($q_fac_msg); + + $q_ticket = "INSERT INTO `$mydbName`.`ticket` (`account_id`, `subject`, `dept_id`, `open_by`, `assign_to`, `status`, `waiting_for`, `parent`, `due_date`, `due_time`, `date_create`, `last_update`, `wizard`, `wizard_fibre`, `bon_id`, `followed_by`) VALUES "; + $q_ticket .= "('{$_POST['account_id']}', '$subject', '$ticket_dept', '$userid', '$ticket_assign', 'pending', '$ticket_fact', '$ticket_fact', '$date', '{$_POST['date_time']}', $time_now, $time_now, '$service_list', '$wiz_fibre', '$bon_id', '$follow')"; + } + else{ + $q_ticket = "INSERT INTO `$mydbName`.`ticket` (`account_id`, `subject`, `dept_id`, `open_by`, `assign_to`, `status`, `due_date`, `due_time`, `date_create`, `last_update`, `wizard`, `wizard_fibre`, `bon_id`, `followed_by`) VALUES "; + $q_ticket .= "('{$_POST['account_id']}', '$subject', '$ticket_dept', '$userid', '$ticket_assign', 'open', '$date', '{$_POST['date_time']}', $time_now, $time_now, '$service_list', '$wiz_fibre', '$bon_id', '$follow')"; + } + + + + //$q_ticket = "INSERT INTO `$mydbName`.`ticket` (`account_id`, `subject`, `dept_id`, `open_by`, `assign_to`, `status`, `due_date`, `due_time`, `date_create`, `last_update`, `wizard`, `wizard_fibre`, `bon_id`, `followed_by`) VALUES "; + //$q_ticket .= "('{$_POST['account_id']}', '$subject', '$ticket_dept', '$userid', '$ticket_assign', 'open', '$date', '{$_POST['date_time']}', $time_now, $time_now, '$service_list', '$wiz_fibre', '$bon_id', '$follow')"; + $sql->query($q_ticket); + $ticket_id = $sql->insert_id; + + $ticket_msg = change_quote($ticket_msg); + $q_ticket_msg = "INSERT INTO `$mydbName`.`ticket_msg` (`ticket_id`, `staff_id`, `msg`, `date_orig`, `unread_csv`, `public`) VALUES ('$ticket_id','$userid','$ticket_msg','$time_now','$unread_csv','0')"; + $sql->query($q_ticket_msg); + + if(isset($_POST['chk_phone'])){ + + $result_aID = $sql->query("SHOW TABLE STATUS LIKE 'ticket';"); + $row_aID = $result_aID->fetch_array(); + $ticket_id_t = $row_aID['Auto_increment']; + + $q_ticket = "INSERT INTO `ticket` (`parent`, `account_id`, `subject`, `dept_id`, `open_by`, `assign_to`, `status`, `due_date`, `due_time`, `date_create`, `last_update`, `wizard`) VALUES "; + $q_ticket .= "('$ticket_id','{$_POST['account_id']}', 'Configuration boitier tel', '23', '$userid', '4661', 'open', '$date', '{$_POST['date_time']}', $time_now, $time_now, '$service_list')"; + $sql->query($q_ticket); + $ticket_id_t = $sql->insert_id; + + $q_ticket_msg = "INSERT INTO `ticket_msg` (`ticket_id`, `staff_id`, `msg`, `date_orig`, `unread_csv`, `public`) VALUES ('$ticket_id_t','$userid','voir parent','$time_now','$unread_csv','0')"; + $sql->query($q_ticket_msg); + } + + + if(isset($_FILES['uploadedfile'])){ + $base_path = "uploads/ticket/$ticket_id/"; + if(!is_dir("uploads/ticket/$ticket_id")) mkdir("uploads/ticket/$ticket_id"); + + $q = "SELECT `attachment_ext` FROM `compta_setup`"; + $res = $sql->query($q); + $list_attachement = $res->fetch_array(); + $list_attachement = $list_attachement[0]; + + for($i=0; $i < count($_FILES['uploadedfile']['name']); $i++){ + $filename = basename( $_FILES['uploadedfile']['name'][$i]); + $filename = getRewriteString($filename); + $target_path = $base_path . $filename; + + $tmp = explode('.',$target_path); + $ext = strtolower($tmp[count($tmp)-1]); + + if(stripos($list_attachement,$ext) !== false){ + copy($_FILES['uploadedfile']['tmp_name'][$i], $target_path); + } + } + } + + + $log_cf = file_get_contents('/targo/facturation/logs/log'); + $log_cf .= "[" . date('D d M Y H:i:s') . "] [ACCOUNT : WIZARD] <$staff_username> Wizard pour l'adresse {$_POST['delivery_id']} - ticket #$ticket_id \n"; + file_put_contents('/targo/facturation/logs/log', $log_cf); + + + $_POST['wiz_account'] = $_POST['account_id']; + $_POST['wiz_delivery'] = $_POST['delivery_id']; + + echo ""; + +} + + +$account_id = $_POST['wiz_account']; +$delivery_id = $_POST['wiz_delivery']; + + +$q_account = "SELECT * FROM `$mydbName`.`account` WHERE `id` = '$account_id'"; +$res_account = $sql->query($q_account); +$row_account = $res_account->fetch_array(); + +$q_delivery = "SELECT * FROM `$mydbName`.`delivery` WHERE `id` = '$delivery_id'"; +$res_delivery = $sql->query($q_delivery); +$row_delivery = $res_delivery->fetch_array(); + + + +## Telephonie +//sku phone mensu: TELEPMENS +//sku activation: ACTTELEP +//sku transfert: TELEPTRANS +$res_tel_prod = $sql->query("SELECT `price` FROM `product` WHERE `sku` IN ('ACTTELEP','TELEPMENS','TELEPTRANS')" ); +$row_tel_prod = $res_tel_prod->fetch_array(); +$price_activ = $row_tel_prod['price']; +$row_tel_prod = $res_tel_prod->fetch_array(); +$price_tel = $row_tel_prod['price']; +$row_tel_prod = $res_tel_prod->fetch_array(); +$price_transf = $row_tel_prod['price']; +#echo "$price_tel -- $price_activ -- $price_transf "; + +//placeholder prix telephonie en fibre. +$price_tel_fibre = '24.95'; +$price_activ_fibre = '0'; +$price_transf_fibre = '0'; + + + +$q_install = "SELECT `id`, `price` FROM `$mydbName`.`product` WHERE `id` IN (42,43,351,362,584) ORDER BY `id`"; +$res_install = $sql->query($q_install); +$row_install = $res_install->fetch_array(); +$option_install = ""; +$row_install = $res_install->fetch_array(); +$option_install .= ""; +$install_value = $row_install['price']; +$row_install = $res_install->fetch_array(); +$option_install .= ""; +$row_install = $res_install->fetch_array(); +$option_install .= ""; +$row_install = $res_install->fetch_array(); +$option_install .= ""; + +#product + +$prod_blacklist = array(556,557,558, 549,551,553); //ne jamais permettre de choisir ces produits + +$res_product = $sql->query("SELECT * FROM `$mydbName`.`product` WHERE `active` = '1' AND `category` IN (4,23,32) ORDER BY `sku`"); +$option_product = ""; +while($row_product = $res_product->fetch_array()){ + if(in_array($row_product['id'],$prod_blacklist)) continue; + $option_product .= ""; +} + + +$res_product = $sql->query("SELECT * FROM `$mydbName`.`product` WHERE `active` = '1' AND `category` IN (4,23,32) AND `type` != 2 AND `commercial` = 0 ORDER BY `sku`"); +$opt_prod_sfres = ""; +while($row_product = $res_product->fetch_array()){ + if(in_array($row_product['id'],$prod_blacklist)) continue; + $opt_prod_sfres .= ""; +} + +$res_product = $sql->query("SELECT * FROM `$mydbName`.`product` WHERE `active` = '1' AND `category` IN (4,23,32) AND ((`type` = 1 AND `commercial` = 1) OR `type` NOT IN (1,2)) ORDER BY `sku`"); +$opt_prod_sfcom = ""; +while($row_product = $res_product->fetch_array()){ + if(in_array($row_product['id'],$prod_blacklist)) continue; + $opt_prod_sfcom .= ""; +} + +$res_product = $sql->query("SELECT * FROM `$mydbName`.`product` WHERE `active` = '1' AND `category` IN (4,23,26,32) AND `type`!= 1 AND `commercial` = 0 ORDER BY `sku`"); +$opt_prod_fibres = ""; +while($row_product = $res_product->fetch_array()){ + if(in_array($row_product['id'],$prod_blacklist)) continue; + $opt_prod_fibres .= ""; +} + +$res_product = $sql->query("SELECT * FROM `$mydbName`.`product` WHERE `active` = '1' AND `category` IN (4,23,26,32) AND ((`type` = 2 AND `commercial` = 1) OR `type` NOT IN (1,2)) ORDER BY `sku`"); +$opt_prod_fibcom = ""; +while($row_product = $res_product->fetch_array()){ + if(in_array($row_product['id'],$prod_blacklist)) continue; + $opt_prod_fibcom .= ""; +} + +## Tele +$res_tele = $sql->query("SELECT * FROM `$mydbName`.`product` WHERE `active` = '1' AND `category` = 33 AND `type` = 4 ORDER BY `sku`"); +$option_tele = ""; +while($row_tele = $res_tele->fetch_array()){ + if($row_tele['id'] == 561) continue; //package de base, a eviter + $option_tele .= ""; +} + +$option_tele .= ""; + + + +## fibre id - tente de trouver le id dans la table fibre. + +$civic = explode(' ',$row_delivery['address1'],2); +$rue = trim($civic[1]); +$civic = trim(str_replace(',','',$civic[0])); + +$zip = str_replace(' ','',$row_delivery['zip']); + +$q_fibre = "SELECT * FROM `fibre` WHERE `zip` = '$zip' AND `terrain` = '$civic'"; +$res_fibre = $sql->query($q_fibre); + +$fibre_find = 0; +$fibre_result = ''; + +if($res_fibre->num_rows > 0){ + $fibre_result .= "Addresse disponible (UN option DOIT être selectionné): Si vous croyez que l'adresse devrait être disponible, valider avec Dominique ou Pierre pour vous assurer que l'adresse a bien été ajoutée via la map infrastructure.
Aucune adresse trouvée. Non disponible. ";
+
+?>
+
+
+
+
+
+
+Installation Client Wizard+ ++ + + + + + + + + + + + diff --git a/docs/legacy-wizard/account_wizard_ajax.php b/docs/legacy-wizard/account_wizard_ajax.php new file mode 100644 index 0000000..2e95657 --- /dev/null +++ b/docs/legacy-wizard/account_wizard_ajax.php @@ -0,0 +1,51 @@ +real_escape_string(str_replace(' ','',$_POST['cp'])); + $civic = $sql->real_escape_string($_POST['civic']); + + $q_fibre = "SELECT * FROM `fibre` WHERE `zip` = '$zip' AND `terrain` = '$civic'"; + $res_fibre = $sql->query($q_fibre); + + $fibre_find = 0; + $fibre_result = ''; + + if($res_fibre->num_rows > 0){ + $fibre_result .= "Addresse disponible (UN option DOIT être selectionné):
Aucune adresse trouvée. Non disponible. ";
+
+ echo $fibre_result;
+
+}
+
+
+?>
\ No newline at end of file
diff --git a/docs/legacy-wizard/tele_wizard_package.php b/docs/legacy-wizard/tele_wizard_package.php
new file mode 100644
index 0000000..56a08f1
--- /dev/null
+++ b/docs/legacy-wizard/tele_wizard_package.php
@@ -0,0 +1,250 @@
+"; print_r($_POST); echo "";
+
+/*
+$_POST['tw_delivery_id'] = 2;
+$_POST['tw_sub_id'] = 3169;
+*/
+
+$q_delivery = "SELECT * FROM `delivery` WHERE `id` = {$_POST['tw_delivery_id']}";
+$res_delivery = $sql->query($q_delivery);
+$row_delivery = $res_delivery->fetch_assoc();
+
+$q_account = "SELECT * FROM `account` WHERE `id` = {$row_delivery['account_id']}";
+$res_account = $sql->query($q_account);
+$row_account = $res_account->fetch_assoc();
+
+
+if(isset($_POST['date_due'])){
+
+
+
+ $time = time();
+ $date_due = explode('-',$_POST['date_due']);
+ $date_due = mktime(0,0,0,$date_due[1],$date_due[0],$date_due[2]);
+ $aid = $_POST['tw_account_id'];
+ $did = $_POST['tw_delivery_id'];
+ $nb = $_POST['nb_stb'];
+ $credit = (isset($_POST['chk_credit'])) ? 1 : 0;
+ $fbase = $_POST['forfbase'];
+ $fthem = (isset($_POST['theme_forfait'])) ? json_encode($_POST['theme_forfait']) : '';
+
+
+ //die("$fthem -- {$row_account['customer_id']}");
+
+ $subject = change_quote("{$row_delivery['city']} | [TELE $nb STB] {$row_delivery['name']}");
+ $msg = '';
+
+
+ $q_ticket = "INSERT INTO `ticket` (`account_id`, `subject`, `dept_id`, `open_by`, `assign_to`, `due_date`, `date_create`, `last_update`) VALUES ($aid, '$subject', 41, $userid, 3301, '$date_due','$time','$time')";
+ if($sql->query($q_ticket)){
+
+ $ticket_id = $sql->insert_id;
+
+ $q_wiz = "INSERT INTO `tele_wiz` (`account_id`, `delivery_id`, `ticket_id`, `nb_stb`, `credit`, `fbase`, `fthem`) VALUES ($aid,$did,$ticket_id,$nb,$credit,$fbase,'$fthem')";
+ $sql->query($q_wiz);
+ $wiz_id = $sql->insert_id;
+
+ $msg = $_POST['memo'] . "Activer le(s) STB en cliquant ici"; + $msg = $sql->real_escape_string($msg); + + $q_ticket_msg = "INSERT INTO `ticket_msg` (`ticket_id`, `staff_id`, `msg`, `date_orig`) VALUES ($ticket_id,$userid,'$msg','$time')"; + $sql->query($q_ticket_msg); + + + if($_POST['opt_carte'] == 1){ + //next step chaine a la carte + die(" + "); + } + else{ + die(""); + } + + + + ##echo " $q_ticket"; + + } + +} + + + + +$option_base = ""; + +$q_forfait = "SELECT `product`.`id`,`product`.`sku`, `product_translate`.`name` FROM `product` LEFT JOIN `product_translate` ON `product_translate`.`product_id` = `product`.`id` WHERE `product`.`type` = '4' AND `product_translate`.`language_id` = 'francais' AND `product`.`active` = 1"; +$res_forfait = $sql->query($q_forfait); +$option_forfait = ""; +$black_list = array(629,634,635,636); +while($row_forfait = $res_forfait->fetch_assoc()){ + + if(in_array($row_forfait['id'],$black_list)) continue; + + $option_forfait .= ""; + +} + +$memo = "# Client:{$row_account['customer_id'] }\nNom: {$row_delivery['name']}\nAdresse: {$row_delivery['address1']}\nVille: {$row_delivery['city']}\nCode Postal:{$row_delivery['zip']}\nTéléphone: {$row_delivery['tel_home']}\nCell: {$row_delivery['cell']}\nEmail: {$row_delivery['email']}\n\n"; + + +?> + + + Tele Wiz - Package+ + + + \ No newline at end of file diff --git a/docs/legacy-wizard/tele_wizard_subs.php b/docs/legacy-wizard/tele_wizard_subs.php new file mode 100644 index 0000000..02ecbf6 --- /dev/null +++ b/docs/legacy-wizard/tele_wizard_subs.php @@ -0,0 +1,130 @@ +set_charset("latin1_swedish_ci"); + + $address_id = $_POST['ws_delivery_id']; + $first_name = str_replace("'","",$sql_epg->real_escape_string(utf8_decode($_POST['first_name']))); + $last_name = str_replace("'","",$sql_epg->real_escape_string(utf8_decode($_POST['last_name']))); + $city = str_replace("'","", $sql_epg->real_escape_string(utf8_decode($_POST['city']))); + $code_postal = $_POST['zip']; + $address = str_replace("'","",$sql_epg->real_escape_string(utf8_decode($_POST['address']))); + //$civicno = $_POST['civicno']; + $civicno = $_POST['ws_delivery_id']; + + $q_sub = "INSERT INTO `subs` (`groupid`,`fname`,`lname`,`city`,`zip`,`address`,`resellerid`,`status`,`defaultSaverTimeout`,`civicno`,`defPvrMaxHours`,`appProfileId`) + VALUES (2,'$first_name','$last_name','$city','$code_postal','$address',1,'ACTIVE',2147483647,'$civicno',100,1)"; + + if($sql_epg->query($q_sub)){ + + //echo "$q_sub "; + //print_r($sql_epg->error_list); + + $subid = 0; + $subid = $sql_epg->insert_id; + + $q_update = "UPDATE `delivery` SET `epg_subid` = $subid WHERE `id` = {$_POST['ws_delivery_id']}"; + if($sql->query($q_update)){ + + //echo "$q_update "; + + $sql_epg->close(); + + + die(" + "); + + } + else{ + echo " Erreur Link epg->F. subid: $subid - delivery: {$_POST['ws_delivery_id']} ";
+ }
+ }
+ else{
+ echo "Le compte n'a pu etre cree dans epg: {$sql_epg->error_list} ";
+ }
+
+ $sql_epg->close();
+
+}
+
+
+
+//debug
+//$_POST['ws_delivery_id'] = 2;
+
+
+$delivery_id = $_POST['ws_delivery_id'];
+
+$res_delivery = $sql->query("SELECT * FROM `delivery` WHERE `id` = $delivery_id");
+$row_delivery = $res_delivery->fetch_assoc();
+$name = explode(' ',$row_delivery['name']);
+$civicno = explode(' ',$row_delivery['address1']);
+$civicno = str_replace(',','',$civicno[0]);
+$civicno = str_replace(' ','',$civicno);
+
+
+?>
+
+Tele Wiz - Ouverture compte tele (epg)+ + \ No newline at end of file diff --git a/erpnext/flow_scheduler.py b/erpnext/flow_scheduler.py new file mode 100644 index 0000000..9993455 --- /dev/null +++ b/erpnext/flow_scheduler.py @@ -0,0 +1,181 @@ +""" +flow_scheduler.py — Frappe scheduler tick for delayed Flow steps. + +Purpose +─────── +The Flow Runtime in targo-hub stores delayed steps as `Flow Step Pending` +rows with a `trigger_at` timestamp. This script (run every minute by a +Frappe cron hook) fetches rows whose `trigger_at <= now()` and nudges the +hub to advance the owning Flow Run. + +How it's hooked +─────────────── +In ERPNext, add to hooks.py (or scheduler config): + + scheduler_events = { + "cron": { + "* * * * *": [ + "path.to.flow_scheduler.tick", + ], + }, + } + +Or invoke manually from bench console for testing: + + docker exec -u frappe erpnext-backend-1 bash -c \\ + 'cd /home/frappe/frappe-bench/sites && \\ + /home/frappe/frappe-bench/env/bin/python -c \\ + "import frappe; frappe.init(site=\\"erp.gigafibre.ca\\"); frappe.connect(); \\ + from flow_scheduler import tick; tick()"' + +Behaviour +───────── +- Atomic status flip: `pending` → `running` under a transaction so two + ticks can't double-fire. +- POSTs to `HUB_URL/flow/complete` with `{run, step_id}`. Hub fires the + kind handler and advances the run. +- On hub error: retry_count += 1, status stays `pending` until 5 retries + exhausted (then marked `failed`). +- On success: status = `completed`. + +Idempotent: safe to call multiple times — only truly due rows are fired. +""" + +import os +import json +import time +import urllib.request +import urllib.error +import frappe + +# --------------------------------------------------------------------------- +# Config — these default to the Docker compose network hostname. +# Override via environment variables in ERPNext container if hub URL changes. +# --------------------------------------------------------------------------- + +HUB_URL = os.environ.get("HUB_URL", "http://targo-hub:3300") +INTERNAL_TOKEN = os.environ.get("HUB_INTERNAL_TOKEN", "") +MAX_RETRIES = 5 +BATCH_LIMIT = 50 # safety net: fire at most N pending steps per tick + + +# --------------------------------------------------------------------------- +# Public entry point — Frappe cron calls this +# --------------------------------------------------------------------------- + + +def tick(): + """Process all due pending steps. Called every minute by Frappe cron.""" + due = _claim_due_rows() + if not due: + return + frappe.logger().info(f"[flow_scheduler] claimed {len(due)} due rows") + for row in due: + _fire_row(row) + + +# --------------------------------------------------------------------------- +# Claim phase: atomically flip due rows from pending → running +# --------------------------------------------------------------------------- + + +def _claim_due_rows(): + """ + Return all Flow Step Pending rows where: + status = 'pending' AND trigger_at <= now() + Flips them to 'running' in the same transaction to prevent double-firing. + """ + now = frappe.utils.now() + rows = frappe.get_all( + "Flow Step Pending", + filters={ + "status": "pending", + "trigger_at": ["<=", now], + }, + fields=["name", "flow_run", "step_id", "retry_count"], + limit=BATCH_LIMIT, + order_by="trigger_at asc", + ) + if not rows: + return [] + + # Optimistic claim: update in bulk. If it fails (concurrent tick), we + # simply re-query; PostgreSQL serialisation will prevent double-runs. + names = [r["name"] for r in rows] + frappe.db.sql( + """UPDATE `tabFlow Step Pending` + SET status='running', modified=%s, modified_by=%s + WHERE name IN %s AND status='pending'""", + (now, "Administrator", tuple(names)), + ) + frappe.db.commit() + return rows + + +# --------------------------------------------------------------------------- +# Fire phase: POST to Hub /flow/complete +# --------------------------------------------------------------------------- + + +def _fire_row(row): + """Send a completion event to the Hub. Update row status on response.""" + payload = { + "run": row["flow_run"], + "step_id": row["step_id"], + "result": {"fired_by": "scheduler", "fired_at": frappe.utils.now()}, + } + try: + _post_hub("/flow/complete", payload) + _mark_done(row["name"]) + except Exception as e: # noqa: BLE001 + _handle_failure(row, str(e)) + + +def _post_hub(path, body): + data = json.dumps(body).encode("utf-8") + req = urllib.request.Request( + HUB_URL + path, + data=data, + method="POST", + headers={"Content-Type": "application/json"}, + ) + if INTERNAL_TOKEN: + req.add_header("Authorization", "Bearer " + INTERNAL_TOKEN) + with urllib.request.urlopen(req, timeout=15) as resp: + body = resp.read().decode("utf-8") + if resp.status >= 400: + raise RuntimeError(f"Hub HTTP {resp.status}: {body[:200]}") + return body + + +def _mark_done(row_name): + frappe.db.set_value( + "Flow Step Pending", + row_name, + { + "status": "completed", + "executed_at": frappe.utils.now(), + "last_error": "", + }, + update_modified=False, + ) + frappe.db.commit() + + +def _handle_failure(row, err_msg): + retries = int(row.get("retry_count") or 0) + 1 + final = retries >= MAX_RETRIES + frappe.db.set_value( + "Flow Step Pending", + row["name"], + { + "status": "failed" if final else "pending", + "retry_count": retries, + "last_error": err_msg[:500], + }, + update_modified=False, + ) + frappe.db.commit() + frappe.logger().error( + f"[flow_scheduler] row={row['name']} retry={retries} err={err_msg[:200]}" + ) diff --git a/erpnext/seed_flow_templates.py b/erpnext/seed_flow_templates.py new file mode 100644 index 0000000..d9bfe0d --- /dev/null +++ b/erpnext/seed_flow_templates.py @@ -0,0 +1,459 @@ +""" +seed_flow_templates.py — Seed initial Flow Templates (system-owned). + +Migrates the 4 hardcoded project templates (fiber_install, phone_service, +move_service, repair_service) from apps/ops/src/config/project-templates.js +into Flow Template docs (is_system=1). + +Also seeds: + - residential_onboarding : runs on_contract_signed, ties install + reminders + - quotation_follow_up : runs on_quotation_created, sends reminders + +Run (inside backend container): + docker exec -u frappe erpnext-backend-1 bash -c \\ + 'cd /home/frappe/frappe-bench/sites && \\ + /home/frappe/frappe-bench/env/bin/python -c \\ + "import frappe; frappe.init(site=\\"erp.gigafibre.ca\\"); frappe.connect(); \\ + from seed_flow_templates import seed_all; seed_all()"' + +Idempotent: skips templates that already exist by template_name. +""" + +import json +import frappe + + +# Flow definition schema: +# { +# "version": 1, +# "trigger": { "event": "...", "condition": "" }, +# "variables": {...}, # flow-level defaults +# "steps": [
+
+
+
+
+
+
diff --git a/scripts/migration/logo-targo-green.svg b/scripts/migration/logo-targo-green.svg
new file mode 100644
index 0000000..a3e9f51
--- /dev/null
+++ b/scripts/migration/logo-targo-green.svg
@@ -0,0 +1 @@
+
diff --git a/scripts/migration/setup_invoice_print_format.py b/scripts/migration/setup_invoice_print_format.py
index 88c6ce6..29317f6 100644
--- a/scripts/migration/setup_invoice_print_format.py
+++ b/scripts/migration/setup_invoice_print_format.py
@@ -1,357 +1,699 @@
"""
-Create custom Print Format for Sales Invoice — Gigafibre/TARGO style.
-Inspired by Cogeco layout: summary page 1, details page 2, envelope window address.
+Create / update the custom Print Format "Facture TARGO" on ERPNext.
-Run inside erpnext-backend-1:
- /home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/setup_invoice_print_format.py
+Design
+------
+- Letter-size, #10 envelope-window compatible
+- 2-column layout: left = client + per-location charges, right = summary + QR
+- Items grouped by Sales Invoice Item.service_location (Link → Service Location).
+ Identical (name, rate) rows within a location are consolidated with a "(xN)"
+ suffix, matching the reference preview.
+- QR code is embedded as base64 data URI via the whitelisted method
+ `gigafibre_utils.api.invoice_qr_base64`. Depends on the custom app
+ `gigafibre_utils` being installed on the bench (see /opt/erpnext/custom/
+ on the ERPNext host). wkhtmltopdf does NOT fetch the QR over HTTP.
+- SVG logo paths use inline `fill="#019547"` (wkhtmltopdf / QtWebKit does not
+ honour `Merci de choisir local — Merci de choisir TARGO
+
-
+
+
+
-
- |
-
- |
-
+
+
+
+ |
+
+
+
+
+ {{ doc_title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if _is_en %}
+ Refer a friend and each receive $50 credit on your monthly bill · targo.ca/parrainage
+ {% else %}
+ Référez un ami et recevez chacun 50 $ de crédit sur votre facture mensuelle · targo.ca/parrainage
+ {% endif %}
+
+
+
+
+ {% if _is_en %}Your code{% else %}Votre code{% endif %}
+ {{ referral_code }}
+
+
+ {% if _is_en %}
+ By paying this invoice, you accept our terms and conditions.
+
+
+ + Please note that any invoice not paid by the due date will be subject to a late payment fee. + {% else %} + En payant cette facture, vous acceptez nos termes et conditions. + Prendre note que toute facture non acquittée à la date d'échéance sera assujettie à des frais de retard. + {% endif %} +
+
+
+
+ {% if _is_en %}Contact us{% else %}Contactez-nous{% endif %}
+ TARGO Communications Inc.+ 1867 chemin de la Rivière + Sainte-Clotilde, QC J0L 1W0 + 855 888-2746 · {% if _is_en %}Mon-Fri 8am-5pm{% else %}Lun-Ven 8h-17h{% endif %} + info@targo.ca • www.targo.ca +
+ {% if _is_en %}
+ Complaint about your telecom or TV service? The CCTS can help you for free: www.ccts-cprst.ca.
+ {% else %}
+ Plainte relative à votre service de télécommunication ou de télévision? La CPRST peut vous aider sans frais : www.ccts-cprst.ca.
+ {% endif %}
+
+
+ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||