diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index 3f7d4bb741..78c2f5a187 100644
--- a/.github/workflows/ci-tests.yml
+++ b/.github/workflows/ci-tests.yml
@@ -4,7 +4,7 @@ on: [pull_request, workflow_dispatch, push]
jobs:
test:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-18.04
strategy:
fail-fast: false
diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index c801cfcbba..0606823821 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -214,6 +214,7 @@ class Account(NestedSet):
if parent_value_changed:
doc.save()
+ @frappe.whitelist()
def convert_group_to_ledger(self):
if self.check_if_child_exists():
throw(_("Account with child nodes cannot be converted to ledger"))
@@ -224,6 +225,7 @@ class Account(NestedSet):
self.save()
return 1
+ @frappe.whitelist()
def convert_ledger_to_group(self):
if self.check_gle_exists():
throw(_("Account with existing transaction can not be converted to group."))
diff --git a/erpnext/accounts/doctype/accounting_period/accounting_period.py b/erpnext/accounts/doctype/accounting_period/accounting_period.py
index df6cedd7cf..63b5dbbd3e 100644
--- a/erpnext/accounts/doctype/accounting_period/accounting_period.py
+++ b/erpnext/accounts/doctype/accounting_period/accounting_period.py
@@ -39,6 +39,7 @@ class AccountingPeriod(Document):
frappe.throw(_("Accounting Period overlaps with {0}")
.format(existing_accounting_period[0].get("name")), OverlapError)
+ @frappe.whitelist()
def get_doctypes_for_closing(self):
docs_for_closing = []
doctypes = ["Sales Invoice", "Purchase Invoice", "Journal Entry", "Payroll Entry", \
diff --git a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py
index 022d7a7e80..10cd939894 100644
--- a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py
+++ b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py
@@ -11,36 +11,36 @@ from erpnext.accounts.doctype.accounting_period.accounting_period import Overlap
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
class TestAccountingPeriod(unittest.TestCase):
- def test_overlap(self):
- ap1 = create_accounting_period(start_date = "2018-04-01",
- end_date = "2018-06-30", company = "Wind Power LLC")
- ap1.save()
+ def test_overlap(self):
+ ap1 = create_accounting_period(start_date = "2018-04-01",
+ end_date = "2018-06-30", company = "Wind Power LLC")
+ ap1.save()
- ap2 = create_accounting_period(start_date = "2018-06-30",
- end_date = "2018-07-10", company = "Wind Power LLC", period_name = "Test Accounting Period 1")
- self.assertRaises(OverlapError, ap2.save)
+ ap2 = create_accounting_period(start_date = "2018-06-30",
+ end_date = "2018-07-10", company = "Wind Power LLC", period_name = "Test Accounting Period 1")
+ self.assertRaises(OverlapError, ap2.save)
- def test_accounting_period(self):
- ap1 = create_accounting_period(period_name = "Test Accounting Period 2")
- ap1.save()
+ def test_accounting_period(self):
+ ap1 = create_accounting_period(period_name = "Test Accounting Period 2")
+ ap1.save()
- doc = create_sales_invoice(do_not_submit=1, cost_center = "_Test Company - _TC", warehouse = "Stores - _TC")
- self.assertRaises(ClosedAccountingPeriod, doc.submit)
+ doc = create_sales_invoice(do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC")
+ self.assertRaises(ClosedAccountingPeriod, doc.submit)
- def tearDown(self):
- for d in frappe.get_all("Accounting Period"):
- frappe.delete_doc("Accounting Period", d.name)
+ def tearDown(self):
+ for d in frappe.get_all("Accounting Period"):
+ frappe.delete_doc("Accounting Period", d.name)
def create_accounting_period(**args):
- args = frappe._dict(args)
+ args = frappe._dict(args)
- accounting_period = frappe.new_doc("Accounting Period")
- accounting_period.start_date = args.start_date or nowdate()
- accounting_period.end_date = args.end_date or add_months(nowdate(), 1)
- accounting_period.company = args.company or "_Test Company"
- accounting_period.period_name =args.period_name or "_Test_Period_Name_1"
- accounting_period.append("closed_documents", {
- "document_type": 'Sales Invoice', "closed": 1
- })
+ accounting_period = frappe.new_doc("Accounting Period")
+ accounting_period.start_date = args.start_date or nowdate()
+ accounting_period.end_date = args.end_date or add_months(nowdate(), 1)
+ accounting_period.company = args.company or "_Test Company"
+ accounting_period.period_name =args.period_name or "_Test_Period_Name_1"
+ accounting_period.append("closed_documents", {
+ "document_type": 'Sales Invoice', "closed": 1
+ })
- return accounting_period
\ No newline at end of file
+ return accounting_period
diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
index 76d82e7339..79f5596384 100644
--- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
+++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
@@ -12,6 +12,7 @@ form_grid_templates = {
}
class BankClearance(Document):
+ @frappe.whitelist()
def get_payment_entries(self):
if not (self.from_date and self.to_date):
frappe.throw(_("From Date and To Date are Mandatory"))
@@ -108,6 +109,7 @@ class BankClearance(Document):
row.update(d)
self.total_amount += flt(amount)
+ @frappe.whitelist()
def update_clearance_date(self):
clearance_date_updated = False
for d in self.get('payment_entries'):
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
index ad4ff9ee60..3dbd605344 100644
--- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
@@ -532,43 +532,4 @@ frappe.ui.form.on("Bank Statement Import", {
`);
},
-
- show_missing_link_values(frm, missing_link_values) {
- let can_be_created_automatically = missing_link_values.every(
- (d) => d.has_one_mandatory_field
- );
-
- let html = missing_link_values
- .map((d) => {
- let doctype = d.doctype;
- let values = d.missing_values;
- return `
-
- {{__("Suppliies made to Composition Taxable Persons")}} |
+ {{__("Supplies made to Composition Taxable Persons")}} |
{% for row in data.inter_sup.comp_details %}
{% if row %}
diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
index 68c8a0d4d3..a5dd5a2e09 100644
--- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
@@ -172,7 +172,6 @@ class GSTR3BReport(Document):
self.json_output = frappe.as_json(self.report_dict)
def set_inward_nil_exempt(self, inward_nil_exempt):
-
self.report_dict["inward_sup"]["isup_details"][0]["inter"] = flt(inward_nil_exempt.get("gst").get("inter"), 2)
self.report_dict["inward_sup"]["isup_details"][0]["intra"] = flt(inward_nil_exempt.get("gst").get("intra"), 2)
self.report_dict["inward_sup"]["isup_details"][1]["inter"] = flt(inward_nil_exempt.get("non_gst").get("inter"), 2)
@@ -238,7 +237,6 @@ class GSTR3BReport(Document):
self.report_dict[supply_type][supply_category]["txval"] += flt(txval, 2)
def set_inter_state_supply(self, inter_state_supply):
-
osup_det = self.report_dict["sup_details"]["osup_det"]
for key, value in iteritems(inter_state_supply):
@@ -349,13 +347,20 @@ class GSTR3BReport(Document):
return inter_state_supply_details
def get_inward_nil_exempt(self, state):
-
inward_nil_exempt = frappe.db.sql(""" select p.place_of_supply, sum(i.base_amount) as base_amount,
i.is_nil_exempt, i.is_non_gst from `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
where p.docstatus = 1 and p.name = i.parent
- and i.is_nil_exempt = 1 or i.is_non_gst = 1 and
+ and p.gst_category != 'Registered Composition'
+ and (i.is_nil_exempt = 1 or i.is_non_gst = 1) and
month(p.posting_date) = %s and year(p.posting_date) = %s and p.company = %s and p.company_gstin = %s
- group by p.place_of_supply """, (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
+ group by p.place_of_supply, i.is_nil_exempt, i.is_non_gst""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
+
+ inward_nil_exempt += frappe.db.sql("""SELECT sum(base_net_total) as base_amount, gst_category, place_of_supply
+ FROM `tabPurchase Invoice`
+ WHERE docstatus = 1 and gst_category = 'Registered Composition'
+ and month(posting_date) = %s and year(posting_date) = %s
+ and company = %s and company_gstin = %s
+ group by place_of_supply""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
inward_nil_exempt_details = {
"gst": {
@@ -370,9 +375,11 @@ class GSTR3BReport(Document):
for d in inward_nil_exempt:
if d.place_of_supply:
- if d.is_nil_exempt == 1 and state == d.place_of_supply.split("-")[1]:
+ if (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \
+ and state == d.place_of_supply.split("-")[1]:
inward_nil_exempt_details["gst"]["intra"] += d.base_amount
- elif d.is_nil_exempt == 1 and state != d.place_of_supply.split("-")[1]:
+ elif (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \
+ and state != d.place_of_supply.split("-")[1]:
inward_nil_exempt_details["gst"]["inter"] += d.base_amount
elif d.is_non_gst == 1 and state == d.place_of_supply.split("-")[1]:
inward_nil_exempt_details["non_gst"]["intra"] += d.base_amount
diff --git a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
index 023b4ed22b..ef8af24c42 100644
--- a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
@@ -64,7 +64,7 @@ class TestGSTR3BReport(unittest.TestCase):
self.assertEqual(output["sup_details"]["osup_zero"]["iamt"], 18),
self.assertEqual(output["inter_sup"]["unreg_details"][0]["iamt"], 18),
self.assertEqual(output["sup_details"]["osup_nil_exmp"]["txval"], 100),
- self.assertEqual(output["inward_sup"]["isup_details"][0]["inter"], 250)
+ self.assertEqual(output["inward_sup"]["isup_details"][0]["intra"], 250)
self.assertEqual(output["itc_elg"]["itc_avl"][4]["samt"], 22.50)
self.assertEqual(output["itc_elg"]["itc_avl"][4]["camt"], 22.50)
@@ -228,6 +228,19 @@ def create_purchase_invoices():
pi1.submit()
+ pi2 = make_purchase_invoice(company="_Test Company GST",
+ customer = '_Test Registered Supplier',
+ currency = 'INR',
+ item = 'Milk',
+ warehouse = 'Finished Goods - _GST',
+ expense_account = 'Cost of Goods Sold - _GST',
+ cost_center = 'Main - _GST',
+ rate=250,
+ qty=1,
+ do_not_save=1
+ )
+ pi2.submit()
+
def make_suppliers():
if not frappe.db.exists("Supplier", "_Test Registered Supplier"):
frappe.get_doc({
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
index 5bbd5750f9..41a0f1193b 100644
--- a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
@@ -50,6 +50,7 @@ class TaxExemption80GCertificate(Document):
frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('PAN Number'),
get_link_to_form('Company', self.company)))
+ @frappe.whitelist()
def set_company_address(self):
address = get_company_address(self.company)
self.company_address = address.company_address
@@ -70,6 +71,7 @@ class TaxExemption80GCertificate(Document):
else:
self.title = self.donor_name
+ @frappe.whitelist()
def get_payments(self):
if not self.member:
frappe.throw(_('Please select a Member first.'))
@@ -81,7 +83,7 @@ class TaxExemption80GCertificate(Document):
'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
'to_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
'membership_status': ('!=', 'Cancelled')
- }, ['from_date', 'amount', 'name', 'invoice', 'payment_id'])
+ }, ['from_date', 'amount', 'name', 'invoice', 'payment_id'], order_by='from_date')
if not memberships:
frappe.msgprint(_('No Membership Payments found against the Member {0}').format(self.member))
diff --git a/erpnext/regional/india/e_invoice/einv_validation.json b/erpnext/regional/india/e_invoice/einv_validation.json
index 86290cfe52..f4a3542a60 100644
--- a/erpnext/regional/india/e_invoice/einv_validation.json
+++ b/erpnext/regional/india/e_invoice/einv_validation.json
@@ -919,7 +919,8 @@
"minLength": 1,
"maxLength": 15,
"pattern": "^([0-9A-Z/-]){1,15}$",
- "description": "Tranport Document Number"
+ "description": "Tranport Document Number",
+ "validationMsg": "Transport Receipt No is invalid"
},
"TransDocDt": {
"type": "string",
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index ee49aae050..f7689cfa19 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -5,6 +5,7 @@ from __future__ import unicode_literals
import frappe, os, json
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.permissions import add_permission, update_permission_property
from erpnext.regional.india import states
from erpnext.accounts.utils import get_fiscal_year, FiscalYearError
@@ -18,6 +19,7 @@ def setup(company=None, patch=True):
# TODO: for all countries
def setup_company_independent_fixtures():
make_custom_fields()
+ make_property_setters()
add_permissions()
add_custom_roles_for_reports()
frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test)
@@ -110,6 +112,11 @@ def add_print_formats():
frappe.db.set_value("Print Format", "GST Tax Invoice", "disabled", 0)
frappe.db.set_value("Print Format", "GST E-Invoice", "disabled", 0)
+def make_property_setters():
+ # GST rules do not allow for an invoice no. bigger than 16 characters
+ make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '')
+ make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '')
+
def make_custom_fields(update=True):
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
fieldtype='Data', fetch_from='item_code.gst_hsn_code', insert_after='description',
@@ -860,4 +867,4 @@ def create_gratuity_rule():
})
rule.flags.ignore_mandatory = True
- rule.save()
\ No newline at end of file
+ rule.save()
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index e24bd6c3d0..3637de438c 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -823,8 +823,11 @@ def get_regional_round_off_accounts(company, account_list):
return
gst_accounts = get_gst_accounts(company)
- gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
- + gst_accounts.get('igst_account')
+
+ gst_account_list = []
+ for account in ['cgst_account', 'sgst_account', 'igst_account']:
+ if account in gst_accounts:
+ gst_account_list += gst_accounts.get(account)
account_list.extend(gst_account_list)
diff --git a/erpnext/regional/italy/sales_invoice.js b/erpnext/regional/italy/sales_invoice.js
index 586a52937b..b54ac53812 100644
--- a/erpnext/regional/italy/sales_invoice.js
+++ b/erpnext/regional/italy/sales_invoice.js
@@ -11,15 +11,10 @@ erpnext.setup_e_invoice_button = (doctype) => {
callback: function(r) {
frm.reload_doc();
if(r.message) {
- var w = window.open(
- frappe.urllib.get_full_url(
- "/api/method/erpnext.regional.italy.utils.download_e_invoice_file?"
- + "file_name=" + r.message
- )
- )
- if (!w) {
- frappe.msgprint(__("Please enable pop-ups")); return;
- }
+ open_url_post(frappe.request.url, {
+ cmd: 'frappe.core.doctype.file.file.download_file',
+ file_url: r.message
+ });
}
}
});
diff --git a/erpnext/regional/italy/setup.py b/erpnext/regional/italy/setup.py
index 95b92e76a6..a1f5bb9836 100644
--- a/erpnext/regional/italy/setup.py
+++ b/erpnext/regional/italy/setup.py
@@ -128,11 +128,8 @@ def make_custom_fields(update=True):
fetch_from="company.vat_collectability"),
dict(fieldname='sb_e_invoicing_reference', label='E-Invoicing',
fieldtype='Section Break', insert_after='against_income_account', print_hide=1),
- dict(fieldname='company_tax_id', label='Company Tax ID',
- fieldtype='Data', insert_after='sb_e_invoicing_reference', print_hide=1, read_only=1,
- fetch_from="company.tax_id"),
dict(fieldname='company_fiscal_code', label='Company Fiscal Code',
- fieldtype='Data', insert_after='company_tax_id', print_hide=1, read_only=1,
+ fieldtype='Data', insert_after='sb_e_invoicing_reference', print_hide=1, read_only=1,
fetch_from="company.fiscal_code"),
dict(fieldname='company_fiscal_regime', label='Company Fiscal Regime',
fieldtype='Data', insert_after='company_fiscal_code', print_hide=1, read_only=1,
@@ -217,4 +214,4 @@ def add_permissions():
update_permission_property(doctype, 'Accounts Manager', 0, 'delete', 1)
add_permission(doctype, 'Accounts Manager', 1)
update_permission_property(doctype, 'Accounts Manager', 1, 'write', 1)
- update_permission_property(doctype, 'Accounts Manager', 1, 'create', 1)
\ No newline at end of file
+ update_permission_property(doctype, 'Accounts Manager', 1, 'create', 1)
diff --git a/erpnext/regional/italy/utils.py b/erpnext/regional/italy/utils.py
index 6842fb2a61..08573cddcd 100644
--- a/erpnext/regional/italy/utils.py
+++ b/erpnext/regional/italy/utils.py
@@ -1,6 +1,8 @@
from __future__ import unicode_literals
-import frappe, json, os
+import io
+import json
+import frappe
from frappe.utils import flt, cstr
from erpnext.controllers.taxes_and_totals import get_itemised_tax
from frappe import _
@@ -28,20 +30,22 @@ def update_itemised_tax_data(doc):
@frappe.whitelist()
def export_invoices(filters=None):
- saved_xmls = []
+ frappe.has_permission('Sales Invoice', throw=True)
- invoices = frappe.get_all("Sales Invoice", filters=get_conditions(filters), fields=["*"])
+ invoices = frappe.get_all(
+ "Sales Invoice",
+ filters=get_conditions(filters),
+ fields=["name", "company_tax_id"]
+ )
- for invoice in invoices:
- attachments = get_e_invoice_attachments(invoice)
- saved_xmls += [attachment.file_name for attachment in attachments]
+ attachments = get_e_invoice_attachments(invoices)
- zip_filename = "{0}-einvoices.zip".format(frappe.utils.get_datetime().strftime("%Y%m%d_%H%M%S"))
+ zip_filename = "{0}-einvoices.zip".format(
+ frappe.utils.get_datetime().strftime("%Y%m%d_%H%M%S"))
- download_zip(saved_xmls, zip_filename)
+ download_zip(attachments, zip_filename)
-@frappe.whitelist()
def prepare_invoice(invoice, progressive_number):
#set company information
company = frappe.get_doc("Company", invoice.company)
@@ -98,7 +102,7 @@ def prepare_invoice(invoice, progressive_number):
def get_conditions(filters):
filters = json.loads(filters)
- conditions = {"docstatus": 1}
+ conditions = {"docstatus": 1, "company_tax_id": ("!=", "")}
if filters.get("company"): conditions["company"] = filters["company"]
if filters.get("customer"): conditions["customer"] = filters["customer"]
@@ -111,23 +115,22 @@ def get_conditions(filters):
return conditions
-#TODO: Use function from frappe once PR #6853 is merged.
+
def download_zip(files, output_filename):
- from zipfile import ZipFile
+ import zipfile
- input_files = [frappe.get_site_path('private', 'files', filename) for filename in files]
- output_path = frappe.get_site_path('private', 'files', output_filename)
+ zip_stream = io.BytesIO()
+ with zipfile.ZipFile(zip_stream, 'w', zipfile.ZIP_DEFLATED) as zip_file:
+ for file in files:
+ file_path = frappe.utils.get_files_path(
+ file.file_name, is_private=file.is_private)
- with ZipFile(output_path, 'w') as output_zip:
- for input_file in input_files:
- output_zip.write(input_file, arcname=os.path.basename(input_file))
-
- with open(output_path, 'rb') as fileobj:
- filedata = fileobj.read()
+ zip_file.write(file_path, arcname=file.file_name)
frappe.local.response.filename = output_filename
- frappe.local.response.filecontent = filedata
+ frappe.local.response.filecontent = zip_stream.getvalue()
frappe.local.response.type = "download"
+ zip_stream.close()
def get_invoice_summary(items, taxes):
summary_data = frappe._dict()
@@ -307,23 +310,12 @@ def prepare_and_attach_invoice(doc, replace=False):
@frappe.whitelist()
def generate_single_invoice(docname):
doc = frappe.get_doc("Sales Invoice", docname)
-
+ frappe.has_permission("Sales Invoice", doc=doc, throw=True)
e_invoice = prepare_and_attach_invoice(doc, True)
+ return e_invoice.file_url
- return e_invoice.file_name
-
-@frappe.whitelist()
-def download_e_invoice_file(file_name):
- content = None
- with open(frappe.get_site_path('private', 'files', file_name), "r") as f:
- content = f.read()
-
- frappe.local.response.filename = file_name
- frappe.local.response.filecontent = content
- frappe.local.response.type = "download"
-
-#Delete e-invoice attachment on cancel.
+# Delete e-invoice attachment on cancel.
def sales_invoice_on_cancel(doc, method):
if get_company_country(doc.company) not in ['Italy',
'Italia', 'Italian Republic', 'Repubblica Italiana']:
@@ -335,16 +327,38 @@ def sales_invoice_on_cancel(doc, method):
def get_company_country(company):
return frappe.get_cached_value('Company', company, 'country')
-def get_e_invoice_attachments(invoice):
- if not invoice.company_tax_id:
- return []
+def get_e_invoice_attachments(invoices):
+ if not isinstance(invoices, list):
+ if not invoices.company_tax_id:
+ return
+
+ invoices = [invoices]
+
+ tax_id_map = {
+ invoice.name: (
+ invoice.company_tax_id
+ if invoice.company_tax_id.startswith("IT")
+ else "IT" + invoice.company_tax_id
+ ) for invoice in invoices
+ }
+
+ attachments = frappe.get_all(
+ "File",
+ fields=("name", "file_name", "attached_to_name", "is_private"),
+ filters= {
+ "attached_to_name": ('in', tax_id_map),
+ "attached_to_doctype": 'Sales Invoice'
+ }
+ )
out = []
- attachments = get_attachments(invoice.doctype, invoice.name)
- company_tax_id = invoice.company_tax_id if invoice.company_tax_id.startswith("IT") else "IT" + invoice.company_tax_id
-
for attachment in attachments:
- if attachment.file_name and attachment.file_name.startswith(company_tax_id) and attachment.file_name.endswith(".xml"):
+ if (
+ attachment.file_name
+ and attachment.file_name.endswith(".xml")
+ and attachment.file_name.startswith(
+ tax_id_map.get(attachment.attached_to_name))
+ ):
out.append(attachment)
return out
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index 09b04ff367..62faa30e3f 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -78,7 +78,7 @@ class Gstr1Report(object):
place_of_supply = invoice_details.get("place_of_supply")
ecommerce_gstin = invoice_details.get("ecommerce_gstin")
- b2cs_output.setdefault((rate, place_of_supply, ecommerce_gstin, inv),{
+ b2cs_output.setdefault((rate, place_of_supply, ecommerce_gstin),{
"place_of_supply": "",
"ecommerce_gstin": "",
"rate": "",
@@ -90,7 +90,7 @@ class Gstr1Report(object):
"invoice_value": invoice_details.get("base_grand_total"),
})
- row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin, inv))
+ row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin))
row["place_of_supply"] = place_of_supply
row["ecommerce_gstin"] = ecommerce_gstin
row["rate"] = rate
diff --git a/erpnext/regional/report/gstr_2/gstr_2.py b/erpnext/regional/report/gstr_2/gstr_2.py
index f899349ccc..616c2b853d 100644
--- a/erpnext/regional/report/gstr_2/gstr_2.py
+++ b/erpnext/regional/report/gstr_2/gstr_2.py
@@ -44,7 +44,7 @@ class Gstr2Report(Gstr1Report):
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
invoice_details = self.invoices.get(inv)
for rate, items in items_based_on_rate.items():
- if rate:
+ if rate or invoice_details.get('gst_category') == 'Registered Composition':
if inv not in self.igst_invoices:
rate = rate / 2
row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items)
@@ -86,7 +86,7 @@ class Gstr2Report(Gstr1Report):
conditions += opts[1]
if self.filters.get("type_of_business") == "B2B":
- conditions += "and ifnull(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') and is_return != 1 "
+ conditions += "and ifnull(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ', 'Registered Composition') and is_return != 1 "
elif self.filters.get("type_of_business") == "CDNR":
conditions += """ and is_return = 1 """
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index c452594608..96b3fa4ccd 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -230,13 +230,20 @@ class Customer(TransactionBase):
frappe.db.set(self, "customer_name", newdn)
def set_loyalty_program(self):
- if self.loyalty_program: return
+ if self.loyalty_program:
+ return
+
loyalty_program = get_loyalty_programs(self)
- if not loyalty_program: return
+ if not loyalty_program:
+ return
+
if len(loyalty_program) == 1:
self.loyalty_program = loyalty_program[0]
else:
- frappe.msgprint(_("Multiple Loyalty Program found for the Customer. Please select manually."))
+ frappe.msgprint(
+ _("Multiple Loyalty Programs found for Customer {}. Please select manually.")
+ .format(frappe.bold(self.customer_name))
+ )
def create_onboarding_docs(self, args):
defaults = frappe.defaults.get_defaults()
@@ -340,7 +347,6 @@ def _set_missing_values(source, target):
@frappe.whitelist()
def get_loyalty_programs(doc):
''' returns applicable loyalty programs for a customer '''
- from frappe.desk.treeview import get_children
lp_details = []
loyalty_programs = frappe.get_all("Loyalty Program",
@@ -349,15 +355,33 @@ def get_loyalty_programs(doc):
"ifnull(to_date, '2500-01-01')": [">=", today()]})
for loyalty_program in loyalty_programs:
- customer_groups = [d.value for d in get_children("Customer Group", loyalty_program.customer_group)] + [loyalty_program.customer_group]
- customer_territories = [d.value for d in get_children("Territory", loyalty_program.customer_territory)] + [loyalty_program.customer_territory]
-
- if (not loyalty_program.customer_group or doc.customer_group in customer_groups)\
- and (not loyalty_program.customer_territory or doc.territory in customer_territories):
+ if (
+ (not loyalty_program.customer_group
+ or doc.customer_group in get_nested_links(
+ "Customer Group",
+ loyalty_program.customer_group,
+ doc.flags.ignore_permissions
+ ))
+ and (not loyalty_program.customer_territory
+ or doc.territory in get_nested_links(
+ "Territory",
+ loyalty_program.customer_territory,
+ doc.flags.ignore_permissions
+ ))
+ ):
lp_details.append(loyalty_program.name)
return lp_details
+def get_nested_links(link_doctype, link_name, ignore_permissions=False):
+ from frappe.desk.treeview import _get_children
+
+ links = [link_name]
+ for d in _get_children(link_doctype, link_name, ignore_permissions):
+ links.append(d.value)
+
+ return links
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None):
@@ -572,4 +596,4 @@ def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, fil
""", {
'customer': customer,
'txt': '%%%s%%' % txt
- })
\ No newline at end of file
+ })
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index 5da248c1b5..246f9234a4 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -64,6 +64,7 @@ class Quotation(SellingController):
opp = frappe.get_doc("Opportunity", opportunity)
opp.set_status(status=status, update=True)
+ @frappe.whitelist()
def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None):
if not self.has_sales_order():
get_lost_reasons = frappe.get_list('Quotation Lost Reason',
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index e56129170c..d9e52e1d69 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -150,7 +150,7 @@ class SalesOrder(SellingController):
if enq:
frappe.db.sql("update `tabOpportunity` set status = %s where name=%s",(flag,enq[0][0]))
- def update_prevdoc_status(self, flag):
+ def update_prevdoc_status(self, flag=None):
for quotation in list(set([d.prevdoc_docname for d in self.get("items")])):
if quotation:
doc = frappe.get_doc("Quotation", quotation)
@@ -372,6 +372,7 @@ class SalesOrder(SellingController):
self.indicator_color = "green"
self.indicator_title = _("Paid")
+ @frappe.whitelist()
def get_work_order_items(self, for_raw_material_request=0):
'''Returns items with BOM that already do not have a linked work order'''
items = []
@@ -778,6 +779,7 @@ def get_events(start, end, filters=None):
@frappe.whitelist()
def make_purchase_order_for_default_supplier(source_name, selected_items=None, target_doc=None):
+ """Creates Purchase Order for each Supplier. Returns a list of doc objects."""
if not selected_items: return
if isinstance(selected_items, string_types):
@@ -820,15 +822,16 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty))
target.project = source_parent.project
- suppliers = [item.get('supplier') for item in selected_items if item.get('supplier') and item.get('supplier')]
- suppliers = list(set(suppliers))
+ suppliers = [item.get('supplier') for item in selected_items if item.get('supplier')]
+ suppliers = list(dict.fromkeys(suppliers)) # remove duplicates while preserving order
- items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code') and item.get('item_code')]
+ items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code')]
items_to_map = list(set(items_to_map))
if not suppliers:
frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order."))
+ purchase_orders = []
for supplier in suppliers:
doc = get_mapped_doc("Sales Order", source_name, {
"Sales Order": {
@@ -872,7 +875,9 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
doc.insert()
frappe.db.commit()
- return doc
+ purchase_orders.append(doc)
+
+ return purchase_orders
@frappe.whitelist()
def make_purchase_order(source_name, selected_items=None, target_doc=None):
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index ee16f44171..3137621fd7 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -1,11 +1,12 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
-import frappe
import json
-from frappe.utils import flt, add_days, nowdate
-import frappe.permissions
import unittest
+import frappe
+import frappe.permissions
+from frappe.utils import flt, add_days, nowdate
+from frappe.core.doctype.user_permission.test_user_permission import create_user
from erpnext.selling.doctype.sales_order.sales_order \
import make_material_request, make_delivery_note, make_sales_invoice, WarehouseRequired
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@@ -340,6 +341,9 @@ class TestSalesOrder(unittest.TestCase):
prev_total = so.get("base_total")
prev_total_in_words = so.get("base_in_words")
+ # get reserved qty before update items
+ reserved_qty_for_second_item = get_reserved_qty("_Test Item 2")
+
first_item_of_so = so.get("items")[0]
trans_item = json.dumps([
{'item_code' : first_item_of_so.item_code, 'rate' : first_item_of_so.rate, \
@@ -353,6 +357,10 @@ class TestSalesOrder(unittest.TestCase):
self.assertEqual(so.get("items")[-1].rate, 200)
self.assertEqual(so.get("items")[-1].qty, 7)
self.assertEqual(so.get("items")[-1].amount, 1400)
+
+ # reserved qty should increase after adding row
+ self.assertEqual(get_reserved_qty('_Test Item 2'), reserved_qty_for_second_item + 7)
+
self.assertEqual(so.status, 'To Deliver and Bill')
updated_total = so.get("base_total")
@@ -372,6 +380,9 @@ class TestSalesOrder(unittest.TestCase):
create_dn_against_so(so.name, 2)
make_sales_invoice(so.name)
+ # get reserved qty before update items
+ reserved_qty_for_second_item = get_reserved_qty("_Test Item 2")
+
# add an item so as to try removing items
trans_item = json.dumps([
{"item_code": '_Test Item', "qty": 5, "rate":1000, "docname": so.get("items")[0].name},
@@ -381,6 +392,9 @@ class TestSalesOrder(unittest.TestCase):
so.reload()
self.assertEqual(len(so.get("items")), 2)
+ # reserved qty should increase after adding row
+ self.assertEqual(get_reserved_qty('_Test Item 2'), reserved_qty_for_second_item + 2)
+
# check if delivered items can be removed
trans_item = json.dumps([{
"item_code": '_Test Item 2',
@@ -401,6 +415,10 @@ class TestSalesOrder(unittest.TestCase):
so.reload()
self.assertEqual(len(so.get("items")), 1)
+
+ # reserved qty should decrease (back to initial) after deleting row
+ self.assertEqual(get_reserved_qty('_Test Item 2'), reserved_qty_for_second_item)
+
self.assertEqual(so.status, 'To Deliver and Bill')
@@ -444,10 +462,8 @@ class TestSalesOrder(unittest.TestCase):
def test_update_child_perm(self):
so = make_sales_order(item_code= "_Test Item", qty=4)
- user = 'test@example.com'
- test_user = frappe.get_doc('User', user)
- test_user.add_roles("Accounts User")
- frappe.set_user(user)
+ test_user = create_user("test_so_child_perms@example.com", "Accounts User")
+ frappe.set_user(test_user.name)
# update qty
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': so.items[0].name}])
@@ -456,18 +472,14 @@ class TestSalesOrder(unittest.TestCase):
# add new item
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 100, 'qty' : 2}])
self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name)
- test_user.remove_roles("Accounts User")
- frappe.set_user("Administrator")
def test_update_child_qty_rate_with_workflow(self):
from frappe.model.workflow import apply_workflow
- frappe.set_user("Administrator")
workflow = make_sales_order_workflow()
so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1)
apply_workflow(so, 'Approve')
- frappe.set_user("Administrator")
user = 'test@example.com'
test_user = frappe.get_doc('User', user)
test_user.add_roles("Sales User", "Test Junior Approver")
@@ -508,12 +520,18 @@ class TestSalesOrder(unittest.TestCase):
so = make_sales_order(item_code = "_Test Item", warehouse=None)
+ # get reserved qty of packed item
+ existing_reserved_qty = get_reserved_qty("_Packed Item")
+
added_item = json.dumps([{"item_code" : "_Product Bundle Item", "rate" : 200, 'qty' : 2}])
update_child_qty_rate('Sales Order', added_item, so.name)
so.reload()
self.assertEqual(so.packed_items[0].qty, 4)
+ # reserved qty in packed item should increase after adding bundle item
+ self.assertEqual(get_reserved_qty("_Packed Item"), existing_reserved_qty + 4)
+
# test uom and conversion factor change
update_uom_conv_factor = json.dumps([{
'item_code': so.get("items")[0].item_code,
@@ -528,6 +546,9 @@ class TestSalesOrder(unittest.TestCase):
so.reload()
self.assertEqual(so.packed_items[0].qty, 8)
+ # reserved qty in packed item should increase after changing bundle item uom
+ self.assertEqual(get_reserved_qty("_Packed Item"), existing_reserved_qty + 8)
+
def test_update_child_with_tax_template(self):
"""
Test Action: Create a SO with one item having its tax account head already in the SO.
@@ -618,33 +639,31 @@ class TestSalesOrder(unittest.TestCase):
frappe.db.set_value("Stock Settings", None, "default_warehouse", old_stock_settings_value)
def test_warehouse_user(self):
- frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com")
- frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", "test2@example.com")
- frappe.permissions.add_user_permission("Company", "_Test Company 1", "test2@example.com")
-
- test_user = frappe.get_doc("User", "test@example.com")
- test_user.add_roles("Sales User", "Stock User")
- test_user.remove_roles("Sales Manager")
+ test_user = create_user("test_so_warehouse_user@example.com", "Sales User", "Stock User")
test_user_2 = frappe.get_doc("User", "test2@example.com")
test_user_2.add_roles("Sales User", "Stock User")
test_user_2.remove_roles("Sales Manager")
- frappe.set_user("test@example.com")
+ frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 1 - _TC", test_user.name)
+ frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", test_user_2.name)
+ frappe.permissions.add_user_permission("Company", "_Test Company 1", test_user_2.name)
- so = make_sales_order(company="_Test Company 1",
+ frappe.set_user(test_user.name)
+
+ so = make_sales_order(company="_Test Company 1", customer="_Test Customer 1",
warehouse="_Test Warehouse 2 - _TC1", do_not_save=True)
so.conversion_rate = 0.02
so.plc_conversion_rate = 0.02
self.assertRaises(frappe.PermissionError, so.insert)
- frappe.set_user("test2@example.com")
+ frappe.set_user(test_user_2.name)
so.insert()
frappe.set_user("Administrator")
- frappe.permissions.remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com")
- frappe.permissions.remove_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", "test2@example.com")
- frappe.permissions.remove_user_permission("Company", "_Test Company 1", "test2@example.com")
+ frappe.permissions.remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", test_user.name)
+ frappe.permissions.remove_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", test_user_2.name)
+ frappe.permissions.remove_user_permission("Company", "_Test Company 1", test_user_2.name)
def test_block_delivery_note_against_cancelled_sales_order(self):
so = make_sales_order()
@@ -743,7 +762,7 @@ class TestSalesOrder(unittest.TestCase):
so = make_sales_order(item_list=so_items, do_not_submit=True)
so.submit()
- po = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]])
+ po = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]])[0]
po.submit()
dn = create_dn_against_so(so.name, delivered_qty=2)
@@ -825,7 +844,7 @@ class TestSalesOrder(unittest.TestCase):
so.submit()
# create po for only one item
- po1 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]])
+ po1 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]])[0]
po1.submit()
self.assertEqual(so.customer, po1.customer)
@@ -835,7 +854,7 @@ class TestSalesOrder(unittest.TestCase):
self.assertEqual(len(po1.items), 1)
# create po for remaining item
- po2 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[1]])
+ po2 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[1]])[0]
po2.submit()
# teardown
@@ -846,6 +865,45 @@ class TestSalesOrder(unittest.TestCase):
so.load_from_db()
so.cancel()
+ def test_drop_shipping_full_for_default_suppliers(self):
+ """Test if multiple POs are generated in one go against different default suppliers."""
+ from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order_for_default_supplier
+
+ if not frappe.db.exists("Item", "_Test Item for Drop Shipping 1"):
+ make_item("_Test Item for Drop Shipping 1", {"is_stock_item": 1, "delivered_by_supplier": 1})
+
+ if not frappe.db.exists("Item", "_Test Item for Drop Shipping 2"):
+ make_item("_Test Item for Drop Shipping 2", {"is_stock_item": 1, "delivered_by_supplier": 1})
+
+ so_items = [
+ {
+ "item_code": "_Test Item for Drop Shipping 1",
+ "warehouse": "",
+ "qty": 2,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier'
+ },
+ {
+ "item_code": "_Test Item for Drop Shipping 2",
+ "warehouse": "",
+ "qty": 2,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier 1'
+ }
+ ]
+
+ # create so and po
+ so = make_sales_order(item_list=so_items, do_not_submit=True)
+ so.submit()
+
+ purchase_orders = make_purchase_order_for_default_supplier(so.name, selected_items=so_items)
+
+ self.assertEqual(len(purchase_orders), 2)
+ self.assertEqual(purchase_orders[0].supplier, '_Test Supplier')
+ self.assertEqual(purchase_orders[1].supplier, '_Test Supplier 1')
+
def test_reserved_qty_for_closing_so(self):
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
fields=["reserved_qty"])
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index 278821e392..9e3c9a5656 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -397,6 +397,7 @@ erpnext.PointOfSale.Controller = class {
this.recent_order_list.toggle_component(false);
frappe.run_serially([
() => this.frm.refresh(name),
+ () => this.frm.call('reset_mode_of_payments'),
() => this.cart.load_invoice(),
() => this.item_selector.toggle_component(true)
]);
diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
index be2b769a8a..b10a9e33c5 100644
--- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
+++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
@@ -64,10 +64,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
{fieldname: 'print', fieldtype: 'Data', label: 'Print Preview'}
],
primary_action: () => {
- const frm = this.events.get_frm();
- frm.doc = this.doc;
- frm.print_preview.lang_code = frm.doc.language;
- frm.print_preview.printit(true);
+ this.print_receipt();
},
primary_action_label: __('Print'),
});
@@ -192,13 +189,21 @@ erpnext.PointOfSale.PastOrderSummary = class {
});
this.$summary_container.on('click', '.print-btn', () => {
- const frm = this.events.get_frm();
- frm.doc = this.doc;
- frm.print_preview.lang_code = frm.doc.language;
- frm.print_preview.printit(true);
+ this.print_receipt();
});
}
+ print_receipt() {
+ const frm = this.events.get_frm();
+ frappe.utils.print(
+ frm.doctype,
+ frm.docname,
+ frm.pos_print_format,
+ frm.doc.letter_head,
+ frm.doc.language || frappe.boot.lang
+ );
+ }
+
attach_shortcuts() {
const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl';
this.$summary_container.find('.print-btn').attr("title", `${ctrl_label}+P`);
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index c041d269a7..c2b5e4f9a9 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -259,6 +259,7 @@ erpnext.company.setup_queries = function(frm) {
["default_payroll_payable_account", {"root_type": "Liability"}],
["round_off_account", {"root_type": "Expense"}],
["write_off_account", {"root_type": "Expense"}],
+ ["default_discount_account", {}],
["discount_allowed_account", {"root_type": "Expense"}],
["discount_received_account", {"root_type": "Income"}],
["exchange_gain_loss_account", {"root_type": "Expense"}],
@@ -275,7 +276,7 @@ erpnext.company.setup_queries = function(frm) {
["expenses_included_in_asset_valuation", {"account_type": "Expenses Included In Asset Valuation"}],
["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}],
["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}],
- ["unrealized_profit_loss_account", {"root_type": "Liability"}]
+ ["unrealized_profit_loss_account", {"root_type": "Liability"},]
], function(i, v) {
erpnext.company.set_custom_query(frm, v);
});
diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json
index 56f60dfcff..83cbf475ab 100644
--- a/erpnext/setup/doctype/company/company.json
+++ b/erpnext/setup/doctype/company/company.json
@@ -59,6 +59,7 @@
"default_deferred_expense_account",
"default_payroll_payable_account",
"default_expense_claim_payable_account",
+ "default_discount_account",
"section_break_22",
"cost_center",
"column_break_26",
@@ -733,6 +734,12 @@
"fieldtype": "Link",
"label": "Unrealized Profit / Loss Account",
"options": "Account"
+ },
+ {
+ "fieldname": "default_discount_account",
+ "fieldtype": "Link",
+ "label": "Default Payment Discount Account",
+ "options": "Account"
}
],
"icon": "fa fa-building",
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 433851cde5..09221714d3 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -66,6 +66,7 @@ class Company(NestedSet):
if frappe.db.sql("select abbr from tabCompany where name!=%s and abbr=%s", (self.name, self.abbr)):
frappe.throw(_("Abbreviation already used for another company"))
+ @frappe.whitelist()
def create_default_tax_template(self):
from erpnext.setup.setup_wizard.operations.taxes_setup import create_sales_tax
create_sales_tax({
diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py
index cbb4c7c5de..ac55fdfdb8 100644
--- a/erpnext/setup/doctype/email_digest/email_digest.py
+++ b/erpnext/setup/doctype/email_digest/email_digest.py
@@ -24,6 +24,7 @@ class EmailDigest(Document):
self._accounts = {}
self.currency = frappe.db.get_value('Company', self.company, "default_currency")
+ @frappe.whitelist()
def get_users(self):
"""get list of users"""
user_list = frappe.db.sql("""
@@ -41,6 +42,7 @@ class EmailDigest(Document):
frappe.response['user_list'] = user_list
+ @frappe.whitelist()
def send(self):
# send email only to enabled users
valid_users = [p[0] for p in frappe.db.sql("""select name from `tabUser`
diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.py b/erpnext/setup/doctype/global_defaults/global_defaults.py
index fa7bc504b6..76a8450829 100644
--- a/erpnext/setup/doctype/global_defaults/global_defaults.py
+++ b/erpnext/setup/doctype/global_defaults/global_defaults.py
@@ -50,6 +50,7 @@ class GlobalDefaults(Document):
# clear cache
frappe.clear_cache()
+ @frappe.whitelist()
def get_defaults(self):
return frappe.defaults.get_defaults()
diff --git a/erpnext/setup/doctype/naming_series/naming_series.py b/erpnext/setup/doctype/naming_series/naming_series.py
index abff97364c..c4f1de14e4 100644
--- a/erpnext/setup/doctype/naming_series/naming_series.py
+++ b/erpnext/setup/doctype/naming_series/naming_series.py
@@ -10,10 +10,12 @@ from frappe import msgprint, throw, _
from frappe.model.document import Document
from frappe.model.naming import parse_naming_series
from frappe.permissions import get_doctypes_with_read
+from frappe.core.doctype.doctype.doctype import validate_series
class NamingSeriesNotSetError(frappe.ValidationError): pass
class NamingSeries(Document):
+ @frappe.whitelist()
def get_transactions(self, arg=None):
doctypes = list(set(frappe.db.sql_list("""select parent
from `tabDocField` df where fieldname='naming_series'""")
@@ -52,6 +54,7 @@ class NamingSeries(Document):
options = list(filter(lambda x: x, [cstr(n).strip() for n in ol]))
return options
+ @frappe.whitelist()
def update_series(self, arg=None):
"""update series list"""
self.validate_series_set()
@@ -126,7 +129,7 @@ class NamingSeries(Document):
dt = frappe.get_doc("DocType", self.select_doc_for_series)
options = self.scrub_options_list(self.set_options.split("\n"))
for series in options:
- dt.validate_series(series)
+ validate_series(dt, series)
for i in sr:
if i[0]:
existing_series = [d.split('.')[0] for d in i[0].split("\n")]
@@ -138,10 +141,12 @@ class NamingSeries(Document):
if not re.match("^[\w\- /.#{}]*$", n, re.UNICODE):
throw(_('Special Characters except "-", "#", ".", "/", "{" and "}" not allowed in naming series'))
+ @frappe.whitelist()
def get_options(self, arg=None):
if frappe.get_meta(arg or self.select_doc_for_series).get_field("naming_series"):
return frappe.get_meta(arg or self.select_doc_for_series).get_field("naming_series").options
+ @frappe.whitelist()
def get_current(self, arg=None):
"""get series current"""
if self.prefix:
diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py
index 1e424dd4b3..82f191d0b7 100644
--- a/erpnext/setup/install.py
+++ b/erpnext/setup/install.py
@@ -142,13 +142,15 @@ def add_standard_navbar_items():
}
]
- current_nabvar_items = navbar_settings.help_dropdown
+ current_navbar_items = navbar_settings.help_dropdown
navbar_settings.set('help_dropdown', [])
for item in erpnext_navbar_items:
- navbar_settings.append('help_dropdown', item)
+ current_labels = [item.get('item_label') for item in current_navbar_items]
+ if not item.get('item_label') in current_labels:
+ navbar_settings.append('help_dropdown', item)
- for item in current_nabvar_items:
+ for item in current_navbar_items:
navbar_settings.append('help_dropdown', {
'item_label': item.item_label,
'item_type': item.item_type,
diff --git a/erpnext/setup/workspace/home/home.json b/erpnext/setup/workspace/home/home.json
index 69ca7cf9ad..305456b266 100644
--- a/erpnext/setup/workspace/home/home.json
+++ b/erpnext/setup/workspace/home/home.json
@@ -10,13 +10,14 @@
"hide_custom": 0,
"icon": "getting-started",
"idx": 0,
+ "is_default": 0,
"is_standard": 1,
"label": "Home",
"links": [
{
"hidden": 0,
"is_query_report": 0,
- "label": "Healthcare",
+ "label": "Accounting",
"onboard": 0,
"type": "Card Break"
},
@@ -24,8 +25,8 @@
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
- "label": "Patient",
- "link_to": "Patient",
+ "label": "Chart of Accounts",
+ "link_to": "Account",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
@@ -34,25 +35,8 @@
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
- "label": "Diagnosis",
- "link_to": "Diagnosis",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Agriculture",
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Crop",
- "link_to": "Crop",
+ "label": "Company",
+ "link_to": "Company",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
@@ -61,8 +45,8 @@
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
- "label": "Crop Cycle",
- "link_to": "Crop Cycle",
+ "label": "Customer",
+ "link_to": "Customer",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
@@ -71,112 +55,8 @@
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
- "label": "Location",
- "link_to": "Location",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Fertilizer",
- "link_to": "Fertilizer",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Education",
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Student",
- "link_to": "Student",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Course",
- "link_to": "Course",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Instructor",
- "link_to": "Instructor",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Room",
- "link_to": "Room",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Non Profit",
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Member",
- "link_to": "Member",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Volunteer",
- "link_to": "Volunteer",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Chapter",
- "link_to": "Chapter",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Donor",
- "link_to": "Donor",
+ "label": "Supplier",
+ "link_to": "Supplier",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
@@ -188,6 +68,16 @@
"onboard": 0,
"type": "Card Break"
},
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Item",
+ "link_to": "Item",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
{
"dependencies": "",
"hidden": 0,
@@ -302,73 +192,6 @@
"onboard": 1,
"type": "Link"
},
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Accounting",
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Item",
- "link_to": "Item",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Customer",
- "link_to": "Customer",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Supplier",
- "link_to": "Supplier",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Company",
- "link_to": "Company",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Chart of Accounts",
- "link_to": "Account",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Opening Invoice Creation Tool",
- "link_to": "Opening Invoice Creation Tool",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
{
"hidden": 0,
"is_query_report": 0,
@@ -386,6 +209,16 @@
"onboard": 1,
"type": "Link"
},
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Opening Invoice Creation Tool",
+ "link_to": "Opening Invoice Creation Tool",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
{
"dependencies": "",
"hidden": 0,
@@ -415,9 +248,177 @@
"link_type": "DocType",
"onboard": 1,
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Healthcare",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Patient",
+ "link_to": "Patient",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Diagnosis",
+ "link_to": "Diagnosis",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Education",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Student",
+ "link_to": "Student",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Instructor",
+ "link_to": "Instructor",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Course",
+ "link_to": "Course",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Room",
+ "link_to": "Room",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Non Profit",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Donor",
+ "link_to": "Donor",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Member",
+ "link_to": "Member",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Volunteer",
+ "link_to": "Volunteer",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Chapter",
+ "link_to": "Chapter",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Agriculture",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Location",
+ "link_to": "Location",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Crop",
+ "link_to": "Crop",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Crop Cycle",
+ "link_to": "Crop Cycle",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Fertilizer",
+ "link_to": "Fertilizer",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
}
],
- "modified": "2021-01-01 12:13:16.055668",
+ "modified": "2021-03-16 15:59:58.416154",
"modified_by": "Administrator",
"module": "Setup",
"name": "Home",
diff --git a/erpnext/stock/doctype/bin/bin.json b/erpnext/stock/doctype/bin/bin.json
index 04d624ec0b..8e79f0e555 100644
--- a/erpnext/stock/doctype/bin/bin.json
+++ b/erpnext/stock/doctype/bin/bin.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "MAT-BIN-.YYYY.-.#####",
"creation": "2013-01-10 16:34:25",
"doctype": "DocType",
@@ -112,7 +113,8 @@
{
"fieldname": "reserved_qty_for_sub_contract",
"fieldtype": "Float",
- "label": "Reserved Qty for sub contract"
+ "label": "Reserved Qty for sub contract",
+ "read_only": 1
},
{
"fieldname": "ma_rate",
@@ -166,7 +168,8 @@
"hide_toolbar": 1,
"idx": 1,
"in_create": 1,
- "modified": "2019-11-18 18:34:59.456882",
+ "links": [],
+ "modified": "2021-03-30 23:09:39.572776",
"modified_by": "Administrator",
"module": "Stock",
"name": "Bin",
@@ -196,5 +199,6 @@
],
"quick_entry": 1,
"search_fields": "item_code,warehouse",
+ "sort_field": "modified",
"sort_order": "ASC"
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 35443906c8..d326a04173 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -101,7 +101,7 @@ class DeliveryNote(SellingController):
for f in fieldname:
toggle_print_hide(self.meta if key == "parent" else item_meta, f)
- super(DeliveryNote, self).before_print()
+ super(DeliveryNote, self).before_print(settings)
def set_actual_qty(self):
for d in self.get('items'):
diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py
index 28e9533186..de85bc3922 100644
--- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py
+++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py
@@ -90,6 +90,7 @@ class DeliveryTrip(Document):
delivery_notes = [get_link_to_form("Delivery Note", note) for note in delivery_notes]
frappe.msgprint(_("Delivery Notes {0} updated").format(", ".join(delivery_notes)))
+ @frappe.whitelist()
def process_route(self, optimize):
"""
Estimate the arrival times for each stop in the Delivery Trip.
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index 33a8fe7c8d..6fed9efa63 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -1054,6 +1054,7 @@
"read_only": 1
},
{
+ "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
"fieldname": "website_image_alt",
"fieldtype": "Data",
"label": "Image Description"
@@ -1066,7 +1067,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 1,
- "modified": "2021-03-15 13:41:04.108932",
+ "modified": "2021-03-18 14:04:38.575519",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
@@ -1137,4 +1138,4 @@
"sort_order": "DESC",
"title_field": "item_name",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 7b7d2da969..7cb84a69f0 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -50,6 +50,7 @@ class Item(WebsiteGenerator):
self.set_onload('stock_exists', self.stock_ledger_created())
self.set_asset_naming_series()
+ @frappe.whitelist()
def set_asset_naming_series(self):
if not hasattr(self, '_asset_naming_series'):
from erpnext.assets.doctype.asset.asset import get_asset_naming_series
@@ -706,6 +707,7 @@ class Item(WebsiteGenerator):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
frappe.db.auto_commit_on_many_writes = 0
+ @frappe.whitelist()
def copy_specification_from_item_group(self):
self.set("website_specifications", [])
if self.item_group:
diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json
index 909c4eeb90..c1f20a47b7 100644
--- a/erpnext/stock/doctype/item/test_records.json
+++ b/erpnext/stock/doctype/item/test_records.json
@@ -12,6 +12,7 @@
"item_name": "_Test Item",
"apply_warehouse_wise_reorder_level": 1,
"gst_hsn_code": "999800",
+ "opening_stock": 10,
"valuation_rate": 100,
"item_defaults": [{
"company": "_Test Company",
diff --git a/erpnext/stock/doctype/item_attribute/test_records.json b/erpnext/stock/doctype/item_attribute/test_records.json
index d346979496..6aa6ffd6c9 100644
--- a/erpnext/stock/doctype/item_attribute/test_records.json
+++ b/erpnext/stock/doctype/item_attribute/test_records.json
@@ -4,10 +4,12 @@
"attribute_name": "Test Size",
"priority": 1,
"item_attribute_values": [
+ {"attribute_value": "Extra Small", "abbr": "XSL"},
{"attribute_value": "Small", "abbr": "S"},
{"attribute_value": "Medium", "abbr": "M"},
{"attribute_value": "Large", "abbr": "L"},
- {"attribute_value": "Extra Small", "abbr": "XSL"}
+ {"attribute_value": "Extra Large", "abbr": "XL"},
+ {"attribute_value": "2XL", "abbr": "2XL"}
]
},
{
diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
index 69a8bf19d3..83109469fc 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
@@ -12,6 +12,7 @@ from erpnext.accounts.doctype.account.account import get_account_currency
from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
class LandedCostVoucher(Document):
+ @frappe.whitelist()
def get_items_from_purchase_receipts(self):
self.set("items", [])
for pr in self.get("purchase_receipts"):
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index 527b0d3ea9..7dfc5da50d 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -354,6 +354,10 @@ frappe.ui.form.on('Material Request', {
},
material_request_type: function(frm) {
frm.toggle_reqd('customer', frm.doc.material_request_type=="Customer Provided");
+
+ if (frm.doc.material_request_type !== 'Material Transfer' && frm.doc.set_from_warehouse) {
+ frm.set_value('set_from_warehouse', '');
+ }
},
});
diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json
index d73349dd39..8d7b238c17 100644
--- a/erpnext/stock/doctype/material_request/material_request.json
+++ b/erpnext/stock/doctype/material_request/material_request.json
@@ -20,9 +20,9 @@
"company",
"amended_from",
"warehouse_section",
- "set_warehouse",
- "column_break5",
"set_from_warehouse",
+ "column_break5",
+ "set_warehouse",
"items_section",
"scan_barcode",
"items",
@@ -314,7 +314,7 @@
"idx": 70,
"is_submittable": 1,
"links": [],
- "modified": "2020-09-19 01:04:09.285862",
+ "modified": "2021-03-31 23:52:55.392512",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request",
diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py
index a7a29cca7f..2008bffcd3 100644
--- a/erpnext/stock/doctype/packing_slip/packing_slip.py
+++ b/erpnext/stock/doctype/packing_slip/packing_slip.py
@@ -152,6 +152,7 @@ class PackingSlip(Document):
return cint(recommended_case_no[0][0]) + 1
+ @frappe.whitelist()
def get_items(self):
self.set("items", [])
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 0da57b734b..61b72092e7 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -25,14 +25,15 @@ class PickList(Document):
if not frappe.get_cached_value('Item', item.item_code, 'has_serial_no'):
continue
if not item.serial_no:
- frappe.throw(_("Row #{0}: {1} does not have any available serial numbers in {2}".format(
- frappe.bold(item.idx), frappe.bold(item.item_code), frappe.bold(item.warehouse))),
+ frappe.throw(_("Row #{0}: {1} does not have any available serial numbers in {2}").format(
+ frappe.bold(item.idx), frappe.bold(item.item_code), frappe.bold(item.warehouse)),
title=_("Serial Nos Required"))
if len(item.serial_no.split('\n')) == item.picked_qty:
continue
frappe.throw(_('For item {0} at row {1}, count of serial numbers does not match with the picked quantity')
.format(frappe.bold(item.item_code), frappe.bold(item.idx)), title=_("Quantity Mismatch"))
+ @frappe.whitelist()
def set_item_locations(self, save=False):
items = self.aggregate_item_qty()
self.item_location_map = frappe._dict()
@@ -380,7 +381,7 @@ def create_stock_entry(pick_list):
stock_entry.set_incoming_rate()
stock_entry.set_actual_qty()
- stock_entry.calculate_rate_and_amount(update_finished_item_rate=False)
+ stock_entry.calculate_rate_and_amount()
return stock_entry.as_dict()
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
index 58b1eca2d3..469511af60 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
@@ -18,6 +18,7 @@ class QualityInspection(Document):
if self.readings:
self.inspect_and_set_status()
+ @frappe.whitelist()
def get_item_specification_details(self):
if not self.quality_inspection_template:
self.quality_inspection_template = frappe.db.get_value('Item',
@@ -32,6 +33,7 @@ class QualityInspection(Document):
child.update(d)
child.status = "Accepted"
+ @frappe.whitelist()
def get_quality_inspection_template(self):
template = ''
if self.bom_no:
@@ -62,17 +64,21 @@ class QualityInspection(Document):
(quality_inspection, self.modified, self.reference_name, self.item_code))
else:
+ args = [quality_inspection, self.modified, self.reference_name, self.item_code]
doctype = self.reference_type + ' Item'
+
if self.reference_type == 'Stock Entry':
doctype = 'Stock Entry Detail'
if self.reference_type and self.reference_name:
conditions = ""
if self.batch_no and self.docstatus == 1:
- conditions += " and t1.batch_no = '%s'"%(self.batch_no)
+ conditions += " and t1.batch_no = %s"
+ args.append(self.batch_no)
if self.docstatus == 2: # if cancel, then remove qi link wherever same name
- conditions += " and t1.quality_inspection = '%s'"%(self.name)
+ conditions += " and t1.quality_inspection = %s"
+ args.append(self.name)
frappe.db.sql("""
UPDATE
@@ -85,7 +91,7 @@ class QualityInspection(Document):
and t1.parent = t2.name
{conditions}
""".format(parent_doc=self.reference_type, child_doc=doctype, conditions=conditions),
- (quality_inspection, self.modified, self.reference_name, self.item_code))
+ args)
def inspect_and_set_status(self):
for reading in self.readings:
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index 8436acbed2..f8cfdf83e7 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -5,7 +5,7 @@
from __future__ import unicode_literals
import frappe, erpnext
from frappe.model.document import Document
-from frappe.utils import cint, get_link_to_form
+from frappe.utils import cint, get_link_to_form, add_to_date, today
from erpnext.stock.stock_ledger import repost_future_sle
from erpnext.accounts.utils import update_gl_entries_after, check_if_stock_and_account_balance_synced
from frappe.utils.user import get_users_with_role
@@ -29,7 +29,7 @@ class RepostItemValuation(Document):
self.company = frappe.get_cached_value(self.voucher_type, self.voucher_no, "company")
elif self.warehouse:
self.company = frappe.get_cached_value("Warehouse", self.warehouse, "company")
-
+
def set_status(self, status=None):
if not status:
status = 'Queued'
@@ -39,6 +39,7 @@ class RepostItemValuation(Document):
frappe.enqueue(repost, timeout=1800, queue='long',
job_name='repost_sle', now=frappe.flags.in_test, doc=self)
+ @frappe.whitelist()
def restart_reposting(self):
self.set_status('Queued')
frappe.enqueue(repost, timeout=1800, queue='long',
@@ -54,7 +55,6 @@ def repost(doc):
repost_sl_entries(doc)
repost_gl_entries(doc)
- check_if_stock_and_account_balance_synced(doc.posting_date, doc.company)
doc.set_status('Completed')
except Exception:
@@ -103,7 +103,7 @@ def notify_error_to_stock_managers(doc, traceback):
recipients = get_users_with_role("Stock Manager")
if not recipients:
get_users_with_role("System Manager")
-
+
subject = _("Error while reposting item valuation")
message = (_("Hi,") + " "
+ _("An error has been appeared while reposting item valuation via {0}")
@@ -112,4 +112,24 @@ def notify_error_to_stock_managers(doc, traceback):
)
frappe.sendmail(recipients=recipients, subject=subject, message=message)
+def repost_entries():
+ riv_entries = get_repost_item_valuation_entries()
+ for row in riv_entries:
+ doc = frappe.get_cached_doc('Repost Item Valuation', row.name)
+ repost(doc)
+
+ riv_entries = get_repost_item_valuation_entries()
+ if riv_entries:
+ return
+
+ for d in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}):
+ check_if_stock_and_account_balance_synced(today(), d.company)
+
+def get_repost_item_valuation_entries():
+ date = add_to_date(today(), hours=-12)
+
+ return frappe.db.sql(""" SELECT name from `tabRepost Item Valuation`
+ WHERE status != 'Completed' and creation <= %s and docstatus = 1
+ ORDER BY timestamp(posting_date, posting_time) asc, creation asc
+ """, date, as_dict=1)
\ No newline at end of file
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index ea1b3873ea..f8ac400a8e 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -458,7 +458,7 @@ class StockEntry(StockController):
Set rate for outgoing, scrapped and finished items
"""
# Set rate for outgoing items
- outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate)
+ outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate, raise_error_if_no_rate)
finished_item_qty = sum([d.transfer_qty for d in self.items if d.is_finished_item])
# Set basic rate for incoming items
@@ -482,13 +482,13 @@ class StockEntry(StockController):
d.basic_rate = flt(d.basic_rate, d.precision("basic_rate"))
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
- def set_rate_for_outgoing_items(self, reset_outgoing_rate=True):
+ def set_rate_for_outgoing_items(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
outgoing_items_cost = 0.0
for d in self.get('items'):
if d.s_warehouse:
if reset_outgoing_rate:
args = self.get_args_for_incoming_rate(d)
- rate = get_incoming_rate(args)
+ rate = get_incoming_rate(args, raise_error_if_no_rate)
if rate > 0:
d.basic_rate = rate
@@ -839,6 +839,7 @@ class StockEntry(StockController):
if not pro_doc.operations:
pro_doc.set_actual_dates()
+ @frappe.whitelist()
def get_item_details(self, args=None, for_update=False):
item = frappe.db.sql("""select i.name, i.stock_uom, i.description, i.image, i.item_name, i.item_group,
i.has_batch_no, i.sample_quantity, i.has_serial_no, i.allow_alternative_item,
@@ -913,6 +914,7 @@ class StockEntry(StockController):
return ret
+ @frappe.whitelist()
def set_items_for_stock_in(self):
self.items = []
@@ -937,6 +939,7 @@ class StockEntry(StockController):
'batch_no': d.batch_no
})
+ @frappe.whitelist()
def get_items(self):
self.set('items', [])
self.validate_work_order()
@@ -1010,7 +1013,8 @@ class StockEntry(StockController):
self.set_scrap_items()
self.set_actual_qty()
- self.calculate_rate_and_amount(raise_error_if_no_rate=False)
+ self.validate_customer_provided_item()
+ self.calculate_rate_and_amount()
def set_scrap_items(self):
if self.purpose != "Send to Subcontractor" and self.purpose in ["Manufacture", "Repack"]:
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index 59f1f3961b..7ebd4e6cb2 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -125,7 +125,7 @@ class TestStockLedgerEntry(unittest.TestCase):
pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-10',
warehouse="Stores - _TC", item_code="_Test Item for Reposting", qty=5, rate=100)
- return_pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-15',
+ return_pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-15',
warehouse="Stores - _TC", item_code="_Test Item for Reposting", is_return=1, return_against=pr.name, qty=-2)
# check sle
@@ -278,7 +278,7 @@ class TestStockLedgerEntry(unittest.TestCase):
frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM")
make_bom(item = subcontracted_item, raw_materials =[rm_item_code], currency="INR")
-
+
# Purchase raw materials on supplier warehouse: Qty = 50, Rate = 100
pr = make_purchase_receipt(company=company, posting_date='2020-04-10',
warehouse="Stores - _TC", item_code=rm_item_code, qty=10, rate=100)
@@ -292,7 +292,7 @@ class TestStockLedgerEntry(unittest.TestCase):
# Update raw material's valuation via LCV, Additional cost = 50
lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
-
+
pr1.reload()
self.assertEqual(pr1.items[0].valuation_rate, 125)
@@ -310,31 +310,33 @@ class TestStockLedgerEntry(unittest.TestCase):
# Back dated stock transactions are only allowed to stock managers
frappe.db.set_value("Stock Settings", None,
"role_allowed_to_create_edit_back_dated_transactions", "Stock Manager")
-
+
# Set User with Stock User role but not Stock Manager
- frappe.set_user("test@example.com")
- user = frappe.get_doc("User", "test@example.com")
- user.add_roles("Stock User")
- user.remove_roles("Stock Manager")
+ try:
+ frappe.set_user("test@example.com")
+ user = frappe.get_doc("User", "test@example.com")
+ user.add_roles("Stock User")
+ user.remove_roles("Stock Manager")
- stock_entry_on_today = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100)
- back_dated_se_1 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100,
- posting_date=add_days(today(), -1), do_not_submit=True)
+ stock_entry_on_today = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100)
+ back_dated_se_1 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100,
+ posting_date=add_days(today(), -1), do_not_submit=True)
- # Block back-dated entry
- self.assertRaises(BackDatedStockTransaction, back_dated_se_1.submit)
+ # Block back-dated entry
+ self.assertRaises(BackDatedStockTransaction, back_dated_se_1.submit)
- user.add_roles("Stock Manager")
+ user.add_roles("Stock Manager")
- # Back dated entry allowed to Stock Manager
- back_dated_se_2 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100,
- posting_date=add_days(today(), -1))
+ # Back dated entry allowed to Stock Manager
+ back_dated_se_2 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100,
+ posting_date=add_days(today(), -1))
- back_dated_se_2.cancel()
- stock_entry_on_today.cancel()
+ back_dated_se_2.cancel()
+ stock_entry_on_today.cancel()
- frappe.db.set_value("Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", None)
- frappe.set_user("Administrator")
+ finally:
+ frappe.db.set_value("Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", None)
+ frappe.set_user("Administrator")
def create_repack_entry(**args):
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index f0a90f9754..b452e96c5e 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -29,6 +29,8 @@ class StockReconciliation(StockController):
self.remove_items_with_no_change()
self.validate_data()
self.validate_expense_account()
+ self.validate_customer_provided_item()
+ self.set_zero_value_for_customer_provided_items()
self.set_total_qty_and_amount()
self.validate_putaway_capacity()
@@ -217,7 +219,7 @@ class StockReconciliation(StockController):
if row.valuation_rate in ("", None):
row.valuation_rate = previous_sle.get("valuation_rate", 0)
- if row.qty and not row.valuation_rate:
+ if row.qty and not row.valuation_rate and not row.allow_zero_valuation_rate:
frappe.throw(_("Valuation Rate required for Item {0} at row {1}").format(row.item_code, row.idx))
if ((previous_sle and row.qty == previous_sle.get("qty_after_transaction")
@@ -436,6 +438,20 @@ class StockReconciliation(StockController):
if frappe.db.get_value("Account", self.expense_account, "report_type") == "Profit and Loss":
frappe.throw(_("Difference Account must be a Asset/Liability type account, since this Stock Reconciliation is an Opening Entry"), OpeningEntryAccountError)
+ def set_zero_value_for_customer_provided_items(self):
+ changed_any_values = False
+
+ for d in self.get('items'):
+ is_customer_item = frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item')
+ if is_customer_item and d.valuation_rate:
+ d.valuation_rate = 0.0
+ changed_any_values = True
+
+ if changed_any_values:
+ msgprint(_("Valuation rate for customer provided items has been set to zero."),
+ title=_("Note"), indicator="blue")
+
+
def set_total_qty_and_amount(self):
for d in self.get("items"):
d.amount = flt(d.qty, d.precision("qty")) * flt(d.valuation_rate, d.precision("valuation_rate"))
@@ -531,4 +547,4 @@ def get_difference_account(purpose, company):
account = frappe.db.get_value('Account', {'is_group': 0,
'company': company, 'account_type': 'Temporary'}, 'name')
- return account
\ No newline at end of file
+ return account
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 088456f865..6690c6a606 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -193,6 +193,16 @@ class TestStockReconciliation(unittest.TestCase):
stock_doc = frappe.get_doc("Stock Reconciliation", d)
stock_doc.cancel()
+ def test_customer_provided_items(self):
+ item_code = 'Stock-Reco-customer-Item-100'
+ create_item(item_code, is_customer_provided_item = 1,
+ customer = '_Test Customer', is_purchase_item = 0)
+
+ sr = create_stock_reconciliation(item_code = item_code, qty = 10, rate = 420)
+
+ self.assertEqual(sr.get("items")[0].allow_zero_valuation_rate, 1)
+ self.assertEqual(sr.get("items")[0].valuation_rate, 0)
+ self.assertEqual(sr.get("items")[0].amount, 0)
def insert_existing_sle(warehouse):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
index e53db0772b..85c7ebe263 100644
--- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
+++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
@@ -13,6 +13,7 @@
"qty",
"valuation_rate",
"amount",
+ "allow_zero_valuation_rate",
"serial_no_and_batch_section",
"serial_no",
"column_break_11",
@@ -166,10 +167,19 @@
"fieldtype": "Link",
"label": "Batch No",
"options": "Batch"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_zero_valuation_rate",
+ "fieldtype": "Check",
+ "label": "Allow Zero Valuation Rate",
+ "print_hide": 1,
+ "read_only": 1
}
],
"istable": 1,
- "modified": "2019-06-14 17:10:53.188305",
+ "links": [],
+ "modified": "2021-03-23 11:09:44.407157",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation Item",
@@ -179,4 +189,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 873cfec85e..e23f7d43d9 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -110,7 +110,7 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
get_gross_profit(out)
if args.doctype == 'Material Request':
out.rate = args.rate or out.price_list_rate
- out.amount = flt(args.qty * out.rate)
+ out.amount = flt(args.qty) * flt(out.rate)
return out
@@ -314,7 +314,9 @@ def get_basic_details(args, item, overwrite_warehouse=True):
"last_purchase_rate": item.last_purchase_rate if args.get("doctype") in ["Purchase Order"] else 0,
"transaction_date": args.get("transaction_date"),
"against_blanket_order": args.get("against_blanket_order"),
- "bom_no": item.get("default_bom")
+ "bom_no": item.get("default_bom"),
+ "weight_per_unit": args.get("weight_per_unit") or item.get("weight_per_unit"),
+ "weight_uom": args.get("weight_uom") or item.get("weight_uom")
})
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
@@ -369,6 +371,9 @@ def get_basic_details(args, item, overwrite_warehouse=True):
if meta.get_field("barcode"):
update_barcode_value(out)
+ if out.get("weight_per_unit"):
+ out['total_weight'] = out.weight_per_unit * out.stock_qty
+
return out
def get_item_warehouse(item, args, overwrite_warehouse, defaults={}):
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index e5d4d626c4..6dfede4590 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -198,7 +198,7 @@ def get_item_warehouse_map(filters, sle):
else:
qty_diff = flt(d.actual_qty)
- value_diff = flt(d.stock_value) - flt(qty_dict.bal_val)
+ value_diff = flt(d.stock_value_difference)
if d.posting_date < from_date:
qty_dict.opening_qty += qty_diff
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index f54b3c1bb2..121c51cf6a 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -207,11 +207,11 @@ class update_entries_after(object):
def build(self):
- from erpnext.controllers.stock_controller import check_if_future_sle_exists
+ from erpnext.controllers.stock_controller import future_sle_exists
if self.args.get("sle_id"):
self.process_sle_against_current_timestamp()
- if not check_if_future_sle_exists(self.args):
+ if not future_sle_exists(self.args):
self.update_bin()
else:
entries_to_fix = self.get_future_entries_to_fix()
@@ -856,4 +856,4 @@ def get_future_sle_with_negative_qty(args):
and qty_after_transaction < 0
order by timestamp(posting_date, posting_time) asc
limit 1
- """, args, as_dict=1)
\ No newline at end of file
+ """, args, as_dict=1)
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index bbbbc4a527..767a8a6a29 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -165,6 +165,7 @@ class Issue(Document):
communication.ignore_mandatory = True
communication.save()
+ @frappe.whitelist()
def split_issue(self, subject, communication_id):
# Bug: Pressing enter doesn't send subject
from copy import deepcopy
@@ -259,6 +260,7 @@ class Issue(Document):
self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement)
frappe.msgprint(_("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement))
+ @frappe.whitelist()
def reset_service_level_agreement(self, reason, user):
if not frappe.db.get_single_value("Support Settings", "allow_resetting_service_level_agreement"):
frappe.throw(_("Allow Resetting Service Level Agreement from Support Settings."))
diff --git a/erpnext/support/report/issue_summary/issue_summary.py b/erpnext/support/report/issue_summary/issue_summary.py
index 3d735314f4..7861e30d25 100644
--- a/erpnext/support/report/issue_summary/issue_summary.py
+++ b/erpnext/support/report/issue_summary/issue_summary.py
@@ -260,8 +260,7 @@ class IssueSummary(object):
self.issue_summary_data[value]['avg_user_resolution_time'] = entry.get('avg_user_resolution_time') or 0.0
def get_chart_data(self):
- if not self.data:
- return None
+ self.chart = []
labels = []
open_issues = []
@@ -310,8 +309,7 @@ class IssueSummary(object):
}
def get_report_summary(self):
- if not self.data:
- return None
+ self.report_summary = []
open_issues = 0
replied = 0
|