Merge branch 'version-13-hotfix' of https://github.com/frappe/erpnext into tds_validity_v13

This commit is contained in:
Deepesh Garg
2021-09-14 15:03:30 +05:30
48 changed files with 4937 additions and 3772 deletions

8
.snyk
View File

@@ -1,8 +0,0 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.14.0
ignore: {}
# patches apply the minimum changes required to fix a vulnerability
patch:
SNYK-JS-LODASH-450202:
- cypress > getos > async > lodash:
patched: '2020-01-31T01:35:12.802Z'

View File

@@ -219,6 +219,7 @@
},
{
"default": "1",
"description": "A customer must have primary contact email.",
"fieldname": "primary_mandatory",
"fieldtype": "Check",
"label": "Send To Primary Contact"
@@ -286,7 +287,7 @@
}
],
"links": [],
"modified": "2021-05-21 10:14:22.426672",
"modified": "2021-09-06 21:00:45.732505",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",

View File

@@ -196,7 +196,10 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
primary_email = customer.get('email_id') or ''
billing_email = get_customer_emails(customer.name, 1, billing_and_primary=False)
if billing_email == '' or (primary_email == '' and int(primary_mandatory)):
if int(primary_mandatory):
if (primary_email == ''):
continue
elif (billing_email == '') and (primary_email == ''):
continue
customer_list.append({
@@ -208,10 +211,29 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
@frappe.whitelist()
def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True):
""" Returns first email from Contact Email table as a Billing email
when Is Billing Contact checked
and Primary email- email with Is Primary checked """
billing_email = frappe.db.sql("""
SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent
WHERE l.link_doctype='Customer' and l.link_name=%s and c.is_billing_contact=1
order by c.creation desc""", customer_name)
SELECT
email.email_id
FROM
`tabContact Email` AS email
JOIN
`tabDynamic Link` AS link
ON
email.parent=link.parent
JOIN
`tabContact` AS contact
ON
contact.name=link.parent
WHERE
link.link_doctype='Customer'
and link.link_name=%s
and contact.is_billing_contact=1
ORDER BY
contact.creation desc""", customer_name)
if len(billing_email) == 0 or (billing_email[0][0] is None):
if billing_and_primary:

View File

@@ -4,7 +4,7 @@
frappe.query_reports["Accounts Payable"] = {
"filters": [
{
"fieldname":"company",
"fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
@@ -12,19 +12,19 @@ frappe.query_reports["Accounts Payable"] = {
"default": frappe.defaults.get_user_default("Company")
},
{
"fieldname":"report_date",
"fieldname": "report_date",
"label": __("Posting Date"),
"fieldtype": "Date",
"default": frappe.datetime.get_today()
},
{
"fieldname":"finance_book",
"fieldname": "finance_book",
"label": __("Finance Book"),
"fieldtype": "Link",
"options": "Finance Book"
},
{
"fieldname":"cost_center",
"fieldname": "cost_center",
"label": __("Cost Center"),
"fieldtype": "Link",
"options": "Cost Center",
@@ -38,7 +38,7 @@ frappe.query_reports["Accounts Payable"] = {
}
},
{
"fieldname":"supplier",
"fieldname": "supplier",
"label": __("Supplier"),
"fieldtype": "Link",
"options": "Supplier",
@@ -54,48 +54,48 @@ frappe.query_reports["Accounts Payable"] = {
}
},
{
"fieldname":"ageing_based_on",
"fieldname": "ageing_based_on",
"label": __("Ageing Based On"),
"fieldtype": "Select",
"options": 'Posting Date\nDue Date\nSupplier Invoice Date',
"default": "Due Date"
},
{
"fieldname":"range1",
"fieldname": "range1",
"label": __("Ageing Range 1"),
"fieldtype": "Int",
"default": "30",
"reqd": 1
},
{
"fieldname":"range2",
"fieldname": "range2",
"label": __("Ageing Range 2"),
"fieldtype": "Int",
"default": "60",
"reqd": 1
},
{
"fieldname":"range3",
"fieldname": "range3",
"label": __("Ageing Range 3"),
"fieldtype": "Int",
"default": "90",
"reqd": 1
},
{
"fieldname":"range4",
"fieldname": "range4",
"label": __("Ageing Range 4"),
"fieldtype": "Int",
"default": "120",
"reqd": 1
},
{
"fieldname":"payment_terms_template",
"fieldname": "payment_terms_template",
"label": __("Payment Terms Template"),
"fieldtype": "Link",
"options": "Payment Terms Template"
},
{
"fieldname":"supplier_group",
"fieldname": "supplier_group",
"label": __("Supplier Group"),
"fieldtype": "Link",
"options": "Supplier Group"
@@ -106,12 +106,17 @@ frappe.query_reports["Accounts Payable"] = {
"fieldtype": "Check"
},
{
"fieldname":"based_on_payment_terms",
"fieldname": "based_on_payment_terms",
"label": __("Based On Payment Terms"),
"fieldtype": "Check",
},
{
"fieldname":"tax_id",
"fieldname": "show_remarks",
"label": __("Show Remarks"),
"fieldtype": "Check",
},
{
"fieldname": "tax_id",
"label": __("Tax Id"),
"fieldtype": "Data",
"hidden": 1

View File

@@ -4,7 +4,7 @@
frappe.query_reports["Accounts Receivable"] = {
"filters": [
{
"fieldname":"company",
"fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
@@ -12,19 +12,19 @@ frappe.query_reports["Accounts Receivable"] = {
"default": frappe.defaults.get_user_default("Company")
},
{
"fieldname":"report_date",
"fieldname": "report_date",
"label": __("Posting Date"),
"fieldtype": "Date",
"default": frappe.datetime.get_today()
},
{
"fieldname":"finance_book",
"fieldname": "finance_book",
"label": __("Finance Book"),
"fieldtype": "Link",
"options": "Finance Book"
},
{
"fieldname":"cost_center",
"fieldname": "cost_center",
"label": __("Cost Center"),
"fieldtype": "Link",
"options": "Cost Center",
@@ -38,7 +38,7 @@ frappe.query_reports["Accounts Receivable"] = {
}
},
{
"fieldname":"customer",
"fieldname": "customer",
"label": __("Customer"),
"fieldtype": "Link",
"options": "Customer",
@@ -67,66 +67,66 @@ frappe.query_reports["Accounts Receivable"] = {
}
},
{
"fieldname":"ageing_based_on",
"fieldname": "ageing_based_on",
"label": __("Ageing Based On"),
"fieldtype": "Select",
"options": 'Posting Date\nDue Date',
"default": "Due Date"
},
{
"fieldname":"range1",
"fieldname": "range1",
"label": __("Ageing Range 1"),
"fieldtype": "Int",
"default": "30",
"reqd": 1
},
{
"fieldname":"range2",
"fieldname": "range2",
"label": __("Ageing Range 2"),
"fieldtype": "Int",
"default": "60",
"reqd": 1
},
{
"fieldname":"range3",
"fieldname": "range3",
"label": __("Ageing Range 3"),
"fieldtype": "Int",
"default": "90",
"reqd": 1
},
{
"fieldname":"range4",
"fieldname": "range4",
"label": __("Ageing Range 4"),
"fieldtype": "Int",
"default": "120",
"reqd": 1
},
{
"fieldname":"customer_group",
"fieldname": "customer_group",
"label": __("Customer Group"),
"fieldtype": "Link",
"options": "Customer Group"
},
{
"fieldname":"payment_terms_template",
"fieldname": "payment_terms_template",
"label": __("Payment Terms Template"),
"fieldtype": "Link",
"options": "Payment Terms Template"
},
{
"fieldname":"sales_partner",
"fieldname": "sales_partner",
"label": __("Sales Partner"),
"fieldtype": "Link",
"options": "Sales Partner"
},
{
"fieldname":"sales_person",
"fieldname": "sales_person",
"label": __("Sales Person"),
"fieldtype": "Link",
"options": "Sales Person"
},
{
"fieldname":"territory",
"fieldname": "territory",
"label": __("Territory"),
"fieldtype": "Link",
"options": "Territory"
@@ -137,45 +137,50 @@ frappe.query_reports["Accounts Receivable"] = {
"fieldtype": "Check"
},
{
"fieldname":"based_on_payment_terms",
"fieldname": "based_on_payment_terms",
"label": __("Based On Payment Terms"),
"fieldtype": "Check",
},
{
"fieldname":"show_future_payments",
"fieldname": "show_future_payments",
"label": __("Show Future Payments"),
"fieldtype": "Check",
},
{
"fieldname":"show_delivery_notes",
"fieldname": "show_delivery_notes",
"label": __("Show Linked Delivery Notes"),
"fieldtype": "Check",
},
{
"fieldname":"show_sales_person",
"fieldname": "show_sales_person",
"label": __("Show Sales Person"),
"fieldtype": "Check",
},
{
"fieldname":"tax_id",
"fieldname": "show_remarks",
"label": __("Show Remarks"),
"fieldtype": "Check",
},
{
"fieldname": "tax_id",
"label": __("Tax Id"),
"fieldtype": "Data",
"hidden": 1
},
{
"fieldname":"customer_name",
"fieldname": "customer_name",
"label": __("Customer Name"),
"fieldtype": "Data",
"hidden": 1
},
{
"fieldname":"payment_terms",
"fieldname": "payment_terms",
"label": __("Payment Tems"),
"fieldtype": "Data",
"hidden": 1
},
{
"fieldname":"credit_limit",
"fieldname": "credit_limit",
"label": __("Credit Limit"),
"fieldtype": "Currency",
"hidden": 1

View File

@@ -106,6 +106,7 @@ class ReceivablePayableReport(object):
party = gle.party,
posting_date = gle.posting_date,
account_currency = gle.account_currency,
remarks = gle.remarks if self.filters.get("show_remarks") else None,
invoiced = 0.0,
paid = 0.0,
credit_note = 0.0,
@@ -583,10 +584,12 @@ class ReceivablePayableReport(object):
else:
select_fields = "debit, credit"
remarks = ", remarks" if self.filters.get("show_remarks") else ""
self.gl_entries = frappe.db.sql("""
select
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
against_voucher_type, against_voucher, account_currency, {0}
against_voucher_type, against_voucher, account_currency, {0} {remarks}
from
`tabGL Entry`
where
@@ -595,7 +598,7 @@ class ReceivablePayableReport(object):
and party_type=%s
and (party is not null and party != '')
{1} {2} {3}"""
.format(select_fields, date_condition, conditions, order_by), values, as_dict=True)
.format(select_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True)
def get_sales_invoices_or_customers_based_on_sales_person(self):
if self.filters.get("sales_person"):
@@ -754,6 +757,10 @@ class ReceivablePayableReport(object):
self.add_column(label=_('Voucher Type'), fieldname='voucher_type', fieldtype='Data')
self.add_column(label=_('Voucher No'), fieldname='voucher_no', fieldtype='Dynamic Link',
options='voucher_type', width=180)
if self.filters.show_remarks:
self.add_column(label=_('Remarks'), fieldname='remarks', fieldtype='Text', width=200),
self.add_column(label='Due Date', fieldtype='Date')
if self.party_type == "Supplier":

View File

@@ -425,7 +425,10 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
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"));

View File

@@ -307,7 +307,7 @@ def bom(doctype, txt, searchfield, start, page_len, filters):
@frappe.validate_and_sanitize_search_inputs
def get_project_name(doctype, txt, searchfield, start, page_len, filters):
cond = ''
if filters.get('customer'):
if filters and filters.get('customer'):
cond = """(`tabProject`.customer = %s or
ifnull(`tabProject`.customer,"")="") and""" %(frappe.db.escape(filters.get("customer")))

View File

@@ -0,0 +1,87 @@
import unittest
from functools import partial
from erpnext.controllers import queries
def add_default_params(func, doctype):
return partial(
func, doctype=doctype, txt="", searchfield="name", start=0, page_len=20, filters=None
)
class TestQueries(unittest.TestCase):
# All tests are based on doctype/test_records.json
def assert_nested_in(self, item, container):
self.assertIn(item, [vals for tuples in container for vals in tuples])
def test_employee_query(self):
query = add_default_params(queries.employee_query, "Employee")
self.assertGreaterEqual(len(query(txt="_Test Employee")), 3)
self.assertGreaterEqual(len(query(txt="_Test Employee 1")), 1)
def test_lead_query(self):
query = add_default_params(queries.lead_query, "Lead")
self.assertGreaterEqual(len(query(txt="_Test Lead")), 4)
self.assertEqual(len(query(txt="_Test Lead 4")), 1)
def test_customer_query(self):
query = add_default_params(queries.customer_query, "Customer")
self.assertGreaterEqual(len(query(txt="_Test Customer")), 7)
self.assertGreaterEqual(len(query(txt="_Test Customer USD")), 1)
def test_supplier_query(self):
query = add_default_params(queries.supplier_query, "Supplier")
self.assertGreaterEqual(len(query(txt="_Test Supplier")), 7)
self.assertGreaterEqual(len(query(txt="_Test Supplier USD")), 1)
def test_item_query(self):
query = add_default_params(queries.item_query, "Item")
self.assertGreaterEqual(len(query(txt="_Test Item")), 7)
self.assertEqual(len(query(txt="_Test Item Home Desktop 100 3")), 1)
fg_item = "_Test FG Item"
stock_items = query(txt=fg_item, filters={"is_stock_item": 1})
self.assert_nested_in("_Test FG Item", stock_items)
bundled_stock_items = query(txt="_test product bundle item 5", filters={"is_stock_item": 1})
self.assertEqual(len(bundled_stock_items), 0)
def test_bom_qury(self):
query = add_default_params(queries.bom, "BOM")
self.assertGreaterEqual(len(query(txt="_Test Item Home Desktop Manufactured")), 1)
def test_project_query(self):
query = add_default_params(queries.get_project_name, "BOM")
self.assertGreaterEqual(len(query(txt="_Test Project")), 1)
def test_account_query(self):
query = add_default_params(queries.get_account_list, "Account")
debtor_accounts = query(txt="Debtors", filters={"company": "_Test Company"})
self.assert_nested_in("Debtors - _TC", debtor_accounts)
def test_income_account_query(self):
query = add_default_params(queries.get_income_account, "Account")
self.assertGreaterEqual(len(query(filters={"company": "_Test Company"})), 1)
def test_expense_account_query(self):
query = add_default_params(queries.get_expense_account, "Account")
self.assertGreaterEqual(len(query(filters={"company": "_Test Company"})), 1)
def test_warehouse_query(self):
query = add_default_params(queries.warehouse_query, "Account")
wh = query(filters=[["Bin", "item_code", "=", "_Test Item"]])
self.assertGreaterEqual(len(wh), 1)

View File

@@ -138,7 +138,7 @@ class WebsiteItem(WebsiteGenerator):
self.website_image = None
def make_thumbnail(self):
if frappe.flags.in_import:
if frappe.flags.in_import or frappe.flags.in_migrate:
return
"""Make a thumbnail of `website_image`"""

View File

@@ -4,6 +4,7 @@ import frappe
import taxjar
from frappe import _
from frappe.contacts.doctype.address.address import get_company_address
from frappe.utils import cint
from erpnext import get_default_company
@@ -14,6 +15,10 @@ TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_cal
SUPPORTED_COUNTRY_CODES = ["AT", "AU", "BE", "BG", "CA", "CY", "CZ", "DE", "DK", "EE", "ES", "FI",
"FR", "GB", "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV", "MT", "NL", "PL", "PT", "RO",
"SE", "SI", "SK", "US"]
SUPPORTED_STATE_CODES = ['AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'DC', 'FL', 'GA', 'HI', 'ID', 'IL',
'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD', 'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE',
'NV', 'NH', 'NJ', 'NM', 'NY', 'NC', 'ND', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC', 'SD',
'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY']
def get_client():
@@ -27,7 +32,11 @@ def get_client():
api_url = taxjar.SANDBOX_API_URL
if api_key and api_url:
return taxjar.Client(api_key=api_key, api_url=api_url)
client = taxjar.Client(api_key=api_key, api_url=api_url)
client.set_api_config('headers', {
'x-api-version': '2020-08-07'
})
return client
def create_transaction(doc, method):
@@ -57,7 +66,10 @@ def create_transaction(doc, method):
tax_dict['amount'] = doc.total + tax_dict['shipping']
try:
client.create_order(tax_dict)
if doc.is_return:
client.create_refund(tax_dict)
else:
client.create_order(tax_dict)
except taxjar.exceptions.TaxJarResponseError as err:
frappe.throw(_(sanitize_error_response(err)))
except Exception as ex:
@@ -89,13 +101,15 @@ def get_tax_data(doc):
to_country_code = frappe.db.get_value("Country", to_address.country, "code")
to_country_code = to_country_code.upper()
if to_country_code not in SUPPORTED_COUNTRY_CODES:
return
shipping = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == SHIP_ACCOUNT_HEAD])
if to_shipping_state is not None:
to_shipping_state = get_iso_3166_2_state_code(to_address)
line_items = [get_line_item_dict(item) for item in doc.items]
if from_shipping_state not in SUPPORTED_STATE_CODES:
from_shipping_state = get_state_code(from_address, 'Company')
if to_shipping_state not in SUPPORTED_STATE_CODES:
to_shipping_state = get_state_code(to_address, 'Shipping')
tax_dict = {
'from_country': from_country_code,
@@ -109,11 +123,29 @@ def get_tax_data(doc):
'to_street': to_address.address_line1,
'to_state': to_shipping_state,
'shipping': shipping,
'amount': doc.net_total
'amount': doc.net_total,
'plugin': 'erpnext',
'line_items': line_items
}
return tax_dict
def get_state_code(address, location):
if address is not None:
state_code = get_iso_3166_2_state_code(address)
if state_code not in SUPPORTED_STATE_CODES:
frappe.throw(_("Please enter a valid State in the {0} Address").format(location))
else:
frappe.throw(_("Please enter a valid State in the {0} Address").format(location))
return state_code
def get_line_item_dict(item):
return dict(
id = item.get('idx'),
quantity = item.get('qty'),
unit_price = item.get('rate'),
product_tax_code = item.get('product_tax_category')
)
def set_sales_tax(doc, method):
if not TAXJAR_CALCULATE_TAX:
@@ -122,17 +154,7 @@ def set_sales_tax(doc, method):
if not doc.items:
return
# if the party is exempt from sales tax, then set all tax account heads to zero
sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \
or frappe.db.has_column("Customer", "exempt_from_sales_tax") and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax")
if sales_tax_exempted:
for tax in doc.taxes:
if tax.account_head == TAX_ACCOUNT_HEAD:
tax.tax_amount = 0
break
doc.run_method("calculate_taxes_and_totals")
if check_sales_tax_exemption(doc):
return
tax_dict = get_tax_data(doc)
@@ -143,7 +165,6 @@ def set_sales_tax(doc, method):
return
tax_data = validate_tax_request(tax_dict)
if tax_data is not None:
if not tax_data.amount_to_collect:
setattr(doc, "taxes", [tax for tax in doc.taxes if tax.account_head != TAX_ACCOUNT_HEAD])
@@ -163,9 +184,28 @@ def set_sales_tax(doc, method):
"account_head": TAX_ACCOUNT_HEAD,
"tax_amount": tax_data.amount_to_collect
})
# Assigning values to tax_collectable and taxable_amount fields in sales item table
for item in tax_data.breakdown.line_items:
doc.get('items')[cint(item.id)-1].tax_collectable = item.tax_collectable
doc.get('items')[cint(item.id)-1].taxable_amount = item.taxable_amount
doc.run_method("calculate_taxes_and_totals")
def check_sales_tax_exemption(doc):
# if the party is exempt from sales tax, then set all tax account heads to zero
sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \
or frappe.db.has_column("Customer", "exempt_from_sales_tax") \
and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax")
if sales_tax_exempted:
for tax in doc.taxes:
if tax.account_head == TAX_ACCOUNT_HEAD:
tax.tax_amount = 0
break
doc.run_method("calculate_taxes_and_totals")
return True
else:
return False
def validate_tax_request(tax_dict):
"""Return the sales tax that should be collected for a given order."""
@@ -200,6 +240,8 @@ def get_shipping_address_details(doc):
if doc.shipping_address_name:
shipping_address = frappe.get_doc("Address", doc.shipping_address_name)
elif doc.customer_address:
shipping_address = frappe.get_doc("Address", doc.customer_address_name)
else:
shipping_address = get_company_address_details(doc)

View File

@@ -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'));
}

View File

@@ -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,7 +200,7 @@
],
"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",

View File

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

View File

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

View File

@@ -148,7 +148,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)

View File

@@ -31,8 +31,8 @@ frappe.ui.form.on('Maintenance Visit', {
},
onload: function (frm, cdt, cdn) {
let item = locals[cdt][cdn];
if (frm.maintenance_type == 'Scheduled') {
let schedule_id = item.purposes[0].prevdoc_detail_docname;
if (frm.doc.maintenance_type === "Scheduled") {
const schedule_id = item.purposes[0].prevdoc_detail_docname || frm.doc.maintenance_schedule_detail;
frappe.call({
method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.update_serial_nos",
args: {
@@ -43,6 +43,9 @@ frappe.ui.form.on('Maintenance Visit', {
}
});
}
else {
frm.clear_table("purposes");
}
if (!frm.doc.status) {
frm.set_value({ status: 'Draft' });

View File

@@ -91,7 +91,7 @@ class JobCard(Document):
if args.get("employee"):
# override capacity for employee
production_capacity = 1
validate_overlap_for = " and jc.employee = %(employee)s "
validate_overlap_for = " and jctl.employee = %(employee)s "
extra_cond = ''
if check_next_available_slot:

View File

@@ -8,71 +8,91 @@ import unittest
import frappe
from frappe.utils import random_string
from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError
from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError, OverlapError
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
class TestJobCard(unittest.TestCase):
def setUp(self):
self.work_order = make_wo_order_test_record(item="_Test FG Item 2", qty=2)
def tearDown(self):
frappe.db.rollback()
def test_job_card(self):
data = frappe.get_cached_value('BOM',
{'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item'])
if data:
bom, bom_item = data
job_cards = frappe.get_all('Job Card',
filters = {'work_order': self.work_order.name}, fields = ["operation_id", "name"])
work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom)
if job_cards:
job_card = job_cards[0]
frappe.db.set_value("Job Card", job_card.name, "operation_row_number", job_card.operation_id)
job_cards = frappe.get_all('Job Card',
filters = {'work_order': work_order.name}, fields = ["operation_id", "name"])
doc = frappe.get_doc("Job Card", job_card.name)
doc.operation_id = "Test Data"
self.assertRaises(OperationMismatchError, doc.save)
if job_cards:
job_card = job_cards[0]
frappe.db.set_value("Job Card", job_card.name, "operation_row_number", job_card.operation_id)
doc = frappe.get_doc("Job Card", job_card.name)
doc.operation_id = "Test Data"
self.assertRaises(OperationMismatchError, doc.save)
for d in job_cards:
frappe.delete_doc("Job Card", d.name)
for d in job_cards:
frappe.delete_doc("Job Card", d.name)
def test_job_card_with_different_work_station(self):
data = frappe.get_cached_value('BOM',
{'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item'])
job_cards = frappe.get_all('Job Card',
filters = {'work_order': self.work_order.name},
fields = ["operation_id", "workstation", "name", "for_quantity"])
if data:
bom, bom_item = data
job_card = job_cards[0]
work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom)
if job_card:
workstation = frappe.db.get_value("Workstation",
{"name": ("not in", [job_card.workstation])}, "name")
job_cards = frappe.get_all('Job Card',
filters = {'work_order': work_order.name},
fields = ["operation_id", "workstation", "name", "for_quantity"])
if not workstation or job_card.workstation == workstation:
workstation = make_workstation(workstation_name=random_string(5)).name
job_card = job_cards[0]
doc = frappe.get_doc("Job Card", job_card.name)
doc.workstation = workstation
doc.append("time_logs", {
"from_time": "2009-01-01 12:06:25",
"to_time": "2009-01-01 12:37:25",
"time_in_mins": "31.00002",
"completed_qty": job_card.for_quantity
})
doc.submit()
if job_card:
workstation = frappe.db.get_value("Workstation",
{"name": ("not in", [job_card.workstation])}, "name")
completed_qty = frappe.db.get_value("Work Order Operation", job_card.operation_id, "completed_qty")
self.assertEqual(completed_qty, job_card.for_quantity)
if not workstation or job_card.workstation == workstation:
workstation = make_workstation(workstation_name=random_string(5)).name
doc = frappe.get_doc("Job Card", job_card.name)
doc.workstation = workstation
doc.append("time_logs", {
"from_time": "2009-01-01 12:06:25",
"to_time": "2009-01-01 12:37:25",
"time_in_mins": "31.00002",
"completed_qty": job_card.for_quantity
})
doc.submit()
completed_qty = frappe.db.get_value("Work Order Operation", job_card.operation_id, "completed_qty")
self.assertEqual(completed_qty, job_card.for_quantity)
doc.cancel()
doc.cancel()
for d in job_cards:
frappe.delete_doc("Job Card", d.name)
def test_job_card_overlap(self):
wo2 = make_wo_order_test_record(item="_Test FG Item 2", qty=2)
jc1_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
jc2_name = frappe.db.get_value("Job Card", {'work_order': wo2.name})
jc1 = frappe.get_doc("Job Card", jc1_name)
jc2 = frappe.get_doc("Job Card", jc2_name)
employee = "_T-Employee-00001" # from test records
jc1.append("time_logs", {
"from_time": "2021-01-01 00:00:00",
"to_time": "2021-01-01 08:00:00",
"completed_qty": 1,
"employee": employee,
})
jc1.save()
# add a new entry in same time slice
jc2.append("time_logs", {
"from_time": "2021-01-01 00:01:00",
"to_time": "2021-01-01 06:00:00",
"completed_qty": 1,
"employee": employee,
})
self.assertRaises(OverlapError, jc2.save)

View File

@@ -16,10 +16,12 @@
"customer",
"warehouse",
"project",
"sales_order_status",
"column_break2",
"from_date",
"to_date",
"sales_order_status",
"from_delivery_date",
"to_delivery_date",
"sales_orders_detail",
"get_sales_orders",
"sales_orders",
@@ -358,13 +360,23 @@
"fieldname": "get_sub_assembly_items",
"fieldtype": "Button",
"label": "Get Sub Assembly Items"
},
{
"fieldname": "from_delivery_date",
"fieldtype": "Date",
"label": "From Delivery Date"
},
{
"fieldname": "to_delivery_date",
"fieldtype": "Date",
"label": "To Delivery Date"
}
],
"icon": "fa fa-calendar",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-08-23 17:26:03.799876",
"modified": "2021-09-06 18:35:59.642232",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",

View File

@@ -735,43 +735,42 @@ def get_material_request_items(row, sales_order, company,
def get_sales_orders(self):
so_filter = item_filter = ""
bom_item = "bom.item = so_item.item_code"
if self.from_date:
so_filter += " and so.transaction_date >= %(from_date)s"
if self.to_date:
so_filter += " and so.transaction_date <= %(to_date)s"
if self.customer:
so_filter += " and so.customer = %(customer)s"
if self.project:
so_filter += " and so.project = %(project)s"
if self.sales_order_status:
so_filter += "and so.status = %(sales_order_status)s"
date_field_mapper = {
'from_date': ('>=', 'so.transaction_date'),
'to_date': ('<=', 'so.transaction_date'),
'from_delivery_date': ('>=', 'so_item.delivery_date'),
'to_delivery_date': ('<=', 'so_item.delivery_date')
}
for field, value in date_field_mapper.items():
if self.get(field):
so_filter += f" and {value[1]} {value[0]} %({field})s"
for field in ['customer', 'project', 'sales_order_status']:
if self.get(field):
so_field = 'status' if field == 'sales_order_status' else field
so_filter += f" and so.{so_field} = %({field})s"
if self.item_code and frappe.db.exists('Item', self.item_code):
bom_item = self.get_bom_item() or bom_item
item_filter += " and so_item.item_code = %(item)s"
item_filter += " and so_item.item_code = %(item_code)s"
open_so = frappe.db.sql("""
open_so = frappe.db.sql(f"""
select distinct so.name, so.transaction_date, so.customer, so.base_grand_total
from `tabSales Order` so, `tabSales Order Item` so_item
where so_item.parent = so.name
and so.docstatus = 1 and so.status not in ("Stopped", "Closed")
and so.company = %(company)s
and so_item.qty > so_item.work_order_qty {0} {1}
and (exists (select name from `tabBOM` bom where {2}
and so_item.qty > so_item.work_order_qty {so_filter} {item_filter}
and (exists (select name from `tabBOM` bom where {bom_item}
and bom.is_active = 1)
or exists (select name from `tabPacked Item` pi
where pi.parent = so.name and pi.parent_item = so_item.item_code
and exists (select name from `tabBOM` bom where bom.item=pi.item_code
and bom.is_active = 1)))
""".format(so_filter, item_filter, bom_item), {
"from_date": self.from_date,
"to_date": self.to_date,
"customer": self.customer,
"project": self.project,
"item": self.item_code,
"company": self.company,
"sales_order_status": self.sales_order_status
}, as_dict=1)
""", self.as_dict(), as_dict=1)
return open_so
@frappe.whitelist()

View File

@@ -306,6 +306,7 @@ erpnext.patches.v13_0.add_custom_field_for_south_africa #2
erpnext.patches.v13_0.rename_discharge_ordered_date_in_ip_record
erpnext.patches.v13_0.migrate_stripe_api
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
erpnext.patches.v13_0.custom_fields_for_taxjar_integration
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_website_items

View File

@@ -7,6 +7,8 @@ def execute():
frappe.reload_doc("e_commerce", "doctype", "website_item")
frappe.reload_doc("e_commerce", "doctype", "website_item_tabbed_section")
frappe.reload_doc("e_commerce", "doctype", "website_offer")
frappe.reload_doc("e_commerce", "doctype", "recommended_items")
frappe.reload_doc("e_commerce", "doctype", "e_commerce_settings")
frappe.reload_doc("stock", "doctype", "item")
item_fields = ["item_code", "item_name", "item_group", "stock_uom", "brand", "image",

View File

@@ -0,0 +1,32 @@
from __future__ import unicode_literals
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from erpnext.regional.united_states.setup import add_permissions
def execute():
company = frappe.get_all('Company', filters = {'country': 'United States'}, fields=['name'])
if not company:
return
frappe.reload_doc("regional", "doctype", "product_tax_category")
custom_fields = {
'Sales Invoice Item': [
dict(fieldname='product_tax_category', fieldtype='Link', insert_after='description', options='Product Tax Category',
label='Product Tax Category', fetch_from='item_code.product_tax_category'),
dict(fieldname='tax_collectable', fieldtype='Currency', insert_after='net_amount',
label='Tax Collectable', read_only=1),
dict(fieldname='taxable_amount', fieldtype='Currency', insert_after='tax_collectable',
label='Taxable Amount', read_only=1)
],
'Item': [
dict(fieldname='product_tax_category', fieldtype='Link', insert_after='item_group', options='Product Tax Category',
label='Product Tax Category')
]
}
create_custom_fields(custom_fields, update=True)
add_permissions()
frappe.enqueue('erpnext.regional.united_states.setup.add_product_tax_categories', now=True)

View File

@@ -13,7 +13,7 @@ def execute():
frappe.reload_doc('stock', 'doctype', 'stock_settings')
def update_from_return_docs(doctype):
for return_doc in frappe.get_all(doctype, filters={'is_return' : 1, 'docstatus' : 1}):
for return_doc in frappe.get_all(doctype, filters={'is_return' : 1, 'docstatus' : 1, 'return_against': ('!=', '')}):
# Update original receipt/delivery document from return
return_doc = frappe.get_cached_doc(doctype, return_doc.name)
try:

View File

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

View File

@@ -4,18 +4,11 @@
frappe.ui.form.on('Salary Component', {
setup: function(frm) {
frm.set_query("account", "accounts", function(doc, cdt, cdn) {
let d = frappe.get_doc(cdt, cdn);
let root_type = "Liability";
if (frm.doc.type == "Deduction") {
root_type = "Expense";
}
var d = locals[cdt][cdn];
return {
filters: {
"is_group": 0,
"company": d.company,
"root_type": root_type
"company": d.company
}
};
});

View File

@@ -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):
@@ -713,6 +713,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}.")
@@ -870,14 +881,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
@@ -888,7 +892,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:
@@ -1059,7 +1066,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"])
@@ -1069,9 +1076,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

View File

@@ -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)
@@ -851,6 +911,7 @@ def setup_test():
def make_holiday_list():
fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
holiday_list = frappe.db.exists("Holiday List", "Salary Slip Test Holiday List")
if not frappe.db.get_value("Holiday List", "Salary Slip Test Holiday List"):
holiday_list = frappe.get_doc({
"doctype": "Holiday List",
@@ -861,3 +922,94 @@ def make_holiday_list():
}).insert()
holiday_list.get_weekly_off_dates()
holiday_list.save()
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

View File

@@ -227,7 +227,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
{
fieldtype: "HTML",
fieldname: "no_matching_vouchers",
options: "<div class='text-muted text-center'>No Matching Vouchers Found</div>"
options: "<div class=\"text-muted text-center\">No Matching Vouchers Found</div>"
},
{
fieldtype: "Section Break",

View File

@@ -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();
},

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Product Tax Category', {
// refresh: function(frm) {
// }
});

View File

@@ -0,0 +1,70 @@
{
"actions": [],
"autoname": "field:product_tax_code",
"creation": "2021-08-23 12:33:37.910225",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"product_tax_code",
"column_break_2",
"category_name",
"section_break_4",
"description"
],
"fields": [
{
"fieldname": "product_tax_code",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Product Tax Code",
"reqd": 1,
"unique": 1
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description"
},
{
"fieldname": "category_name",
"fieldtype": "Data",
"label": "Category Name",
"length": 255
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-08-24 09:10:25.313642",
"modified_by": "Administrator",
"module": "Regional",
"name": "Product Tax Category",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "category_name",
"track_changes": 1
}

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ProductTaxCategory(Document):
pass

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
class TestProductTaxCategory(unittest.TestCase):
pass

File diff suppressed because it is too large Load Diff

View File

@@ -4,13 +4,42 @@
from __future__ import unicode_literals
import frappe
import os
import json
from frappe.permissions import add_permission, update_permission_property
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
def setup(company=None, patch=True):
# Company independent fixtures should be called only once at the first company setup
if frappe.db.count('Company', {'country': 'United States'}) <=1:
setup_company_independent_fixtures(patch=patch)
def setup_company_independent_fixtures(company=None, patch=True):
add_product_tax_categories()
make_custom_fields()
add_permissions()
frappe.enqueue('erpnext.regional.united_states.setup.add_product_tax_categories', now=False)
add_print_formats()
# Product Tax categories imported from taxjar api
def add_product_tax_categories():
with open(os.path.join(os.path.dirname(__file__), 'product_tax_category_data.json'), 'r') as f:
tax_categories = json.loads(f.read())
create_tax_categories(tax_categories['categories'])
def create_tax_categories(data):
for d in data:
tax_category = frappe.new_doc('Product Tax Category')
tax_category.description = d.get("description")
tax_category.product_tax_code = d.get("product_tax_code")
tax_category.category_name = d.get("name")
try:
tax_category.db_insert()
except frappe.DuplicateEntryError:
pass
def make_custom_fields(update=True):
custom_fields = {
'Supplier': [
@@ -32,10 +61,29 @@ def make_custom_fields(update=True):
'Quotation': [
dict(fieldname='exempt_from_sales_tax', fieldtype='Check', insert_after='taxes_and_charges',
label='Is customer exempted from sales tax?')
],
'Sales Invoice Item': [
dict(fieldname='product_tax_category', fieldtype='Link', insert_after='description', options='Product Tax Category',
label='Product Tax Category', fetch_from='item_code.product_tax_category'),
dict(fieldname='tax_collectable', fieldtype='Currency', insert_after='net_amount',
label='Tax Collectable', read_only=1),
dict(fieldname='taxable_amount', fieldtype='Currency', insert_after='tax_collectable',
label='Taxable Amount', read_only=1)
],
'Item': [
dict(fieldname='product_tax_category', fieldtype='Link', insert_after='item_group', options='Product Tax Category',
label='Product Tax Category')
]
}
create_custom_fields(custom_fields, update=update)
def add_permissions():
doctype = "Product Tax Category"
for role in ('Accounts Manager', 'Accounts User', 'System Manager','Item Manager', 'Stock Manager'):
add_permission(doctype, role, 0)
update_permission_property(doctype, role, 0, 'write', 1)
update_permission_property(doctype, role, 0, 'create', 1)
def add_print_formats():
frappe.reload_doc("regional", "print_format", "irs_1099_form")
frappe.db.set_value("Print Format", "IRS 1099 Form", "disabled", 0)

View File

@@ -297,6 +297,7 @@ erpnext.PointOfSale.Payment = class {
this.render_payment_mode_dom();
this.make_invoice_fields_control();
this.update_totals_section();
this.focus_on_default_mop();
}
edit_cart() {
@@ -378,17 +379,24 @@ erpnext.PointOfSale.Payment = class {
});
this[`${mode}_control`].toggle_label(false);
this[`${mode}_control`].set_value(p.amount);
});
this.render_loyalty_points_payment_mode();
this.attach_cash_shortcuts(doc);
}
focus_on_default_mop() {
const doc = this.events.get_frm().doc;
const payments = doc.payments;
payments.forEach(p => {
const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase();
if (p.default) {
setTimeout(() => {
this.$payment_modes.find(`.${mode}.mode-of-payment-control`).parent().click();
}, 500);
}
});
this.render_loyalty_points_payment_mode();
this.attach_cash_shortcuts(doc);
}
attach_cash_shortcuts(doc) {

View File

@@ -243,7 +243,12 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
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
);
}
},

View File

@@ -950,7 +950,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 1,
"modified": "2021-08-26 12:23:07.277077",
"modified": "2021-09-10 12:23:07.277077",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",

View File

@@ -2,19 +2,32 @@
// For license information, please see license.txt
frappe.ui.form.on('Item Variant Settings', {
setup: function(frm) {
refresh: function(frm) {
const allow_fields = [];
const exclude_fields = ["naming_series", "item_code", "item_name", "published_in_website",
"opening_stock", "variant_of", "valuation_rate"];
const existing_fields = frm.doc.fields.map(row => row.field_name);
const exclude_fields = [...existing_fields, "naming_series", "item_code", "item_name",
"show_in_website", "show_variant_in_website", "standard_rate", "opening_stock", "image",
"variant_of", "valuation_rate", "barcodes", "website_image", "thumbnail",
"website_specifiations", "web_long_description", "has_variants", "attributes"];
const exclude_field_types = ['HTML', 'Section Break', 'Column Break', 'Button', 'Read Only'];
frappe.model.with_doctype('Item', () => {
frappe.get_meta('Item').fields.forEach(d => {
if(!in_list(['HTML', 'Section Break', 'Column Break', 'Button', 'Read Only'], d.fieldtype)
if (!in_list(exclude_field_types, d.fieldtype)
&& !d.no_copy && !in_list(exclude_fields, d.fieldname)) {
allow_fields.push(d.fieldname);
}
});
if (allow_fields.length == 0) {
allow_fields.push({
label: __("No additional fields available"),
value: "",
});
}
frm.fields_dict.fields.grid.update_docfield_property(
'field_name', 'options', allow_fields
);

View File

@@ -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": {

View File

@@ -22,7 +22,15 @@ frappe.query_reports["Stock Ageing"] = {
"fieldname":"warehouse",
"label": __("Warehouse"),
"fieldtype": "Link",
"options": "Warehouse"
"options": "Warehouse",
get_query: () => {
const company = frappe.query_report.get_filter_value("company");
return {
filters: {
...company && {company},
}
};
}
},
{
"fieldname":"item_code",

View File

@@ -53,13 +53,14 @@ frappe.query_reports["Stock Balance"] = {
"width": "80",
"options": "Warehouse",
get_query: () => {
var warehouse_type = frappe.query_report.get_filter_value('warehouse_type');
if(warehouse_type){
return {
filters: {
'warehouse_type': warehouse_type
}
};
let warehouse_type = frappe.query_report.get_filter_value("warehouse_type");
let company = frappe.query_report.get_filter_value("company");
return {
filters: {
...warehouse_type && {warehouse_type},
...company && {company}
}
}
}
},

View File

@@ -673,11 +673,15 @@ class update_entries_after(object):
if self.wh_data.stock_queue[-1][1]==incoming_rate:
self.wh_data.stock_queue[-1][0] += actual_qty
else:
# Item has a positive balance qty, add new entry
if self.wh_data.stock_queue[-1][0] > 0:
self.wh_data.stock_queue.append([actual_qty, incoming_rate])
else:
else: # negative balance qty
qty = self.wh_data.stock_queue[-1][0] + actual_qty
self.wh_data.stock_queue[-1] = [qty, incoming_rate]
if qty > 0: # new balance qty is positive
self.wh_data.stock_queue[-1] = [qty, incoming_rate]
else: # new balance qty is still negative, maintain same rate
self.wh_data.stock_queue[-1][0] = qty
else:
qty_to_pop = abs(actual_qty)
while qty_to_pop:

View File

@@ -11,16 +11,10 @@
"bugs": {
"url": "https://github.com/frappe/erpnext/issues"
},
"devDependencies": {
"snyk": "^1.518.0"
},
"devDependencies": {},
"dependencies": {
"onscan.js": "^1.5.2",
"html2canvas": "^1.1.4"
"html2canvas": "^1.1.4",
"onscan.js": "^1.5.2"
},
"scripts": {
"snyk-protect": "snyk protect",
"prepare": "yarn run snyk-protect"
},
"snyk": true
"scripts": {}
}

3554
yarn.lock

File diff suppressed because it is too large Load Diff