Merge branch 'develop' into multiple-shifts

This commit is contained in:
Rucha Mahabal 2022-04-06 11:22:46 +05:30 committed by GitHub
commit 00ddb4c42a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 449 additions and 128 deletions

View File

@ -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": []
} }

View File

@ -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

View File

@ -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>

View File

@ -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(

View File

@ -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
} }

View File

@ -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

View File

@ -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);
} }
}); });

View File

@ -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")
)

View File

@ -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()

View File

@ -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):

View File

@ -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',

View File

@ -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

View File

@ -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",

View File

@ -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;

View File

@ -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"]},
], ],
} }

View File

@ -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)

View File

@ -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