mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-06 06:58:27 +00:00
Hub (#10934)
* [WIP]Hub * [listing] Show items, users and item_groups * Show filters * [start] cart, api for rfq and opp * rfq working * [wip] keys * wip quotes * [hub] register/unregister * [hub] rename password to access_token, remove passed company field * [hub] publishing cases, api call wrapper * [hub] add and remove fields working * [hub] fix flags, update on client save working * [hub] new hub page, client item CUD at hub working * listing, standard rate, local site hack * item listing, item page, search, back to home * [hub] implement hub company * [hub] company filter * [hub] basic rfq-ing, item page cleanup * categories wip * [hub] use get_doc_before_save() * [hub] send opportunity message to hub, api to make locally * [hub] enqueueing in hub api request wrapper * cleanup * [hub] refactor shopping cart's product.py to reuse * sync dynamic item fields daily * Scheduler heartbeat check * [wip] hub categories * [hub] wip enqueued callbacks * [hub] outgoing messages, fixing callback loop * [hub] bug: callback save after primary save * [hub] pricing, stock, currency * [hub] replace send_hub_request with make_and_enqueue * add hub.less, refactor code * Remove template html files, add styling for hub item cards * fix paging * add breadcrumb * Add sidebar * [hub] add company page, change country * [hub] order_by filters * [hub] make hub category a tree * [hub] enqueue batched item enqueueing * [hub] requested products page * [minor] * update hub url * [fix] url * [fix] more reform * fix recursion * [hub] data migration plans as jsons * Hub register, create data connector, sync with run * [add] user registration by session user * Removed hub_message * Remove sync code from hub_settings * Remove hub methods from item.py * Update Hub Sync plan * Hub unregister * Update Hub connector on reregister * Dont delete Hub Connector on unregister * Enable hub on success response * Add new hub whitelisted methods * [hub] list working * Hub register from hub page * [hub] Add hub logo in desk icon, link to page * [hub] hide page head on empty state * [hub] make rfq * [hub] push opportunity doc, poll for opportunity docs * add fields to item mapping * update hub mappings * Make RFQ * [hub] item, home routing * Make rfq and send opportunity refactor * [hub][fix] remote lead data * images passed as base64 * set default company on register * Revert "images passed as base64" This reverts commit 0b033a5fb7072b2d39a1b87a47dc41e7af707bb4. * Add sync to hub page * Prompt for publish items to hub * add post process to hub document to lead * Rename Hub document to Hub message, create opportunity in post process
This commit is contained in:
committed by
Rushabh Mehta
parent
ff689a658f
commit
fd345f8e90
@@ -1,90 +1,870 @@
|
||||
/* globals Hub, HubList */
|
||||
|
||||
frappe.provide('erpnext.hub');
|
||||
|
||||
frappe.pages['hub'].on_page_load = function(wrapper) {
|
||||
var page = frappe.ui.make_app_page({
|
||||
const page = frappe.ui.make_app_page({
|
||||
parent: wrapper,
|
||||
title: 'Hub',
|
||||
single_column: true
|
||||
single_col: false
|
||||
});
|
||||
|
||||
frappe.hub = new frappe.Hub({page:page});
|
||||
erpnext.hub.Hub = new Hub({ page });
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
frappe.pages['hub'].on_page_show = function() {
|
||||
frappe.hub.refresh();
|
||||
}
|
||||
|
||||
frappe.Hub = Class.extend({
|
||||
init: function(args) {
|
||||
$.extend(this, args);
|
||||
this.render();
|
||||
},
|
||||
refresh: function() {
|
||||
if(this.hub && this.hub.publish && !this.hub_list) {
|
||||
this.setup_list();
|
||||
frappe.pages['hub'].on_page_show = function(wrapper) {
|
||||
const current_route = frappe.get_route();
|
||||
if(current_route[1] === "Products") {
|
||||
const item_code = current_route[2];
|
||||
if(item_code) {
|
||||
erpnext.hub.Hub.go_to_item_page(item_code);
|
||||
}
|
||||
},
|
||||
render: function() {
|
||||
this.page.main.empty();
|
||||
var me = this;
|
||||
frappe.model.with_doc("Hub Settings", "Hub Settings", function() {
|
||||
me.hub = locals["Hub Settings"]["Hub Settings"];
|
||||
if(!me.hub.publish) {
|
||||
$(frappe.render_template("register_in_hub", {})).appendTo(me.page.main);
|
||||
}
|
||||
|
||||
if(current_route[1] === "Company") {
|
||||
const company_name = current_route[2];
|
||||
if(company_name) {
|
||||
erpnext.hub.Hub.go_to_company_page(company_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.Hub = class Hub {
|
||||
constructor({ page }) {
|
||||
this.page = page;
|
||||
frappe.require('/assets/erpnext/css/hub.css', () => {
|
||||
this.setup();
|
||||
});
|
||||
}
|
||||
|
||||
setup() {
|
||||
this.setup_header();
|
||||
this.company_cache = {};
|
||||
this.item_cache = {};
|
||||
this.filters = {};
|
||||
this.order_by = '';
|
||||
|
||||
this.$hub_main_section =
|
||||
$(`<div class='hub-main-section'>`).appendTo(this.page.body);
|
||||
this.bind_events();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.$hub_main_section.empty();
|
||||
this.page.page_form.hide();
|
||||
|
||||
const $layout_main = this.page.wrapper.find('.layout-main');
|
||||
const $page_head = this.page.wrapper.find('.page-head');
|
||||
|
||||
frappe.model.with_doc('Hub Settings', 'Hub Settings', () => {
|
||||
this.hub_settings = frappe.get_doc('Hub Settings');
|
||||
|
||||
if(this.hub_settings.enabled == 0) {
|
||||
let $empty_state = this.page.get_empty_state(
|
||||
__("Register for Hub"),
|
||||
__(`Let other ERPNext users discover your products
|
||||
and automate workflow with Supplier from within ERPNext.`),
|
||||
__("Register")
|
||||
);
|
||||
|
||||
$page_head.hide();
|
||||
$layout_main
|
||||
.find('.layout-side-section, .layout-main-section-wrapper')
|
||||
.hide();
|
||||
$layout_main.append($empty_state);
|
||||
|
||||
$empty_state.find('.btn-primary').on('click', () => {
|
||||
this.register_for_hub();
|
||||
});
|
||||
} else {
|
||||
me.setup_list();
|
||||
$page_head.show();
|
||||
$layout_main.find('.page-card-container').remove();
|
||||
$layout_main.find('.layout-side-section, .layout-main-section-wrapper').show();
|
||||
this.setup_live_state();
|
||||
}
|
||||
});
|
||||
},
|
||||
setup_list: function() {
|
||||
var me = this;
|
||||
$(frappe.render_template("hub_body", {})).appendTo(this.page.main);
|
||||
this.hub_list = this.page.main.find(".hub-list");
|
||||
this.search = this.page.main.find("input").on("keypress", function(e) {
|
||||
if(e.which===13) {
|
||||
me.reset();
|
||||
}
|
||||
}
|
||||
|
||||
register_for_hub() {
|
||||
frappe.verify_password(() => {
|
||||
frappe.call({
|
||||
method: 'erpnext.hub_node.enable_hub',
|
||||
callback: (r) => {
|
||||
if(r.message.enabled == 1) {
|
||||
Object.assign(this.hub_settings, r.message);
|
||||
this.refresh();
|
||||
this.prompt_for_item_sync();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
this.loading = this.page.main.find(".loading");
|
||||
this.done = this.page.main.find(".done");
|
||||
this.more = this.page.main.find(".more")
|
||||
this.more.find(".btn").on("click", function() { me.next_page() });
|
||||
this.reset();
|
||||
},
|
||||
reset: function() {
|
||||
this.hub_list.empty();
|
||||
this.start = 0;
|
||||
this.page_length = 20;
|
||||
this.next_page();
|
||||
},
|
||||
next_page: function() {
|
||||
var me = this;
|
||||
this.loading.toggleClass("hide", false);
|
||||
}
|
||||
|
||||
prompt_for_item_sync() {
|
||||
frappe.call({
|
||||
method: "erpnext.hub_node.get_items",
|
||||
method: 'frappe.client.get_list',
|
||||
args: {
|
||||
text: this.get_text(),
|
||||
start: this.start,
|
||||
limit: this.page_length
|
||||
doctype: 'Data Migration Run',
|
||||
filters: {
|
||||
'data_migration_plan': 'Hub Sync'
|
||||
},
|
||||
limit_page_length: 1
|
||||
},
|
||||
callback: function(r) {
|
||||
me.loading.toggleClass("hide", true);
|
||||
if(!r.message)
|
||||
r.message = [];
|
||||
me.start += r.message.length;
|
||||
$(frappe.render_template("hub_list", {items: r.message})).appendTo(me.hub_list);
|
||||
if(r.message.length && r.message.length===me.page_length) {
|
||||
// more
|
||||
me.more.removeClass("hide");
|
||||
me.done.addClass("hide");
|
||||
} else {
|
||||
// done
|
||||
me.more.addClass("hide");
|
||||
me.done.removeClass("hide");
|
||||
if (!r) {
|
||||
frappe.confirm(__('Do you want to publish your Items to Hub ?'), () => {
|
||||
this.sync_items_to_hub();
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setup_header() {
|
||||
this.page.page_title = this.page.wrapper.find('.page-title');
|
||||
this.tag_line = $(`
|
||||
<div class='tag-line-container'>
|
||||
<span class='tag-line text-muted small'>
|
||||
${__('Product listing and discovery for ERPNext users')}
|
||||
</span>
|
||||
</div>`)
|
||||
.appendTo(this.page.page_title);
|
||||
|
||||
this.bind_title();
|
||||
}
|
||||
|
||||
setup_live_state() {
|
||||
if(!this.$search) {
|
||||
this.setup_filters();
|
||||
}
|
||||
this.page.page_form.show();
|
||||
this.setup_menu();
|
||||
this.setup_sidebar();
|
||||
this.render_body();
|
||||
this.setup_lists();
|
||||
}
|
||||
|
||||
setup_filters() {
|
||||
|
||||
// frappe.call({
|
||||
// method: 'erpnext.hub_node.get_categories'
|
||||
// }).then((r) => {
|
||||
// if (r.message) {
|
||||
// const categories = r.message;
|
||||
// console.log("categories", categories);
|
||||
// categories
|
||||
// .map(c => c.hub_category_name)
|
||||
// .map(c => this.sidebar.add_item({
|
||||
// label: c,
|
||||
// on_click: () => {
|
||||
// this.home_item_list &&
|
||||
// this.home_item_list.refresh({
|
||||
// text: '',
|
||||
// start: 0,
|
||||
// limit: 20,
|
||||
// category: c && c !== 'All Categories' ? c : undefined
|
||||
// });
|
||||
// }
|
||||
// }, __('Hub Category')));
|
||||
|
||||
|
||||
// }
|
||||
// });
|
||||
|
||||
// this.category_select = this.page.add_select(__('Category'),
|
||||
// [
|
||||
// {label: __('Sort by Price ...'), value: '' },
|
||||
// {label: __('High to Low'), value: 'price desc' },
|
||||
// {label: __('Low to High'), value: 'price' },
|
||||
// ]
|
||||
// );
|
||||
|
||||
this.price_sort_select = this.page.add_select(__('Sort by Price'),
|
||||
[
|
||||
{label: __('Sort by Price ...'), value: '' },
|
||||
{label: __('High to Low'), value: 'price desc' },
|
||||
{label: __('Low to High'), value: 'price' },
|
||||
]
|
||||
);
|
||||
|
||||
this.criteria_select = this.page.add_select(__('Sort by Criteria'),
|
||||
[
|
||||
{label: __('Most Popular'), value: 'request_count' },
|
||||
{label: __('Newest'), value: 'creation' },
|
||||
]
|
||||
);
|
||||
|
||||
this.price_sort_select.on('change', () => {
|
||||
this.refresh_item_only_page();
|
||||
});
|
||||
},
|
||||
get_text: function() {
|
||||
return this.search.val();
|
||||
},
|
||||
})
|
||||
|
||||
this.criteria_select.on('change', () => {
|
||||
this.refresh_item_only_page();
|
||||
});
|
||||
|
||||
this.$search = this.page.add_data(__('Search'));
|
||||
this.setup_search();
|
||||
}
|
||||
|
||||
bind_events() {
|
||||
const me = this;
|
||||
this.$hub_main_section
|
||||
.on('click', '.company-link a', function(e) {
|
||||
e.preventDefault();
|
||||
const company_name = $(this).attr('data-company-name');
|
||||
me.get_company_details(company_name);
|
||||
})
|
||||
.on('click', '.breadcrumb li', function(e) {
|
||||
e.preventDefault();
|
||||
const $li = $(this);
|
||||
if ($li.attr('data-route') === 'Home') {
|
||||
me.go_to_home_page();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
update_filters() {
|
||||
let price_sort = $(this.price_sort_select).val() || '';
|
||||
let criteria = $(this.criteria_select).val() || '';
|
||||
|
||||
let order_by_params = [];
|
||||
let query_string = '';
|
||||
if(criteria) {
|
||||
order_by_params.push(criteria);
|
||||
// query_string += 'sort_by=' + criteria
|
||||
}
|
||||
if(price_sort) order_by_params.push(price_sort);
|
||||
this.order_by = order_by_params.join(",");
|
||||
// return query_string;
|
||||
}
|
||||
|
||||
reset_filters() {
|
||||
this.order_by = '';
|
||||
$(this.category_select).val('');
|
||||
$(this.price_sort_select).val('');
|
||||
$(this.criteria_select).val('Most Popular');
|
||||
}
|
||||
|
||||
refresh_item_only_page() {
|
||||
this.reset_search();
|
||||
this.update_filters();
|
||||
this.go_to_items_only_page(
|
||||
['hub', 'Products'],
|
||||
'', 'product-list'
|
||||
);
|
||||
}
|
||||
|
||||
bind_title() {
|
||||
this.page.page_title.find('.title-text').on('click', () => {
|
||||
this.go_to_home_page();
|
||||
});
|
||||
}
|
||||
|
||||
render_body() {
|
||||
this.$home_page = $(`
|
||||
<div class = 'hub-home-page'>
|
||||
<div class='banner'></div>
|
||||
<div class='listing-body row'>
|
||||
<div class='main-list-section'></div>
|
||||
</div>
|
||||
</div>
|
||||
`).appendTo(this.$hub_main_section);
|
||||
|
||||
this.$banner = this.$hub_main_section.find('.banner');
|
||||
this.$listing_body = this.$hub_main_section.find('.listing-body');
|
||||
this.$main_list_section = this.$hub_main_section.find('.main-list-section');
|
||||
this.$side_list_section = this.$hub_main_section.find('.side-list-section');
|
||||
}
|
||||
|
||||
setup_lists() {
|
||||
this.home_item_list = new HubList({
|
||||
parent: this.$main_list_section,
|
||||
title: 'New',
|
||||
page_length: 20,
|
||||
list_css_class: 'home-product-list',
|
||||
method: 'erpnext.hub_node.get_items',
|
||||
// order_by: 'request_count',
|
||||
filters: {text: '', country: this.country}, // filters at the time of creation
|
||||
on_item_click: (item_code) => {
|
||||
frappe.set_route('hub', 'Products', item_code);
|
||||
}
|
||||
});
|
||||
|
||||
this.home_item_list.setup();
|
||||
}
|
||||
|
||||
setup_search() {
|
||||
this.$search.on('keypress', (e) => {
|
||||
if(e.which === 13) {
|
||||
var search_term = ($(this.$search).val() || '').toLowerCase();
|
||||
this.go_to_items_only_page(
|
||||
['hub', 'search', search_term],
|
||||
'Search results for \'' + search_term + '\'',
|
||||
'search-product-list',
|
||||
{text: search_term}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
go_to_items_only_page(route, title, class_name, filters = {text: ''}, by_item_codes=0) {
|
||||
frappe.set_route(route);
|
||||
this.$hub_main_section.empty();
|
||||
this.filtered_item_list = new HubList({
|
||||
parent: this.$hub_main_section,
|
||||
title: title,
|
||||
page_length: 20,
|
||||
list_css_class: class_name,
|
||||
method: 'erpnext.hub_node.get_items',
|
||||
order_by: this.order_by,
|
||||
filters: filters,
|
||||
by_item_codes: by_item_codes
|
||||
});
|
||||
this.filtered_item_list.on_item_click = (item_code) => {
|
||||
frappe.set_route('hub', 'Products', item_code);
|
||||
}
|
||||
this.filtered_item_list.setup();
|
||||
}
|
||||
|
||||
go_to_item_page(item_code) {
|
||||
if(this.item_cache) {
|
||||
let item = this.item_cache[item_code];
|
||||
if(item) {
|
||||
this.render_item_page(item);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.item_cache = {};
|
||||
}
|
||||
frappe.call({
|
||||
args:{
|
||||
hub_sync_id: item_code
|
||||
},
|
||||
method: "erpnext.hub_node.get_item_details",
|
||||
callback: (r) => {
|
||||
let item = r.message;
|
||||
this.item_cache[item_code] = item;
|
||||
this.render_item_page(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render_item_page(item) {
|
||||
this.$hub_main_section.empty();
|
||||
|
||||
|
||||
let $item_page =
|
||||
$(this.get_item_page(item))
|
||||
.appendTo(this.$hub_main_section);
|
||||
|
||||
let $company_items = $item_page.find('.company-items');
|
||||
|
||||
let company_item_list = new HubList({
|
||||
parent: $company_items,
|
||||
title: 'More by ' + item.company_name,
|
||||
page_length: 5,
|
||||
list_css_class: 'company-item-list',
|
||||
method: 'erpnext.hub_node.get_items',
|
||||
// order_by: 'request_count',
|
||||
filters: {text: '', company_name: item.company_name, country: this.country},
|
||||
paginated: 0,
|
||||
img_size: 150
|
||||
});
|
||||
|
||||
company_item_list.on_item_click = (item_code) => {
|
||||
frappe.set_route('hub', 'Products', item_code);
|
||||
}
|
||||
company_item_list.setup();
|
||||
|
||||
$item_page.find('.rfq-btn')
|
||||
.click((e) => {
|
||||
const $btn = $(e.target);
|
||||
|
||||
this.show_rfq_modal(item)
|
||||
.then(values => {
|
||||
item.item_code = values.item_code;
|
||||
delete values.item_code;
|
||||
|
||||
const supplier = values;
|
||||
return [item, supplier];
|
||||
})
|
||||
.then(([item, supplier]) => {
|
||||
return this.make_rfq(item, supplier, $btn);
|
||||
})
|
||||
.then(r => {
|
||||
console.log(r);
|
||||
if (r.message && r.message.rfq) {
|
||||
$btn.addClass('disabled').html(`<span><i class='fa fa-check'></i> ${__('Quote Requested')}</span>`);
|
||||
} else {
|
||||
throw r;
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e); //eslint-disable-line
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
show_rfq_modal(item) {
|
||||
return new Promise(res => {
|
||||
let fields = [
|
||||
{ label: __('Item Code'), fieldtype: 'Data', fieldname: 'item_code', default: item.item_code },
|
||||
{ fieldtype: 'Column Break' },
|
||||
{ label: __('Item Group'), fieldtype: 'Link', fieldname: 'item_group', default: item.item_group },
|
||||
{ label: __('Supplier Details'), fieldtype: 'Section Break' },
|
||||
{ label: __('Supplier Name'), fieldtype: 'Data', fieldname: 'supplier_name', default: item.company_name },
|
||||
{ label: __('Supplier Email'), fieldtype: 'Data', fieldname: 'supplier_email', default: item.seller },
|
||||
{ fieldtype: 'Column Break' },
|
||||
{ label: __('Supplier Type'), fieldname: 'supplier_type',
|
||||
fieldtype: 'Link', options: 'Supplier Type' }
|
||||
];
|
||||
fields = fields.map(f => { f.reqd = 1; return f; });
|
||||
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __('Request for Quotation'),
|
||||
fields: fields,
|
||||
primary_action_label: __('Send'),
|
||||
primary_action: (values) => {
|
||||
res(values);
|
||||
d.hide();
|
||||
}
|
||||
});
|
||||
|
||||
d.show();
|
||||
});
|
||||
}
|
||||
|
||||
get_company_details(company_id) {
|
||||
// get from cache if exists
|
||||
let company_details = this.company_cache[company_id];
|
||||
if(this.company_cache[company_id]) {
|
||||
this.go_to_company_page(company_details);
|
||||
return;
|
||||
}
|
||||
frappe.call({
|
||||
method: 'erpnext.hub_node.get_company_details',
|
||||
args: {company_id: company_id}
|
||||
}).then((r) => {
|
||||
if (r.message) {
|
||||
const company_details = r.message.company_details;
|
||||
this.company_cache[company_id] = company_details;
|
||||
this.go_to_company_page(company_details)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
go_to_company_page(company_details) {
|
||||
frappe.set_route('hub', 'Company', company_details.company_name);
|
||||
this.$hub_main_section.empty();
|
||||
|
||||
let $company_page =
|
||||
$(this.get_company_page(company_details))
|
||||
.appendTo(this.$hub_main_section);
|
||||
|
||||
let $company_items = $company_page.find('.company-items');
|
||||
|
||||
let company_item_list = new HubList({
|
||||
parent: $company_items,
|
||||
title: 'More by ' + company_details.company_name,
|
||||
page_length: 5,
|
||||
list_css_class: 'company-item-list',
|
||||
method: 'erpnext.hub_node.get_items',
|
||||
// order_by: 'request_count',
|
||||
filters: {text: '', company: company_details.company_name, country: this.country},
|
||||
paginated: 0,
|
||||
img_size: 150
|
||||
});
|
||||
|
||||
company_item_list.on_item_click = (item_code) => {
|
||||
frappe.set_route('hub', 'Products', item_code);
|
||||
}
|
||||
company_item_list.setup();
|
||||
}
|
||||
|
||||
get_item_page(item) {
|
||||
return `
|
||||
<div class="hub-item-page">
|
||||
<div class="item-header">
|
||||
<div class="item-page-image">
|
||||
${ this.home_item_list.get_item_image(item) }
|
||||
</div>
|
||||
<div class="title-content">
|
||||
<div class="breadcrumbs">
|
||||
${this.get_breadcrumb(item.item_name, "Products") }
|
||||
</div>
|
||||
<div class="title">
|
||||
<h2>${ item.item_name }</h2>
|
||||
</div>
|
||||
<div class="company">
|
||||
<span class="">${ item.company_name }</span>
|
||||
</div>
|
||||
<div class="category">
|
||||
<span class="text-muted">Products</span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<span class="small">${ item.description ? item.description : "" }</span>
|
||||
</div>
|
||||
<div class="price">
|
||||
${ item.formatted_price ? item.formatted_price : '' }
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a class="btn btn-primary rfq-btn">Request A Quote</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="item-more-info"></div>
|
||||
<div class="company-items">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
get_company_page(company_details) {
|
||||
return `
|
||||
<div class="hub-item-page">
|
||||
<div class="item-header">
|
||||
<div class="title-content">
|
||||
<div class="breadcrumbs">
|
||||
${this.get_breadcrumb(company_details.company_name, "Company") }
|
||||
</div>
|
||||
<div class="title">
|
||||
<h2>${ company_details.company_name }</h2>
|
||||
</div>
|
||||
<div class="company">
|
||||
<span class="">${ company_details.seller_city }</span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<span class="small">${ company_details.seller_description }</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="item-more-info"></div>
|
||||
<div class="company-items">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
get_breadcrumb(name, type) {
|
||||
return `
|
||||
<ul class="breadcrumb">
|
||||
<li data-route="Home">
|
||||
<a href><span>Home</span></a>
|
||||
</li>
|
||||
<li data-route="List">
|
||||
<a href><span>${type}</span></a>
|
||||
</li>
|
||||
<li class="active">
|
||||
<span>${name}</span>
|
||||
</li>
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
|
||||
go_to_home_page() {
|
||||
frappe.set_route('hub');
|
||||
this.reset_filters();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
setup_menu() {
|
||||
this.page.add_menu_item(__('Hub Settings'),
|
||||
() => frappe.set_route('Form', 'Hub Settings'));
|
||||
|
||||
this.page.add_menu_item(__('Refresh'), () => this.refresh());
|
||||
|
||||
this.page.add_menu_item(__('Sync'), () => this.sync_items_to_hub());
|
||||
}
|
||||
|
||||
sync_items_to_hub() {
|
||||
frappe.call('erpnext.hub_node.doctype.hub_settings.hub_settings.sync')
|
||||
}
|
||||
|
||||
setup_sidebar() {
|
||||
var me = this;
|
||||
this.sidebar = new HubSidebar({
|
||||
wrapper: this.page.wrapper.find('.layout-side-section')
|
||||
});
|
||||
|
||||
this.add_account_to_sidebar();
|
||||
|
||||
}
|
||||
|
||||
add_account_to_sidebar() {
|
||||
this.sidebar.add_item({
|
||||
label: this.hub_settings.company,
|
||||
on_click: () => frappe.set_route('Form', 'Company', this.hub_settings.company)
|
||||
}, __("Account"));
|
||||
|
||||
this.sidebar.add_item({
|
||||
label: __("Requested Products"),
|
||||
on_click: () => this.go_to_seen_items()
|
||||
}, __("Account"));
|
||||
}
|
||||
|
||||
get_search_term() {
|
||||
return this.$search.val();
|
||||
}
|
||||
|
||||
reset_search() {
|
||||
this.$search.val('');
|
||||
}
|
||||
|
||||
make_rfq(item, supplier, btn) {
|
||||
console.log(supplier);
|
||||
return new Promise((resolve, reject) => {
|
||||
frappe.call({
|
||||
method: 'erpnext.hub_node.make_rfq_and_send_opportunity',
|
||||
args: { item, supplier },
|
||||
callback: resolve,
|
||||
btn,
|
||||
}).fail(reject);
|
||||
});
|
||||
}
|
||||
|
||||
go_to_seen_items() {
|
||||
this.go_to_items_only_page(
|
||||
['hub', 'Requested Products'],
|
||||
__('Requested Products'),
|
||||
'requested-product-list',
|
||||
{}, 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HubList {
|
||||
constructor({
|
||||
parent = null,
|
||||
title = 'Products',
|
||||
page_length = 20,
|
||||
list_css_class = '',
|
||||
method = 'erpnext.hub_node.get_items',
|
||||
filters = {text: ''},
|
||||
order_by = '',
|
||||
by_item_codes = 0,
|
||||
paginated = 1,
|
||||
on_item_click = null,
|
||||
img_size = 200
|
||||
}) {
|
||||
this.parent = parent;
|
||||
this.title = title;
|
||||
this.page_length = page_length;
|
||||
this.list_css_class = list_css_class;
|
||||
this.method = method;
|
||||
this.filters = filters;
|
||||
this.order_by = order_by;
|
||||
this.by_item_codes = by_item_codes;
|
||||
this.paginated = paginated;
|
||||
|
||||
this.on_item_click = on_item_click;
|
||||
this.img_size = img_size;
|
||||
}
|
||||
|
||||
// to be called on demand
|
||||
setup() {
|
||||
this.container = $(`
|
||||
<div class='item-list-container ${this.list_css_class}' data-page-length='${this.page_length}'>
|
||||
<div class='item-list-header'>
|
||||
<h3>${this.title}</h3>
|
||||
</div>
|
||||
<div class='item-list'></div>
|
||||
<div class='list-state'>
|
||||
<div class='loading'>
|
||||
<p class='text-muted text-center'>${__('Loading...')}</p>
|
||||
</div>
|
||||
<div class='done hide'>
|
||||
<p class='text-muted text-center'>${__('No more results')}</p>
|
||||
</div>
|
||||
<div class='more text-right'>
|
||||
<button class='btn btn-default btn-sm'>${__('More')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`)
|
||||
.appendTo(this.parent);
|
||||
|
||||
this.$item_list_title = this.container.find('.item-list-header h3');
|
||||
this.$list = this.container.find('.item-list');
|
||||
this.$loading = this.container.find('.loading').hide();
|
||||
this.$more = this.container.find('.more').hide();
|
||||
this.$done = this.container.find('.done');
|
||||
|
||||
this.$more.on('click', () => {
|
||||
this.next_page();
|
||||
});
|
||||
|
||||
this.next_page();
|
||||
}
|
||||
|
||||
refresh(filters = this.filters) {
|
||||
this.reset();
|
||||
this.set_filters(filters);
|
||||
this.next_page();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.$list.empty();
|
||||
}
|
||||
|
||||
set_filters(filters) {
|
||||
this.filters = filters;
|
||||
}
|
||||
|
||||
next_page() {
|
||||
this.$item_list_title.html(this.title);
|
||||
const start = this.$list.find('.hub-item-wrapper').length;
|
||||
this.$loading.show();
|
||||
|
||||
// build args
|
||||
let args = {
|
||||
start: start,
|
||||
// query one extra
|
||||
limit: this.page_length + 1
|
||||
};
|
||||
Object.assign(args, this.filters);
|
||||
console.log("filters: ", args);
|
||||
args.order_by = this.order_by;
|
||||
args.by_item_codes = this.by_item_codes;
|
||||
|
||||
frappe.call({
|
||||
method: this.method,
|
||||
args: args,
|
||||
callback: (r) => {
|
||||
let items = r.message;
|
||||
console.log("items: ", items);
|
||||
this.render_items(items);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render_items(items) {
|
||||
if(items) {
|
||||
let done = 0;
|
||||
console.log("items length", items.length);
|
||||
if(items.length && items.length > this.page_length) {
|
||||
// remove the extra queried
|
||||
items.pop();
|
||||
} else {
|
||||
done = 1;
|
||||
}
|
||||
items.forEach((item) => {
|
||||
this.make_item_card(item).appendTo(this.$list);
|
||||
});
|
||||
console.log(done);
|
||||
this.update_list_state(done);
|
||||
} else {
|
||||
this.$item_list_title.html('No results found');
|
||||
}
|
||||
}
|
||||
|
||||
update_list_state(done=0) {
|
||||
this.$loading.hide();
|
||||
if(done) {
|
||||
this.$done.removeClass('hide');
|
||||
this.$more.hide();
|
||||
} else {
|
||||
this.$more.show();
|
||||
this.$done.addClass('hide');
|
||||
}
|
||||
}
|
||||
|
||||
make_item_card(item) {
|
||||
let $item_card = $(`
|
||||
<div class="hub-item-wrapper" style="max-width: ${this.img_size}px;">
|
||||
<a class="item-link" href>
|
||||
<div class="hub-item-image">
|
||||
${ this.get_item_image(item) }
|
||||
</div>
|
||||
<div class="hub-item-title">
|
||||
<h5 class="bold">
|
||||
${!item.seen ? item.item_name : `<span class="indicator blue">${item.item_name}</span>`}
|
||||
<h5>
|
||||
</div>
|
||||
</a>
|
||||
<div class="company-link">
|
||||
<a data-company-name="${ item.company_id }" class="">${ item.company_name }</a>
|
||||
</div>
|
||||
<div>${ item.formatted_price ? item.formatted_price : ''}</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
$item_card.find(".item-link").click((e) => {
|
||||
e.preventDefault();
|
||||
this.on_item_click && this.on_item_click(item.name);
|
||||
});
|
||||
|
||||
return $item_card;
|
||||
}
|
||||
|
||||
get_item_image(item, size=this.img_size) {
|
||||
const _size = size + 'px';
|
||||
const item_image = item.image ?
|
||||
`<img src="${item.image}"><span class="helper"></span>` :
|
||||
`<div class="standard-image">${item.item_name[0]}</div>`;
|
||||
|
||||
return `
|
||||
<div class="img-wrapper"
|
||||
style="max-width: ${_size}; width: ${_size}; height: ${_size};">
|
||||
${item_image}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
class HubSidebar {
|
||||
constructor({ wrapper }) {
|
||||
this.wrapper = wrapper;
|
||||
this.make_dom();
|
||||
}
|
||||
|
||||
make_dom() {
|
||||
this.wrapper.html(`
|
||||
<div class="hub-sidebar overlay-sidebar hidden-xs hidden-sm">
|
||||
</div>
|
||||
`);
|
||||
|
||||
this.$sidebar = this.wrapper.find('.hub-sidebar');
|
||||
}
|
||||
|
||||
add_item(item, section) {
|
||||
let $section;
|
||||
if(!section && this.wrapper.find('.sidebar-menu').length === 0) {
|
||||
// if no section, add section with no heading
|
||||
$section = this.get_section();
|
||||
} else {
|
||||
$section = this.get_section(section);
|
||||
}
|
||||
|
||||
const $li_item = $(`
|
||||
<li><a ${item.href ? `href="${item.href}"` : ''}>${item.label}</a></li>
|
||||
`).click(
|
||||
() => item.on_click && item.on_click()
|
||||
);
|
||||
|
||||
$section.append($li_item);
|
||||
}
|
||||
|
||||
get_section(section_heading="") {
|
||||
let $section = $(this.wrapper.find(
|
||||
`[data-section-heading="${section_heading}"]`));
|
||||
if($section.length) {
|
||||
return $section;
|
||||
}
|
||||
|
||||
const $section_heading = section_heading ?
|
||||
`<li class="h6">${section_heading}</li>` : '';
|
||||
|
||||
$section = $(`
|
||||
<ul class="list-unstyled sidebar-menu" data-section-heading="${section_heading || 'default'}">
|
||||
${$section_heading}
|
||||
</ul>
|
||||
`);
|
||||
|
||||
this.$sidebar.append($section);
|
||||
return $section;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<div class="padding">
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<input class="form-control" name="search" placeholder="{%= __("Search") %}">
|
||||
</div>
|
||||
</div>
|
||||
<hr style="margin: 15px -15px">
|
||||
<div class="hub-list">
|
||||
|
||||
</div>
|
||||
<div class="loading">
|
||||
<p class="text-muted text-center">{%= __("Loading...") %}</p>
|
||||
</div>
|
||||
<div class="more text-center hide">
|
||||
<button class="btn btn-default btn-sm">{%= __("More") %}</div>
|
||||
</div>
|
||||
<div class="done text-center text-extra-muted hide">
|
||||
<p>{%= __("No more results.") %}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,16 +0,0 @@
|
||||
{% for(var i=0, l=items.length; i < l; i++) { %}
|
||||
<div class="list-item">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<h6>{%= items[i].item_name %}<h6>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<h6>{%= items[i].seller_name %}<h6>
|
||||
</div>
|
||||
<div class="col-sm-3 text-right">
|
||||
<h6 class="text-muted">{%= items[i].seller_country %}<h6>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small">{%= items[i].description %}</p>
|
||||
</div>
|
||||
{% } %}
|
||||
@@ -1,19 +0,0 @@
|
||||
<div style="padding: 70px 0px;">
|
||||
<h2 class="text-center">{%= __("Register For ERPNext Hub") %}</h2>
|
||||
<br>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<ul>
|
||||
<li>Free listing of your products</li>
|
||||
<li>Let other ERPNext users discover your products</li>
|
||||
<li>Discover products quickly</li>
|
||||
<li>Automate workflow with Supplier from within ERPNext (later)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="text-center">
|
||||
<a class="btn btn-primary" href="#Form/Hub Settings">
|
||||
Hub Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user