Merge branch 'develop' into job-card-excess-transfer

This commit is contained in:
Marica 2021-09-14 16:50:06 +05:30 committed by GitHub
commit de9f78350b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 657 additions and 202 deletions

View File

@ -425,7 +425,10 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
status: ["!=", "Stopped"], status: ["!=", "Stopped"],
per_ordered: ["<", 100], per_ordered: ["<", 100],
company: me.frm.doc.company company: me.frm.doc.company
} },
allow_child_item_selection: true,
child_fielname: "items",
child_columns: ["item_code", "qty"]
}) })
}, __("Get Items From")); }, __("Get Items From"));

View File

@ -433,12 +433,12 @@
"image_field": "image", "image_field": "image",
"links": [ "links": [
{ {
"group": "Item Group", "group": "Allowed Items",
"link_doctype": "Supplier Item Group", "link_doctype": "Party Specific Item",
"link_fieldname": "supplier" "link_fieldname": "party"
} }
], ],
"modified": "2021-08-27 18:02:44.314077", "modified": "2021-09-06 17:37:56.522233",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier", "name": "Supplier",

View File

@ -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
}

View File

@ -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."))

View File

@ -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

View File

@ -7,6 +7,7 @@ import json
from collections import defaultdict from collections import defaultdict
import frappe import frappe
from frappe import scrub
from frappe.desk.reportview import get_filters_cond, get_match_cond from frappe.desk.reportview import get_filters_cond, get_match_cond
from frappe.utils import nowdate, unique 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] if not field in searchfields]
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields]) searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
if filters and isinstance(filters, dict) and filters.get('supplier'): if filters and isinstance(filters, dict):
item_group_list = frappe.get_all('Supplier Item Group', if filters.get('customer') or filters.get('supplier'):
filters = {'supplier': filters.get('supplier')}, fields = ['item_group']) 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 = [] filters_dict = {}
for i in item_group_list: for rule in item_rules_list:
item_groups.append(i.item_group) if rule['restrict_based_on'] == 'Item':
rule['restrict_based_on'] = 'name'
filters_dict[rule.restrict_based_on] = []
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'] del filters['supplier']
if item_groups:
filters['item_group'] = ['in', item_groups]
description_cond = '' description_cond = ''
if frappe.db.count('Item', cache=True) < 50000: if frappe.db.count('Item', cache=True) < 50000:

View File

@ -7,6 +7,7 @@ import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.modules.utils import get_module_app
from frappe.utils import flt, has_common from frappe.utils import flt, has_common
from frappe.utils.user import is_website_user from frappe.utils.user import is_website_user
@ -21,8 +22,32 @@ def get_list_context(context=None):
"get_list": get_transaction_list "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 user = frappe.session.user
ignore_permissions = False 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)) filters.append(('customer', 'in', customers))
elif suppliers: elif suppliers:
filters.append(('supplier', 'in', suppliers)) filters.append(('supplier', 'in', suppliers))
else: elif not custom:
return [] return []
if doctype == 'Request for Quotation': 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 # Since customers and supplier do not have direct access to internal doctypes
ignore_permissions = True 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, transactions = get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length,
fields='name', ignore_permissions=ignore_permissions, order_by='modified desc') fields='name', ignore_permissions=ignore_permissions, order_by='modified desc')
if custom:
return transactions
return post_process(doctype, transactions) return post_process(doctype, transactions)
def get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length=20, def get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length=20,

View File

@ -62,6 +62,7 @@ treeviews = ['Account', 'Cost Center', 'Warehouse', 'Item Group', 'Customer Grou
# website # website
update_website_context = ["erpnext.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.update_website_context"] 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" 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"] calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "Course Schedule"]

View File

@ -73,7 +73,7 @@ frappe.ui.form.on('Employee Advance', {
frm.trigger('make_return_entry'); frm.trigger('make_return_entry');
}, __('Create')); }, __('Create'));
} else if (frm.doc.repay_unclaimed_amount_from_salary == 1 && frappe.model.can_create("Additional Salary")) { } 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); frm.events.make_deduction_via_additional_salary(frm);
}, __('Create')); }, __('Create'));
} }

View File

@ -170,7 +170,7 @@
"default": "0", "default": "0",
"fieldname": "repay_unclaimed_amount_from_salary", "fieldname": "repay_unclaimed_amount_from_salary",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Repay unclaimed amount from salary" "label": "Repay Unclaimed Amount from Salary"
}, },
{ {
"depends_on": "eval:cur_frm.doc.employee", "depends_on": "eval:cur_frm.doc.employee",
@ -200,10 +200,11 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-31 22:31:53.746659", "modified": "2021-09-11 18:38:38.617478",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee Advance", "name": "Employee Advance",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {

View File

@ -172,7 +172,10 @@ def get_paying_amount_paying_exchange_rate(payment_account, doc):
@frappe.whitelist() @frappe.whitelist()
def create_return_through_additional_salary(doc): def create_return_through_additional_salary(doc):
import json import json
if isinstance(doc, str):
doc = frappe._dict(json.loads(doc)) doc = frappe._dict(json.loads(doc))
additional_salary = frappe.new_doc('Additional Salary') additional_salary = frappe.new_doc('Additional Salary')
additional_salary.employee = doc.employee additional_salary.employee = doc.employee
additional_salary.currency = doc.currency additional_salary.currency = doc.currency

View File

@ -12,8 +12,11 @@ import erpnext
from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.employee_advance.employee_advance import ( from erpnext.hr.doctype.employee_advance.employee_advance import (
EmployeeAdvanceOverPayment, EmployeeAdvanceOverPayment,
create_return_through_additional_salary,
make_bank_entry, 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): class TestEmployeeAdvance(unittest.TestCase):
@ -33,6 +36,46 @@ class TestEmployeeAdvance(unittest.TestCase):
journal_entry1 = make_payment_entry(advance) journal_entry1 = make_payment_entry(advance)
self.assertRaises(EmployeeAdvanceOverPayment, journal_entry1.submit) 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): def make_payment_entry(advance):
journal_entry = frappe.get_doc(make_bank_entry("Employee Advance", advance.name)) journal_entry = frappe.get_doc(make_bank_entry("Employee Advance", advance.name))
journal_entry.cheque_no = "123123" journal_entry.cheque_no = "123123"
@ -41,7 +84,7 @@ def make_payment_entry(advance):
return journal_entry return journal_entry
def make_employee_advance(employee_name): def make_employee_advance(employee_name, args=None):
doc = frappe.new_doc("Employee Advance") doc = frappe.new_doc("Employee Advance")
doc.employee = employee_name doc.employee = employee_name
doc.company = "_Test company" doc.company = "_Test company"
@ -51,6 +94,10 @@ def make_employee_advance(employee_name):
doc.advance_amount = 1000 doc.advance_amount = 1000
doc.posting_date = nowdate() doc.posting_date = nowdate()
doc.advance_account = "_Test Employee Advance - _TC" doc.advance_account = "_Test Employee Advance - _TC"
if args:
doc.update(args)
doc.insert() doc.insert()
doc.submit() doc.submit()

View File

@ -32,7 +32,10 @@ def set_employee_name(doc):
def update_employee(employee, details, date=None, cancel=False): def update_employee(employee, details, date=None, cancel=False):
internal_work_history = {} internal_work_history = {}
for item in details: 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 new_data = item.new if not cancel else item.current
if fieldtype == "Date" and new_data: if fieldtype == "Date" and new_data:
new_data = getdate(new_data) new_data = getdate(new_data)

View File

@ -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.validate_options_for_data_field
erpnext.patches.v13_0.create_gst_payment_entry_fields erpnext.patches.v13_0.create_gst_payment_entry_fields
erpnext.patches.v14_0.delete_shopify_doctypes 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.v13_0.update_dates_in_tax_withholding_category
erpnext.patches.v14_0.update_opportunity_currency_fields erpnext.patches.v14_0.update_opportunity_currency_fields

View File

@ -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()

View File

@ -14,12 +14,11 @@ from erpnext.hr.utils import validate_active_employee
class AdditionalSalary(Document): class AdditionalSalary(Document):
def on_submit(self): def on_submit(self):
if self.ref_doctype == "Employee Advance" and self.ref_docname: self.update_return_amount_in_employee_advance()
frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", self.amount)
self.update_employee_referral() self.update_employee_referral()
def on_cancel(self): def on_cancel(self):
self.update_return_amount_in_employee_advance()
self.update_employee_referral(cancel=True) self.update_employee_referral(cancel=True)
def validate(self): 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.throw(_("Additional Salary for referral bonus can only be created against Employee Referral with status {0}").format(
frappe.bold("Accepted"))) 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): def update_employee_referral(self, cancel=False):
if self.ref_doctype == "Employee Referral": if self.ref_doctype == "Employee Referral":
status = "Unpaid" if cancel else "Paid" status = "Unpaid" if cancel else "Paid"

View File

@ -487,7 +487,7 @@ class SalarySlip(TransactionBase):
self.calculate_component_amounts("deductions") self.calculate_component_amounts("deductions")
self.set_loan_repayment() self.set_loan_repayment()
self.set_component_amounts_based_on_payment_days() self.set_precision_for_component_amounts()
self.set_net_pay() self.set_net_pay()
def set_net_pay(self): def set_net_pay(self):
@ -709,6 +709,17 @@ class SalarySlip(TransactionBase):
component_row.amount = amount 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): def calculate_variable_based_on_taxable_salary(self, tax_component, payroll_period):
if not payroll_period: if not payroll_period:
frappe.msgprint(_("Start and end dates not in a valid Payroll Period, cannot calculate {0}.") 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 return total_tax_paid
def get_taxable_earnings(self, allow_tax_exemption=False, based_on_payment_days=0): 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, joining_date, relieving_date = self.get_joining_and_relieving_dates()
["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)))
taxable_earnings = 0 taxable_earnings = 0
additional_income = 0 additional_income = 0
@ -884,7 +888,10 @@ class SalarySlip(TransactionBase):
if based_on_payment_days: if based_on_payment_days:
amount, additional_amount = self.get_amount_based_on_payment_days(earning, joining_date, relieving_date) amount, additional_amount = self.get_amount_based_on_payment_days(earning, joining_date, relieving_date)
else: else:
if earning.additional_amount:
amount, additional_amount = earning.amount, 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 earning.is_tax_applicable:
if additional_amount: if additional_amount:
@ -1055,7 +1062,7 @@ class SalarySlip(TransactionBase):
total += amount total += amount
return total 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, joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
["date_of_joining", "relieving_date"]) ["date_of_joining", "relieving_date"])
@ -1065,9 +1072,7 @@ class SalarySlip(TransactionBase):
if not joining_date: if not joining_date:
frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name))) frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name)))
for component_type in ("earnings", "deductions"): return joining_date, relieving_date
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"))
def set_loan_repayment(self): def set_loan_repayment(self):
self.total_loan_repayment = 0 self.total_loan_repayment = 0

View File

@ -17,6 +17,7 @@ from frappe.utils import (
getdate, getdate,
nowdate, nowdate,
) )
from frappe.utils.make_random import get_random
import erpnext import erpnext
from erpnext.accounts.utils import get_fiscal_year 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") 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): def test_salary_slip_with_holidays_included(self):
no_of_days = self.get_no_of_days() no_of_days = self.get_no_of_days()
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1) 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 holiday_list = holiday_list.name
return holiday_list 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

View File

@ -709,6 +709,9 @@ erpnext.utils.map_current_doc = function(opts) {
setters: opts.setters, setters: opts.setters,
get_query: opts.get_query, get_query: opts.get_query,
add_filters_group: 1, 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) { action: function(selections, args) {
let values = selections; let values = selections;
if(values.length === 0){ if(values.length === 0){
@ -716,7 +719,7 @@ erpnext.utils.map_current_doc = function(opts) {
return; return;
} }
opts.source_name = values; opts.source_name = values;
opts.setters = args; opts.args = args;
d.dialog.hide(); d.dialog.hide();
_map(); _map();
}, },

View File

@ -20,7 +20,6 @@
"tax_withholding_category", "tax_withholding_category",
"default_bank_account", "default_bank_account",
"lead_name", "lead_name",
"prospect",
"opportunity_name", "opportunity_name",
"image", "image",
"column_break0", "column_break0",
@ -214,7 +213,8 @@
"fieldtype": "Link", "fieldtype": "Link",
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
"label": "Represents Company", "label": "Represents Company",
"options": "Company" "options": "Company",
"unique": 1
}, },
{ {
"depends_on": "represents_company", "depends_on": "represents_company",
@ -497,14 +497,6 @@
"label": "Tax Withholding Category", "label": "Tax Withholding Category",
"options": "Tax Withholding Category" "options": "Tax Withholding Category"
}, },
{
"fieldname": "prospect",
"fieldtype": "Link",
"label": "Prospect",
"no_copy": 1,
"options": "Prospect",
"print_hide": 1
},
{ {
"fieldname": "opportunity_name", "fieldname": "opportunity_name",
"fieldtype": "Link", "fieldtype": "Link",
@ -518,8 +510,14 @@
"idx": 363, "idx": 363,
"image_field": "image", "image_field": "image",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [
"modified": "2021-08-25 18:56:09.929905", {
"group": "Allowed Items",
"link_doctype": "Party Specific Item",
"link_fieldname": "party"
}
],
"modified": "2021-09-06 17:38:54.196663",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Customer", "name": "Customer",

View File

@ -1,7 +1,7 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Supplier Item Group', { frappe.ui.form.on('Party Specific Item', {
// refresh: function(frm) { // refresh: function(frm) {
// } // }

View File

@ -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
}

View File

@ -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))

View File

@ -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)

View File

@ -73,7 +73,7 @@ def get_data(conditions, filters):
`tabSales Order` so, `tabSales Order` so,
`tabSales Order Item` soi `tabSales Order Item` soi
LEFT JOIN `tabSales Invoice Item` sii LEFT JOIN `tabSales Invoice Item` sii
ON sii.so_detail = soi.name ON sii.so_detail = soi.name and sii.docstatus = 1
WHERE WHERE
soi.parent = so.name soi.parent = so.name
and so.status not in ('Stopped', 'Closed', 'On Hold') and so.status not in ('Stopped', 'Closed', 'On Hold')

View File

@ -63,7 +63,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
this.frm.set_query("item_code", "items", function() { this.frm.set_query("item_code", "items", function() {
return { return {
query: "erpnext.controllers.queries.item_query", 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")); var editable_price_list_rate = cint(frappe.defaults.get_default("editable_price_list_rate"));
if(df && 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
);
} }
} }

View File

@ -99,7 +99,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
filter_engine = ProductFiltersBuilder(self.name) filter_engine = ProductFiltersBuilder(self.name)
context.field_filters = filter_engine.get_field_filters() 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({ context.update({
"parents": get_parent_item_groups(self.parent_item_group), "parents": get_parent_item_groups(self.parent_item_group),

View File

@ -4,7 +4,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _dict
class ProductFiltersBuilder: class ProductFiltersBuilder:
@ -57,37 +56,31 @@ class ProductFiltersBuilder:
return filter_data return filter_data
def get_attribute_fitlers(self): def get_attribute_filters(self):
attributes = [row.attribute for row in self.doc.filter_attributes] 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: result = frappe.db.sql(
selected_attributes = [] """
for attr in attr_doc.item_attribute_values: select
or_filters = [] distinct attribute, attribute_value
filters= [ from
["Item Variant Attribute", "attribute", "=", attr.parent], `tabItem Variant Attribute`
["Item Variant Attribute", "attribute_value", "=", attr.attribute_value] where
] attribute in %(attributes)s
if self.item_group: and attribute_value is not null
or_filters.extend([ """,
["item_group", "=", self.item_group], {"attributes": attributes},
["Website Item Group", "item_group", "=", self.item_group] as_dict=1,
])
if frappe.db.get_all("Item", filters, or_filters=or_filters, limit=1):
selected_attributes.append(attr)
if selected_attributes:
valid_attributes.append(
_dict(
item_attribute_values=selected_attributes,
name=attr_doc.name
)
) )
return valid_attributes attribute_value_map = {}
for d in result:
attribute_value_map.setdefault(d.attribute, []).append(d.attribute_value)
out = []
for name, values in attribute_value_map.items():
out.append(frappe._dict(name=name, item_attribute_values=values))
return out

View File

@ -6,10 +6,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import json
import frappe import frappe
from frappe import _, msgprint from frappe import _, msgprint
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.utils import cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate 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.buying.utils import check_on_hold_or_closed_status, validate_for_items
from erpnext.controllers.buying_controller import BuyingController from erpnext.controllers.buying_controller import BuyingController
@ -269,7 +272,10 @@ def update_status(name, status):
material_request.update_status(status) material_request.update_status(status)
@frappe.whitelist() @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): def postprocess(source, target_doc):
if frappe.flags.args and frappe.flags.args.default_supplier: 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) set_missing_values(source, target_doc)
def select_item(d): 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, { doclist = get_mapped_doc("Material Request", source_name, {
"Material Request": { "Material Request": {

View File

@ -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)

View File

@ -98,14 +98,14 @@
<div class="filter-options"> <div class="filter-options">
{% for attr_value in attribute.item_attribute_values %} {% for attr_value in attribute.item_attribute_values %}
<div class="checkbox"> <div class="checkbox">
<label data-value="{{ value }}"> <label>
<input type="checkbox" <input type="checkbox"
class="product-filter attribute-filter" class="product-filter attribute-filter"
id="{{attr_value.name}}" id="{{attr_value}}"
data-attribute-name="{{ attribute.name }}" data-attribute-name="{{ attribute.name }}"
data-attribute-value="{{ attr_value.attribute_value }}" data-attribute-value="{{ attr_value }}"
{% if attr_value.checked %} checked {% endif %}> {% if attr_value.checked %} checked {% endif %}>
<span class="label-area">{{ attr_value.attribute_value }}</span> <span class="label-area">{{ attr_value }}</span>
</label> </label>
</div> </div>
{% endfor %} {% endfor %}

View File

@ -27,7 +27,7 @@ def get_context(context):
filter_engine = ProductFiltersBuilder() filter_engine = ProductFiltersBuilder()
context.field_filters = filter_engine.get_field_filters() 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.product_settings = product_settings context.product_settings = product_settings
context.body_class = "product-page" context.body_class = "product-page"