Merge branch 'develop' into fix-dn-mapping
diff --git a/.flake8 b/.flake8
index 56c9b9a..5735456 100644
--- a/.flake8
+++ b/.flake8
@@ -28,6 +28,7 @@
B007,
B950,
W191,
+ E124, # closing bracket, irritating while writing QB code
max-line-length = 200
exclude=.github/helper/semgrep_rules
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 44cea31..51e1d6e 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -2,9 +2,10 @@
# For license information, please see license.txt
+from functools import reduce
+
import frappe
from frappe.utils import flt
-from six.moves import reduce
from erpnext.controllers.status_updater import StatusUpdater
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
index ddb833f..19d8d49 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
@@ -135,7 +135,7 @@
default_uom = frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos")
rate = flt(row.outstanding_amount) / flt(row.qty)
- return frappe._dict({
+ item_dict = frappe._dict({
"uom": default_uom,
"rate": rate or 0.0,
"qty": row.qty,
@@ -146,6 +146,13 @@
"cost_center": cost_center
})
+ for dimension in get_accounting_dimensions():
+ item_dict.update({
+ dimension: row.get(dimension)
+ })
+
+ return item_dict
+
item = get_item_dict()
invoice = frappe._dict({
@@ -166,7 +173,7 @@
accounting_dimension = get_accounting_dimensions()
for dimension in accounting_dimension:
invoice.update({
- dimension: item.get(dimension)
+ dimension: self.get(dimension) or item.get(dimension)
})
return invoice
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
index b5aae98..6700e9b 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
@@ -7,21 +7,26 @@
from frappe.cache_manager import clear_doctype_cache
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
+from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
+ create_dimension,
+ disable_dimension,
+)
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
get_temporary_opening_account,
)
-test_dependencies = ["Customer", "Supplier"]
+test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
class TestOpeningInvoiceCreationTool(unittest.TestCase):
def setUp(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
make_company()
+ create_dimension()
- def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None):
+ def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None, department=None):
doc = frappe.get_single("Opening Invoice Creation Tool")
args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
- party_1=party_1, party_2=party_2, invoice_number=invoice_number)
+ party_1=party_1, party_2=party_2, invoice_number=invoice_number, department=department)
doc.update(args)
return doc.make_invoices()
@@ -106,6 +111,19 @@
doc = frappe.get_doc('Sales Invoice', inv)
doc.cancel()
+ def test_opening_invoice_with_accounting_dimension(self):
+ invoices = self.make_invoices(invoice_type="Sales", company="_Test Opening Invoice Company", department='Sales - _TOIC')
+
+ expected_value = {
+ "keys": ["customer", "outstanding_amount", "status", "department"],
+ 0: ["_Test Customer", 300, "Overdue", "Sales - _TOIC"],
+ 1: ["_Test Customer 1", 250, "Overdue", "Sales - _TOIC"],
+ }
+ self.check_expected_values(invoices, expected_value, invoice_type="Sales")
+
+ def tearDown(self):
+ disable_dimension()
+
def get_opening_invoice_creation_dict(**args):
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
company = args.get("company", "_Test Company")
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index 6b4b43d..c13bc23 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -58,7 +58,7 @@
frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError)
party = frappe.get_doc(party_type, party)
- currency = party.default_currency if party.get("default_currency") else get_company_currency(company)
+ currency = party.get("default_currency") or currency or get_company_currency(company)
party_address, shipping_address = set_address_details(party_details, party, party_type, doctype, company, party_address, company_address, shipping_address)
set_contact_details(party_details, party, party_type)
diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
index 4559fa9..8e3bd8b 100644
--- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
+++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
@@ -5,7 +5,6 @@
import frappe
from frappe import _, scrub
from frappe.utils import cint, flt
-from six import iteritems
from erpnext.accounts.party import get_partywise_advanced_payment_amount
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
@@ -40,7 +39,7 @@
if self.filters.show_gl_balance:
gl_balance_map = get_gl_balance(self.filters.report_date)
- for party, party_dict in iteritems(self.party_total):
+ for party, party_dict in self.party_total.items():
if party_dict.outstanding == 0:
continue
diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py
index 1de6fb6..86eb213 100644
--- a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py
+++ b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py
@@ -17,10 +17,42 @@
class TestDeferredRevenueAndExpense(unittest.TestCase):
@classmethod
def setUpClass(self):
- clear_old_entries()
+ clear_accounts_and_items()
create_company()
+ self.maxDiff = None
+
+ def clear_old_entries(self):
+ sinv = qb.DocType("Sales Invoice")
+ sinv_item = qb.DocType("Sales Invoice Item")
+ pinv = qb.DocType("Purchase Invoice")
+ pinv_item = qb.DocType("Purchase Invoice Item")
+
+ # delete existing invoices with deferred items
+ deferred_invoices = (
+ qb.from_(sinv)
+ .join(sinv_item)
+ .on(sinv.name == sinv_item.parent)
+ .select(sinv.name)
+ .where(sinv_item.enable_deferred_revenue == 1)
+ .run()
+ )
+ if deferred_invoices:
+ qb.from_(sinv).delete().where(sinv.name.isin(deferred_invoices)).run()
+
+ deferred_invoices = (
+ qb.from_(pinv)
+ .join(pinv_item)
+ .on(pinv.name == pinv_item.parent)
+ .select(pinv.name)
+ .where(pinv_item.enable_deferred_expense == 1)
+ .run()
+ )
+ if deferred_invoices:
+ qb.from_(pinv).delete().where(pinv.name.isin(deferred_invoices)).run()
def test_deferred_revenue(self):
+ self.clear_old_entries()
+
# created deferred expense accounts, if not found
deferred_revenue_account = create_account(
account_name="Deferred Revenue",
@@ -108,6 +140,8 @@
self.assertEqual(report.period_total, expected)
def test_deferred_expense(self):
+ self.clear_old_entries()
+
# created deferred expense accounts, if not found
deferred_expense_account = create_account(
account_name="Deferred Expense",
@@ -198,6 +232,91 @@
]
self.assertEqual(report.period_total, expected)
+ def test_zero_months(self):
+ self.clear_old_entries()
+ # created deferred expense accounts, if not found
+ deferred_revenue_account = create_account(
+ account_name="Deferred Revenue",
+ parent_account="Current Liabilities - _CD",
+ company="_Test Company DR",
+ )
+
+ acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
+ acc_settings.book_deferred_entries_based_on = "Months"
+ acc_settings.save()
+
+ customer = frappe.new_doc("Customer")
+ customer.customer_name = "_Test Customer DR"
+ customer.type = "Individual"
+ customer.insert()
+
+ item = create_item(
+ "_Test Internet Subscription",
+ is_stock_item=0,
+ warehouse="All Warehouses - _CD",
+ company="_Test Company DR",
+ )
+ item.enable_deferred_revenue = 1
+ item.deferred_revenue_account = deferred_revenue_account
+ item.no_of_months = 0
+ item.save()
+
+ si = create_sales_invoice(
+ item=item.name,
+ company="_Test Company DR",
+ customer="_Test Customer DR",
+ debit_to="Debtors - _CD",
+ posting_date="2021-05-01",
+ parent_cost_center="Main - _CD",
+ cost_center="Main - _CD",
+ do_not_submit=True,
+ rate=300,
+ price_list_rate=300,
+ )
+ si.items[0].enable_deferred_revenue = 1
+ si.items[0].deferred_revenue_account = deferred_revenue_account
+ si.items[0].income_account = "Sales - _CD"
+ si.save()
+ si.submit()
+
+ pda = frappe.get_doc(
+ dict(
+ doctype="Process Deferred Accounting",
+ posting_date=nowdate(),
+ start_date="2021-05-01",
+ end_date="2021-08-01",
+ type="Income",
+ company="_Test Company DR",
+ )
+ )
+ pda.insert()
+ pda.submit()
+
+ # execute report
+ fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year"))
+ self.filters = frappe._dict(
+ {
+ "company": frappe.defaults.get_user_default("Company"),
+ "filter_based_on": "Date Range",
+ "period_start_date": "2021-05-01",
+ "period_end_date": "2021-08-01",
+ "from_fiscal_year": fiscal_year.year,
+ "to_fiscal_year": fiscal_year.year,
+ "periodicity": "Monthly",
+ "type": "Revenue",
+ "with_upcoming_postings": False,
+ }
+ )
+
+ report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
+ report.run()
+ expected = [
+ {"key": "may_2021", "total": 300.0, "actual": 300.0},
+ {"key": "jun_2021", "total": 0, "actual": 0},
+ {"key": "jul_2021", "total": 0, "actual": 0},
+ {"key": "aug_2021", "total": 0, "actual": 0},
+ ]
+ self.assertEqual(report.period_total, expected)
def create_company():
company = frappe.db.exists("Company", "_Test Company DR")
@@ -209,15 +328,11 @@
company.insert()
-def clear_old_entries():
+def clear_accounts_and_items():
item = qb.DocType("Item")
account = qb.DocType("Account")
customer = qb.DocType("Customer")
supplier = qb.DocType("Supplier")
- sinv = qb.DocType("Sales Invoice")
- sinv_item = qb.DocType("Sales Invoice Item")
- pinv = qb.DocType("Purchase Invoice")
- pinv_item = qb.DocType("Purchase Invoice Item")
qb.from_(account).delete().where(
(account.account_name == "Deferred Revenue")
@@ -228,26 +343,3 @@
).run()
qb.from_(customer).delete().where(customer.customer_name == "_Test Customer DR").run()
qb.from_(supplier).delete().where(supplier.supplier_name == "_Test Furniture Supplier").run()
-
- # delete existing invoices with deferred items
- deferred_invoices = (
- qb.from_(sinv)
- .join(sinv_item)
- .on(sinv.name == sinv_item.parent)
- .select(sinv.name)
- .where(sinv_item.enable_deferred_revenue == 1)
- .run()
- )
- if deferred_invoices:
- qb.from_(sinv).delete().where(sinv.name.isin(deferred_invoices)).run()
-
- deferred_invoices = (
- qb.from_(pinv)
- .join(pinv_item)
- .on(pinv.name == pinv_item.parent)
- .select(pinv.name)
- .where(pinv_item.enable_deferred_expense == 1)
- .run()
- )
- if deferred_invoices:
- qb.from_(pinv).delete().where(pinv.name.isin(deferred_invoices)).run()
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index f088b9f..a181af7 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -70,9 +70,18 @@
# set contact and address details for supplier, if they are not mentioned
if getattr(self, "supplier", None):
- self.update_if_missing(get_party_details(self.supplier, party_type="Supplier", ignore_permissions=self.flags.ignore_permissions,
- doctype=self.doctype, company=self.company, party_address=self.supplier_address, shipping_address=self.get('shipping_address'),
- fetch_payment_terms_template= not self.get('ignore_default_payment_terms_template')))
+ self.update_if_missing(
+ get_party_details(
+ self.supplier,
+ party_type="Supplier",
+ doctype=self.doctype,
+ company=self.company,
+ party_address=self.get("supplier_address"),
+ shipping_address=self.get('shipping_address'),
+ fetch_payment_terms_template= not self.get('ignore_default_payment_terms_template'),
+ ignore_permissions=self.flags.ignore_permissions
+ )
+ )
self.set_missing_item_details(for_validate)
diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
index 8fd4978..d2ac10a 100644
--- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
+++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
@@ -2,13 +2,14 @@
# For license information, please see license.txt
+from urllib.parse import urlencode
+
import frappe
import requests
from frappe import _
from frappe.model.document import Document
from frappe.utils import get_url_to_form
from frappe.utils.file_manager import get_file_path
-from six.moves.urllib.parse import urlencode
class LinkedInSettings(Document):
diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py
index 66826ba..03c1a1a 100644
--- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py
+++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py
@@ -5,11 +5,11 @@
import csv
import math
import time
+from io import StringIO
import dateutil
import frappe
from frappe import _
-from six import StringIO
import erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_api as mws
diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
index e242ace..a8119ac 100644
--- a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
+++ b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
@@ -2,13 +2,14 @@
# For license information, please see license.txt
+from urllib.parse import urlencode
+
import frappe
import gocardless_pro
from frappe import _
from frappe.integrations.utils import create_payment_gateway, create_request_log
from frappe.model.document import Document
from frappe.utils import call_hook_method, cint, flt, get_url
-from six.moves.urllib.parse import urlencode
class GoCardlessSettings(Document):
diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py
index 8da52f4..309d2cb 100644
--- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py
+++ b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py
@@ -2,12 +2,13 @@
# For license information, please see license.txt
+from urllib.parse import urlparse
+
import frappe
from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.model.document import Document
from frappe.utils.nestedset import get_root_of
-from six.moves.urllib.parse import urlparse
class WoocommerceSettings(Document):
diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py
index d922d87..30d3948 100644
--- a/erpnext/erpnext_integrations/utils.py
+++ b/erpnext/erpnext_integrations/utils.py
@@ -1,10 +1,10 @@
import base64
import hashlib
import hmac
+from urllib.parse import urlparse
import frappe
from frappe import _
-from six.moves.urllib.parse import urlparse
from erpnext import get_default_company
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 1dc5b31..70250f5 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -22,6 +22,7 @@
from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry
from erpnext.hr.utils import (
+ get_holiday_dates_for_employee,
get_leave_period,
set_employee_name,
share_doc_with_approver,
@@ -159,33 +160,57 @@
.format(formatdate(future_allocation[0].from_date), future_allocation[0].name))
def update_attendance(self):
- if self.status == "Approved":
- for dt in daterange(getdate(self.from_date), getdate(self.to_date)):
- date = dt.strftime("%Y-%m-%d")
- status = "Half Day" if self.half_day_date and getdate(date) == getdate(self.half_day_date) else "On Leave"
- attendance_name = frappe.db.exists('Attendance', dict(employee = self.employee,
- attendance_date = date, docstatus = ('!=', 2)))
+ if self.status != "Approved":
+ return
+ holiday_dates = []
+ if not frappe.db.get_value("Leave Type", self.leave_type, "include_holiday"):
+ holiday_dates = get_holiday_dates_for_employee(self.employee, self.from_date, self.to_date)
+
+ for dt in daterange(getdate(self.from_date), getdate(self.to_date)):
+ date = dt.strftime("%Y-%m-%d")
+ attendance_name = frappe.db.exists("Attendance", dict(employee = self.employee,
+ attendance_date = date, docstatus = ('!=', 2)))
+
+ # don't mark attendance for holidays
+ # if leave type does not include holidays within leaves as leaves
+ if date in holiday_dates:
if attendance_name:
- # update existing attendance, change absent to on leave
- doc = frappe.get_doc('Attendance', attendance_name)
- if doc.status != status:
- doc.db_set('status', status)
- doc.db_set('leave_type', self.leave_type)
- doc.db_set('leave_application', self.name)
- else:
- # make new attendance and submit it
- doc = frappe.new_doc("Attendance")
- doc.employee = self.employee
- doc.employee_name = self.employee_name
- doc.attendance_date = date
- doc.company = self.company
- doc.leave_type = self.leave_type
- doc.leave_application = self.name
- doc.status = status
- doc.flags.ignore_validate = True
- doc.insert(ignore_permissions=True)
- doc.submit()
+ # cancel and delete existing attendance for holidays
+ attendance = frappe.get_doc("Attendance", attendance_name)
+ attendance.flags.ignore_permissions = True
+ if attendance.docstatus == 1:
+ attendance.cancel()
+ frappe.delete_doc("Attendance", attendance_name, force=1)
+ continue
+
+ self.create_or_update_attendance(attendance_name, date)
+
+ def create_or_update_attendance(self, attendance_name, date):
+ status = "Half Day" if self.half_day_date and getdate(date) == getdate(self.half_day_date) else "On Leave"
+
+ if attendance_name:
+ # update existing attendance, change absent to on leave
+ doc = frappe.get_doc('Attendance', attendance_name)
+ if doc.status != status:
+ doc.db_set({
+ 'status': status,
+ 'leave_type': self.leave_type,
+ 'leave_application': self.name
+ })
+ else:
+ # make new attendance and submit it
+ doc = frappe.new_doc("Attendance")
+ doc.employee = self.employee
+ doc.employee_name = self.employee_name
+ doc.attendance_date = date
+ doc.company = self.company
+ doc.leave_type = self.leave_type
+ doc.leave_application = self.name
+ doc.status = status
+ doc.flags.ignore_validate = True
+ doc.insert(ignore_permissions=True)
+ doc.submit()
def cancel_attendance(self):
if self.docstatus == 2:
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 9b8d638..75e99f8 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -5,7 +5,16 @@
import frappe
from frappe.permissions import clear_user_permissions_for_doctype
-from frappe.utils import add_days, add_months, getdate, nowdate
+from frappe.utils import (
+ add_days,
+ add_months,
+ get_first_day,
+ get_last_day,
+ get_year_ending,
+ get_year_start,
+ getdate,
+ nowdate,
+)
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
@@ -19,6 +28,10 @@
create_assignment_for_multiple_employees,
)
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
+from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
+ make_holiday_list,
+ make_leave_application,
+)
test_dependencies = ["Leave Allocation", "Leave Block List", "Employee"]
@@ -61,13 +74,15 @@
for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]:
frappe.db.sql("DELETE FROM `tab%s`" % dt) #nosec
+ frappe.set_user("Administrator")
+
@classmethod
def setUpClass(cls):
set_leave_approver()
frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'")
def tearDown(self):
- frappe.set_user("Administrator")
+ frappe.db.rollback()
def _clear_roles(self):
frappe.db.sql("""delete from `tabHas Role` where parent in
@@ -106,6 +121,72 @@
for d in ('2018-01-01', '2018-01-02', '2018-01-03'):
self.assertTrue(getdate(d) in dates)
+ def test_attendance_for_include_holidays(self):
+ # Case 1: leave type with 'Include holidays within leaves as leaves' enabled
+ frappe.delete_doc_if_exists("Leave Type", "Test Include Holidays", force=1)
+ leave_type = frappe.get_doc(dict(
+ leave_type_name="Test Include Holidays",
+ doctype="Leave Type",
+ include_holiday=True
+ )).insert()
+
+ date = getdate()
+ make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
+
+ holiday_list = make_holiday_list()
+ frappe.db.set_value("Company", "_Test Company", "default_holiday_list", holiday_list)
+ first_sunday = get_first_sunday(holiday_list)
+
+ leave_application = make_leave_application("_T-Employee-00001", first_sunday, add_days(first_sunday, 3), leave_type.name)
+ leave_application.reload()
+ self.assertEqual(leave_application.total_leave_days, 4)
+ self.assertEqual(frappe.db.count('Attendance', {'leave_application': leave_application.name}), 4)
+
+ leave_application.cancel()
+
+ def test_attendance_update_for_exclude_holidays(self):
+ # Case 2: leave type with 'Include holidays within leaves as leaves' disabled
+ frappe.delete_doc_if_exists("Leave Type", "Test Do Not Include Holidays", force=1)
+ leave_type = frappe.get_doc(dict(
+ leave_type_name="Test Do Not Include Holidays",
+ doctype="Leave Type",
+ include_holiday=False
+ )).insert()
+
+ date = getdate()
+ make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
+
+ holiday_list = make_holiday_list()
+ frappe.db.set_value("Company", "_Test Company", "default_holiday_list", holiday_list)
+ first_sunday = get_first_sunday(holiday_list)
+
+ # already marked attendance on a holiday should be deleted in this case
+ config = {
+ "doctype": "Attendance",
+ "employee": "_T-Employee-00001",
+ "status": "Present"
+ }
+ attendance_on_holiday = frappe.get_doc(config)
+ attendance_on_holiday.attendance_date = first_sunday
+ attendance_on_holiday.save()
+
+ # already marked attendance on a non-holiday should be updated
+ attendance = frappe.get_doc(config)
+ attendance.attendance_date = add_days(first_sunday, 3)
+ attendance.save()
+
+ leave_application = make_leave_application("_T-Employee-00001", first_sunday, add_days(first_sunday, 3), leave_type.name)
+ leave_application.reload()
+ # holiday should be excluded while marking attendance
+ self.assertEqual(leave_application.total_leave_days, 3)
+ self.assertEqual(frappe.db.count("Attendance", {"leave_application": leave_application.name}), 3)
+
+ # attendance on holiday deleted
+ self.assertFalse(frappe.db.exists("Attendance", attendance_on_holiday.name))
+
+ # attendance on non-holiday updated
+ self.assertEqual(frappe.db.get_value("Attendance", attendance.name, "status"), "On Leave")
+
def test_block_list(self):
self._clear_roles()
@@ -241,7 +322,13 @@
leave_period = get_leave_period()
today = nowdate()
holiday_list = 'Test Holiday List for Optional Holiday'
- optional_leave_date = add_days(today, 7)
+ employee = get_employee()
+
+ default_holiday_list = make_holiday_list()
+ frappe.db.set_value("Company", "_Test Company", "default_holiday_list", default_holiday_list)
+ first_sunday = get_first_sunday(default_holiday_list)
+
+ optional_leave_date = add_days(first_sunday, 1)
if not frappe.db.exists('Holiday List', holiday_list):
frappe.get_doc(dict(
@@ -253,7 +340,6 @@
dict(holiday_date = optional_leave_date, description = 'Test')
]
)).insert()
- employee = get_employee()
frappe.db.set_value('Leave Period', leave_period.name, 'optional_holiday_list', holiday_list)
leave_type = 'Test Optional Type'
@@ -266,7 +352,7 @@
allocate_leaves(employee, leave_period, leave_type, 10)
- date = add_days(today, 6)
+ date = add_days(first_sunday, 2)
leave_application = frappe.get_doc(dict(
doctype = 'Leave Application',
@@ -637,13 +723,13 @@
carry_forward=1)
leave_allocation.submit()
-def make_allocation_record(employee=None, leave_type=None):
+def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None):
allocation = frappe.get_doc({
"doctype": "Leave Allocation",
"employee": employee or "_T-Employee-00001",
"leave_type": leave_type or "_Test Leave Type",
- "from_date": "2013-01-01",
- "to_date": "2019-12-31",
+ "from_date": from_date or "2013-01-01",
+ "to_date": to_date or "2019-12-31",
"new_leaves_allocated": 30
})
@@ -692,3 +778,16 @@
}).insert()
allocate_leave.submit()
+
+
+def get_first_sunday(holiday_list):
+ month_start_date = get_first_day(nowdate())
+ month_end_date = get_last_day(nowdate())
+ first_sunday = frappe.db.sql("""
+ select holiday_date from `tabHoliday`
+ where parent = %s
+ and holiday_date between %s and %s
+ order by holiday_date
+ """, (holiday_list, month_start_date, month_end_date))[0][0]
+
+ return first_sunday
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index 6d35d65..fc3b971 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -331,7 +331,7 @@
});
});
- if (has_template_rm) {
+ if (has_template_rm && has_template_rm.length) {
dialog.fields_dict.items.grid.refresh();
}
},
@@ -467,7 +467,8 @@
"uom": d.uom,
"stock_uom": d.stock_uom,
"conversion_factor": d.conversion_factor,
- "sourced_by_supplier": d.sourced_by_supplier
+ "sourced_by_supplier": d.sourced_by_supplier,
+ "do_not_explode": d.do_not_explode
},
callback: function(r) {
d = locals[cdt][cdn];
@@ -640,6 +641,13 @@
});
});
+frappe.ui.form.on("BOM Item", {
+ do_not_explode: function(frm, cdt, cdn) {
+ get_bom_material_detail(frm.doc, cdt, cdn, false);
+ }
+})
+
+
frappe.ui.form.on("BOM Item", "qty", function(frm, cdt, cdn) {
var d = locals[cdt][cdn];
d.stock_qty = d.qty * d.conversion_factor;
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 5a60fb7..045e5bc 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -149,6 +149,7 @@
self.set_bom_material_details()
self.set_bom_scrap_items_detail()
self.validate_materials()
+ self.validate_transfer_against()
self.set_routing_operations()
self.validate_operations()
self.calculate_cost()
@@ -203,6 +204,10 @@
for item in self.get("items"):
self.validate_bom_currency(item)
+ item.bom_no = ''
+ if not item.do_not_explode:
+ item.bom_no = item.bom_no
+
ret = self.get_bom_material_detail({
"company": self.company,
"item_code": item.item_code,
@@ -214,8 +219,10 @@
"uom": item.uom,
"stock_uom": item.stock_uom,
"conversion_factor": item.conversion_factor,
- "sourced_by_supplier": item.sourced_by_supplier
+ "sourced_by_supplier": item.sourced_by_supplier,
+ "do_not_explode": item.do_not_explode
})
+
for r in ret:
if not item.get(r):
item.set(r, ret[r])
@@ -267,6 +274,9 @@
'sourced_by_supplier' : args.get('sourced_by_supplier', 0)
}
+ if args.get('do_not_explode'):
+ ret_item['bom_no'] = ''
+
return ret_item
def validate_bom_currency(self, item):
@@ -681,6 +691,12 @@
if act_pbom and act_pbom[0][0]:
frappe.throw(_("Cannot deactivate or cancel BOM as it is linked with other BOMs"))
+ def validate_transfer_against(self):
+ if not self.with_operations:
+ self.transfer_material_against = "Work Order"
+ if not self.transfer_material_against and not self.is_new():
+ frappe.throw(_("Setting {} is required").format(self.meta.get_label("transfer_material_against")), title=_("Missing value"))
+
def set_routing_operations(self):
if self.routing and self.with_operations and not self.operations:
self.get_routing()
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 178d92c..53437c8 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -385,6 +385,53 @@
self.assertNotEqual(len(test_items), len(filtered), msg="Item filtering showing excessive results")
self.assertTrue(0 < len(filtered) <= 3, msg="Item filtering showing excessive results")
+ def test_exclude_exploded_items_from_bom(self):
+ bom_no = get_default_bom()
+ new_bom = frappe.copy_doc(frappe.get_doc('BOM', bom_no))
+ for row in new_bom.items:
+ if row.item_code == '_Test Item Home Desktop Manufactured':
+ self.assertTrue(row.bom_no)
+ row.do_not_explode = True
+
+ new_bom.docstatus = 0
+ new_bom.save()
+ new_bom.load_from_db()
+
+ for row in new_bom.items:
+ if row.item_code == '_Test Item Home Desktop Manufactured' and row.do_not_explode:
+ self.assertFalse(row.bom_no)
+
+ new_bom.delete()
+
+ def test_valid_transfer_defaults(self):
+ bom_with_op = frappe.db.get_value("BOM", {"item": "_Test FG Item 2", "with_operations": 1, "is_active": 1})
+ bom = frappe.copy_doc(frappe.get_doc("BOM", bom_with_op), ignore_no_copy=False)
+
+ # test defaults
+ bom.docstatus = 0
+ bom.transfer_material_against = None
+ bom.insert()
+ self.assertEqual(bom.transfer_material_against, "Work Order")
+
+ bom.reload()
+ bom.transfer_material_against = None
+ with self.assertRaises(frappe.ValidationError):
+ bom.save()
+ bom.reload()
+
+ # test saner default
+ bom.transfer_material_against = "Job Card"
+ bom.with_operations = 0
+ bom.save()
+ self.assertEqual(bom.transfer_material_against, "Work Order")
+
+ # test no value on existing doc
+ bom.transfer_material_against = None
+ bom.with_operations = 0
+ bom.save()
+ self.assertEqual(bom.transfer_material_against, "Work Order")
+ bom.delete()
+
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json
index 4c9877f..3406215 100644
--- a/erpnext/manufacturing/doctype/bom_item/bom_item.json
+++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json
@@ -10,6 +10,7 @@
"item_name",
"operation",
"column_break_3",
+ "do_not_explode",
"bom_no",
"source_warehouse",
"allow_alternative_item",
@@ -73,6 +74,7 @@
"fieldtype": "Column Break"
},
{
+ "depends_on": "eval:!doc.do_not_explode",
"fieldname": "bom_no",
"fieldtype": "Link",
"in_filter": 1,
@@ -284,18 +286,25 @@
"fieldname": "sourced_by_supplier",
"fieldtype": "Check",
"label": "Sourced by Supplier"
+ },
+ {
+ "default": "0",
+ "fieldname": "do_not_explode",
+ "fieldtype": "Check",
+ "label": "Do Not Explode"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-10-08 14:19:37.563300",
+ "modified": "2022-01-24 16:57:57.020232",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Item",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index e7eb9c6..a399edd 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -911,6 +911,54 @@
self.assertEqual(wo1.operations[0].time_in_mins, wo2.operations[0].time_in_mins)
+ def test_partial_manufacture_entries(self):
+ cancel_stock_entry = []
+
+ frappe.db.set_value("Manufacturing Settings", None,
+ "backflush_raw_materials_based_on", "Material Transferred for Manufacture")
+
+ wo_order = make_wo_order_test_record(planned_start_date=now(), qty=100)
+ ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item",
+ target="_Test Warehouse - _TC", qty=120, basic_rate=5000.0)
+ ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100",
+ target="_Test Warehouse - _TC", qty=240, basic_rate=1000.0)
+
+ cancel_stock_entry.extend([ste1.name, ste2.name])
+
+ sm = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 100))
+ for row in sm.get('items'):
+ if row.get('item_code') == '_Test Item':
+ row.qty = 110
+
+ sm.submit()
+ cancel_stock_entry.append(sm.name)
+
+ s = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 90))
+ for row in s.get('items'):
+ if row.get('item_code') == '_Test Item':
+ self.assertEqual(row.get('qty'), 100)
+ s.submit()
+ cancel_stock_entry.append(s.name)
+
+ s1 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 5))
+ for row in s1.get('items'):
+ if row.get('item_code') == '_Test Item':
+ self.assertEqual(row.get('qty'), 5)
+ s1.submit()
+ cancel_stock_entry.append(s1.name)
+
+ s2 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 5))
+ for row in s2.get('items'):
+ if row.get('item_code') == '_Test Item':
+ self.assertEqual(row.get('qty'), 5)
+
+ cancel_stock_entry.reverse()
+ for ste in cancel_stock_entry:
+ doc = frappe.get_doc("Stock Entry", ste)
+ doc.cancel()
+
+ frappe.db.set_value("Manufacturing Settings", None,
+ "backflush_raw_materials_based_on", "BOM")
def update_job_card(job_card, jc_qty=None):
employee = frappe.db.get_value('Employee', {'status': 'Active'}, 'name')
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json
index 12cd58f..9452a63 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.json
+++ b/erpnext/manufacturing/doctype/work_order/work_order.json
@@ -333,12 +333,13 @@
"options": "fa fa-wrench"
},
{
- "default": "Work Order",
"depends_on": "operations",
+ "fetch_from": "bom_no.transfer_material_against",
+ "fetch_if_empty": 1,
"fieldname": "transfer_material_against",
"fieldtype": "Select",
"label": "Transfer Material Against",
- "options": "Work Order\nJob Card"
+ "options": "\nWork Order\nJob Card"
},
{
"fieldname": "operations",
@@ -574,7 +575,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2021-11-08 17:36:07.016300",
+ "modified": "2022-01-24 21:18:12.160114",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",
@@ -607,6 +608,7 @@
],
"sort_field": "modified",
"sort_order": "ASC",
+ "states": [],
"title_field": "production_item",
"track_changes": 1,
"track_seen": 1
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 170454c..93ca805 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -65,6 +65,7 @@
self.validate_warehouse_belongs_to_company()
self.calculate_operating_cost()
self.validate_qty()
+ self.validate_transfer_against()
self.validate_operation_time()
self.status = self.get_status()
@@ -72,6 +73,7 @@
self.set_required_items(reset_only_qty = len(self.get("required_items")))
+
def validate_sales_order(self):
if self.sales_order:
self.check_sales_order_on_hold_or_close()
@@ -625,6 +627,16 @@
if not self.qty > 0:
frappe.throw(_("Quantity to Manufacture must be greater than 0."))
+ def validate_transfer_against(self):
+ if not self.docstatus == 1:
+ # let user configure operations until they're ready to submit
+ return
+ if not self.operations:
+ self.transfer_material_against = "Work Order"
+ if not self.transfer_material_against:
+ frappe.throw(_("Setting {} is required").format(self.meta.get_label("transfer_material_against")), title=_("Missing value"))
+
+
def validate_operation_time(self):
for d in self.operations:
if not d.time_in_mins > 0:
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 83bab06..ad5062f 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -1,3 +1,4 @@
+[pre_model_sync]
erpnext.patches.v12_0.update_is_cancelled_field
erpnext.patches.v11_0.rename_production_order_to_work_order
erpnext.patches.v11_0.refactor_naming_series
@@ -226,7 +227,6 @@
erpnext.patches.v13_0.update_reason_for_resignation_in_employee
execute:frappe.delete_doc("Report", "Quoted Item Comparison")
erpnext.patches.v13_0.update_member_email_address
-erpnext.patches.v13_0.updates_for_multi_currency_payroll
erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy
erpnext.patches.v13_0.update_pos_closing_entry_in_merge_log
erpnext.patches.v13_0.add_po_to_global_search
@@ -252,7 +252,7 @@
erpnext.patches.v12_0.purchase_receipt_status
erpnext.patches.v13_0.fix_non_unique_represents_company
erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing
-erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021
+erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021 #17-01-2022
erpnext.patches.v13_0.update_shipment_status
erpnext.patches.v13_0.remove_attribute_field_from_item_variant_setting
erpnext.patches.v13_0.germany_make_custom_fields
@@ -266,7 +266,6 @@
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
erpnext.patches.v13_0.update_response_by_variance
-erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.update_job_card_details
erpnext.patches.v13_0.update_level_in_bom #1234sswef
erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
@@ -285,11 +284,9 @@
erpnext.patches.v13_0.einvoicing_deprecation_warning
execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings")
execute:frappe.reload_doc("erpnext_integrations", "doctype", "Product Tax Category")
-erpnext.patches.v14_0.delete_einvoicing_doctypes
erpnext.patches.v13_0.custom_fields_for_taxjar_integration #08-11-2021
erpnext.patches.v13_0.set_operation_time_based_on_operating_cost
erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021
-erpnext.patches.v14_0.delete_shopify_doctypes
erpnext.patches.v13_0.fix_invoice_statuses
erpnext.patches.v13_0.replace_supplier_item_group_with_party_specific_item
erpnext.patches.v13_0.update_dates_in_tax_withholding_category
@@ -312,24 +309,29 @@
erpnext.patches.v14_0.delete_healthcare_doctypes
erpnext.patches.v13_0.update_category_in_ltds_certificate
erpnext.patches.v13_0.create_pan_field_for_india #2
-erpnext.patches.v14_0.delete_hub_doctypes
erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit
erpnext.patches.v13_0.create_ksa_vat_custom_fields # 07-01-2022
-erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
erpnext.patches.v14_0.migrate_crm_settings
erpnext.patches.v13_0.rename_ksa_qr_field
erpnext.patches.v13_0.wipe_serial_no_field_for_0_qty
erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021
-erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template
erpnext.patches.v13_0.update_tax_category_for_rcm
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
erpnext.patches.v14_0.set_payroll_cost_centers
erpnext.patches.v13_0.agriculture_deprecation_warning
-erpnext.patches.v14_0.delete_agriculture_doctypes
erpnext.patches.v13_0.hospitality_deprecation_warning
-erpnext.patches.v14_0.delete_hospitality_doctypes # 20-01-2022
erpnext.patches.v13_0.update_exchange_rate_settings
-erpnext.patches.v14_0.rearrange_company_fields
-erpnext.patches.v14_0.update_leave_notification_template
erpnext.patches.v13_0.update_asset_quantity_field
erpnext.patches.v13_0.delete_bank_reconciliation_detail
+
+[post_model_sync]
+erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
+erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template
+erpnext.patches.v14_0.delete_einvoicing_doctypes
+erpnext.patches.v14_0.delete_shopify_doctypes
+erpnext.patches.v14_0.delete_hub_doctypes
+erpnext.patches.v14_0.delete_hospitality_doctypes # 20-01-2022
+erpnext.patches.v14_0.delete_agriculture_doctypes
+erpnext.patches.v14_0.rearrange_company_fields
+erpnext.patches.v14_0.update_leave_notification_template
+erpnext.patches.v13_0.update_sane_transfer_against
diff --git a/erpnext/patches/v12_0/update_is_cancelled_field.py b/erpnext/patches/v12_0/update_is_cancelled_field.py
index df78750..0401034 100644
--- a/erpnext/patches/v12_0/update_is_cancelled_field.py
+++ b/erpnext/patches/v12_0/update_is_cancelled_field.py
@@ -2,14 +2,28 @@
def execute():
- try:
- frappe.db.sql("UPDATE `tabStock Ledger Entry` SET is_cancelled = 0 where is_cancelled in ('', NULL, 'No')")
- frappe.db.sql("UPDATE `tabSerial No` SET is_cancelled = 0 where is_cancelled in ('', NULL, 'No')")
+ #handle type casting for is_cancelled field
+ module_doctypes = (
+ ('stock', 'Stock Ledger Entry'),
+ ('stock', 'Serial No'),
+ ('accounts', 'GL Entry')
+ )
- frappe.db.sql("UPDATE `tabStock Ledger Entry` SET is_cancelled = 1 where is_cancelled = 'Yes'")
- frappe.db.sql("UPDATE `tabSerial No` SET is_cancelled = 1 where is_cancelled = 'Yes'")
+ for module, doctype in module_doctypes:
+ if (not frappe.db.has_column(doctype, "is_cancelled")
+ or frappe.db.get_column_type(doctype, "is_cancelled").lower() == "int(1)"
+ ):
+ continue
- frappe.reload_doc("stock", "doctype", "stock_ledger_entry")
- frappe.reload_doc("stock", "doctype", "serial_no")
- except Exception:
- pass
+ frappe.db.sql("""
+ UPDATE `tab{doctype}`
+ SET is_cancelled = 0
+ where is_cancelled in ('', NULL, 'No')"""
+ .format(doctype=doctype))
+ frappe.db.sql("""
+ UPDATE `tab{doctype}`
+ SET is_cancelled = 1
+ where is_cancelled = 'Yes'"""
+ .format(doctype=doctype))
+
+ frappe.reload_doc(module, "doctype", frappe.scrub(doctype))
diff --git a/erpnext/patches/v13_0/delete_old_sales_reports.py b/erpnext/patches/v13_0/delete_old_sales_reports.py
index c597fe8..e6eba0a 100644
--- a/erpnext/patches/v13_0/delete_old_sales_reports.py
+++ b/erpnext/patches/v13_0/delete_old_sales_reports.py
@@ -12,6 +12,7 @@
for report in reports_to_delete:
if frappe.db.exists("Report", report):
+ delete_links_from_desktop_icons(report)
delete_auto_email_reports(report)
check_and_delete_linked_reports(report)
@@ -22,3 +23,9 @@
auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"])
for auto_email_report in auto_email_reports:
frappe.delete_doc("Auto Email Report", auto_email_report[0])
+
+def delete_links_from_desktop_icons(report):
+ """ Check for one or multiple Desktop Icons and delete """
+ desktop_icons = frappe.db.get_values("Desktop Icon", {"_report": report}, ["name"])
+ for desktop_icon in desktop_icons:
+ frappe.delete_doc("Desktop Icon", desktop_icon[0])
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/make_non_standard_user_type.py b/erpnext/patches/v13_0/make_non_standard_user_type.py
index a7bdf93..ff241a3 100644
--- a/erpnext/patches/v13_0/make_non_standard_user_type.py
+++ b/erpnext/patches/v13_0/make_non_standard_user_type.py
@@ -10,8 +10,15 @@
def execute():
doctype_dict = {
'projects': ['Timesheet'],
- 'payroll': ['Salary Slip', 'Employee Tax Exemption Declaration', 'Employee Tax Exemption Proof Submission'],
- 'hr': ['Employee', 'Expense Claim', 'Leave Application', 'Attendance Request', 'Compensatory Leave Request']
+ 'payroll': [
+ 'Salary Slip', 'Employee Tax Exemption Declaration', 'Employee Tax Exemption Proof Submission',
+ 'Employee Benefit Application', 'Employee Benefit Claim'
+ ],
+ 'hr': [
+ 'Employee', 'Expense Claim', 'Leave Application', 'Attendance Request', 'Compensatory Leave Request',
+ 'Holiday List', 'Employee Advance', 'Training Program', 'Training Feedback',
+ 'Shift Request', 'Employee Grievance', 'Employee Referral', 'Travel Request'
+ ]
}
for module, doctypes in doctype_dict.items():
diff --git a/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py b/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py
index 7a2a253..2d35ea3 100644
--- a/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py
+++ b/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py
@@ -5,6 +5,9 @@
def execute():
if frappe.get_all('Company', filters = {'country': 'India'}):
+ frappe.reload_doc('accounts', 'doctype', 'POS Invoice')
+ frappe.reload_doc('accounts', 'doctype', 'POS Invoice Item')
+
make_custom_fields()
if not frappe.db.exists('Party Type', 'Donor'):
diff --git a/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py b/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py
index 55fd465..60466eb 100644
--- a/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py
+++ b/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py
@@ -37,4 +37,4 @@
jc.production_item = wo.production_item, jc.item_name = wo.item_name
WHERE
jc.work_order = wo.name and IFNULL(jc.production_item, "") = ""
- """)
+ """)
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/update_sane_transfer_against.py b/erpnext/patches/v13_0/update_sane_transfer_against.py
new file mode 100644
index 0000000..a163d38
--- /dev/null
+++ b/erpnext/patches/v13_0/update_sane_transfer_against.py
@@ -0,0 +1,11 @@
+import frappe
+
+
+def execute():
+ bom = frappe.qb.DocType("BOM")
+
+ (frappe.qb
+ .update(bom)
+ .set(bom.transfer_material_against, "Work Order")
+ .where(bom.with_operations == 0)
+ ).run()
diff --git a/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py b/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py
index 120182a..2a8b6ef 100644
--- a/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py
+++ b/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py
@@ -5,9 +5,6 @@
def execute():
- frappe.reload_doc("email", "doctype", "email_template")
- frappe.reload_doc("hr", "doctype", "hr_settings")
-
template = frappe.db.exists("Email Template", _("Exit Questionnaire Notification"))
if not template:
base_path = frappe.get_app_path("erpnext", "hr", "doctype")
diff --git a/erpnext/patches/v14_0/rearrange_company_fields.py b/erpnext/patches/v14_0/rearrange_company_fields.py
index dd953ff..fd7eb7f 100644
--- a/erpnext/patches/v14_0/rearrange_company_fields.py
+++ b/erpnext/patches/v14_0/rearrange_company_fields.py
@@ -1,10 +1,7 @@
-import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
def execute():
- frappe.reload_doc('setup', 'doctype', 'company')
-
custom_fields = {
'Company': [
dict(fieldname='hra_section', label='HRA Settings',
@@ -28,4 +25,4 @@
]
}
- create_custom_fields(custom_fields, update=True)
\ No newline at end of file
+ create_custom_fields(custom_fields, update=True)
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index c0e005a..bcf981b 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -994,6 +994,8 @@
))
leave_application.submit()
+ return leave_application
+
def setup_test():
make_earning_salary_component(setup=True, company_list=["_Test Company"])
make_deduction_salary_component(setup=True, company_list=["_Test Company"])
diff --git a/erpnext/regional/germany/utils/datev/datev_csv.py b/erpnext/regional/germany/utils/datev/datev_csv.py
index 2d1e02e..ec271a1 100644
--- a/erpnext/regional/germany/utils/datev/datev_csv.py
+++ b/erpnext/regional/germany/utils/datev/datev_csv.py
@@ -1,11 +1,11 @@
import datetime
import zipfile
from csv import QUOTE_NONNUMERIC
+from io import BytesIO
import frappe
import pandas as pd
from frappe import _
-from six import BytesIO
from .datev_constants import DataCategory
diff --git a/erpnext/regional/report/datev/test_datev.py b/erpnext/regional/report/datev/test_datev.py
index 14d5495..052fb2a 100644
--- a/erpnext/regional/report/datev/test_datev.py
+++ b/erpnext/regional/report/datev/test_datev.py
@@ -1,9 +1,9 @@
import zipfile
+from io import BytesIO
from unittest import TestCase
import frappe
from frappe.utils import cstr, now_datetime, today
-from six import BytesIO
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.regional.germany.utils.datev.datev_constants import (
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index b7f74df..d74d5a6 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -142,7 +142,7 @@
self.update_lead_status()
if self.flags.is_new_doc:
- self.create_lead_address_contact()
+ self.link_lead_address_and_contact()
self.update_customer_groups()
@@ -176,62 +176,24 @@
if self.lead_name:
frappe.db.set_value("Lead", self.lead_name, "status", "Converted")
- def create_lead_address_contact(self):
+ def link_lead_address_and_contact(self):
if self.lead_name:
- # assign lead address to customer (if already not set)
- address_names = frappe.get_all('Dynamic Link', filters={
- "parenttype":"Address",
- "link_doctype":"Lead",
- "link_name":self.lead_name
- }, fields=["parent as name"])
+ # assign lead address and contact to customer (if already not set)
+ linked_contacts_and_addresses = frappe.get_all(
+ "Dynamic Link",
+ filters=[
+ ["parenttype", "in", ["Contact", "Address"]],
+ ["link_doctype", "=", "Lead"],
+ ["link_name", "=", self.lead_name],
+ ],
+ fields=["parent as name", "parenttype as doctype"],
+ )
- for address_name in address_names:
- address = frappe.get_doc('Address', address_name.get('name'))
- if not address.has_link('Customer', self.name):
- address.append('links', dict(link_doctype='Customer', link_name=self.name))
- address.save(ignore_permissions=self.flags.ignore_permissions)
-
- lead = frappe.db.get_value("Lead", self.lead_name, ["company_name", "lead_name", "email_id", "phone", "mobile_no", "gender", "salutation"], as_dict=True)
-
- if not lead.lead_name:
- frappe.throw(_("Please mention the Lead Name in Lead {0}").format(self.lead_name))
-
- contact_names = frappe.get_all('Dynamic Link', filters={
- "parenttype":"Contact",
- "link_doctype":"Lead",
- "link_name":self.lead_name
- }, fields=["parent as name"])
-
- for contact_name in contact_names:
- contact = frappe.get_doc('Contact', contact_name.get('name'))
- if not contact.has_link('Customer', self.name):
- contact.append('links', dict(link_doctype='Customer', link_name=self.name))
- contact.save(ignore_permissions=self.flags.ignore_permissions)
-
- if not contact_names:
- lead.lead_name = lead.lead_name.lstrip().split(" ")
- lead.first_name = lead.lead_name[0]
- lead.last_name = " ".join(lead.lead_name[1:])
-
- # create contact from lead
- contact = frappe.new_doc('Contact')
- contact.first_name = lead.first_name
- contact.last_name = lead.last_name
- contact.gender = lead.gender
- contact.salutation = lead.salutation
- contact.email_id = lead.email_id
- contact.phone = lead.phone
- contact.mobile_no = lead.mobile_no
- contact.is_primary_contact = 1
- contact.append('links', dict(link_doctype='Customer', link_name=self.name))
- if lead.email_id:
- contact.append('email_ids', dict(email_id=lead.email_id, is_primary=1))
- if lead.mobile_no:
- contact.append('phone_nos', dict(phone=lead.mobile_no, is_primary_mobile_no=1))
- contact.flags.ignore_permissions = self.flags.ignore_permissions
- contact.autoname()
- if not frappe.db.exists("Contact", contact.name):
- contact.insert()
+ for row in linked_contacts_and_addresses:
+ linked_doc = frappe.get_doc(row.doctype, row.name)
+ if not linked_doc.has_link('Customer', self.name):
+ linked_doc.append('links', dict(link_doctype='Customer', link_name=self.name))
+ linked_doc.save(ignore_permissions=self.flags.ignore_permissions)
def validate_name_with_customer_group(self):
if frappe.db.exists("Customer Group", self.name):
diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
index b2bf546..3e22d0f 100644
--- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
+++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
@@ -85,7 +85,7 @@
and so.docstatus = 1
{conditions}
GROUP BY soi.name
- ORDER BY so.transaction_date ASC
+ ORDER BY so.transaction_date ASC, soi.item_code ASC
""".format(conditions=conditions), filters, as_dict=1)
return data
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index c94b346..9f1eb75 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -3,6 +3,7 @@
import copy
+from urllib.parse import quote
import frappe
from frappe import _
@@ -10,7 +11,6 @@
from frappe.utils.nestedset import NestedSet
from frappe.website.utils import clear_cache
from frappe.website.website_generator import WebsiteGenerator
-from six.moves.urllib.parse import quote
from erpnext.shopping_cart.filters import ProductFiltersBuilder
from erpnext.shopping_cart.product_info import set_product_info_for_website
diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py
index bafaab8..1d7bad2 100644
--- a/erpnext/setup/install.py
+++ b/erpnext/setup/install.py
@@ -189,7 +189,7 @@
user_type_limit = {}
for user_type, data in user_types.items():
- user_type_limit.setdefault(frappe.scrub(user_type), 10)
+ user_type_limit.setdefault(frappe.scrub(user_type), 20)
update_site_config('user_type_doctype_limit', user_type_limit)
@@ -204,15 +204,33 @@
'apply_user_permission_on': 'Employee',
'user_id_field': 'user_id',
'doctypes': {
- 'Salary Slip': ['read'],
+ # masters
+ 'Holiday List': ['read'],
'Employee': ['read', 'write'],
+ # payroll
+ 'Salary Slip': ['read'],
+ 'Employee Benefit Application': ['read', 'write', 'create', 'delete'],
+ # expenses
'Expense Claim': ['read', 'write', 'create', 'delete'],
+ 'Employee Advance': ['read', 'write', 'create', 'delete'],
+ # leave and attendance
'Leave Application': ['read', 'write', 'create', 'delete'],
'Attendance Request': ['read', 'write', 'create', 'delete'],
'Compensatory Leave Request': ['read', 'write', 'create', 'delete'],
+ # tax
'Employee Tax Exemption Declaration': ['read', 'write', 'create', 'delete'],
'Employee Tax Exemption Proof Submission': ['read', 'write', 'create', 'delete'],
- 'Timesheet': ['read', 'write', 'create', 'delete', 'submit', 'cancel', 'amend']
+ # projects
+ 'Timesheet': ['read', 'write', 'create', 'delete', 'submit', 'cancel', 'amend'],
+ # trainings
+ 'Training Program': ['read'],
+ 'Training Feedback': ['read', 'write', 'create', 'delete', 'submit', 'cancel', 'amend'],
+ # shifts
+ 'Shift Request': ['read', 'write', 'create', 'delete', 'submit', 'cancel', 'amend'],
+ # misc
+ 'Employee Grievance': ['read', 'write', 'create', 'delete'],
+ 'Employee Referral': ['read', 'write', 'create', 'delete'],
+ 'Travel Request': ['read', 'write', 'create', 'delete']
}
}
}
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 281e881..d99fadc 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -602,14 +602,6 @@
frappe.throw(_("Barcode {0} is not a valid {1} code").format(
item_barcode.barcode, item_barcode.barcode_type), InvalidBarcode)
- if item_barcode.barcode != item_barcode.name:
- # if barcode is getting updated , the row name has to reset.
- # Delete previous old row doc and re-enter row as if new to reset name in db.
- item_barcode.set("__islocal", True)
- item_barcode_entry_name = item_barcode.name
- item_barcode.name = None
- frappe.delete_doc("Item Barcode", item_barcode_entry_name)
-
def validate_warehouse_for_reorder(self):
'''Validate Reorder level table for duplicate and conditional mandatory'''
warehouse = []
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index a61b319..60154af 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -1445,14 +1445,15 @@
qty = req_qty_each * flt(self.fg_completed_qty)
elif backflushed_materials.get(item.item_code):
+ precision = frappe.get_precision("Stock Entry Detail", "qty")
for d in backflushed_materials.get(item.item_code):
- if d.get(item.warehouse):
+ if d.get(item.warehouse) > 0:
if (qty > req_qty):
- qty = (qty/trans_qty) * flt(self.fg_completed_qty)
+ qty = ((flt(qty, precision) - flt(d.get(item.warehouse), precision))
+ / (flt(trans_qty, precision) - flt(produced_qty, precision))
+ ) * flt(self.fg_completed_qty)
- if consumed_qty and frappe.db.get_single_value("Manufacturing Settings",
- "material_consumption"):
- qty -= consumed_qty
+ d[item.warehouse] -= qty
if cint(frappe.get_cached_value('UOM', item.stock_uom, 'must_be_whole_number')):
qty = frappe.utils.ceil(qty)
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js
index fe2417b..ef7c2cc 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.js
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.js
@@ -86,10 +86,10 @@
],
"formatter": function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
- if (column.fieldname == "out_qty" && data.out_qty < 0) {
+ if (column.fieldname == "out_qty" && data && data.out_qty < 0) {
value = "<span style='color:red'>" + value + "</span>";
}
- else if (column.fieldname == "in_qty" && data.in_qty > 0) {
+ else if (column.fieldname == "in_qty" && data && data.in_qty > 0) {
value = "<span style='color:green'>" + value + "</span>";
}
diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py
index 1dcf863..525af40 100644
--- a/erpnext/stock/report/test_reports.py
+++ b/erpnext/stock/report/test_reports.py
@@ -1,6 +1,8 @@
import unittest
from typing import List, Tuple
+import frappe
+
from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report
DEFAULT_FILTERS = {
@@ -10,8 +12,12 @@
}
+batch = frappe.db.get_value("Batch", fieldname=["name"], as_dict=True, order_by="creation desc")
+
REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
("Stock Ledger", {"_optional": True}),
+ ("Stock Ledger", {"batch_no": batch}),
+ ("Stock Ledger", {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}),
("Stock Balance", {"_optional": True}),
("Stock Projected Qty", {"_optional": True}),
("Batch-Wise Balance History", {}),
@@ -40,6 +46,13 @@
("Item Variant Details", {"item": "_Test Variant Item",}),
("Total Stock Summary", {"group_by": "warehouse",}),
("Batch Item Expiry Status", {}),
+ ("Incorrect Stock Value Report", {"company": "_Test Company with perpetual inventory"}),
+ ("Incorrect Serial No Valuation", {}),
+ ("Incorrect Balance Qty After Transaction", {}),
+ ("Supplier-Wise Sales Analytics", {}),
+ ("Item Prices", {"items": "Enabled Items only"}),
+ ("Delayed Item Report", {"based_on": "Sales Invoice"}),
+ ("Delayed Item Report", {"based_on": "Delivery Note"}),
("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
("Stock Ledger Invariant Check",
{
diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py
index bc9f04e..2bd7e9e 100644
--- a/erpnext/tests/utils.py
+++ b/erpnext/tests/utils.py
@@ -92,6 +92,8 @@
for key, value in settings_dict.items():
setattr(settings, key, value)
settings.save()
+ # singles are cached by default, clear to avoid flake
+ frappe.db.value_cache[settings] = {}
yield # yield control to calling function
finally: