Merge branch 'develop' into item-group-filter-portal

This commit is contained in:
Anupam Kumar 2020-08-31 16:42:47 +05:30 committed by GitHub
commit 8089b00df2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
87 changed files with 1777 additions and 1181 deletions

View File

@ -135,7 +135,7 @@ var create_import_button = function(frm) {
callback: function(r) { callback: function(r) {
if(!r.exc) { if(!r.exc) {
clearInterval(frm.page["interval"]); clearInterval(frm.page["interval"]);
frm.page.set_indicator(__('Import Successfull'), 'blue'); frm.page.set_indicator(__('Import Successful'), 'blue');
create_reset_button(frm); create_reset_button(frm);
} }
} }

View File

@ -9,6 +9,8 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
from erpnext.stock.get_item_details import get_item_details from erpnext.stock.get_item_details import get_item_details
from frappe.test_runner import make_test_objects from frappe.test_runner import make_test_objects
test_dependencies = ['Item']
def test_create_test_data(): def test_create_test_data():
frappe.set_user("Administrator") frappe.set_user("Administrator")
# create test item # create test item
@ -95,7 +97,6 @@ def test_create_test_data():
}) })
coupon_code.insert() coupon_code.insert()
class TestCouponCode(unittest.TestCase): class TestCouponCode(unittest.TestCase):
def setUp(self): def setUp(self):
test_create_test_data() test_create_test_data()

View File

@ -13,7 +13,7 @@ def get_data():
}, },
{ {
'label': _('References'), 'label': _('References'),
'items': ['Period Closing Voucher', 'Request for Quotation', 'Tax Withholding Category'] 'items': ['Period Closing Voucher', 'Tax Withholding Category']
}, },
{ {
'label': _('Target Details'), 'label': _('Target Details'),

View File

@ -21,7 +21,7 @@ from six import iteritems
class POSInvoice(SalesInvoice): class POSInvoice(SalesInvoice):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(POSInvoice, self).__init__(*args, **kwargs) super(POSInvoice, self).__init__(*args, **kwargs)
def validate(self): def validate(self):
if not cint(self.is_pos): if not cint(self.is_pos):
frappe.throw(_("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment"))) frappe.throw(_("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment")))
@ -58,7 +58,7 @@ class POSInvoice(SalesInvoice):
if self.redeem_loyalty_points and self.loyalty_points: if self.redeem_loyalty_points and self.loyalty_points:
self.apply_loyalty_points() self.apply_loyalty_points()
self.set_status(update=True) self.set_status(update=True)
def on_cancel(self): def on_cancel(self):
# run on cancel method of selling controller # run on cancel method of selling controller
super(SalesInvoice, self).on_cancel() super(SalesInvoice, self).on_cancel()
@ -68,10 +68,10 @@ class POSInvoice(SalesInvoice):
against_psi_doc = frappe.get_doc("POS Invoice", self.return_against) against_psi_doc = frappe.get_doc("POS Invoice", self.return_against)
against_psi_doc.delete_loyalty_point_entry() against_psi_doc.delete_loyalty_point_entry()
against_psi_doc.make_loyalty_point_entry() against_psi_doc.make_loyalty_point_entry()
def validate_stock_availablility(self): def validate_stock_availablility(self):
allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock') allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')
for d in self.get('items'): for d in self.get('items'):
if d.serial_no: if d.serial_no:
filters = { filters = {
@ -89,11 +89,11 @@ class POSInvoice(SalesInvoice):
for s in serial_nos: for s in serial_nos:
if s in reserved_serial_nos: if s in reserved_serial_nos:
invalid_serial_nos.append(s) invalid_serial_nos.append(s)
if len(invalid_serial_nos): if len(invalid_serial_nos):
multiple_nos = 's' if len(invalid_serial_nos) > 1 else '' multiple_nos = 's' if len(invalid_serial_nos) > 1 else ''
frappe.throw(_("Row #{}: Serial No{}. {} has already been transacted into another POS Invoice. \ frappe.throw(_("Row #{}: Serial No{}. {} has already been transacted into another POS Invoice. \
Please select valid serial no.".format(d.idx, multiple_nos, Please select valid serial no.".format(d.idx, multiple_nos,
frappe.bold(', '.join(invalid_serial_nos)))), title=_("Not Available")) frappe.bold(', '.join(invalid_serial_nos)))), title=_("Not Available"))
else: else:
if allow_negative_stock: if allow_negative_stock:
@ -105,9 +105,9 @@ class POSInvoice(SalesInvoice):
.format(d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse))), title=_("Not Available")) .format(d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse))), title=_("Not Available"))
elif flt(available_stock) < flt(d.qty): elif flt(available_stock) < flt(d.qty):
frappe.msgprint(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. \ frappe.msgprint(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. \
Available quantity {}.'.format(d.idx, frappe.bold(d.item_code), Available quantity {}.'.format(d.idx, frappe.bold(d.item_code),
frappe.bold(d.warehouse), frappe.bold(d.qty))), title=_("Not Available")) frappe.bold(d.warehouse), frappe.bold(d.qty))), title=_("Not Available"))
def validate_serialised_or_batched_item(self): def validate_serialised_or_batched_item(self):
for d in self.get("items"): for d in self.get("items"):
serialized = d.get("has_serial_no") serialized = d.get("has_serial_no")
@ -125,7 +125,7 @@ class POSInvoice(SalesInvoice):
if batched and no_batch_selected: if batched and no_batch_selected:
frappe.throw(_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.' frappe.throw(_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.'
.format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item")) .format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item"))
def validate_return_items(self): def validate_return_items(self):
if not self.get("is_return"): return if not self.get("is_return"): return
@ -158,7 +158,7 @@ class POSInvoice(SalesInvoice):
frappe.throw(_("Row #{0} (Payment Table): Amount must be positive").format(entry.idx)) frappe.throw(_("Row #{0} (Payment Table): Amount must be positive").format(entry.idx))
if self.is_return and entry.amount > 0: if self.is_return and entry.amount > 0:
frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx)) frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx))
def validate_pos_return(self): def validate_pos_return(self):
if self.is_pos and self.is_return: if self.is_pos and self.is_return:
total_amount_in_payments = 0 total_amount_in_payments = 0
@ -167,12 +167,12 @@ class POSInvoice(SalesInvoice):
invoice_total = self.rounded_total or self.grand_total invoice_total = self.rounded_total or self.grand_total
if total_amount_in_payments < invoice_total: if total_amount_in_payments < invoice_total:
frappe.throw(_("Total payments amount can't be greater than {}".format(-invoice_total))) frappe.throw(_("Total payments amount can't be greater than {}".format(-invoice_total)))
def validate_loyalty_transaction(self): def validate_loyalty_transaction(self):
if self.redeem_loyalty_points and (not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center): if self.redeem_loyalty_points and (not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center):
expense_account, cost_center = frappe.db.get_value('Loyalty Program', self.loyalty_program, ["expense_account", "cost_center"]) expense_account, cost_center = frappe.db.get_value('Loyalty Program', self.loyalty_program, ["expense_account", "cost_center"])
if not self.loyalty_redemption_account: if not self.loyalty_redemption_account:
self.loyalty_redemption_account = expense_account self.loyalty_redemption_account = expense_account
if not self.loyalty_redemption_cost_center: if not self.loyalty_redemption_cost_center:
self.loyalty_redemption_cost_center = cost_center self.loyalty_redemption_cost_center = cost_center
@ -212,7 +212,7 @@ class POSInvoice(SalesInvoice):
if update: if update:
self.db_set('status', self.status, update_modified = update_modified) self.db_set('status', self.status, update_modified = update_modified)
def set_pos_fields(self, for_validate=False): def set_pos_fields(self, for_validate=False):
"""Set retail related fields from POS Profiles""" """Set retail related fields from POS Profiles"""
from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile
@ -315,25 +315,25 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist() @frappe.whitelist()
def get_stock_availability(item_code, warehouse): def get_stock_availability(item_code, warehouse):
latest_sle = frappe.db.sql("""select qty_after_transaction latest_sle = frappe.db.sql("""select qty_after_transaction
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where item_code = %s and warehouse = %s where item_code = %s and warehouse = %s
order by posting_date desc, posting_time desc order by posting_date desc, posting_time desc
limit 1""", (item_code, warehouse), as_dict=1) limit 1""", (item_code, warehouse), as_dict=1)
pos_sales_qty = frappe.db.sql("""select sum(p_item.qty) as qty pos_sales_qty = frappe.db.sql("""select sum(p_item.qty) as qty
from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item
where p.name = p_item.parent where p.name = p_item.parent
and p.consolidated_invoice is NULL and p.consolidated_invoice is NULL
and p.docstatus = 1 and p.docstatus = 1
and p_item.docstatus = 1 and p_item.docstatus = 1
and p_item.item_code = %s and p_item.item_code = %s
and p_item.warehouse = %s and p_item.warehouse = %s
""", (item_code, warehouse), as_dict=1) """, (item_code, warehouse), as_dict=1)
sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0 sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0
pos_sales_qty = pos_sales_qty[0].qty or 0 if pos_sales_qty else 0 pos_sales_qty = pos_sales_qty[0].qty or 0 if pos_sales_qty else 0
if sle_qty and pos_sales_qty and sle_qty > pos_sales_qty: if sle_qty and pos_sales_qty and sle_qty > pos_sales_qty:
return sle_qty - pos_sales_qty return sle_qty - pos_sales_qty
else: else:
@ -360,14 +360,14 @@ def make_merge_log(invoices):
merge_log = frappe.new_doc("POS Invoice Merge Log") merge_log = frappe.new_doc("POS Invoice Merge Log")
merge_log.posting_date = getdate(nowdate()) merge_log.posting_date = getdate(nowdate())
for inv in invoices: for inv in invoices:
inv_data = frappe.db.get_values("POS Invoice", inv.get('name'), inv_data = frappe.db.get_values("POS Invoice", inv.get('name'),
["customer", "posting_date", "grand_total"], as_dict=1)[0] ["customer", "posting_date", "grand_total"], as_dict=1)[0]
merge_log.customer = inv_data.customer merge_log.customer = inv_data.customer
merge_log.append("pos_invoices", { merge_log.append("pos_invoices", {
'pos_invoice': inv.get('name'), 'pos_invoice': inv.get('name'),
'customer': inv_data.customer, 'customer': inv_data.customer,
'posting_date': inv_data.posting_date, 'posting_date': inv_data.posting_date,
'grand_total': inv_data.grand_total 'grand_total': inv_data.grand_total
}) })
if merge_log.get('pos_invoices'): if merge_log.get('pos_invoices'):

View File

@ -8,6 +8,8 @@ import unittest
from erpnext.stock.get_item_details import get_pos_profile from erpnext.stock.get_item_details import get_pos_profile
from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes
test_dependencies = ['Item']
class TestPOSProfile(unittest.TestCase): class TestPOSProfile(unittest.TestCase):
def test_pos_profile(self): def test_pos_profile(self):
make_pos_profile() make_pos_profile()
@ -88,7 +90,7 @@ def make_pos_profile(**args):
"write_off_account": args.write_off_account or "_Test Write Off - _TC", "write_off_account": args.write_off_account or "_Test Write Off - _TC",
"write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC" "write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC"
}) })
payments = [{ payments = [{
'mode_of_payment': 'Cash', 'mode_of_payment': 'Cash',
'default': 1 'default': 1

View File

@ -1,5 +1,4 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
# For license information, please see license.txt # For license information, please see license.txt
@ -208,7 +207,7 @@ def get_serial_no_for_item(args):
def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False): def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False):
from erpnext.accounts.doctype.pricing_rule.utils import (get_pricing_rules, from erpnext.accounts.doctype.pricing_rule.utils import (get_pricing_rules,
get_applied_pricing_rules, get_pricing_rule_items, get_product_discount_rule) get_applied_pricing_rules, get_pricing_rule_items, get_product_discount_rule)
if isinstance(doc, string_types): if isinstance(doc, string_types):
doc = json.loads(doc) doc = json.loads(doc)
@ -237,7 +236,7 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
update_args_for_pricing_rule(args) update_args_for_pricing_rule(args)
pricing_rules = (get_applied_pricing_rules(args) pricing_rules = (get_applied_pricing_rules(args.get('pricing_rules'))
if for_validate and args.get("pricing_rules") else get_pricing_rules(args, doc)) if for_validate and args.get("pricing_rules") else get_pricing_rules(args, doc))
if pricing_rules: if pricing_rules:
@ -365,8 +364,9 @@ def set_discount_amount(rate, item_details):
item_details.rate = rate item_details.rate = rate
def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None):
from erpnext.accounts.doctype.pricing_rule.utils import get_pricing_rule_items from erpnext.accounts.doctype.pricing_rule.utils import (get_applied_pricing_rules,
for d in json.loads(pricing_rules): get_pricing_rule_items)
for d in get_applied_pricing_rules(pricing_rules):
if not d or not frappe.db.exists("Pricing Rule", d): continue if not d or not frappe.db.exists("Pricing Rule", d): continue
pricing_rule = frappe.get_cached_doc('Pricing Rule', d) pricing_rule = frappe.get_cached_doc('Pricing Rule', d)

View File

@ -447,9 +447,14 @@ def apply_pricing_rule_on_transaction(doc):
apply_pricing_rule_for_free_items(doc, item_details.free_item_data) apply_pricing_rule_for_free_items(doc, item_details.free_item_data)
doc.set_missing_values() doc.set_missing_values()
def get_applied_pricing_rules(item_row): def get_applied_pricing_rules(pricing_rules):
return (json.loads(item_row.get("pricing_rules")) if pricing_rules:
if item_row.get("pricing_rules") else []) if pricing_rules.startswith('['):
return json.loads(pricing_rules)
else:
return pricing_rules.split(',')
return []
def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
free_item = pricing_rule.free_item free_item = pricing_rule.free_item

View File

@ -1,5 +1,4 @@
{ {
"actions": [],
"autoname": "hash", "autoname": "hash",
"creation": "2013-05-22 12:43:10", "creation": "2013-05-22 12:43:10",
"doctype": "DocType", "doctype": "DocType",
@ -82,6 +81,7 @@
"item_tax_rate", "item_tax_rate",
"bom", "bom",
"include_exploded_items", "include_exploded_items",
"purchase_invoice_item",
"col_break6", "col_break6",
"purchase_order", "purchase_order",
"po_detail", "po_detail",
@ -769,12 +769,21 @@
"collapsible": 1, "collapsible": 1,
"fieldname": "col_break7", "fieldname": "col_break7",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"depends_on": "eval:parent.update_stock == 1",
"fieldname": "purchase_invoice_item",
"fieldtype": "Data",
"ignore_user_permissions": 1,
"label": "Purchase Invoice Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "modified": "2020-08-20 11:48:01.398356",
"modified": "2020-04-22 10:37:35.103176",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@ -447,7 +447,7 @@
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "po_no", "fieldname": "po_no",
"fieldtype": "Small Text", "fieldtype": "Data",
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Customer's Purchase Order", "label": "Customer's Purchase Order",
@ -1946,7 +1946,7 @@
"idx": 181, "idx": 181,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-08-03 23:31:12.675040", "modified": "2020-08-27 01:56:28.532140",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -13,7 +13,8 @@ def get_data():
'Auto Repeat': 'reference_document', 'Auto Repeat': 'reference_document',
}, },
'internal_links': { 'internal_links': {
'Sales Order': ['items', 'sales_order'] 'Sales Order': ['items', 'sales_order'],
'Delivery Note': ['items', 'delivery_note']
}, },
'transactions': [ 'transactions': [
{ {

View File

@ -1,5 +1,4 @@
{ {
"actions": [],
"autoname": "hash", "autoname": "hash",
"creation": "2013-06-04 11:02:19", "creation": "2013-06-04 11:02:19",
"doctype": "DocType", "doctype": "DocType",
@ -87,6 +86,7 @@
"edit_references", "edit_references",
"sales_order", "sales_order",
"so_detail", "so_detail",
"sales_invoice_item",
"column_break_74", "column_break_74",
"delivery_note", "delivery_note",
"dn_detail", "dn_detail",
@ -790,12 +790,22 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Project", "label": "Project",
"options": "Project" "options": "Project"
} },
{
"depends_on": "eval:parent.update_stock == 1",
"fieldname": "sales_invoice_item",
"fieldtype": "Data",
"ignore_user_permissions": 1,
"label": "Sales Invoice Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-07-18 12:24:41.749986", "modified": "2020-08-20 11:24:41.749986",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@ -3,6 +3,22 @@
frappe.ui.form.on('Shipping Rule', { frappe.ui.form.on('Shipping Rule', {
refresh: function(frm) { refresh: function(frm) {
frm.set_query("cost_center", function() {
return {
filters: {
company: frm.doc.company
}
}
})
frm.set_query("account", function() {
return {
filters: {
company: frm.doc.company
}
}
})
frm.trigger('toggle_reqd'); frm.trigger('toggle_reqd');
}, },
calculate_based_on: function(frm) { calculate_based_on: function(frm) {
@ -12,4 +28,4 @@ frappe.ui.form.on('Shipping Rule', {
frm.toggle_reqd("shipping_amount", frm.doc.calculate_based_on === 'Fixed'); frm.toggle_reqd("shipping_amount", frm.doc.calculate_based_on === 'Fixed');
frm.toggle_reqd("conditions", frm.doc.calculate_based_on !== 'Fixed'); frm.toggle_reqd("conditions", frm.doc.calculate_based_on !== 'Fixed');
} }
}); });

View File

@ -1,134 +1,66 @@
{ {
"allow_copy": 0, "actions": [],
"allow_events_in_timeline": 0, "allow_rename": 1,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "field:title", "autoname": "field:title",
"beta": 0,
"creation": "2018-11-22 23:38:39.668804", "creation": "2018-11-22 23:38:39.668804",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"title"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "title", "fieldname": "title",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Title", "label": "Title",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 1 "unique": 1
} }
], ],
"has_web_view": 0, "index_web_pages_for_search": 1,
"hide_heading": 0, "links": [],
"hide_toolbar": 0, "modified": "2020-08-30 19:41:25.783852",
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2020-01-15 17:14:28.951793",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Tax Category", "name": "Tax Category",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "System Manager", "role": "System Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Accounts Manager", "role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Accounts User", "role": "Accounts User",
"set_user_permissions": 0, "share": 1
"share": 1,
"submit": 0,
"write": 0
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1, "track_changes": 1
"track_seen": 0, }
"track_views": 0
}

View File

@ -378,7 +378,7 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g
if filters and filters.get('presentation_currency') != d.default_currency: if filters and filters.get('presentation_currency') != d.default_currency:
currency_info['company'] = d.name currency_info['company'] = d.name
currency_info['company_currency'] = d.default_currency currency_info['company_currency'] = d.default_currency
convert_to_presentation_currency(gl_entries, currency_info) convert_to_presentation_currency(gl_entries, currency_info, filters.get('company'))
for entry in gl_entries: for entry in gl_entries:
key = entry.account_number or entry.account_name key = entry.account_number or entry.account_name

View File

@ -173,7 +173,7 @@ class PartyLedgerSummaryReport(object):
from `tabGL Entry` gle from `tabGL Entry` gle
{join} {join}
where where
gle.docstatus < 2 and gle.party_type=%(party_type)s and ifnull(gle.party, '') != '' gle.docstatus < 2 and gle.is_cancelled = 0 and gle.party_type=%(party_type)s and ifnull(gle.party, '') != ''
and gle.posting_date <= %(to_date)s {conditions} and gle.posting_date <= %(to_date)s {conditions}
order by gle.posting_date order by gle.posting_date
""".format(join=join, join_field=join_field, conditions=conditions), self.filters, as_dict=True) """.format(join=join, join_field=join_field, conditions=conditions), self.filters, as_dict=True)
@ -248,7 +248,7 @@ class PartyLedgerSummaryReport(object):
from from
`tabGL Entry` `tabGL Entry`
where where
docstatus < 2 docstatus < 2 and is_cancelled = 0
and (voucher_type, voucher_no) in ( and (voucher_type, voucher_no) in (
select voucher_type, voucher_no from `tabGL Entry` gle, `tabAccount` acc select voucher_type, voucher_no from `tabGL Entry` gle, `tabAccount` acc
where acc.name = gle.account and acc.account_type = '{income_or_expense}' where acc.name = gle.account and acc.account_type = '{income_or_expense}'

View File

@ -14,7 +14,7 @@ import frappe, erpnext
from erpnext.accounts.report.utils import get_currency, convert_to_presentation_currency from erpnext.accounts.report.utils import get_currency, convert_to_presentation_currency
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from frappe import _ from frappe import _
from frappe.utils import (flt, getdate, get_first_day, add_months, add_days, formatdate, cstr) from frappe.utils import (flt, getdate, get_first_day, add_months, add_days, formatdate, cstr, cint)
from six import itervalues from six import itervalues
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions, get_dimension_with_children from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions, get_dimension_with_children
@ -46,7 +46,7 @@ def get_period_list(from_fiscal_year, to_fiscal_year, period_start_date, period_
start_date = year_start_date start_date = year_start_date
months = get_months(year_start_date, year_end_date) months = get_months(year_start_date, year_end_date)
for i in range(math.ceil(months / months_to_add)): for i in range(cint(math.ceil(months / months_to_add))):
period = frappe._dict({ period = frappe._dict({
"from_date": start_date "from_date": start_date
}) })
@ -423,7 +423,7 @@ def set_gl_entries_by_account(
distributed_cost_center_query=distributed_cost_center_query), gl_filters, as_dict=True) #nosec distributed_cost_center_query=distributed_cost_center_query), gl_filters, as_dict=True) #nosec
if filters and filters.get('presentation_currency'): if filters and filters.get('presentation_currency'):
convert_to_presentation_currency(gl_entries, get_currency(filters)) convert_to_presentation_currency(gl_entries, get_currency(filters), filters.get('company'))
for entry in gl_entries: for entry in gl_entries:
gl_entries_by_account.setdefault(entry.account, []).append(entry) gl_entries_by_account.setdefault(entry.account, []).append(entry)

View File

@ -146,6 +146,12 @@ frappe.query_reports["General Ledger"] = {
return frappe.db.get_link_options('Project', txt); return frappe.db.get_link_options('Project', txt);
} }
}, },
{
"fieldname": "include_dimensions",
"label": __("Consider Accounting Dimensions"),
"fieldtype": "Check",
"default": 0
},
{ {
"fieldname": "show_opening_entries", "fieldname": "show_opening_entries",
"label": __("Show Opening Entries"), "label": __("Show Opening Entries"),

View File

@ -106,15 +106,20 @@ def set_account_currency(filters):
return filters return filters
def get_result(filters, account_details): def get_result(filters, account_details):
gl_entries = get_gl_entries(filters) accounting_dimensions = []
if filters.get("include_dimensions"):
accounting_dimensions = get_accounting_dimensions()
data = get_data_with_opening_closing(filters, account_details, gl_entries) gl_entries = get_gl_entries(filters, accounting_dimensions)
data = get_data_with_opening_closing(filters, account_details,
accounting_dimensions, gl_entries)
result = get_result_as_list(data, filters) result = get_result_as_list(data, filters)
return result return result
def get_gl_entries(filters): def get_gl_entries(filters, accounting_dimensions):
currency_map = get_currency(filters) currency_map = get_currency(filters)
select_fields = """, debit, credit, debit_in_account_currency, select_fields = """, debit, credit, debit_in_account_currency,
credit_in_account_currency """ credit_in_account_currency """
@ -128,6 +133,10 @@ def get_gl_entries(filters):
filters['company_fb'] = frappe.db.get_value("Company", filters['company_fb'] = frappe.db.get_value("Company",
filters.get("company"), 'default_finance_book') filters.get("company"), 'default_finance_book')
dimension_fields = ""
if accounting_dimensions:
dimension_fields = ', '.join(accounting_dimensions) + ','
distributed_cost_center_query = "" distributed_cost_center_query = ""
if filters and filters.get('cost_center'): if filters and filters.get('cost_center'):
select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit, credit*(DCC_allocation.percentage_allocation/100) as credit, debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency, select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit, credit*(DCC_allocation.percentage_allocation/100) as credit, debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency,
@ -141,7 +150,7 @@ def get_gl_entries(filters):
party_type, party_type,
party, party,
voucher_type, voucher_type,
voucher_no, voucher_no, {dimension_fields}
cost_center, project, cost_center, project,
against_voucher_type, against_voucher_type,
against_voucher, against_voucher,
@ -160,13 +169,14 @@ def get_gl_entries(filters):
{conditions} {conditions}
AND posting_date <= %(to_date)s AND posting_date <= %(to_date)s
AND cost_center = DCC_allocation.parent AND cost_center = DCC_allocation.parent
""".format(select_fields_with_percentage=select_fields_with_percentage, conditions=get_conditions(filters).replace("and cost_center in %(cost_center)s ", '')) """.format(dimension_fields=dimension_fields,select_fields_with_percentage=select_fields_with_percentage, conditions=get_conditions(filters).replace("and cost_center in %(cost_center)s ", ''))
gl_entries = frappe.db.sql( gl_entries = frappe.db.sql(
""" """
select select
name as gl_entry, posting_date, account, party_type, party, name as gl_entry, posting_date, account, party_type, party,
voucher_type, voucher_no, cost_center, project, voucher_type, voucher_no, {dimension_fields}
cost_center, project,
against_voucher_type, against_voucher, account_currency, against_voucher_type, against_voucher, account_currency,
remarks, against, is_opening, creation {select_fields} remarks, against, is_opening, creation {select_fields}
from `tabGL Entry` from `tabGL Entry`
@ -174,13 +184,13 @@ def get_gl_entries(filters):
{distributed_cost_center_query} {distributed_cost_center_query}
{order_by_statement} {order_by_statement}
""".format( """.format(
select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query, dimension_fields=dimension_fields, select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query,
order_by_statement=order_by_statement order_by_statement=order_by_statement
), ),
filters, as_dict=1) filters, as_dict=1)
if filters.get('presentation_currency'): if filters.get('presentation_currency'):
return convert_to_presentation_currency(gl_entries, currency_map) return convert_to_presentation_currency(gl_entries, currency_map, filters.get('company'))
else: else:
return gl_entries return gl_entries
@ -247,12 +257,12 @@ def get_conditions(filters):
return "and {}".format(" and ".join(conditions)) if conditions else "" return "and {}".format(" and ".join(conditions)) if conditions else ""
def get_data_with_opening_closing(filters, account_details, gl_entries): def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
data = [] data = []
gle_map = initialize_gle_map(gl_entries, filters) gle_map = initialize_gle_map(gl_entries, filters)
totals, entries = get_accountwise_gle(filters, gl_entries, gle_map) totals, entries = get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map)
# Opening for filtered account # Opening for filtered account
data.append(totals.opening) data.append(totals.opening)
@ -318,7 +328,7 @@ def initialize_gle_map(gl_entries, filters):
return gle_map return gle_map
def get_accountwise_gle(filters, gl_entries, gle_map): def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
totals = get_totals_dict() totals = get_totals_dict()
entries = [] entries = []
consolidated_gle = OrderedDict() consolidated_gle = OrderedDict()
@ -350,8 +360,11 @@ def get_accountwise_gle(filters, gl_entries, gle_map):
if filters.get("group_by") != _('Group by Voucher (Consolidated)'): if filters.get("group_by") != _('Group by Voucher (Consolidated)'):
gle_map[gle.get(group_by)].entries.append(gle) gle_map[gle.get(group_by)].entries.append(gle)
elif filters.get("group_by") == _('Group by Voucher (Consolidated)'): elif filters.get("group_by") == _('Group by Voucher (Consolidated)'):
key = (gle.get("voucher_type"), gle.get("voucher_no"), keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
gle.get("account"), gle.get("cost_center")) for dim in accounting_dimensions:
keylist.append(gle.get(dim))
keylist.append(gle.get("cost_center"))
key = tuple(keylist)
if key not in consolidated_gle: if key not in consolidated_gle:
consolidated_gle.setdefault(key, gle) consolidated_gle.setdefault(key, gle)
else: else:
@ -478,7 +491,19 @@ def get_columns(filters):
"options": "Project", "options": "Project",
"fieldname": "project", "fieldname": "project",
"width": 100 "width": 100
}, }
])
if filters.get("include_dimensions"):
for dim in get_accounting_dimensions(as_list = False):
columns.append({
"label": _(dim.label),
"options": dim.label,
"fieldname": dim.fieldname,
"width": 100
})
columns.extend([
{ {
"label": _("Cost Center"), "label": _("Cost Center"),
"options": "Cost Center", "options": "Cost Center",

View File

@ -6,10 +6,6 @@ from erpnext.accounts.doctype.fiscal_year.fiscal_year import get_from_and_to_dat
from frappe.utils import cint, get_datetime_str, formatdate, flt from frappe.utils import cint, get_datetime_str, formatdate, flt
__exchange_rates = {} __exchange_rates = {}
P_OR_L_ACCOUNTS = list(
sum(frappe.get_list('Account', fields=['name'], or_filters=[{'root_type': 'Income'}, {'root_type': 'Expense'}], as_list=True), ())
)
def get_currency(filters): def get_currency(filters):
""" """
@ -73,18 +69,7 @@ def get_rate_as_at(date, from_currency, to_currency):
return rate return rate
def convert_to_presentation_currency(gl_entries, currency_info, company):
def is_p_or_l_account(account_name):
"""
Check if the given `account name` is an `Account` with `root_type` of either 'Income'
or 'Expense'.
:param account_name:
:return: Boolean
"""
return account_name in P_OR_L_ACCOUNTS
def convert_to_presentation_currency(gl_entries, currency_info):
""" """
Take a list of GL Entries and change the 'debit' and 'credit' values to currencies Take a list of GL Entries and change the 'debit' and 'credit' values to currencies
in `currency_info`. in `currency_info`.
@ -96,6 +81,9 @@ def convert_to_presentation_currency(gl_entries, currency_info):
presentation_currency = currency_info['presentation_currency'] presentation_currency = currency_info['presentation_currency']
company_currency = currency_info['company_currency'] company_currency = currency_info['company_currency']
pl_accounts = [d.name for d in frappe.get_list('Account',
filters={'report_type': 'Profit and Loss', 'company': company})]
for entry in gl_entries: for entry in gl_entries:
account = entry['account'] account = entry['account']
debit = flt(entry['debit']) debit = flt(entry['debit'])
@ -107,7 +95,7 @@ def convert_to_presentation_currency(gl_entries, currency_info):
if account_currency != presentation_currency: if account_currency != presentation_currency:
value = debit or credit value = debit or credit
date = currency_info['report_date'] if not is_p_or_l_account(account) else entry['posting_date'] date = entry['posting_date'] if account in pl_accounts else currency_info['report_date']
converted_value = convert(value, presentation_currency, company_currency, date) converted_value = convert(value, presentation_currency, company_currency, date)
if entry.get('debit'): if entry.get('debit'):

View File

@ -325,7 +325,7 @@ class AccountsController(TransactionBase):
apply_pricing_rule_for_free_items(self, pricing_rule_args.get('free_item_data')) apply_pricing_rule_for_free_items(self, pricing_rule_args.get('free_item_data'))
elif pricing_rule_args.get("validate_applied_rule"): elif pricing_rule_args.get("validate_applied_rule"):
for pricing_rule in get_applied_pricing_rules(item): for pricing_rule in get_applied_pricing_rules(item.get('pricing_rules')):
pricing_rule_doc = frappe.get_cached_doc("Pricing Rule", pricing_rule) pricing_rule_doc = frappe.get_cached_doc("Pricing Rule", pricing_rule)
for field in ['discount_percentage', 'discount_amount', 'rate']: for field in ['discount_percentage', 'discount_amount', 'rate']:
if item.get(field) < pricing_rule_doc.get(field): if item.get(field) < pricing_rule_doc.get(field):

View File

@ -276,6 +276,9 @@ class BuyingController(StockController):
qty_to_be_received_map = get_qty_to_be_received(purchase_orders) qty_to_be_received_map = get_qty_to_be_received(purchase_orders)
for item in self.get('items'): for item in self.get('items'):
if not item.purchase_order:
continue
# reset raw_material cost # reset raw_material cost
item.rm_supp_cost = 0 item.rm_supp_cost = 0
@ -288,6 +291,12 @@ class BuyingController(StockController):
fg_yet_to_be_received = qty_to_be_received_map.get(item_key) fg_yet_to_be_received = qty_to_be_received_map.get(item_key)
if not fg_yet_to_be_received:
frappe.throw(_("Row #{0}: Item {1} is already fully received in Purchase Order {2}")
.format(item.idx, frappe.bold(item.item_code),
frappe.utils.get_link_to_form("Purchase Order", item.purchase_order)),
title=_("Limit Crossed"))
transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code) transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code)
backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code) backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
@ -559,9 +568,19 @@ class BuyingController(StockController):
"serial_no": cstr(d.serial_no).strip() "serial_no": cstr(d.serial_no).strip()
}) })
if self.is_return: if self.is_return:
original_incoming_rate = frappe.db.get_value("Stock Ledger Entry", filters = {
{"voucher_type": "Purchase Receipt", "voucher_no": self.return_against, "voucher_type": self.doctype,
"item_code": d.item_code}, "incoming_rate") "voucher_no": self.return_against,
"item_code": d.item_code
}
if (self.doctype == "Purchase Invoice" and self.update_stock
and d.get("purchase_invoice_item")):
filters["voucher_detail_no"] = d.purchase_invoice_item
elif self.doctype == "Purchase Receipt" and d.get("purchase_receipt_item"):
filters["voucher_detail_no"] = d.purchase_receipt_item
original_incoming_rate = frappe.db.get_value("Stock Ledger Entry", filters, "incoming_rate")
sle.update({ sle.update({
"outgoing_rate": original_incoming_rate "outgoing_rate": original_incoming_rate

View File

@ -497,24 +497,18 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters):
conditions, bin_conditions = [], [] conditions, bin_conditions = [], []
filter_dict = get_doctype_wise_filters(filters) filter_dict = get_doctype_wise_filters(filters)
sub_query = """ select round(`tabBin`.actual_qty, 2) from `tabBin`
where `tabBin`.warehouse = `tabWarehouse`.name
{bin_conditions} """.format(
bin_conditions=get_filters_cond(doctype, filter_dict.get("Bin"),
bin_conditions, ignore_permissions=True))
query = """select `tabWarehouse`.name, query = """select `tabWarehouse`.name,
CONCAT_WS(" : ", "Actual Qty", ifnull( ({sub_query}), 0) ) as actual_qty CONCAT_WS(" : ", "Actual Qty", ifnull(round(`tabBin`.actual_qty, 2), 0 )) actual_qty
from `tabWarehouse` from `tabWarehouse` left join `tabBin`
on `tabBin`.warehouse = `tabWarehouse`.name {bin_conditions}
where where
`tabWarehouse`.`{key}` like {txt} `tabWarehouse`.`{key}` like {txt}
{fcond} {mcond} {fcond} {mcond}
order by order by ifnull(`tabBin`.actual_qty, 0) desc
`tabWarehouse`.name desc
limit limit
{start}, {page_len} {start}, {page_len}
""".format( """.format(
sub_query=sub_query, bin_conditions=get_filters_cond(doctype, filter_dict.get("Bin"),bin_conditions, ignore_permissions=True),
key=searchfield, key=searchfield,
fcond=get_filters_cond(doctype, filter_dict.get("Warehouse"), conditions), fcond=get_filters_cond(doctype, filter_dict.get("Warehouse"), conditions),
mcond=get_match_cond(doctype), mcond=get_match_cond(doctype),

View File

@ -281,6 +281,8 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.rejected_warehouse = source_doc.rejected_warehouse target_doc.rejected_warehouse = source_doc.rejected_warehouse
target_doc.po_detail = source_doc.po_detail target_doc.po_detail = source_doc.po_detail
target_doc.pr_detail = source_doc.pr_detail target_doc.pr_detail = source_doc.pr_detail
target_doc.purchase_invoice_item = source_doc.name
elif doctype == "Delivery Note": elif doctype == "Delivery Note":
target_doc.against_sales_order = source_doc.against_sales_order target_doc.against_sales_order = source_doc.against_sales_order
target_doc.against_sales_invoice = source_doc.against_sales_invoice target_doc.against_sales_invoice = source_doc.against_sales_invoice
@ -296,6 +298,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.so_detail = source_doc.so_detail target_doc.so_detail = source_doc.so_detail
target_doc.dn_detail = source_doc.dn_detail target_doc.dn_detail = source_doc.dn_detail
target_doc.expense_account = source_doc.expense_account target_doc.expense_account = source_doc.expense_account
target_doc.sales_invoice_item = source_doc.name
if default_warehouse_for_sales_return: if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return target_doc.warehouse = default_warehouse_for_sales_return

View File

@ -217,7 +217,9 @@ class SellingController(StockController):
'target_warehouse': p.target_warehouse, 'target_warehouse': p.target_warehouse,
'company': self.company, 'company': self.company,
'voucher_type': self.doctype, 'voucher_type': self.doctype,
'allow_zero_valuation': d.allow_zero_valuation_rate 'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"),
'delivery_note_item': d.get("dn_detail")
})) }))
else: else:
il.append(frappe._dict({ il.append(frappe._dict({
@ -233,7 +235,9 @@ class SellingController(StockController):
'target_warehouse': d.target_warehouse, 'target_warehouse': d.target_warehouse,
'company': self.company, 'company': self.company,
'voucher_type': self.doctype, 'voucher_type': self.doctype,
'allow_zero_valuation': d.allow_zero_valuation_rate 'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"),
'delivery_note_item': d.get("dn_detail")
})) }))
return il return il
@ -302,7 +306,11 @@ class SellingController(StockController):
d.conversion_factor = get_conversion_factor(d.item_code, d.uom).get("conversion_factor") or 1.0 d.conversion_factor = get_conversion_factor(d.item_code, d.uom).get("conversion_factor") or 1.0
return_rate = 0 return_rate = 0
if cint(self.is_return) and self.return_against and self.docstatus==1: if cint(self.is_return) and self.return_against and self.docstatus==1:
return_rate = self.get_incoming_rate_for_return(d.item_code, self.return_against) against_document_no = (d.get("sales_invoice_item")
if self.doctype == "Sales Invoice" else d.get("delivery_note_item"))
return_rate = self.get_incoming_rate_for_return(d.item_code,
self.return_against, against_document_no)
# On cancellation or if return entry submission, make stock ledger entry for # On cancellation or if return entry submission, make stock ledger entry for
# target warehouse first, to update serial no values properly # target warehouse first, to update serial no values properly

View File

@ -301,14 +301,19 @@ class StockController(AccountsController):
return serialized_items return serialized_items
def get_incoming_rate_for_return(self, item_code, against_document): def get_incoming_rate_for_return(self, item_code, against_document, against_document_no=None):
incoming_rate = 0.0 incoming_rate = 0.0
cond = ''
if against_document and item_code: if against_document and item_code:
if against_document_no:
cond = " and voucher_detail_no = %s" %(frappe.db.escape(against_document_no))
incoming_rate = frappe.db.sql("""select abs(stock_value_difference / actual_qty) incoming_rate = frappe.db.sql("""select abs(stock_value_difference / actual_qty)
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where voucher_type = %s and voucher_no = %s where voucher_type = %s and voucher_no = %s
and item_code = %s limit 1""", and item_code = %s {0} limit 1""".format(cond),
(self.doctype, against_document, item_code)) (self.doctype, against_document, item_code))
incoming_rate = incoming_rate[0][0] if incoming_rate else 0.0 incoming_rate = incoming_rate[0][0] if incoming_rate else 0.0
return incoming_rate return incoming_rate

View File

@ -9,6 +9,7 @@ from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
from erpnext.controllers.accounts_controller import validate_conversion_rate, \ from erpnext.controllers.accounts_controller import validate_conversion_rate, \
validate_taxes_and_charges, validate_inclusive_tax validate_taxes_and_charges, validate_inclusive_tax
from erpnext.stock.get_item_details import _get_item_tax_template from erpnext.stock.get_item_details import _get_item_tax_template
from erpnext.accounts.doctype.pricing_rule.utils import get_applied_pricing_rules
class calculate_taxes_and_totals(object): class calculate_taxes_and_totals(object):
def __init__(self, doc): def __init__(self, doc):
@ -209,7 +210,7 @@ class calculate_taxes_and_totals(object):
elif tax.charge_type == "On Previous Row Total": elif tax.charge_type == "On Previous Row Total":
current_tax_fraction = (tax_rate / 100.0) * \ current_tax_fraction = (tax_rate / 100.0) * \
self.doc.get("taxes")[cint(tax.row_id) - 1].grand_total_fraction_for_current_item self.doc.get("taxes")[cint(tax.row_id) - 1].grand_total_fraction_for_current_item
elif tax.charge_type == "On Item Quantity": elif tax.charge_type == "On Item Quantity":
inclusive_tax_amount_per_qty = flt(tax_rate) inclusive_tax_amount_per_qty = flt(tax_rate)
@ -607,7 +608,7 @@ class calculate_taxes_and_totals(object):
base_rate_with_margin = 0.0 base_rate_with_margin = 0.0
if item.price_list_rate: if item.price_list_rate:
if item.pricing_rules and not self.doc.ignore_pricing_rule: if item.pricing_rules and not self.doc.ignore_pricing_rule:
for d in json.loads(item.pricing_rules): for d in get_applied_pricing_rules(item.pricing_rules):
pricing_rule = frappe.get_cached_doc('Pricing Rule', d) pricing_rule = frappe.get_cached_doc('Pricing Rule', d)
if (pricing_rule.margin_type == 'Amount' and pricing_rule.currency == self.doc.currency)\ if (pricing_rule.margin_type == 'Amount' and pricing_rule.currency == self.doc.currency)\

View File

@ -325,7 +325,7 @@ def auto_close_opportunity():
doc.save() doc.save()
@frappe.whitelist() @frappe.whitelist()
def make_opportunity_from_communication(communication, ignore_communication_links=False): def make_opportunity_from_communication(communication, company, ignore_communication_links=False):
from erpnext.crm.doctype.lead.lead import make_lead_from_communication from erpnext.crm.doctype.lead.lead import make_lead_from_communication
doc = frappe.get_doc("Communication", communication) doc = frappe.get_doc("Communication", communication)
@ -337,6 +337,7 @@ def make_opportunity_from_communication(communication, ignore_communication_link
opportunity = frappe.get_doc({ opportunity = frappe.get_doc({
"doctype": "Opportunity", "doctype": "Opportunity",
"company": company,
"opportunity_from": opportunity_from, "opportunity_from": opportunity_from,
"party_name": lead "party_name": lead
}).insert(ignore_permissions=True) }).insert(ignore_permissions=True)

View File

@ -30,14 +30,14 @@ frappe.ui.form.on('Social Media Post', {
let color = frm.doc.twitter_post_id ? "green" : "red"; let color = frm.doc.twitter_post_id ? "green" : "red";
let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted"; let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted";
html += `<div class="col-xs-6"> html += `<div class="col-xs-6">
<span class="indicator whitespace-nowrap ${color}"><span class="hidden-xs">Twitter : ${status} </span></span> <span class="indicator whitespace-nowrap ${color}"><span>Twitter : ${status} </span></span>
</div>` ; </div>` ;
} }
if (frm.doc.linkedin){ if (frm.doc.linkedin){
let color = frm.doc.linkedin_post_id ? "green" : "red"; let color = frm.doc.linkedin_post_id ? "green" : "red";
let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted"; let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted";
html += `<div class="col-xs-6"> html += `<div class="col-xs-6">
<span class="indicator whitespace-nowrap ${color}"><span class="hidden-xs">LinkedIn : ${status} </span></span> <span class="indicator whitespace-nowrap ${color}"><span>LinkedIn : ${status} </span></span>
</div>` ; </div>` ;
} }
html = `<div class="row">${html}</div>`; html = `<div class="row">${html}</div>`;

View File

@ -0,0 +1,52 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Lead Details"] = {
"filters": [
{
"fieldname":"company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"default": frappe.defaults.get_user_default("Company"),
"reqd": 1
},
{
"fieldname":"from_date",
"label": __("From Date"),
"fieldtype": "Date",
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -12),
"reqd": 1
},
{
"fieldname":"to_date",
"label": __("To Date"),
"fieldtype": "Date",
"default": frappe.datetime.get_today(),
"reqd": 1
},
{
"fieldname":"status",
"label": __("Status"),
"fieldtype": "Select",
options: [
{ "value": "Lead", "label": __("Lead") },
{ "value": "Open", "label": __("Open") },
{ "value": "Replied", "label": __("Replied") },
{ "value": "Opportunity", "label": __("Opportunity") },
{ "value": "Quotation", "label": __("Quotation") },
{ "value": "Lost Quotation", "label": __("Lost Quotation") },
{ "value": "Interested", "label": __("Interested") },
{ "value": "Converted", "label": __("Converted") },
{ "value": "Do Not Contact", "label": __("Do Not Contact") },
],
},
{
"fieldname":"territory",
"label": __("Territory"),
"fieldtype": "Link",
"options": "Territory",
}
]
};

View File

@ -7,16 +7,15 @@
"doctype": "Report", "doctype": "Report",
"idx": 3, "idx": 3,
"is_standard": "Yes", "is_standard": "Yes",
"modified": "2020-01-22 16:51:56.591110", "modified": "2020-07-26 23:59:49.897577",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Lead Details", "name": "Lead Details",
"owner": "Administrator", "owner": "Administrator",
"prepared_report": 0, "prepared_report": 0,
"query": "SELECT\n `tabLead`.name as \"Lead Id:Link/Lead:120\",\n `tabLead`.lead_name as \"Lead Name::120\",\n\t`tabLead`.company_name as \"Company Name::120\",\n\t`tabLead`.status as \"Status::120\",\n\tconcat_ws(', ', \n\t\ttrim(',' from `tabAddress`.address_line1), \n\t\ttrim(',' from tabAddress.address_line2)\n\t) as 'Address::180',\n\t`tabAddress`.state as \"State::100\",\n\t`tabAddress`.pincode as \"Pincode::70\",\n\t`tabAddress`.country as \"Country::100\",\n\t`tabLead`.phone as \"Phone::100\",\n\t`tabLead`.mobile_no as \"Mobile No::100\",\n\t`tabLead`.email_id as \"Email Id::120\",\n\t`tabLead`.lead_owner as \"Lead Owner::120\",\n\t`tabLead`.source as \"Source::120\",\n\t`tabLead`.territory as \"Territory::120\",\n\t`tabLead`.notes as \"Notes::360\",\n `tabLead`.owner as \"Owner:Link/User:120\"\nFROM\n\t`tabLead`\n\tleft join `tabDynamic Link` on (\n\t\t`tabDynamic Link`.link_name=`tabLead`.name \n\t\tand `tabDynamic Link`.parenttype = 'Address'\n\t)\n\tleft join `tabAddress` on (\n\t\t`tabAddress`.name=`tabDynamic Link`.parent\n\t)\nWHERE\n\t`tabLead`.docstatus<2\nORDER BY\n\t`tabLead`.name asc",
"ref_doctype": "Lead", "ref_doctype": "Lead",
"report_name": "Lead Details", "report_name": "Lead Details",
"report_type": "Query Report", "report_type": "Script Report",
"roles": [ "roles": [
{ {
"role": "Sales User" "role": "Sales User"

View File

@ -0,0 +1,158 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
from frappe import _
import frappe
def execute(filters=None):
columns, data = get_columns(), get_data(filters)
return columns, data
def get_columns():
columns = [
{
"label": _("Lead"),
"fieldname": "name",
"fieldtype": "Link",
"options": "Lead",
"width": 150,
},
{
"label": _("Lead Name"),
"fieldname": "lead_name",
"fieldtype": "Data",
"width": 120
},
{
"fieldname":"status",
"label": _("Status"),
"fieldtype": "Data",
"width": 100
},
{
"fieldname":"lead_owner",
"label": _("Lead Owner"),
"fieldtype": "Link",
"options": "User",
"width": 100
},
{
"label": _("Territory"),
"fieldname": "territory",
"fieldtype": "Link",
"options": "Territory",
"width": 100
},
{
"label": _("Source"),
"fieldname": "source",
"fieldtype": "Data",
"width": 120
},
{
"label": _("Email"),
"fieldname": "email_id",
"fieldtype": "Data",
"width": 120
},
{
"label": _("Mobile"),
"fieldname": "mobile_no",
"fieldtype": "Data",
"width": 120
},
{
"label": _("Phone"),
"fieldname": "phone",
"fieldtype": "Data",
"width": 120
},
{
"label": _("Owner"),
"fieldname": "owner",
"fieldtype": "Link",
"options": "user",
"width": 120
},
{
"label": _("Company"),
"fieldname": "company",
"fieldtype": "Link",
"options": "Company",
"width": 120
},
{
"fieldname":"address",
"label": _("Address"),
"fieldtype": "Data",
"width": 130
},
{
"fieldname":"state",
"label": _("State"),
"fieldtype": "Data",
"width": 100
},
{
"fieldname":"pincode",
"label": _("Postal Code"),
"fieldtype": "Data",
"width": 90
},
{
"fieldname":"country",
"label": _("Country"),
"fieldtype": "Link",
"options": "Country",
"width": 100
},
]
return columns
def get_data(filters):
return frappe.db.sql("""
SELECT
`tabLead`.name,
`tabLead`.lead_name,
`tabLead`.status,
`tabLead`.lead_owner,
`tabLead`.territory,
`tabLead`.source,
`tabLead`.email_id,
`tabLead`.mobile_no,
`tabLead`.phone,
`tabLead`.owner,
`tabLead`.company,
concat_ws(', ',
trim(',' from `tabAddress`.address_line1),
trim(',' from tabAddress.address_line2)
) AS address,
`tabAddress`.state,
`tabAddress`.pincode,
`tabAddress`.country
FROM
`tabLead` left join `tabDynamic Link` on (
`tabLead`.name = `tabDynamic Link`.link_name and
`tabDynamic Link`.parenttype = 'Address')
left join `tabAddress` on (
`tabAddress`.name=`tabDynamic Link`.parent)
WHERE
company = %(company)s
AND `tabLead`.creation BETWEEN %(from_date)s AND %(to_date)s
{conditions}
ORDER BY
`tabLead`.creation asc """.format(conditions=get_conditions(filters)), filters, as_dict=1)
def get_conditions(filters) :
conditions = []
if filters.get("territory"):
conditions.append(" and `tabLead`.territory=%(territory)s")
if filters.get("status"):
conditions.append(" and `tabLead`.status=%(status)s")
return " ".join(conditions) if conditions else ""

View File

@ -0,0 +1,67 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Lost Opportunity"] = {
"filters": [
{
"fieldname":"company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"default": frappe.defaults.get_user_default("Company"),
"reqd": 1
},
{
"fieldname":"from_date",
"label": __("From Date"),
"fieldtype": "Date",
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -12),
"reqd": 1
},
{
"fieldname":"to_date",
"label": __("To Date"),
"fieldtype": "Date",
"default": frappe.datetime.get_today(),
"reqd": 1
},
{
"fieldname":"lost_reason",
"label": __("Lost Reason"),
"fieldtype": "Link",
"options": "Opportunity Lost Reason"
},
{
"fieldname":"territory",
"label": __("Territory"),
"fieldtype": "Link",
"options": "Territory"
},
{
"fieldname":"opportunity_from",
"label": __("Opportunity From"),
"fieldtype": "Link",
"options": "DocType",
"get_query": function() {
return {
"filters": {
"name": ["in", ["Customer", "Lead"]],
}
}
}
},
{
"fieldname":"party_name",
"label": __("Party"),
"fieldtype": "Dynamic Link",
"options": "opportunity_from"
},
{
"fieldname":"contact_by",
"label": __("Next Contact By"),
"fieldtype": "Link",
"options": "User"
},
]
};

View File

@ -1,13 +1,14 @@
{ {
"add_total_row": 0, "add_total_row": 0,
"creation": "2018-12-31 16:30:57.188837", "creation": "2018-12-31 16:30:57.188837",
"disable_prepared_report": 0,
"disabled": 0, "disabled": 0,
"docstatus": 0, "docstatus": 0,
"doctype": "Report", "doctype": "Report",
"idx": 0, "idx": 0,
"is_standard": "Yes", "is_standard": "Yes",
"json": "{\"order_by\": \"`tabOpportunity`.`modified` desc\", \"filters\": [[\"Opportunity\", \"status\", \"=\", \"Lost\"]], \"fields\": [[\"name\", \"Opportunity\"], [\"opportunity_from\", \"Opportunity\"], [\"party_name\", \"Opportunity\"], [\"customer_name\", \"Opportunity\"], [\"opportunity_type\", \"Opportunity\"], [\"status\", \"Opportunity\"], [\"contact_by\", \"Opportunity\"], [\"docstatus\", \"Opportunity\"], [\"lost_reason\", \"Lost Reason Detail\"]], \"add_totals_row\": 0, \"add_total_row\": 0, \"page_length\": 20}", "json": "{\"order_by\": \"`tabOpportunity`.`modified` desc\", \"filters\": [[\"Opportunity\", \"status\", \"=\", \"Lost\"]], \"fields\": [[\"name\", \"Opportunity\"], [\"opportunity_from\", \"Opportunity\"], [\"party_name\", \"Opportunity\"], [\"customer_name\", \"Opportunity\"], [\"opportunity_type\", \"Opportunity\"], [\"status\", \"Opportunity\"], [\"contact_by\", \"Opportunity\"], [\"docstatus\", \"Opportunity\"], [\"lost_reason\", \"Lost Reason Detail\"]], \"add_totals_row\": 0, \"add_total_row\": 0, \"page_length\": 20}",
"modified": "2019-06-26 16:33:08.083618", "modified": "2020-07-29 15:49:02.848845",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Lost Opportunity", "name": "Lost Opportunity",
@ -15,7 +16,7 @@
"prepared_report": 0, "prepared_report": 0,
"ref_doctype": "Opportunity", "ref_doctype": "Opportunity",
"report_name": "Lost Opportunity", "report_name": "Lost Opportunity",
"report_type": "Report Builder", "report_type": "Script Report",
"roles": [ "roles": [
{ {
"role": "Sales User" "role": "Sales User"

View File

@ -0,0 +1,131 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
from frappe import _
import frappe
def execute(filters=None):
columns, data = get_columns(), get_data(filters)
return columns, data
def get_columns():
columns = [
{
"label": _("Opportunity"),
"fieldname": "name",
"fieldtype": "Link",
"options": "Opportunity",
"width": 170,
},
{
"label": _("Opportunity From"),
"fieldname": "opportunity_from",
"fieldtype": "Link",
"options": "DocType",
"width": 130
},
{
"label": _("Party"),
"fieldname":"party_name",
"fieldtype": "Dynamic Link",
"options": "opportunity_from",
"width": 160
},
{
"label": _("Customer/Lead Name"),
"fieldname":"customer_name",
"fieldtype": "Data",
"width": 150
},
{
"label": _("Opportunity Type"),
"fieldname": "opportunity_type",
"fieldtype": "Data",
"width": 130
},
{
"label": _("Lost Reasons"),
"fieldname": "lost_reason",
"fieldtype": "Data",
"width": 220
},
{
"label": _("Sales Stage"),
"fieldname": "sales_stage",
"fieldtype": "Link",
"options": "Sales Stage",
"width": 150
},
{
"label": _("Territory"),
"fieldname": "territory",
"fieldtype": "Link",
"options": "Territory",
"width": 150
},
{
"label": _("Next Contact By"),
"fieldname": "contact_by",
"fieldtype": "Link",
"options": "User",
"width": 150
}
]
return columns
def get_data(filters):
return frappe.db.sql("""
SELECT
`tabOpportunity`.name,
`tabOpportunity`.opportunity_from,
`tabOpportunity`.party_name,
`tabOpportunity`.customer_name,
`tabOpportunity`.opportunity_type,
`tabOpportunity`.contact_by,
GROUP_CONCAT(`tabOpportunity Lost Reason Detail`.lost_reason separator ', ') lost_reason,
`tabOpportunity`.sales_stage,
`tabOpportunity`.territory
FROM
`tabOpportunity`
{join}
WHERE
`tabOpportunity`.status = 'Lost' and `tabOpportunity`.company = %(company)s
AND `tabOpportunity`.modified BETWEEN %(from_date)s AND %(to_date)s
{conditions}
GROUP BY
`tabOpportunity`.name
ORDER BY
`tabOpportunity`.creation asc """.format(conditions=get_conditions(filters), join=get_join(filters)), filters, as_dict=1)
def get_conditions(filters):
conditions = []
if filters.get("territory"):
conditions.append(" and `tabOpportunity`.territory=%(territory)s")
if filters.get("opportunity_from"):
conditions.append(" and `tabOpportunity`.opportunity_from=%(opportunity_from)s")
if filters.get("party_name"):
conditions.append(" and `tabOpportunity`.party_name=%(party_name)s")
if filters.get("contact_by"):
conditions.append(" and `tabOpportunity`.contact_by=%(contact_by)s")
return " ".join(conditions) if conditions else ""
def get_join(filters):
join = """LEFT JOIN `tabOpportunity Lost Reason Detail`
ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and
`tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name"""
if filters.get("lost_reason"):
join = """JOIN `tabOpportunity Lost Reason Detail`
ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and
`tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name and
`tabOpportunity Lost Reason Detail`.lost_reason = '{0}'
""".format(filters.get("lost_reason"))
return join

View File

@ -7,6 +7,8 @@ import unittest
import frappe import frappe
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_clinical_procedure_template from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_clinical_procedure_template
test_dependencies = ['Item']
class TestClinicalProcedure(unittest.TestCase): class TestClinicalProcedure(unittest.TestCase):
def test_procedure_template_item(self): def test_procedure_template_item(self):
patient, medical_department, practitioner = create_healthcare_docs() patient, medical_department, practitioner = create_healthcare_docs()

View File

@ -226,7 +226,9 @@ let check_and_set_availability = function(frm) {
primary_action_label: __('Book'), primary_action_label: __('Book'),
primary_action: function() { primary_action: function() {
frm.set_value('appointment_time', selected_slot); frm.set_value('appointment_time', selected_slot);
frm.set_value('duration', duration); if (!frm.doc.duration) {
frm.set_value('duration', duration);
}
frm.set_value('practitioner', d.get_value('practitioner')); frm.set_value('practitioner', d.get_value('practitioner'));
frm.set_value('department', d.get_value('department')); frm.set_value('department', d.get_value('department'));
frm.set_value('appointment_date', d.get_value('appointment_date')); frm.set_value('appointment_date', d.get_value('appointment_date'));

View File

@ -17,10 +17,10 @@
"payroll_cost_center", "payroll_cost_center",
"column_break_9", "column_break_9",
"leave_block_list", "leave_block_list",
"leave_section", "approvers",
"leave_approvers", "leave_approvers",
"expense_section",
"expense_approvers", "expense_approvers",
"shift_request_approver",
"lft", "lft",
"rgt", "rgt",
"old_parent" "old_parent"
@ -33,14 +33,18 @@
"label": "Department", "label": "Department",
"oldfieldname": "department_name", "oldfieldname": "department_name",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "parent_department", "fieldname": "parent_department",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Parent Department", "label": "Parent Department",
"options": "Department" "options": "Department",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "company", "fieldname": "company",
@ -48,7 +52,9 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Company", "label": "Company",
"options": "Company", "options": "Company",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"bold": 1, "bold": 1,
@ -56,17 +62,23 @@
"fieldname": "is_group", "fieldname": "is_group",
"fieldtype": "Check", "fieldtype": "Check",
"in_list_view": 1, "in_list_view": 1,
"label": "Is Group" "label": "Is Group",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "disabled", "fieldname": "disabled",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Disabled" "label": "Disabled",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "section_break_4", "fieldname": "section_break_4",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"description": "Days for which Holidays are blocked for this department.", "description": "Days for which Holidays are blocked for this department.",
@ -74,31 +86,25 @@
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Leave Block List", "label": "Leave Block List",
"options": "Leave Block List" "options": "Leave Block List",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "leave_section",
"fieldtype": "Section Break",
"label": "Leave Approvers"
},
{
"description": "The first Leave Approver in the list will be set as the default Leave Approver.",
"fieldname": "leave_approvers", "fieldname": "leave_approvers",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Leave Approver", "label": "Leave Approver",
"options": "Department Approver" "options": "Department Approver",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "expense_section",
"fieldtype": "Section Break",
"label": "Expense Approvers"
},
{
"description": "The first Expense Approver in the list will be set as the default Expense Approver.",
"fieldname": "expense_approvers", "fieldname": "expense_approvers",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Expense Approver", "label": "Expense Approver",
"options": "Department Approver" "options": "Department Approver",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "lft", "fieldname": "lft",
@ -106,7 +112,9 @@
"hidden": 1, "hidden": 1,
"label": "lft", "label": "lft",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "rgt", "fieldname": "rgt",
@ -114,7 +122,9 @@
"hidden": 1, "hidden": 1,
"label": "rgt", "label": "rgt",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "old_parent", "fieldname": "old_parent",
@ -122,28 +132,52 @@
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
"label": "Old Parent", "label": "Old Parent",
"print_hide": 1 "print_hide": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "payroll_cost_center", "fieldname": "payroll_cost_center",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Payroll Cost Center", "label": "Payroll Cost Center",
"options": "Cost Center" "options": "Cost Center",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_9", "fieldname": "column_break_9",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"description": "The first Approver in the list will be set as the default Approver.",
"fieldname": "approvers",
"fieldtype": "Section Break",
"label": "Approvers",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "shift_request_approver",
"fieldtype": "Table",
"label": "Shift Request Approver",
"options": "Department Approver",
"show_days": 1,
"show_seconds": 1
} }
], ],
"icon": "fa fa-sitemap", "icon": "fa fa-sitemap",
"idx": 1, "idx": 1,
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2020-05-05 18:49:28.503931", "modified": "2020-06-23 15:42:00.563272",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Department", "name": "Department",

View File

@ -15,12 +15,12 @@ class DepartmentApprover(Document):
def get_approvers(doctype, txt, searchfield, start, page_len, filters): def get_approvers(doctype, txt, searchfield, start, page_len, filters):
if not filters.get("employee"): if not filters.get("employee"):
frappe.throw(_("Please select Employee Record first.")) frappe.throw(_("Please select Employee first."))
approvers = [] approvers = []
department_details = {} department_details = {}
department_list = [] department_list = []
employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver", "expense_approver"], as_dict=True) employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver", "expense_approver", "shift_request_approver"], as_dict=True)
employee_department = filters.get("department") or employee.department employee_department = filters.get("department") or employee.department
if employee_department: if employee_department:
@ -37,13 +37,18 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters):
if filters.get("doctype") == "Expense Claim" and employee.expense_approver: if filters.get("doctype") == "Expense Claim" and employee.expense_approver:
approvers.append(frappe.db.get_value("User", employee.expense_approver, ['name', 'first_name', 'last_name'])) approvers.append(frappe.db.get_value("User", employee.expense_approver, ['name', 'first_name', 'last_name']))
if filters.get("doctype") == "Shift Request" and employee.shift_request_approver:
approvers.append(frappe.db.get_value("User", employee.shift_request_approver, ['name', 'first_name', 'last_name']))
if filters.get("doctype") == "Leave Application": if filters.get("doctype") == "Leave Application":
parentfield = "leave_approvers" parentfield = "leave_approvers"
field_name = "Leave Approver" field_name = "Leave Approver"
else: elif filters.get("doctype") == "Expense Claim":
parentfield = "expense_approvers" parentfield = "expense_approvers"
field_name = "Expense Approver" field_name = "Expense Approver"
elif filters.get("doctype") == "Shift Request":
parentfield = "shift_request_approver"
field_name = "Shift Request Approver"
if department_list: if department_list:
for d in department_list: for d in department_list:
approvers += frappe.db.sql("""select user.name, user.first_name, user.last_name from approvers += frappe.db.sql("""select user.name, user.first_name, user.last_name from

View File

@ -51,10 +51,14 @@
"column_break_31", "column_break_31",
"grade", "grade",
"branch", "branch",
"approvers_section",
"expense_approver",
"leave_approver",
"column_break_45",
"shift_request_approver",
"attendance_and_leave_details", "attendance_and_leave_details",
"leave_policy", "leave_policy",
"attendance_device_id", "attendance_device_id",
"leave_approver",
"column_break_44", "column_break_44",
"holiday_list", "holiday_list",
"default_shift", "default_shift",
@ -62,7 +66,6 @@
"salary_mode", "salary_mode",
"payroll_cost_center", "payroll_cost_center",
"column_break_52", "column_break_52",
"expense_approver",
"bank_name", "bank_name",
"bank_ac_no", "bank_ac_no",
"health_insurance_section", "health_insurance_section",
@ -806,14 +809,37 @@
"fieldname": "expense_approver", "fieldname": "expense_approver",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Expense Approver", "label": "Expense Approver",
"options": "User" "options": "User",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "approvers_section",
"fieldtype": "Section Break",
"label": "Approvers",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_45",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "shift_request_approver",
"fieldtype": "Link",
"label": "Shift Request Approver",
"options": "User",
"show_days": 1,
"show_seconds": 1
} }
], ],
"icon": "fa fa-user", "icon": "fa fa-user",
"idx": 24, "idx": 24,
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2020-07-03 21:28:04.109189", "modified": "2020-07-28 01:36:04.109189",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee", "name": "Employee",

View File

@ -3,6 +3,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import cint
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe import _ from frappe import _
@ -24,8 +25,7 @@ class JobOffer(Document):
check_vacancies = frappe.get_single("HR Settings").check_vacancies check_vacancies = frappe.get_single("HR Settings").check_vacancies
if staffing_plan and check_vacancies: if staffing_plan and check_vacancies:
job_offers = self.get_job_offer(staffing_plan.from_date, staffing_plan.to_date) job_offers = self.get_job_offer(staffing_plan.from_date, staffing_plan.to_date)
if not staffing_plan.get("vacancies") or cint(staffing_plan.vacancies) - len(job_offers) <= 0:
if not staffing_plan.get("vacancies") or staffing_plan.vacancies - len(job_offers) <= 0:
error_variable = 'for ' + frappe.bold(self.designation) error_variable = 'for ' + frappe.bold(self.designation)
if staffing_plan.get("parent"): if staffing_plan.get("parent"):
error_variable = frappe.bold(get_link_to_form("Staffing Plan", staffing_plan.parent)) error_variable = frappe.bold(get_link_to_form("Staffing Plan", staffing_plan.parent))
@ -65,7 +65,7 @@ def get_staffing_plan_detail(designation, company, offer_date):
AND %s between sp.from_date and sp.to_date AND %s between sp.from_date and sp.to_date
""", (designation, company, offer_date), as_dict=1) """, (designation, company, offer_date), as_dict=1)
return frappe._dict(detail[0]) if detail else None return frappe._dict(detail[0]) if (detail and detail[0].parent) else None
@frappe.whitelist() @frappe.whitelist()
def make_employee(source_name, target_doc=None): def make_employee(source_name, target_doc=None):

View File

@ -10,9 +10,11 @@
"employee", "employee",
"employee_name", "employee_name",
"shift_type", "shift_type",
"status",
"column_break_3", "column_break_3",
"company", "company",
"date", "start_date",
"end_date",
"shift_request", "shift_request",
"department", "department",
"amended_from" "amended_from"
@ -59,12 +61,6 @@
"options": "Company", "options": "Company",
"reqd": 1 "reqd": 1
}, },
{
"fieldname": "date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date"
},
{ {
"fieldname": "shift_request", "fieldname": "shift_request",
"fieldtype": "Link", "fieldtype": "Link",
@ -80,11 +76,36 @@
"options": "Shift Assignment", "options": "Shift Assignment",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Start Date",
"reqd": 1
},
{
"allow_on_submit": 1,
"fieldname": "end_date",
"fieldtype": "Date",
"label": "End Date",
"show_days": 1,
"show_seconds": 1
},
{
"allow_on_submit": 1,
"default": "Active",
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Active\nInactive",
"show_days": 1,
"show_seconds": 1
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2019-12-12 15:49:06.956901", "modified": "2020-06-15 14:27:54.310773",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Shift Assignment", "name": "Shift Assignment",

View File

@ -11,38 +11,63 @@ from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
from datetime import timedelta, datetime from datetime import timedelta, datetime
class OverlapError(frappe.ValidationError): pass
class ShiftAssignment(Document): class ShiftAssignment(Document):
def validate(self): def validate(self):
self.validate_overlapping_dates() self.validate_overlapping_dates()
if self.end_date and self.end_date <= self.start_date:
frappe.throw(_("End Date must not be lesser than Start Date"))
def validate_overlapping_dates(self): def validate_overlapping_dates(self):
if not self.name: if not self.name:
self.name = "New Shift Assignment" self.name = "New Shift Assignment"
d = frappe.db.sql(""" condition = """and (
select end_date is null
name, shift_type, date or
from `tabShift Assignment` %(start_date)s between start_date and end_date
where employee = %(employee)s and docstatus < 2 """
and date = %(date)s
and name != %(name)s""", {
"employee": self.employee,
"shift_type": self.shift_type,
"date": self.date,
"name": self.name
}, as_dict = 1)
for date_overlap in d: if self.end_date:
if date_overlap['name']: condition += """ or
self.throw_overlap_error(date_overlap) %(end_date)s between start_date and end_date
or
start_date between %(start_date)s and %(end_date)s
) """
else:
condition += """ ) """
def throw_overlap_error(self, d): assigned_shifts = frappe.db.sql("""
msg = _("Employee {0} has already applied for {1} on {2} : ").format(self.employee, select name, shift_type, start_date ,end_date, docstatus, status
d['shift_type'], formatdate(d['date'])) \ from `tabShift Assignment`
+ """ <b><a href="#Form/Shift Assignment/{0}">{0}</a></b>""".format(d["name"]) where
frappe.throw(msg, OverlapError) employee=%(employee)s and docstatus = 1
and name != %(name)s
and status = "Active"
{0}
""".format(condition), {
"employee": self.employee,
"shift_type": self.shift_type,
"start_date": self.start_date,
"end_date": self.end_date,
"name": self.name
}, as_dict = 1)
if len(assigned_shifts):
self.throw_overlap_error(assigned_shifts[0])
def throw_overlap_error(self, shift_details):
shift_details = frappe._dict(shift_details)
if shift_details.docstatus == 1 and shift_details.status == "Active":
msg = _("Employee {0} already has Active Shift {1}: {2}").format(frappe.bold(self.employee), frappe.bold(self.shift_type), frappe.bold(shift_details.name))
if shift_details.start_date:
msg += _(" from {0}").format(getdate(self.start_date).strftime("%d-%m-%Y"))
title = "Ongoing Shift"
if shift_details.end_date:
msg += _(" to {0}").format(getdate(self.end_date).strftime("%d-%m-%Y"))
title = "Active Shift"
if msg:
frappe.throw(msg, title=title)
@frappe.whitelist() @frappe.whitelist()
def get_events(start, end, filters=None): def get_events(start, end, filters=None):
@ -62,19 +87,22 @@ def get_events(start, end, filters=None):
return events return events
def add_assignments(events, start, end, conditions=None): def add_assignments(events, start, end, conditions=None):
query = """select name, date, employee_name, query = """select name, start_date, end_date, employee_name,
employee, docstatus employee, docstatus
from `tabShift Assignment` where from `tabShift Assignment` where
date <= %(date)s start_date >= %(start_date)s
and docstatus < 2""" or end_date <= %(end_date)s
or (%(start_date)s between start_date and end_date and %(end_date)s between start_date and end_date)
and docstatus = 1"""
if conditions: if conditions:
query += conditions query += conditions
for d in frappe.db.sql(query, {"date":start, "date":end}, as_dict=True): for d in frappe.db.sql(query, {"start_date":start, "end_date":end}, as_dict=True):
e = { e = {
"name": d.name, "name": d.name,
"doctype": "Shift Assignment", "doctype": "Shift Assignment",
"date": d.date, "start_date": d.start_date,
"end_date": d.end_date if d.end_date else nowdate(),
"title": cstr(d.employee_name) + \ "title": cstr(d.employee_name) + \
cstr(d.shift_type), cstr(d.shift_type),
"docstatus": d.docstatus "docstatus": d.docstatus
@ -92,7 +120,16 @@ def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=Fals
:param next_shift_direction: One of: None, 'forward', 'reverse'. Direction to look for next shift if shift not found on given date. :param next_shift_direction: One of: None, 'forward', 'reverse'. Direction to look for next shift if shift not found on given date.
""" """
default_shift = frappe.db.get_value('Employee', employee, 'default_shift') default_shift = frappe.db.get_value('Employee', employee, 'default_shift')
shift_type_name = frappe.db.get_value('Shift Assignment', {'employee':employee, 'date': for_date, 'docstatus': '1'}, 'shift_type') shift_type_name = None
shift_assignment_details = frappe.db.get_value('Shift Assignment', {'employee':employee, 'start_date':('<=', for_date), 'docstatus': '1', 'status': "Active"}, ['shift_type', 'end_date'])
if shift_assignment_details:
shift_type_name = shift_assignment_details[0]
# if end_date present means that shift is over after end_date else it is a ongoing shift.
if shift_assignment_details[1] and for_date >= shift_assignment_details[1] :
shift_type_name = None
if not shift_type_name and consider_default_shift: if not shift_type_name and consider_default_shift:
shift_type_name = default_shift shift_type_name = default_shift
if shift_type_name: if shift_type_name:
@ -117,16 +154,20 @@ def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=Fals
direction = '<' if next_shift_direction == 'reverse' else '>' direction = '<' if next_shift_direction == 'reverse' else '>'
sort_order = 'desc' if next_shift_direction == 'reverse' else 'asc' sort_order = 'desc' if next_shift_direction == 'reverse' else 'asc'
dates = frappe.db.get_all('Shift Assignment', dates = frappe.db.get_all('Shift Assignment',
'date', ['start_date', 'end_date'],
{'employee':employee, 'date':(direction, for_date), 'docstatus': '1'}, {'employee':employee, 'start_date':(direction, for_date), 'docstatus': '1', "status": "Active"},
as_list=True, as_list=True,
limit=MAX_DAYS, order_by="date "+sort_order) limit=MAX_DAYS, order_by="start_date "+sort_order)
for date in dates:
shift_details = get_employee_shift(employee, date[0], consider_default_shift, None) if dates:
if shift_details: for date in dates:
shift_type_name = shift_details.shift_type.name if date[1] and date[1] < for_date:
for_date = date[0] continue
break shift_details = get_employee_shift(employee, date[0], consider_default_shift, None)
if shift_details:
shift_type_name = shift_details.shift_type.name
for_date = date[0]
break
return get_shift_details(shift_type_name, for_date) return get_shift_details(shift_type_name, for_date)
@ -134,7 +175,7 @@ def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=Fals
def get_employee_shift_timings(employee, for_timestamp=now_datetime(), consider_default_shift=False): def get_employee_shift_timings(employee, for_timestamp=now_datetime(), consider_default_shift=False):
"""Returns previous shift, current/upcoming shift, next_shift for the given timestamp and employee """Returns previous shift, current/upcoming shift, next_shift for the given timestamp and employee
""" """
# write and verify a test case for midnight shift. # write and verify a test case for midnight shift.
prev_shift = curr_shift = next_shift = None prev_shift = curr_shift = next_shift = None
curr_shift = get_employee_shift(employee, for_timestamp.date(), consider_default_shift, 'forward') curr_shift = get_employee_shift(employee, for_timestamp.date(), consider_default_shift, 'forward')
if curr_shift: if curr_shift:

View File

@ -3,8 +3,8 @@
frappe.views.calendar["Shift Assignment"] = { frappe.views.calendar["Shift Assignment"] = {
field_map: { field_map: {
"start": "date", "start": "start_date",
"end": "date", "end": "end_date",
"id": "name", "id": "name",
"docstatus": 1 "docstatus": 1
}, },

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
from frappe.utils import nowdate from frappe.utils import nowdate, add_days
test_dependencies = ["Shift Type"] test_dependencies = ["Shift Type"]
@ -20,8 +20,61 @@ class TestShiftAssignment(unittest.TestCase):
"shift_type": "Day Shift", "shift_type": "Day Shift",
"company": "_Test Company", "company": "_Test Company",
"employee": "_T-Employee-00001", "employee": "_T-Employee-00001",
"date": nowdate() "start_date": nowdate()
}).insert() }).insert()
shift_assignment.submit() shift_assignment.submit()
self.assertEqual(shift_assignment.docstatus, 1) self.assertEqual(shift_assignment.docstatus, 1)
def test_overlapping_for_ongoing_shift(self):
# shift should be Ongoing if Only start_date is present and status = Active
shift_assignment_1 = frappe.get_doc({
"doctype": "Shift Assignment",
"shift_type": "Day Shift",
"company": "_Test Company",
"employee": "_T-Employee-00001",
"start_date": nowdate(),
"status": 'Active'
}).insert()
shift_assignment_1.submit()
self.assertEqual(shift_assignment_1.docstatus, 1)
shift_assignment = frappe.get_doc({
"doctype": "Shift Assignment",
"shift_type": "Day Shift",
"company": "_Test Company",
"employee": "_T-Employee-00001",
"start_date": add_days(nowdate(), 2)
})
self.assertRaises(frappe.ValidationError, shift_assignment.save)
def test_overlapping_for_fixed_period_shift(self):
# shift should is for Fixed period if Only start_date and end_date both are present and status = Active
shift_assignment_1 = frappe.get_doc({
"doctype": "Shift Assignment",
"shift_type": "Day Shift",
"company": "_Test Company",
"employee": "_T-Employee-00001",
"start_date": nowdate(),
"end_date": add_days(nowdate(), 30),
"status": 'Active'
}).insert()
shift_assignment_1.submit()
# it should not allowed within period of any shift.
shift_assignment_3 = frappe.get_doc({
"doctype": "Shift Assignment",
"shift_type": "Day Shift",
"company": "_Test Company",
"employee": "_T-Employee-00001",
"start_date":add_days(nowdate(), 10),
"end_date": add_days(nowdate(), 35),
"status": 'Active'
})
self.assertRaises(frappe.ValidationError, shift_assignment_3.save)

View File

@ -2,7 +2,16 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Shift Request', { frappe.ui.form.on('Shift Request', {
refresh: function(frm) { setup: function(frm) {
frm.set_query("approver", function() {
} return {
query: "erpnext.hr.doctype.department_approver.department_approver.get_approvers",
filters: {
employee: frm.doc.employee,
doctype: frm.doc.doctype
}
};
});
frm.set_query("employee", erpnext.queries.employee);
},
}); });

View File

@ -1,396 +1,155 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0, "allow_import": 1,
"allow_import": 1, "autoname": "HR-SHR-.YY.-.MM.-.#####",
"allow_rename": 0, "creation": "2018-04-13 16:32:27.974273",
"autoname": "HR-SHR-.YY.-.MM.-.#####", "doctype": "DocType",
"beta": 0, "editable_grid": 1,
"creation": "2018-04-13 16:32:27.974273", "engine": "InnoDB",
"custom": 0, "field_order": [
"docstatus": 0, "shift_type",
"doctype": "DocType", "employee",
"document_type": "", "employee_name",
"editable_grid": 1, "department",
"engine": "InnoDB", "status",
"column_break_4",
"company",
"approver",
"from_date",
"to_date",
"amended_from"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "shift_type",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Shift Type",
"collapsible": 0, "options": "Shift Type",
"columns": 0, "reqd": 1
"fieldname": "shift_type", },
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Shift Type",
"length": 0,
"no_copy": 0,
"options": "Shift Type",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "employee",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Employee",
"collapsible": 0, "options": "Employee",
"columns": 0, "reqd": 1
"fieldname": "employee", },
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Employee",
"length": 0,
"no_copy": 0,
"options": "Employee",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fetch_from": "employee.employee_name",
"allow_in_quick_entry": 0, "fieldname": "employee_name",
"allow_on_submit": 0, "fieldtype": "Data",
"bold": 0, "label": "Employee Name",
"collapsible": 0, "read_only": 1
"columns": 0, },
"fetch_from": "employee.employee_name",
"fieldname": "employee_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Employee Name",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fetch_from": "employee.department",
"allow_in_quick_entry": 0, "fieldname": "department",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "label": "Department",
"collapsible": 0, "options": "Department",
"columns": 0, "read_only": 1
"fetch_from": "employee.department", },
"fieldname": "department",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Department",
"length": 0,
"no_copy": 0,
"options": "Department",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_4",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_4",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "company",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Company",
"collapsible": 0, "options": "Company",
"columns": 0, "reqd": 1
"fieldname": "company", },
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "from_date",
"allow_in_quick_entry": 0, "fieldtype": "Date",
"allow_on_submit": 0, "label": "From Date",
"bold": 0, "reqd": 1
"collapsible": 0, },
"columns": 0,
"fieldname": "from_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "From Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "to_date",
"allow_in_quick_entry": 0, "fieldtype": "Date",
"allow_on_submit": 0, "label": "To Date"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "to_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "To Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "amended_from",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "label": "Amended From",
"bold": 0, "no_copy": 1,
"collapsible": 0, "options": "Shift Request",
"columns": 0, "print_hide": 1,
"fieldname": "amended_from", "read_only": 1
"fieldtype": "Link", },
"hidden": 0, {
"ignore_user_permissions": 0, "default": "Draft",
"ignore_xss_filter": 0, "fieldname": "status",
"in_filter": 0, "fieldtype": "Select",
"in_global_search": 0, "label": "Status",
"in_list_view": 0, "options": "Draft\nApproved\nRejected",
"in_standard_filter": 0, "reqd": 1
"label": "Amended From", },
"length": 0, {
"no_copy": 1, "fetch_from": "employee.shift_request_approver",
"options": "Shift Request", "fetch_if_empty": 1,
"permlevel": 0, "fieldname": "approver",
"print_hide": 1, "fieldtype": "Link",
"print_hide_if_no_value": 0, "label": "Approver",
"read_only": 1, "options": "User",
"remember_last_selected_value": 0, "reqd": 1
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "is_submittable": 1,
"hide_heading": 0, "links": [],
"hide_toolbar": 0, "modified": "2020-08-10 17:59:31.550558",
"idx": 0, "modified_by": "Administrator",
"image_view": 0, "module": "HR",
"in_create": 0, "name": "Shift Request",
"is_submittable": 1, "owner": "Administrator",
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-08-21 16:15:36.577448",
"modified_by": "Administrator",
"module": "HR",
"name": "Shift Request",
"name_case": "",
"owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0, "create": 1,
"cancel": 0, "email": 1,
"create": 1, "export": 1,
"delete": 0, "print": 1,
"email": 1, "read": 1,
"export": 1, "report": 1,
"if_owner": 0, "role": "Employee",
"import": 0, "share": 1,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1 "write": 1
}, },
{ {
"amend": 1, "amend": 1,
"cancel": 1, "cancel": 1,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0, "print": 1,
"import": 0, "read": 1,
"permlevel": 0, "report": 1,
"print": 1, "role": "HR Manager",
"read": 1, "share": 1,
"report": 1, "submit": 1,
"role": "HR Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1 "write": 1
}, },
{ {
"amend": 0, "create": 1,
"cancel": 0, "email": 1,
"create": 1, "export": 1,
"delete": 0, "print": 1,
"email": 1, "read": 1,
"export": 1, "report": 1,
"if_owner": 0, "role": "HR User",
"import": 0, "share": 1,
"permlevel": 0, "submit": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0, "sort_field": "modified",
"read_only": 0, "sort_order": "DESC",
"read_only_onload": 0, "title_field": "employee_name",
"show_name_in_global_search": 0, "track_changes": 1
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "employee_name",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
} }

View File

@ -14,19 +14,26 @@ class ShiftRequest(Document):
def validate(self): def validate(self):
self.validate_dates() self.validate_dates()
self.validate_shift_request_overlap_dates() self.validate_shift_request_overlap_dates()
self.validate_approver()
self.validate_default_shift()
def on_submit(self): def on_submit(self):
date_list = self.get_working_days(self.from_date, self.to_date) if self.status not in ["Approved", "Rejected"]:
for date in date_list: frappe.throw(_("Only Shift Request with status 'Approved' and 'Rejected' can be submitted"))
if self.status == "Approved":
assignment_doc = frappe.new_doc("Shift Assignment") assignment_doc = frappe.new_doc("Shift Assignment")
assignment_doc.company = self.company assignment_doc.company = self.company
assignment_doc.shift_type = self.shift_type assignment_doc.shift_type = self.shift_type
assignment_doc.employee = self.employee assignment_doc.employee = self.employee
assignment_doc.date = date assignment_doc.start_date = self.from_date
if self.to_date:
assignment_doc.end_date = self.to_date
assignment_doc.shift_request = self.name assignment_doc.shift_request = self.name
assignment_doc.insert() assignment_doc.insert()
assignment_doc.submit() assignment_doc.submit()
frappe.msgprint(_("Shift Assignment: {0} created for Employee: {1}").format(frappe.bold(assignment_doc.name), frappe.bold(self.employee)))
def on_cancel(self): def on_cancel(self):
shift_assignment_list = frappe.get_list("Shift Assignment", {'employee': self.employee, 'shift_request': self.name}) shift_assignment_list = frappe.get_list("Shift Assignment", {'employee': self.employee, 'shift_request': self.name})
if shift_assignment_list: if shift_assignment_list:
@ -34,6 +41,19 @@ class ShiftRequest(Document):
shift_assignment_doc = frappe.get_doc("Shift Assignment", shift['name']) shift_assignment_doc = frappe.get_doc("Shift Assignment", shift['name'])
shift_assignment_doc.cancel() shift_assignment_doc.cancel()
def validate_default_shift(self):
default_shift = frappe.get_value("Employee", self.employee, "default_shift")
if self.shift_type == default_shift:
frappe.throw(_("You can not request for your Default Shift: {0}").format(frappe.bold(self.shift_type)))
def validate_approver(self):
department = frappe.get_value("Employee", self.employee, "department")
shift_approver = frappe.get_value("Employee", self.employee, "shift_request_approver")
approvers = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department))
approvers = [approver[0] for approver in approvers]
approvers.append(shift_approver)
if self.approver not in approvers:
frappe.throw(_("Only Approvers can Approve this Request."))
def validate_dates(self): def validate_dates(self):
if self.from_date and self.to_date and (getdate(self.to_date) < getdate(self.from_date)): if self.from_date and self.to_date and (getdate(self.to_date) < getdate(self.from_date)):
@ -68,28 +88,4 @@ class ShiftRequest(Document):
msg = _("Employee {0} has already applied for {1} between {2} and {3} : ").format(self.employee, msg = _("Employee {0} has already applied for {1} between {2} and {3} : ").format(self.employee,
d['shift_type'], formatdate(d['from_date']), formatdate(d['to_date'])) \ d['shift_type'], formatdate(d['from_date']), formatdate(d['to_date'])) \
+ """ <b><a href="#Form/Shift Request/{0}">{0}</a></b>""".format(d["name"]) + """ <b><a href="#Form/Shift Request/{0}">{0}</a></b>""".format(d["name"])
frappe.throw(msg, OverlapError) frappe.throw(msg, OverlapError)
def get_working_days(self, start_date, end_date):
start_date, end_date = getdate(start_date), getdate(end_date)
from datetime import timedelta
date_list = []
employee_holiday_list = []
employee_holidays = frappe.db.sql("""select holiday_date from `tabHoliday`
where parent in (select holiday_list from `tabEmployee`
where name = %s)""",self.employee,as_dict=1)
for d in employee_holidays:
employee_holiday_list.append(d.holiday_date)
reference_date = start_date
while reference_date <= end_date:
if reference_date not in employee_holiday_list:
date_list.append(reference_date)
reference_date += timedelta(days=1)
return date_list

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
from frappe.utils import nowdate from frappe.utils import nowdate, add_days
class TestShiftRequest(unittest.TestCase): class TestShiftRequest(unittest.TestCase):
def setUp(self): def setUp(self):
@ -13,14 +13,20 @@ class TestShiftRequest(unittest.TestCase):
frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype)) frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype))
def test_make_shift_request(self): def test_make_shift_request(self):
department = frappe.get_value("Employee", "_T-Employee-00001", 'department')
set_shift_approver(department)
approver = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department))[0][0]
shift_request = frappe.get_doc({ shift_request = frappe.get_doc({
"doctype": "Shift Request", "doctype": "Shift Request",
"shift_type": "Day Shift", "shift_type": "Day Shift",
"company": "_Test Company", "company": "_Test Company",
"employee": "_T-Employee-00001", "employee": "_T-Employee-00001",
"employee_name": "_Test Employee", "employee_name": "_Test Employee",
"start_date": nowdate(), "from_date": nowdate(),
"end_date": nowdate() "to_date": add_days(nowdate(), 10),
"approver": approver,
"status": "Approved"
}) })
shift_request.insert() shift_request.insert()
shift_request.submit() shift_request.submit()
@ -34,4 +40,10 @@ class TestShiftRequest(unittest.TestCase):
self.assertEqual(shift_request.employee, employee) self.assertEqual(shift_request.employee, employee)
shift_request.cancel() shift_request.cancel()
shift_assignment_doc = frappe.get_doc("Shift Assignment", {"shift_request": d.get('shift_request')}) shift_assignment_doc = frappe.get_doc("Shift Assignment", {"shift_request": d.get('shift_request')})
self.assertEqual(shift_assignment_doc.docstatus, 2) self.assertEqual(shift_assignment_doc.docstatus, 2)
def set_shift_approver(department):
department_doc = frappe.get_doc("Department", department)
department_doc.append('shift_request_approver',{'approver': "test1@example.com"})
department_doc.save()
department_doc.reload()

View File

@ -4,7 +4,7 @@
frappe.ui.form.on('Shift Type', { frappe.ui.form.on('Shift Type', {
refresh: function(frm) { refresh: function(frm) {
frm.add_custom_button( frm.add_custom_button(
'Mark Auto Attendance', 'Mark Attendance',
() => frm.call({ () => frm.call({
doc: frm.doc, doc: frm.doc,
method: 'process_auto_attendance', method: 'process_auto_attendance',

View File

@ -79,9 +79,10 @@ class ShiftType(Document):
mark_attendance(employee, date, 'Absent', self.name) mark_attendance(employee, date, 'Absent', self.name)
def get_assigned_employee(self, from_date=None, consider_default_shift=False): def get_assigned_employee(self, from_date=None, consider_default_shift=False):
filters = {'date':('>=', from_date), 'shift_type': self.name, 'docstatus': '1'} filters = {'start_date':('>', from_date), 'shift_type': self.name, 'docstatus': '1'}
if not from_date: if not from_date:
del filters['date'] del filters["start_date"]
assigned_employees = frappe.get_all('Shift Assignment', 'employee', filters, as_list=True) assigned_employees = frappe.get_all('Shift Assignment', 'employee', filters, as_list=True)
assigned_employees = [x[0] for x in assigned_employees] assigned_employees = [x[0] for x in assigned_employees]

View File

@ -132,6 +132,9 @@ def get_conditions(filters):
if filters.get('employee'): if filters.get('employee'):
conditions['name'] = filters.get('employee') conditions['name'] = filters.get('employee')
if filters.get('company'):
conditions['company'] = filters.get('company')
return conditions return conditions
def get_department_leave_approver_map(department=None): def get_department_leave_approver_map(department=None):

View File

@ -17,6 +17,7 @@ from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loa
from erpnext.loan_management.doctype.loan.loan import create_loan_security_unpledge from erpnext.loan_management.doctype.loan.loan import create_loan_security_unpledge
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty
from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge
from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount
class TestLoan(unittest.TestCase): class TestLoan(unittest.TestCase):
def setUp(self): def setUp(self):
@ -323,6 +324,56 @@ class TestLoan(unittest.TestCase):
self.assertEqual(loan.status, 'Closed') self.assertEqual(loan.status, 'Closed')
self.assertEquals(sum(pledged_qty.values()), 0) self.assertEquals(sum(pledged_qty.values()), 0)
def test_disbursal_check_with_shortfall(self):
pledges = [{
"loan_security": "Test Security 2",
"qty": 8000.00,
"haircut": 50,
}]
loan_application = create_loan_application('_Test Company', self.applicant2,
'Stock Loan', pledges, "Repay Over Number of Periods", 12)
create_pledge(loan_application)
loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application)
loan.submit()
#Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge
make_loan_disbursement_entry(loan.name, 700000)
frappe.db.sql("""UPDATE `tabLoan Security Price` SET loan_security_price = 100
where loan_security='Test Security 2'""")
create_process_loan_security_shortfall()
loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name})
self.assertTrue(loan_security_shortfall)
self.assertEqual(get_disbursal_amount(loan.name), 0)
frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 250
where loan_security='Test Security 2'""")
def test_disbursal_check_without_shortfall(self):
pledges = [{
"loan_security": "Test Security 2",
"qty": 8000.00,
"haircut": 50,
}]
loan_application = create_loan_application('_Test Company', self.applicant2,
'Stock Loan', pledges, "Repay Over Number of Periods", 12)
create_pledge(loan_application)
loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application)
loan.submit()
#Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge
make_loan_disbursement_entry(loan.name, 700000)
self.assertEqual(get_disbursal_amount(loan.name), 300000)
def create_loan_accounts(): def create_loan_accounts():
if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"):

View File

@ -33,18 +33,18 @@ frappe.ui.form.on('Loan Application', {
if (frm.doc.is_secured_loan) { if (frm.doc.is_secured_loan) {
frappe.db.get_value("Loan Security Pledge", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => { frappe.db.get_value("Loan Security Pledge", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => {
if (!r) { if (Object.keys(r).length === 0) {
frm.add_custom_button(__('Loan Security Pledge'), function() { frm.add_custom_button(__('Loan Security Pledge'), function() {
frm.trigger('create_loan_security_pledge') frm.trigger('create_loan_security_pledge');
},__('Create')) },__('Create'))
} }
}); });
} }
frappe.db.get_value("Loan", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => { frappe.db.get_value("Loan", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => {
if (!r) { if (Object.keys(r).length === 0) {
frm.add_custom_button(__('Loan'), function() { frm.add_custom_button(__('Loan'), function() {
frm.trigger('create_loan') frm.trigger('create_loan');
},__('Create')) },__('Create'))
} else { } else {
frm.set_df_property('status', 'read_only', 1); frm.set_df_property('status', 'read_only', 1);
@ -54,7 +54,7 @@ frappe.ui.form.on('Loan Application', {
}, },
create_loan: function(frm) { create_loan: function(frm) {
if (frm.doc.status != "Approved") { if (frm.doc.status != "Approved") {
frappe.throw(__("Cannot create loan until application is approved")) frappe.throw(__("Cannot create loan until application is approved"));
} }
frappe.model.open_mapped_doc({ frappe.model.open_mapped_doc({

View File

@ -67,28 +67,10 @@ class LoanDisbursement(AccountsController):
disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount
total_payment = loan_details.total_payment total_payment = loan_details.total_payment
if disbursed_amount > loan_details.loan_amount and loan_details.is_term_loan: possible_disbursal_amount = get_disbursal_amount(self.against_loan)
frappe.throw(_("Disbursed Amount cannot be greater than loan amount"))
if loan_details.status == 'Disbursed': if self.disbursed_amount > possible_disbursal_amount:
pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \ frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount))
- flt(loan_details.total_principal_paid)
else:
pending_principal_amount = loan_details.disbursed_amount
security_value = 0.0
if loan_details.is_secured_loan:
security_value = get_total_pledged_security_value(self.against_loan)
if not security_value:
security_value = loan_details.loan_amount
if pending_principal_amount + self.disbursed_amount > flt(security_value):
allowed_amount = security_value - pending_principal_amount
if allowed_amount < 0:
allowed_amount = 0
frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(allowed_amount))
if loan_details.status == "Disbursed" and not loan_details.is_term_loan: if loan_details.status == "Disbursed" and not loan_details.is_term_loan:
process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1), process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1),
@ -176,3 +158,32 @@ def get_total_pledged_security_value(loan):
security_value += (loan_security_price_map.get(security) * qty * hair_cut_map.get(security))/100 security_value += (loan_security_price_map.get(security) * qty * hair_cut_map.get(security))/100
return security_value return security_value
@frappe.whitelist()
def get_disbursal_amount(loan):
loan_details = frappe.get_all("Loan", fields = ["loan_amount", "disbursed_amount", "total_payment",
"total_principal_paid", "total_interest_payable", "status", "is_term_loan", "is_secured_loan"],
filters= { "name": loan })[0]
if loan_details.is_secured_loan and frappe.get_all('Loan Security Shortfall', filters={'loan': loan,
'status': 'Pending'}):
return 0
if loan_details.status == 'Disbursed':
pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \
- flt(loan_details.total_principal_paid)
else:
pending_principal_amount = flt(loan_details.disbursed_amount)
security_value = 0.0
if loan_details.is_secured_loan:
security_value = get_total_pledged_security_value(loan)
if not security_value and not loan_details.is_secured_loan:
security_value = flt(loan_details.loan_amount)
disbursal_amount = flt(security_value) - flt(pending_principal_amount)
return disbursal_amount

View File

@ -85,8 +85,11 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i
if no_of_days <= 0: if no_of_days <= 0:
return return
pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \ if loan.status == 'Disbursed':
- flt(loan.total_principal_paid) pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \
- flt(loan.total_principal_paid)
else:
pending_principal_amount = loan.disbursed_amount
interest_per_day = (pending_principal_amount * loan.rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100) interest_per_day = (pending_principal_amount * loan.rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100)
payable_interest = interest_per_day * no_of_days payable_interest = interest_per_day * no_of_days
@ -107,7 +110,7 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i
def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None): def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None):
query_filters = { query_filters = {
"status": "Disbursed", "status": ('in', ['Disbursed', 'Partially Disbursed']),
"docstatus": 1 "docstatus": 1
} }
@ -118,8 +121,9 @@ def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_inte
if not open_loans: if not open_loans:
open_loans = frappe.get_all("Loan", open_loans = frappe.get_all("Loan",
fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", "is_term_loan", fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account",
"disbursement_date", "applicant_type", "applicant", "rate_of_interest", "total_interest_payable", "repayment_start_date"], "is_term_loan", "status", "disbursement_date", "disbursed_amount", "applicant_type", "applicant",
"rate_of_interest", "total_interest_payable", "total_principal_paid", "repayment_start_date"],
filters=query_filters) filters=query_filters)
for loan in open_loans: for loan in open_loans:

View File

@ -281,7 +281,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
due_date = add_days(entry.posting_date, 1) due_date = add_days(entry.posting_date, 1)
no_of_late_days = date_diff(posting_date, no_of_late_days = date_diff(posting_date,
add_days(due_date, loan_type_details.grace_period_in_days)) add_days(due_date, loan_type_details.grace_period_in_days))
if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary): if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary):
penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)/365 penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)/365
@ -297,7 +297,10 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
if not final_due_date: if not final_due_date:
final_due_date = add_days(due_date, loan_type_details.grace_period_in_days) final_due_date = add_days(due_date, loan_type_details.grace_period_in_days)
pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable if against_loan_doc.status == 'Disbursed':
pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable
else:
pending_principal_amount = against_loan_doc.disbursed_amount
if payment_type == "Loan Closure": if payment_type == "Loan Closure":
if due_date: if due_date:

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import get_datetime from frappe.utils import get_datetime, flt
from frappe.model.document import Document from frappe.model.document import Document
from six import iteritems from six import iteritems
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty
@ -51,13 +51,19 @@ def check_for_ltv_shortfall(process_loan_security_shortfall):
"valid_upto": (">=", update_time) "valid_upto": (">=", update_time)
}, as_list=1)) }, as_list=1))
loans = frappe.get_all('Loan', fields=['name', 'loan_amount', 'total_principal_paid'], loans = frappe.get_all('Loan', fields=['name', 'loan_amount', 'total_principal_paid', 'total_payment',
filters={'status': 'Disbursed', 'is_secured_loan': 1}) 'total_interest_payable', 'disbursed_amount', 'status'],
filters={'status': ('in',['Disbursed','Partially Disbursed']), 'is_secured_loan': 1})
loan_security_map = {} loan_security_map = {}
for loan in loans: for loan in loans:
outstanding_amount = loan.loan_amount - loan.total_principal_paid if loan.status == 'Disbursed':
outstanding_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \
- flt(loan.total_principal_paid)
else:
outstanding_amount = loan.disbursed_amount
pledged_securities = get_pledged_security_qty(loan.name) pledged_securities = get_pledged_security_qty(loan.name)
ltv_ratio = '' ltv_ratio = ''
security_value = 0.0 security_value = 0.0

View File

@ -67,16 +67,16 @@ class MaintenanceSchedule(TransactionBase):
for key in scheduled_date: for key in scheduled_date:
description =frappe._("Reference: {0}, Item Code: {1} and Customer: {2}").format(self.name, d.item_code, self.customer) description =frappe._("Reference: {0}, Item Code: {1} and Customer: {2}").format(self.name, d.item_code, self.customer)
frappe.get_doc({ event = frappe.get_doc({
"doctype": "Event", "doctype": "Event",
"owner": email_map.get(d.sales_person, self.owner), "owner": email_map.get(d.sales_person, self.owner),
"subject": description, "subject": description,
"description": description, "description": description,
"starts_on": cstr(key["scheduled_date"]) + " 10:00:00", "starts_on": cstr(key["scheduled_date"]) + " 10:00:00",
"event_type": "Private", "event_type": "Private",
"ref_type": self.doctype, })
"ref_name": self.name event.add_participant(self.doctype, self.name)
}).insert(ignore_permissions=1) event.insert(ignore_permissions=1)
frappe.db.set(self, 'status', 'Submitted') frappe.db.set(self, 'status', 'Submitted')

View File

@ -2,6 +2,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
from frappe.utils.data import get_datetime, add_days
import frappe import frappe
import unittest import unittest
@ -9,4 +10,39 @@ import unittest
# test_records = frappe.get_test_records('Maintenance Schedule') # test_records = frappe.get_test_records('Maintenance Schedule')
class TestMaintenanceSchedule(unittest.TestCase): class TestMaintenanceSchedule(unittest.TestCase):
pass def test_events_should_be_created_and_deleted(self):
ms = make_maintenance_schedule()
ms.generate_schedule()
ms.submit()
all_events = get_events(ms)
self.assertTrue(len(all_events) > 0)
ms.cancel()
events_after_cancel = get_events(ms)
self.assertTrue(len(events_after_cancel) == 0)
def get_events(ms):
return frappe.get_all("Event Participants", filters={
"reference_doctype": ms.doctype,
"reference_docname": ms.name,
"parenttype": "Event"
})
def make_maintenance_schedule():
ms = frappe.new_doc("Maintenance Schedule")
ms.company = "_Test Company"
ms.customer = "_Test Customer"
ms.transaction_date = get_datetime()
ms.append("items", {
"item_code": "_Test Item",
"start_date": get_datetime(),
"end_date": add_days(get_datetime(), 32),
"periodicity": "Weekly",
"no_of_visits": 4,
"sales_person": "Sales Team",
})
ms.insert(ignore_permissions=True)
return ms

View File

@ -494,7 +494,7 @@ class BOM(WebsiteGenerator):
'image' : d.image, 'image' : d.image,
'stock_uom' : d.stock_uom, 'stock_uom' : d.stock_uom,
'stock_qty' : flt(d.stock_qty), 'stock_qty' : flt(d.stock_qty),
'rate' : flt(d.base_rate) / flt(d.conversion_factor), 'rate' : flt(d.base_rate) / (flt(d.conversion_factor) or 1.0),
'include_item_in_manufacturing': d.include_item_in_manufacturing 'include_item_in_manufacturing': d.include_item_in_manufacturing
})) }))

View File

@ -90,6 +90,7 @@ def update_latest_price_in_all_boms():
update_cost() update_cost()
def replace_bom(args): def replace_bom(args):
frappe.db.auto_commit_on_many_writes = 1
args = frappe._dict(args) args = frappe._dict(args)
doc = frappe.get_doc("BOM Update Tool") doc = frappe.get_doc("BOM Update Tool")
@ -97,6 +98,8 @@ def replace_bom(args):
doc.new_bom = args.new_bom doc.new_bom = args.new_bom
doc.replace_bom() doc.replace_bom()
frappe.db.auto_commit_on_many_writes = 0
def update_cost(): def update_cost():
frappe.db.auto_commit_on_many_writes = 1 frappe.db.auto_commit_on_many_writes = 1
bom_list = get_boms_in_bottom_up_order() bom_list = get_boms_in_bottom_up_order()

View File

@ -718,6 +718,8 @@ erpnext.patches.v13_0.delete_report_requested_items_to_order
erpnext.patches.v12_0.update_item_tax_template_company erpnext.patches.v12_0.update_item_tax_template_company
erpnext.patches.v13_0.move_branch_code_to_bank_account erpnext.patches.v13_0.move_branch_code_to_bank_account
erpnext.patches.v13_0.healthcare_lab_module_rename_doctypes erpnext.patches.v13_0.healthcare_lab_module_rename_doctypes
erpnext.patches.v13_0.add_standard_navbar_items #4
erpnext.patches.v13_0.stock_entry_enhancements erpnext.patches.v13_0.stock_entry_enhancements
erpnext.patches.v12_0.update_state_code_for_daman_and_diu erpnext.patches.v12_0.update_state_code_for_daman_and_diu
erpnext.patches.v12_0.rename_lost_reason_detail erpnext.patches.v12_0.rename_lost_reason_detail
erpnext.patches.v13_0.update_start_end_date_for_old_shift_assignment

View File

@ -7,6 +7,7 @@ def execute():
frappe.reload_doc('accounts', 'doctype', 'allowed_to_transact_with', force=True) frappe.reload_doc('accounts', 'doctype', 'allowed_to_transact_with', force=True)
frappe.reload_doc('accounts', 'doctype', 'pricing_rule_detail', force=True) frappe.reload_doc('accounts', 'doctype', 'pricing_rule_detail', force=True)
frappe.reload_doc('crm', 'doctype', 'lost_reason_detail', force=True) frappe.reload_doc('crm', 'doctype', 'lost_reason_detail', force=True)
frappe.reload_doc('setup', 'doctype', 'quotation_lost_reason_detail', force=True)
company = frappe.get_all('Company', filters = {'country': 'United States'}) company = frappe.get_all('Company', filters = {'country': 'United States'})
if not company: if not company:

View File

@ -3,6 +3,7 @@ import frappe
def execute(): def execute():
if frappe.db.exists("DocType", "Lost Reason Detail"): if frappe.db.exists("DocType", "Lost Reason Detail"):
frappe.reload_doc("crm", "doctype", "opportunity_lost_reason")
frappe.reload_doc("crm", "doctype", "opportunity_lost_reason_detail") frappe.reload_doc("crm", "doctype", "opportunity_lost_reason_detail")
frappe.reload_doc("setup", "doctype", "quotation_lost_reason_detail") frappe.reload_doc("setup", "doctype", "quotation_lost_reason_detail")
@ -10,8 +11,8 @@ def execute():
frappe.db.sql("""INSERT INTO `tabQuotation Lost Reason Detail` SELECT * FROM `tabLost Reason Detail` WHERE `parenttype` = 'Quotation'""") frappe.db.sql("""INSERT INTO `tabQuotation Lost Reason Detail` SELECT * FROM `tabLost Reason Detail` WHERE `parenttype` = 'Quotation'""")
frappe.db.sql("""INSERT INTO `tabQuotation Lost Reason` (`name`, `creation`, `modified`, `modified_by`, `owner`, `docstatus`, `parent`, `parentfield`, `parenttype`, `idx`, `_comments`, `_assign`, `_user_tags`, `_liked_by`, `order_lost_reason`) frappe.db.sql("""INSERT INTO `tabQuotation Lost Reason` (`name`, `creation`, `modified`, `modified_by`, `owner`, `docstatus`, `parent`, `parentfield`, `parenttype`, `idx`, `_comments`, `_assign`, `_user_tags`, `_liked_by`, `order_lost_reason`)
SELECT o.`name`, o.`creation`, o.`modified`, o.`modified_by`, o.`owner`, o.`docstatus`, o.`parent`, o.`parentfield`, o.`parenttype`, o.`idx`, o.`_comments`, o.`_assign`, o.`_user_tags`, o.`_liked_by`, o.`lost_reason` SELECT o.`name`, o.`creation`, o.`modified`, o.`modified_by`, o.`owner`, o.`docstatus`, o.`parent`, o.`parentfield`, o.`parenttype`, o.`idx`, o.`_comments`, o.`_assign`, o.`_user_tags`, o.`_liked_by`, o.`lost_reason`
FROM `tabOpportunity Lost Reason` o LEFT JOIN `tabQuotation Lost Reason` q ON q.name = o.name WHERE q.name IS NULL""") FROM `tabOpportunity Lost Reason` o LEFT JOIN `tabQuotation Lost Reason` q ON q.name = o.name WHERE q.name IS NULL""")
frappe.delete_doc("DocType", "Lost Reason Detail") frappe.delete_doc("DocType", "Lost Reason Detail")

View File

@ -0,0 +1,7 @@
from __future__ import unicode_literals
# import frappe
from erpnext.setup.install import add_standard_navbar_items
def execute():
# Add standard navbar items for ERPNext in Navbar Settings
add_standard_navbar_items()

View File

@ -0,0 +1,13 @@
# Copyright (c) 2019, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc('hr', 'doctype', 'shift_assignment')
if frappe.db.has_column('Shift Assignment', 'date'):
frappe.db.sql("""update `tabShift Assignment`
set end_date=date, start_date=date
where date IS NOT NULL and start_date IS NULL and end_date IS NULL;""")

View File

@ -21,34 +21,6 @@ frappe.ui.form.on('Homepage', {
}); });
frappe.ui.form.on('Homepage Featured Product', { frappe.ui.form.on('Homepage Featured Product', {
item_code: function(frm, cdt, cdn) {
var featured_product = frappe.model.get_doc(cdt, cdn);
if (featured_product.item_code) {
frappe.call({
method: 'frappe.client.get_value',
args: {
'doctype': 'Item',
'filters': {'name': featured_product.item_code},
'fieldname': [
'item_name',
'web_long_description',
'description',
'image',
'thumbnail'
]
},
callback: function(r) {
if (!r.exc) {
$.extend(featured_product, r.message);
if (r.message.web_long_description) {
featured_product.description = r.message.web_long_description;
}
frm.refresh_field('products');
}
}
});
}
},
view: function(frm, cdt, cdn){ view: function(frm, cdt, cdn){
var child= locals[cdt][cdn] var child= locals[cdt][cdn]

View File

@ -1,301 +1,116 @@
{ {
"allow_copy": 0, "actions": [],
"allow_import": 0, "autoname": "hash",
"allow_rename": 0, "creation": "2016-04-22 05:57:06.261401",
"autoname": "hash", "doctype": "DocType",
"beta": 0, "document_type": "Document",
"creation": "2016-04-22 05:57:06.261401", "editable_grid": 1,
"custom": 0, "engine": "InnoDB",
"docstatus": 0, "field_order": [
"doctype": "DocType", "item_code",
"document_type": "Document", "col_break1",
"editable_grid": 1, "item_name",
"view",
"section_break_5",
"description",
"column_break_7",
"image",
"thumbnail",
"route"
],
"fields": [ "fields": [
{ {
"allow_on_submit": 0, "bold": 1,
"bold": 1, "fieldname": "item_code",
"collapsible": 0, "fieldtype": "Link",
"fieldname": "item_code", "in_filter": 1,
"fieldtype": "Link", "in_list_view": 1,
"hidden": 0, "label": "Item Code",
"ignore_user_permissions": 0, "oldfieldname": "item_code",
"ignore_xss_filter": 0, "oldfieldtype": "Link",
"in_filter": 1, "options": "Item",
"in_list_view": 1, "print_width": "150px",
"label": "Item Code", "reqd": 1,
"length": 0, "search_index": 1,
"no_copy": 0,
"oldfieldname": "item_code",
"oldfieldtype": "Link",
"options": "Item",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "150px",
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 1,
"set_only_once": 0,
"unique": 0,
"width": "150px" "width": "150px"
}, },
{ {
"allow_on_submit": 0, "fieldname": "col_break1",
"bold": 0, "fieldtype": "Column Break"
"collapsible": 0, },
"fieldname": "col_break1",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "fetch_from": "item_code.item_name",
"bold": 0, "fetch_if_empty": 1,
"collapsible": 0, "fieldname": "item_name",
"fieldname": "item_name", "fieldtype": "Data",
"fieldtype": "Data", "in_list_view": 1,
"hidden": 0, "label": "Item Name",
"ignore_user_permissions": 0, "oldfieldname": "item_name",
"ignore_xss_filter": 0, "oldfieldtype": "Data",
"in_filter": 0, "print_hide": 1,
"in_list_view": 1, "print_width": "150",
"label": "Item Name", "read_only": 1,
"length": 0, "reqd": 1,
"no_copy": 0,
"oldfieldname": "item_name",
"oldfieldtype": "Data",
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": "150",
"read_only": 1,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "150" "width": "150"
}, },
{ {
"allow_on_submit": 0, "fieldname": "view",
"bold": 0, "fieldtype": "Button",
"collapsible": 0, "in_list_view": 1,
"fieldname": "view", "label": "View"
"fieldtype": "Button", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "View",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "collapsible": 1,
"bold": 0, "fieldname": "section_break_5",
"collapsible": 1, "fieldtype": "Section Break",
"fieldname": "section_break_5", "label": "Description"
"fieldtype": "Section Break", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "fetch_from": "item_code.web_long_description",
"bold": 0, "fieldname": "description",
"collapsible": 0, "fieldtype": "Text Editor",
"fieldname": "description", "in_filter": 1,
"fieldtype": "Text Editor", "in_list_view": 1,
"hidden": 0, "label": "Description",
"ignore_user_permissions": 0, "oldfieldname": "description",
"ignore_xss_filter": 0, "oldfieldtype": "Small Text",
"in_filter": 1, "print_width": "300px",
"in_list_view": 1,
"label": "Description",
"length": 0,
"no_copy": 0,
"oldfieldname": "description",
"oldfieldtype": "Small Text",
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "300px",
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "300px" "width": "300px"
}, },
{ {
"allow_on_submit": 0, "fieldname": "column_break_7",
"bold": 0, "fieldtype": "Column Break"
"collapsible": 0, },
"fieldname": "column_break_7",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "fetch_from": "item_code.website_image",
"bold": 0, "fetch_if_empty": 1,
"collapsible": 0, "fieldname": "image",
"fieldname": "image", "fieldtype": "Attach Image",
"fieldtype": "Attach Image", "label": "Image"
"hidden": 0, },
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Image",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "fieldname": "thumbnail",
"bold": 0, "fieldtype": "Attach Image",
"collapsible": 0, "hidden": 1,
"fieldname": "thumbnail", "label": "Thumbnail"
"fieldtype": "Attach Image", },
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Thumbnail",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "fieldname": "route",
"bold": 0, "fieldtype": "Small Text",
"collapsible": 0, "label": "route",
"fieldname": "route", "read_only": 1
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "route",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
} }
], ],
"hide_heading": 0, "index_web_pages_for_search": 1,
"hide_toolbar": 0, "istable": 1,
"idx": 0, "links": [],
"image_view": 0, "modified": "2020-08-25 15:27:49.573537",
"in_create": 0, "modified_by": "Administrator",
"module": "Portal",
"is_submittable": 0, "name": "Homepage Featured Product",
"issingle": 0, "owner": "Administrator",
"istable": 1, "permissions": [],
"max_attachments": 0, "quick_entry": 1,
"modified": "2016-08-09 06:09:34.731971", "sort_field": "modified",
"modified_by": "Administrator", "sort_order": "DESC"
"module": "Portal",
"name": "Homepage Featured Product",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_seen": 0
} }

View File

@ -7,7 +7,7 @@ frappe.ui.form.on("Communication", {
}, },
setup_custom_buttons: (frm) => { setup_custom_buttons: (frm) => {
let confirm_msg = "Are you sure you want to create {0} from this email"; let confirm_msg = "Are you sure you want to create {0} from this email?";
if(frm.doc.reference_doctype !== "Issue") { if(frm.doc.reference_doctype !== "Issue") {
frm.add_custom_button(__("Issue"), () => { frm.add_custom_button(__("Issue"), () => {
frappe.confirm(__(confirm_msg, [__("Issue")]), () => { frappe.confirm(__(confirm_msg, [__("Issue")]), () => {
@ -62,17 +62,36 @@ frappe.ui.form.on("Communication", {
}, },
make_opportunity_from_communication: (frm) => { make_opportunity_from_communication: (frm) => {
return frappe.call({ const fields = [{
method: "erpnext.crm.doctype.opportunity.opportunity.make_opportunity_from_communication", fieldtype: 'Link',
args: { label: __('Select a Company'),
communication: frm.doc.name fieldname: 'company',
}, options: 'Company',
freeze: true, reqd: 1,
callback: (r) => { default: frappe.defaults.get_user_default("Company")
if(r.message) { }];
frm.reload_doc()
frappe.prompt(fields, data => {
frappe.call({
method: "erpnext.crm.doctype.opportunity.opportunity.make_opportunity_from_communication",
args: {
communication: frm.doc.name,
company: data.company
},
freeze: true,
callback: (r) => {
if(r.message) {
frm.reload_doc();
frappe.show_alert({
message: __("Opportunity {0} created",
['<a href="#Form/Opportunity/'+r.message+'">' + r.message + '</a>']),
indicator: 'green'
});
}
} }
} });
}) },
'Create an Opportunity',
'Create');
} }
}); });

View File

@ -3,32 +3,6 @@
frappe.provide('erpnext'); frappe.provide('erpnext');
// add toolbar icon
$(document).bind('toolbar_setup', function() {
frappe.app.name = "ERPNext";
frappe.help_feedback_link = '<p><a class="text-muted" \
href="https://discuss.erpnext.com">Feedback</a></p>'
$('[data-link="docs"]').attr("href", "https://erpnext.com/docs")
$('[data-link="issues"]').attr("href", "https://github.com/frappe/erpnext/issues")
// default documentation goes to erpnext
// $('[data-link-type="documentation"]').attr('data-path', '/erpnext/manual/index');
// additional help links for erpnext
var $help_menu = $('.dropdown-help ul .documentation-links');
$('<li><a data-link-type="forum" href="https://erpnext.com/docs/user/manual" \
target="_blank">'+__('Documentation')+'</a></li>').insertBefore($help_menu);
$('<li><a data-link-type="forum" href="https://discuss.erpnext.com" \
target="_blank">'+__('User Forum')+'</a></li>').insertBefore($help_menu);
$('<li><a href="https://github.com/frappe/erpnext/issues" \
target="_blank">'+__('Report an Issue')+'</a></li>').insertBefore($help_menu);
});
// preferred modules for breadcrumbs // preferred modules for breadcrumbs
$.extend(frappe.breadcrumbs.preferred, { $.extend(frappe.breadcrumbs.preferred, {
"Item Group": "Stock", "Item Group": "Stock",

View File

@ -22,6 +22,8 @@
} }
.filter-options { .filter-options {
margin-left: -5px;
padding-left: 5px;
max-height: 300px; max-height: 300px;
overflow: auto; overflow: auto;
} }

View File

@ -1,6 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, re, json import frappe, re, json
from frappe import _ from frappe import _
import erpnext
from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words
from erpnext.regional.india import states, state_numbers from erpnext.regional.india import states, state_numbers
from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount
@ -673,25 +674,34 @@ def update_grand_total_for_rcm(doc, method):
if country != 'India': if country != 'India':
return return
if not doc.total_taxes_and_charges:
return
if doc.reverse_charge == 'Y': if doc.reverse_charge == 'Y':
gst_accounts = get_gst_accounts(doc.company) gst_accounts = get_gst_accounts(doc.company)
gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \ gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
+ gst_accounts.get('igst_account') + gst_accounts.get('igst_account')
base_gst_tax = 0
gst_tax = 0 gst_tax = 0
for tax in doc.get('taxes'): for tax in doc.get('taxes'):
if tax.category not in ("Total", "Valuation and Total"): if tax.category not in ("Total", "Valuation and Total"):
continue continue
if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list: if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list:
gst_tax += tax.base_tax_amount_after_discount_amount base_gst_tax += tax.base_tax_amount_after_discount_amount
gst_tax += tax.tax_amount_after_discount_amount
doc.taxes_and_charges_added -= gst_tax doc.taxes_and_charges_added -= gst_tax
doc.total_taxes_and_charges -= gst_tax doc.total_taxes_and_charges -= gst_tax
doc.base_taxes_and_charges_added -= base_gst_tax
doc.base_total_taxes_and_charges -= base_gst_tax
update_totals(gst_tax, doc) update_totals(gst_tax, base_gst_tax, doc)
def update_totals(gst_tax, doc): def update_totals(gst_tax, base_gst_tax, doc):
doc.base_grand_total -= base_gst_tax
doc.grand_total -= gst_tax doc.grand_total -= gst_tax
if doc.meta.get_field("rounded_total"): if doc.meta.get_field("rounded_total"):
@ -707,13 +717,14 @@ def update_totals(gst_tax, doc):
doc.outstanding_amount = doc.rounded_total or doc.grand_total doc.outstanding_amount = doc.rounded_total or doc.grand_total
doc.in_words = money_in_words(doc.grand_total, doc.currency) doc.in_words = money_in_words(doc.grand_total, doc.currency)
doc.base_in_words = money_in_words(doc.base_grand_total, erpnext.get_company_currency(doc.company))
doc.set_payment_schedule() doc.set_payment_schedule()
def make_regional_gl_entries(gl_entries, doc): def make_regional_gl_entries(gl_entries, doc):
country = frappe.get_cached_value('Company', doc.company, 'country') country = frappe.get_cached_value('Company', doc.company, 'country')
if country != 'India': if country != 'India':
return return gl_entries
if doc.reverse_charge == 'Y': if doc.reverse_charge == 'Y':
gst_accounts = get_gst_accounts(doc.company) gst_accounts = get_gst_accounts(doc.company)
@ -724,6 +735,7 @@ def make_regional_gl_entries(gl_entries, doc):
if tax.category not in ("Total", "Valuation and Total"): if tax.category not in ("Total", "Valuation and Total"):
continue continue
dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list: if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list:
account_currency = get_account_currency(tax.account_head) account_currency = get_account_currency(tax.account_head)
@ -733,8 +745,8 @@ def make_regional_gl_entries(gl_entries, doc):
"cost_center": tax.cost_center, "cost_center": tax.cost_center,
"posting_date": doc.posting_date, "posting_date": doc.posting_date,
"against": doc.supplier, "against": doc.supplier,
"credit": tax.base_tax_amount_after_discount_amount, dr_or_cr: tax.base_tax_amount_after_discount_amount,
"credits_in_account_currency": tax.base_tax_amount_after_discount_amount \ dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount \
if account_currency==doc.company_currency \ if account_currency==doc.company_currency \
else tax.tax_amount_after_discount_amount else tax.tax_amount_after_discount_amount
}, account_currency, item=tax) }, account_currency, item=tax)

View File

@ -7,6 +7,8 @@ from frappe import _
from frappe.utils import flt from frappe.utils import flt
from frappe.model.meta import get_field_precision from frappe.model.meta import get_field_precision
from frappe.utils.xlsxutils import handle_html from frappe.utils.xlsxutils import handle_html
from six import iteritems
import json
def execute(filters=None): def execute(filters=None):
return _execute(filters) return _execute(filters)
@ -21,21 +23,24 @@ def _execute(filters=None):
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency) itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
data = [] data = []
added_item = []
for d in item_list: for d in item_list:
row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty] if (d.parent, d.item_code) not in added_item:
total_tax = 0 row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty]
for tax in tax_columns: total_tax = 0
item_tax = itemised_tax.get(d.name, {}).get(tax, {}) for tax in tax_columns:
total_tax += flt(item_tax.get("tax_amount")) item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
total_tax += flt(item_tax.get("tax_amount", 0))
row += [d.base_net_amount + total_tax] row += [d.base_net_amount + total_tax]
row += [d.base_net_amount] row += [d.base_net_amount]
for tax in tax_columns: for tax in tax_columns:
item_tax = itemised_tax.get(d.name, {}).get(tax, {}) item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
row += [item_tax.get("tax_amount", 0)] row += [item_tax.get("tax_amount", 0)]
data.append(row) data.append(row)
added_item.append((d.parent, d.item_code))
if data: if data:
data = get_merged_data(columns, data) # merge same hsn code data data = get_merged_data(columns, data) # merge same hsn code data
return columns, data return columns, data
@ -103,7 +108,7 @@ def get_items(filters):
match_conditions = " and {0} ".format(match_conditions) match_conditions = " and {0} ".format(match_conditions)
return frappe.db.sql(""" items = frappe.db.sql("""
select select
`tabSales Invoice Item`.name, `tabSales Invoice Item`.base_price_list_rate, `tabSales Invoice Item`.name, `tabSales Invoice Item`.base_price_list_rate,
`tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.stock_qty,
@ -118,10 +123,9 @@ def get_items(filters):
""" % (conditions, match_conditions), filters, as_dict=1) """ % (conditions, match_conditions), filters, as_dict=1)
return items
def get_tax_accounts(item_list, columns, company_currency, def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoice", tax_doctype="Sales Taxes and Charges"):
doctype="Sales Invoice", tax_doctype="Sales Taxes and Charges"):
import json
item_row_map = {} item_row_map = {}
tax_columns = [] tax_columns = []
invoice_item_row = {} invoice_item_row = {}
@ -171,7 +175,7 @@ def get_tax_accounts(item_list, columns, company_currency,
for d in item_row_map.get(parent, {}).get(item_code, []): for d in item_row_map.get(parent, {}).get(item_code, []):
item_tax_amount = tax_amount item_tax_amount = tax_amount
if item_tax_amount: if item_tax_amount:
itemised_tax.setdefault(d.name, {})[description] = frappe._dict({ itemised_tax.setdefault((parent, item_code), {})[description] = frappe._dict({
"tax_amount": flt(item_tax_amount, tax_amount_precision) "tax_amount": flt(item_tax_amount, tax_amount_precision)
}) })
except ValueError: except ValueError:
@ -179,42 +183,32 @@ def get_tax_accounts(item_list, columns, company_currency,
tax_columns.sort() tax_columns.sort()
for desc in tax_columns: for desc in tax_columns:
columns.append(desc + " Amount:Currency/currency:160") columns.append({
"label": desc,
"fieldname": frappe.scrub(desc),
"fieldtype": "Float",
"width": 110
})
# columns += ["Total Amount:Currency/currency:110"]
return itemised_tax, tax_columns return itemised_tax, tax_columns
def get_merged_data(columns, data): def get_merged_data(columns, data):
merged_hsn_dict = {} # to group same hsn under one key and perform row addition merged_hsn_dict = {} # to group same hsn under one key and perform row addition
add_column_index = [] # store index of columns that needs to be added result = []
tax_col = len(get_columns())
fields_to_merge = ["stock_qty", "total_amount", "taxable_amount"] # columns for which index needs to be found
for i,d in enumerate(columns):
# check if fieldname in to_merge list and ignore tax-columns
if i < tax_col and d["fieldname"] in fields_to_merge:
add_column_index.append(i)
for row in data: for row in data:
if row[0] in merged_hsn_dict: merged_hsn_dict.setdefault(row[0], {})
to_add_row = merged_hsn_dict.get(row[0]) for i, d in enumerate(columns):
if d['fieldtype'] not in ('Int', 'Float', 'Currency'):
merged_hsn_dict[row[0]][d['fieldname']] = row[i]
else:
if merged_hsn_dict.get(row[0], {}).get(d['fieldname'], ''):
merged_hsn_dict[row[0]][d['fieldname']] += row[i]
else:
merged_hsn_dict[row[0]][d['fieldname']] = row[i]
# add columns from the add_column_index table for key, value in iteritems(merged_hsn_dict):
for k in add_column_index: result.append(value)
to_add_row[k] += row[k]
# add tax columns return result
for k in range(len(columns)):
if tax_col <= k < len(columns):
to_add_row[k] += row[k]
# update hsn dict with the newly added data
merged_hsn_dict[row[0]] = to_add_row
else:
merged_hsn_dict[row[0]] = row
# extract data rows to be displayed in report
data = [merged_hsn_dict[d] for d in merged_hsn_dict]
return data

View File

@ -3,7 +3,7 @@
{ {
"hidden": 0, "hidden": 0,
"label": "Retail Operations", "label": "Retail Operations",
"links": "[\n {\n \"description\": \"Setup default values for POS Invoices\",\n \"label\": \"Point-of-Sale Profile\",\n \"name\": \"POS Profile\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"POS Profile\"\n ],\n \"description\": \"Point of Sale\",\n \"label\": \"POS\",\n \"name\": \"pos\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"description\": \"Cashier Closing\",\n \"label\": \"Cashier Closing\",\n \"name\": \"Cashier Closing\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup mode of POS (Online / Offline)\",\n \"label\": \"POS Settings\",\n \"name\": \"POS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To make Customer based incentive schemes.\",\n \"label\": \"Loyalty Program\",\n \"name\": \"Loyalty Program\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To view logs of Loyalty Points assigned to a Customer.\",\n \"label\": \"Loyalty Point Entry\",\n \"name\": \"Loyalty Point Entry\",\n \"type\": \"doctype\"\n }\n]" "links": "[\n {\n \"description\": \"Setup default values for POS Invoices\",\n \"label\": \"Point of Sale Profile\",\n \"name\": \"POS Profile\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"POS Profile\"\n ],\n \"description\": \"Point of Sale\",\n \"label\": \"Point of Sale\",\n \"name\": \"point-of-sale\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"description\": \"Setup mode of POS (Online / Offline)\",\n \"label\": \"POS Settings\",\n \"name\": \"POS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Cashier Closing\",\n \"label\": \"Cashier Closing\",\n \"name\": \"Cashier Closing\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To make Customer based incentive schemes.\",\n \"label\": \"Loyalty Program\",\n \"name\": \"Loyalty Program\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To view logs of Loyalty Points assigned to a Customer.\",\n \"label\": \"Loyalty Point Entry\",\n \"name\": \"Loyalty Point Entry\",\n \"type\": \"doctype\"\n }\n]"
} }
], ],
"category": "Domains", "category": "Domains",
@ -14,10 +14,11 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Desk Page", "doctype": "Desk Page",
"extends_another_page": 0, "extends_another_page": 0,
"hide_custom": 0,
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"label": "Retail", "label": "Retail",
"modified": "2020-04-26 22:42:39.346750", "modified": "2020-08-20 18:00:07.515691",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Retail", "name": "Retail",
@ -25,5 +26,27 @@
"pin_to_bottom": 0, "pin_to_bottom": 0,
"pin_to_top": 0, "pin_to_top": 0,
"restrict_to_domain": "Retail", "restrict_to_domain": "Retail",
"shortcuts": [] "shortcuts": [
{
"color": "#9deca2",
"doc_view": "",
"format": "{} Active",
"label": "Point of Sale Profile",
"link_to": "POS Profile",
"stats_filter": "{\n \"disabled\": 0\n}",
"type": "DocType"
},
{
"doc_view": "",
"label": "Point of Sale",
"link_to": "point-of-sale",
"type": "Page"
},
{
"doc_view": "",
"label": "POS Settings",
"link_to": "POS Settings",
"type": "DocType"
}
]
} }

View File

@ -33,7 +33,7 @@ def get_data():
}, },
{ {
'label': _('Support'), 'label': _('Support'),
'items': ['Issue'] 'items': ['Issue', 'Maintenance Visit', 'Installation Note', 'Warranty Claim']
}, },
{ {
'label': _('Projects'), 'label': _('Projects'),

View File

@ -285,9 +285,17 @@ def _make_customer(source_name, ignore_permissions=False):
return customer return customer
else: else:
raise raise
except frappe.MandatoryError: except frappe.MandatoryError as e:
mandatory_fields = e.args[0].split(':')[1].split(',')
mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields]
frappe.local.message_log = [] frappe.local.message_log = []
frappe.throw(_("Please create Customer from Lead {0}").format(lead_name)) lead_link = frappe.utils.get_link_to_form("Lead", lead_name)
message = _("Could not auto create Customer due to the following missing mandatory field(s):") + "<br>"
message += "<br><ul><li>" + "</li><li>".join(mandatory_fields) + "</li></ul>"
message += _("Please create Customer from Lead {0}.").format(lead_link)
frappe.throw(message, title=_("Mandatory Missing"))
else: else:
return customer_name return customer_name
else: else:

View File

@ -10,6 +10,7 @@ def get_data():
'Payment Entry': 'reference_name', 'Payment Entry': 'reference_name',
'Payment Request': 'reference_name', 'Payment Request': 'reference_name',
'Auto Repeat': 'reference_document', 'Auto Repeat': 'reference_document',
'Maintenance Visit': 'prevdoc_docname'
}, },
'internal_links': { 'internal_links': {
'Quotation': ['items', 'prevdoc_docname'] 'Quotation': ['items', 'prevdoc_docname']
@ -17,7 +18,7 @@ def get_data():
'transactions': [ 'transactions': [
{ {
'label': _('Fulfillment'), 'label': _('Fulfillment'),
'items': ['Sales Invoice', 'Pick List', 'Delivery Note'] 'items': ['Sales Invoice', 'Pick List', 'Delivery Note', 'Maintenance Visit']
}, },
{ {
'label': _('Purchasing'), 'label': _('Purchasing'),

View File

@ -356,7 +356,7 @@ erpnext.PointOfSale.ItemCart = class {
onchange: function() { onchange: function() {
if (this.value || this.value == 0) { if (this.value || this.value == 0) {
const frm = me.events.get_frm(); const frm = me.events.get_frm();
frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', this.value); frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', flt(this.value));
me.hide_discount_control(this.value); me.hide_discount_control(this.value);
} }
}, },
@ -948,4 +948,4 @@ erpnext.PointOfSale.ItemCart = class {
show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none'); show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
} }
} }

View File

@ -86,7 +86,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
this.$summary_container.append( this.$summary_container.append(
`<div class="summary-btns flex summary-btns justify-between w-full f-shrink-0"></div>` `<div class="summary-btns flex summary-btns justify-between w-full f-shrink-0"></div>`
) )
this.$summary_btns = this.$summary_container.find('.summary-btns'); this.$summary_btns = this.$summary_container.find('.summary-btns');
} }
@ -110,7 +110,10 @@ erpnext.PointOfSale.PastOrderSummary = class {
{fieldname:'print', fieldtype:'Data', label:'Print Preview'} {fieldname:'print', fieldtype:'Data', label:'Print Preview'}
], ],
primary_action: () => { primary_action: () => {
this.events.get_frm().print_preview.printit(true); const frm = this.events.get_frm();
frm.doc = this.doc;
frm.print_preview.lang_code = frm.doc.language;
frm.print_preview.printit(true);
}, },
primary_action_label: __('Print'), primary_action_label: __('Print'),
}); });
@ -174,7 +177,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
<div class="flex"> <div class="flex">
<div class="text-md-0 text-dark-grey text-bold w-fit">Tax Charges</div> <div class="text-md-0 text-dark-grey text-bold w-fit">Tax Charges</div>
<div class="flex ml-6 text-dark-grey"> <div class="flex ml-6 text-dark-grey">
${ ${
doc.taxes.map((t, i) => { doc.taxes.map((t, i) => {
let margin_left = ''; let margin_left = '';
if (i !== 0) margin_left = 'ml-2'; if (i !== 0) margin_left = 'ml-2';
@ -271,6 +274,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
// this.print_dialog.show(); // this.print_dialog.show();
const frm = this.events.get_frm(); const frm = this.events.get_frm();
frm.doc = this.doc; frm.doc = this.doc;
frm.print_preview.lang_code = frm.doc.language;
frm.print_preview.printit(true); frm.print_preview.printit(true);
}); });
} }
@ -284,9 +288,9 @@ erpnext.PointOfSale.PastOrderSummary = class {
this.$summary_container.find('.print-btn').click(); this.$summary_container.find('.print-btn').click();
}); });
} }
toggle_component(show) { toggle_component(show) {
show ? show ?
this.$component.removeClass('d-none') : this.$component.removeClass('d-none') :
this.$component.addClass('d-none'); this.$component.addClass('d-none');
} }
@ -372,9 +376,9 @@ erpnext.PointOfSale.PastOrderSummary = class {
} }
get_condition_btn_map(after_submission) { get_condition_btn_map(after_submission) {
if (after_submission) if (after_submission)
return [{ condition: true, visible_btns: ['Print Receipt', 'Email Receipt', 'New Order'] }]; return [{ condition: true, visible_btns: ['Print Receipt', 'Email Receipt', 'New Order'] }];
return [ return [
{ condition: this.doc.docstatus === 0, visible_btns: ['Edit Order'] }, { condition: this.doc.docstatus === 0, visible_btns: ['Edit Order'] },
{ condition: !this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt', 'Return']}, { condition: !this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt', 'Return']},
@ -384,7 +388,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
load_summary_of(doc, after_submission=false) { load_summary_of(doc, after_submission=false) {
this.$summary_wrapper.removeClass("d-none"); this.$summary_wrapper.removeClass("d-none");
after_submission ? after_submission ?
this.switch_to_post_submit_summary() : this.switch_to_recent_invoice_summary(); this.switch_to_post_submit_summary() : this.switch_to_recent_invoice_summary();

View File

@ -26,7 +26,8 @@ def delete_company_transactions(company_name):
tabDocField where fieldtype='Link' and options='Company'"""): tabDocField where fieldtype='Link' and options='Company'"""):
if doctype not in ("Account", "Cost Center", "Warehouse", "Budget", if doctype not in ("Account", "Cost Center", "Warehouse", "Budget",
"Party Account", "Employee", "Sales Taxes and Charges Template", "Party Account", "Employee", "Sales Taxes and Charges Template",
"Purchase Taxes and Charges Template", "POS Profile", 'BOM'): "Purchase Taxes and Charges Template", "POS Profile", "BOM",
"Company", "Bank Account"):
delete_for_doctype(doctype, company_name) delete_for_doctype(doctype, company_name)
# reset company values # reset company values

View File

@ -7,6 +7,7 @@ import frappe
from erpnext.accounts.doctype.cash_flow_mapper.default_cash_flow_mapper import DEFAULT_MAPPERS from erpnext.accounts.doctype.cash_flow_mapper.default_cash_flow_mapper import DEFAULT_MAPPERS
from .default_success_action import get_default_success_action from .default_success_action import get_default_success_action
from frappe import _ from frappe import _
from frappe.utils import cint
from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to
from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from erpnext.setup.default_energy_point_rules import get_default_energy_point_rules from erpnext.setup.default_energy_point_rules import get_default_energy_point_rules
@ -25,12 +26,13 @@ def after_install():
create_default_success_action() create_default_success_action()
create_default_energy_point_rules() create_default_energy_point_rules()
add_company_to_session_defaults() add_company_to_session_defaults()
add_standard_navbar_items()
frappe.db.commit() frappe.db.commit()
def check_setup_wizard_not_completed(): def check_setup_wizard_not_completed():
if frappe.db.get_default('desktop:home_page') != 'setup-wizard': if cint(frappe.db.get_single_value('System Settings', 'setup_complete') or 0):
message = """ERPNext can only be installed on a fresh site where the setup wizard is not completed. message = """ERPNext can only be installed on a fresh site where the setup wizard is not completed.
You can reinstall this site (after saving your data) using: bench --site [sitename] reinstall""" You can reinstall this site (after saving your data) using: bench --site [sitename] reinstall"""
frappe.throw(message) frappe.throw(message)
@ -103,3 +105,45 @@ def add_company_to_session_defaults():
"ref_doctype": "Company" "ref_doctype": "Company"
}) })
settings.save() settings.save()
def add_standard_navbar_items():
navbar_settings = frappe.get_single("Navbar Settings")
erpnext_navbar_items = [
{
'item_label': 'Documentation',
'item_type': 'Route',
'route': 'https://erpnext.com/docs/user/manual',
'is_standard': 1
},
{
'item_label': 'User Forum',
'item_type': 'Route',
'route': 'https://discuss.erpnext.com',
'is_standard': 1
},
{
'item_label': 'Report an Issue',
'item_type': 'Route',
'route': 'https://github.com/frappe/erpnext/issues',
'is_standard': 1
}
]
current_nabvar_items = navbar_settings.help_dropdown
navbar_settings.set('help_dropdown', [])
for item in erpnext_navbar_items:
navbar_settings.append('help_dropdown', item)
for item in current_nabvar_items:
navbar_settings.append('help_dropdown', {
'item_label': item.item_label,
'item_type': item.item_type,
'route': item.route,
'action': item.action,
'is_standard': item.is_standard,
'hidden': item.hidden
})
navbar_settings.save()

View File

@ -111,6 +111,7 @@ class Item(WebsiteGenerator):
self.synced_with_hub = 0 self.synced_with_hub = 0
self.validate_has_variants() self.validate_has_variants()
self.validate_attributes_in_variants()
self.validate_stock_exists_for_template_item() self.validate_stock_exists_for_template_item()
self.validate_attributes() self.validate_attributes()
self.validate_variant_attributes() self.validate_variant_attributes()
@ -806,6 +807,77 @@ class Item(WebsiteGenerator):
if frappe.db.exists("Item", {"variant_of": self.name}): if frappe.db.exists("Item", {"variant_of": self.name}):
frappe.throw(_("Item has variants.")) frappe.throw(_("Item has variants."))
def validate_attributes_in_variants(self):
if not self.has_variants or self.get("__islocal"):
return
old_doc = self.get_doc_before_save()
old_doc_attributes = set([attr.attribute for attr in old_doc.attributes])
own_attributes = [attr.attribute for attr in self.attributes]
# Check if old attributes were removed from the list
# Is old_attrs is a subset of new ones
# that means we need not check any changes
if old_doc_attributes.issubset(set(own_attributes)):
return
from collections import defaultdict
# get all item variants
items = [item["name"] for item in frappe.get_all("Item", {"variant_of": self.name})]
# get all deleted attributes
deleted_attribute = list(old_doc_attributes.difference(set(own_attributes)))
# fetch all attributes of these items
item_attributes = frappe.get_all(
"Item Variant Attribute",
filters={
"parent": ["in", items],
"attribute": ["in", deleted_attribute]
},
fields=["attribute", "parent"]
)
not_included = defaultdict(list)
for attr in item_attributes:
if attr["attribute"] not in own_attributes:
not_included[attr["parent"]].append(attr["attribute"])
if not len(not_included):
return
def body(docnames):
docnames.sort()
return "<br>".join(docnames)
def table_row(title, body):
return """<tr>
<td>{0}</td>
<td>{1}</td>
</tr>""".format(title, body)
rows = ''
for docname, attr_list in not_included.items():
link = "<a href='#Form/Item/{0}'>{0}</a>".format(frappe.bold(_(docname)))
rows += table_row(link, body(attr_list))
error_description = _('The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template.')
message = """
<div>{0}</div><br>
<table class="table">
<thead>
<td>{1}</td>
<td>{2}</td>
</thead>
{3}
</table>
""".format(error_description, _('Variant Items'), _('Attributes'), rows)
frappe.throw(message, title=_("Variant Attribute Error"), is_minimizable=True, wide=True)
def validate_stock_exists_for_template_item(self): def validate_stock_exists_for_template_item(self):
if self.stock_ledger_created() and self._doc_before_save: if self.stock_ledger_created() and self._doc_before_save:
if (cint(self._doc_before_save.has_variants) != cint(self.has_variants) if (cint(self._doc_before_save.has_variants) != cint(self.has_variants)

View File

@ -227,6 +227,14 @@ class PurchaseReceipt(BuyingController):
if not stock_value_diff: if not stock_value_diff:
continue continue
# If PR is sub-contracted and fg item rate is zero
# in that case if account for shource and target warehouse are same,
# then GL entries should not be posted
if flt(stock_value_diff) == flt(d.rm_supp_cost) \
and warehouse_account.get(self.supplier_warehouse) \
and warehouse_account[d.warehouse]["account"] == warehouse_account[self.supplier_warehouse]["account"]:
continue
gl_entries.append(self.get_gl_dict({ gl_entries.append(self.get_gl_dict({
"account": warehouse_account[d.warehouse]["account"], "account": warehouse_account[d.warehouse]["account"],
"against": stock_rbnb, "against": stock_rbnb,
@ -242,16 +250,16 @@ class PurchaseReceipt(BuyingController):
credit_amount = flt(d.base_net_amount, d.precision("base_net_amount")) \ credit_amount = flt(d.base_net_amount, d.precision("base_net_amount")) \
if credit_currency == self.company_currency else flt(d.net_amount, d.precision("net_amount")) if credit_currency == self.company_currency else flt(d.net_amount, d.precision("net_amount"))
if credit_amount:
gl_entries.append(self.get_gl_dict({ gl_entries.append(self.get_gl_dict({
"account": warehouse_account[d.from_warehouse]['account'] \ "account": warehouse_account[d.from_warehouse]['account'] \
if d.from_warehouse else stock_rbnb, if d.from_warehouse else stock_rbnb,
"against": warehouse_account[d.warehouse]["account"], "against": warehouse_account[d.warehouse]["account"],
"cost_center": d.cost_center, "cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": -1 * flt(d.base_net_amount, d.precision("base_net_amount")), "debit": -1 * flt(d.base_net_amount, d.precision("base_net_amount")),
"debit_in_account_currency": -1 * credit_amount "debit_in_account_currency": -1 * credit_amount
}, credit_currency, item=d)) }, credit_currency, item=d))
negative_expense_to_be_booked += flt(d.item_tax_amount) negative_expense_to_be_booked += flt(d.item_tax_amount)

View File

@ -3,6 +3,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest import unittest
import json
import frappe, erpnext import frappe, erpnext
import frappe.defaults import frappe.defaults
from frappe.utils import cint, flt, cstr, today, random_string from frappe.utils import cint, flt, cstr, today, random_string
@ -18,6 +19,28 @@ class TestPurchaseReceipt(unittest.TestCase):
set_perpetual_inventory(0) set_perpetual_inventory(0)
frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1)
def test_reverse_purchase_receipt_sle(self):
frappe.db.set_value('UOM', '_Test UOM', 'must_be_whole_number', 0)
pr = make_purchase_receipt(qty=0.5)
sl_entry = frappe.db.get_all("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
"voucher_no": pr.name}, ['actual_qty'])
self.assertEqual(len(sl_entry), 1)
self.assertEqual(sl_entry[0].actual_qty, 0.5)
pr.cancel()
sl_entry_cancelled = frappe.db.get_all("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
"voucher_no": pr.name}, ['actual_qty'], order_by='creation')
self.assertEqual(len(sl_entry_cancelled), 2)
self.assertEqual(sl_entry_cancelled[1].actual_qty, -0.5)
frappe.db.set_value('UOM', '_Test UOM', 'must_be_whole_number', 1)
def test_make_purchase_invoice(self): def test_make_purchase_invoice(self):
pr = make_purchase_receipt(do_not_save=True) pr = make_purchase_receipt(do_not_save=True)
self.assertRaises(frappe.ValidationError, make_purchase_invoice, pr.name) self.assertRaises(frappe.ValidationError, make_purchase_invoice, pr.name)
@ -121,6 +144,87 @@ class TestPurchaseReceipt(unittest.TestCase):
rm_supp_cost = sum([d.amount for d in pr.get("supplied_items")]) rm_supp_cost = sum([d.amount for d in pr.get("supplied_items")])
self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2)) self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2))
def test_subcontracting_gle_fg_item_rate_zero(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
set_perpetual_inventory()
frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM")
make_stock_entry(item_code="_Test Item", target="Work In Progress - TCP1", qty=100, basic_rate=100, company="_Test Company with perpetual inventory")
make_stock_entry(item_code="_Test Item Home Desktop 100", target="Work In Progress - TCP1",
qty=100, basic_rate=100, company="_Test Company with perpetual inventory")
pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=0, is_subcontracted="Yes",
company="_Test Company with perpetual inventory", warehouse='Stores - TCP1', supplier_warehouse='Work In Progress - TCP1')
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
self.assertFalse(gl_entries)
set_perpetual_inventory(0)
def test_subcontracting_over_receipt(self):
"""
Behaviour: Raise multiple PRs against one PO that in total
receive more than the required qty in the PO.
Expected Result: Error Raised for Over Receipt against PO.
"""
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.buying.doctype.purchase_order.test_purchase_order import (update_backflush_based_on,
make_subcontracted_item, create_purchase_order)
from erpnext.buying.doctype.purchase_order.purchase_order import (make_purchase_receipt,
make_rm_stock_entry as make_subcontract_transfer_entry)
update_backflush_based_on("Material Transferred for Subcontract")
item_code = "_Test Subcontracted FG Item 1"
make_subcontracted_item(item_code)
po = create_purchase_order(item_code=item_code, qty=1,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
#stock raw materials in a warehouse before transfer
make_stock_entry(target="_Test Warehouse - _TC",
item_code="_Test Item Home Desktop 100", qty=1, basic_rate=100)
make_stock_entry(target="_Test Warehouse - _TC",
item_code = "Test Extra Item 1", qty=1, basic_rate=100)
make_stock_entry(target="_Test Warehouse - _TC",
item_code = "_Test Item", qty=1, basic_rate=100)
rm_items = [
{
"item_code": item_code,
"rm_item_code": po.supplied_items[0].rm_item_code,
"item_name": "_Test Item",
"qty": po.supplied_items[0].required_qty,
"warehouse": "_Test Warehouse - _TC",
"stock_uom": "Nos"
},
{
"item_code": item_code,
"rm_item_code": po.supplied_items[1].rm_item_code,
"item_name": "Test Extra Item 1",
"qty": po.supplied_items[1].required_qty,
"warehouse": "_Test Warehouse - _TC",
"stock_uom": "Nos"
},
{
"item_code": item_code,
"rm_item_code": po.supplied_items[2].rm_item_code,
"item_name": "_Test Item Home Desktop 100",
"qty": po.supplied_items[2].required_qty,
"warehouse": "_Test Warehouse - _TC",
"stock_uom": "Nos"
}
]
rm_item_string = json.dumps(rm_items)
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
se.to_warehouse = "_Test Warehouse 1 - _TC"
se.save()
se.submit()
pr1 = make_purchase_receipt(po.name)
pr2 = make_purchase_receipt(po.name)
pr1.submit()
self.assertRaises(frappe.ValidationError, pr2.submit)
def test_serial_no_supplier(self): def test_serial_no_supplier(self):
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1)
self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"), self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"),
@ -688,7 +792,7 @@ def make_purchase_receipt(**args):
"received_qty": received_qty, "received_qty": received_qty,
"rejected_qty": rejected_qty, "rejected_qty": rejected_qty,
"rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" if rejected_qty != 0 else "", "rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" if rejected_qty != 0 else "",
"rate": args.rate or 50, "rate": args.rate if args.rate != None else 50,
"conversion_factor": args.conversion_factor or 1.0, "conversion_factor": args.conversion_factor or 1.0,
"serial_no": args.serial_no, "serial_no": args.serial_no,
"stock_uom": args.stock_uom or "_Test UOM", "stock_uom": args.stock_uom or "_Test UOM",

View File

@ -31,7 +31,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
sle['posting_time'] = now_datetime().strftime('%H:%M:%S.%f') sle['posting_time'] = now_datetime().strftime('%H:%M:%S.%f')
if cancel: if cancel:
sle['actual_qty'] = -flt(sle.get('actual_qty'), 0) sle['actual_qty'] = -flt(sle.get('actual_qty'))
if sle['actual_qty'] < 0 and not sle.get('outgoing_rate'): if sle['actual_qty'] < 0 and not sle.get('outgoing_rate'):
sle['outgoing_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code, sle['outgoing_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code,

View File

@ -209,11 +209,11 @@ function set_time_to_resolve_and_response(frm) {
frm.dashboard.set_headline_alert( frm.dashboard.set_headline_alert(
'<div class="row">' + '<div class="row">' +
'<div class="col-xs-6">' + '<div class="col-xs-12 col-sm-6">' +
'<span class="indicator whitespace-nowrap '+ time_to_respond.indicator +'"><span class="hidden-xs">Time to Respond: '+ time_to_respond.diff_display +'</span></span> ' + '<span class="indicator whitespace-nowrap '+ time_to_respond.indicator +'"><span>Time to Respond: '+ time_to_respond.diff_display +'</span></span> ' +
'</div>' + '</div>' +
'<div class="col-xs-6">' + '<div class="col-xs-12 col-sm-6">' +
'<span class="indicator whitespace-nowrap '+ time_to_resolve.indicator +'"><span class="hidden-xs">Time to Resolve: '+ time_to_resolve.diff_display +'</span></span> ' + '<span class="indicator whitespace-nowrap '+ time_to_resolve.indicator +'"><span>Time to Resolve: '+ time_to_resolve.diff_display +'</span></span> ' +
'</div>' + '</div>' +
'</div>' '</div>'
); );

View File

@ -20,6 +20,13 @@ frappe.ready(() => {
options: 'Email', options: 'Email',
reqd: 1 reqd: 1
}, },
{
fieldtype: 'Data',
label: __('Phone Number'),
fieldname: 'phone',
options: 'Phone',
reqd: 1
},
{ {
fieldtype: 'Data', fieldtype: 'Data',
label: __('Subject'), label: __('Subject'),