diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 521432d296..2005dac37d 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -425,7 +425,10 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e status: ["!=", "Stopped"], per_ordered: ["<", 100], company: me.frm.doc.company - } + }, + allow_child_item_selection: true, + child_fielname: "items", + child_columns: ["item_code", "qty"] }) }, __("Get Items From")); diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index c7a5db5994..12a09cdd0e 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -433,12 +433,12 @@ "image_field": "image", "links": [ { - "group": "Item Group", - "link_doctype": "Supplier Item Group", - "link_fieldname": "supplier" + "group": "Allowed Items", + "link_doctype": "Party Specific Item", + "link_fieldname": "party" } ], - "modified": "2021-08-27 18:02:44.314077", + "modified": "2021-09-06 17:37:56.522233", "modified_by": "Administrator", "module": "Buying", "name": "Supplier", diff --git a/erpnext/buying/doctype/supplier_item_group/supplier_item_group.json b/erpnext/buying/doctype/supplier_item_group/supplier_item_group.json deleted file mode 100644 index 1971458f61..0000000000 --- a/erpnext/buying/doctype/supplier_item_group/supplier_item_group.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "actions": [], - "creation": "2021-05-07 18:16:40.621421", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "supplier", - "item_group" - ], - "fields": [ - { - "fieldname": "supplier", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Supplier", - "options": "Supplier", - "reqd": 1 - }, - { - "fieldname": "item_group", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Item Group", - "options": "Item Group", - "reqd": 1 - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2021-05-19 13:48:16.742303", - "modified_by": "Administrator", - "module": "Buying", - "name": "Supplier Item Group", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Purchase User", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Purchase Manager", - "share": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/buying/doctype/supplier_item_group/supplier_item_group.py b/erpnext/buying/doctype/supplier_item_group/supplier_item_group.py deleted file mode 100644 index 6d71f7d516..0000000000 --- a/erpnext/buying/doctype/supplier_item_group/supplier_item_group.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals - -import frappe -from frappe import _ -from frappe.model.document import Document - - -class SupplierItemGroup(Document): - def validate(self): - exists = frappe.db.exists({ - 'doctype': 'Supplier Item Group', - 'supplier': self.supplier, - 'item_group': self.item_group - }) - if exists: - frappe.throw(_("Item Group has already been linked to this supplier.")) diff --git a/erpnext/buying/doctype/supplier_item_group/test_supplier_item_group.py b/erpnext/buying/doctype/supplier_item_group/test_supplier_item_group.py deleted file mode 100644 index 55ba85ef2d..0000000000 --- a/erpnext/buying/doctype/supplier_item_group/test_supplier_item_group.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -from __future__ import unicode_literals - -# import frappe -import unittest - - -class TestSupplierItemGroup(unittest.TestCase): - pass diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index ccd417be04..9f28646a0b 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -7,6 +7,7 @@ import json from collections import defaultdict import frappe +from frappe import scrub from frappe.desk.reportview import get_filters_cond, get_match_cond from frappe.utils import nowdate, unique @@ -223,18 +224,29 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals if not field in searchfields] searchfields = " or ".join([field + " like %(txt)s" for field in searchfields]) - if filters and isinstance(filters, dict) and filters.get('supplier'): - item_group_list = frappe.get_all('Supplier Item Group', - filters = {'supplier': filters.get('supplier')}, fields = ['item_group']) + if filters and isinstance(filters, dict): + if filters.get('customer') or filters.get('supplier'): + party = filters.get('customer') or filters.get('supplier') + item_rules_list = frappe.get_all('Party Specific Item', + filters = {'party': party}, fields = ['restrict_based_on', 'based_on_value']) - item_groups = [] - for i in item_group_list: - item_groups.append(i.item_group) + filters_dict = {} + for rule in item_rules_list: + if rule['restrict_based_on'] == 'Item': + rule['restrict_based_on'] = 'name' + filters_dict[rule.restrict_based_on] = [] - del filters['supplier'] + for rule in item_rules_list: + filters_dict[rule.restrict_based_on].append(rule.based_on_value) + + for filter in filters_dict: + filters[scrub(filter)] = ['in', filters_dict[filter]] + + if filters.get('customer'): + del filters['customer'] + else: + del filters['supplier'] - if item_groups: - filters['item_group'] = ['in', item_groups] description_cond = '' if frappe.db.count('Item', cache=True) < 50000: diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py index ff2ed45bd2..8e5952c4a3 100644 --- a/erpnext/controllers/website_list_for_contact.py +++ b/erpnext/controllers/website_list_for_contact.py @@ -7,6 +7,7 @@ import json import frappe from frappe import _ +from frappe.modules.utils import get_module_app from frappe.utils import flt, has_common from frappe.utils.user import is_website_user @@ -21,8 +22,32 @@ def get_list_context(context=None): "get_list": get_transaction_list } +def get_webform_list_context(module): + if get_module_app(module) != 'erpnext': + return + return { + "get_list": get_webform_transaction_list + } -def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified"): +def get_webform_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified"): + """ Get List of transactions for custom doctypes """ + from frappe.www.list import get_list + + if not filters: + filters = [] + + meta = frappe.get_meta(doctype) + + for d in meta.fields: + if d.fieldtype == 'Link' and d.fieldname != 'amended_from': + allowed_docs = [d.name for d in get_transaction_list(doctype=d.options, custom=True)] + allowed_docs.append('') + filters.append((d.fieldname, 'in', allowed_docs)) + + return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=False, + fields=None, order_by="modified") + +def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified", custom=False): user = frappe.session.user ignore_permissions = False @@ -46,7 +71,7 @@ def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_p filters.append(('customer', 'in', customers)) elif suppliers: filters.append(('supplier', 'in', suppliers)) - else: + elif not custom: return [] if doctype == 'Request for Quotation': @@ -56,9 +81,16 @@ def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_p # Since customers and supplier do not have direct access to internal doctypes ignore_permissions = True + if not customers and not suppliers and custom: + ignore_permissions = False + filters = [] + transactions = get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length, fields='name', ignore_permissions=ignore_permissions, order_by='modified desc') + if custom: + return transactions + return post_process(doctype, transactions) def get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length=20, diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 5b6e1eeafc..be05e35113 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -62,6 +62,7 @@ treeviews = ['Account', 'Cost Center', 'Warehouse', 'Item Group', 'Customer Grou # website update_website_context = ["erpnext.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.update_website_context"] my_account_context = "erpnext.shopping_cart.utils.update_my_account_context" +webform_list_context = "erpnext.controllers.website_list_for_contact.get_webform_list_context" calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "Course Schedule"] diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.js b/erpnext/hr/doctype/employee_advance/employee_advance.js index fa4b06aad3..7d1c7cbf4a 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.js +++ b/erpnext/hr/doctype/employee_advance/employee_advance.js @@ -73,7 +73,7 @@ frappe.ui.form.on('Employee Advance', { frm.trigger('make_return_entry'); }, __('Create')); } else if (frm.doc.repay_unclaimed_amount_from_salary == 1 && frappe.model.can_create("Additional Salary")) { - frm.add_custom_button(__("Deduction from salary"), function() { + frm.add_custom_button(__("Deduction from Salary"), function() { frm.events.make_deduction_via_additional_salary(frm); }, __('Create')); } diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json index ea25aa720a..04754530c3 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.json +++ b/erpnext/hr/doctype/employee_advance/employee_advance.json @@ -170,7 +170,7 @@ "default": "0", "fieldname": "repay_unclaimed_amount_from_salary", "fieldtype": "Check", - "label": "Repay unclaimed amount from salary" + "label": "Repay Unclaimed Amount from Salary" }, { "depends_on": "eval:cur_frm.doc.employee", @@ -200,10 +200,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-03-31 22:31:53.746659", + "modified": "2021-09-11 18:38:38.617478", "modified_by": "Administrator", "module": "HR", "name": "Employee Advance", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index 87d42d34e3..8d90bccd2d 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -172,7 +172,10 @@ def get_paying_amount_paying_exchange_rate(payment_account, doc): @frappe.whitelist() def create_return_through_additional_salary(doc): import json - doc = frappe._dict(json.loads(doc)) + + if isinstance(doc, str): + doc = frappe._dict(json.loads(doc)) + additional_salary = frappe.new_doc('Additional Salary') additional_salary.employee = doc.employee additional_salary.currency = doc.currency diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py index f8e5f535cb..c439d45b55 100644 --- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py @@ -12,8 +12,11 @@ import erpnext from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.employee_advance.employee_advance import ( EmployeeAdvanceOverPayment, + create_return_through_additional_salary, make_bank_entry, ) +from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component +from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure class TestEmployeeAdvance(unittest.TestCase): @@ -33,6 +36,46 @@ class TestEmployeeAdvance(unittest.TestCase): journal_entry1 = make_payment_entry(advance) self.assertRaises(EmployeeAdvanceOverPayment, journal_entry1.submit) + def test_repay_unclaimed_amount_from_salary(self): + employee_name = make_employee("_T@employe.advance") + advance = make_employee_advance(employee_name, {"repay_unclaimed_amount_from_salary": 1}) + + args = {"type": "Deduction"} + create_salary_component("Advance Salary - Deduction", **args) + make_salary_structure("Test Additional Salary for Advance Return", "Monthly", employee=employee_name) + + # additional salary for 700 first + advance.reload() + additional_salary = create_return_through_additional_salary(advance) + additional_salary.salary_component = "Advance Salary - Deduction" + additional_salary.payroll_date = nowdate() + additional_salary.amount = 700 + additional_salary.insert() + additional_salary.submit() + + advance.reload() + self.assertEqual(advance.return_amount, 700) + + # additional salary for remaining 300 + additional_salary = create_return_through_additional_salary(advance) + additional_salary.salary_component = "Advance Salary - Deduction" + additional_salary.payroll_date = nowdate() + additional_salary.amount = 300 + additional_salary.insert() + additional_salary.submit() + + advance.reload() + self.assertEqual(advance.return_amount, 1000) + + # update advance return amount on additional salary cancellation + additional_salary.cancel() + advance.reload() + self.assertEqual(advance.return_amount, 700) + + def tearDown(self): + frappe.db.rollback() + + def make_payment_entry(advance): journal_entry = frappe.get_doc(make_bank_entry("Employee Advance", advance.name)) journal_entry.cheque_no = "123123" @@ -41,7 +84,7 @@ def make_payment_entry(advance): return journal_entry -def make_employee_advance(employee_name): +def make_employee_advance(employee_name, args=None): doc = frappe.new_doc("Employee Advance") doc.employee = employee_name doc.company = "_Test company" @@ -51,6 +94,10 @@ def make_employee_advance(employee_name): doc.advance_amount = 1000 doc.posting_date = nowdate() doc.advance_account = "_Test Employee Advance - _TC" + + if args: + doc.update(args) + doc.insert() doc.submit() diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index deec644209..b6f4cadcc9 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -32,7 +32,10 @@ def set_employee_name(doc): def update_employee(employee, details, date=None, cancel=False): internal_work_history = {} for item in details: - fieldtype = frappe.get_meta("Employee").get_field(item.fieldname).fieldtype + field = frappe.get_meta("Employee").get_field(item.fieldname) + if not field: + continue + fieldtype = field.fieldtype new_data = item.new if not cancel else item.current if fieldtype == "Date" and new_data: new_data = getdate(new_data) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 15b196fd78..3bc40a17e0 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -304,5 +304,6 @@ erpnext.patches.v13_0.set_operation_time_based_on_operating_cost erpnext.patches.v13_0.validate_options_for_data_field erpnext.patches.v13_0.create_gst_payment_entry_fields erpnext.patches.v14_0.delete_shopify_doctypes +erpnext.patches.v13_0.replace_supplier_item_group_with_party_specific_item erpnext.patches.v13_0.update_dates_in_tax_withholding_category erpnext.patches.v14_0.update_opportunity_currency_fields diff --git a/erpnext/patches/v13_0/replace_supplier_item_group_with_party_specific_item.py b/erpnext/patches/v13_0/replace_supplier_item_group_with_party_specific_item.py new file mode 100644 index 0000000000..ba96fdd226 --- /dev/null +++ b/erpnext/patches/v13_0/replace_supplier_item_group_with_party_specific_item.py @@ -0,0 +1,17 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe + + +def execute(): + if frappe.db.table_exists('Supplier Item Group'): + frappe.reload_doc("selling", "doctype", "party_specific_item") + sig = frappe.db.get_all("Supplier Item Group", fields=["name", "supplier", "item_group"]) + for item in sig: + psi = frappe.new_doc("Party Specific Item") + psi.party_type = "Supplier" + psi.party = item.supplier + psi.restrict_based_on = "Item Group" + psi.based_on_value = item.item_group + psi.insert() diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py index ed10f2bc67..7c0a8eac99 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -14,12 +14,11 @@ from erpnext.hr.utils import validate_active_employee class AdditionalSalary(Document): def on_submit(self): - if self.ref_doctype == "Employee Advance" and self.ref_docname: - frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", self.amount) - + self.update_return_amount_in_employee_advance() self.update_employee_referral() def on_cancel(self): + self.update_return_amount_in_employee_advance() self.update_employee_referral(cancel=True) def validate(self): @@ -98,6 +97,17 @@ class AdditionalSalary(Document): frappe.throw(_("Additional Salary for referral bonus can only be created against Employee Referral with status {0}").format( frappe.bold("Accepted"))) + def update_return_amount_in_employee_advance(self): + if self.ref_doctype == "Employee Advance" and self.ref_docname: + return_amount = frappe.db.get_value("Employee Advance", self.ref_docname, "return_amount") + + if self.docstatus == 2: + return_amount -= self.amount + else: + return_amount += self.amount + + frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", return_amount) + def update_employee_referral(self, cancel=False): if self.ref_doctype == "Employee Referral": status = "Unpaid" if cancel else "Paid" diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 8c48345d8f..d113e7e569 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -487,7 +487,7 @@ class SalarySlip(TransactionBase): self.calculate_component_amounts("deductions") self.set_loan_repayment() - self.set_component_amounts_based_on_payment_days() + self.set_precision_for_component_amounts() self.set_net_pay() def set_net_pay(self): @@ -709,6 +709,17 @@ class SalarySlip(TransactionBase): component_row.amount = amount + self.update_component_amount_based_on_payment_days(component_row) + + def update_component_amount_based_on_payment_days(self, component_row): + joining_date, relieving_date = self.get_joining_and_relieving_dates() + component_row.amount = self.get_amount_based_on_payment_days(component_row, joining_date, relieving_date)[0] + + def set_precision_for_component_amounts(self): + for component_type in ("earnings", "deductions"): + for component_row in self.get(component_type): + component_row.amount = flt(component_row.amount, component_row.precision("amount")) + def calculate_variable_based_on_taxable_salary(self, tax_component, payroll_period): if not payroll_period: frappe.msgprint(_("Start and end dates not in a valid Payroll Period, cannot calculate {0}.") @@ -866,14 +877,7 @@ class SalarySlip(TransactionBase): return total_tax_paid def get_taxable_earnings(self, allow_tax_exemption=False, based_on_payment_days=0): - joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, - ["date_of_joining", "relieving_date"]) - - if not relieving_date: - relieving_date = getdate(self.end_date) - - if not joining_date: - frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name))) + joining_date, relieving_date = self.get_joining_and_relieving_dates() taxable_earnings = 0 additional_income = 0 @@ -884,7 +888,10 @@ class SalarySlip(TransactionBase): if based_on_payment_days: amount, additional_amount = self.get_amount_based_on_payment_days(earning, joining_date, relieving_date) else: - amount, additional_amount = earning.amount, earning.additional_amount + if earning.additional_amount: + amount, additional_amount = earning.amount, earning.additional_amount + else: + amount, additional_amount = earning.default_amount, earning.additional_amount if earning.is_tax_applicable: if additional_amount: @@ -1055,7 +1062,7 @@ class SalarySlip(TransactionBase): total += amount return total - def set_component_amounts_based_on_payment_days(self): + def get_joining_and_relieving_dates(self): joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, ["date_of_joining", "relieving_date"]) @@ -1065,9 +1072,7 @@ class SalarySlip(TransactionBase): if not joining_date: frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name))) - for component_type in ("earnings", "deductions"): - for d in self.get(component_type): - d.amount = flt(self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0], d.precision("amount")) + return joining_date, relieving_date def set_loan_repayment(self): self.total_loan_repayment = 0 diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 480daa2595..bff36a4149 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -17,6 +17,7 @@ from frappe.utils import ( getdate, nowdate, ) +from frappe.utils.make_random import get_random import erpnext from erpnext.accounts.utils import get_fiscal_year @@ -134,6 +135,65 @@ class TestSalarySlip(unittest.TestCase): frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") + def test_component_amount_dependent_on_another_payment_days_based_component(self): + from erpnext.hr.doctype.attendance.attendance import mark_attendance + from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( + create_salary_structure_assignment, + ) + + no_of_days = self.get_no_of_days() + # Payroll based on attendance + frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance") + + salary_structure = make_salary_structure_for_payment_days_based_component_dependency() + employee = make_employee("test_payment_days_based_component@salary.com", company="_Test Company") + + # base = 50000 + create_salary_structure_assignment(employee, salary_structure.name, company="_Test Company", currency="INR") + + # mark employee absent for a day since this case works fine if payment days are equal to working days + 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 = 'Salary Slip Test Holiday List' + and holiday_date between %s and %s + order by holiday_date + """, (month_start_date, month_end_date))[0][0] + + mark_attendance(employee, add_days(first_sunday, 1), 'Absent', ignore_validate=True) # counted as absent + + # make salary slip and assert payment days + ss = make_salary_slip_for_payment_days_dependency_test("test_payment_days_based_component@salary.com", salary_structure.name) + self.assertEqual(ss.absent_days, 1) + + days_in_month = no_of_days[0] + no_of_holidays = no_of_days[1] + + self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 1) + + ss.reload() + payment_days_based_comp_amount = 0 + for component in ss.earnings: + if component.salary_component == "HRA - Payment Days": + payment_days_based_comp_amount = flt(component.amount, component.precision("amount")) + break + + # check if the dependent component is calculated using the amount updated after payment days + actual_amount = 0 + precision = 0 + for component in ss.deductions: + if component.salary_component == "P - Employee Provident Fund": + precision = component.precision("amount") + actual_amount = flt(component.amount, precision) + break + + expected_amount = flt((flt(ss.gross_pay) - payment_days_based_comp_amount) * 0.12, precision) + + self.assertEqual(actual_amount, expected_amount) + frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") + def test_salary_slip_with_holidays_included(self): no_of_days = self.get_no_of_days() frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1) @@ -864,3 +924,91 @@ def make_holiday_list(): holiday_list = holiday_list.name return holiday_list + +def make_salary_structure_for_payment_days_based_component_dependency(): + earnings = [ + { + "salary_component": "Basic Salary - Payment Days", + "abbr": "P_BS", + "type": "Earning", + "formula": "base", + "amount_based_on_formula": 1 + }, + { + "salary_component": "HRA - Payment Days", + "abbr": "P_HRA", + "type": "Earning", + "depends_on_payment_days": 1, + "amount_based_on_formula": 1, + "formula": "base * 0.20" + } + ] + + make_salary_component(earnings, False, company_list=["_Test Company"]) + + deductions = [ + { + "salary_component": "P - Professional Tax", + "abbr": "P_PT", + "type": "Deduction", + "depends_on_payment_days": 1, + "amount": 200.00 + }, + { + "salary_component": "P - Employee Provident Fund", + "abbr": "P_EPF", + "type": "Deduction", + "exempted_from_income_tax": 1, + "amount_based_on_formula": 1, + "depends_on_payment_days": 0, + "formula": "(gross_pay - P_HRA) * 0.12" + } + ] + + make_salary_component(deductions, False, company_list=["_Test Company"]) + + salary_structure = "Salary Structure with PF" + if frappe.db.exists("Salary Structure", salary_structure): + frappe.db.delete("Salary Structure", salary_structure) + + details = { + "doctype": "Salary Structure", + "name": salary_structure, + "company": "_Test Company", + "payroll_frequency": "Monthly", + "payment_account": get_random("Account", filters={"account_currency": "INR"}), + "currency": "INR" + } + + salary_structure_doc = frappe.get_doc(details) + + for entry in earnings: + salary_structure_doc.append("earnings", entry) + + for entry in deductions: + salary_structure_doc.append("deductions", entry) + + salary_structure_doc.insert() + salary_structure_doc.submit() + + return salary_structure_doc + +def make_salary_slip_for_payment_days_dependency_test(employee, salary_structure): + employee = frappe.db.get_value("Employee", { + "user_id": employee + }, + ["name", "company", "employee_name"], + as_dict=True) + + salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": employee})}) + + if not salary_slip_name: + salary_slip = make_salary_slip(salary_structure, employee=employee.name) + salary_slip.employee_name = employee.employee_name + salary_slip.payroll_frequency = "Monthly" + salary_slip.posting_date = nowdate() + salary_slip.insert() + else: + salary_slip = frappe.get_doc("Salary Slip", salary_slip_name) + + return salary_slip \ No newline at end of file diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index e0610eb8c0..2a74d6015f 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -709,6 +709,9 @@ erpnext.utils.map_current_doc = function(opts) { setters: opts.setters, get_query: opts.get_query, add_filters_group: 1, + allow_child_item_selection: opts.allow_child_item_selection, + child_fieldname: opts.child_fielname, + child_columns: opts.child_columns, action: function(selections, args) { let values = selections; if(values.length === 0){ @@ -716,7 +719,7 @@ erpnext.utils.map_current_doc = function(opts) { return; } opts.source_name = values; - opts.setters = args; + opts.args = args; d.dialog.hide(); _map(); }, diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 5913b849eb..e811435e66 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -20,7 +20,6 @@ "tax_withholding_category", "default_bank_account", "lead_name", - "prospect", "opportunity_name", "image", "column_break0", @@ -214,7 +213,8 @@ "fieldtype": "Link", "ignore_user_permissions": 1, "label": "Represents Company", - "options": "Company" + "options": "Company", + "unique": 1 }, { "depends_on": "represents_company", @@ -497,14 +497,6 @@ "label": "Tax Withholding Category", "options": "Tax Withholding Category" }, - { - "fieldname": "prospect", - "fieldtype": "Link", - "label": "Prospect", - "no_copy": 1, - "options": "Prospect", - "print_hide": 1 - }, { "fieldname": "opportunity_name", "fieldtype": "Link", @@ -518,8 +510,14 @@ "idx": 363, "image_field": "image", "index_web_pages_for_search": 1, - "links": [], - "modified": "2021-08-25 18:56:09.929905", + "links": [ + { + "group": "Allowed Items", + "link_doctype": "Party Specific Item", + "link_fieldname": "party" + } + ], + "modified": "2021-09-06 17:38:54.196663", "modified_by": "Administrator", "module": "Selling", "name": "Customer", diff --git a/erpnext/buying/doctype/supplier_item_group/__init__.py b/erpnext/selling/doctype/party_specific_item/__init__.py similarity index 100% rename from erpnext/buying/doctype/supplier_item_group/__init__.py rename to erpnext/selling/doctype/party_specific_item/__init__.py diff --git a/erpnext/buying/doctype/supplier_item_group/supplier_item_group.js b/erpnext/selling/doctype/party_specific_item/party_specific_item.js similarity index 79% rename from erpnext/buying/doctype/supplier_item_group/supplier_item_group.js rename to erpnext/selling/doctype/party_specific_item/party_specific_item.js index f7da90d98d..077b93631e 100644 --- a/erpnext/buying/doctype/supplier_item_group/supplier_item_group.js +++ b/erpnext/selling/doctype/party_specific_item/party_specific_item.js @@ -1,7 +1,7 @@ // Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Supplier Item Group', { +frappe.ui.form.on('Party Specific Item', { // refresh: function(frm) { // } diff --git a/erpnext/selling/doctype/party_specific_item/party_specific_item.json b/erpnext/selling/doctype/party_specific_item/party_specific_item.json new file mode 100644 index 0000000000..32b5d478bb --- /dev/null +++ b/erpnext/selling/doctype/party_specific_item/party_specific_item.json @@ -0,0 +1,77 @@ +{ + "actions": [], + "creation": "2021-08-27 19:28:07.559978", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "party_type", + "party", + "column_break_3", + "restrict_based_on", + "based_on_value" + ], + "fields": [ + { + "fieldname": "party_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Party Type", + "options": "Customer\nSupplier", + "reqd": 1 + }, + { + "fieldname": "party", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Party Name", + "options": "party_type", + "reqd": 1 + }, + { + "fieldname": "restrict_based_on", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Restrict Items Based On", + "options": "Item\nItem Group\nBrand", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "based_on_value", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Based On Value", + "options": "restrict_based_on", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-09-14 13:27:58.612334", + "modified_by": "Administrator", + "module": "Selling", + "name": "Party Specific Item", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "party", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/selling/doctype/party_specific_item/party_specific_item.py b/erpnext/selling/doctype/party_specific_item/party_specific_item.py new file mode 100644 index 0000000000..a408af5642 --- /dev/null +++ b/erpnext/selling/doctype/party_specific_item/party_specific_item.py @@ -0,0 +1,19 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document + + +class PartySpecificItem(Document): + def validate(self): + exists = frappe.db.exists({ + 'doctype': 'Party Specific Item', + 'party_type': self.party_type, + 'party': self.party, + 'restrict_based_on': self.restrict_based_on, + 'based_on': self.based_on_value, + }) + if exists: + frappe.throw(_("This item filter has already been applied for the {0}").format(self.party_type)) diff --git a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py new file mode 100644 index 0000000000..874a364592 --- /dev/null +++ b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py @@ -0,0 +1,38 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import unittest + +import frappe + +from erpnext.controllers.queries import item_query + +test_dependencies = ['Item', 'Customer', 'Supplier'] + +def create_party_specific_item(**args): + psi = frappe.new_doc("Party Specific Item") + psi.party_type = args.get('party_type') + psi.party = args.get('party') + psi.restrict_based_on = args.get('restrict_based_on') + psi.based_on_value = args.get('based_on_value') + psi.insert() + +class TestPartySpecificItem(unittest.TestCase): + def setUp(self): + self.customer = frappe.get_last_doc("Customer") + self.supplier = frappe.get_last_doc("Supplier") + self.item = frappe.get_last_doc("Item") + + def test_item_query_for_customer(self): + create_party_specific_item(party_type='Customer', party=self.customer.name, restrict_based_on='Item', based_on_value=self.item.name) + filters = {'is_sales_item': 1, 'customer': self.customer.name} + items = item_query(doctype= 'Item', txt= '', searchfield= 'name', start= 0, page_len= 20,filters=filters, as_dict= False) + for item in items: + self.assertEqual(item[0], self.item.name) + + def test_item_query_for_supplier(self): + create_party_specific_item(party_type='Supplier', party=self.supplier.name, restrict_based_on='Item Group', based_on_value=self.item.item_group) + filters = {'supplier': self.supplier.name, 'is_purchase_item': 1} + items = item_query(doctype= 'Item', txt= '', searchfield= 'name', start= 0, page_len= 20,filters=filters, as_dict= False) + for item in items: + self.assertEqual(item[2], self.item.item_group) 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 805c3d804f..5c4d8b601f 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -73,7 +73,7 @@ def get_data(conditions, filters): `tabSales Order` so, `tabSales Order Item` soi LEFT JOIN `tabSales Invoice Item` sii - ON sii.so_detail = soi.name + ON sii.so_detail = soi.name and sii.docstatus = 1 WHERE soi.parent = so.name and so.status not in ('Stopped', 'Closed', 'On Hold') diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 955ef5e939..ddd4c4e6a5 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -63,7 +63,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran this.frm.set_query("item_code", "items", function() { return { query: "erpnext.controllers.queries.item_query", - filters: {'is_sales_item': 1} + filters: {'is_sales_item': 1, 'customer': cur_frm.doc.customer} } }); } @@ -247,7 +247,12 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran var editable_price_list_rate = cint(frappe.defaults.get_default("editable_price_list_rate")); if(df && editable_price_list_rate) { - df.read_only = 0; + const parent_field = frappe.meta.get_parentfield(this.frm.doc.doctype, this.frm.doc.doctype + " Item"); + if (!this.frm.fields_dict[parent_field]) return; + + this.frm.fields_dict[parent_field].grid.update_docfield_property( + 'price_list_rate', 'read_only', 0 + ); } } diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index ddf3e662e0..b26c6a49b4 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -99,7 +99,7 @@ class ItemGroup(NestedSet, WebsiteGenerator): filter_engine = ProductFiltersBuilder(self.name) context.field_filters = filter_engine.get_field_filters() - context.attribute_filters = filter_engine.get_attribute_fitlers() + context.attribute_filters = filter_engine.get_attribute_filters() context.update({ "parents": get_parent_item_groups(self.parent_item_group), diff --git a/erpnext/shopping_cart/filters.py b/erpnext/shopping_cart/filters.py index aaeff0fe07..4787ae534c 100644 --- a/erpnext/shopping_cart/filters.py +++ b/erpnext/shopping_cart/filters.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import frappe -from frappe import _dict class ProductFiltersBuilder: @@ -57,37 +56,31 @@ class ProductFiltersBuilder: return filter_data - def get_attribute_fitlers(self): + def get_attribute_filters(self): attributes = [row.attribute for row in self.doc.filter_attributes] - attribute_docs = [ - frappe.get_doc('Item Attribute', attribute) for attribute in attributes - ] - valid_attributes = [] + if not attributes: + return [] - for attr_doc in attribute_docs: - selected_attributes = [] - for attr in attr_doc.item_attribute_values: - or_filters = [] - filters= [ - ["Item Variant Attribute", "attribute", "=", attr.parent], - ["Item Variant Attribute", "attribute_value", "=", attr.attribute_value] - ] - if self.item_group: - or_filters.extend([ - ["item_group", "=", self.item_group], - ["Website Item Group", "item_group", "=", self.item_group] - ]) + result = frappe.db.sql( + """ + select + distinct attribute, attribute_value + from + `tabItem Variant Attribute` + where + attribute in %(attributes)s + and attribute_value is not null + """, + {"attributes": attributes}, + as_dict=1, + ) - if frappe.db.get_all("Item", filters, or_filters=or_filters, limit=1): - selected_attributes.append(attr) + attribute_value_map = {} + for d in result: + attribute_value_map.setdefault(d.attribute, []).append(d.attribute_value) - if selected_attributes: - valid_attributes.append( - _dict( - item_attribute_values=selected_attributes, - name=attr_doc.name - ) - ) - - return valid_attributes + out = [] + for name, values in attribute_value_map.items(): + out.append(frappe._dict(name=name, item_attribute_values=values)) + return out diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 9eb4721626..2569c04251 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -6,10 +6,13 @@ from __future__ import unicode_literals +import json + import frappe from frappe import _, msgprint from frappe.model.mapper import get_mapped_doc from frappe.utils import cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate +from six import string_types from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items from erpnext.controllers.buying_controller import BuyingController @@ -269,7 +272,10 @@ def update_status(name, status): material_request.update_status(status) @frappe.whitelist() -def make_purchase_order(source_name, target_doc=None): +def make_purchase_order(source_name, target_doc=None, args={}): + + if isinstance(args, string_types): + args = json.loads(args) def postprocess(source, target_doc): if frappe.flags.args and frappe.flags.args.default_supplier: @@ -284,7 +290,10 @@ def make_purchase_order(source_name, target_doc=None): set_missing_values(source, target_doc) def select_item(d): - return d.ordered_qty < d.stock_qty + filtered_items = args.get('filtered_children', []) + child_filter = d.name in filtered_items if filtered_items else True + + return d.ordered_qty < d.stock_qty and child_filter doclist = get_mapped_doc("Material Request", source_name, { "Material Request": { diff --git a/erpnext/tests/test_webform.py b/erpnext/tests/test_webform.py new file mode 100644 index 0000000000..19255db33c --- /dev/null +++ b/erpnext/tests/test_webform.py @@ -0,0 +1,138 @@ +import unittest + +import frappe + +from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + + +class TestWebsite(unittest.TestCase): + def test_permission_for_custom_doctype(self): + create_user('Supplier 1', 'supplier1@gmail.com') + create_user('Supplier 2', 'supplier2@gmail.com') + create_supplier_with_contact('Supplier1', 'All Supplier Groups', 'Supplier 1', 'supplier1@gmail.com') + create_supplier_with_contact('Supplier2', 'All Supplier Groups', 'Supplier 2', 'supplier2@gmail.com') + po1 = create_purchase_order(supplier='Supplier1') + po2 = create_purchase_order(supplier='Supplier2') + + create_custom_doctype() + create_webform() + create_order_assignment(supplier='Supplier1', po = po1.name) + create_order_assignment(supplier='Supplier2', po = po2.name) + + frappe.set_user("Administrator") + # checking if data consist of all order assignment of Supplier1 and Supplier2 + self.assertTrue('Supplier1' and 'Supplier2' in [data.supplier for data in get_data()]) + + frappe.set_user("supplier1@gmail.com") + # checking if data only consist of order assignment of Supplier1 + self.assertTrue('Supplier1' in [data.supplier for data in get_data()]) + self.assertFalse([data.supplier for data in get_data() if data.supplier != 'Supplier1']) + + frappe.set_user("supplier2@gmail.com") + # checking if data only consist of order assignment of Supplier2 + self.assertTrue('Supplier2' in [data.supplier for data in get_data()]) + self.assertFalse([data.supplier for data in get_data() if data.supplier != 'Supplier2']) + + frappe.set_user("Administrator") + +def get_data(): + webform_list_contexts = frappe.get_hooks('webform_list_context') + if webform_list_contexts: + context = frappe._dict(frappe.get_attr(webform_list_contexts[0])('Buying') or {}) + kwargs = dict(doctype='Order Assignment', order_by = 'modified desc') + return context.get_list(**kwargs) + +def create_user(name, email): + frappe.get_doc({ + 'doctype': 'User', + 'send_welcome_email': 0, + 'user_type': 'Website User', + 'first_name': name, + 'email': email, + 'roles': [{"doctype": "Has Role", "role": "Supplier"}] + }).insert(ignore_if_duplicate = True) + +def create_supplier_with_contact(name, group, contact_name, contact_email): + supplier = frappe.get_doc({ + 'doctype': 'Supplier', + 'supplier_name': name, + 'supplier_group': group + }).insert(ignore_if_duplicate = True) + + if not frappe.db.exists('Contact', contact_name+'-1-'+name): + new_contact = frappe.new_doc("Contact") + new_contact.first_name = contact_name + new_contact.is_primary_contact = True, + new_contact.append('links', { + "link_doctype": "Supplier", + "link_name": supplier.name + }) + new_contact.append('email_ids', { + "email_id": contact_email, + "is_primary": 1 + }) + + new_contact.insert(ignore_mandatory=True) + +def create_custom_doctype(): + frappe.get_doc({ + 'doctype': 'DocType', + 'name': 'Order Assignment', + 'module': 'Buying', + 'custom': 1, + 'autoname': 'field:po', + 'fields': [ + {'label': 'PO', 'fieldname': 'po', 'fieldtype': 'Link', 'options': 'Purchase Order'}, + {'label': 'Supplier', 'fieldname': 'supplier', 'fieldtype': 'Data', "fetch_from": "po.supplier"} + ], + 'permissions': [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "Supplier" + } + ] + }).insert(ignore_if_duplicate = True) + +def create_webform(): + frappe.get_doc({ + 'doctype': 'Web Form', + 'module': 'Buying', + 'title': 'SO Schedule', + 'route': 'so-schedule', + 'doc_type': 'Order Assignment', + 'web_form_fields': [ + { + 'doctype': 'Web Form Field', + 'fieldname': 'po', + 'fieldtype': 'Link', + 'options': 'Purchase Order', + 'label': 'PO' + }, + { + 'doctype': 'Web Form Field', + 'fieldname': 'supplier', + 'fieldtype': 'Data', + 'label': 'Supplier' + } + ] + + }).insert(ignore_if_duplicate = True) + +def create_order_assignment(supplier, po): + frappe.get_doc({ + 'doctype': 'Order Assignment', + 'po': po, + 'supplier': supplier, + }).insert(ignore_if_duplicate = True) \ No newline at end of file diff --git a/erpnext/www/all-products/index.html b/erpnext/www/all-products/index.html index 7c18ecc41f..a7838eebc1 100644 --- a/erpnext/www/all-products/index.html +++ b/erpnext/www/all-products/index.html @@ -98,14 +98,14 @@