Merge branch 'staging-fixes' of https://github.com/frappe/erpnext into stock-recon
diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index 0d5ac9d..4197d54 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -5,7 +5,7 @@
from erpnext.hooks import regional_overrides
from frappe.utils import getdate
-__version__ = '10.1.74'
+__version__ = '10.1.76'
def get_default_company(user=None):
'''Get default company for user'''
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index 7a91bd2..1085cdd 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -5,7 +5,7 @@
import frappe, erpnext
from frappe import _, msgprint, scrub
-from frappe.defaults import get_user_permissions
+from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
from frappe.model.utils import get_fetch_values
from frappe.utils import (add_days, getdate, formatdate, date_diff,
add_years, get_timestamp, nowdate, flt, add_months, get_last_day)
@@ -151,10 +151,7 @@
def set_price_list(out, party, party_type, given_price_list):
# price list
- price_list = filter(None, get_user_permissions()
- .get("Price List", {})
- .get("docs", []))
- price_list = list(price_list)
+ price_list = get_permitted_documents('Price List')
if price_list:
price_list = price_list[0]
diff --git a/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py b/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py
index 05c8fb7..c234da0 100644
--- a/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py
+++ b/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py
@@ -108,33 +108,33 @@
def get_pos_invoice_data(filters):
conditions = get_conditions(filters)
result = frappe.db.sql(''
- 'SELECT '
- 'posting_date, owner, sum(net_total) as "net_total", sum(total_taxes) as "total_taxes", '
- 'sum(paid_amount) as "paid_amount", sum(outstanding_amount) as "outstanding_amount", '
- 'mode_of_payment, warehouse, cost_center '
- 'FROM ('
- 'SELECT '
- 'parent, item_code, sum(amount) as "base_total", warehouse, cost_center '
- 'from `tabSales Invoice Item` group by parent'
- ') t1 '
- 'left join '
- '(select parent, mode_of_payment from `tabSales Invoice Payment` group by parent) t3 '
- 'on (t3.parent = t1.parent) '
- 'JOIN ('
- 'SELECT '
- 'docstatus, company, is_pos, name, posting_date, owner, sum(base_total) as "base_total", '
- 'sum(net_total) as "net_total", sum(total_taxes_and_charges) as "total_taxes", '
- 'sum(base_paid_amount) as "paid_amount", sum(outstanding_amount) as "outstanding_amount" '
- 'FROM `tabSales Invoice` '
- 'GROUP BY name'
- ') a '
- 'ON ('
- 't1.parent = a.name and t1.base_total = a.base_total) '
- 'WHERE a.docstatus = 1'
- ' AND {conditions} '
- 'GROUP BY '
- 'owner, posting_date, warehouse'.format(conditions=conditions), filters, as_dict=1
- )
+ 'SELECT '
+ 'posting_date, owner, sum(net_total) as "net_total", sum(total_taxes) as "total_taxes", '
+ 'sum(paid_amount) as "paid_amount", sum(outstanding_amount) as "outstanding_amount", '
+ 'mode_of_payment, warehouse, cost_center '
+ 'FROM ('
+ 'SELECT '
+ 'parent, item_code, sum(amount) as "base_total", warehouse, cost_center '
+ 'from `tabSales Invoice Item` group by parent'
+ ') t1 '
+ 'left join '
+ '(select parent, mode_of_payment from `tabSales Invoice Payment` group by parent) t3 '
+ 'on (t3.parent = t1.parent) '
+ 'JOIN ('
+ 'SELECT '
+ 'docstatus, company, is_pos, name, posting_date, owner, sum(base_total) as "base_total", '
+ 'sum(net_total) as "net_total", sum(total_taxes_and_charges) as "total_taxes", '
+ 'sum(base_paid_amount) as "paid_amount", sum(outstanding_amount) as "outstanding_amount" '
+ 'FROM `tabSales Invoice` '
+ 'GROUP BY name'
+ ') a '
+ 'ON ('
+ 't1.parent = a.name and t1.base_total = a.base_total) '
+ 'WHERE a.docstatus = 1'
+ ' AND {conditions} '
+ 'GROUP BY '
+ 'owner, posting_date, warehouse'.format(conditions=conditions), filters, as_dict=1
+ )
return result
@@ -156,7 +156,6 @@
def get_mode_of_payments(filters):
- frappe.log_error(filters, 'filters')
mode_of_payments = {}
invoice_list = get_invoices(filters)
invoice_list_names = ",".join(['"' + invoice['name'] + '"' for invoice in invoice_list])
@@ -164,12 +163,14 @@
inv_mop = frappe.db.sql("""select a.owner,a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment
from `tabSales Invoice` a, `tabSales Invoice Payment` b
where a.name = b.parent
+ and a.docstatus = 1
and a.name in ({invoice_list_names})
union
select a.owner,a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment
from `tabSales Invoice` a, `tabPayment Entry` b,`tabPayment Entry Reference` c
where a.name = c.reference_name
and b.name = c.parent
+ and b.docstatus = 1
and a.name in ({invoice_list_names})
union
select a.owner, a.posting_date,
@@ -196,13 +197,13 @@
def get_mode_of_payment_details(filters):
mode_of_payment_details = {}
invoice_list = get_invoices(filters)
- frappe.log_error(invoice_list, 'invoice_list')
invoice_list_names = ",".join(['"' + invoice['name'] + '"' for invoice in invoice_list])
if invoice_list:
inv_mop_detail = frappe.db.sql("""select a.owner, a.posting_date,
ifnull(b.mode_of_payment, '') as mode_of_payment, sum(b.base_amount) as paid_amount
from `tabSales Invoice` a, `tabSales Invoice Payment` b
where a.name = b.parent
+ and a.docstatus = 1
and a.name in ({invoice_list_names})
group by a.owner, a.posting_date, mode_of_payment
union
@@ -211,6 +212,7 @@
from `tabSales Invoice` a, `tabPayment Entry` b,`tabPayment Entry Reference` c
where a.name = c.reference_name
and b.name = c.parent
+ and b.docstatus = 1
and a.name in ({invoice_list_names})
group by a.owner, a.posting_date, mode_of_payment
union
diff --git a/erpnext/accounts/report/sales_payment_summary/test_sales_payment_summary.py b/erpnext/accounts/report/sales_payment_summary/test_sales_payment_summary.py
new file mode 100644
index 0000000..62843e7
--- /dev/null
+++ b/erpnext/accounts/report/sales_payment_summary/test_sales_payment_summary.py
@@ -0,0 +1,165 @@
+# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import unittest
+import frappe
+from erpnext.accounts.report.sales_payment_summary.sales_payment_summary import get_mode_of_payments, get_mode_of_payment_details
+from frappe.utils import today
+from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+
+test_dependencies = ["Sales Invoice"]
+
+class TestSalesPaymentSummary(unittest.TestCase):
+ @classmethod
+ def setUpClass(self):
+ create_records()
+ pes = frappe.get_all("Payment Entry")
+ jes = frappe.get_all("Journal Entry")
+ sis = frappe.get_all("Sales Invoice")
+ for pe in pes:
+ frappe.db.set_value("Payment Entry", pe.name, "docstatus", 2)
+ for je in jes:
+ frappe.db.set_value("Journal Entry", je.name, "docstatus", 2)
+ for si in sis:
+ frappe.db.set_value("Sales Invoice", si.name, "docstatus", 2)
+
+ def test_get_mode_of_payments(self):
+ filters = get_filters()
+
+ for dummy in range(2):
+ si = create_sales_invoice_record()
+ si.insert()
+ si.submit()
+
+ if int(si.name[-3:])%2 == 0:
+ bank_account = "_Test Cash - _TC"
+ mode_of_payment = "Cash"
+ else:
+ bank_account = "_Test Bank - _TC"
+ mode_of_payment = "Credit Card"
+
+ pe = get_payment_entry("Sales Invoice", si.name, bank_account=bank_account)
+ pe.reference_no = "_Test"
+ pe.reference_date = today()
+ pe.mode_of_payment = mode_of_payment
+ pe.insert()
+ pe.submit()
+
+ mop = get_mode_of_payments(filters)
+ self.assertTrue('Credit Card' in mop.values()[0])
+ self.assertTrue('Cash' in mop.values()[0])
+
+ # Cancel all Cash payment entry and check if this mode of payment is still fetched.
+ payment_entries = frappe.get_all("Payment Entry", filters={"mode_of_payment": "Cash", "docstatus": 1}, fields=["name", "docstatus"])
+ for payment_entry in payment_entries:
+ pe = frappe.get_doc("Payment Entry", payment_entry.name)
+ pe.cancel()
+
+ mop = get_mode_of_payments(filters)
+ self.assertTrue('Credit Card' in mop.values()[0])
+ self.assertTrue('Cash' not in mop.values()[0])
+
+ def test_get_mode_of_payments_details(self):
+ filters = get_filters()
+
+ for dummy in range(2):
+ si = create_sales_invoice_record()
+ si.insert()
+ si.submit()
+
+ if int(si.name[-3:])%2 == 0:
+ bank_account = "_Test Cash - _TC"
+ mode_of_payment = "Cash"
+ else:
+ bank_account = "_Test Bank - _TC"
+ mode_of_payment = "Credit Card"
+
+ pe = get_payment_entry("Sales Invoice", si.name, bank_account=bank_account)
+ pe.reference_no = "_Test"
+ pe.reference_date = today()
+ pe.mode_of_payment = mode_of_payment
+ pe.insert()
+ pe.submit()
+
+ mopd = get_mode_of_payment_details(filters)
+
+ mopd_values = mopd.values()[0]
+ for mopd_value in mopd_values:
+ if mopd_value[0] == "Credit Card":
+ cc_init_amount = mopd_value[1]
+
+ # Cancel one Credit Card Payment Entry and check that it is not fetched in mode of payment details.
+ payment_entries = frappe.get_all("Payment Entry", filters={"mode_of_payment": "Credit Card", "docstatus": 1}, fields=["name", "docstatus"])
+ for payment_entry in payment_entries[:1]:
+ pe = frappe.get_doc("Payment Entry", payment_entry.name)
+ pe.cancel()
+
+ mopd = get_mode_of_payment_details(filters)
+ mopd_values = mopd.values()[0]
+ for mopd_value in mopd_values:
+ if mopd_value[0] == "Credit Card":
+ cc_final_amount = mopd_value[1]
+
+ self.assertTrue(cc_init_amount > cc_final_amount)
+
+def get_filters():
+ return {
+ "from_date": "1900-01-01",
+ "to_date": today(),
+ "company": "_Test Company"
+ }
+
+def create_sales_invoice_record(qty=1):
+ # return sales invoice doc object
+ return frappe.get_doc({
+ "doctype": "Sales Invoice",
+ "customer": frappe.get_doc('Customer', {"customer_name": "Prestiga-Biz"}).name,
+ "company": '_Test Company',
+ "due_date": today(),
+ "posting_date": today(),
+ "currency": "INR",
+ "taxes_and_charges": "",
+ "debit_to": "Debtors - _TC",
+ "taxes": [],
+ "items": [{
+ 'doctype': 'Sales Invoice Item',
+ 'item_code': frappe.get_doc('Item', {'item_name': 'Consulting'}).name,
+ 'qty': qty,
+ "rate": 10000,
+ 'income_account': 'Sales - _TC',
+ 'cost_center': 'Main - _TC',
+ 'expense_account': 'Cost of Goods Sold - _TC'
+ }]
+ })
+
+def create_records():
+ if frappe.db.exists("Customer", "Prestiga-Biz"):
+ return
+
+ #customer
+ frappe.get_doc({
+ "customer_group": "_Test Customer Group",
+ "customer_name": "Prestiga-Biz",
+ "customer_type": "Company",
+ "doctype": "Customer",
+ "territory": "_Test Territory"
+ }).insert()
+
+ # item
+ item = frappe.get_doc({
+ "doctype": "Item",
+ "item_code": "Consulting",
+ "item_name": "Consulting",
+ "item_group": "All Item Groups",
+ "company": "_Test Company",
+ "is_stock_item": 0
+ }).insert()
+
+ # item price
+ frappe.get_doc({
+ "doctype": "Item Price",
+ "price_list": "Standard Selling",
+ "item_code": item.item_code,
+ "price_list_rate": 10000
+ }).insert()
\ No newline at end of file
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index ed761ce..3c4ef2b 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -91,6 +91,7 @@
self.party_account_currency = get_party_account_currency("Supplier", self.supplier, self.company)
def validate_minimum_order_qty(self):
+ if not self.get("items"): return
items = list(set([d.item_code for d in self.get("items")]))
itemwise_min_order_qty = frappe._dict(frappe.db.sql("""select name, min_order_qty
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index ee6dc2a..541e56d 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -678,7 +678,7 @@
frappe.db.sql("delete from `tabSerial No` where purchase_document_no=%s", self.name)
def validate_schedule_date(self):
- if not self.schedule_date:
+ if not self.schedule_date and self.get("items"):
self.schedule_date = min([d.schedule_date for d in self.get("items")])
if self.schedule_date:
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 53ec3d6..14e4f68 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -12,7 +12,7 @@
source_link = "https://github.com/frappe/erpnext"
develop_version = '12.x.x-develop'
-staging_version = '11.0.3-beta.31'
+staging_version = '11.0.3-beta.32'
error_report_email = "support@erpnext.com"
diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index cb4c190..d518cd8 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -13,8 +13,8 @@
from erpnext.utilities.transaction_base import delete_events
from frappe.utils.nestedset import NestedSet
-class EmployeeUserDisabledError(frappe.ValidationError):
- pass
+class EmployeeUserDisabledError(frappe.ValidationError): pass
+class EmployeeLeftValidationError(frappe.ValidationError): pass
class Employee(NestedSet):
nsm_parent_field = 'reports_to'
@@ -62,8 +62,8 @@
def validate_user_details(self):
data = frappe.db.get_value('User',
self.user_id, ['enabled', 'user_image'], as_dict=1)
-
- self.image = data.get("user_image")
+ if data.get("user_image"):
+ self.image = data.get("user_image")
self.validate_for_enabled_user_id(data.get("enabled", 0))
self.validate_duplicate_user_id()
@@ -147,8 +147,16 @@
validate_email_add(self.personal_email, True)
def validate_status(self):
- if self.status == 'Left' and not self.relieving_date:
- throw(_("Please enter relieving date."))
+ if self.status == 'Left':
+ reports_to = frappe.db.get_all('Employee',
+ filters={'reports_to': self.name}
+ )
+ if reports_to:
+ link_to_employees = [frappe.utils.get_link_to_form('Employee', employee.name) for employee in reports_to]
+ throw(_("Employee status cannot be set to 'Left' as following employees are currently reporting to this employee: ")
+ + ', '.join(link_to_employees), EmployeeLeftValidationError)
+ if not self.relieving_date:
+ throw(_("Please enter relieving date."))
def validate_for_enabled_user_id(self, enabled):
if not self.status == 'Active':
diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py
index 1afb8f4..5a63beb 100644
--- a/erpnext/hr/doctype/employee/test_employee.py
+++ b/erpnext/hr/doctype/employee/test_employee.py
@@ -7,6 +7,7 @@
import erpnext
import unittest
import frappe.utils
+from erpnext.hr.doctype.employee.employee import EmployeeLeftValidationError
test_records = frappe.get_test_records('Employee')
@@ -32,6 +33,18 @@
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertTrue("Subject: Birthday Reminder" in email_queue[0].message)
+ def test_employee_status_left(self):
+ employee1 = make_employee("test_employee_1@company.com")
+ employee2 = make_employee("test_employee_2@company.com")
+ employee1_doc = frappe.get_doc("Employee", employee1)
+ employee2_doc = frappe.get_doc("Employee", employee2)
+ employee2_doc.reload()
+ employee2_doc.reports_to = employee1_doc.name
+ employee2_doc.save()
+ employee1_doc.reload()
+ employee1_doc.status = 'Left'
+ self.assertRaises(EmployeeLeftValidationError, employee1_doc.save)
+
def make_employee(user):
if not frappe.db.get_value("User", user):
frappe.get_doc({
diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js
index a77dd32..5bce348 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.js
+++ b/erpnext/hr/doctype/leave_application/leave_application.js
@@ -14,7 +14,7 @@
doctype: frm.doc.doctype
}
};
- });
+ });
frm.set_query("employee", erpnext.queries.employee);
},
@@ -83,7 +83,7 @@
if (!frm.doc.employee && frappe.defaults.get_user_permissions()) {
const perm = frappe.defaults.get_user_permissions();
if (perm && perm['Employee']) {
- frm.set_value('employee', perm['Employee']["docs"][0])
+ frm.set_value('employee', perm['Employee'].map(perm_doc => perm_doc.doc)[0]);
}
}
},
diff --git a/erpnext/hr/doctype/upload_attendance/test_upload_attendance.py b/erpnext/hr/doctype/upload_attendance/test_upload_attendance.py
new file mode 100644
index 0000000..5ab2847
--- /dev/null
+++ b/erpnext/hr/doctype/upload_attendance/test_upload_attendance.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+from frappe.utils import getdate
+from erpnext.hr.doctype.upload_attendance.upload_attendance import get_data
+from erpnext.hr.doctype.employee.test_employee import make_employee
+
+class TestUploadAttendance(unittest.TestCase):
+ def test_date_range(self):
+ employee = make_employee("test_employee@company.com")
+ employee_doc = frappe.get_doc("Employee", employee)
+ date_of_joining = "2018-01-02"
+ relieving_date = "2018-01-03"
+ from_date = "2018-01-01"
+ to_date = "2018-01-04"
+ employee_doc.date_of_joining = date_of_joining
+ employee_doc.relieving_date = relieving_date
+ employee_doc.save()
+ args = {
+ "from_date": from_date,
+ "to_date": to_date
+ }
+ data = get_data(args)
+ filtered_data = []
+ for row in data:
+ if row[1] == employee:
+ filtered_data.append(row)
+ print(filtered_data)
+ for row in filtered_data:
+ self.assertTrue(getdate(row[3]) >= getdate(date_of_joining) and getdate(row[3]) <= getdate(relieving_date))
diff --git a/erpnext/hr/doctype/upload_attendance/upload_attendance.py b/erpnext/hr/doctype/upload_attendance/upload_attendance.py
index 3d080a7..db74b10 100644
--- a/erpnext/hr/doctype/upload_attendance/upload_attendance.py
+++ b/erpnext/hr/doctype/upload_attendance/upload_attendance.py
@@ -41,16 +41,28 @@
return w
def add_data(w, args):
+ data = get_data(args)
+ writedata(w, data)
+ return w
+
+def get_data(args):
dates = get_dates(args)
employees = get_active_employees()
existing_attendance_records = get_existing_attendance_records(args)
+ data = []
for date in dates:
for employee in employees:
+ if getdate(date) < getdate(employee.date_of_joining):
+ continue
+ if employee.relieving_date:
+ if getdate(date) > getdate(employee.relieving_date):
+ continue
existing_attendance = {}
if existing_attendance_records \
- and tuple([getdate(date), employee.name]) in existing_attendance_records:
+ and tuple([getdate(date), employee.name]) in existing_attendance_records \
+ and getdate(employee.date_of_joining) >= getdate(date) \
+ and getdate(employee.relieving_date) <= getdate(date):
existing_attendance = existing_attendance_records[tuple([getdate(date), employee.name])]
-
row = [
existing_attendance and existing_attendance.name or "",
employee.name, employee.employee_name, date,
@@ -58,8 +70,12 @@
existing_attendance and existing_attendance.leave_type or "", employee.company,
existing_attendance and existing_attendance.naming_series or get_naming_series(),
]
- w.writerow(row)
- return w
+ data.append(row)
+ return data
+
+def writedata(w, data):
+ for row in data:
+ w.writerow(row)
def get_dates(args):
"""get list of dates in between from date and to date"""
@@ -68,8 +84,13 @@
return dates
def get_active_employees():
- employees = frappe.db.sql("""select name, employee_name, company
- from tabEmployee where docstatus < 2 and status = 'Active'""", as_dict=1)
+ employees = frappe.db.get_all('Employee',
+ fields=['name', 'employee_name', 'date_of_joining', 'company', 'relieving_date'],
+ filters={
+ 'docstatus': ['<', 2],
+ 'status': 'Active'
+ }
+ )
return employees
def get_existing_attendance_records(args):
diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json
index b520360..8d4d69b 100644
--- a/erpnext/manufacturing/doctype/bom_item/bom_item.json
+++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json
@@ -79,67 +79,67 @@
"unique": 0
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "operation",
- "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": "Item operation",
- "length": 0,
- "no_copy": 0,
- "options": "Operation",
- "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,
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "operation",
+ "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": "Item operation",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Operation",
+ "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,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_3",
- "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,
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "column_break_3",
+ "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
},
{
@@ -966,6 +966,7 @@
"collapsible": 0,
"columns": 0,
"fetch_from": "item_code.include_item_in_manufacturing",
+ "fieldname": "include_item_in_manufacturing",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
@@ -974,6 +975,7 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
+ "label": "Include Item In Manufacturing",
"length": 0,
"no_copy": 0,
"permlevel": 0,
@@ -987,29 +989,6 @@
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
- "fieldname": "include_item_in_manufacturing",
- "fieldtype": "Check",
- "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": "Include Item In Manufacturing",
- "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
},
{
@@ -1044,71 +1023,6 @@
"set_only_once": 0,
"translatable": 0,
"unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "operation",
- "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": "Item operation",
- "length": 0,
- "no_copy": 0,
- "options": "Operation",
- "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,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "allow_alternative_item",
- "fieldtype": "Check",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Allow Alternative Item",
- "length": 0,
- "no_copy": 0,
- "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
}
],
"has_web_view": 0,
@@ -1121,7 +1035,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
- "modified": "2018-11-22 15:04:55.187136",
+ "modified": "2018-12-28 16:38:56.529079",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Item",
diff --git a/erpnext/patches/v11_0/rename_bom_wo_fields.py b/erpnext/patches/v11_0/rename_bom_wo_fields.py
index 43fbea5..c8106a6 100644
--- a/erpnext/patches/v11_0/rename_bom_wo_fields.py
+++ b/erpnext/patches/v11_0/rename_bom_wo_fields.py
@@ -8,7 +8,19 @@
def execute():
for doctype in ['BOM Explosion Item', 'BOM Item', 'Work Order Item', 'Item']:
if frappe.db.has_column(doctype, 'allow_transfer_for_manufacture'):
- rename_field('BOM Item', "allow_transfer_for_manufacture", "include_item_in_manufacturing")
+ if doctype != 'Item':
+ frappe.reload_doc('manufacturing', 'doctype', frappe.scrub(doctype))
+ else:
+ frappe.reload_doc('stock', 'doctype', frappe.scrub(doctype))
+
+ rename_field(doctype, "allow_transfer_for_manufacture", "include_item_in_manufacturing")
+
+ if frappe.db.has_column('BOM', 'allow_same_item_multiple_times'):
+ frappe.db.sql(""" UPDATE tabBOM
+ SET
+ allow_same_item_multiple_times = 0
+ WHERE
+ trim(coalesce(allow_same_item_multiple_times, '')) = '' """)
for doctype in ['BOM', 'Work Order']:
frappe.reload_doc('manufacturing', 'doctype', frappe.scrub(doctype))
diff --git a/erpnext/patches/v11_0/skip_user_permission_check_for_department.py b/erpnext/patches/v11_0/skip_user_permission_check_for_department.py
index 123eed5..7f7cfc1 100644
--- a/erpnext/patches/v11_0/skip_user_permission_check_for_department.py
+++ b/erpnext/patches/v11_0/skip_user_permission_check_for_department.py
@@ -1,28 +1,60 @@
import frappe
+from frappe.desk.form.linked_with import get_linked_doctypes
# Skips user permission check for doctypes where department link field was recently added
# https://github.com/frappe/erpnext/pull/14121
def execute():
- user_permissions = frappe.get_all("User Permission",
- filters=[['allow', '=', 'Department']],
- fields=['name', 'skip_for_doctype'])
+ doctypes_to_skip = []
+ for doctype in ['Appraisal', 'Leave Allocation', 'Expense Claim', 'Instructor', 'Salary Slip',
+ 'Attendance', 'Training Feedback', 'Training Result Employee',
+ 'Leave Application', 'Employee Advance', 'Activity Cost', 'Training Event Employee',
+ 'Timesheet', 'Sales Person', 'Payroll Employee Detail']:
+ if frappe.db.exists('Custom Field', { 'dt': doctype, 'fieldname': 'department'}): continue
+ doctypes_to_skip.append(doctype)
- doctypes_to_skip = []
+ frappe.reload_doctype('User Permission')
- for doctype in ['Appraisal', 'Leave Allocation', 'Expense Claim', 'Instructor', 'Salary Slip',
- 'Attendance', 'Training Feedback', 'Training Result Employee',
- 'Leave Application', 'Employee Advance', 'Activity Cost', 'Training Event Employee',
- 'Timesheet', 'Sales Person', 'Payroll Employee Detail']:
- if frappe.db.exists('Custom Field', { 'dt': doctype, 'fieldname': 'department'}): continue
- doctypes_to_skip.append(doctype)
+ user_permissions = frappe.get_all("User Permission",
+ filters=[['allow', '=', 'Department'], ['applicable_for', 'in', [None] + doctypes_to_skip]],
+ fields=['name', 'applicable_for'])
- for perm in user_permissions:
- skip_for_doctype = perm.get('skip_for_doctype')
+ user_permissions_to_delete = []
+ new_user_permissions_list = []
- skip_for_doctype = skip_for_doctype.split('\n') + doctypes_to_skip
- skip_for_doctype = set(skip_for_doctype) # to remove duplicates
- skip_for_doctype = '\n'.join(skip_for_doctype) # convert back to string
+ for user_permission in user_permissions:
+ if user_permission.applicable_for:
+ # simply delete user permission record since it needs to be skipped.
+ user_permissions_to_delete.append(user_permission.name)
+ else:
+ # if applicable_for is `None` it means that user permission is applicable for every doctype
+ # to avoid this we need to create other user permission records and only skip the listed doctypes in this patch
+ linked_doctypes = get_linked_doctypes(user_permission.allow, True).keys()
+ applicable_for_doctypes = list(set(linked_doctypes) - set(doctypes_to_skip))
- frappe.set_value('User Permission', perm.name, 'skip_for_doctype', skip_for_doctype)
+ user_permissions_to_delete.append(user_permission.name)
+ for doctype in applicable_for_doctypes:
+ if doctype:
+ # Maintain sequence (name, user, allow, for_value, applicable_for, apply_to_all_doctypes)
+ new_user_permissions_list.append((
+ frappe.generate_hash("", 10),
+ user_permission.user,
+ user_permission.allow,
+ user_permission.for_value,
+ doctype,
+ 0
+ ))
+
+ if new_user_permissions_list:
+ frappe.db.sql('''
+ INSERT INTO `tabUser Permission`
+ (`name`, `user`, `allow`, `for_value`, `applicable_for`, `apply_to_all_doctypes`)
+ VALUES {}'''.format(', '.join(['%s'] * len(new_user_permissions_list))), # nosec
+ tuple(new_user_permissions_list)
+ )
+
+ if user_permissions_to_delete:
+ frappe.db.sql('DELETE FROM `tabUser Permission` WHERE `name` IN ({})'.format( # nosec
+ ','.join(['%s'] * len(user_permissions_to_delete))
+ ), tuple(user_permissions_to_delete))
\ No newline at end of file
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index 3913178..de45ec3 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -258,13 +258,13 @@
self.total_purchase_cost = total_purchase_cost and total_purchase_cost[0][0] or 0
def update_sales_amount(self):
- total_sales_amount = frappe.db.sql("""select sum(base_grand_total)
+ total_sales_amount = frappe.db.sql("""select sum(base_net_total)
from `tabSales Order` where project = %s and docstatus=1""", self.name)
self.total_sales_amount = total_sales_amount and total_sales_amount[0][0] or 0
def update_billed_amount(self):
- total_billed_amount = frappe.db.sql("""select sum(base_grand_total)
+ total_billed_amount = frappe.db.sql("""select sum(base_net_total)
from `tabSales Invoice` where project = %s and docstatus=1""", self.name)
self.total_billed_amount = total_billed_amount and total_billed_amount[0][0] or 0
diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js
index 82d6f6e..9beba6a 100644
--- a/erpnext/public/js/setup_wizard.js
+++ b/erpnext/public/js/setup_wizard.js
@@ -97,6 +97,9 @@
if (!this.values.company_abbr) {
return false;
}
+ if (this.values.company_abbr.length > 5) {
+ return false;
+ }
return true;
}
},
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 0784b02..adb7c6d 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -237,7 +237,7 @@
let unscrub_option = frappe.model.unscrub(option);
let user_permission = frappe.defaults.get_user_permissions();
if(user_permission && user_permission[unscrub_option]) {
- return user_permission[unscrub_option]["docs"];
+ return user_permission[unscrub_option].map(perm => perm.doc);
} else {
return $.map(locals[`:${unscrub_option}`], function(c) { return c.name; }).sort();
}
diff --git a/erpnext/selling/README.md b/erpnext/selling/README.md
index db05132..d186133 100644
--- a/erpnext/selling/README.md
+++ b/erpnext/selling/README.md
@@ -1,6 +1,11 @@
-Selling management module. Includes forms for capturing / managing the sales process.
+Selling management module. Includes forms for capturing / managing the sales process:
+
+- Customer
+- Campaign
+- Quotation
+- Sales Order
+
+Moved to CRM Module:
- Lead
- Opportunity
-- Quotation
-- Sales Order
\ No newline at end of file
diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json
index 3b1c480..7ea8397 100644
--- a/erpnext/selling/doctype/quotation_item/quotation_item.json
+++ b/erpnext/selling/doctype/quotation_item/quotation_item.json
@@ -871,10 +871,12 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -993,6 +995,7 @@
"label": "Net Rate",
"length": 0,
"no_copy": 0,
+ "options": "currency",
"permlevel": 0,
"precision": "",
"print_hide": 1,
@@ -1910,7 +1913,7 @@
"istable": 1,
"max_attachments": 0,
"menu_index": 0,
- "modified": "2018-08-22 16:15:52.750381",
+ "modified": "2018-12-12 05:52:46.135944",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation Item",
@@ -1925,4 +1928,4 @@
"track_changes": 1,
"track_seen": 0,
"track_views": 0
-}
\ No newline at end of file
+}
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 9a35aed..229f4f6 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -623,7 +623,7 @@
def update_item(source, target, source_parent):
target.amount = flt(source.amount) - flt(source.billed_amt)
target.base_amount = target.amount * flt(source_parent.conversion_rate)
- target.qty = target.amount / flt(source.rate) if (source.rate and source.billed_amt) else source.qty
+ target.qty = target.amount / flt(source.rate) if (source.rate and source.billed_amt) else source.qty - source.returned_qty
if source_parent.project:
target.cost_center = frappe.db.get_value("Project", source_parent.project, "cost_center")
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index ab624d1..50996ed 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -391,8 +391,24 @@
return invoiced_qty_map
+def get_returned_qty_map(sales_orders):
+ """returns a map: {so_detail: returned_qty}"""
+ returned_qty_map = {}
+
+ for name, returned_qty in frappe.get_all('Sales Order Item', fields = ["name", "returned_qty"],
+ filters = {'parent': ('in', sales_orders), 'docstatus': 1}, as_list=1):
+ if not returned_qty_map.get(name):
+ returned_qty_map[name] = 0
+ returned_qty_map[name] += returned_qty
+
+ return returned_qty_map
+
@frappe.whitelist()
def make_sales_invoice(source_name, target_doc=None):
+ doc = frappe.get_doc('Delivery Note', source_name)
+ sales_orders = [d.against_sales_order for d in doc.items]
+ returned_qty_map = get_returned_qty_map(sales_orders)
+
invoiced_qty_map = get_invoiced_qty_map(source_name)
def set_missing_values(source, target):
@@ -412,7 +428,9 @@
target.update(get_fetch_values("Sales Invoice", 'company_address', target.company_address))
def update_item(source_doc, target_doc, source_parent):
- target_doc.qty = source_doc.qty - invoiced_qty_map.get(source_doc.name, 0)
+ target_doc.qty = (source_doc.qty -
+ invoiced_qty_map.get(source_doc.name, 0) - returned_qty_map.get(source_doc.so_detail, 0))
+
if source_doc.serial_no and source_parent.per_billed > 0:
target_doc.serial_no = get_delivery_note_serial_no(source_doc.item_code,
target_doc.qty, source_parent.name)
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js
index 6a50c5a..6fc51ec 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js
@@ -1,7 +1,8 @@
frappe.listview_settings['Delivery Note'] = {
- add_fields: ["grand_total", "is_return", "per_billed", "status", "currency"],
- get_indicator: function (doc) {
- if (cint(doc.is_return) == 1) {
+ add_fields: ["customer", "customer_name", "base_grand_total", "per_installed", "per_billed",
+ "transporter_name", "grand_total", "is_return", "status", "currency"],
+ get_indicator: function(doc) {
+ if(cint(doc.is_return)==1) {
return [__("Return"), "darkgrey", "is_return,=,Yes"];
} else if (doc.status === "Closed") {
return [__("Closed"), "green", "status,=,Closed"];
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 026d83c..0c5a71c 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -636,6 +636,24 @@
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
set_perpetual_inventory(0, company)
+
+ def test_make_sales_invoice_from_dn_for_returned_qty(self):
+ from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
+ from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
+
+ so = make_sales_order(qty=2)
+ so.submit()
+
+ dn = make_delivery_note(so.name)
+ dn.submit()
+
+ dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-1, do_not_submit=True)
+ dn1.items[0].against_sales_order = so.name
+ dn1.items[0].so_detail = so.items[0].name
+ dn1.submit()
+
+ si = make_sales_invoice(dn.name)
+ self.assertEquals(si.items[0].qty, 1)
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js
index e1d5b08..e81f323 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js
@@ -1,7 +1,8 @@
frappe.listview_settings['Purchase Receipt'] = {
- add_fields: ["is_return", "grand_total", "status", "per_billed"],
- get_indicator: function (doc) {
- if (cint(doc.is_return) == 1) {
+ add_fields: ["supplier", "supplier_name", "base_grand_total", "is_subcontracted",
+ "transporter_name", "is_return", "status", "per_billed", "currency"],
+ get_indicator: function(doc) {
+ if(cint(doc.is_return)==1) {
return [__("Return"), "darkgrey", "is_return,=,Yes"];
} else if (doc.status === "Closed") {
return [__("Closed"), "green", "status,=,Closed"];
diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py
index 318a8e0..ebcb106 100644
--- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py
+++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py
@@ -94,11 +94,10 @@
filters["company"] = frappe.defaults.get_user_default("Company")
def get_warehouse_list(filters):
- from frappe.defaults import get_user_permissions
+ from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
+
condition = ''
- user_permitted_warehouse = filter(None, get_user_permissions()
- .get("Warehouse", {})
- .get("docs", []))
+ user_permitted_warehouse = get_permitted_documents('Warehouse')
value = ()
if user_permitted_warehouse:
condition = "and name in %s"