feat: complete sales manager pricing system
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,244 @@
|
|||||||
|
import frappe
|
||||||
|
import re
|
||||||
|
from frappe.utils import now, now_datetime
|
||||||
|
|
||||||
|
def get_pricing_config():
|
||||||
|
if not frappe.db.exists("Pricing Score Config", "Pricing Score Config"):
|
||||||
|
return None
|
||||||
|
return frappe.get_doc("Pricing Score Config", "Pricing Score Config")
|
||||||
|
|
||||||
|
def parse_ram(ram_str):
|
||||||
|
if not ram_str:
|
||||||
|
return 0
|
||||||
|
m = re.search(r'(\d+(?:\.\d+)?)', str(ram_str))
|
||||||
|
if m:
|
||||||
|
return int(float(m.group(1)))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def parse_cpu(cpu_str):
|
||||||
|
if not cpu_str:
|
||||||
|
return {"brand": "", "series": "", "gen": 0}
|
||||||
|
cpu = str(cpu_str).lower()
|
||||||
|
result = {"brand": "", "series": "", "gen": 0}
|
||||||
|
if "intel" in cpu or "core" in cpu or "i3" in cpu or "i5" in cpu or "i7" in cpu or "i9" in cpu or "xeon" in cpu:
|
||||||
|
result["brand"] = "intel"
|
||||||
|
elif "ryzen" in cpu or "athlon" in cpu or "amd" in cpu:
|
||||||
|
result["brand"] = "amd"
|
||||||
|
elif "apple" in cpu or "m1" in cpu or "m2" in cpu or "m3" in cpu:
|
||||||
|
result["brand"] = "apple"
|
||||||
|
series_match = re.search(r'(i[3579])', cpu)
|
||||||
|
if series_match:
|
||||||
|
result["series"] = series_match.group(1)
|
||||||
|
elif "xeon" in cpu:
|
||||||
|
result["series"] = "xeon"
|
||||||
|
elif "ryzen" in cpu:
|
||||||
|
ryz_match = re.search(r'ryzen\s+(\d+)', cpu)
|
||||||
|
if ryz_match:
|
||||||
|
result["series"] = f"r{ryz_match.group(1)}"
|
||||||
|
elif "m1" in cpu:
|
||||||
|
result["series"] = "m1"
|
||||||
|
elif "m2" in cpu:
|
||||||
|
result["series"] = "m2"
|
||||||
|
elif "m3" in cpu:
|
||||||
|
result["series"] = "m3"
|
||||||
|
gen_match = re.search(r'(?:i[3579]-|core\s+i[3579]\s+)(\d)', cpu)
|
||||||
|
if gen_match:
|
||||||
|
first_digit = int(gen_match.group(1))
|
||||||
|
if first_digit == 1:
|
||||||
|
gen_match2 = re.search(r'(?:i[3579]-|core\s+i[3579]\s+)1(\d)', cpu)
|
||||||
|
if gen_match2:
|
||||||
|
result["gen"] = 10 + int(gen_match2.group(1))
|
||||||
|
else:
|
||||||
|
result["gen"] = 1
|
||||||
|
else:
|
||||||
|
result["gen"] = first_digit
|
||||||
|
gen_match3 = re.search(r'(?:i[3579]-)(\d{4})', cpu)
|
||||||
|
if gen_match3:
|
||||||
|
model = gen_match3.group(1)
|
||||||
|
first_digit = int(model[0])
|
||||||
|
if first_digit == 1:
|
||||||
|
result["gen"] = 10 + int(model[1])
|
||||||
|
else:
|
||||||
|
result["gen"] = first_digit
|
||||||
|
return result
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def calculate_serial_score(serial_no):
|
||||||
|
serial = frappe.get_doc("Serial No", serial_no)
|
||||||
|
config = get_pricing_config()
|
||||||
|
if not config:
|
||||||
|
return {"error": "No Pricing Score Config found"}
|
||||||
|
|
||||||
|
score = 0.0
|
||||||
|
details = []
|
||||||
|
|
||||||
|
cos_grade = serial.get("cosmetic_grade", "")
|
||||||
|
if cos_grade:
|
||||||
|
grade_match = re.search(r'[Cc](\d+)', str(cos_grade))
|
||||||
|
if grade_match:
|
||||||
|
g = int(grade_match.group(1))
|
||||||
|
if g <= 2:
|
||||||
|
return {"status": "scrap", "reason": f"Cosmetic C{g} - below minimum"}
|
||||||
|
base = getattr(config, f"c{g}_base", 0)
|
||||||
|
score += base
|
||||||
|
details.append(f"Cosmetic C{g} = +{base}")
|
||||||
|
|
||||||
|
cpu_info = parse_cpu(serial.get("processor", ""))
|
||||||
|
if cpu_info["series"] in ("i7", "r7"):
|
||||||
|
score += config.i7_bonus
|
||||||
|
details.append(f"CPU {cpu_info['series'].upper()} = +{config.i7_bonus}")
|
||||||
|
elif cpu_info["series"] in ("i9", "r9"):
|
||||||
|
score += config.i9_bonus
|
||||||
|
details.append(f"CPU {cpu_info['series'].upper()} = +{config.i9_bonus}")
|
||||||
|
|
||||||
|
gen = cpu_info["gen"]
|
||||||
|
gen_bonus = 0
|
||||||
|
if gen == 10: gen_bonus = config.gen_10_bonus
|
||||||
|
elif gen == 11: gen_bonus = config.gen_11_bonus
|
||||||
|
elif gen == 12: gen_bonus = config.gen_12_bonus
|
||||||
|
elif gen == 13: gen_bonus = config.gen_13_bonus
|
||||||
|
elif gen == 14: gen_bonus = config.gen_14_bonus
|
||||||
|
if gen_bonus > 0:
|
||||||
|
score += gen_bonus
|
||||||
|
details.append(f"Gen {gen} = +{gen_bonus}")
|
||||||
|
|
||||||
|
ram_gb = parse_ram(serial.get("ram", ""))
|
||||||
|
if ram_gb >= 16:
|
||||||
|
score += config.ram_16_bonus
|
||||||
|
details.append(f"RAM {ram_gb}GB = +{config.ram_16_bonus}")
|
||||||
|
if ram_gb >= 32:
|
||||||
|
score += config.ram_32_bonus
|
||||||
|
details.append(f"RAM {ram_gb}GB = +{config.ram_32_bonus}")
|
||||||
|
if ram_gb > 32:
|
||||||
|
details.append(f"WARNING: {ram_gb}GB exceeds laptop/desktop norm")
|
||||||
|
|
||||||
|
if score >= config.medium_threshold:
|
||||||
|
tier = "High"
|
||||||
|
elif score >= config.low_threshold:
|
||||||
|
tier = "Medium"
|
||||||
|
else:
|
||||||
|
tier = "Low"
|
||||||
|
|
||||||
|
market = {"low": 0, "median": 0, "high": 0}
|
||||||
|
age_days = 0
|
||||||
|
age_status = "unknown"
|
||||||
|
|
||||||
|
if serial.item_code:
|
||||||
|
item = frappe.get_doc("Item", serial.item_code)
|
||||||
|
market = {
|
||||||
|
"low": item.market_low or item.base_market_price or 0,
|
||||||
|
"median": item.market_median or item.base_market_price or 0,
|
||||||
|
"high": item.market_high or item.base_market_price or 0,
|
||||||
|
}
|
||||||
|
if item.market_last_priced:
|
||||||
|
age_days = (now_datetime() - item.market_last_priced).days
|
||||||
|
if age_days <= 30: age_status = "current"
|
||||||
|
elif age_days <= 60: age_status = "stale"
|
||||||
|
elif age_days <= 90: age_status = "aging"
|
||||||
|
else: age_status = "expired"
|
||||||
|
|
||||||
|
suggested_price = market["high"] if tier == "High" else market["median"] if tier == "Medium" else market["low"]
|
||||||
|
|
||||||
|
serial.desirability_score = round(score, 1)
|
||||||
|
serial.suggested_tier = tier
|
||||||
|
serial.suggested_price = suggested_price
|
||||||
|
serial.save()
|
||||||
|
frappe.db.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"serial_no": serial_no,
|
||||||
|
"score": round(score, 1),
|
||||||
|
"tier": tier,
|
||||||
|
"details": details,
|
||||||
|
"market_prices": market,
|
||||||
|
"suggested_price": suggested_price,
|
||||||
|
"age_days": age_days,
|
||||||
|
"age_status": age_status,
|
||||||
|
}
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def batch_calculate_scores(batch_size=100):
|
||||||
|
serials = frappe.get_all("Serial No",
|
||||||
|
filters={"cosmetic_grade": ["is", "set"]},
|
||||||
|
fields=["name"],
|
||||||
|
limit=int(batch_size)
|
||||||
|
)
|
||||||
|
results = {"updated": 0, "scrap": 0, "errors": 0}
|
||||||
|
for s in serials:
|
||||||
|
try:
|
||||||
|
result = calculate_serial_score(s.name)
|
||||||
|
if result.get("status") == "scrap":
|
||||||
|
results["scrap"] += 1
|
||||||
|
else:
|
||||||
|
results["updated"] += 1
|
||||||
|
except Exception as e:
|
||||||
|
results["errors"] += 1
|
||||||
|
frappe.log_error(f"Score calc error for {s.name}: {e}")
|
||||||
|
return results
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_sales_pricing_data(limit=50):
|
||||||
|
"""Get pricing data for Sales Manager page."""
|
||||||
|
config = get_pricing_config()
|
||||||
|
if not config:
|
||||||
|
return {"error": "No Pricing Score Config"}
|
||||||
|
|
||||||
|
serials = frappe.get_all("Serial No",
|
||||||
|
filters={"cosmetic_grade": ["is", "set"]},
|
||||||
|
fields=["name", "serial_no", "item_code", "item_name", "cosmetic_grade", "processor", "ram", "desirability_score", "suggested_tier", "suggested_price", "assigned_price", "pricing_status"],
|
||||||
|
limit=int(limit),
|
||||||
|
order_by="modified desc"
|
||||||
|
)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for s in serials:
|
||||||
|
age = {"days": 0, "status": "unknown", "color": "gray"}
|
||||||
|
market = {"low": 0, "median": 0, "high": 0}
|
||||||
|
|
||||||
|
if s.item_code:
|
||||||
|
item = frappe.get_doc("Item", s.item_code)
|
||||||
|
market = {
|
||||||
|
"low": item.market_low or 0,
|
||||||
|
"median": item.market_median or 0,
|
||||||
|
"high": item.market_high or 0,
|
||||||
|
}
|
||||||
|
if item.market_last_priced:
|
||||||
|
age_days = (now_datetime() - item.market_last_priced).days
|
||||||
|
age["days"] = age_days
|
||||||
|
if age_days <= 30:
|
||||||
|
age["status"] = "current"
|
||||||
|
age["color"] = "green"
|
||||||
|
elif age_days <= 60:
|
||||||
|
age["status"] = "stale"
|
||||||
|
age["color"] = "yellow"
|
||||||
|
elif age_days <= 90:
|
||||||
|
age["status"] = "aging"
|
||||||
|
age["color"] = "orange"
|
||||||
|
else:
|
||||||
|
age["status"] = "expired"
|
||||||
|
age["color"] = "red"
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"serial_no": s.name,
|
||||||
|
"item_code": s.item_code,
|
||||||
|
"item_name": s.item_name,
|
||||||
|
"cosmetic_grade": s.cosmetic_grade,
|
||||||
|
"processor": s.processor,
|
||||||
|
"ram": s.ram,
|
||||||
|
"score": s.desirability_score,
|
||||||
|
"tier": s.suggested_tier,
|
||||||
|
"market": market,
|
||||||
|
"suggested_price": s.suggested_price,
|
||||||
|
"assigned_price": s.assigned_price,
|
||||||
|
"pricing_status": s.pricing_status,
|
||||||
|
"age": age,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"serials": results,
|
||||||
|
"config": {
|
||||||
|
"low_threshold": config.low_threshold,
|
||||||
|
"medium_threshold": config.medium_threshold,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<div class="container-fluid">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>Sales Manager — Pricing Queue</h2>
|
||||||
|
<p class="text-muted">Review suggested prices, adjust tiers, and set final prices</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-white bg-success">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Current</h5>
|
||||||
|
<p class="card-text">0-30 days</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-dark bg-warning">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Stale</h5>
|
||||||
|
<p class="card-text">30-60 days</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-white" style="background-color: #fd7e14;">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Aging</h5>
|
||||||
|
<p class="card-text">60-90 days</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-white bg-danger">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Expired</h5>
|
||||||
|
<p class="card-text">90+ days</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button type="button" class="btn btn-outline-primary" id="btn-all">All</button>
|
||||||
|
<button type="button" class="btn btn-outline-success" id="btn-current">Current</button>
|
||||||
|
<button type="button" class="btn btn-outline-warning" id="btn-stale">Stale</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" id="btn-aging">Aging</button>
|
||||||
|
<button type="button" class="btn btn-outline-danger" id="btn-expired">Expired</button>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary float-right" id="btn-refresh">Refresh</button>
|
||||||
|
<button class="btn btn-secondary float-right mr-2" id="btn-batch-score">Batch Score</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover" id="pricing-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Serial</th>
|
||||||
|
<th>Item</th>
|
||||||
|
<th>Grade</th>
|
||||||
|
<th>Specs</th>
|
||||||
|
<th>Score</th>
|
||||||
|
<th>Tier</th>
|
||||||
|
<th>Market Range</th>
|
||||||
|
<th>Suggested</th>
|
||||||
|
<th>Age</th>
|
||||||
|
<th>Final Price</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="pricing-tbody">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="loading" class="text-center py-5" style="display: none;">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
frappe.pages['sales-manager'].on_page_load = function(wrapper) {
|
||||||
|
var page = frappe.ui.make_app_page({
|
||||||
|
parent: wrapper,
|
||||||
|
title: 'Sales Manager',
|
||||||
|
single_column: true
|
||||||
|
});
|
||||||
|
|
||||||
|
var $content = $(wrapper).find('.page-content');
|
||||||
|
$content.html(frappe.render_template('sales_manager'));
|
||||||
|
|
||||||
|
loadPricingData();
|
||||||
|
|
||||||
|
$('#btn-refresh').on('click', function() {
|
||||||
|
loadPricingData();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btn-batch-score').on('click', function() {
|
||||||
|
frappe.confirm('Calculate scores for all serials?', function() {
|
||||||
|
frappe.call({
|
||||||
|
method: 'westech_r2.api.scoring.batch_calculate_scores',
|
||||||
|
args: {batch_size: 500},
|
||||||
|
callback: function(r) {
|
||||||
|
if (r.message) {
|
||||||
|
frappe.msgprint('Updated: ' + r.message.updated +
|
||||||
|
', Scrap: ' + r.message.scrap +
|
||||||
|
', Errors: ' + r.message.errors);
|
||||||
|
loadPricingData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btn-all, #btn-current, #btn-stale, #btn-aging, #btn-expired').on('click', function() {
|
||||||
|
var filter = $(this).attr('id').replace('btn-', '');
|
||||||
|
filterTable(filter);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadPricingData() {
|
||||||
|
$('#loading').show();
|
||||||
|
$('#pricing-tbody').empty();
|
||||||
|
|
||||||
|
frappe.call({
|
||||||
|
method: 'westech_r2.api.scoring.get_sales_pricing_data',
|
||||||
|
args: {limit: 100},
|
||||||
|
callback: function(r) {
|
||||||
|
$('#loading').hide();
|
||||||
|
if (r.message && r.message.serials) {
|
||||||
|
renderPricingTable(r.message.serials);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPricingTable(serials) {
|
||||||
|
var tbody = $('#pricing-tbody');
|
||||||
|
tbody.empty();
|
||||||
|
|
||||||
|
serials.forEach(function(s) {
|
||||||
|
var ageBadge = '<span class="badge badge-' + (s.age.color || 'secondary') + '">' +
|
||||||
|
(s.age.status || 'unknown') + '</span>';
|
||||||
|
|
||||||
|
var tierBadge = '<span class="badge badge-info">' + (s.tier || 'N/A') + '</span>';
|
||||||
|
|
||||||
|
var specs = [];
|
||||||
|
if (s.processor) specs.push(s.processor);
|
||||||
|
if (s.ram) specs.push(s.ram);
|
||||||
|
|
||||||
|
var marketRange = '$' + (s.market.low || 0).toFixed(0) + ' - $' + (s.market.high || 0).toFixed(0);
|
||||||
|
|
||||||
|
var row = '<tr data-age="' + s.age.status + '">' +
|
||||||
|
'<td><a href="/app/serial-no/' + s.serial_no + '">' + s.serial_no + '</a></td>' +
|
||||||
|
'<td>' + (s.item_name || s.item_code) + '</td>' +
|
||||||
|
'<td>' + (s.cosmetic_grade || '') + '</td>' +
|
||||||
|
'<td><small>' + specs.join(' / ') + '</small></td>' +
|
||||||
|
'<td><strong>' + (s.score || 0) + '</strong></td>' +
|
||||||
|
'<td>' + tierBadge + '</td>' +
|
||||||
|
'<td><small>' + marketRange + '</small></td>' +
|
||||||
|
'<td>$' + (s.suggested_price || 0).toFixed(2) + '</td>' +
|
||||||
|
'<td>' + ageBadge + '<br><small>' + s.age.days + 'd</small></td>' +
|
||||||
|
'<td><input type="number" class="form-control form-control-sm final-price" ' +
|
||||||
|
'data-serial="' + s.serial_no + '" value="' + (s.assigned_price || '') + '" step="0.01"></td>' +
|
||||||
|
'<td>' +
|
||||||
|
'<button class="btn btn-sm btn-success btn-save-price" data-serial="' + s.serial_no + '">Save</button>' +
|
||||||
|
'</td>' +
|
||||||
|
'</tr>';
|
||||||
|
|
||||||
|
tbody.append(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.btn-save-price').on('click', function() {
|
||||||
|
var serial = $(this).data('serial');
|
||||||
|
var price = $(this).closest('tr').find('.final-price').val();
|
||||||
|
saveFinalPrice(serial, price);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterTable(filter) {
|
||||||
|
if (filter === 'all') {
|
||||||
|
$('#pricing-tbody tr').show();
|
||||||
|
} else {
|
||||||
|
$('#pricing-tbody tr').hide();
|
||||||
|
$('#pricing-tbody tr[data-age="' + filter + '"]').show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveFinalPrice(serial_no, price) {
|
||||||
|
frappe.call({
|
||||||
|
method: 'frappe.client.set_value',
|
||||||
|
args: {
|
||||||
|
doctype: 'Serial No',
|
||||||
|
name: serial_no,
|
||||||
|
fieldname: {
|
||||||
|
'assigned_price': price,
|
||||||
|
'pricing_status': 'Manual Override'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
callback: function(r) {
|
||||||
|
if (!r.exc) {
|
||||||
|
frappe.show_alert({message: 'Price saved for ' + serial_no, indicator: 'green'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"title": "Sales Manager",
|
||||||
|
"route": "sales-manager",
|
||||||
|
"icon": "fa fa-dollar-sign",
|
||||||
|
"roles": [
|
||||||
|
{"role": "System Manager"},
|
||||||
|
{"role": "Sales User"}
|
||||||
|
],
|
||||||
|
"standard": "Yes",
|
||||||
|
"type": "page",
|
||||||
|
"module": "Westech R2"
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
|
||||||
|
def get_context(context):
|
||||||
|
context.no_cache = 1
|
||||||
|
return context
|
||||||
Binary file not shown.
@@ -0,0 +1,7 @@
|
|||||||
|
frappe.pages['sales-manager'].on_page_load = function(wrapper) {
|
||||||
|
var page = frappe.ui.make_app_page({
|
||||||
|
parent: wrapper,
|
||||||
|
title: 'Sales Manager',
|
||||||
|
single_column: true
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"content": null,
|
||||||
|
"creation": "2026-05-17 08:50:06.927009",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Page",
|
||||||
|
"idx": 0,
|
||||||
|
"modified": "2026-05-17 08:50:06.927009",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Westech R2",
|
||||||
|
"name": "sales-manager",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"page_name": "sales-manager",
|
||||||
|
"roles": [],
|
||||||
|
"script": null,
|
||||||
|
"standard": "Yes",
|
||||||
|
"style": null,
|
||||||
|
"system_page": 0,
|
||||||
|
"title": "Sales Manager"
|
||||||
|
}
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
"type": "Link"
|
"type": "Link"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2026-05-17 06:07:36.785015",
|
"modified": "2026-05-17 08:50:07.192685",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Westech R2",
|
"module": "Westech R2",
|
||||||
"name": "Westech",
|
"name": "Westech",
|
||||||
@@ -180,6 +180,12 @@
|
|||||||
"label": "eBay Pricing",
|
"label": "eBay Pricing",
|
||||||
"link_to": "ebay-pricing",
|
"link_to": "ebay-pricing",
|
||||||
"type": "Page"
|
"type": "Page"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"doc_view": "",
|
||||||
|
"label": "Sales Manager",
|
||||||
|
"link_to": "sales-manager",
|
||||||
|
"type": "Page"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "Westech"
|
"title": "Westech"
|
||||||
|
|||||||
Reference in New Issue
Block a user