@@ -49,28 +49,28 @@ erpnext.PointOfSale.ItemDetails = class {
this.$item_image = this.$component.find('.item-image');
this.$form_container = this.$component.find('.form-container');
this.$dicount_section = this.$component.find('.discount-section');
- }
+ }
- toggle_item_details_section(item) {
+ toggle_item_details_section(item) {
const { item_code, batch_no, uom } = this.current_item;
const item_code_is_same = item && item_code === item.item_code;
const batch_is_same = item && batch_no == item.batch_no;
const uom_is_same = item && uom === item.uom;
- this.item_has_changed = !item ? false : item_code_is_same && batch_is_same && uom_is_same ? false : true;
+ this.item_has_changed = !item ? false : item_code_is_same && batch_is_same && uom_is_same ? false : true;
- this.events.toggle_item_selector(this.item_has_changed);
+ this.events.toggle_item_selector(this.item_has_changed);
this.toggle_component(this.item_has_changed);
-
+
if (this.item_has_changed) {
- this.doctype = item.doctype;
+ this.doctype = item.doctype;
this.item_meta = frappe.get_meta(this.doctype);
this.name = item.name;
this.item_row = item;
- this.currency = this.events.get_frm().doc.currency;
-
- this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom };
-
+ this.currency = this.events.get_frm().doc.currency;
+
+ this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom };
+
this.render_dom(item);
this.render_discount_dom(item);
this.render_form(item);
@@ -102,9 +102,9 @@ erpnext.PointOfSale.ItemDetails = class {
this.events.remove_item_from_cart();
}
}
-
- render_dom(item) {
- let { item_code ,item_name, description, image, price_list_rate } = item;
+
+ render_dom(item) {
+ let { item_code ,item_name, description, image, price_list_rate } = item;
function get_description_html() {
if (description) {
@@ -112,8 +112,8 @@ erpnext.PointOfSale.ItemDetails = class {
return description;
}
return ``;
- }
-
+ }
+
this.$item_name.html(item_name);
this.$item_description.html(get_description_html());
this.$item_price.html(format_currency(price_list_rate, this.currency));
@@ -125,9 +125,9 @@ erpnext.PointOfSale.ItemDetails = class {
this.$item_image.html(frappe.get_abbr(item_code));
}
- }
-
- render_discount_dom(item) {
+ }
+
+ render_discount_dom(item) {
if (item.discount_percentage) {
this.$dicount_section.html(
`
@@ -141,9 +141,9 @@ erpnext.PointOfSale.ItemDetails = class {
} else {
this.$dicount_section.html(``)
}
- }
+ }
- render_form(item) {
+ render_form(item) {
const fields_to_display = this.get_form_fields(item);
this.$form_container.html('');
@@ -157,7 +157,7 @@ erpnext.PointOfSale.ItemDetails = class {
const field_meta = this.item_meta.fields.find(df => df.fieldname === fieldname);
fieldname === 'discount_percentage' ? (field_meta.label = __('Discount (%)')) : '';
const me = this;
-
+
this[`${fieldname}_control`] = frappe.ui.form.make_control({
df: {
...field_meta,
@@ -174,16 +174,16 @@ erpnext.PointOfSale.ItemDetails = class {
this.make_auto_serial_selection_btn(item);
this.bind_custom_control_change_event();
- }
+ }
- get_form_fields(item) {
+ get_form_fields(item) {
const fields = ['qty', 'uom', 'rate', 'price_list_rate', 'discount_percentage', 'warehouse', 'actual_qty'];
if (item.has_serial_no) fields.push('serial_no');
if (item.has_batch_no) fields.push('batch_no');
return fields;
}
- make_auto_serial_selection_btn(item) {
+ make_auto_serial_selection_btn(item) {
if (item.has_serial_no) {
this.$form_container.append(
`
`
@@ -203,8 +203,8 @@ erpnext.PointOfSale.ItemDetails = class {
this.$form_container.find('.serial_no-control').parent().addClass('row-span-2');
}
}
-
- bind_custom_control_change_event() {
+
+ bind_custom_control_change_event() {
const me = this;
if (this.rate_control) {
this.rate_control.df.onchange = function() {
@@ -276,8 +276,8 @@ erpnext.PointOfSale.ItemDetails = class {
};
this.batch_no_control.df.onchange = function() {
me.events.set_value_in_current_cart_item('batch-no', this.value);
- me.events.form_updated(me.doctype, me.name, 'batch_no', this.value);
- me.current_item.batch_no = this.value;
+ me.events.form_updated(me.doctype, me.name, 'batch_no', this.value);
+ me.current_item.batch_no = this.value;
}
this.batch_no_control.refresh();
}
@@ -289,9 +289,9 @@ erpnext.PointOfSale.ItemDetails = class {
me.current_item.uom = this.value;
}
}
- }
-
- async auto_update_batch_no() {
+ }
+
+ async auto_update_batch_no() {
if (this.serial_no_control && this.batch_no_control) {
const selected_serial_nos = this.serial_no_control.get_value().split(`\n`).filter(s => s);
if (!selected_serial_nos.length) return;
@@ -310,9 +310,9 @@ erpnext.PointOfSale.ItemDetails = class {
const batch_no = Object.keys(batch_serial_map)[0];
const batch_serial_nos = batch_serial_map[batch_no].join(`\n`);
// eg. 10 selected serial no. -> 5 belongs to first batch other 5 belongs to second batch
- const serial_nos_belongs_to_other_batch = selected_serial_nos.length !== batch_serial_map[batch_no].length;
-
- const current_batch_no = this.batch_no_control.get_value();
+ const serial_nos_belongs_to_other_batch = selected_serial_nos.length !== batch_serial_map[batch_no].length;
+
+ const current_batch_no = this.batch_no_control.get_value();
current_batch_no != batch_no && await this.batch_no_control.set_value(batch_no);
if (serial_nos_belongs_to_other_batch) {
@@ -326,8 +326,8 @@ erpnext.PointOfSale.ItemDetails = class {
this.events.clone_new_batch_item_in_frm(batch_serial_map, this.current_item);
}
}
-
- bind_events() {
+
+ bind_events() {
this.bind_auto_serial_fetch_event();
this.bind_fields_to_numpad_fields();
@@ -345,7 +345,7 @@ erpnext.PointOfSale.ItemDetails = class {
});
}
- bind_fields_to_numpad_fields() {
+ bind_fields_to_numpad_fields() {
const me = this;
this.$form_container.on('click', '.input-with-feedback', function() {
const fieldname = $(this).attr('data-fieldname');
@@ -355,8 +355,8 @@ erpnext.PointOfSale.ItemDetails = class {
}
});
}
-
- bind_auto_serial_fetch_event() {
+
+ bind_auto_serial_fetch_event() {
this.$form_container.on('click', '.auto-fetch-btn', () => {
this.batch_no_control.set_value('');
let qty = this.qty_control.get_value();
@@ -382,7 +382,7 @@ erpnext.PointOfSale.ItemDetails = class {
frappe.msgprint(`Fetched only ${records_length} available serial numbers.`);
this.qty_control.set_value(records_length);
}
- numbers = auto_fetched_serial_numbers.join(`\n`);
+ numbers = auto_fetched_serial_numbers.join(`\n`);
this.serial_no_control.set_value(numbers);
});
})
@@ -390,5 +390,5 @@ erpnext.PointOfSale.ItemDetails = class {
toggle_component(show) {
show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
- }
+ }
}
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js
index ee0c06d45d..c87b845a41 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_selector.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js
@@ -1,115 +1,115 @@
erpnext.PointOfSale.ItemSelector = class {
- constructor({ frm, wrapper, events, pos_profile }) {
+ constructor({ frm, wrapper, events, pos_profile }) {
this.wrapper = wrapper;
this.events = events;
- this.pos_profile = pos_profile;
-
- this.inti_component();
- }
-
- inti_component() {
- this.prepare_dom();
- this.make_search_bar();
- this.load_items_data();
- this.bind_events();
- this.attach_shortcuts();
- }
+ this.pos_profile = pos_profile;
+
+ this.inti_component();
+ }
+
+ inti_component() {
+ this.prepare_dom();
+ this.make_search_bar();
+ this.load_items_data();
+ this.bind_events();
+ this.attach_shortcuts();
+ }
- prepare_dom() {
+ prepare_dom() {
this.wrapper.append(
- `
`
- );
-
- this.$component = this.wrapper.find('.items-selector');
- }
+ `
`
+ );
+
+ this.$component = this.wrapper.find('.items-selector');
+ }
- async load_items_data() {
- if (!this.item_group) {
- const res = await frappe.db.get_value("Item Group", {lft: 1, is_group: 1}, "name");
- this.parent_item_group = res.message.name;
- };
- if (!this.price_list) {
- const res = await frappe.db.get_value("POS Profile", this.pos_profile, "selling_price_list");
- this.price_list = res.message.selling_price_list;
- }
+ async load_items_data() {
+ if (!this.item_group) {
+ const res = await frappe.db.get_value("Item Group", {lft: 1, is_group: 1}, "name");
+ this.parent_item_group = res.message.name;
+ };
+ if (!this.price_list) {
+ const res = await frappe.db.get_value("POS Profile", this.pos_profile, "selling_price_list");
+ this.price_list = res.message.selling_price_list;
+ }
- this.get_items({}).then(({message}) => {
- this.render_item_list(message.items);
- });
- }
+ this.get_items({}).then(({message}) => {
+ this.render_item_list(message.items);
+ });
+ }
- get_items({start = 0, page_length = 40, search_value=''}) {
- const price_list = this.events.get_frm().doc?.selling_price_list || this.price_list;
- let { item_group, pos_profile } = this;
+ get_items({start = 0, page_length = 40, search_value=''}) {
+ const price_list = this.events.get_frm().doc?.selling_price_list || this.price_list;
+ let { item_group, pos_profile } = this;
- !item_group && (item_group = this.parent_item_group);
-
+ !item_group && (item_group = this.parent_item_group);
+
return frappe.call({
method: "erpnext.selling.page.point_of_sale.point_of_sale.get_items",
freeze: true,
- args: { start, page_length, price_list, item_group, search_value, pos_profile },
- });
+ args: { start, page_length, price_list, item_group, search_value, pos_profile },
+ });
}
render_item_list(items) {
- this.$items_container = this.$component.find('.items-container');
- this.$items_container.html('');
+ this.$items_container = this.$component.find('.items-container');
+ this.$items_container.html('');
- items.forEach(item => {
- const item_html = this.get_item_html(item);
- this.$items_container.append(item_html);
- })
- }
+ items.forEach(item => {
+ const item_html = this.get_item_html(item);
+ this.$items_container.append(item_html);
+ })
+ }
- get_item_html(item) {
- const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom } = item;
- const indicator_color = actual_qty > 10 ? "green" : actual_qty !== 0 ? "orange" : "red";
+ get_item_html(item) {
+ const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom } = item;
+ const indicator_color = actual_qty > 10 ? "green" : actual_qty !== 0 ? "orange" : "red";
- function get_item_image_html() {
- if (item_image) {
- return `
-
-
`
- } else {
- return `
- ${frappe.get_abbr(item.item_name)}
-
`
- }
- }
+ function get_item_image_html() {
+ if (item_image) {
+ return `
+
+
`
+ } else {
+ return `
+ ${frappe.get_abbr(item.item_name)}
+
`
+ }
+ }
return (
- `
- ${get_item_image_html()}
-
-
-
- ${frappe.ellipsis(item.item_name, 18)}
-
-
${format_currency(item.price_list_rate, item.currency, 0) || 0}
-
-
`
- )
- }
+ `
+ ${get_item_image_html()}
+
+
+
+ ${frappe.ellipsis(item.item_name, 18)}
+
+
${format_currency(item.price_list_rate, item.currency, 0) || 0}
+
+
`
+ )
+ }
- make_search_bar() {
- const me = this;
- this.$component.find('.search-field').html('');
- this.$component.find('.item-group-field').html('');
+ make_search_bar() {
+ const me = this;
+ this.$component.find('.search-field').html('');
+ this.$component.find('.item-group-field').html('');
this.search_field = frappe.ui.form.make_control({
df: {
@@ -119,104 +119,104 @@ erpnext.PointOfSale.ItemSelector = class {
},
parent: this.$component.find('.search-field'),
render_input: true,
- });
+ });
this.item_group_field = frappe.ui.form.make_control({
df: {
label: __('Item Group'),
fieldtype: 'Link',
options: 'Item Group',
- placeholder: __('Select item group'),
- onchange: function() {
- me.item_group = this.value;
- !me.item_group && (me.item_group = me.parent_item_group);
- me.filter_items();
- },
- get_query: function () {
- return {
- query: 'erpnext.selling.page.point_of_sale.point_of_sale.item_group_query',
- filters: {
- pos_profile: me.events.get_frm().doc?.pos_profile
- }
- }
- },
+ placeholder: __('Select item group'),
+ onchange: function() {
+ me.item_group = this.value;
+ !me.item_group && (me.item_group = me.parent_item_group);
+ me.filter_items();
+ },
+ get_query: function () {
+ return {
+ query: 'erpnext.selling.page.point_of_sale.point_of_sale.item_group_query',
+ filters: {
+ pos_profile: me.events.get_frm().doc?.pos_profile
+ }
+ }
+ },
},
- parent: this.$component.find('.item-group-field'),
+ parent: this.$component.find('.item-group-field'),
render_input: true,
- });
- this.search_field.toggle_label(false);
+ });
+ this.search_field.toggle_label(false);
this.item_group_field.toggle_label(false);
}
- bind_events() {
- const me = this;
- onScan.attachTo(document, {
- onScan: (sScancode) => {
- if (this.search_field && this.$component.is(':visible')) {
- this.search_field.set_focus();
- $(this.search_field.$input[0]).val(sScancode).trigger("input");
- this.barcode_scanned = true;
- }
- }
- });
+ bind_events() {
+ const me = this;
+ onScan.attachTo(document, {
+ onScan: (sScancode) => {
+ if (this.search_field && this.$component.is(':visible')) {
+ this.search_field.set_focus();
+ $(this.search_field.$input[0]).val(sScancode).trigger("input");
+ this.barcode_scanned = true;
+ }
+ }
+ });
this.$component.on('click', '.item-wrapper', function() {
const $item = $(this);
const item_code = unescape($item.attr('data-item-code'));
- let batch_no = unescape($item.attr('data-batch-no'));
- let serial_no = unescape($item.attr('data-serial-no'));
- let uom = unescape($item.attr('data-uom'));
-
- // escape(undefined) returns "undefined" then unescape returns "undefined"
- batch_no = batch_no === "undefined" ? undefined : batch_no;
- serial_no = serial_no === "undefined" ? undefined : serial_no;
- uom = uom === "undefined" ? undefined : uom;
+ let batch_no = unescape($item.attr('data-batch-no'));
+ let serial_no = unescape($item.attr('data-serial-no'));
+ let uom = unescape($item.attr('data-uom'));
+
+ // escape(undefined) returns "undefined" then unescape returns "undefined"
+ batch_no = batch_no === "undefined" ? undefined : batch_no;
+ serial_no = serial_no === "undefined" ? undefined : serial_no;
+ uom = uom === "undefined" ? undefined : uom;
- me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom }});
- })
+ me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom }});
+ })
- this.search_field.$input.on('input', (e) => {
+ this.search_field.$input.on('input', (e) => {
clearTimeout(this.last_search);
this.last_search = setTimeout(() => {
const search_term = e.target.value;
this.filter_items({ search_term });
}, 300);
- });
- }
+ });
+ }
- attach_shortcuts() {
- frappe.ui.keys.on("ctrl+i", () => {
- const selector_is_visible = this.$component.is(':visible');
- if (!selector_is_visible) return;
- this.search_field.set_focus();
- });
- frappe.ui.keys.on("ctrl+g", () => {
- const selector_is_visible = this.$component.is(':visible');
- if (!selector_is_visible) return;
- this.item_group_field.set_focus();
- });
- // for selecting the last filtered item on search
- frappe.ui.keys.on("enter", () => {
- const selector_is_visible = this.$component.is(':visible');
- if (!selector_is_visible || this.search_field.get_value() === "") return;
+ attach_shortcuts() {
+ frappe.ui.keys.on("ctrl+i", () => {
+ const selector_is_visible = this.$component.is(':visible');
+ if (!selector_is_visible) return;
+ this.search_field.set_focus();
+ });
+ frappe.ui.keys.on("ctrl+g", () => {
+ const selector_is_visible = this.$component.is(':visible');
+ if (!selector_is_visible) return;
+ this.item_group_field.set_focus();
+ });
+ // for selecting the last filtered item on search
+ frappe.ui.keys.on("enter", () => {
+ const selector_is_visible = this.$component.is(':visible');
+ if (!selector_is_visible || this.search_field.get_value() === "") return;
- if (this.items.length == 1) {
- this.$items_container.find(".item-wrapper").click();
- frappe.utils.play_sound("submit");
- $(this.search_field.$input[0]).val("").trigger("input");
- } else if (this.items.length == 0 && this.barcode_scanned) {
- // only show alert of barcode is scanned and enter is pressed
- frappe.show_alert({
- message: __("No items found. Scan barcode again."),
- indicator: 'orange'
- });
- frappe.utils.play_sound("error");
- this.barcode_scanned = false;
- $(this.search_field.$input[0]).val("").trigger("input");
- }
- });
- }
-
- filter_items({ search_term='' }={}) {
+ if (this.items.length == 1) {
+ this.$items_container.find(".item-wrapper").click();
+ frappe.utils.play_sound("submit");
+ $(this.search_field.$input[0]).val("").trigger("input");
+ } else if (this.items.length == 0 && this.barcode_scanned) {
+ // only show alert of barcode is scanned and enter is pressed
+ frappe.show_alert({
+ message: __("No items found. Scan barcode again."),
+ indicator: 'orange'
+ });
+ frappe.utils.play_sound("error");
+ this.barcode_scanned = false;
+ $(this.search_field.$input[0]).val("").trigger("input");
+ }
+ });
+ }
+
+ filter_items({ search_term='' }={}) {
if (search_term) {
search_term = search_term.toLowerCase();
@@ -227,39 +227,39 @@ erpnext.PointOfSale.ItemSelector = class {
this.items = items;
this.render_item_list(items);
return;
- }
+ }
}
this.get_items({ search_value: search_term })
- .then(({ message }) => {
- const { items, serial_no, batch_no, barcode } = message;
+ .then(({ message }) => {
+ const { items, serial_no, batch_no, barcode } = message;
if (search_term && !barcode) {
this.search_index[search_term] = items;
}
this.items = items;
- this.render_item_list(items);
- });
+ this.render_item_list(items);
+ });
}
-
- resize_selector(minimize) {
- minimize ?
- this.$component.find('.search-field').removeClass('mr-8') :
- this.$component.find('.search-field').addClass('mr-8');
+
+ resize_selector(minimize) {
+ minimize ?
+ this.$component.find('.search-field').removeClass('mr-8') :
+ this.$component.find('.search-field').addClass('mr-8');
- minimize ?
- this.$component.find('.filter-section').addClass('flex-col') :
- this.$component.find('.filter-section').removeClass('flex-col');
+ minimize ?
+ this.$component.find('.filter-section').addClass('flex-col') :
+ this.$component.find('.filter-section').removeClass('flex-col');
- minimize ?
- this.$component.removeClass('col-span-6').addClass('col-span-2') :
- this.$component.removeClass('col-span-2').addClass('col-span-6')
+ minimize ?
+ this.$component.removeClass('col-span-6').addClass('col-span-2') :
+ this.$component.removeClass('col-span-2').addClass('col-span-6')
- minimize ?
- this.$items_container.removeClass('grid-cols-4').addClass('grid-cols-1') :
- this.$items_container.removeClass('grid-cols-1').addClass('grid-cols-4')
- }
+ minimize ?
+ this.$items_container.removeClass('grid-cols-4').addClass('grid-cols-1') :
+ this.$items_container.removeClass('grid-cols-1').addClass('grid-cols-4')
+ }
- toggle_component(show) {
+ toggle_component(show) {
show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
- }
+ }
}
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_number_pad.js b/erpnext/selling/page/point_of_sale/pos_number_pad.js
index 2ffc2c0229..4b8e841805 100644
--- a/erpnext/selling/page/point_of_sale/pos_number_pad.js
+++ b/erpnext/selling/page/point_of_sale/pos_number_pad.js
@@ -1,49 +1,48 @@
erpnext.PointOfSale.NumberPad = class {
- constructor({ wrapper, events, cols, keys, css_classes, fieldnames_map }) {
- this.wrapper = wrapper;
- this.events = events;
- this.cols = cols;
- this.keys = keys;
- this.css_classes = css_classes || [];
- this.fieldnames = fieldnames_map || {};
+ constructor({ wrapper, events, cols, keys, css_classes, fieldnames_map }) {
+ this.wrapper = wrapper;
+ this.events = events;
+ this.cols = cols;
+ this.keys = keys;
+ this.css_classes = css_classes || [];
+ this.fieldnames = fieldnames_map || {};
- this.init_component();
- }
+ this.init_component();
+ }
- init_component() {
- this.prepare_dom();
- this.bind_events();
- }
+ init_component() {
+ this.prepare_dom();
+ this.bind_events();
+ }
- prepare_dom() {
- const { cols, keys, css_classes, fieldnames } = this;
+ prepare_dom() {
+ const { cols, keys, css_classes, fieldnames } = this;
- function get_keys() {
- return keys.reduce((a, row, i) => {
- return a + row.reduce((a2, number, j) => {
- const class_to_append = css_classes && css_classes[i] ? css_classes[i][j] : '';
- const fieldname = fieldnames && fieldnames[number] ?
- fieldnames[number] :
- typeof number === 'string' ? frappe.scrub(number) : number;
-
- return a2 + `
${number}
`
- }, '')
- }, '');
- }
+ function get_keys() {
+ return keys.reduce((a, row, i) => {
+ return a + row.reduce((a2, number, j) => {
+ const class_to_append = css_classes && css_classes[i] ? css_classes[i][j] : '';
+ const fieldname = fieldnames && fieldnames[number] ?
+ fieldnames[number] : typeof number === 'string' ? frappe.scrub(number) : number;
- this.wrapper.html(
- `
- ${get_keys()}
-
`
- )
- }
+ return a2 + `
${number}
`
+ }, '')
+ }, '');
+ }
- bind_events() {
- const me = this;
- this.wrapper.on('click', '.numpad-btn', function() {
- const $btn = $(this);
- me.events.numpad_event($btn);
- })
- }
+ this.wrapper.html(
+ `
+ ${get_keys()}
+
`
+ )
+ }
+
+ bind_events() {
+ const me = this;
+ this.wrapper.on('click', '.numpad-btn', function() {
+ const $btn = $(this);
+ me.events.numpad_event($btn);
+ });
+ }
}
\ No newline at end of file
diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
index 0a70b97648..c716aa96e0 100644
--- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
+++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
@@ -188,7 +188,7 @@ def get_conditions(filters):
conditions += "AND so.transaction_date <= '%s'" %filters.to_date
if filters.get("item_code"):
- conditions += "AND so_item.item_code = '%s'" %frappe.db.escape(filters.item_code)
+ conditions += "AND so_item.item_code = %s" %frappe.db.escape(filters.item_code)
if filters.get("customer"):
conditions += "AND so.customer = %s" %frappe.db.escape(filters.customer)
diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.js b/erpnext/selling/report/sales_analytics/sales_analytics.js
index 80874c1deb..0e565a3fb6 100644
--- a/erpnext/selling/report/sales_analytics/sales_analytics.js
+++ b/erpnext/selling/report/sales_analytics/sales_analytics.js
@@ -8,7 +8,7 @@ frappe.query_reports["Sales Analytics"] = {
fieldname: "tree_type",
label: __("Tree Type"),
fieldtype: "Select",
- options: ["Customer Group","Customer","Item Group","Item","Territory","Order Type"],
+ options: ["Customer Group", "Customer", "Item Group", "Item", "Territory", "Order Type", "Project"],
default: "Customer",
reqd: 1
},
diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py
index 4d113c8e9e..d036a1cb09 100644
--- a/erpnext/selling/report/sales_analytics/sales_analytics.py
+++ b/erpnext/selling/report/sales_analytics/sales_analytics.py
@@ -34,7 +34,7 @@ class Analytics(object):
def get_columns(self):
self.columns = [{
- "label": _(self.filters.tree_type + " ID"),
+ "label": _(self.filters.tree_type),
"options": self.filters.tree_type if self.filters.tree_type != "Order Type" else "",
"fieldname": "entity",
"fieldtype": "Link" if self.filters.tree_type != "Order Type" else "Data",
@@ -97,6 +97,10 @@ class Analytics(object):
self.get_sales_transactions_based_on_order_type()
self.get_rows_by_group()
+ elif self.filters.tree_type == "Project":
+ self.get_sales_transactions_based_on_project()
+ self.get_rows()
+
def get_sales_transactions_based_on_order_type(self):
if self.filters["value_quantity"] == 'Value':
value_field = "base_net_total"
@@ -198,6 +202,24 @@ class Analytics(object):
self.get_groups()
+ def get_sales_transactions_based_on_project(self):
+ if self.filters["value_quantity"] == 'Value':
+ value_field = "base_net_total as value_field"
+ else:
+ value_field = "total_qty as value_field"
+
+ entity = "project as entity"
+
+ self.entries = frappe.get_all(self.filters.doc_type,
+ fields=[entity, value_field, self.date_field],
+ filters={
+ "docstatus": 1,
+ "company": self.filters.company,
+ "project": ["!=", ""],
+ self.date_field: ('between', [self.filters.from_date, self.filters.to_date])
+ }
+ )
+
def get_rows(self):
self.data = []
self.get_periodic_data()
@@ -205,7 +227,7 @@ class Analytics(object):
for entity, period_data in iteritems(self.entity_periodic_data):
row = {
"entity": entity,
- "entity_name": self.entity_names.get(entity)
+ "entity_name": self.entity_names.get(entity) if hasattr(self, 'entity_names') else None
}
total = 0
for end_date in self.periodic_daterange:
diff --git a/erpnext/stock/doctype/batch/batch.json b/erpnext/stock/doctype/batch/batch.json
index 1eb457734e..943cb3401f 100644
--- a/erpnext/stock/doctype/batch/batch.json
+++ b/erpnext/stock/doctype/batch/batch.json
@@ -166,11 +166,11 @@
"idx": 1,
"image_field": "image",
"max_attachments": 5,
- "modified": "2019-10-03 22:38:45.104056",
+ "modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Stock",
"name": "Batch",
- "owner": "harshada@webnotestech.com",
+ "owner": "Administrator",
"permissions": [
{
"create": 1,
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js
index 60f6a68f49..19d0bec26c 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.js
@@ -130,12 +130,18 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend(
if (this.frm.doc.docstatus===0) {
this.frm.add_custom_button(__('Sales Order'),
function() {
+ if (!me.frm.doc.customer) {
+ frappe.throw({
+ title: __("Mandatory"),
+ message: __("Please Select a Customer")
+ });
+ }
erpnext.utils.map_current_doc({
method: "erpnext.selling.doctype.sales_order.sales_order.make_delivery_note",
source_doctype: "Sales Order",
target: me.frm,
setters: {
- customer: me.frm.doc.customer || undefined,
+ customer: me.frm.doc.customer,
},
get_query_filters: {
docstatus: 1,
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 38e5fe53a7..faeeb578fe 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -26,19 +26,19 @@ frappe.ui.form.on("Item", {
refresh: function(frm) {
if (frm.doc.is_stock_item) {
- frm.add_custom_button(__("Balance"), function() {
+ frm.add_custom_button(__("Stock Balance"), function() {
frappe.route_options = {
"item_code": frm.doc.name
}
frappe.set_route("query-report", "Stock Balance");
}, __("View"));
- frm.add_custom_button(__("Ledger"), function() {
+ frm.add_custom_button(__("Stock Ledger"), function() {
frappe.route_options = {
"item_code": frm.doc.name
}
frappe.set_route("query-report", "Stock Ledger");
}, __("View"));
- frm.add_custom_button(__("Projected"), function() {
+ frm.add_custom_button(__("Stock Projected Qty"), function() {
frappe.route_options = {
"item_code": frm.doc.name
}
diff --git a/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json b/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json
index 90a392c145..b24d621c31 100644
--- a/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json
+++ b/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json
@@ -133,11 +133,11 @@
],
"idx": 1,
"istable": 1,
- "modified": "2019-11-12 15:41:21.053462",
+ "modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Stock",
"name": "Landed Cost Item",
- "owner": "wasim@webnotestech.com",
+ "owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
diff --git a/erpnext/stock/doctype/landed_cost_purchase_receipt/landed_cost_purchase_receipt.json b/erpnext/stock/doctype/landed_cost_purchase_receipt/landed_cost_purchase_receipt.json
index f49c147682..9b2b5da9cb 100644
--- a/erpnext/stock/doctype/landed_cost_purchase_receipt/landed_cost_purchase_receipt.json
+++ b/erpnext/stock/doctype/landed_cost_purchase_receipt/landed_cost_purchase_receipt.json
@@ -173,11 +173,11 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
- "modified": "2016-07-20 10:49:34.228751",
+ "modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Stock",
"name": "Landed Cost Purchase Receipt",
- "owner": "wasim@webnotestech.com",
+ "owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
index 50c18f6282..c504e23677 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
@@ -116,12 +116,18 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend
if (this.frm.doc.docstatus == 0) {
this.frm.add_custom_button(__('Purchase Order'),
function () {
+ if (!me.frm.doc.supplier) {
+ frappe.throw({
+ title: __("Mandatory"),
+ message: __("Please Select a Supplier")
+ });
+ }
erpnext.utils.map_current_doc({
method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt",
source_doctype: "Purchase Order",
target: me.frm,
setters: {
- supplier: me.frm.doc.supplier || undefined,
+ supplier: me.frm.doc.supplier,
},
get_query_filters: {
docstatus: 1,
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 67161aa6dd..1e7153e774 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -494,8 +494,7 @@ class TestPurchaseReceipt(unittest.TestCase):
"expected_value_after_useful_life": 10,
"depreciation_method": "Straight Line",
"total_number_of_depreciations": 3,
- "frequency_of_depreciation": 1,
- "depreciation_start_date": frappe.utils.nowdate()
+ "frequency_of_depreciation": 1
})
asset.submit()
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
index c951066aa8..3643174fb4 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
@@ -73,7 +73,8 @@
"fieldname": "reference_type",
"fieldtype": "Select",
"label": "Reference Type",
- "options": "\nPurchase Receipt\nPurchase Invoice\nDelivery Note\nSales Invoice\nStock Entry"
+ "options": "\nPurchase Receipt\nPurchase Invoice\nDelivery Note\nSales Invoice\nStock Entry",
+ "reqd": 1
},
{
"fieldname": "reference_name",
@@ -84,7 +85,8 @@
"label": "Reference Name",
"oldfieldname": "purchase_receipt_no",
"oldfieldtype": "Link",
- "options": "reference_type"
+ "options": "reference_type",
+ "reqd": 1
},
{
"fieldname": "section_break_7",
@@ -231,9 +233,10 @@
],
"icon": "fa fa-search",
"idx": 1,
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-04-26 17:50:25.068222",
+ "modified": "2020-09-12 16:11:31.910508",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection",
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 9845bc2f70..1f95447951 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -23,6 +23,24 @@ frappe.ui.form.on('Stock Entry', {
}
});
+ frm.set_query('source_warehouse_address', function() {
+ return {
+ filters: {
+ link_doctype: 'Warehouse',
+ link_name: frm.doc.from_warehouse
+ }
+ }
+ });
+
+ frm.set_query('target_warehouse_address', function() {
+ return {
+ filters: {
+ link_doctype: 'Warehouse',
+ link_name: frm.doc.to_warehouse
+ }
+ }
+ });
+
frappe.db.get_value('Stock Settings', {name: 'Stock Settings'}, 'sample_retention_warehouse', (r) => {
if (r.sample_retention_warehouse) {
var filters = [
diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js
index 23700c94ee..2499c801d2 100644
--- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js
+++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js
@@ -9,13 +9,15 @@ frappe.query_reports["Batch-Wise Balance History"] = {
"fieldtype": "Date",
"width": "80",
"default": frappe.sys_defaults.year_start_date,
+ "reqd": 1
},
{
"fieldname":"to_date",
"label": __("To Date"),
"fieldtype": "Date",
"width": "80",
- "default": frappe.datetime.get_today()
+ "default": frappe.datetime.get_today(),
+ "reqd": 1
},
{
"fieldname": "item",
diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
index 2c95084b81..8f3e246e7f 100644
--- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
+++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
@@ -11,6 +11,9 @@ from frappe.utils import cint, flt, getdate
def execute(filters=None):
if not filters: filters = {}
+ if filters.from_date > filters.to_date:
+ frappe.throw(_("From Date must be before To Date"))
+
float_precision = cint(frappe.db.get_default("float_precision")) or 3
columns = get_columns(filters)
diff --git a/erpnext/support/desk_page/support/support.json b/erpnext/support/desk_page/support/support.json
index b1ad7c8aa0..28410f3a71 100644
--- a/erpnext/support/desk_page/support/support.json
+++ b/erpnext/support/desk_page/support/support.json
@@ -28,7 +28,7 @@
{
"hidden": 0,
"label": "Reports",
- "links": "[\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"Minutes to First Response for Issues\",\n \"name\": \"Minutes to First Response for Issues\",\n \"type\": \"report\"\n }\n]"
+ "links": "[\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"First Response Time for Issues\",\n \"name\": \"First Response Time for Issues\",\n \"type\": \"report\"\n }\n]"
}
],
"category": "Modules",
@@ -43,7 +43,7 @@
"idx": 0,
"is_standard": 1,
"label": "Support",
- "modified": "2020-06-04 11:54:56.124219",
+ "modified": "2020-08-11 15:49:34.307341",
"modified_by": "Administrator",
"module": "Support",
"name": "Support",
diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js
index 858564a527..fe01d4b983 100644
--- a/erpnext/support/doctype/issue/issue.js
+++ b/erpnext/support/doctype/issue/issue.js
@@ -2,10 +2,14 @@ frappe.ui.form.on("Issue", {
onload: function(frm) {
frm.email_field = "raised_by";
- frappe.db.get_value("Support Settings", {name: "Support Settings"}, "allow_resetting_service_level_agreement", (r) => {
- if (!r.allow_resetting_service_level_agreement) {
- frm.set_df_property("reset_service_level_agreement", "hidden", 1) ;
- }
+ frappe.db.get_value("Support Settings", {name: "Support Settings"},
+ ["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => {
+ if (r && r.track_service_level_agreement == "0") {
+ frm.set_df_property("service_level_section", "hidden", 1);
+ }
+ if (r && r.allow_resetting_service_level_agreement == "0") {
+ frm.set_df_property("reset_service_level_agreement", "hidden", 1);
+ }
});
if (frm.doc.service_level_agreement) {
@@ -38,7 +42,7 @@ frappe.ui.form.on("Issue", {
},
refresh: function (frm) {
- if (frm.doc.status !== "Closed" && frm.doc.agreement_fulfilled === "Ongoing") {
+ if (frm.doc.status !== "Closed" && frm.doc.agreement_status === "Ongoing") {
if (frm.doc.service_level_agreement) {
frappe.call({
'method': 'frappe.client.get',
@@ -85,14 +89,14 @@ frappe.ui.form.on("Issue", {
if (frm.doc.service_level_agreement) {
frm.dashboard.clear_headline();
- let agreement_fulfilled = (frm.doc.agreement_fulfilled == "Fulfilled") ?
+ let agreement_status = (frm.doc.agreement_status == "Fulfilled") ?
{"indicator": "green", "msg": "Service Level Agreement has been fulfilled"} :
{"indicator": "red", "msg": "Service Level Agreement Failed"};
frm.dashboard.set_headline_alert(
'
' +
'
' +
- ''+ agreement_fulfilled.msg +' ' +
+ ''+ agreement_status.msg +' ' +
'
' +
'
'
);
@@ -198,13 +202,13 @@ function set_time_to_resolve_and_response(frm) {
frm.dashboard.clear_headline();
var time_to_respond = get_status(frm.doc.response_by_variance);
- if (!frm.doc.first_responded_on && frm.doc.agreement_fulfilled === "Ongoing") {
- time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_fulfilled);
+ if (!frm.doc.first_responded_on && frm.doc.agreement_status === "Ongoing") {
+ time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_status);
}
var time_to_resolve = get_status(frm.doc.resolution_by_variance);
- if (!frm.doc.resolution_date && frm.doc.agreement_fulfilled === "Ongoing") {
- time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_fulfilled);
+ if (!frm.doc.resolution_date && frm.doc.agreement_status === "Ongoing") {
+ time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status);
}
frm.dashboard.set_headline_alert(
@@ -219,10 +223,10 @@ function set_time_to_resolve_and_response(frm) {
);
}
-function get_time_left(timestamp, agreement_fulfilled) {
+function get_time_left(timestamp, agreement_status) {
const diff = moment(timestamp).diff(moment());
const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : "Failed";
- let indicator = (diff_display == 'Failed' && agreement_fulfilled != "Fulfilled") ? "red" : "green";
+ let indicator = (diff_display == 'Failed' && agreement_status != "Fulfilled") ? "red" : "green";
return {"diff_display": diff_display, "indicator": indicator};
}
diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json
index 6525ab27d3..a43381c5c6 100644
--- a/erpnext/support/doctype/issue/issue.json
+++ b/erpnext/support/doctype/issue/issue.json
@@ -27,17 +27,25 @@
"response_by_variance",
"reset_service_level_agreement",
"cb",
- "agreement_fulfilled",
+ "agreement_status",
"resolution_by",
"resolution_by_variance",
"service_level_agreement_creation",
"on_hold_since",
"total_hold_time",
"response",
- "mins_to_first_response",
+ "first_response_time",
"first_responded_on",
"column_break_26",
"avg_response_time",
+ "section_break_19",
+ "resolution_details",
+ "column_break1",
+ "opening_date",
+ "opening_time",
+ "resolution_date",
+ "resolution_time",
+ "user_resolution_time",
"additional_info",
"lead",
"contact",
@@ -46,23 +54,14 @@
"customer_name",
"project",
"company",
- "section_break_19",
- "resolution_details",
- "column_break1",
- "opening_date",
- "opening_time",
- "resolution_date",
- "content_type",
- "attachment",
"via_customer_portal",
- "resolution_time",
- "user_resolution_time"
+ "attachment",
+ "content_type"
],
"fields": [
{
"fieldname": "subject_section",
"fieldtype": "Section Break",
- "label": "Subject",
"options": "fa fa-flag"
},
{
@@ -158,7 +157,7 @@
"collapsible": 1,
"fieldname": "service_level_section",
"fieldtype": "Section Break",
- "label": "Service Level"
+ "label": "Service Level Agreement Details"
},
{
"fieldname": "service_level_agreement",
@@ -191,14 +190,7 @@
"collapsible": 1,
"fieldname": "response",
"fieldtype": "Section Break",
- "label": "Response"
- },
- {
- "bold": 1,
- "fieldname": "mins_to_first_response",
- "fieldtype": "Float",
- "label": "Mins to First Response",
- "read_only": 1
+ "label": "Response Details"
},
{
"fieldname": "first_responded_on",
@@ -261,7 +253,7 @@
"collapsible": 1,
"fieldname": "section_break_19",
"fieldtype": "Section Break",
- "label": "Resolution"
+ "label": "Resolution Details"
},
{
"depends_on": "eval:!doc.__islocal",
@@ -326,28 +318,19 @@
"fieldtype": "Check",
"label": "Via Customer Portal"
},
- {
- "default": "Ongoing",
- "depends_on": "eval: doc.service_level_agreement",
- "fieldname": "agreement_fulfilled",
- "fieldtype": "Select",
- "label": "Service Level Agreement Fulfilled",
- "options": "Ongoing\nFulfilled\nFailed",
- "read_only": 1
- },
{
"depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';",
- "description": "in hours",
"fieldname": "response_by_variance",
- "fieldtype": "Float",
+ "fieldtype": "Duration",
+ "hide_seconds": 1,
"label": "Response By Variance",
"read_only": 1
},
{
"depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';",
- "description": "in hours",
"fieldname": "resolution_by_variance",
- "fieldtype": "Float",
+ "fieldtype": "Duration",
+ "hide_seconds": 1,
"label": "Resolution By Variance",
"read_only": 1
},
@@ -406,12 +389,28 @@
"fieldtype": "Duration",
"label": "Total Hold Time",
"read_only": 1
+ },
+ {
+ "default": "Ongoing",
+ "depends_on": "eval: doc.service_level_agreement",
+ "fieldname": "agreement_status",
+ "fieldtype": "Select",
+ "label": "Service Level Agreement Status",
+ "options": "Ongoing\nFulfilled\nFailed",
+ "read_only": 1
+ },
+ {
+ "bold": 1,
+ "fieldname": "first_response_time",
+ "fieldtype": "Duration",
+ "label": "First Response Time",
+ "read_only": 1
}
],
"icon": "fa fa-ticket",
"idx": 7,
"links": [],
- "modified": "2020-06-10 12:47:37.146914",
+ "modified": "2020-08-11 18:49:07.574769",
"modified_by": "Administrator",
"module": "Support",
"name": "Issue",
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index 87168e151e..920c13c38d 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -61,7 +61,7 @@ class Issue(Document):
if self.status in ["Closed", "Resolved"] and status not in ["Resolved", "Closed"]:
self.resolution_date = frappe.flags.current_time or now_datetime()
- if frappe.db.get_value("Issue", self.name, "agreement_fulfilled") == "Ongoing":
+ if frappe.db.get_value("Issue", self.name, "agreement_status") == "Ongoing":
set_service_level_agreement_variance(issue=self.name)
self.update_agreement_status()
set_resolution_time(issue=self)
@@ -72,7 +72,7 @@ class Issue(Document):
self.resolution_date = None
self.reset_issue_metrics()
# enable SLA and variance on Reopen
- self.agreement_fulfilled = "Ongoing"
+ self.agreement_status = "Ongoing"
set_service_level_agreement_variance(issue=self.name)
self.handle_hold_time(status)
@@ -113,39 +113,39 @@ class Issue(Document):
if not self.first_responded_on:
response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
response_by = add_to_date(response_by, seconds=round(last_hold_time))
- response_by_variance = round(time_diff_in_hours(response_by, now_time))
+ response_by_variance = round(time_diff_in_seconds(response_by, now_time))
update_values['response_by'] = response_by
- update_values['response_by_variance'] = response_by_variance + (last_hold_time // 3600)
+ update_values['response_by_variance'] = response_by_variance + last_hold_time
resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time))
- resolution_by_variance = round(time_diff_in_hours(resolution_by, now_time))
+ resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time))
update_values['resolution_by'] = resolution_by
- update_values['resolution_by_variance'] = resolution_by_variance + (last_hold_time // 3600)
+ update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time
update_values['on_hold_since'] = None
self.db_set(update_values)
def update_agreement_status(self):
- if self.service_level_agreement and self.agreement_fulfilled == "Ongoing":
+ if self.service_level_agreement and self.agreement_status == "Ongoing":
if frappe.db.get_value("Issue", self.name, "response_by_variance") < 0 or \
frappe.db.get_value("Issue", self.name, "resolution_by_variance") < 0:
- self.agreement_fulfilled = "Failed"
+ self.agreement_status = "Failed"
else:
- self.agreement_fulfilled = "Fulfilled"
+ self.agreement_status = "Fulfilled"
- def update_agreement_fulfilled_on_custom_status(self):
+ def update_agreement_status_on_custom_status(self):
"""
Update Agreement Fulfilled status using Custom Scripts for Custom Issue Status
"""
if not self.first_responded_on: # first_responded_on set when first reply is sent to customer
- self.response_by_variance = round(time_diff_in_hours(self.response_by, now_datetime()), 2)
+ self.response_by_variance = round(time_diff_in_seconds(self.response_by, now_datetime()), 2)
if not self.resolution_date: # resolution_date set when issue has been closed
- self.resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_datetime()), 2)
+ self.resolution_by_variance = round(time_diff_in_seconds(self.resolution_by, now_datetime()), 2)
- self.agreement_fulfilled = "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed"
+ self.agreement_status = "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed"
def create_communication(self):
communication = frappe.new_doc("Communication")
@@ -172,7 +172,7 @@ class Issue(Document):
replicated_issue = deepcopy(self)
replicated_issue.subject = subject
replicated_issue.issue_split_from = self.name
- replicated_issue.mins_to_first_response = 0
+ replicated_issue.first_response_time = 0
replicated_issue.first_responded_on = None
replicated_issue.creation = now_datetime()
@@ -180,7 +180,7 @@ class Issue(Document):
if replicated_issue.service_level_agreement:
replicated_issue.service_level_agreement_creation = now_datetime()
replicated_issue.service_level_agreement = None
- replicated_issue.agreement_fulfilled = "Ongoing"
+ replicated_issue.agreement_status = "Ongoing"
replicated_issue.response_by = None
replicated_issue.response_by_variance = None
replicated_issue.resolution_by = None
@@ -241,8 +241,8 @@ class Issue(Document):
self.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
self.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
- self.response_by_variance = round(time_diff_in_hours(self.response_by, now_datetime()))
- self.resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_datetime()))
+ self.response_by_variance = round(time_diff_in_seconds(self.response_by, now_datetime()))
+ self.resolution_by_variance = round(time_diff_in_seconds(self.resolution_by, now_datetime()))
def change_service_level_agreement_and_priority(self):
if self.service_level_agreement and frappe.db.exists("Issue", self.name) and \
@@ -271,7 +271,7 @@ class Issue(Document):
self.service_level_agreement_creation = now_datetime()
self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement)
- self.agreement_fulfilled = "Ongoing"
+ self.agreement_status = "Ongoing"
self.save()
def reset_issue_metrics(self):
@@ -347,7 +347,7 @@ def get_expected_time_for(parameter, service_level, start_date_time):
def set_service_level_agreement_variance(issue=None):
current_time = frappe.flags.current_time or now_datetime()
- filters = {"status": "Open", "agreement_fulfilled": "Ongoing"}
+ filters = {"status": "Open", "agreement_status": "Ongoing"}
if issue:
filters = {"name": issue}
@@ -358,13 +358,13 @@ def set_service_level_agreement_variance(issue=None):
variance = round(time_diff_in_hours(doc.response_by, current_time), 2)
frappe.db.set_value(dt="Issue", dn=doc.name, field="response_by_variance", val=variance, update_modified=False)
if variance < 0:
- frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_fulfilled", val="Failed", update_modified=False)
+ frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False)
if not doc.resolution_date: # resolution_date set when issue has been closed
variance = round(time_diff_in_hours(doc.resolution_by, current_time), 2)
frappe.db.set_value(dt="Issue", dn=doc.name, field="resolution_by_variance", val=variance, update_modified=False)
if variance < 0:
- frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_fulfilled", val="Failed", update_modified=False)
+ frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False)
def set_resolution_time(issue):
diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py
index fb8ceb53b2..c962dc6b31 100644
--- a/erpnext/support/doctype/issue/test_issue.py
+++ b/erpnext/support/doctype/issue/test_issue.py
@@ -73,7 +73,7 @@ class TestIssue(unittest.TestCase):
issue.status = 'Closed'
issue.save()
- self.assertEqual(issue.agreement_fulfilled, 'Fulfilled')
+ self.assertEqual(issue.agreement_status, 'Fulfilled')
def test_issue_metrics(self):
creation = datetime.datetime(2020, 3, 4, 4, 0)
diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.json b/erpnext/support/doctype/warranty_claim/warranty_claim.json
index ae1a7a569d..88ee4a3beb 100644
--- a/erpnext/support/doctype/warranty_claim/warranty_claim.json
+++ b/erpnext/support/doctype/warranty_claim/warranty_claim.json
@@ -361,11 +361,11 @@
],
"icon": "fa fa-bug",
"idx": 1,
- "modified": "2019-05-24 10:56:30.626200",
+ "modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Support",
"name": "Warranty Claim",
- "owner": "harshada@webnotestech.com",
+ "owner": "Administrator",
"permissions": [
{
"create": 1,
diff --git a/erpnext/support/report/first_response_time_for_issues/__init__.py b/erpnext/support/report/first_response_time_for_issues/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js
new file mode 100644
index 0000000000..576e0b76da
--- /dev/null
+++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js
@@ -0,0 +1,44 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["First Response Time for Issues"] = {
+ "filters": [
+ {
+ "fieldname": "from_date",
+ "label": __("From Date"),
+ "fieldtype": "Date",
+ "reqd": 1,
+ "default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30)
+ },
+ {
+ "fieldname": "to_date",
+ "label": __("To Date"),
+ "fieldtype": "Date",
+ "reqd": 1,
+ "default":frappe.datetime.nowdate()
+ }
+ ],
+ get_chart_data: function(_columns, result) {
+ return {
+ data: {
+ labels: result.map(d => d[0]),
+ datasets: [{
+ name: 'First Response Time',
+ values: result.map(d => d[1])
+ }]
+ },
+ type: "line",
+ tooltipOptions: {
+ formatTooltipY: d => {
+ let duration_options = {
+ hide_days: 0,
+ hide_seconds: 0
+ };
+ value = frappe.utils.get_formatted_duration(d, duration_options);
+ return value;
+ }
+ }
+ }
+ }
+};
diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.json b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.json
new file mode 100644
index 0000000000..c4fe6f5193
--- /dev/null
+++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.json
@@ -0,0 +1,26 @@
+{
+ "add_total_row": 0,
+ "creation": "2020-08-10 18:12:42.391224",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "idx": 0,
+ "is_standard": "Yes",
+ "letter_head": "Test 2",
+ "modified": "2020-08-10 18:12:42.391224",
+ "modified_by": "Administrator",
+ "module": "Support",
+ "name": "First Response Time for Issues",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "query": "select date(creation) as creation_date, avg(mins_to_first_response) from tabIssue where creation > '2016-05-01' group by date(creation) order by creation_date;",
+ "ref_doctype": "Issue",
+ "report_name": "First Response Time for Issues",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Support Team"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py
new file mode 100644
index 0000000000..922da2b33d
--- /dev/null
+++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py
@@ -0,0 +1,35 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute(filters=None):
+ columns = [
+ {
+ 'fieldname': 'creation_date',
+ 'label': 'Date',
+ 'fieldtype': 'Date',
+ 'width': 300
+ },
+ {
+ 'fieldname': 'first_response_time',
+ 'fieldtype': 'Duration',
+ 'label': 'First Response Time',
+ 'width': 300
+ },
+ ]
+
+ data = frappe.db.sql('''
+ SELECT
+ date(creation) as creation_date,
+ avg(first_response_time) as avg_response_time
+ FROM tabIssue
+ WHERE
+ date(creation) between %s and %s
+ and first_response_time > 0
+ GROUP BY creation_date
+ ORDER BY creation_date desc
+ ''', (filters.from_date, filters.to_date))
+
+ return columns, data
\ No newline at end of file
diff --git a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.js b/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.js
deleted file mode 100644
index 034e7779a6..0000000000
--- a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.js
+++ /dev/null
@@ -1,30 +0,0 @@
-frappe.query_reports["Minutes to First Response for Issues"] = {
- "filters": [
- {
- "fieldname":"from_date",
- "label": __("From Date"),
- "fieldtype": "Date",
- 'reqd': 1,
- "default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30)
- },
- {
- "fieldname":"to_date",
- "label": __("To Date"),
- "fieldtype": "Date",
- 'reqd': 1,
- "default":frappe.datetime.nowdate()
- },
- ],
- get_chart_data: function(columns, result) {
- return {
- data: {
- labels: result.map(d => d[0]),
- datasets: [{
- name: 'Mins to first response',
- values: result.map(d => d[1])
- }]
- },
- type: 'line',
- }
- }
-}
diff --git a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.json b/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.json
deleted file mode 100644
index 539d3d941f..0000000000
--- a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "add_total_row": 0,
- "apply_user_permissions": 1,
- "creation": "2016-06-14 17:44:26.034112",
- "disabled": 0,
- "docstatus": 0,
- "doctype": "Report",
- "idx": 2,
- "is_standard": "Yes",
- "modified": "2017-02-24 20:06:18.391100",
- "modified_by": "Administrator",
- "module": "Support",
- "name": "Minutes to First Response for Issues",
- "owner": "Administrator",
- "query": "select date(creation) as creation_date, avg(mins_to_first_response) from tabIssue where creation > '2016-05-01' group by date(creation) order by creation_date;",
- "ref_doctype": "Issue",
- "report_name": "Minutes to First Response for Issues",
- "report_type": "Script Report",
- "roles": [
- {
- "role": "Support Team"
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.py b/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.py
deleted file mode 100644
index 57c2d442b2..0000000000
--- a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute(filters=None):
- columns = [
- {
- 'fieldname': 'creation_date',
- 'label': 'Date',
- 'fieldtype': 'Date'
- },
- {
- 'fieldname': 'mins',
- 'fieldtype': 'Float',
- 'label': 'Mins to First Response'
- },
- ]
-
- data = frappe.db.sql('''select date(creation) as creation_date,
- avg(mins_to_first_response) as mins
- from tabIssue
- where date(creation) between %s and %s
- and mins_to_first_response > 0
- group by creation_date order by creation_date desc''', (filters.from_date, filters.to_date))
-
- return columns, data
diff --git a/erpnext/utilities/activation.py b/erpnext/utilities/activation.py
index 63c36b35d1..7b17c8c464 100644
--- a/erpnext/utilities/activation.py
+++ b/erpnext/utilities/activation.py
@@ -11,8 +11,39 @@ def get_level():
activation_level = 0
sales_data = []
min_count = 0
- doctypes = {"Item": 5, "Customer": 5, "Sales Order": 2, "Sales Invoice": 2, "Purchase Order": 2, "Employee": 3, "Lead": 3, "Quotation": 3,
- "Payment Entry": 2, "User": 5, "Student": 5, "Instructor": 5, "BOM": 3, "Journal Entry": 3, "Stock Entry": 3}
+ doctypes = {
+ "Asset": 5,
+ "BOM": 3,
+ "Customer": 5,
+ "Delivery Note": 5,
+ "Employee": 3,
+ "Instructor": 5,
+ "Instructor": 5,
+ "Issue": 5,
+ "Item": 5,
+ "Journal Entry": 3,
+ "Lead": 3,
+ "Leave Application": 5,
+ "Material Request": 5,
+ "Opportunity": 5,
+ "Payment Entry": 2,
+ "Project": 5,
+ "Purchase Order": 2,
+ "Purchase Invoice": 5,
+ "Purchase Receipt": 5,
+ "Quotation": 3,
+ "Salary Slip": 5,
+ "Salary Structure": 5,
+ "Sales Order": 2,
+ "Sales Invoice": 2,
+ "Stock Entry": 3,
+ "Student": 5,
+ "Supplier": 5,
+ "Task": 5,
+ "User": 5,
+ "Work Order": 5
+ }
+
for doctype, min_count in iteritems(doctypes):
count = frappe.db.count(doctype)
if count > min_count:
diff --git a/erpnext/utilities/desk_page/utilities/utilities.json b/erpnext/utilities/desk_page/utilities/utilities.json
new file mode 100644
index 0000000000..591eab5ed4
--- /dev/null
+++ b/erpnext/utilities/desk_page/utilities/utilities.json
@@ -0,0 +1,29 @@
+{
+ "cards": [
+ {
+ "hidden": 0,
+ "label": "Video",
+ "links": "[\n {\n \"description\": \"Video\",\n \"label\": \"Video\",\n \"name\": \"Video\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Video settings\",\n \"label\": \"Video Settings\",\n \"name\": \"Video Settings\",\n \"type\": \"doctype\"\n }\n]"
+ }
+ ],
+ "category": "Modules",
+ "charts": [],
+ "creation": "2020-09-10 12:21:22.335307",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Desk Page",
+ "extends_another_page": 0,
+ "hide_custom": 0,
+ "idx": 0,
+ "is_standard": 1,
+ "label": "Utilities",
+ "modified": "2020-09-10 12:33:30.089853",
+ "modified_by": "user@erpnext.com",
+ "module": "Utilities",
+ "name": "Utilities",
+ "owner": "user@erpnext.com",
+ "pin_to_bottom": 1,
+ "pin_to_top": 0,
+ "shortcuts": []
+}
\ No newline at end of file
diff --git a/erpnext/utilities/doctype/video/video.js b/erpnext/utilities/doctype/video/video.js
index 056bd3ccd6..9cb5a155ad 100644
--- a/erpnext/utilities/doctype/video/video.js
+++ b/erpnext/utilities/doctype/video/video.js
@@ -2,7 +2,16 @@
// For license information, please see license.txt
frappe.ui.form.on('Video', {
- // refresh: function(frm) {
+ refresh: function (frm) {
+ frm.events.toggle_youtube_statistics_section(frm);
+ frm.add_custom_button("Watch Video", () => frappe.help.show_video(frm.doc.url, frm.doc.title));
+ },
- // }
+ toggle_youtube_statistics_section: (frm) => {
+ if (frm.doc.provider === "YouTube") {
+ frappe.db.get_single_value("Video Settings", "enable_youtube_tracking").then( val => {
+ frm.toggle_display("youtube_tracking_section", val);
+ });
+ }
+ }
});
diff --git a/erpnext/utilities/doctype/video/video.json b/erpnext/utilities/doctype/video/video.json
index 5d2cc13348..2a82db2514 100644
--- a/erpnext/utilities/doctype/video/video.json
+++ b/erpnext/utilities/doctype/video/video.json
@@ -11,11 +11,19 @@
"title",
"provider",
"url",
+ "youtube_video_id",
"column_break_4",
"publish_date",
"duration",
+ "youtube_tracking_section",
+ "like_count",
+ "view_count",
+ "col_break",
+ "dislike_count",
+ "comment_count",
"section_break_7",
- "description"
+ "description",
+ "image"
],
"fields": [
{
@@ -37,7 +45,6 @@
{
"fieldname": "url",
"fieldtype": "Data",
- "in_list_view": 1,
"label": "URL",
"reqd": 1
},
@@ -48,11 +55,12 @@
{
"fieldname": "publish_date",
"fieldtype": "Date",
+ "in_list_view": 1,
"label": "Publish Date"
},
{
"fieldname": "duration",
- "fieldtype": "Data",
+ "fieldtype": "Duration",
"label": "Duration"
},
{
@@ -62,13 +70,67 @@
{
"fieldname": "description",
"fieldtype": "Text Editor",
- "in_list_view": 1,
"label": "Description",
"reqd": 1
+ },
+ {
+ "fieldname": "like_count",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Likes",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "view_count",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Views",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "col_break",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "dislike_count",
+ "fieldtype": "Float",
+ "label": "Dislikes",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "comment_count",
+ "fieldtype": "Float",
+ "label": "Comments",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "image",
+ "fieldtype": "Attach Image",
+ "hidden": 1,
+ "label": "Image",
+ "no_copy": 1
+ },
+ {
+ "depends_on": "eval:doc.provider==\"YouTube\"",
+ "fieldname": "youtube_tracking_section",
+ "fieldtype": "Section Break",
+ "label": "Youtube Statistics"
+ },
+ {
+ "fieldname": "youtube_video_id",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Youtube ID",
+ "read_only": 1
}
],
+ "image_field": "image",
"links": [],
- "modified": "2020-07-21 19:29:46.603734",
+ "modified": "2020-09-07 17:02:20.185794",
"modified_by": "Administrator",
"module": "Utilities",
"name": "Video",
diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py
index 3c17b560f3..c2e414eef8 100644
--- a/erpnext/utilities/doctype/video/video.py
+++ b/erpnext/utilities/doctype/video/video.py
@@ -3,8 +3,144 @@
# For license information, please see license.txt
from __future__ import unicode_literals
-# import frappe
+import frappe
+import re
+import pytz
from frappe.model.document import Document
+from frappe import _
+from datetime import datetime
+from six import string_types
+from pyyoutube import Api
class Video(Document):
- pass
+ def validate(self):
+ if self.provider == "YouTube" and is_tracking_enabled():
+ self.set_video_id()
+ self.set_youtube_statistics()
+
+ def set_video_id(self):
+ if self.url and not self.get("youtube_video_id"):
+ self.youtube_video_id = get_id_from_url(self.url)
+
+ def set_youtube_statistics(self):
+ api_key = frappe.db.get_single_value("Video Settings", "api_key")
+ api = Api(api_key=api_key)
+
+ try:
+ video = api.get_video_by_id(video_id=self.youtube_video_id)
+ video_stats = video.items[0].to_dict().get('statistics')
+
+ self.like_count = video_stats.get('likeCount')
+ self.view_count = video_stats.get('viewCount')
+ self.dislike_count = video_stats.get('dislikeCount')
+ self.comment_count = video_stats.get('commentCount')
+
+ except Exception:
+ title = "Failed to Update YouTube Statistics for Video: {0}".format(self.name)
+ frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title)
+
+def is_tracking_enabled():
+ return frappe.db.get_single_value("Video Settings", "enable_youtube_tracking")
+
+
+def get_frequency(value):
+ # Return numeric value from frequency field, return 1 as fallback default value: 1 hour
+ if value != "Daily":
+ return frappe.utils.cint(value[:2].strip())
+ elif value:
+ return 24
+ return 1
+
+
+def update_youtube_data():
+ # Called every 30 minutes via hooks
+ enable_youtube_tracking, frequency = frappe.db.get_value("Video Settings", "Video Settings", ["enable_youtube_tracking", "frequency"])
+
+ if not enable_youtube_tracking:
+ return
+
+ frequency = get_frequency(frequency)
+ time = datetime.now()
+ timezone = pytz.timezone(frappe.utils.get_time_zone())
+ site_time = time.astimezone(timezone)
+
+ if frequency == 30:
+ batch_update_youtube_data()
+ elif site_time.hour % frequency == 0 and site_time.minute < 15:
+ # make sure it runs within the first 15 mins of the hour
+ batch_update_youtube_data()
+
+
+def get_formatted_ids(video_list):
+ # format ids to comma separated string for bulk request
+ ids = []
+ for video in video_list:
+ ids.append(video.youtube_video_id)
+
+ return ','.join(ids)
+
+
+@frappe.whitelist()
+def get_id_from_url(url):
+ """
+ Returns video id from url
+ :param youtube url: String URL
+ """
+ if not isinstance(url, string_types):
+ frappe.throw(_("URL can only be a string"), title=_("Invalid URL"))
+
+ pattern = re.compile(r'[a-z\:\//\.]+(youtube|youtu)\.(com|be)/(watch\?v=|embed/|.+\?v=)?([^"&?\s]{11})?')
+ id = pattern.match(url)
+ return id.groups()[-1]
+
+
+@frappe.whitelist()
+def batch_update_youtube_data():
+ def get_youtube_statistics(video_ids):
+ api_key = frappe.db.get_single_value("Video Settings", "api_key")
+ api = Api(api_key=api_key)
+ try:
+ video = api.get_video_by_id(video_id=video_ids)
+ video_stats = video.items
+ return video_stats
+ except Exception:
+ title = "Failed to Update YouTube Statistics"
+ frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title)
+
+ def prepare_and_set_data(video_list):
+ video_ids = get_formatted_ids(video_list)
+ stats = get_youtube_statistics(video_ids)
+ set_youtube_data(stats)
+
+ def set_youtube_data(entries):
+ for entry in entries:
+ video_stats = entry.to_dict().get('statistics')
+ video_id = entry.to_dict().get('id')
+ stats = {
+ 'like_count' : video_stats.get('likeCount'),
+ 'view_count' : video_stats.get('viewCount'),
+ 'dislike_count' : video_stats.get('dislikeCount'),
+ 'comment_count' : video_stats.get('commentCount'),
+ 'video_id': video_id
+ }
+
+ frappe.db.sql("""
+ UPDATE `tabVideo`
+ SET
+ like_count = %(like_count)s,
+ view_count = %(view_count)s,
+ dislike_count = %(dislike_count)s,
+ comment_count = %(comment_count)s
+ WHERE youtube_video_id = %(video_id)s""", stats)
+
+ video_list = frappe.get_all("Video", fields=["youtube_video_id"])
+ if len(video_list) > 50:
+ # Update in batches of 50
+ start, end = 0, 50
+ while start < len(video_list):
+ batch = video_list[start:end]
+ prepare_and_set_data(batch)
+ start += 50
+ end += 50
+ else:
+ prepare_and_set_data(video_list)
diff --git a/erpnext/utilities/doctype/video/video_list.js b/erpnext/utilities/doctype/video/video_list.js
new file mode 100644
index 0000000000..8273a4a781
--- /dev/null
+++ b/erpnext/utilities/doctype/video/video_list.js
@@ -0,0 +1,7 @@
+frappe.listview_settings["Video"] = {
+ onload: (listview) => {
+ listview.page.add_menu_item(__("Video Settings"), function() {
+ frappe.set_route("Form","Video Settings", "Video Settings");
+ });
+ }
+}
\ No newline at end of file
diff --git a/erpnext/utilities/doctype/video_settings/__init__.py b/erpnext/utilities/doctype/video_settings/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/utilities/doctype/video_settings/test_video_settings.py b/erpnext/utilities/doctype/video_settings/test_video_settings.py
new file mode 100644
index 0000000000..b217afe3d8
--- /dev/null
+++ b/erpnext/utilities/doctype/video_settings/test_video_settings.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestVideoSettings(unittest.TestCase):
+ pass
diff --git a/erpnext/utilities/doctype/video_settings/video_settings.js b/erpnext/utilities/doctype/video_settings/video_settings.js
new file mode 100644
index 0000000000..9ac8b9ec16
--- /dev/null
+++ b/erpnext/utilities/doctype/video_settings/video_settings.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Video Settings', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/utilities/doctype/video_settings/video_settings.json b/erpnext/utilities/doctype/video_settings/video_settings.json
new file mode 100644
index 0000000000..fb3274decd
--- /dev/null
+++ b/erpnext/utilities/doctype/video_settings/video_settings.json
@@ -0,0 +1,60 @@
+{
+ "actions": [],
+ "creation": "2020-08-02 03:50:21.339609",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "enable_youtube_tracking",
+ "api_key",
+ "frequency"
+ ],
+ "fields": [
+ {
+ "default": "0",
+ "fieldname": "enable_youtube_tracking",
+ "fieldtype": "Check",
+ "label": "Enable YouTube Tracking"
+ },
+ {
+ "depends_on": "eval:doc.enable_youtube_tracking",
+ "fieldname": "api_key",
+ "fieldtype": "Data",
+ "label": "API Key",
+ "mandatory_depends_on": "eval:doc.enable_youtube_tracking"
+ },
+ {
+ "default": "1 hr",
+ "depends_on": "eval:doc.enable_youtube_tracking",
+ "fieldname": "frequency",
+ "fieldtype": "Select",
+ "label": "Frequency",
+ "mandatory_depends_on": "eval:doc.enable_youtube_tracking",
+ "options": "30 mins\n1 hr\n6 hrs\nDaily"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2020-09-07 16:09:00.360668",
+ "modified_by": "Administrator",
+ "module": "Utilities",
+ "name": "Video Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/utilities/doctype/video_settings/video_settings.py b/erpnext/utilities/doctype/video_settings/video_settings.py
new file mode 100644
index 0000000000..36fb54f015
--- /dev/null
+++ b/erpnext/utilities/doctype/video_settings/video_settings.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from apiclient.discovery import build
+
+class VideoSettings(Document):
+ def validate(self):
+ self.validate_youtube_api_key()
+
+ def validate_youtube_api_key(self):
+ if self.enable_youtube_tracking and self.api_key:
+ try:
+ build("youtube", "v3", developerKey=self.api_key)
+ except Exception:
+ title = _("Failed to Authenticate the API key.")
+ frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title)
+ frappe.throw(title + " Please check the error logs.", title=_("Invalid Credentials"))
\ No newline at end of file
diff --git a/erpnext/utilities/report/__init__.py b/erpnext/utilities/report/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/utilities/report/youtube_interactions/__init__.py b/erpnext/utilities/report/youtube_interactions/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/utilities/report/youtube_interactions/youtube_interactions.js b/erpnext/utilities/report/youtube_interactions/youtube_interactions.js
new file mode 100644
index 0000000000..6e3e4e6980
--- /dev/null
+++ b/erpnext/utilities/report/youtube_interactions/youtube_interactions.js
@@ -0,0 +1,20 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["YouTube Interactions"] = {
+ "filters": [
+ {
+ fieldname: "from_date",
+ label: __("From Date"),
+ fieldtype: "Date",
+ default: frappe.datetime.add_months(frappe.datetime.now_date(), -12),
+ },
+ {
+ fieldname:"to_date",
+ label: __("To Date"),
+ fieldtype: "Date",
+ default: frappe.datetime.now_date(),
+ }
+ ]
+};
diff --git a/erpnext/utilities/report/youtube_interactions/youtube_interactions.json b/erpnext/utilities/report/youtube_interactions/youtube_interactions.json
new file mode 100644
index 0000000000..a40247b6df
--- /dev/null
+++ b/erpnext/utilities/report/youtube_interactions/youtube_interactions.json
@@ -0,0 +1,27 @@
+{
+ "add_total_row": 0,
+ "creation": "2020-08-02 05:05:00.457093",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2020-08-02 05:05:00.457093",
+ "modified_by": "Administrator",
+ "module": "Utilities",
+ "name": "YouTube Interactions",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Video",
+ "report_name": "YouTube Interactions",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "All"
+ },
+ {
+ "role": "System Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/utilities/report/youtube_interactions/youtube_interactions.py b/erpnext/utilities/report/youtube_interactions/youtube_interactions.py
new file mode 100644
index 0000000000..3516a35097
--- /dev/null
+++ b/erpnext/utilities/report/youtube_interactions/youtube_interactions.py
@@ -0,0 +1,113 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+from frappe.utils import flt
+
+def execute(filters=None):
+ if not frappe.db.get_single_value("Video Settings", "enable_youtube_tracking") or not filters:
+ return [], []
+
+ columns = get_columns()
+ data = get_data(filters)
+ chart_data, summary = get_chart_summary_data(data)
+ return columns, data, None, chart_data, summary
+
+def get_columns():
+ return [
+ {
+ "label": _("Published Date"),
+ "fieldname": "publish_date",
+ "fieldtype": "Date",
+ "width": 100
+ },
+ {
+ "label": _("Title"),
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "width": 200
+ },
+ {
+ "label": _("Duration"),
+ "fieldname": "duration",
+ "fieldtype": "Duration",
+ "width": 100
+ },
+ {
+ "label": _("Views"),
+ "fieldname": "view_count",
+ "fieldtype": "Float",
+ "width": 200
+ },
+ {
+ "label": _("Likes"),
+ "fieldname": "like_count",
+ "fieldtype": "Float",
+ "width": 200
+ },
+ {
+ "label": _("Dislikes"),
+ "fieldname": "dislike_count",
+ "fieldtype": "Float",
+ "width": 100
+ },
+ {
+ "label": _("Comments"),
+ "fieldname": "comment_count",
+ "fieldtype": "Float",
+ "width": 100
+ }
+ ]
+
+def get_data(filters):
+ return frappe.db.sql("""
+ SELECT
+ publish_date, title, provider, duration,
+ view_count, like_count, dislike_count, comment_count
+ FROM `tabVideo`
+ WHERE view_count is not null
+ and publish_date between %(from_date)s and %(to_date)s
+ ORDER BY view_count desc""", filters, as_dict=1)
+
+def get_chart_summary_data(data):
+ labels, likes, views = [], [], []
+ total_views = 0
+
+ for row in data:
+ labels.append(row.get('title'))
+ likes.append(row.get('like_count'))
+ views.append(row.get('view_count'))
+ total_views += flt(row.get('view_count'))
+
+
+ chart_data = {
+ "data" : {
+ "labels" : labels,
+ "datasets" : [
+ {
+ "name" : "Likes",
+ "values" : likes
+ },
+ {
+ "name" : "Views",
+ "values" : views
+ }
+ ]
+ },
+ "type": "bar",
+ "barOptions": {
+ "stacked": 1
+ },
+ }
+
+ summary = [
+ {
+ "value": total_views,
+ "indicator": "Blue",
+ "label": "Total Views",
+ "datatype": "Float",
+ }
+ ]
+ return chart_data, summary
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 912d61f7a6..c4f9171fca 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,10 +3,11 @@ frappe
gocardless-pro==1.11.0
googlemaps==3.1.1
pandas==1.0.5
-plaid-python==3.4.0
+plaid-python==6.0.0
pycountry==19.8.18
PyGithub==1.44.1
python-stdnum==1.12
+python-youtube==0.6.0
taxjar==1.9.0
tweepy==3.8.0
Unidecode==1.1.1