Merge branch 'develop' into multiple-shifts
This commit is contained in:
commit
00ddb4c42a
@ -3,6 +3,7 @@
|
|||||||
"creation": "2014-08-29 16:02:39.740505",
|
"creation": "2014-08-29 16:02:39.740505",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"company",
|
"company",
|
||||||
"account"
|
"account"
|
||||||
@ -11,6 +12,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "company",
|
"fieldname": "company",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
"ignore_user_permissions": 1,
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Company",
|
"label": "Company",
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
@ -27,7 +29,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-04-07 18:13:08.833822",
|
"modified": "2022-04-04 12:31:02.994197",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Party Account",
|
"name": "Party Account",
|
||||||
@ -35,5 +37,6 @@
|
|||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC"
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
}
|
}
|
@ -34,7 +34,9 @@ class TaxWithholdingCategory(Document):
|
|||||||
|
|
||||||
def validate_thresholds(self):
|
def validate_thresholds(self):
|
||||||
for d in self.get("rates"):
|
for d in self.get("rates"):
|
||||||
if d.cumulative_threshold and d.cumulative_threshold < d.single_threshold:
|
if (
|
||||||
|
d.cumulative_threshold and d.single_threshold and d.cumulative_threshold < d.single_threshold
|
||||||
|
):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold").format(
|
_("Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold").format(
|
||||||
d.idx
|
d.idx
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%}
|
{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%}
|
||||||
{%- set einvoice = json.loads(doc.signed_einvoice) -%}
|
|
||||||
|
|
||||||
<div class="page-break">
|
<div class="page-break">
|
||||||
|
{% if doc.signed_einvoice %}
|
||||||
|
{%- set einvoice = json.loads(doc.signed_einvoice) -%}
|
||||||
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
|
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
|
||||||
{% if letter_head and not no_letterhead %}
|
{% if letter_head and not no_letterhead %}
|
||||||
<div class="letter-head">{{ letter_head }}</div>
|
<div class="letter-head">{{ letter_head }}</div>
|
||||||
@ -170,4 +171,10 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center" style="color: var(--gray-500); font-size: 14px;">
|
||||||
|
You must generate IRN before you can preview GST E-Invoice.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -463,7 +463,10 @@ class BuyingController(StockController, Subcontracting):
|
|||||||
stock_items = self.get_stock_items()
|
stock_items = self.get_stock_items()
|
||||||
|
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
if d.item_code in stock_items and d.warehouse:
|
if d.item_code not in stock_items:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if d.warehouse:
|
||||||
pr_qty = flt(d.qty) * flt(d.conversion_factor)
|
pr_qty = flt(d.qty) * flt(d.conversion_factor)
|
||||||
|
|
||||||
if pr_qty:
|
if pr_qty:
|
||||||
@ -488,6 +491,7 @@ class BuyingController(StockController, Subcontracting):
|
|||||||
sle = self.get_sl_entries(
|
sle = self.get_sl_entries(
|
||||||
d, {"actual_qty": flt(pr_qty), "serial_no": cstr(d.serial_no).strip()}
|
d, {"actual_qty": flt(pr_qty), "serial_no": cstr(d.serial_no).strip()}
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.is_return:
|
if self.is_return:
|
||||||
outgoing_rate = get_rate_for_return(
|
outgoing_rate = get_rate_for_return(
|
||||||
self.doctype, self.name, d.item_code, self.return_against, item_row=d
|
self.doctype, self.name, d.item_code, self.return_against, item_row=d
|
||||||
@ -517,18 +521,18 @@ class BuyingController(StockController, Subcontracting):
|
|||||||
|
|
||||||
sl_entries.append(from_warehouse_sle)
|
sl_entries.append(from_warehouse_sle)
|
||||||
|
|
||||||
if flt(d.rejected_qty) != 0:
|
if flt(d.rejected_qty) != 0:
|
||||||
sl_entries.append(
|
sl_entries.append(
|
||||||
self.get_sl_entries(
|
self.get_sl_entries(
|
||||||
d,
|
d,
|
||||||
{
|
{
|
||||||
"warehouse": d.rejected_warehouse,
|
"warehouse": d.rejected_warehouse,
|
||||||
"actual_qty": flt(d.rejected_qty) * flt(d.conversion_factor),
|
"actual_qty": flt(d.rejected_qty) * flt(d.conversion_factor),
|
||||||
"serial_no": cstr(d.rejected_serial_no).strip(),
|
"serial_no": cstr(d.rejected_serial_no).strip(),
|
||||||
"incoming_rate": 0.0,
|
"incoming_rate": 0.0,
|
||||||
},
|
},
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
self.make_sl_entries_for_supplier_warehouse(sl_entries)
|
self.make_sl_entries_for_supplier_warehouse(sl_entries)
|
||||||
self.make_sl_entries(
|
self.make_sl_entries(
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
"item_search_settings_section",
|
"item_search_settings_section",
|
||||||
"redisearch_warning",
|
"redisearch_warning",
|
||||||
"search_index_fields",
|
"search_index_fields",
|
||||||
"show_categories_in_search_autocomplete",
|
"is_redisearch_enabled",
|
||||||
"is_redisearch_loaded",
|
"is_redisearch_loaded",
|
||||||
"shop_by_category_section",
|
"shop_by_category_section",
|
||||||
"slideshow",
|
"slideshow",
|
||||||
@ -293,6 +293,7 @@
|
|||||||
"fieldname": "search_index_fields",
|
"fieldname": "search_index_fields",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Small Text",
|
||||||
"label": "Search Index Fields",
|
"label": "Search Index Fields",
|
||||||
|
"mandatory_depends_on": "is_redisearch_enabled",
|
||||||
"read_only_depends_on": "eval:!doc.is_redisearch_loaded"
|
"read_only_depends_on": "eval:!doc.is_redisearch_loaded"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -301,13 +302,6 @@
|
|||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Item Search Settings"
|
"label": "Item Search Settings"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "1",
|
|
||||||
"fieldname": "show_categories_in_search_autocomplete",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Show Categories in Search Autocomplete",
|
|
||||||
"read_only_depends_on": "eval:!doc.is_redisearch_loaded"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "is_redisearch_loaded",
|
"fieldname": "is_redisearch_loaded",
|
||||||
@ -365,12 +359,19 @@
|
|||||||
"fieldname": "show_price_in_quotation",
|
"fieldname": "show_price_in_quotation",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Show Price in Quotation"
|
"label": "Show Price in Quotation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_redisearch_enabled",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enable Redisearch",
|
||||||
|
"read_only_depends_on": "eval:!doc.is_redisearch_loaded"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-09-02 14:02:44.785824",
|
"modified": "2022-04-01 18:35:56.106756",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "E-commerce",
|
"module": "E-commerce",
|
||||||
"name": "E Commerce Settings",
|
"name": "E Commerce Settings",
|
||||||
@ -389,5 +390,6 @@
|
|||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
@ -9,6 +9,7 @@ from frappe.utils import comma_and, flt, unique
|
|||||||
|
|
||||||
from erpnext.e_commerce.redisearch_utils import (
|
from erpnext.e_commerce.redisearch_utils import (
|
||||||
create_website_items_index,
|
create_website_items_index,
|
||||||
|
define_autocomplete_dictionary,
|
||||||
get_indexable_web_fields,
|
get_indexable_web_fields,
|
||||||
is_search_module_loaded,
|
is_search_module_loaded,
|
||||||
)
|
)
|
||||||
@ -21,6 +22,8 @@ class ShoppingCartSetupError(frappe.ValidationError):
|
|||||||
class ECommerceSettings(Document):
|
class ECommerceSettings(Document):
|
||||||
def onload(self):
|
def onload(self):
|
||||||
self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series")
|
self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series")
|
||||||
|
|
||||||
|
# flag >> if redisearch is installed and loaded
|
||||||
self.is_redisearch_loaded = is_search_module_loaded()
|
self.is_redisearch_loaded = is_search_module_loaded()
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
@ -34,6 +37,20 @@ class ECommerceSettings(Document):
|
|||||||
|
|
||||||
frappe.clear_document_cache("E Commerce Settings", "E Commerce Settings")
|
frappe.clear_document_cache("E Commerce Settings", "E Commerce Settings")
|
||||||
|
|
||||||
|
self.is_redisearch_enabled_pre_save = frappe.db.get_single_value(
|
||||||
|
"E Commerce Settings", "is_redisearch_enabled"
|
||||||
|
)
|
||||||
|
|
||||||
|
def after_save(self):
|
||||||
|
self.create_redisearch_indexes()
|
||||||
|
|
||||||
|
def create_redisearch_indexes(self):
|
||||||
|
# if redisearch is enabled (value changed) create indexes and dictionary
|
||||||
|
value_changed = self.is_redisearch_enabled != self.is_redisearch_enabled_pre_save
|
||||||
|
if self.is_redisearch_loaded and self.is_redisearch_enabled and value_changed:
|
||||||
|
define_autocomplete_dictionary()
|
||||||
|
create_website_items_index()
|
||||||
|
|
||||||
def validate_field_filters(self):
|
def validate_field_filters(self):
|
||||||
if not (self.enable_field_filters and self.filter_fields):
|
if not (self.enable_field_filters and self.filter_fields):
|
||||||
return
|
return
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('Website Item', {
|
frappe.ui.form.on('Website Item', {
|
||||||
onload: function(frm) {
|
onload: (frm) => {
|
||||||
// should never check Private
|
// should never check Private
|
||||||
frm.fields_dict["website_image"].df.is_private = 0;
|
frm.fields_dict["website_image"].df.is_private = 0;
|
||||||
|
|
||||||
@ -13,18 +13,35 @@ frappe.ui.form.on('Website Item', {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
image: function() {
|
refresh: (frm) => {
|
||||||
|
frm.add_custom_button(__("Prices"), function() {
|
||||||
|
frappe.set_route("List", "Item Price", {"item_code": frm.doc.item_code});
|
||||||
|
}, __("View"));
|
||||||
|
|
||||||
|
frm.add_custom_button(__("Stock"), function() {
|
||||||
|
frappe.route_options = {
|
||||||
|
"item_code": frm.doc.item_code
|
||||||
|
};
|
||||||
|
frappe.set_route("query-report", "Stock Balance");
|
||||||
|
}, __("View"));
|
||||||
|
|
||||||
|
frm.add_custom_button(__("E Commerce Settings"), function() {
|
||||||
|
frappe.set_route("Form", "E Commerce Settings");
|
||||||
|
}, __("View"));
|
||||||
|
},
|
||||||
|
|
||||||
|
image: () => {
|
||||||
refresh_field("image_view");
|
refresh_field("image_view");
|
||||||
},
|
},
|
||||||
|
|
||||||
copy_from_item_group: function(frm) {
|
copy_from_item_group: (frm) => {
|
||||||
return frm.call({
|
return frm.call({
|
||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
method: "copy_specification_from_item_group"
|
method: "copy_specification_from_item_group"
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
set_meta_tags(frm) {
|
set_meta_tags: (frm) => {
|
||||||
frappe.utils.set_meta_tag(frm.doc.route);
|
frappe.utils.set_meta_tag(frm.doc.route);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe import _
|
||||||
from frappe.utils.redis_wrapper import RedisWrapper
|
from frappe.utils.redis_wrapper import RedisWrapper
|
||||||
|
from redis import ResponseError
|
||||||
from redisearch import AutoCompleter, Client, IndexDefinition, Suggestion, TagField, TextField
|
from redisearch import AutoCompleter, Client, IndexDefinition, Suggestion, TagField, TextField
|
||||||
|
|
||||||
WEBSITE_ITEM_INDEX = "website_items_index"
|
WEBSITE_ITEM_INDEX = "website_items_index"
|
||||||
@ -22,6 +26,12 @@ def get_indexable_web_fields():
|
|||||||
return [df.fieldname for df in valid_fields]
|
return [df.fieldname for df in valid_fields]
|
||||||
|
|
||||||
|
|
||||||
|
def is_redisearch_enabled():
|
||||||
|
"Return True only if redisearch is loaded and enabled."
|
||||||
|
is_redisearch_enabled = frappe.db.get_single_value("E Commerce Settings", "is_redisearch_enabled")
|
||||||
|
return is_search_module_loaded() and is_redisearch_enabled
|
||||||
|
|
||||||
|
|
||||||
def is_search_module_loaded():
|
def is_search_module_loaded():
|
||||||
try:
|
try:
|
||||||
cache = frappe.cache()
|
cache = frappe.cache()
|
||||||
@ -32,14 +42,14 @@ def is_search_module_loaded():
|
|||||||
)
|
)
|
||||||
return "search" in parsed_output
|
return "search" in parsed_output
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False # handling older redis versions
|
||||||
|
|
||||||
|
|
||||||
def if_redisearch_loaded(function):
|
def if_redisearch_enabled(function):
|
||||||
"Decorator to check if Redisearch is loaded."
|
"Decorator to check if Redisearch is enabled."
|
||||||
|
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
if is_search_module_loaded():
|
if is_redisearch_enabled():
|
||||||
func = function(*args, **kwargs)
|
func = function(*args, **kwargs)
|
||||||
return func
|
return func
|
||||||
return
|
return
|
||||||
@ -51,22 +61,25 @@ def make_key(key):
|
|||||||
return "{0}|{1}".format(frappe.conf.db_name, key).encode("utf-8")
|
return "{0}|{1}".format(frappe.conf.db_name, key).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
@if_redisearch_loaded
|
@if_redisearch_enabled
|
||||||
def create_website_items_index():
|
def create_website_items_index():
|
||||||
"Creates Index Definition."
|
"Creates Index Definition."
|
||||||
|
|
||||||
# CREATE index
|
# CREATE index
|
||||||
client = Client(make_key(WEBSITE_ITEM_INDEX), conn=frappe.cache())
|
client = Client(make_key(WEBSITE_ITEM_INDEX), conn=frappe.cache())
|
||||||
|
|
||||||
# DROP if already exists
|
|
||||||
try:
|
try:
|
||||||
client.drop_index()
|
client.drop_index() # drop if already exists
|
||||||
except Exception:
|
except ResponseError:
|
||||||
|
# will most likely raise a ResponseError if index does not exist
|
||||||
|
# ignore and create index
|
||||||
pass
|
pass
|
||||||
|
except Exception:
|
||||||
|
raise_redisearch_error()
|
||||||
|
|
||||||
idx_def = IndexDefinition([make_key(WEBSITE_ITEM_KEY_PREFIX)])
|
idx_def = IndexDefinition([make_key(WEBSITE_ITEM_KEY_PREFIX)])
|
||||||
|
|
||||||
# Based on e-commerce settings
|
# Index fields mentioned in e-commerce settings
|
||||||
idx_fields = frappe.db.get_single_value("E Commerce Settings", "search_index_fields")
|
idx_fields = frappe.db.get_single_value("E Commerce Settings", "search_index_fields")
|
||||||
idx_fields = idx_fields.split(",") if idx_fields else []
|
idx_fields = idx_fields.split(",") if idx_fields else []
|
||||||
|
|
||||||
@ -91,20 +104,20 @@ def to_search_field(field):
|
|||||||
return TextField(field)
|
return TextField(field)
|
||||||
|
|
||||||
|
|
||||||
@if_redisearch_loaded
|
@if_redisearch_enabled
|
||||||
def insert_item_to_index(website_item_doc):
|
def insert_item_to_index(website_item_doc):
|
||||||
# Insert item to index
|
# Insert item to index
|
||||||
key = get_cache_key(website_item_doc.name)
|
key = get_cache_key(website_item_doc.name)
|
||||||
cache = frappe.cache()
|
cache = frappe.cache()
|
||||||
web_item = create_web_item_map(website_item_doc)
|
web_item = create_web_item_map(website_item_doc)
|
||||||
|
|
||||||
for k, v in web_item.items():
|
for field, value in web_item.items():
|
||||||
super(RedisWrapper, cache).hset(make_key(key), k, v)
|
super(RedisWrapper, cache).hset(make_key(key), field, value)
|
||||||
|
|
||||||
insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name)
|
insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name)
|
||||||
|
|
||||||
|
|
||||||
@if_redisearch_loaded
|
@if_redisearch_enabled
|
||||||
def insert_to_name_ac(web_name, doc_name):
|
def insert_to_name_ac(web_name, doc_name):
|
||||||
ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=frappe.cache())
|
ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=frappe.cache())
|
||||||
ac.add_suggestions(Suggestion(web_name, payload=doc_name))
|
ac.add_suggestions(Suggestion(web_name, payload=doc_name))
|
||||||
@ -114,20 +127,20 @@ def create_web_item_map(website_item_doc):
|
|||||||
fields_to_index = get_fields_indexed()
|
fields_to_index = get_fields_indexed()
|
||||||
web_item = {}
|
web_item = {}
|
||||||
|
|
||||||
for f in fields_to_index:
|
for field in fields_to_index:
|
||||||
web_item[f] = website_item_doc.get(f) or ""
|
web_item[field] = website_item_doc.get(field) or ""
|
||||||
|
|
||||||
return web_item
|
return web_item
|
||||||
|
|
||||||
|
|
||||||
@if_redisearch_loaded
|
@if_redisearch_enabled
|
||||||
def update_index_for_item(website_item_doc):
|
def update_index_for_item(website_item_doc):
|
||||||
# Reinsert to Cache
|
# Reinsert to Cache
|
||||||
insert_item_to_index(website_item_doc)
|
insert_item_to_index(website_item_doc)
|
||||||
define_autocomplete_dictionary()
|
define_autocomplete_dictionary()
|
||||||
|
|
||||||
|
|
||||||
@if_redisearch_loaded
|
@if_redisearch_enabled
|
||||||
def delete_item_from_index(website_item_doc):
|
def delete_item_from_index(website_item_doc):
|
||||||
cache = frappe.cache()
|
cache = frappe.cache()
|
||||||
key = get_cache_key(website_item_doc.name)
|
key = get_cache_key(website_item_doc.name)
|
||||||
@ -135,13 +148,13 @@ def delete_item_from_index(website_item_doc):
|
|||||||
try:
|
try:
|
||||||
cache.delete(key)
|
cache.delete(key)
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
raise_redisearch_error()
|
||||||
|
|
||||||
delete_from_ac_dict(website_item_doc)
|
delete_from_ac_dict(website_item_doc)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@if_redisearch_loaded
|
@if_redisearch_enabled
|
||||||
def delete_from_ac_dict(website_item_doc):
|
def delete_from_ac_dict(website_item_doc):
|
||||||
"""Removes this items's name from autocomplete dictionary"""
|
"""Removes this items's name from autocomplete dictionary"""
|
||||||
cache = frappe.cache()
|
cache = frappe.cache()
|
||||||
@ -149,40 +162,60 @@ def delete_from_ac_dict(website_item_doc):
|
|||||||
name_ac.delete(website_item_doc.web_item_name)
|
name_ac.delete(website_item_doc.web_item_name)
|
||||||
|
|
||||||
|
|
||||||
@if_redisearch_loaded
|
@if_redisearch_enabled
|
||||||
def define_autocomplete_dictionary():
|
def define_autocomplete_dictionary():
|
||||||
"""Creates an autocomplete search dictionary for `name`.
|
"""
|
||||||
Also creats autocomplete dictionary for `categories` if
|
Defines/Redefines an autocomplete search dictionary for Website Item Name.
|
||||||
checked in E Commerce Settings"""
|
Also creats autocomplete dictionary for Published Item Groups.
|
||||||
|
"""
|
||||||
|
|
||||||
cache = frappe.cache()
|
cache = frappe.cache()
|
||||||
name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
|
item_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
|
||||||
cat_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=cache)
|
item_group_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=cache)
|
||||||
|
|
||||||
ac_categories = frappe.db.get_single_value(
|
|
||||||
"E Commerce Settings", "show_categories_in_search_autocomplete"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Delete both autocomplete dicts
|
# Delete both autocomplete dicts
|
||||||
try:
|
try:
|
||||||
cache.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE))
|
cache.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE))
|
||||||
cache.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE))
|
cache.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE))
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
raise_redisearch_error()
|
||||||
|
|
||||||
|
create_items_autocomplete_dict(autocompleter=item_ac)
|
||||||
|
create_item_groups_autocomplete_dict(autocompleter=item_group_ac)
|
||||||
|
|
||||||
|
|
||||||
|
@if_redisearch_enabled
|
||||||
|
def create_items_autocomplete_dict(autocompleter):
|
||||||
|
"Add items as suggestions in Autocompleter."
|
||||||
items = frappe.get_all(
|
items = frappe.get_all(
|
||||||
"Website Item", fields=["web_item_name", "item_group"], filters={"published": 1}
|
"Website Item", fields=["web_item_name", "item_group"], filters={"published": 1}
|
||||||
)
|
)
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
name_ac.add_suggestions(Suggestion(item.web_item_name))
|
autocompleter.add_suggestions(Suggestion(item.web_item_name))
|
||||||
if ac_categories and item.item_group:
|
|
||||||
cat_ac.add_suggestions(Suggestion(item.item_group))
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@if_redisearch_loaded
|
@if_redisearch_enabled
|
||||||
|
def create_item_groups_autocomplete_dict(autocompleter):
|
||||||
|
"Add item groups with weightage as suggestions in Autocompleter."
|
||||||
|
published_item_groups = frappe.get_all(
|
||||||
|
"Item Group", fields=["name", "route", "weightage"], filters={"show_in_website": 1}
|
||||||
|
)
|
||||||
|
if not published_item_groups:
|
||||||
|
return
|
||||||
|
|
||||||
|
for item_group in published_item_groups:
|
||||||
|
payload = json.dumps({"name": item_group.name, "route": item_group.route})
|
||||||
|
autocompleter.add_suggestions(
|
||||||
|
Suggestion(
|
||||||
|
string=item_group.name,
|
||||||
|
score=frappe.utils.flt(item_group.weightage) or 1.0,
|
||||||
|
payload=payload, # additional info that can be retrieved later
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@if_redisearch_enabled
|
||||||
def reindex_all_web_items():
|
def reindex_all_web_items():
|
||||||
items = frappe.get_all("Website Item", fields=get_fields_indexed(), filters={"published": True})
|
items = frappe.get_all("Website Item", fields=get_fields_indexed(), filters={"published": True})
|
||||||
|
|
||||||
@ -191,8 +224,8 @@ def reindex_all_web_items():
|
|||||||
web_item = create_web_item_map(item)
|
web_item = create_web_item_map(item)
|
||||||
key = make_key(get_cache_key(item.name))
|
key = make_key(get_cache_key(item.name))
|
||||||
|
|
||||||
for k, v in web_item.items():
|
for field, value in web_item.items():
|
||||||
super(RedisWrapper, cache).hset(key, k, v)
|
super(RedisWrapper, cache).hset(key, field, value)
|
||||||
|
|
||||||
|
|
||||||
def get_cache_key(name):
|
def get_cache_key(name):
|
||||||
@ -210,7 +243,12 @@ def get_fields_indexed():
|
|||||||
return fields_to_index
|
return fields_to_index
|
||||||
|
|
||||||
|
|
||||||
# TODO: Remove later
|
def raise_redisearch_error():
|
||||||
# # Figure out a way to run this at startup
|
"Create an Error Log and raise error."
|
||||||
define_autocomplete_dictionary()
|
traceback = frappe.get_traceback()
|
||||||
create_website_items_index()
|
log = frappe.log_error(traceback, frappe._("Redisearch Error"))
|
||||||
|
log_link = frappe.utils.get_link_to_form("Error Log", log.name)
|
||||||
|
|
||||||
|
frappe.throw(
|
||||||
|
msg=_("Something went wrong. Check {0}").format(log_link), title=_("Redisearch Error")
|
||||||
|
)
|
||||||
|
@ -39,11 +39,15 @@ class LeaveAllocation(Document):
|
|||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_period()
|
self.validate_period()
|
||||||
self.validate_allocation_overlap()
|
self.validate_allocation_overlap()
|
||||||
self.validate_back_dated_allocation()
|
|
||||||
self.set_total_leaves_allocated()
|
|
||||||
self.validate_total_leaves_allocated()
|
|
||||||
self.validate_lwp()
|
self.validate_lwp()
|
||||||
set_employee_name(self)
|
set_employee_name(self)
|
||||||
|
self.set_total_leaves_allocated()
|
||||||
|
self.validate_leave_days_and_dates()
|
||||||
|
|
||||||
|
def validate_leave_days_and_dates(self):
|
||||||
|
# all validations that should run on save as well as on update after submit
|
||||||
|
self.validate_back_dated_allocation()
|
||||||
|
self.validate_total_leaves_allocated()
|
||||||
self.validate_leave_allocation_days()
|
self.validate_leave_allocation_days()
|
||||||
|
|
||||||
def validate_leave_allocation_days(self):
|
def validate_leave_allocation_days(self):
|
||||||
@ -56,14 +60,19 @@ class LeaveAllocation(Document):
|
|||||||
leave_allocated = 0
|
leave_allocated = 0
|
||||||
if leave_period:
|
if leave_period:
|
||||||
leave_allocated = get_leave_allocation_for_period(
|
leave_allocated = get_leave_allocation_for_period(
|
||||||
self.employee, self.leave_type, leave_period[0].from_date, leave_period[0].to_date
|
self.employee,
|
||||||
|
self.leave_type,
|
||||||
|
leave_period[0].from_date,
|
||||||
|
leave_period[0].to_date,
|
||||||
|
exclude_allocation=self.name,
|
||||||
)
|
)
|
||||||
leave_allocated += flt(self.new_leaves_allocated)
|
leave_allocated += flt(self.new_leaves_allocated)
|
||||||
if leave_allocated > max_leaves_allowed:
|
if leave_allocated > max_leaves_allowed:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
"Total allocated leaves are more days than maximum allocation of {0} leave type for employee {1} in the period"
|
"Total allocated leaves are more than maximum allocation allowed for {0} leave type for employee {1} in the period"
|
||||||
).format(self.leave_type, self.employee)
|
).format(self.leave_type, self.employee),
|
||||||
|
OverAllocationError,
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
@ -84,6 +93,12 @@ class LeaveAllocation(Document):
|
|||||||
def on_update_after_submit(self):
|
def on_update_after_submit(self):
|
||||||
if self.has_value_changed("new_leaves_allocated"):
|
if self.has_value_changed("new_leaves_allocated"):
|
||||||
self.validate_against_leave_applications()
|
self.validate_against_leave_applications()
|
||||||
|
|
||||||
|
# recalculate total leaves allocated
|
||||||
|
self.total_leaves_allocated = flt(self.unused_leaves) + flt(self.new_leaves_allocated)
|
||||||
|
# run required validations again since total leaves are being updated
|
||||||
|
self.validate_leave_days_and_dates()
|
||||||
|
|
||||||
leaves_to_be_added = self.new_leaves_allocated - self.get_existing_leave_count()
|
leaves_to_be_added = self.new_leaves_allocated - self.get_existing_leave_count()
|
||||||
args = {
|
args = {
|
||||||
"leaves": leaves_to_be_added,
|
"leaves": leaves_to_be_added,
|
||||||
@ -92,6 +107,7 @@ class LeaveAllocation(Document):
|
|||||||
"is_carry_forward": 0,
|
"is_carry_forward": 0,
|
||||||
}
|
}
|
||||||
create_leave_ledger_entry(self, args, True)
|
create_leave_ledger_entry(self, args, True)
|
||||||
|
self.db_update()
|
||||||
|
|
||||||
def get_existing_leave_count(self):
|
def get_existing_leave_count(self):
|
||||||
ledger_entries = frappe.get_all(
|
ledger_entries = frappe.get_all(
|
||||||
@ -279,27 +295,27 @@ def get_previous_allocation(from_date, leave_type, employee):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_leave_allocation_for_period(employee, leave_type, from_date, to_date):
|
def get_leave_allocation_for_period(
|
||||||
leave_allocated = 0
|
employee, leave_type, from_date, to_date, exclude_allocation=None
|
||||||
leave_allocations = frappe.db.sql(
|
):
|
||||||
"""
|
from frappe.query_builder.functions import Sum
|
||||||
select employee, leave_type, from_date, to_date, total_leaves_allocated
|
|
||||||
from `tabLeave Allocation`
|
|
||||||
where employee=%(employee)s and leave_type=%(leave_type)s
|
|
||||||
and docstatus=1
|
|
||||||
and (from_date between %(from_date)s and %(to_date)s
|
|
||||||
or to_date between %(from_date)s and %(to_date)s
|
|
||||||
or (from_date < %(from_date)s and to_date > %(to_date)s))
|
|
||||||
""",
|
|
||||||
{"from_date": from_date, "to_date": to_date, "employee": employee, "leave_type": leave_type},
|
|
||||||
as_dict=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
if leave_allocations:
|
Allocation = frappe.qb.DocType("Leave Allocation")
|
||||||
for leave_alloc in leave_allocations:
|
return (
|
||||||
leave_allocated += leave_alloc.total_leaves_allocated
|
frappe.qb.from_(Allocation)
|
||||||
|
.select(Sum(Allocation.total_leaves_allocated).as_("total_allocated_leaves"))
|
||||||
return leave_allocated
|
.where(
|
||||||
|
(Allocation.employee == employee)
|
||||||
|
& (Allocation.leave_type == leave_type)
|
||||||
|
& (Allocation.docstatus == 1)
|
||||||
|
& (Allocation.name != exclude_allocation)
|
||||||
|
& (
|
||||||
|
(Allocation.from_date.between(from_date, to_date))
|
||||||
|
| (Allocation.to_date.between(from_date, to_date))
|
||||||
|
| ((Allocation.from_date < from_date) & (Allocation.to_date > to_date))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).run()[0][0] or 0.0
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
@ -1,24 +1,26 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
from frappe.utils import add_days, add_months, getdate, nowdate
|
from frappe.utils import add_days, add_months, getdate, nowdate
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
|
from erpnext.hr.doctype.leave_allocation.leave_allocation import (
|
||||||
|
BackDatedAllocationError,
|
||||||
|
OverAllocationError,
|
||||||
|
)
|
||||||
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
|
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
|
||||||
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
|
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
|
||||||
|
|
||||||
|
|
||||||
class TestLeaveAllocation(unittest.TestCase):
|
class TestLeaveAllocation(FrappeTestCase):
|
||||||
@classmethod
|
def setUp(self):
|
||||||
def setUpClass(cls):
|
frappe.db.delete("Leave Period")
|
||||||
frappe.db.sql("delete from `tabLeave Period`")
|
frappe.db.delete("Leave Allocation")
|
||||||
|
|
||||||
emp_id = make_employee("test_emp_leave_allocation@salary.com")
|
emp_id = make_employee("test_emp_leave_allocation@salary.com", company="_Test Company")
|
||||||
cls.employee = frappe.get_doc("Employee", emp_id)
|
self.employee = frappe.get_doc("Employee", emp_id)
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
frappe.db.rollback()
|
|
||||||
|
|
||||||
def test_overlapping_allocation(self):
|
def test_overlapping_allocation(self):
|
||||||
leaves = [
|
leaves = [
|
||||||
@ -65,7 +67,7 @@ class TestLeaveAllocation(unittest.TestCase):
|
|||||||
# invalid period
|
# invalid period
|
||||||
self.assertRaises(frappe.ValidationError, doc.save)
|
self.assertRaises(frappe.ValidationError, doc.save)
|
||||||
|
|
||||||
def test_allocated_leave_days_over_period(self):
|
def test_validation_for_over_allocation(self):
|
||||||
doc = frappe.get_doc(
|
doc = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Leave Allocation",
|
"doctype": "Leave Allocation",
|
||||||
@ -80,7 +82,135 @@ class TestLeaveAllocation(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# allocated leave more than period
|
# allocated leave more than period
|
||||||
self.assertRaises(frappe.ValidationError, doc.save)
|
self.assertRaises(OverAllocationError, doc.save)
|
||||||
|
|
||||||
|
def test_validation_for_over_allocation_post_submission(self):
|
||||||
|
allocation = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Leave Allocation",
|
||||||
|
"__islocal": 1,
|
||||||
|
"employee": self.employee.name,
|
||||||
|
"employee_name": self.employee.employee_name,
|
||||||
|
"leave_type": "_Test Leave Type",
|
||||||
|
"from_date": getdate("2015-09-1"),
|
||||||
|
"to_date": getdate("2015-09-30"),
|
||||||
|
"new_leaves_allocated": 15,
|
||||||
|
}
|
||||||
|
).submit()
|
||||||
|
allocation.reload()
|
||||||
|
# allocated leaves more than period after submission
|
||||||
|
allocation.new_leaves_allocated = 35
|
||||||
|
self.assertRaises(OverAllocationError, allocation.save)
|
||||||
|
|
||||||
|
def test_validation_for_over_allocation_based_on_leave_setup(self):
|
||||||
|
frappe.delete_doc_if_exists("Leave Period", "Test Allocation Period")
|
||||||
|
leave_period = frappe.get_doc(
|
||||||
|
dict(
|
||||||
|
name="Test Allocation Period",
|
||||||
|
doctype="Leave Period",
|
||||||
|
from_date=add_months(nowdate(), -6),
|
||||||
|
to_date=add_months(nowdate(), 6),
|
||||||
|
company="_Test Company",
|
||||||
|
is_active=1,
|
||||||
|
)
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
leave_type = create_leave_type(leave_type_name="_Test Allocation Validation", is_carry_forward=1)
|
||||||
|
leave_type.max_leaves_allowed = 25
|
||||||
|
leave_type.save()
|
||||||
|
|
||||||
|
# 15 leaves allocated in this period
|
||||||
|
allocation = create_leave_allocation(
|
||||||
|
leave_type=leave_type.name,
|
||||||
|
employee=self.employee.name,
|
||||||
|
employee_name=self.employee.employee_name,
|
||||||
|
from_date=leave_period.from_date,
|
||||||
|
to_date=nowdate(),
|
||||||
|
)
|
||||||
|
allocation.submit()
|
||||||
|
|
||||||
|
# trying to allocate additional 15 leaves
|
||||||
|
allocation = create_leave_allocation(
|
||||||
|
leave_type=leave_type.name,
|
||||||
|
employee=self.employee.name,
|
||||||
|
employee_name=self.employee.employee_name,
|
||||||
|
from_date=add_days(nowdate(), 1),
|
||||||
|
to_date=leave_period.to_date,
|
||||||
|
)
|
||||||
|
self.assertRaises(OverAllocationError, allocation.save)
|
||||||
|
|
||||||
|
def test_validation_for_over_allocation_based_on_leave_setup_post_submission(self):
|
||||||
|
frappe.delete_doc_if_exists("Leave Period", "Test Allocation Period")
|
||||||
|
leave_period = frappe.get_doc(
|
||||||
|
dict(
|
||||||
|
name="Test Allocation Period",
|
||||||
|
doctype="Leave Period",
|
||||||
|
from_date=add_months(nowdate(), -6),
|
||||||
|
to_date=add_months(nowdate(), 6),
|
||||||
|
company="_Test Company",
|
||||||
|
is_active=1,
|
||||||
|
)
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
leave_type = create_leave_type(leave_type_name="_Test Allocation Validation", is_carry_forward=1)
|
||||||
|
leave_type.max_leaves_allowed = 30
|
||||||
|
leave_type.save()
|
||||||
|
|
||||||
|
# 15 leaves allocated
|
||||||
|
allocation = create_leave_allocation(
|
||||||
|
leave_type=leave_type.name,
|
||||||
|
employee=self.employee.name,
|
||||||
|
employee_name=self.employee.employee_name,
|
||||||
|
from_date=leave_period.from_date,
|
||||||
|
to_date=nowdate(),
|
||||||
|
)
|
||||||
|
allocation.submit()
|
||||||
|
allocation.reload()
|
||||||
|
|
||||||
|
# allocate additional 15 leaves
|
||||||
|
allocation = create_leave_allocation(
|
||||||
|
leave_type=leave_type.name,
|
||||||
|
employee=self.employee.name,
|
||||||
|
employee_name=self.employee.employee_name,
|
||||||
|
from_date=add_days(nowdate(), 1),
|
||||||
|
to_date=leave_period.to_date,
|
||||||
|
)
|
||||||
|
allocation.submit()
|
||||||
|
allocation.reload()
|
||||||
|
|
||||||
|
# trying to allocate 25 leaves in 2nd alloc within leave period
|
||||||
|
# total leaves = 40 which is more than `max_leaves_allowed` setting i.e. 30
|
||||||
|
allocation.new_leaves_allocated = 25
|
||||||
|
self.assertRaises(OverAllocationError, allocation.save)
|
||||||
|
|
||||||
|
def test_validate_back_dated_allocation_update(self):
|
||||||
|
leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1)
|
||||||
|
leave_type.save()
|
||||||
|
|
||||||
|
# initial leave allocation = 15
|
||||||
|
leave_allocation = create_leave_allocation(
|
||||||
|
employee=self.employee.name,
|
||||||
|
employee_name=self.employee.employee_name,
|
||||||
|
leave_type="_Test_CF_leave",
|
||||||
|
from_date=add_months(nowdate(), -12),
|
||||||
|
to_date=add_months(nowdate(), -1),
|
||||||
|
carry_forward=0,
|
||||||
|
)
|
||||||
|
leave_allocation.submit()
|
||||||
|
|
||||||
|
# new_leaves = 15, carry_forwarded = 10
|
||||||
|
leave_allocation_1 = create_leave_allocation(
|
||||||
|
employee=self.employee.name,
|
||||||
|
employee_name=self.employee.employee_name,
|
||||||
|
leave_type="_Test_CF_leave",
|
||||||
|
carry_forward=1,
|
||||||
|
)
|
||||||
|
leave_allocation_1.submit()
|
||||||
|
|
||||||
|
# try updating initial leave allocation
|
||||||
|
leave_allocation.reload()
|
||||||
|
leave_allocation.new_leaves_allocated = 20
|
||||||
|
self.assertRaises(BackDatedAllocationError, leave_allocation.save)
|
||||||
|
|
||||||
def test_carry_forward_calculation(self):
|
def test_carry_forward_calculation(self):
|
||||||
leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1)
|
leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1)
|
||||||
@ -108,8 +238,10 @@ class TestLeaveAllocation(unittest.TestCase):
|
|||||||
carry_forward=1,
|
carry_forward=1,
|
||||||
)
|
)
|
||||||
leave_allocation_1.submit()
|
leave_allocation_1.submit()
|
||||||
|
leave_allocation_1.reload()
|
||||||
|
|
||||||
self.assertEqual(leave_allocation_1.unused_leaves, 10)
|
self.assertEqual(leave_allocation_1.unused_leaves, 10)
|
||||||
|
self.assertEqual(leave_allocation_1.total_leaves_allocated, 25)
|
||||||
|
|
||||||
leave_allocation_1.cancel()
|
leave_allocation_1.cancel()
|
||||||
|
|
||||||
@ -197,9 +329,12 @@ class TestLeaveAllocation(unittest.TestCase):
|
|||||||
employee=self.employee.name, employee_name=self.employee.employee_name
|
employee=self.employee.name, employee_name=self.employee.employee_name
|
||||||
)
|
)
|
||||||
leave_allocation.submit()
|
leave_allocation.submit()
|
||||||
|
leave_allocation.reload()
|
||||||
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
||||||
|
|
||||||
leave_allocation.new_leaves_allocated = 40
|
leave_allocation.new_leaves_allocated = 40
|
||||||
leave_allocation.submit()
|
leave_allocation.submit()
|
||||||
|
leave_allocation.reload()
|
||||||
self.assertTrue(leave_allocation.total_leaves_allocated, 40)
|
self.assertTrue(leave_allocation.total_leaves_allocated, 40)
|
||||||
|
|
||||||
def test_leave_subtraction_after_submit(self):
|
def test_leave_subtraction_after_submit(self):
|
||||||
@ -207,9 +342,12 @@ class TestLeaveAllocation(unittest.TestCase):
|
|||||||
employee=self.employee.name, employee_name=self.employee.employee_name
|
employee=self.employee.name, employee_name=self.employee.employee_name
|
||||||
)
|
)
|
||||||
leave_allocation.submit()
|
leave_allocation.submit()
|
||||||
|
leave_allocation.reload()
|
||||||
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
||||||
|
|
||||||
leave_allocation.new_leaves_allocated = 10
|
leave_allocation.new_leaves_allocated = 10
|
||||||
leave_allocation.submit()
|
leave_allocation.submit()
|
||||||
|
leave_allocation.reload()
|
||||||
self.assertTrue(leave_allocation.total_leaves_allocated, 10)
|
self.assertTrue(leave_allocation.total_leaves_allocated, 10)
|
||||||
|
|
||||||
def test_validation_against_leave_application_after_submit(self):
|
def test_validation_against_leave_application_after_submit(self):
|
||||||
|
@ -105,6 +105,30 @@ erpnext.setup_einvoice_actions = (doctype) => {
|
|||||||
},
|
},
|
||||||
primary_action_label: __('Submit')
|
primary_action_label: __('Submit')
|
||||||
});
|
});
|
||||||
|
d.fields_dict.transporter.df.onchange = function () {
|
||||||
|
const transporter = d.fields_dict.transporter.value;
|
||||||
|
if (transporter) {
|
||||||
|
frappe.db.get_value('Supplier', transporter, ['gst_transporter_id', 'supplier_name'])
|
||||||
|
.then(({ message }) => {
|
||||||
|
d.set_value('gst_transporter_id', message.gst_transporter_id);
|
||||||
|
d.set_value('transporter_name', message.supplier_name);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
d.set_value('gst_transporter_id', '');
|
||||||
|
d.set_value('transporter_name', '');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
d.fields_dict.driver.df.onchange = function () {
|
||||||
|
const driver = d.fields_dict.driver.value;
|
||||||
|
if (driver) {
|
||||||
|
frappe.db.get_value('Driver', driver, ['full_name'])
|
||||||
|
.then(({ message }) => {
|
||||||
|
d.set_value('driver_name', message.full_name);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
d.set_value('driver_name', '');
|
||||||
|
}
|
||||||
|
};
|
||||||
d.show();
|
d.show();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -153,7 +177,6 @@ const get_ewaybill_fields = (frm) => {
|
|||||||
'fieldname': 'gst_transporter_id',
|
'fieldname': 'gst_transporter_id',
|
||||||
'label': 'GST Transporter ID',
|
'label': 'GST Transporter ID',
|
||||||
'fieldtype': 'Data',
|
'fieldtype': 'Data',
|
||||||
'fetch_from': 'transporter.gst_transporter_id',
|
|
||||||
'default': frm.doc.gst_transporter_id
|
'default': frm.doc.gst_transporter_id
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -189,9 +212,9 @@ const get_ewaybill_fields = (frm) => {
|
|||||||
'fieldname': 'transporter_name',
|
'fieldname': 'transporter_name',
|
||||||
'label': 'Transporter Name',
|
'label': 'Transporter Name',
|
||||||
'fieldtype': 'Data',
|
'fieldtype': 'Data',
|
||||||
'fetch_from': 'transporter.name',
|
|
||||||
'read_only': 1,
|
'read_only': 1,
|
||||||
'default': frm.doc.transporter_name
|
'default': frm.doc.transporter_name,
|
||||||
|
'depends_on': 'transporter'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'mode_of_transport',
|
'fieldname': 'mode_of_transport',
|
||||||
@ -206,7 +229,8 @@ const get_ewaybill_fields = (frm) => {
|
|||||||
'fieldtype': 'Data',
|
'fieldtype': 'Data',
|
||||||
'fetch_from': 'driver.full_name',
|
'fetch_from': 'driver.full_name',
|
||||||
'read_only': 1,
|
'read_only': 1,
|
||||||
'default': frm.doc.driver_name
|
'default': frm.doc.driver_name,
|
||||||
|
'depends_on': 'driver'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'fieldname': 'lr_date',
|
'fieldname': 'lr_date',
|
||||||
|
@ -387,7 +387,7 @@ def update_other_charges(
|
|||||||
|
|
||||||
def get_payment_details(invoice):
|
def get_payment_details(invoice):
|
||||||
payee_name = invoice.company
|
payee_name = invoice.company
|
||||||
mode_of_payment = ", ".join([d.mode_of_payment for d in invoice.payments])
|
mode_of_payment = ""
|
||||||
paid_amount = invoice.base_paid_amount
|
paid_amount = invoice.base_paid_amount
|
||||||
outstanding_amount = invoice.outstanding_amount
|
outstanding_amount = invoice.outstanding_amount
|
||||||
|
|
||||||
|
@ -8,7 +8,8 @@
|
|||||||
"domain": "Manufacturing",
|
"domain": "Manufacturing",
|
||||||
"chart_of_accounts": "Standard",
|
"chart_of_accounts": "Standard",
|
||||||
"default_holiday_list": "_Test Holiday List",
|
"default_holiday_list": "_Test Holiday List",
|
||||||
"enable_perpetual_inventory": 0
|
"enable_perpetual_inventory": 0,
|
||||||
|
"allow_account_creation_against_child_company": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"abbr": "_TC1",
|
"abbr": "_TC1",
|
||||||
|
@ -55,10 +55,15 @@ frappe.ui.form.on("Item", {
|
|||||||
|
|
||||||
if (frm.doc.has_variants) {
|
if (frm.doc.has_variants) {
|
||||||
frm.set_intro(__("This Item is a Template and cannot be used in transactions. Item attributes will be copied over into the variants unless 'No Copy' is set"), true);
|
frm.set_intro(__("This Item is a Template and cannot be used in transactions. Item attributes will be copied over into the variants unless 'No Copy' is set"), true);
|
||||||
|
|
||||||
frm.add_custom_button(__("Show Variants"), function() {
|
frm.add_custom_button(__("Show Variants"), function() {
|
||||||
frappe.set_route("List", "Item", {"variant_of": frm.doc.name});
|
frappe.set_route("List", "Item", {"variant_of": frm.doc.name});
|
||||||
}, __("View"));
|
}, __("View"));
|
||||||
|
|
||||||
|
frm.add_custom_button(__("Item Variant Settings"), function() {
|
||||||
|
frappe.set_route("Form", "Item Variant Settings");
|
||||||
|
}, __("View"));
|
||||||
|
|
||||||
frm.add_custom_button(__("Variant Details Report"), function() {
|
frm.add_custom_button(__("Variant Details Report"), function() {
|
||||||
frappe.set_route("query-report", "Item Variant Details", {"item": frm.doc.name});
|
frappe.set_route("query-report", "Item Variant Details", {"item": frm.doc.name});
|
||||||
}, __("View"));
|
}, __("View"));
|
||||||
@ -110,6 +115,13 @@ frappe.ui.form.on("Item", {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, __('Actions'));
|
}, __('Actions'));
|
||||||
|
} else {
|
||||||
|
frm.add_custom_button(__("Website Item"), function() {
|
||||||
|
frappe.db.get_value("Website Item", {item_code: frm.doc.name}, "name", (d) => {
|
||||||
|
if (!d.name) frappe.throw(__("Website Item not found"));
|
||||||
|
frappe.set_route("Form", "Website Item", d.name);
|
||||||
|
});
|
||||||
|
}, __("View"));
|
||||||
}
|
}
|
||||||
|
|
||||||
erpnext.item.edit_prices_button(frm);
|
erpnext.item.edit_prices_button(frm);
|
||||||
@ -131,12 +143,6 @@ frappe.ui.form.on("Item", {
|
|||||||
frappe.set_route('Form', 'Item', new_item.name);
|
frappe.set_route('Form', 'Item', new_item.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
if(frm.doc.has_variants) {
|
|
||||||
frm.add_custom_button(__("Item Variant Settings"), function() {
|
|
||||||
frappe.set_route("Form", "Item Variant Settings");
|
|
||||||
}, __("View"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const stock_exists = (frm.doc.__onload
|
const stock_exists = (frm.doc.__onload
|
||||||
&& frm.doc.__onload.stock_exists) ? 1 : 0;
|
&& frm.doc.__onload.stock_exists) ? 1 : 0;
|
||||||
|
|
||||||
|
@ -32,5 +32,6 @@ def get_data():
|
|||||||
{"label": _("Manufacture"), "items": ["Production Plan", "Work Order", "Item Manufacturer"]},
|
{"label": _("Manufacture"), "items": ["Production Plan", "Work Order", "Item Manufacturer"]},
|
||||||
{"label": _("Traceability"), "items": ["Serial No", "Batch"]},
|
{"label": _("Traceability"), "items": ["Serial No", "Batch"]},
|
||||||
{"label": _("Move"), "items": ["Stock Entry"]},
|
{"label": _("Move"), "items": ["Stock Entry"]},
|
||||||
|
{"label": _("E-commerce"), "items": ["Website Item"]},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
@ -647,6 +647,45 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
return_pr.cancel()
|
return_pr.cancel()
|
||||||
pr.cancel()
|
pr.cancel()
|
||||||
|
|
||||||
|
def test_purchase_receipt_for_rejected_gle_without_accepted_warehouse(self):
|
||||||
|
from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse
|
||||||
|
|
||||||
|
rejected_warehouse = "_Test Rejected Warehouse - TCP1"
|
||||||
|
if not frappe.db.exists("Warehouse", rejected_warehouse):
|
||||||
|
get_warehouse(
|
||||||
|
company="_Test Company with perpetual inventory",
|
||||||
|
abbr=" - TCP1",
|
||||||
|
warehouse_name="_Test Rejected Warehouse",
|
||||||
|
).name
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
company="_Test Company with perpetual inventory",
|
||||||
|
warehouse="Stores - TCP1",
|
||||||
|
received_qty=2,
|
||||||
|
rejected_qty=2,
|
||||||
|
rejected_warehouse=rejected_warehouse,
|
||||||
|
do_not_save=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
pr.items[0].qty = 0.0
|
||||||
|
pr.items[0].warehouse = ""
|
||||||
|
pr.submit()
|
||||||
|
|
||||||
|
actual_qty = frappe.db.get_value(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
{
|
||||||
|
"voucher_type": "Purchase Receipt",
|
||||||
|
"voucher_no": pr.name,
|
||||||
|
"warehouse": pr.items[0].rejected_warehouse,
|
||||||
|
"is_cancelled": 0,
|
||||||
|
},
|
||||||
|
"actual_qty",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(actual_qty, 2)
|
||||||
|
self.assertFalse(pr.items[0].warehouse)
|
||||||
|
pr.cancel()
|
||||||
|
|
||||||
def test_purchase_return_for_serialized_items(self):
|
def test_purchase_return_for_serialized_items(self):
|
||||||
def _check_serial_no_values(serial_no, field_values):
|
def _check_serial_no_values(serial_no, field_values):
|
||||||
serial_no = frappe.get_doc("Serial No", serial_no)
|
serial_no = frappe.get_doc("Serial No", serial_no)
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import cint, cstr
|
from frappe.utils import cint, cstr
|
||||||
from redisearch import AutoCompleter, Client, Query
|
from redisearch import AutoCompleter, Client, Query
|
||||||
@ -9,7 +11,7 @@ from erpnext.e_commerce.redisearch_utils import (
|
|||||||
WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE,
|
WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE,
|
||||||
WEBSITE_ITEM_INDEX,
|
WEBSITE_ITEM_INDEX,
|
||||||
WEBSITE_ITEM_NAME_AUTOCOMPLETE,
|
WEBSITE_ITEM_NAME_AUTOCOMPLETE,
|
||||||
is_search_module_loaded,
|
is_redisearch_enabled,
|
||||||
make_key,
|
make_key,
|
||||||
)
|
)
|
||||||
from erpnext.e_commerce.shopping_cart.product_info import set_product_info_for_website
|
from erpnext.e_commerce.shopping_cart.product_info import set_product_info_for_website
|
||||||
@ -74,8 +76,8 @@ def search(query):
|
|||||||
def product_search(query, limit=10, fuzzy_search=True):
|
def product_search(query, limit=10, fuzzy_search=True):
|
||||||
search_results = {"from_redisearch": True, "results": []}
|
search_results = {"from_redisearch": True, "results": []}
|
||||||
|
|
||||||
if not is_search_module_loaded():
|
if not is_redisearch_enabled():
|
||||||
# Redisearch module not loaded
|
# Redisearch module not enabled
|
||||||
search_results["from_redisearch"] = False
|
search_results["from_redisearch"] = False
|
||||||
search_results["results"] = get_product_data(query, 0, limit)
|
search_results["results"] = get_product_data(query, 0, limit)
|
||||||
return search_results
|
return search_results
|
||||||
@ -86,6 +88,8 @@ def product_search(query, limit=10, fuzzy_search=True):
|
|||||||
red = frappe.cache()
|
red = frappe.cache()
|
||||||
query = clean_up_query(query)
|
query = clean_up_query(query)
|
||||||
|
|
||||||
|
# TODO: Check perf/correctness with Suggestions & Query vs only Query
|
||||||
|
# TODO: Use Levenshtein Distance in Query (max=3)
|
||||||
ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=red)
|
ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=red)
|
||||||
client = Client(make_key(WEBSITE_ITEM_INDEX), conn=red)
|
client = Client(make_key(WEBSITE_ITEM_INDEX), conn=red)
|
||||||
suggestions = ac.get_suggestions(
|
suggestions = ac.get_suggestions(
|
||||||
@ -121,8 +125,8 @@ def convert_to_dict(redis_search_doc):
|
|||||||
def get_category_suggestions(query):
|
def get_category_suggestions(query):
|
||||||
search_results = {"results": []}
|
search_results = {"results": []}
|
||||||
|
|
||||||
if not is_search_module_loaded():
|
if not is_redisearch_enabled():
|
||||||
# Redisearch module not loaded, query db
|
# Redisearch module not enabled, query db
|
||||||
categories = frappe.db.get_all(
|
categories = frappe.db.get_all(
|
||||||
"Item Group",
|
"Item Group",
|
||||||
filters={"name": ["like", "%{0}%".format(query)], "show_in_website": 1},
|
filters={"name": ["like", "%{0}%".format(query)], "show_in_website": 1},
|
||||||
@ -135,8 +139,10 @@ def get_category_suggestions(query):
|
|||||||
return search_results
|
return search_results
|
||||||
|
|
||||||
ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=frappe.cache())
|
ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=frappe.cache())
|
||||||
suggestions = ac.get_suggestions(query, num=10)
|
suggestions = ac.get_suggestions(query, num=10, with_payloads=True)
|
||||||
|
|
||||||
search_results["results"] = [s.string for s in suggestions]
|
results = [json.loads(s.payload) for s in suggestions]
|
||||||
|
|
||||||
|
search_results["results"] = results
|
||||||
|
|
||||||
return search_results
|
return search_results
|
||||||
|
Loading…
Reference in New Issue
Block a user