fix: resolved conflicts in patches.txt

This commit is contained in:
Anuja 2021-08-02 12:58:46 +05:30
commit 3bcbc86291
111 changed files with 1417 additions and 977 deletions

View File

@ -8,11 +8,12 @@ on:
jobs: jobs:
main: main:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 60
steps: steps:
- name: Checkout Actions - name: Checkout Actions
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
repository: "ankush/backport" repository: "frappe/backport"
path: ./actions path: ./actions
ref: develop ref: develop
- name: Install Actions - name: Install Actions

View File

@ -6,6 +6,7 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10
steps: steps:
- name: 'Setup Environment' - name: 'Setup Environment'

View File

@ -5,6 +5,7 @@ on: [pull_request, workflow_dispatch]
jobs: jobs:
test: test:
runs-on: ubuntu-18.04 runs-on: ubuntu-18.04
timeout-minutes: 60
name: Patch Test name: Patch Test

View File

@ -9,6 +9,7 @@ on:
jobs: jobs:
test: test:
runs-on: ubuntu-18.04 runs-on: ubuntu-18.04
timeout-minutes: 60
strategy: strategy:
fail-fast: false fail-fast: false

View File

@ -7,6 +7,7 @@ on:
jobs: jobs:
test: test:
runs-on: ubuntu-18.04 runs-on: ubuntu-18.04
timeout-minutes: 60
strategy: strategy:
fail-fast: false fail-fast: false

View File

@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides from erpnext.hooks import regional_overrides
from frappe.utils import getdate from frappe.utils import getdate
__version__ = '13.7.1' __version__ = '13.8.0'
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' '''Get default company for user'''

View File

@ -391,5 +391,5 @@ def set_default_accounts(company):
}) })
company.save() company.save()
install_country_fixtures(company.name) install_country_fixtures(company.name, company.country)
company.create_default_tax_template() company.create_default_tax_template()

View File

@ -27,6 +27,9 @@ class ExchangeRateRevaluation(Document):
if not (self.company and self.posting_date): if not (self.company and self.posting_date):
frappe.throw(_("Please select Company and Posting Date to getting entries")) frappe.throw(_("Please select Company and Posting Date to getting entries"))
def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry')
@frappe.whitelist() @frappe.whitelist()
def check_journal_entry_condition(self): def check_journal_entry_condition(self):
total_debit = frappe.db.get_value("Journal Entry Account", { total_debit = frappe.db.get_value("Journal Entry Account", {
@ -99,10 +102,12 @@ class ExchangeRateRevaluation(Document):
sum(debit) - sum(credit) as balance sum(debit) - sum(credit) as balance
from `tabGL Entry` from `tabGL Entry`
where account in (%s) where account in (%s)
group by account, party_type, party and posting_date <= %s
and is_cancelled = 0
group by account, NULLIF(party_type,''), NULLIF(party,'')
having sum(debit) != sum(credit) having sum(debit) != sum(credit)
order by account order by account
""" % ', '.join(['%s']*len(accounts)), tuple(accounts), as_dict=1) """ % (', '.join(['%s']*len(accounts)), '%s'), tuple(accounts + [self.posting_date]), as_dict=1)
return account_details return account_details
@ -143,9 +148,9 @@ class ExchangeRateRevaluation(Document):
"party_type": d.get("party_type"), "party_type": d.get("party_type"),
"party": d.get("party"), "party": d.get("party"),
"account_currency": d.get("account_currency"), "account_currency": d.get("account_currency"),
"balance": d.get("balance_in_account_currency"), "balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")),
dr_or_cr: abs(d.get("balance_in_account_currency")), dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
"exchange_rate":d.get("new_exchange_rate"), "exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")),
"reference_type": "Exchange Rate Revaluation", "reference_type": "Exchange Rate Revaluation",
"reference_name": self.name, "reference_name": self.name,
}) })
@ -154,9 +159,9 @@ class ExchangeRateRevaluation(Document):
"party_type": d.get("party_type"), "party_type": d.get("party_type"),
"party": d.get("party"), "party": d.get("party"),
"account_currency": d.get("account_currency"), "account_currency": d.get("account_currency"),
"balance": d.get("balance_in_account_currency"), "balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")),
reverse_dr_or_cr: abs(d.get("balance_in_account_currency")), reverse_dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
"exchange_rate": d.get("current_exchange_rate"), "exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
"reference_type": "Exchange Rate Revaluation", "reference_type": "Exchange Rate Revaluation",
"reference_name": self.name "reference_name": self.name
}) })
@ -185,9 +190,9 @@ def get_account_details(account, company, posting_date, party_type=None, party=N
account_details = {} account_details = {}
company_currency = erpnext.get_company_currency(company) company_currency = erpnext.get_company_currency(company)
balance = get_balance_on(account, party_type=party_type, party=party, in_account_currency=False) balance = get_balance_on(account, date=posting_date, party_type=party_type, party=party, in_account_currency=False)
if balance: if balance:
balance_in_account_currency = get_balance_on(account, party_type=party_type, party=party) balance_in_account_currency = get_balance_on(account, date=posting_date, party_type=party_type, party=party)
current_exchange_rate = balance / balance_in_account_currency if balance_in_account_currency else 0 current_exchange_rate = balance / balance_in_account_currency if balance_in_account_currency else 0
new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date) new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate

View File

@ -306,5 +306,5 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
} }
] ]
}) })
jv.flags.ignore_mandatory = True
jv.submit() jv.submit()

View File

@ -15,6 +15,7 @@ from erpnext.healthcare.doctype.lab_test_template.lab_test_template import make_
class TestPricingRule(unittest.TestCase): class TestPricingRule(unittest.TestCase):
def setUp(self): def setUp(self):
delete_existing_pricing_rules() delete_existing_pricing_rules()
setup_pricing_rule_data()
def tearDown(self): def tearDown(self):
delete_existing_pricing_rules() delete_existing_pricing_rules()
@ -554,6 +555,8 @@ class TestPricingRule(unittest.TestCase):
for doc in [si, si1]: for doc in [si, si1]:
doc.delete() doc.delete()
test_dependencies = ["Campaign"]
def make_pricing_rule(**args): def make_pricing_rule(**args):
args = frappe._dict(args) args = frappe._dict(args)
@ -600,6 +603,13 @@ def make_pricing_rule(**args):
if args.get(applicable_for): if args.get(applicable_for):
doc.db_set(applicable_for, args.get(applicable_for)) doc.db_set(applicable_for, args.get(applicable_for))
def setup_pricing_rule_data():
if not frappe.db.exists('Campaign', '_Test Campaign'):
frappe.get_doc({
'doctype': 'Campaign',
'campaign_name': '_Test Campaign',
'name': '_Test Campaign'
}).insert()
def delete_existing_pricing_rules(): def delete_existing_pricing_rules():
for doctype in ["Pricing Rule", "Pricing Rule Item Code", for doctype in ["Pricing Rule", "Pricing Rule Item Code",

View File

@ -168,7 +168,7 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
frappe.throw(_("Invalid {0}").format(args.get(field))) frappe.throw(_("Invalid {0}").format(args.get(field)))
parent_groups = frappe.db.sql_list("""select name from `tab%s` parent_groups = frappe.db.sql_list("""select name from `tab%s`
where lft>=%s and rgt<=%s""" % (parenttype, '%s', '%s'), (lft, rgt)) where lft<=%s and rgt>=%s""" % (parenttype, '%s', '%s'), (lft, rgt))
if parenttype in ["Customer Group", "Item Group", "Territory"]: if parenttype in ["Customer Group", "Item Group", "Territory"]:
parent_field = "parent_{0}".format(frappe.scrub(parenttype)) parent_field = "parent_{0}".format(frappe.scrub(parenttype))

View File

@ -27,6 +27,8 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
from erpnext.accounts.deferred_revenue import validate_service_stop_date from erpnext.accounts.deferred_revenue import validate_service_stop_date
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import get_item_account_wise_additional_cost from erpnext.stock.doctype.purchase_receipt.purchase_receipt import get_item_account_wise_additional_cost
class WarehouseMissingError(frappe.ValidationError): pass
form_grid_templates = { form_grid_templates = {
"items": "templates/form_grid/item_grid.html" "items": "templates/form_grid/item_grid.html"
} }
@ -207,8 +209,8 @@ class PurchaseInvoice(BuyingController):
if self.update_stock and for_validate: if self.update_stock and for_validate:
for d in self.get('items'): for d in self.get('items'):
if not d.warehouse: if not d.warehouse:
frappe.throw(_("Warehouse required at Row No {0}, please set default warehouse for the item {1} for the company {2}"). frappe.throw(_("Row No {0}: Warehouse is required. Please set a Default Warehouse for Item {1} and Company {2}").
format(d.idx, d.item_code, self.company)) format(d.idx, d.item_code, self.company), exc=WarehouseMissingError)
super(PurchaseInvoice, self).validate_warehouse() super(PurchaseInvoice, self).validate_warehouse()
@ -246,7 +248,7 @@ class PurchaseInvoice(BuyingController):
and (not item.po_detail or and (not item.po_detail or
not frappe.db.get_value("Purchase Order Item", item.po_detail, "delivered_by_supplier")): not frappe.db.get_value("Purchase Order Item", item.po_detail, "delivered_by_supplier")):
if self.update_stock and (not item.from_warehouse): if self.update_stock and item.warehouse and (not item.from_warehouse):
if for_validate and item.expense_account and item.expense_account != warehouse_account[item.warehouse]["account"]: if for_validate and item.expense_account and item.expense_account != warehouse_account[item.warehouse]["account"]:
msg = _("Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account").format( msg = _("Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account").format(
item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]), frappe.bold(item.expense_account), frappe.bold(item.warehouse)) item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]), frappe.bold(item.expense_account), frappe.bold(item.warehouse))
@ -657,7 +659,7 @@ class PurchaseInvoice(BuyingController):
) )
gl_entries.append( gl_entries.append(
self.get_gl_dict({ self.get_gl_dict({
"account": self.get_company_default("exchange_gain_loss_account"), "account": self.get_company_default("exchange_gain_loss_account"),
"against": self.supplier, "against": self.supplier,
"credit": discrepancy_caused_by_exchange_rate_difference, "credit": discrepancy_caused_by_exchange_rate_difference,
"cost_center": item.cost_center, "cost_center": item.cost_center,
@ -1193,7 +1195,7 @@ def get_purchase_document_details(doc):
purchase_receipts_or_invoices.append(item.get(doc_reference)) purchase_receipts_or_invoices.append(item.get(doc_reference))
if item.get(items_reference): if item.get(items_reference):
items.append(item.get(items_reference)) items.append(item.get(items_reference))
exchange_rate_map = frappe._dict(frappe.get_all(parent_doctype, filters={'name': ('in', exchange_rate_map = frappe._dict(frappe.get_all(parent_doctype, filters={'name': ('in',
purchase_receipts_or_invoices)}, fields=['name', 'conversion_rate'], as_list=1)) purchase_receipts_or_invoices)}, fields=['name', 'conversion_rate'], as_list=1))

View File

@ -240,7 +240,7 @@ class TestPurchaseInvoice(unittest.TestCase):
pi.conversion_rate = 80 pi.conversion_rate = 80
pi.insert() pi.insert()
pi.submit() pi.submit()
# Get exchnage gain and loss account # Get exchnage gain and loss account
exchange_gain_loss_account = frappe.db.get_value('Company', pi.company, 'exchange_gain_loss_account') exchange_gain_loss_account = frappe.db.get_value('Company', pi.company, 'exchange_gain_loss_account')
@ -978,7 +978,7 @@ class TestPurchaseInvoice(unittest.TestCase):
unlink_enabled = frappe.db.get_value( unlink_enabled = frappe.db.get_value(
"Accounts Settings", "Accounts Settings", "Accounts Settings", "Accounts Settings",
"unlink_payment_on_cancel_of_invoice") "unlink_payment_on_cancel_of_invoice")
frappe.db.set_value( frappe.db.set_value(
"Accounts Settings", "Accounts Settings", "Accounts Settings", "Accounts Settings",
"unlink_payment_on_cancel_of_invoice", 1) "unlink_payment_on_cancel_of_invoice", 1)
@ -1018,8 +1018,8 @@ class TestPurchaseInvoice(unittest.TestCase):
expected_gle = [ expected_gle = [
["_Test Account Cost for Goods Sold - _TC", 37500.0], ["_Test Account Cost for Goods Sold - _TC", 37500.0],
["_Test Payable USD - _TC", -40000.0], ["_Test Payable USD - _TC", -35000.0],
["Exchange Gain/Loss - _TC", 2500.0] ["Exchange Gain/Loss - _TC", -2500.0]
] ]
gl_entries = frappe.db.sql(""" gl_entries = frappe.db.sql("""
@ -1027,7 +1027,7 @@ class TestPurchaseInvoice(unittest.TestCase):
where voucher_no=%s where voucher_no=%s
group by account group by account
order by account asc""", (pi.name), as_dict=1) order by account asc""", (pi.name), as_dict=1)
for i, gle in enumerate(gl_entries): for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account) self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.balance) self.assertEqual(expected_gle[i][1], gle.balance)
@ -1049,8 +1049,8 @@ class TestPurchaseInvoice(unittest.TestCase):
expected_gle = [ expected_gle = [
["_Test Account Cost for Goods Sold - _TC", 36500.0], ["_Test Account Cost for Goods Sold - _TC", 36500.0],
["_Test Payable USD - _TC", -38000.0], ["_Test Payable USD - _TC", -35000.0],
["Exchange Gain/Loss - _TC", 1500.0] ["Exchange Gain/Loss - _TC", -1500.0]
] ]
gl_entries = frappe.db.sql(""" gl_entries = frappe.db.sql("""

View File

@ -2,13 +2,14 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe, erpnext
import unittest, copy, time import unittest, copy, time
from frappe.utils import nowdate, flt, getdate, cint, add_days, add_months from frappe.utils import nowdate, flt, getdate, cint, add_days, add_months
from frappe.model.dynamic_links import get_dynamic_link_map from frappe.model.dynamic_links import get_dynamic_link_map
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import WarehouseMissingError
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
@ -1073,7 +1074,7 @@ class TestSalesInvoice(unittest.TestCase):
def test_gle_made_when_asset_is_returned(self): def test_gle_made_when_asset_is_returned(self):
create_asset_data() create_asset_data()
asset = create_asset(item_code="Macbook Pro") asset = create_asset(item_code="Macbook Pro")
si = create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000) si = create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000)
return_si = create_sales_invoice(is_return=1, return_against=si.name, item_code="Macbook Pro", asset=asset.name, qty=-1, rate=90000) return_si = create_sales_invoice(is_return=1, return_against=si.name, item_code="Macbook Pro", asset=asset.name, qty=-1, rate=90000)
@ -1081,7 +1082,7 @@ class TestSalesInvoice(unittest.TestCase):
# Asset value is 100,000 but it was sold for 90,000, so there should be a loss of 10,000 # Asset value is 100,000 but it was sold for 90,000, so there should be a loss of 10,000
loss_for_si = frappe.get_all( loss_for_si = frappe.get_all(
"GL Entry", "GL Entry",
filters = { filters = {
"voucher_no": si.name, "voucher_no": si.name,
"account": disposal_account "account": disposal_account
@ -1090,7 +1091,7 @@ class TestSalesInvoice(unittest.TestCase):
)[0] )[0]
loss_for_return_si = frappe.get_all( loss_for_return_si = frappe.get_all(
"GL Entry", "GL Entry",
filters = { filters = {
"voucher_no": return_si.name, "voucher_no": return_si.name,
"account": disposal_account "account": disposal_account
@ -1836,6 +1837,89 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(target_doc.company, "_Test Company 1") self.assertEqual(target_doc.company, "_Test Company 1")
self.assertEqual(target_doc.supplier, "_Test Internal Supplier") self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
def test_inter_company_transaction_without_default_warehouse(self):
"Check mapping (expense account) of inter company SI to PI in absence of default warehouse."
# setup
old_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock")
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
old_perpetual_inventory = erpnext.is_perpetual_inventory_enabled('_Test Company 1')
frappe.local.enable_perpetual_inventory['_Test Company 1'] = 1
frappe.db.set_value("Company", '_Test Company 1', "stock_received_but_not_billed", "Stock Received But Not Billed - _TC1")
frappe.db.set_value("Company", '_Test Company 1', "expenses_included_in_valuation", "Expenses Included In Valuation - _TC1")
if not frappe.db.exists("Customer", "_Test Internal Customer"):
customer = frappe.get_doc({
"customer_group": "_Test Customer Group",
"customer_name": "_Test Internal Customer",
"customer_type": "Individual",
"doctype": "Customer",
"territory": "_Test Territory",
"is_internal_customer": 1,
"represents_company": "_Test Company 1"
})
customer.append("companies", {
"company": "Wind Power LLC"
})
customer.insert()
if not frappe.db.exists("Supplier", "_Test Internal Supplier"):
supplier = frappe.get_doc({
"supplier_group": "_Test Supplier Group",
"supplier_name": "_Test Internal Supplier",
"doctype": "Supplier",
"is_internal_supplier": 1,
"represents_company": "Wind Power LLC"
})
supplier.append("companies", {
"company": "_Test Company 1"
})
supplier.insert()
# begin test
si = create_sales_invoice(
company = "Wind Power LLC",
customer = "_Test Internal Customer",
debit_to = "Debtors - WP",
warehouse = "Stores - WP",
income_account = "Sales - WP",
expense_account = "Cost of Goods Sold - WP",
cost_center = "Main - WP",
currency = "USD",
update_stock = 1,
do_not_save = 1
)
si.selling_price_list = "_Test Price List Rest of the World"
si.submit()
target_doc = make_inter_company_transaction("Sales Invoice", si.name)
# in absence of warehouse Stock Received But Not Billed is set as expense account while mapping
# mapping is not obstructed
self.assertIsNone(target_doc.items[0].warehouse)
self.assertEqual(target_doc.items[0].expense_account, "Stock Received But Not Billed - _TC1")
target_doc.items[0].update({"cost_center": "Main - _TC1"})
# missing warehouse is validated on save, after mapping
self.assertRaises(WarehouseMissingError, target_doc.save)
target_doc.items[0].update({"warehouse": "Stores - _TC1"})
target_doc.save()
# after warehouse is set, linked account or default inventory account is set
self.assertEqual(target_doc.items[0].expense_account, 'Stock In Hand - _TC1')
# tear down
frappe.local.enable_perpetual_inventory['_Test Company 1'] = old_perpetual_inventory
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", old_negative_stock)
def test_internal_transfer_gl_entry(self): def test_internal_transfer_gl_entry(self):
## Create internal transfer account ## Create internal transfer account
account = create_account(account_name="Unrealized Profit", account = create_account(account_name="Unrealized Profit",

View File

@ -27,7 +27,8 @@
"base_tax_amount", "base_tax_amount",
"base_total", "base_total",
"base_tax_amount_after_discount_amount", "base_tax_amount_after_discount_amount",
"item_wise_tax_detail" "item_wise_tax_detail",
"dont_recompute_tax"
], ],
"fields": [ "fields": [
{ {
@ -200,13 +201,22 @@
"fieldname": "included_in_paid_amount", "fieldname": "included_in_paid_amount",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Considered In Paid Amount" "label": "Considered In Paid Amount"
},
{
"default": "0",
"fieldname": "dont_recompute_tax",
"fieldtype": "Check",
"hidden": 1,
"label": "Dont Recompute tax",
"print_hide": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-06-14 01:44:36.899147", "modified": "2021-07-27 12:40:59.051803",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Taxes and Charges", "name": "Sales Taxes and Charges",

View File

@ -1,263 +1,151 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0, "allow_import": 1,
"allow_import": 1, "allow_rename": 1,
"allow_rename": 1, "autoname": "Prompt",
"autoname": "Prompt", "creation": "2018-04-13 18:42:06.431683",
"beta": 0, "doctype": "DocType",
"creation": "2018-04-13 18:42:06.431683", "editable_grid": 1,
"custom": 0, "engine": "InnoDB",
"docstatus": 0, "field_order": [
"doctype": "DocType", "category_details_section",
"document_type": "", "category_name",
"editable_grid": 1, "round_off_tax_amount",
"engine": "InnoDB", "column_break_2",
"consider_party_ledger_amount",
"tax_on_excess_amount",
"section_break_8",
"rates",
"section_break_7",
"accounts"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "category_name", "fieldname": "category_name",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Category Name", "label": "Category Name",
"length": 0, "show_days": 1,
"no_copy": 0, "show_seconds": 1
"permlevel": 0, },
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_8", "fieldname": "section_break_8",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Tax Withholding Rates", "label": "Tax Withholding Rates",
"length": 0, "show_days": 1,
"no_copy": 0, "show_seconds": 1
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "rates", "fieldname": "rates",
"fieldtype": "Table", "fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Rates", "label": "Rates",
"length": 0,
"no_copy": 0,
"options": "Tax Withholding Rate", "options": "Tax Withholding Rate",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0, "show_days": 1,
"set_only_once": 0, "show_seconds": 1
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "fieldname": "section_break_7",
"allow_in_quick_entry": 0, "fieldtype": "Section Break",
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Account Details", "label": "Account Details",
"length": 0, "show_days": 1,
"no_copy": 0, "show_seconds": 1
"permlevel": 0, },
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "accounts",
"allow_in_quick_entry": 0, "fieldtype": "Table",
"allow_on_submit": 0, "label": "Accounts",
"bold": 0, "options": "Tax Withholding Account",
"collapsible": 0, "reqd": 1,
"columns": 0, "show_days": 1,
"fieldname": "accounts", "show_seconds": 1
"fieldtype": "Table", },
"hidden": 0, {
"ignore_user_permissions": 0, "fieldname": "category_details_section",
"ignore_xss_filter": 0, "fieldtype": "Section Break",
"in_filter": 0, "label": "Category Details",
"in_global_search": 0, "show_days": 1,
"in_list_view": 0, "show_seconds": 1
"in_standard_filter": 0, },
"label": "Accounts", {
"length": 0, "fieldname": "column_break_2",
"no_copy": 0, "fieldtype": "Column Break",
"options": "Tax Withholding Account", "show_days": 1,
"permlevel": 0, "show_seconds": 1
"precision": "", },
"print_hide": 0, {
"print_hide_if_no_value": 0, "default": "0",
"read_only": 0, "description": "Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
"remember_last_selected_value": 0, "fieldname": "consider_party_ledger_amount",
"report_hide": 0, "fieldtype": "Check",
"reqd": 1, "label": "Consider Entire Party Ledger Amount",
"search_index": 0, "show_days": 1,
"set_only_once": 0, "show_seconds": 1
"translatable": 0, },
"unique": 0 {
"default": "0",
"description": "Tax will be withheld only for amount exceeding the cumulative threshold",
"fieldname": "tax_on_excess_amount",
"fieldtype": "Check",
"label": "Only Deduct Tax On Excess Amount ",
"show_days": 1,
"show_seconds": 1
},
{
"description": "Checking this will round off the tax amount to the nearest integer",
"fieldname": "round_off_tax_amount",
"fieldtype": "Check",
"label": "Round Off Tax Amount",
"show_days": 1,
"show_seconds": 1
} }
], ],
"has_web_view": 0, "index_web_pages_for_search": 1,
"hide_heading": 0, "links": [],
"hide_toolbar": 0, "modified": "2021-07-27 21:47:34.396071",
"idx": 0, "modified_by": "Administrator",
"image_view": 0, "module": "Accounts",
"in_create": 0, "name": "Tax Withholding Category",
"is_submittable": 0, "owner": "Administrator",
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-07-17 22:53:26.193179",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Category",
"name_case": "",
"owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0, "create": 1,
"cancel": 0, "delete": 1,
"create": 1, "email": 1,
"delete": 1, "export": 1,
"email": 1, "print": 1,
"export": 1, "read": 1,
"if_owner": 0, "report": 1,
"import": 0, "role": "System Manager",
"permlevel": 0, "share": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0, "create": 1,
"cancel": 0, "delete": 1,
"create": 1, "email": 1,
"delete": 1, "export": 1,
"email": 1, "print": 1,
"export": 1, "read": 1,
"if_owner": 0, "report": 1,
"import": 0, "role": "Accounts Manager",
"permlevel": 0, "share": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0, "create": 1,
"cancel": 0, "delete": 1,
"create": 1, "email": 1,
"delete": 1, "export": 1,
"email": 1, "print": 1,
"export": 1, "read": 1,
"if_owner": 0, "report": 1,
"import": 0, "role": "Accounts User",
"permlevel": 0, "share": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0, "sort_field": "modified",
"read_only": 0, "sort_order": "DESC",
"read_only_onload": 0, "track_changes": 1
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
} }

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import flt, getdate from frappe.utils import flt, getdate, cint
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
class TaxWithholdingCategory(Document): class TaxWithholdingCategory(Document):
@ -86,7 +86,10 @@ def get_tax_withholding_details(tax_withholding_category, fiscal_year, company):
"rate": tax_rate_detail.tax_withholding_rate, "rate": tax_rate_detail.tax_withholding_rate,
"threshold": tax_rate_detail.single_threshold, "threshold": tax_rate_detail.single_threshold,
"cumulative_threshold": tax_rate_detail.cumulative_threshold, "cumulative_threshold": tax_rate_detail.cumulative_threshold,
"description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category "description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category,
"consider_party_ledger_amount": tax_withholding.consider_party_ledger_amount,
"tax_on_excess_amount": tax_withholding.tax_on_excess_amount,
"round_off_tax_amount": tax_withholding.round_off_tax_amount
}) })
def get_tax_withholding_rates(tax_withholding, fiscal_year): def get_tax_withholding_rates(tax_withholding, fiscal_year):
@ -145,6 +148,7 @@ def get_lower_deduction_certificate(fiscal_year, pan_no):
def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None): def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None):
fiscal_year = fiscal_year_details[0] fiscal_year = fiscal_year_details[0]
vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type) vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type) advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
taxable_vouchers = vouchers + advance_vouchers taxable_vouchers = vouchers + advance_vouchers
@ -235,10 +239,18 @@ def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details):
def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers): def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers):
tds_amount = 0 tds_amount = 0
invoice_filters = {
'name': ('in', vouchers),
'docstatus': 1
}
supp_credit_amt = frappe.db.get_value('Purchase Invoice', { field = 'sum(net_total)'
'name': ('in', vouchers), 'docstatus': 1, 'apply_tds': 1
}, 'sum(net_total)') or 0.0 if not cint(tax_details.consider_party_ledger_amount):
invoice_filters.update({'apply_tds': 1})
field = 'sum(grand_total)'
supp_credit_amt = frappe.db.get_value('Purchase Invoice', invoice_filters, field) or 0.0
supp_jv_credit_amt = frappe.db.get_value('Journal Entry Account', { supp_jv_credit_amt = frappe.db.get_value('Journal Entry Account', {
'parent': ('in', vouchers), 'docstatus': 1, 'parent': ('in', vouchers), 'docstatus': 1,
@ -255,6 +267,13 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
cumulative_threshold = tax_details.get('cumulative_threshold', 0) cumulative_threshold = tax_details.get('cumulative_threshold', 0)
if ((threshold and inv.net_total >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)): if ((threshold and inv.net_total >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)):
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(tax_details.tax_on_excess_amount):
# Get net total again as TDS is calculated on net total
# Grand is used to just check for threshold breach
net_total = frappe.db.get_value('Purchase Invoice', invoice_filters, 'sum(net_total)') or 0.0
net_total += inv.net_total
supp_credit_amt = net_total - cumulative_threshold
if ldc and is_valid_certificate( if ldc and is_valid_certificate(
ldc.valid_from, ldc.valid_upto, ldc.valid_from, ldc.valid_upto,
inv.get('posting_date') or inv.get('transaction_date'), tax_deducted, inv.get('posting_date') or inv.get('transaction_date'), tax_deducted,
@ -263,6 +282,9 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details) tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details)
else: else:
tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0 tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
if cint(tax_details.round_off_tax_amount):
tds_amount = round(tds_amount)
return tds_amount return tds_amount

View File

@ -87,6 +87,31 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in invoices: for d in invoices:
d.cancel() d.cancel()
def test_tax_withholding_category_checks(self):
invoices = []
frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category")
# First Invoice with no tds check
pi = create_purchase_invoice(supplier = "Test TDS Supplier3", rate = 20000, do_not_save=True)
pi.apply_tds = 0
pi.save()
pi.submit()
invoices.append(pi)
# Second Invoice will apply TDS checked
pi1 = create_purchase_invoice(supplier = "Test TDS Supplier3", rate = 20000)
pi1.submit()
invoices.append(pi1)
# Cumulative threshold is 30000
# Threshold calculation should be on both the invoices
# TDS should be applied only on 1000
self.assertEqual(pi1.taxes[0].tax_amount, 1000)
for d in invoices:
d.cancel()
def test_cumulative_threshold_tcs(self): def test_cumulative_threshold_tcs(self):
frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS") frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS")
invoices = [] invoices = []
@ -195,7 +220,7 @@ def create_sales_invoice(**args):
def create_records(): def create_records():
# create a new suppliers # create a new suppliers
for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']: for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3']:
if frappe.db.exists('Supplier', name): if frappe.db.exists('Supplier', name):
continue continue
@ -311,3 +336,23 @@ def create_tax_with_holding_category():
'account': 'TDS - _TC' 'account': 'TDS - _TC'
}] }]
}).insert() }).insert()
if not frappe.db.exists("Tax Withholding Category", "New TDS Category"):
frappe.get_doc({
"doctype": "Tax Withholding Category",
"name": "New TDS Category",
"category_name": "New TDS Category",
"round_off_tax_amount": 1,
"consider_party_ledger_amount": 1,
"tax_on_excess_amount": 1,
"rates": [{
'fiscal_year': fiscal_year,
'tax_withholding_rate': 10,
'single_threshold': 0,
'cumulative_threshold': 30000
}],
"accounts": [{
'company': '_Test Company',
'account': 'TDS - _TC'
}]
}).insert()

View File

@ -99,7 +99,6 @@ class ReceivablePayableReport(object):
voucher_no = gle.voucher_no, voucher_no = gle.voucher_no,
party = gle.party, party = gle.party,
posting_date = gle.posting_date, posting_date = gle.posting_date,
remarks = gle.remarks,
account_currency = gle.account_currency, account_currency = gle.account_currency,
invoiced = 0.0, invoiced = 0.0,
paid = 0.0, paid = 0.0,
@ -579,7 +578,7 @@ class ReceivablePayableReport(object):
self.gl_entries = frappe.db.sql(""" self.gl_entries = frappe.db.sql("""
select select
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center, name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
against_voucher_type, against_voucher, account_currency, remarks, {0} against_voucher_type, against_voucher, account_currency, {0}
from from
`tabGL Entry` `tabGL Entry`
where where
@ -792,8 +791,6 @@ class ReceivablePayableReport(object):
self.add_column(label=_('Supplier Group'), fieldname='supplier_group', fieldtype='Link', self.add_column(label=_('Supplier Group'), fieldname='supplier_group', fieldtype='Link',
options='Supplier Group') options='Supplier Group')
self.add_column(label=_('Remarks'), fieldname='remarks', fieldtype='Text', width=200)
def add_column(self, label, fieldname=None, fieldtype='Currency', options=None, width=120): def add_column(self, label, fieldname=None, fieldtype='Currency', options=None, width=120):
if not fieldname: fieldname = scrub(label) if not fieldname: fieldname = scrub(label)
if fieldtype=='Currency': options='currency' if fieldtype=='Currency': options='currency'

View File

@ -241,6 +241,7 @@ class GrossProfitGenerator(object):
sle.voucher_detail_no == row.item_row: sle.voucher_detail_no == row.item_row:
previous_stock_value = len(my_sle) > i+1 and \ previous_stock_value = len(my_sle) > i+1 and \
flt(my_sle[i+1].stock_value) or 0.0 flt(my_sle[i+1].stock_value) or 0.0
if previous_stock_value: if previous_stock_value:
return (previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) return (previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty))
else: else:
@ -335,7 +336,7 @@ class GrossProfitGenerator(object):
res = frappe.db.sql("""select item_code, voucher_type, voucher_no, res = frappe.db.sql("""select item_code, voucher_type, voucher_no,
voucher_detail_no, stock_value, warehouse, actual_qty as qty voucher_detail_no, stock_value, warehouse, actual_qty as qty
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where company=%(company)s where company=%(company)s and is_cancelled = 0
order by order by
item_code desc, warehouse desc, posting_date desc, item_code desc, warehouse desc, posting_date desc,
posting_time desc, creation desc""", self.filters, as_dict=True) posting_time desc, creation desc""", self.filters, as_dict=True)

View File

@ -566,10 +566,10 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no):
frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe))) frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe)))
@frappe.whitelist() @frappe.whitelist()
def get_company_default(company, fieldname): def get_company_default(company, fieldname, ignore_validation=False):
value = frappe.get_cached_value('Company', company, fieldname) value = frappe.get_cached_value('Company', company, fieldname)
if not value: if not ignore_validation and not value:
throw(_("Please set default {0} in Company {1}") throw(_("Please set default {0} in Company {1}")
.format(frappe.get_meta("Company").get_label(fieldname), company)) .format(frappe.get_meta("Company").get_label(fieldname), company))
@ -966,7 +966,7 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
for e in existing_gle: for e in existing_gle:
if entry.account == e.account: if entry.account == e.account:
account_existed = True account_existed = True
if (entry.account == e.account and entry.against_account == e.against_account if (entry.account == e.account
and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center)
and ( flt(entry.debit, precision) != flt(e.debit, precision) or and ( flt(entry.debit, precision) != flt(e.debit, precision) or
flt(entry.credit, precision) != flt(e.credit, precision))): flt(entry.credit, precision) != flt(e.credit, precision))):

View File

@ -0,0 +1,39 @@
# Version 13.8.0 Release Notes
### Features & Enhancements
- Report to show COGS by item groups ([#26222](https://github.com/frappe/erpnext/pull/26222))
- Enhancements in TDS ([#26677](https://github.com/frappe/erpnext/pull/26677))
- API Endpoint to update halted Razorpay subscriptions ([#26564](https://github.com/frappe/erpnext/pull/26564))
### Fixes
- Incorrect bom name ([#26600](https://github.com/frappe/erpnext/pull/26600))
- Exchange rate revaluation posting date and precision fixes ([#26651](https://github.com/frappe/erpnext/pull/26651))
- POS item cart dom updates ([#26460](https://github.com/frappe/erpnext/pull/26460))
- General Ledger report not working with filter group by ([#26439](https://github.com/frappe/erpnext/pull/26438))
- Tax calculation for Recurring additional salary ([#24206](https://github.com/frappe/erpnext/pull/24206))
- Validation check for batch for stock reconciliation type in stock entry ([#26487](https://github.com/frappe/erpnext/pull/26487))
- Improved UX for additional discount field ([#26502](https://github.com/frappe/erpnext/pull/26502))
- Add missing cess amount in GSTR-3B report ([#26644](https://github.com/frappe/erpnext/pull/26644))
- Optimized code for reposting item valuation ([#26431](https://github.com/frappe/erpnext/pull/26431))
- FG item not fetched in manufacture entry ([#26508](https://github.com/frappe/erpnext/pull/26508))
- Errors on parallel requests creation of company for India ([#26420](https://github.com/frappe/erpnext/pull/26420))
- Incorrect valuation rate calculation in gross profit report ([#26558](https://github.com/frappe/erpnext/pull/26558))
- Empty "against account" in Purchase Receipt GLE ([#26712](https://github.com/frappe/erpnext/pull/26712))
- Remove cancelled entries from Stock and Account Value comparison report ([#26721](https://github.com/frappe/erpnext/pull/26721))
- Remove manual permission checking ([#26691](https://github.com/frappe/erpnext/pull/26691))
- Delete child docs when parent doc is deleted ([#26518](https://github.com/frappe/erpnext/pull/26518))
- GST Reports timeout issue ([#26646](https://github.com/frappe/erpnext/pull/26646))
- Parent condition in pricing rules ([#26727](https://github.com/frappe/erpnext/pull/26727))
- Added Company filters for Loan ([#26294](https://github.com/frappe/erpnext/pull/26294))
- Incorrect discount amount on amended document ([#26292](https://github.com/frappe/erpnext/pull/26292))
- Exchange gain loss not set for advances linked with invoices ([#26436](https://github.com/frappe/erpnext/pull/26436))
- Unallocated amount in Payment Entry after taxes ([#26412](https://github.com/frappe/erpnext/pull/26412))
- Wrong operation time in Work Order ([#26613](https://github.com/frappe/erpnext/pull/26613))
- Serial No and Batch validation ([#26614](https://github.com/frappe/erpnext/pull/26614))
- Gl Entries for exchange gain loss ([#26734](https://github.com/frappe/erpnext/pull/26734))
- TDS computation summary shows cancelled invoices ([#26485](https://github.com/frappe/erpnext/pull/26485))
- Price List rate not fetched for return sales invoice fixed ([#26560](https://github.com/frappe/erpnext/pull/26560))
- Included company in link document type filters for contact ([#26576](https://github.com/frappe/erpnext/pull/26576))
- Ignore mandatory fields while creating payment reconciliation Journal Entry ([#26643](https://github.com/frappe/erpnext/pull/26643))
- Unable to download GSTR-1 json ([#26418](https://github.com/frappe/erpnext/pull/26418))
- Paging buttons not working on item group portal page ([#26498](https://github.com/frappe/erpnext/pull/26498))

View File

@ -674,19 +674,24 @@ class AccountsController(TransactionBase):
if self.get('doctype') in ['Purchase Invoice', 'Sales Invoice']: if self.get('doctype') in ['Purchase Invoice', 'Sales Invoice']:
for d in self.get("advances"): for d in self.get("advances"):
if d.exchange_gain_loss: if d.exchange_gain_loss:
party = self.supplier if self.get('doctype') == 'Purchase Invoice' else self.customer is_purchase_invoice = self.get('doctype') == 'Purchase Invoice'
party_account = self.credit_to if self.get('doctype') == 'Purchase Invoice' else self.debit_to party = self.supplier if is_purchase_invoice else self.customer
party_type = "Supplier" if self.get('doctype') == 'Purchase Invoice' else "Customer" party_account = self.credit_to if is_purchase_invoice else self.debit_to
party_type = "Supplier" if is_purchase_invoice else "Customer"
gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account') gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account')
if not gain_loss_account:
frappe.throw(_("Please set Default Exchange Gain/Loss Account in Company {}")
.format(self.get('company')))
account_currency = get_account_currency(gain_loss_account) account_currency = get_account_currency(gain_loss_account)
if account_currency != self.company_currency: if account_currency != self.company_currency:
frappe.throw(_("Currency for {0} must be {1}").format(d.account, self.company_currency)) frappe.throw(_("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency))
# for purchase # for purchase
dr_or_cr = 'debit' if d.exchange_gain_loss > 0 else 'credit' dr_or_cr = 'debit' if d.exchange_gain_loss > 0 else 'credit'
# just reverse for sales? if not is_purchase_invoice:
dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit' # just reverse for sales?
dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit'
gl_entries.append( gl_entries.append(
self.get_gl_dict({ self.get_gl_dict({
@ -904,9 +909,9 @@ class AccountsController(TransactionBase):
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings") frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
.format(item.item_code, item.idx, max_allowed_amt)) .format(item.item_code, item.idx, max_allowed_amt))
def get_company_default(self, fieldname): def get_company_default(self, fieldname, ignore_validation=False):
from erpnext.accounts.utils import get_company_default from erpnext.accounts.utils import get_company_default
return get_company_default(self.company, fieldname) return get_company_default(self.company, fieldname, ignore_validation=ignore_validation)
def get_stock_items(self): def get_stock_items(self):
stock_items = [] stock_items = []
@ -1112,8 +1117,11 @@ class AccountsController(TransactionBase):
for d in self.get("payment_schedule"): for d in self.get("payment_schedule"):
if d.invoice_portion: if d.invoice_portion:
d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount')) d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount'))
d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount')) d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('base_payment_amount'))
d.outstanding = d.payment_amount d.outstanding = d.payment_amount
elif not d.invoice_portion:
d.base_payment_amount = flt(base_grand_total * self.get("conversion_rate"), d.precision('base_payment_amount'))
def set_due_date(self): def set_due_date(self):
due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date] due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date]

View File

@ -0,0 +1,127 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe import _
from frappe.desk.form import assign_to
from frappe.model.document import Document
from frappe.utils import flt, unique
class EmployeeBoardingController(Document):
'''
Create the project and the task for the boarding process
Assign to the concerned person and roles as per the onboarding/separation template
'''
def validate(self):
# remove the task if linked before submitting the form
if self.amended_from:
for activity in self.activities:
activity.task = ''
def on_submit(self):
# create the project for the given employee onboarding
project_name = _(self.doctype) + ' : '
if self.doctype == 'Employee Onboarding':
project_name += self.job_applicant
else:
project_name += self.employee
project = frappe.get_doc({
'doctype': 'Project',
'project_name': project_name,
'expected_start_date': self.date_of_joining if self.doctype == 'Employee Onboarding' else self.resignation_letter_date,
'department': self.department,
'company': self.company
}).insert(ignore_permissions=True, ignore_mandatory=True)
self.db_set('project', project.name)
self.db_set('boarding_status', 'Pending')
self.reload()
self.create_task_and_notify_user()
def create_task_and_notify_user(self):
# create the task for the given project and assign to the concerned person
for activity in self.activities:
if activity.task:
continue
task = frappe.get_doc({
'doctype': 'Task',
'project': self.project,
'subject': activity.activity_name + ' : ' + self.employee_name,
'description': activity.description,
'department': self.department,
'company': self.company,
'task_weight': activity.task_weight
}).insert(ignore_permissions=True)
activity.db_set('task', task.name)
users = [activity.user] if activity.user else []
if activity.role:
user_list = frappe.db.sql_list('''
SELECT
DISTINCT(has_role.parent)
FROM
`tabHas Role` has_role
LEFT JOIN `tabUser` user
ON has_role.parent = user.name
WHERE
has_role.parenttype = 'User'
AND user.enabled = 1
AND has_role.role = %s
''', activity.role)
users = unique(users + user_list)
if 'Administrator' in users:
users.remove('Administrator')
# assign the task the users
if users:
self.assign_task_to_users(task, users)
def assign_task_to_users(self, task, users):
for user in users:
args = {
'assign_to': [user],
'doctype': task.doctype,
'name': task.name,
'description': task.description or task.subject,
'notify': self.notify_users_by_email
}
assign_to.add(args)
def on_cancel(self):
# delete task project
for task in frappe.get_all('Task', filters={'project': self.project}):
frappe.delete_doc('Task', task.name, force=1)
frappe.delete_doc('Project', self.project, force=1)
self.db_set('project', '')
for activity in self.activities:
activity.db_set('task', '')
@frappe.whitelist()
def get_onboarding_details(parent, parenttype):
return frappe.get_all('Employee Boarding Activity',
fields=['activity_name', 'role', 'user', 'required_for_employee_creation', 'description', 'task_weight'],
filters={'parent': parent, 'parenttype': parenttype},
order_by= 'idx')
def update_employee_boarding_status(project):
employee_onboarding = frappe.db.exists('Employee Onboarding', {'project': project.name})
employee_separation = frappe.db.exists('Employee Separation', {'project': project.name})
if not (employee_onboarding or employee_separation):
return
status = 'Pending'
if flt(project.percent_complete) > 0.0 and flt(project.percent_complete) < 100.0:
status = 'In Process'
elif flt(project.percent_complete) == 100.0:
status = 'Completed'
if employee_onboarding:
frappe.db.set_value('Employee Onboarding', employee_onboarding, 'boarding_status', status)
elif employee_separation:
frappe.db.set_value('Employee Separation', employee_separation, 'boarding_status', status)

View File

@ -407,6 +407,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
INNER JOIN `tabBatch` batch on sle.batch_no = batch.name INNER JOIN `tabBatch` batch on sle.batch_no = batch.name
where where
batch.disabled = 0 batch.disabled = 0
and sle.is_cancelled = 0
and sle.item_code = %(item_code)s and sle.item_code = %(item_code)s
and sle.warehouse = %(warehouse)s and sle.warehouse = %(warehouse)s
and (sle.batch_no like %(txt)s and (sle.batch_no like %(txt)s

View File

@ -53,12 +53,17 @@ class StockController(AccountsController):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
for d in self.get("items"): for d in self.get("items"):
if hasattr(d, 'serial_no') and hasattr(d, 'batch_no') and d.serial_no and d.batch_no: if hasattr(d, 'serial_no') and hasattr(d, 'batch_no') and d.serial_no and d.batch_no:
serial_nos = get_serial_nos(d.serial_no) serial_nos = frappe.get_all("Serial No",
for serial_no_data in frappe.get_all("Serial No", fields=["batch_no", "name", "warehouse"],
filters={"name": ("in", serial_nos)}, fields=["batch_no", "name"]): filters={
if serial_no_data.batch_no != d.batch_no: "name": ("in", get_serial_nos(d.serial_no))
}
)
for row in serial_nos:
if row.warehouse and row.batch_no != d.batch_no:
frappe.throw(_("Row #{0}: Serial No {1} does not belong to Batch {2}") frappe.throw(_("Row #{0}: Serial No {1} does not belong to Batch {2}")
.format(d.idx, serial_no_data.name, d.batch_no)) .format(d.idx, row.name, d.batch_no))
if flt(d.qty) > 0.0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2: if flt(d.qty) > 0.0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2:
expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date") expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date")

View File

@ -152,7 +152,7 @@ class calculate_taxes_and_totals(object):
validate_taxes_and_charges(tax) validate_taxes_and_charges(tax)
validate_inclusive_tax(tax, self.doc) validate_inclusive_tax(tax, self.doc)
if not self.doc.get('is_consolidated'): if not (self.doc.get('is_consolidated') or tax.get("dont_recompute_tax")):
tax.item_wise_tax_detail = {} tax.item_wise_tax_detail = {}
tax_fields = ["total", "tax_amount_after_discount_amount", tax_fields = ["total", "tax_amount_after_discount_amount",
@ -347,7 +347,7 @@ class calculate_taxes_and_totals(object):
elif tax.charge_type == "On Item Quantity": elif tax.charge_type == "On Item Quantity":
current_tax_amount = tax_rate * item.qty current_tax_amount = tax_rate * item.qty
if not self.doc.get("is_consolidated"): if not (self.doc.get("is_consolidated") or tax.get("dont_recompute_tax")):
self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount) self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount)
return current_tax_amount return current_tax_amount
@ -455,7 +455,8 @@ class calculate_taxes_and_totals(object):
def _cleanup(self): def _cleanup(self):
if not self.doc.get('is_consolidated'): if not self.doc.get('is_consolidated'):
for tax in self.doc.get("taxes"): for tax in self.doc.get("taxes"):
tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':')) if not tax.get("dont_recompute_tax"):
tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':'))
def set_discount_amount(self): def set_discount_amount(self):
if self.doc.additional_discount_percentage: if self.doc.additional_discount_percentage:

View File

View File

@ -0,0 +1,17 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Campaign', {
refresh: function(frm) {
erpnext.toggle_naming_series();
if (frm.doc.__islocal) {
frm.toggle_display("naming_series", frappe.boot.sysdefaults.campaign_naming_by=="Naming Series");
} else {
cur_frm.add_custom_button(__("View Leads"), function() {
frappe.route_options = {"source": "Campaign", "campaign_name": frm.doc.name};
frappe.set_route("List", "Lead");
}, "fa fa-list", true);
}
}
});

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
@ -39,17 +40,9 @@
"set_only_once": 1 "set_only_once": 1
}, },
{ {
"fieldname": "description", "fieldname": "campaign_schedules_section",
"fieldtype": "Text", "fieldtype": "Section Break",
"in_list_view": 1, "label": "Campaign Schedules"
"label": "Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
"width": "300px"
},
{
"fieldname": "description_section",
"fieldtype": "Section Break"
}, },
{ {
"fieldname": "campaign_schedules", "fieldname": "campaign_schedules",
@ -58,16 +51,25 @@
"options": "Campaign Email Schedule" "options": "Campaign Email Schedule"
}, },
{ {
"fieldname": "campaign_schedules_section", "fieldname": "description_section",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"label": "Campaign Schedules" },
{
"fieldname": "description",
"fieldtype": "Text",
"in_list_view": 1,
"label": "Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
"width": "300px"
} }
], ],
"icon": "fa fa-bullhorn", "icon": "fa fa-bullhorn",
"idx": 1, "idx": 1,
"modified": "2019-07-22 12:03:39.832342", "links": [],
"modified": "2021-06-30 18:05:06.412712",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "CRM",
"name": "Campaign", "name": "Campaign",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [

View File

@ -1,9 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# License: GNU General Public License v3. See license.txt # For license information, please see license.txt
from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.naming import set_name_by_naming_series from frappe.model.naming import set_name_by_naming_series

View File

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

View File

@ -34,11 +34,14 @@ def enroll_student(source_name):
} }
}}, ignore_permissions=True) }}, ignore_permissions=True)
student.save() student.save()
student_applicant = frappe.db.get_value("Student Applicant", source_name,
["student_category", "program"], as_dict=True)
program_enrollment = frappe.new_doc("Program Enrollment") program_enrollment = frappe.new_doc("Program Enrollment")
program_enrollment.student = student.name program_enrollment.student = student.name
program_enrollment.student_category = student.student_category program_enrollment.student_category = student_applicant.student_category
program_enrollment.student_name = student.title program_enrollment.student_name = student.title
program_enrollment.program = frappe.db.get_value("Student Applicant", source_name, "program") program_enrollment.program = student_applicant.program
frappe.publish_realtime('enroll_student_progress', {"progress": [2, 4]}, user=frappe.session.user) frappe.publish_realtime('enroll_student_progress', {"progress": [2, 4]}, user=frappe.session.user)
return program_enrollment return program_enrollment

View File

@ -1,195 +1,68 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0, "creation": "2016-06-10 03:29:02.539914",
"allow_import": 0, "doctype": "DocType",
"allow_rename": 0, "editable_grid": 1,
"beta": 0, "engine": "InnoDB",
"creation": "2016-06-10 03:29:02.539914", "field_order": [
"custom": 0, "student_applicant",
"docstatus": 0, "student",
"doctype": "DocType", "student_name",
"document_type": "", "column_break_3",
"editable_grid": 1, "student_batch_name",
"student_category"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "student_applicant",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Student Applicant",
"columns": 0, "options": "Student Applicant"
"depends_on": "", },
"fieldname": "student_applicant",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Student Applicant",
"length": 0,
"no_copy": 0,
"options": "Student Applicant",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "student",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Student",
"columns": 0, "options": "Student"
"depends_on": "", },
"fieldname": "student",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Student",
"length": 0,
"no_copy": 0,
"options": "Student",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_3",
"allow_on_submit": 0, "fieldtype": "Column Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "student_name",
"allow_on_submit": 0, "fieldtype": "Data",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Student Name",
"columns": 0, "read_only": 1
"fieldname": "student_name", },
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Student Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "student_batch_name",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Student Batch Name",
"columns": 0, "options": "Student Batch Name"
"fieldname": "student_batch_name", },
"fieldtype": "Link", {
"hidden": 0, "fieldname": "student_category",
"ignore_user_permissions": 0, "fieldtype": "Link",
"ignore_xss_filter": 0, "label": "Student Category",
"in_filter": 0, "options": "Student Category",
"in_global_search": 0, "read_only": 1
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Student Batch Name",
"length": 0,
"no_copy": 0,
"options": "Student Batch Name",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "istable": 1,
"hide_heading": 0, "links": [],
"hide_toolbar": 0, "modified": "2021-07-29 18:19:54.471594",
"idx": 0, "modified_by": "Administrator",
"image_view": 0, "module": "Education",
"in_create": 0, "name": "Program Enrollment Tool Student",
"is_submittable": 0, "owner": "Administrator",
"issingle": 0, "permissions": [],
"istable": 1, "quick_entry": 1,
"max_attachments": 0, "restrict_to_domain": "Education",
"modified": "2018-01-02 12:03:53.890741", "sort_field": "modified",
"modified_by": "Administrator", "sort_order": "DESC"
"module": "Education",
"name": "Program Enrollment Tool Student",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"restrict_to_domain": "Education",
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0
} }

View File

@ -9,7 +9,7 @@ from frappe.utils import flt, getdate
from frappe import _ from frappe import _
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import set_employee_name from erpnext.hr.utils import set_employee_name, validate_active_employee
class Appraisal(Document): class Appraisal(Document):
def validate(self): def validate(self):
@ -19,6 +19,7 @@ class Appraisal(Document):
if not self.goals: if not self.goals:
frappe.throw(_("Goals cannot be empty")) frappe.throw(_("Goals cannot be empty"))
validate_active_employee(self.employee)
set_employee_name(self) set_employee_name(self)
self.validate_dates() self.validate_dates()
self.validate_existing_appraisal() self.validate_existing_appraisal()

View File

@ -8,11 +8,13 @@ from frappe.utils import getdate, nowdate
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cstr, get_datetime, formatdate from frappe.utils import cstr, get_datetime, formatdate
from erpnext.hr.utils import validate_active_employee
class Attendance(Document): class Attendance(Document):
def validate(self): def validate(self):
from erpnext.controllers.status_updater import validate_status from erpnext.controllers.status_updater import validate_status
validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"]) validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"])
validate_active_employee(self.employee)
self.validate_attendance_date() self.validate_attendance_date()
self.validate_duplicate_record() self.validate_duplicate_record()
self.validate_employee_status() self.validate_employee_status()

View File

@ -8,10 +8,11 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import date_diff, add_days, getdate from frappe.utils import date_diff, add_days, getdate
from erpnext.hr.doctype.employee.employee import is_holiday from erpnext.hr.doctype.employee.employee import is_holiday
from erpnext.hr.utils import validate_dates from erpnext.hr.utils import validate_dates, validate_active_employee
class AttendanceRequest(Document): class AttendanceRequest(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
validate_dates(self, self.from_date, self.to_date) validate_dates(self, self.from_date, self.to_date)
if self.half_day: if self.half_day:
if not getdate(self.from_date)<=getdate(self.half_day_date)<=getdate(self.to_date): if not getdate(self.from_date)<=getdate(self.half_day_date)<=getdate(self.to_date):

View File

@ -7,12 +7,13 @@ import frappe
from frappe import _ from frappe import _
from frappe.utils import date_diff, add_days, getdate, cint, format_date from frappe.utils import date_diff, add_days, getdate, cint, format_date
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, \ from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, validate_active_employee, \
get_holidays_for_employee, create_additional_leave_ledger_entry get_holidays_for_employee, create_additional_leave_ledger_entry
class CompensatoryLeaveRequest(Document): class CompensatoryLeaveRequest(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
validate_dates(self, self.work_from_date, self.work_end_date) validate_dates(self, self.work_from_date, self.work_end_date)
if self.half_day: if self.half_day:
if not self.half_day_date: if not self.half_day_date:

View File

@ -13,8 +13,10 @@ from frappe.model.document import Document
from erpnext.utilities.transaction_base import delete_events from erpnext.utilities.transaction_base import delete_events
from frappe.utils.nestedset import NestedSet from frappe.utils.nestedset import NestedSet
class EmployeeUserDisabledError(frappe.ValidationError): pass class EmployeeUserDisabledError(frappe.ValidationError):
class EmployeeLeftValidationError(frappe.ValidationError): pass pass
class InactiveEmployeeStatusError(frappe.ValidationError):
pass
class Employee(NestedSet): class Employee(NestedSet):
nsm_parent_field = 'reports_to' nsm_parent_field = 'reports_to'
@ -196,7 +198,7 @@ class Employee(NestedSet):
message += "<br><br><ul><li>" + "</li><li>".join(link_to_employees) message += "<br><br><ul><li>" + "</li><li>".join(link_to_employees)
message += "</li></ul><br>" message += "</li></ul><br>"
message += _("Please make sure the employees above report to another Active employee.") message += _("Please make sure the employees above report to another Active employee.")
throw(message, EmployeeLeftValidationError, _("Cannot Relieve Employee")) throw(message, InactiveEmployeeStatusError, _("Cannot Relieve Employee"))
if not self.relieving_date: if not self.relieving_date:
throw(_("Please enter relieving date.")) throw(_("Please enter relieving date."))

View File

@ -7,7 +7,7 @@ import frappe
import erpnext import erpnext
import unittest import unittest
import frappe.utils import frappe.utils
from erpnext.hr.doctype.employee.employee import EmployeeLeftValidationError from erpnext.hr.doctype.employee.employee import InactiveEmployeeStatusError
test_records = frappe.get_test_records('Employee') test_records = frappe.get_test_records('Employee')
@ -45,10 +45,33 @@ class TestEmployee(unittest.TestCase):
employee2_doc.save() employee2_doc.save()
employee1_doc.reload() employee1_doc.reload()
employee1_doc.status = 'Left' employee1_doc.status = 'Left'
self.assertRaises(EmployeeLeftValidationError, employee1_doc.save) self.assertRaises(InactiveEmployeeStatusError, employee1_doc.save)
def test_employee_status_inactive(self):
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
employee = make_employee("test_employee_status@company.com")
employee_doc = frappe.get_doc("Employee", employee)
employee_doc.status = "Inactive"
employee_doc.save()
employee_doc.reload()
make_holiday_list()
frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List")
frappe.db.sql("""delete from `tabSalary Structure` where name='Test Inactive Employee Salary Slip'""")
salary_structure = make_salary_structure("Test Inactive Employee Salary Slip", "Monthly",
employee=employee_doc.name, company=employee_doc.company)
salary_slip = make_salary_slip(salary_structure.name, employee=employee_doc.name)
self.assertRaises(InactiveEmployeeStatusError, salary_slip.save)
def tearDown(self):
frappe.db.rollback()
def make_employee(user, company=None, **kwargs): def make_employee(user, company=None, **kwargs):
""
if not frappe.db.get_value("User", user): if not frappe.db.get_value("User", user):
frappe.get_doc({ frappe.get_doc({
"doctype": "User", "doctype": "User",
@ -80,4 +103,5 @@ def make_employee(user, company=None, **kwargs):
employee.insert() employee.insert()
return employee.name return employee.name
else: else:
frappe.db.set_value("Employee", {"employee_name":user}, "status", "Active")
return frappe.get_value("Employee", {"employee_name":user}, "name") return frappe.get_value("Employee", {"employee_name":user}, "name")

View File

@ -8,6 +8,7 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import flt, nowdate from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
from erpnext.hr.utils import validate_active_employee
class EmployeeAdvanceOverPayment(frappe.ValidationError): class EmployeeAdvanceOverPayment(frappe.ValidationError):
pass pass
@ -18,11 +19,11 @@ class EmployeeAdvance(Document):
'make_payment_via_journal_entry') 'make_payment_via_journal_entry')
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.set_status() self.set_status()
def on_cancel(self): def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry') self.ignore_linked_doctypes = ('GL Entry')
self.set_status()
def set_status(self): def set_status(self):
if self.docstatus == 0: if self.docstatus == 0:
@ -183,9 +184,9 @@ def make_return_entry(employee, company, employee_advance_name, return_amount,
bank_cash_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment) bank_cash_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment)
if not bank_cash_account: if not bank_cash_account:
frappe.throw(_("Please set a Default Cash Account in Company defaults")) frappe.throw(_("Please set a Default Cash Account in Company defaults"))
advance_account_currency = frappe.db.get_value('Account', advance_account, 'account_currency') advance_account_currency = frappe.db.get_value('Account', advance_account, 'account_currency')
je = frappe.new_doc('Journal Entry') je = frappe.new_doc('Journal Entry')
je.posting_date = nowdate() je.posting_date = nowdate()
je.voucher_type = get_voucher_type(mode_of_payment) je.voucher_type = get_voucher_type(mode_of_payment)
@ -229,4 +230,4 @@ def get_voucher_type(mode_of_payment=None):
if mode_of_payment_type == "Bank": if mode_of_payment_type == "Bank":
voucher_type = "Bank Entry" voucher_type = "Bank Entry"
return voucher_type return voucher_type

View File

@ -9,9 +9,11 @@ from frappe.model.document import Document
from frappe import _ from frappe import _
from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift
from erpnext.hr.utils import validate_active_employee
class EmployeeCheckin(Document): class EmployeeCheckin(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.validate_duplicate_log() self.validate_duplicate_log()
self.fetch_shift() self.fetch_shift()
@ -122,7 +124,7 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki
def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type): def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
"""Given a set of logs in chronological order calculates the total working hours based on the parameters. """Given a set of logs in chronological order calculates the total working hours based on the parameters.
Zero is returned for all invalid cases. Zero is returned for all invalid cases.
:param logs: The List of 'Employee Checkin'. :param logs: The List of 'Employee Checkin'.
:param check_in_out_type: One of: 'Alternating entries as IN and OUT during the same shift', 'Strictly based on Log Type in Employee Checkin' :param check_in_out_type: One of: 'Alternating entries as IN and OUT during the same shift', 'Strictly based on Log Type in Employee Checkin'
:param working_hours_calc_type: One of: 'First Check-in and Last Check-out', 'Every Valid Check-in and Check-out' :param working_hours_calc_type: One of: 'First Check-in and Last Check-out', 'Every Valid Check-in and Check-out'

View File

@ -50,28 +50,13 @@ frappe.ui.form.on('Employee Onboarding', {
}, __('Create')); }, __('Create'));
frm.page.set_inner_btn_group_as_primary(__('Create')); frm.page.set_inner_btn_group_as_primary(__('Create'));
} }
if (frm.doc.docstatus === 1 && frm.doc.project) {
frappe.call({
method: "erpnext.hr.utils.get_boarding_status",
args: {
"project": frm.doc.project
},
callback: function(r) {
if (r.message) {
frm.set_value('boarding_status', r.message);
}
refresh_field("boarding_status");
}
});
}
}, },
employee_onboarding_template: function(frm) { employee_onboarding_template: function(frm) {
frm.set_value("activities" ,""); frm.set_value("activities" ,"");
if (frm.doc.employee_onboarding_template) { if (frm.doc.employee_onboarding_template) {
frappe.call({ frappe.call({
method: "erpnext.hr.utils.get_onboarding_details", method: "erpnext.controllers.employee_boarding_controller.get_onboarding_details",
args: { args: {
"parent": frm.doc.employee_onboarding_template, "parent": frm.doc.employee_onboarding_template,
"parenttype": "Employee Onboarding Template" "parenttype": "Employee Onboarding Template"

View File

@ -30,18 +30,14 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Job Applicant", "label": "Job Applicant",
"options": "Job Applicant", "options": "Job Applicant",
"reqd": 1, "reqd": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "job_offer", "fieldname": "job_offer",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Job Offer", "label": "Job Offer",
"options": "Job Offer", "options": "Job Offer",
"reqd": 1, "reqd": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fetch_from": "job_applicant.applicant_name", "fetch_from": "job_applicant.applicant_name",
@ -49,116 +45,90 @@
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "Employee Name", "label": "Employee Name",
"reqd": 1, "reqd": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "employee", "fieldname": "employee",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Employee", "label": "Employee",
"options": "Employee", "options": "Employee",
"read_only": 1, "read_only": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "date_of_joining", "fieldname": "date_of_joining",
"fieldtype": "Date", "fieldtype": "Date",
"in_list_view": 1, "in_list_view": 1,
"label": "Date of Joining", "label": "Date of Joining"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"default": "Pending",
"fieldname": "boarding_status", "fieldname": "boarding_status",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Status", "label": "Status",
"options": "\nPending\nIn Process\nCompleted", "options": "Pending\nIn Process\nCompleted",
"show_days": 1, "read_only": 1
"show_seconds": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"default": "0", "default": "0",
"fieldname": "notify_users_by_email", "fieldname": "notify_users_by_email",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Notify users by email", "label": "Notify users by email"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_7", "fieldname": "column_break_7",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "employee_onboarding_template", "fieldname": "employee_onboarding_template",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Employee Onboarding Template", "label": "Employee Onboarding Template",
"options": "Employee Onboarding Template", "options": "Employee Onboarding Template"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Company", "label": "Company",
"options": "Company", "options": "Company"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "department", "fieldname": "department",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Department", "label": "Department",
"options": "Department", "options": "Department"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "designation", "fieldname": "designation",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Designation", "label": "Designation",
"options": "Designation", "options": "Designation"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "employee_grade", "fieldname": "employee_grade",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Employee Grade", "label": "Employee Grade",
"options": "Employee Grade", "options": "Employee Grade"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "project", "fieldname": "project",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Project", "label": "Project",
"options": "Project", "options": "Project",
"read_only": 1, "read_only": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "table_for_activity", "fieldname": "table_for_activity",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "activities", "fieldname": "activities",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Activities", "label": "Activities",
"options": "Employee Boarding Activity", "options": "Employee Boarding Activity"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "amended_from", "fieldname": "amended_from",
@ -167,14 +137,12 @@
"no_copy": 1, "no_copy": 1,
"options": "Employee Onboarding", "options": "Employee Onboarding",
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1
"show_days": 1,
"show_seconds": 1
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-06-25 15:22:24.923835", "modified": "2021-06-03 18:01:51.097927",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee Onboarding", "name": "Employee Onboarding",

View File

@ -5,7 +5,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from erpnext.hr.utils import EmployeeBoardingController from erpnext.controllers.employee_boarding_controller import EmployeeBoardingController
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
class IncompleteTaskError(frappe.ValidationError): pass class IncompleteTaskError(frappe.ValidationError): pass
@ -16,9 +16,9 @@ class EmployeeOnboarding(EmployeeBoardingController):
self.validate_duplicate_employee_onboarding() self.validate_duplicate_employee_onboarding()
def validate_duplicate_employee_onboarding(self): def validate_duplicate_employee_onboarding(self):
emp_onboarding = frappe.db.exists("Employee Onboarding",{"job_applicant": self.job_applicant}) emp_onboarding = frappe.db.exists("Employee Onboarding", {"job_applicant": self.job_applicant})
if emp_onboarding and emp_onboarding != self.name: if emp_onboarding and emp_onboarding != self.name:
frappe.throw(_("Employee Onboarding: {0} is already for Job Applicant: {1}").format(frappe.bold(emp_onboarding), frappe.bold(self.job_applicant))) frappe.throw(_("Employee Onboarding: {0} already exists for Job Applicant: {1}").format(frappe.bold(emp_onboarding), frappe.bold(self.job_applicant)))
def validate_employee_creation(self): def validate_employee_creation(self):
if self.docstatus != 1: if self.docstatus != 1:
@ -30,7 +30,7 @@ class EmployeeOnboarding(EmployeeBoardingController):
else: else:
task_status = frappe.db.get_value("Task", activity.task, "status") task_status = frappe.db.get_value("Task", activity.task, "status")
if task_status not in ["Completed", "Cancelled"]: if task_status not in ["Completed", "Cancelled"]:
frappe.throw(_("All the mandatory Task for employee creation hasn't been done yet."), IncompleteTaskError) frappe.throw(_("All the mandatory tasks for employee creation are not completed yet."), IncompleteTaskError)
def on_submit(self): def on_submit(self):
super(EmployeeOnboarding, self).on_submit() super(EmployeeOnboarding, self).on_submit()

View File

@ -11,39 +11,26 @@ from erpnext.hr.doctype.employee_onboarding.employee_onboarding import Incomplet
from erpnext.hr.doctype.job_offer.test_job_offer import create_job_offer from erpnext.hr.doctype.job_offer.test_job_offer import create_job_offer
class TestEmployeeOnboarding(unittest.TestCase): class TestEmployeeOnboarding(unittest.TestCase):
def test_employee_onboarding_incomplete_task(self): def setUp(self):
if frappe.db.exists('Employee Onboarding', {'employee_name': 'Test Researcher'}): if frappe.db.exists('Employee Onboarding', {'employee_name': 'Test Researcher'}):
frappe.delete_doc('Employee Onboarding', {'employee_name': 'Test Researcher'}) frappe.delete_doc('Employee Onboarding', {'employee_name': 'Test Researcher'})
_set_up()
applicant = get_job_applicant()
job_offer = create_job_offer(job_applicant=applicant.name) project = "Employee Onboarding : Test Researcher - test@researcher.com"
job_offer.submit() frappe.db.sql("delete from tabProject where name=%s", project)
frappe.db.sql("delete from tabTask where project=%s", project)
onboarding = frappe.new_doc('Employee Onboarding') def test_employee_onboarding_incomplete_task(self):
onboarding.job_applicant = applicant.name onboarding = create_employee_onboarding()
onboarding.job_offer = job_offer.name
onboarding.company = '_Test Company'
onboarding.designation = 'Researcher'
onboarding.append('activities', {
'activity_name': 'Assign ID Card',
'role': 'HR User',
'required_for_employee_creation': 1
})
onboarding.append('activities', {
'activity_name': 'Assign a laptop',
'role': 'HR User'
})
onboarding.status = 'Pending'
onboarding.insert()
onboarding.submit()
project_name = frappe.db.get_value("Project", onboarding.project, "project_name") project_name = frappe.db.get_value('Project', onboarding.project, 'project_name')
self.assertEqual(project_name, 'Employee Onboarding : Test Researcher - test@researcher.com') self.assertEqual(project_name, 'Employee Onboarding : Test Researcher - test@researcher.com')
# don't allow making employee if onboarding is not complete # don't allow making employee if onboarding is not complete
self.assertRaises(IncompleteTaskError, make_employee, onboarding.name) self.assertRaises(IncompleteTaskError, make_employee, onboarding.name)
# boarding status
self.assertEqual(onboarding.boarding_status, 'Pending')
# complete the task # complete the task
project = frappe.get_doc('Project', onboarding.project) project = frappe.get_doc('Project', onboarding.project)
for task in frappe.get_all('Task', dict(project=project.name)): for task in frappe.get_all('Task', dict(project=project.name)):
@ -51,6 +38,10 @@ class TestEmployeeOnboarding(unittest.TestCase):
task.status = 'Completed' task.status = 'Completed'
task.save() task.save()
# boarding status
onboarding.reload()
self.assertEqual(onboarding.boarding_status, 'Completed')
# make employee # make employee
onboarding.reload() onboarding.reload()
employee = make_employee(onboarding.name) employee = make_employee(onboarding.name)
@ -61,6 +52,13 @@ class TestEmployeeOnboarding(unittest.TestCase):
employee.insert() employee.insert()
self.assertEqual(employee.employee_name, 'Test Researcher') self.assertEqual(employee.employee_name, 'Test Researcher')
def tearDown(self):
for entry in frappe.get_all('Employee Onboarding'):
doc = frappe.get_doc('Employee Onboarding', entry.name)
doc.cancel()
doc.delete()
def get_job_applicant(): def get_job_applicant():
if frappe.db.exists('Job Applicant', 'Test Researcher - test@researcher.com'): if frappe.db.exists('Job Applicant', 'Test Researcher - test@researcher.com'):
return frappe.get_doc('Job Applicant', 'Test Researcher - test@researcher.com') return frappe.get_doc('Job Applicant', 'Test Researcher - test@researcher.com')
@ -72,10 +70,35 @@ def get_job_applicant():
applicant.insert() applicant.insert()
return applicant return applicant
def _set_up(): def get_job_offer(applicant_name):
for doctype in ["Employee Onboarding"]: job_offer = frappe.db.exists('Job Offer', {'job_applicant': applicant_name})
frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype)) if job_offer:
return frappe.get_doc('Job Offer', job_offer)
project = "Employee Onboarding : Test Researcher - test@researcher.com" job_offer = create_job_offer(job_applicant=applicant_name)
frappe.db.sql("delete from tabProject where name=%s", project) job_offer.submit()
frappe.db.sql("delete from tabTask where project=%s", project) return job_offer
def create_employee_onboarding():
applicant = get_job_applicant()
job_offer = get_job_offer(applicant.name)
onboarding = frappe.new_doc('Employee Onboarding')
onboarding.job_applicant = applicant.name
onboarding.job_offer = job_offer.name
onboarding.company = '_Test Company'
onboarding.designation = 'Researcher'
onboarding.append('activities', {
'activity_name': 'Assign ID Card',
'role': 'HR User',
'required_for_employee_creation': 1
})
onboarding.append('activities', {
'activity_name': 'Assign a laptop',
'role': 'HR User'
})
onboarding.status = 'Pending'
onboarding.insert()
onboarding.submit()
return onboarding

View File

@ -7,12 +7,11 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import getdate from frappe.utils import getdate
from erpnext.hr.utils import update_employee from erpnext.hr.utils import update_employee, validate_active_employee
class EmployeePromotion(Document): class EmployeePromotion(Document):
def validate(self): def validate(self):
if frappe.get_value("Employee", self.employee, "status") != "Active": validate_active_employee(self.employee)
frappe.throw(_("Cannot promote Employee with status Left or Inactive"))
def before_submit(self): def before_submit(self):
if getdate(self.promotion_date) > getdate(): if getdate(self.promotion_date) > getdate():

View File

@ -7,9 +7,11 @@ import frappe
from frappe import _ from frappe import _
from frappe.utils import get_link_to_form from frappe.utils import get_link_to_form
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import validate_active_employee
class EmployeeReferral(Document): class EmployeeReferral(Document):
def validate(self): def validate(self):
validate_active_employee(self.referrer)
self.set_full_name() self.set_full_name()
self.set_referral_bonus_payment_status() self.set_referral_bonus_payment_status()

View File

@ -23,27 +23,13 @@ frappe.ui.form.on('Employee Separation', {
frappe.set_route('List', 'Task', {project: frm.doc.project}); frappe.set_route('List', 'Task', {project: frm.doc.project});
},__("View")); },__("View"));
} }
if (frm.doc.docstatus === 1 && frm.doc.project) {
frappe.call({
method: "erpnext.hr.utils.get_boarding_status",
args: {
"project": frm.doc.project
},
callback: function(r) {
if (r.message) {
frm.set_value('boarding_status', r.message);
}
refresh_field("boarding_status");
}
});
}
}, },
employee_separation_template: function(frm) { employee_separation_template: function(frm) {
frm.set_value("activities" ,""); frm.set_value("activities" ,"");
if (frm.doc.employee_separation_template) { if (frm.doc.employee_separation_template) {
frappe.call({ frappe.call({
method: "erpnext.hr.utils.get_onboarding_details", method: "erpnext.controllers.employee_boarding_controller.get_onboarding_details",
args: { args: {
"parent": frm.doc.employee_separation_template, "parent": frm.doc.employee_separation_template,
"parenttype": "Employee Separation Template" "parenttype": "Employee Separation Template"

View File

@ -50,11 +50,12 @@
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"default": "Pending",
"fieldname": "boarding_status", "fieldname": "boarding_status",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Status", "label": "Status",
"options": "\nPending\nIn Process\nCompleted", "options": "Pending\nIn Process\nCompleted",
"reqd": 1 "read_only": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
@ -147,7 +148,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-28 15:58:36.020196", "modified": "2021-06-03 18:02:54.007313",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee Separation", "name": "Employee Separation",

View File

@ -3,7 +3,7 @@
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
from erpnext.hr.utils import EmployeeBoardingController from erpnext.controllers.employee_boarding_controller import EmployeeBoardingController
class EmployeeSeparation(EmployeeBoardingController): class EmployeeSeparation(EmployeeBoardingController):
def validate(self): def validate(self):

View File

@ -6,21 +6,43 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
test_dependencies = ["Employee Onboarding"] test_dependencies = ['Employee Onboarding']
class TestEmployeeSeparation(unittest.TestCase): class TestEmployeeSeparation(unittest.TestCase):
def test_employee_separation(self): def test_employee_separation(self):
employee = frappe.db.get_value("Employee", {"status": "Active"}) separation = create_employee_separation()
separation = frappe.new_doc('Employee Separation')
separation.employee = employee
separation.company = '_Test Company'
separation.append('activities', {
'activity_name': 'Deactivate Employee',
'role': 'HR User'
})
separation.boarding_status = 'Pending'
separation.insert()
separation.submit()
self.assertEqual(separation.docstatus, 1) self.assertEqual(separation.docstatus, 1)
self.assertEqual(separation.boarding_status, 'Pending')
project = frappe.get_doc('Project', separation.project)
project.percent_complete_method = 'Manual'
project.status = 'Completed'
project.save()
separation.reload()
self.assertEqual(separation.boarding_status, 'Completed')
separation.cancel() separation.cancel()
self.assertEqual(separation.project, "") self.assertEqual(separation.project, '')
def tearDown(self):
for entry in frappe.get_all('Employee Separation'):
doc = frappe.get_doc('Employee Separation', entry.name)
if doc.docstatus == 1:
doc.cancel()
doc.delete()
def create_employee_separation():
employee = frappe.db.get_value('Employee', {'status': 'Active'})
separation = frappe.new_doc('Employee Separation')
separation.employee = employee
separation.company = '_Test Company'
separation.append('activities', {
'activity_name': 'Deactivate Employee',
'role': 'HR User'
})
separation.boarding_status = 'Pending'
separation.insert()
separation.submit()
return separation

View File

@ -10,10 +10,6 @@ from frappe.utils import getdate
from erpnext.hr.utils import update_employee from erpnext.hr.utils import update_employee
class EmployeeTransfer(Document): class EmployeeTransfer(Document):
def validate(self):
if frappe.get_value("Employee", self.employee, "status") != "Active":
frappe.throw(_("Cannot transfer Employee with status Left or Inactive"))
def before_submit(self): def before_submit(self):
if getdate(self.transfer_date) > getdate(): if getdate(self.transfer_date) > getdate():
frappe.throw(_("Employee Transfer cannot be submitted before Transfer Date"), frappe.throw(_("Employee Transfer cannot be submitted before Transfer Date"),

View File

@ -6,7 +6,7 @@ import frappe, erpnext
from frappe import _ from frappe import _
from frappe.utils import get_fullname, flt, cstr, get_link_to_form from frappe.utils import get_fullname, flt, cstr, get_link_to_form
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import set_employee_name, share_doc_with_approver from erpnext.hr.utils import set_employee_name, share_doc_with_approver, validate_active_employee
from erpnext.accounts.party import get_party_account from erpnext.accounts.party import get_party_account
from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
@ -23,6 +23,7 @@ class ExpenseClaim(AccountsController):
'make_payment_via_journal_entry') 'make_payment_via_journal_entry')
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.validate_advances() self.validate_advances()
self.validate_sanctioned_amount() self.validate_sanctioned_amount()
self.calculate_total_amount() self.calculate_total_amount()
@ -35,8 +36,8 @@ class ExpenseClaim(AccountsController):
if self.task and not self.project: if self.task and not self.project:
self.project = frappe.db.get_value("Task", self.task, "project") self.project = frappe.db.get_value("Task", self.task, "project")
def set_status(self): def set_status(self, update=False):
self.status = { status = {
"0": "Draft", "0": "Draft",
"1": "Submitted", "1": "Submitted",
"2": "Cancelled" "2": "Cancelled"
@ -44,14 +45,18 @@ class ExpenseClaim(AccountsController):
paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_amount) paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_amount)
precision = self.precision("grand_total") precision = self.precision("grand_total")
if (self.is_paid or (flt(self.total_sanctioned_amount) > 0 if (self.is_paid or (flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1
and flt(self.grand_total, precision) == flt(paid_amount, precision))) \ and flt(self.grand_total, precision) == flt(paid_amount, precision))) and self.approval_status == 'Approved':
and self.docstatus == 1 and self.approval_status == 'Approved': status = "Paid"
self.status = "Paid"
elif flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 and self.approval_status == 'Approved': elif flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 and self.approval_status == 'Approved':
self.status = "Unpaid" status = "Unpaid"
elif self.docstatus == 1 and self.approval_status == 'Rejected': elif self.docstatus == 1 and self.approval_status == 'Rejected':
self.status = 'Rejected' status = 'Rejected'
if update:
self.db_set("status", status)
else:
self.status = status
def on_update(self): def on_update(self):
share_doc_with_approver(self, self.expense_approver) share_doc_with_approver(self, self.expense_approver)
@ -74,7 +79,7 @@ class ExpenseClaim(AccountsController):
if self.is_paid: if self.is_paid:
update_reimbursed_amount(self) update_reimbursed_amount(self)
self.set_status() self.set_status(update=True)
self.update_claimed_amount_in_employee_advance() self.update_claimed_amount_in_employee_advance()
def on_cancel(self): def on_cancel(self):
@ -86,7 +91,6 @@ class ExpenseClaim(AccountsController):
if self.is_paid: if self.is_paid:
update_reimbursed_amount(self) update_reimbursed_amount(self)
self.set_status()
self.update_claimed_amount_in_employee_advance() self.update_claimed_amount_in_employee_advance()
def update_claimed_amount_in_employee_advance(self): def update_claimed_amount_in_employee_advance(self):

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, get_fullname, add_days, nowdate from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, get_fullname, add_days, nowdate
from erpnext.hr.utils import set_employee_name, get_leave_period, share_doc_with_approver from erpnext.hr.utils import set_employee_name, get_leave_period, share_doc_with_approver, validate_active_employee
from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import daterange from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import daterange
@ -22,6 +22,7 @@ class LeaveApplication(Document):
return _("{0}: From {0} of type {1}").format(self.employee_name, self.leave_type) return _("{0}: From {0} of type {1}").format(self.employee_name, self.leave_type)
def validate(self): def validate(self):
validate_active_employee(self.employee)
set_employee_name(self) set_employee_name(self)
self.validate_dates() self.validate_dates()
self.validate_balance_leaves() self.validate_balance_leaves()

View File

@ -7,7 +7,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import getdate, nowdate, flt from frappe.utils import getdate, nowdate, flt
from erpnext.hr.utils import set_employee_name from erpnext.hr.utils import set_employee_name, validate_active_employee
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry
from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves
@ -15,6 +15,7 @@ from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leav
class LeaveEncashment(Document): class LeaveEncashment(Document):
def validate(self): def validate(self):
set_employee_name(self) set_employee_name(self)
validate_active_employee(self.employee)
self.get_leave_details_for_encashment() self.get_leave_details_for_encashment()
self.validate_salary_structure() self.validate_salary_structure()

View File

@ -9,10 +9,12 @@ from frappe.model.document import Document
from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, now_datetime, nowdate from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, now_datetime, nowdate
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
from erpnext.hr.utils import validate_active_employee
from datetime import timedelta, datetime from datetime import timedelta, datetime
class ShiftAssignment(Document): class ShiftAssignment(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.validate_overlapping_dates() self.validate_overlapping_dates()
if self.end_date and self.end_date <= self.start_date: if self.end_date and self.end_date <= self.start_date:

View File

@ -7,12 +7,13 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import formatdate, getdate from frappe.utils import formatdate, getdate
from erpnext.hr.utils import share_doc_with_approver from erpnext.hr.utils import share_doc_with_approver, validate_active_employee
class OverlapError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError): pass
class ShiftRequest(Document): class ShiftRequest(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.validate_dates() self.validate_dates()
self.validate_shift_request_overlap_dates() self.validate_shift_request_overlap_dates()
self.validate_approver() self.validate_approver()

View File

@ -5,6 +5,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import validate_active_employee
class TravelRequest(Document): class TravelRequest(Document):
pass def validate(self):
validate_active_employee(self.employee)

View File

@ -3,128 +3,15 @@
import erpnext import erpnext
import frappe import frappe
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee, InactiveEmployeeStatusError
from frappe import _ from frappe import _
from frappe.desk.form import assign_to from frappe.desk.form import assign_to
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import (add_days, cstr, flt, format_datetime, formatdate, from frappe.utils import (add_days, cstr, flt, format_datetime, formatdate,
get_datetime, getdate, nowdate, today, unique) get_datetime, getdate, nowdate, today, unique, get_link_to_form)
class DuplicateDeclarationError(frappe.ValidationError): pass class DuplicateDeclarationError(frappe.ValidationError): pass
class EmployeeBoardingController(Document):
'''
Create the project and the task for the boarding process
Assign to the concerned person and roles as per the onboarding/separation template
'''
def validate(self):
# remove the task if linked before submitting the form
if self.amended_from:
for activity in self.activities:
activity.task = ''
def on_submit(self):
# create the project for the given employee onboarding
project_name = _(self.doctype) + " : "
if self.doctype == "Employee Onboarding":
project_name += self.job_applicant
else:
project_name += self.employee
project = frappe.get_doc({
"doctype": "Project",
"project_name": project_name,
"expected_start_date": self.date_of_joining if self.doctype == "Employee Onboarding" else self.resignation_letter_date,
"department": self.department,
"company": self.company
}).insert(ignore_permissions=True, ignore_mandatory=True)
self.db_set("project", project.name)
self.db_set("boarding_status", "Pending")
self.reload()
self.create_task_and_notify_user()
def create_task_and_notify_user(self):
# create the task for the given project and assign to the concerned person
for activity in self.activities:
if activity.task:
continue
task = frappe.get_doc({
"doctype": "Task",
"project": self.project,
"subject": activity.activity_name + " : " + self.employee_name,
"description": activity.description,
"department": self.department,
"company": self.company,
"task_weight": activity.task_weight
}).insert(ignore_permissions=True)
activity.db_set("task", task.name)
users = [activity.user] if activity.user else []
if activity.role:
user_list = frappe.db.sql_list('''
SELECT
DISTINCT(has_role.parent)
FROM
`tabHas Role` has_role
LEFT JOIN `tabUser` user
ON has_role.parent = user.name
WHERE
has_role.parenttype = 'User'
AND user.enabled = 1
AND has_role.role = %s
''', activity.role)
users = unique(users + user_list)
if "Administrator" in users:
users.remove("Administrator")
# assign the task the users
if users:
self.assign_task_to_users(task, users)
def assign_task_to_users(self, task, users):
for user in users:
args = {
'assign_to': [user],
'doctype': task.doctype,
'name': task.name,
'description': task.description or task.subject,
'notify': self.notify_users_by_email
}
assign_to.add(args)
def on_cancel(self):
# delete task project
for task in frappe.get_all("Task", filters={"project": self.project}):
frappe.delete_doc("Task", task.name, force=1)
frappe.delete_doc("Project", self.project, force=1)
self.db_set('project', '')
for activity in self.activities:
activity.db_set("task", "")
@frappe.whitelist()
def get_onboarding_details(parent, parenttype):
return frappe.get_all("Employee Boarding Activity",
fields=["activity_name", "role", "user", "required_for_employee_creation", "description", "task_weight"],
filters={"parent": parent, "parenttype": parenttype},
order_by= "idx")
@frappe.whitelist()
def get_boarding_status(project):
status = 'Pending'
if project:
doc = frappe.get_doc('Project', project)
if flt(doc.percent_complete) > 0.0 and flt(doc.percent_complete) < 100.0:
status = 'In Process'
elif flt(doc.percent_complete) == 100.0:
status = 'Completed'
return status
def set_employee_name(doc): def set_employee_name(doc):
if doc.employee and not doc.employee_name: if doc.employee and not doc.employee_name:
doc.employee_name = frappe.db.get_value("Employee", doc.employee, "employee_name") doc.employee_name = frappe.db.get_value("Employee", doc.employee, "employee_name")
@ -522,3 +409,8 @@ def share_doc_with_approver(doc, user):
approver = approvers.get(doc.doctype) approver = approvers.get(doc.doctype)
if doc_before_save.get(approver) != doc.get(approver): if doc_before_save.get(approver) != doc.get(approver):
frappe.share.remove(doc.doctype, doc.name, doc_before_save.get(approver)) frappe.share.remove(doc.doctype, doc.name, doc_before_save.get(approver))
def validate_active_employee(employee):
if frappe.db.get_value("Employee", employee, "status") == "Inactive":
frappe.throw(_("Transactions cannot be created for an Inactive Employee {0}.").format(
get_link_to_form("Employee", employee)), InactiveEmployeeStatusError)

View File

@ -14,7 +14,7 @@ frappe.ui.form.on('Loan Application', {
refresh: function(frm) { refresh: function(frm) {
frm.trigger("toggle_fields"); frm.trigger("toggle_fields");
frm.trigger("add_toolbar_buttons"); frm.trigger("add_toolbar_buttons");
frm.set_query("loan_type", () => { frm.set_query('loan_type', () => {
return { return {
filters: { filters: {
company: frm.doc.company company: frm.doc.company

View File

@ -748,7 +748,7 @@ def get_valuation_rate(args):
if valuation_rate <= 0: if valuation_rate <= 0:
last_valuation_rate = frappe.db.sql("""select valuation_rate last_valuation_rate = frappe.db.sql("""select valuation_rate
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where item_code = %s and valuation_rate > 0 where item_code = %s and valuation_rate > 0 and is_cancelled = 0
order by posting_date desc, posting_time desc, creation desc limit 1""", args['item_code']) order by posting_date desc, posting_time desc, creation desc limit 1""", args['item_code'])
valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0 valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0
@ -1069,13 +1069,6 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
if barcodes: if barcodes:
or_cond_filters["name"] = ("in", barcodes) or_cond_filters["name"] = ("in", barcodes)
for cond in get_match_cond(doctype, as_condition=False):
for key, value in cond.items():
if key == doctype:
key = "name"
query_filters[key] = ("in", value)
if filters and filters.get("item_code"): if filters and filters.get("item_code"):
has_variants = frappe.get_cached_value("Item", filters.get("item_code"), "has_variants") has_variants = frappe.get_cached_value("Item", filters.get("item_code"), "has_variants")
if not has_variants: if not has_variants:
@ -1084,7 +1077,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
if filters and filters.get("is_stock_item"): if filters and filters.get("is_stock_item"):
query_filters["is_stock_item"] = 1 query_filters["is_stock_item"] = 1
return frappe.get_all("Item", return frappe.get_list("Item",
fields = fields, filters=query_filters, fields = fields, filters=query_filters,
or_filters = or_cond_filters, order_by=order_by, or_filters = or_cond_filters, order_by=order_by,
limit_start=start, limit_page_length=page_len, as_list=1) limit_start=start, limit_page_length=page_len, as_list=1)

View File

@ -192,11 +192,11 @@ class JobCard(Document):
"completed_qty": args.get("completed_qty") or 0.0 "completed_qty": args.get("completed_qty") or 0.0
}) })
elif args.get("start_time"): elif args.get("start_time"):
new_args = { new_args = frappe._dict({
"from_time": get_datetime(args.get("start_time")), "from_time": get_datetime(args.get("start_time")),
"operation": args.get("sub_operation"), "operation": args.get("sub_operation"),
"completed_qty": 0.0 "completed_qty": 0.0
} })
if employees: if employees:
for name in employees: for name in employees:

View File

@ -297,4 +297,6 @@ erpnext.patches.v13_0.update_level_in_bom #1234sswef
erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
erpnext.patches.v13_0.update_subscription_status_in_memberships erpnext.patches.v13_0.update_subscription_status_in_memberships
erpnext.patches.v13_0.update_amt_in_work_order_required_items erpnext.patches.v13_0.update_amt_in_work_order_required_items
erpnext.patches.v13_0.update_export_type_for_gst
erpnext.patches.v13_0.update_tds_check_field #3
erpnext.patches.v13_0.add_custom_field_for_south_africa erpnext.patches.v13_0.add_custom_field_for_south_africa

View File

@ -10,6 +10,7 @@ def execute():
if not frappe.db.has_column('Work Order', 'has_batch_no'): if not frappe.db.has_column('Work Order', 'has_batch_no'):
return return
frappe.reload_doc('manufacturing', 'doctype', 'manufacturing_settings')
if cint(frappe.db.get_single_value('Manufacturing Settings', 'make_serial_no_batch_from_work_order')): if cint(frappe.db.get_single_value('Manufacturing Settings', 'make_serial_no_batch_from_work_order')):
return return
@ -107,4 +108,4 @@ def repost_future_sle_and_gle(doc):
"company": doc.company "company": doc.company
}) })
create_repost_item_valuation_entry(args) create_repost_item_valuation_entry(args)

View File

@ -37,7 +37,7 @@ def execute():
if frappe.db.exists('DocType', 'Opportunity'): if frappe.db.exists('DocType', 'Opportunity'):
opportunities = frappe.db.get_all('Opportunity', fields=['name', 'mins_to_first_response'], order_by='creation desc') opportunities = frappe.db.get_all('Opportunity', fields=['name', 'mins_to_first_response'], order_by='creation desc')
frappe.reload_doc('crm', 'doctype', 'opportunity') frappe.reload_doctype('Opportunity', force=True)
rename_field('Opportunity', 'mins_to_first_response', 'first_response_time') rename_field('Opportunity', 'mins_to_first_response', 'first_response_time')
# change fieldtype to duration # change fieldtype to duration

View File

@ -0,0 +1,24 @@
import frappe
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
# Update custom fields
fieldname = frappe.db.get_value('Custom Field', {'dt': 'Customer', 'fieldname': 'export_type'})
if fieldname:
frappe.db.set_value('Custom Field', fieldname, 'default', '')
fieldname = frappe.db.get_value('Custom Field', {'dt': 'Supplier', 'fieldname': 'export_type'})
if fieldname:
frappe.db.set_value('Custom Field', fieldname, 'default', '')
# Update Customer/Supplier Masters
frappe.db.sql("""
UPDATE `tabCustomer` set export_type = '' WHERE gst_category NOT IN ('SEZ', 'Overseas', 'Deemed Export')
""")
frappe.db.sql("""
UPDATE `tabSupplier` set export_type = '' WHERE gst_category NOT IN ('SEZ', 'Overseas')
""")

View File

@ -0,0 +1,9 @@
import frappe
def execute():
if frappe.db.has_table("Tax Withholding Category") \
and frappe.db.has_column("Tax Withholding Category", "round_off_tax_amount"):
frappe.db.sql("""
UPDATE `tabTax Withholding Category` set round_off_tax_amount = 0
WHERE round_off_tax_amount IS NULL
""")

View File

@ -7,6 +7,7 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe import _, bold from frappe import _, bold
from frappe.utils import getdate, date_diff, comma_and, formatdate from frappe.utils import getdate, date_diff, comma_and, formatdate
from erpnext.hr.utils import validate_active_employee
class AdditionalSalary(Document): class AdditionalSalary(Document):
def on_submit(self): def on_submit(self):
@ -19,6 +20,7 @@ class AdditionalSalary(Document):
self.update_employee_referral(cancel=True) self.update_employee_referral(cancel=True)
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.validate_dates() self.validate_dates()
self.validate_salary_structure() self.validate_salary_structure()
self.validate_recurring_additional_salary_overlap() self.validate_recurring_additional_salary_overlap()
@ -110,11 +112,11 @@ class AdditionalSalary(Document):
no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1 no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1
return amount_per_day * no_of_days return amount_per_day * no_of_days
@frappe.whitelist()
def get_additional_salaries(employee, start_date, end_date, component_type): def get_additional_salaries(employee, start_date, end_date, component_type):
additional_salary_list = frappe.db.sql(""" additional_salary_list = frappe.db.sql("""
select name, salary_component as component, type, amount, select name, salary_component as component, type, amount, overwrite_salary_structure_amount as overwrite,
overwrite_salary_structure_amount as overwrite, deduct_full_tax_on_selected_payroll_date, is_recurring
deduct_full_tax_on_selected_payroll_date
from `tabAdditional Salary` from `tabAdditional Salary`
where employee=%(employee)s where employee=%(employee)s
and docstatus = 1 and docstatus = 1

View File

@ -9,10 +9,11 @@ from frappe.utils import date_diff, getdate, rounded, add_days, cstr, cint, flt
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period_days, get_period_factor from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period_days, get_period_factor
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure
from erpnext.hr.utils import get_sal_slip_total_benefit_given, get_holidays_for_employee, get_previous_claimed_amount from erpnext.hr.utils import get_sal_slip_total_benefit_given, get_holidays_for_employee, get_previous_claimed_amount, validate_active_employee
class EmployeeBenefitApplication(Document): class EmployeeBenefitApplication(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.validate_duplicate_on_payroll_period() self.validate_duplicate_on_payroll_period()
if not self.max_benefits: if not self.max_benefits:
self.max_benefits = get_max_benefits_remaining(self.employee, self.date, self.payroll_period) self.max_benefits = get_max_benefits_remaining(self.employee, self.date, self.payroll_period)

View File

@ -8,12 +8,13 @@ from frappe import _
from frappe.utils import flt from frappe.utils import flt
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_max_benefits from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_max_benefits
from erpnext.hr.utils import get_previous_claimed_amount from erpnext.hr.utils import get_previous_claimed_amount, validate_active_employee
from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure
class EmployeeBenefitClaim(Document): class EmployeeBenefitClaim(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
max_benefits = get_max_benefits(self.employee, self.claim_date) max_benefits = get_max_benefits(self.employee, self.claim_date)
if not max_benefits or max_benefits <= 0: if not max_benefits or max_benefits <= 0:
frappe.throw(_("Employee {0} has no maximum benefit amount").format(self.employee)) frappe.throw(_("Employee {0} has no maximum benefit amount").format(self.employee))

View File

@ -6,9 +6,11 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import validate_active_employee
class EmployeeIncentive(Document): class EmployeeIncentive(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.validate_salary_structure() self.validate_salary_structure()
def validate_salary_structure(self): def validate_salary_structure(self):

View File

@ -8,11 +8,12 @@ from frappe.model.document import Document
from frappe import _ from frappe import _
from frappe.utils import flt from frappe.utils import flt
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, \ from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, validate_active_employee, \
calculate_annual_eligible_hra_exemption, validate_duplicate_exemption_for_payroll_period calculate_annual_eligible_hra_exemption, validate_duplicate_exemption_for_payroll_period
class EmployeeTaxExemptionDeclaration(Document): class EmployeeTaxExemptionDeclaration(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
validate_tax_declaration(self.declarations) validate_tax_declaration(self.declarations)
validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee) validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee)
self.set_total_declared_amount() self.set_total_declared_amount()

View File

@ -7,11 +7,12 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe import _ from frappe import _
from frappe.utils import flt from frappe.utils import flt
from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, \ from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, validate_active_employee, \
calculate_hra_exemption_for_period, validate_duplicate_exemption_for_payroll_period calculate_hra_exemption_for_period, validate_duplicate_exemption_for_payroll_period
class EmployeeTaxExemptionProofSubmission(Document): class EmployeeTaxExemptionProofSubmission(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
validate_tax_declaration(self.tax_exemption_proofs) validate_tax_declaration(self.tax_exemption_proofs)
self.set_total_actual_amount() self.set_total_actual_amount()
self.set_total_exemption_amount() self.set_total_exemption_amount()

View File

@ -7,11 +7,10 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe import _ from frappe import _
from frappe.utils import getdate from frappe.utils import getdate
from erpnext.hr.utils import validate_active_employee
class RetentionBonus(Document): class RetentionBonus(Document):
def validate(self): def validate(self):
if frappe.get_value('Employee', self.employee, 'status') != 'Active': validate_active_employee(self.employee)
frappe.throw(_('Cannot create Retention Bonus for Left or Inactive Employees'))
if getdate(self.bonus_payment_date) < getdate(): if getdate(self.bonus_payment_date) < getdate():
frappe.throw(_('Bonus Payment Date cannot be a past date')) frappe.throw(_('Bonus Payment Date cannot be a past date'))

View File

@ -12,6 +12,7 @@
"year_to_date", "year_to_date",
"section_break_5", "section_break_5",
"additional_salary", "additional_salary",
"is_recurring_additional_salary",
"statistical_component", "statistical_component",
"depends_on_payment_days", "depends_on_payment_days",
"exempted_from_income_tax", "exempted_from_income_tax",
@ -235,11 +236,19 @@
"label": "Year To Date", "label": "Year To Date",
"options": "currency", "options": "currency",
"read_only": 1 "read_only": 1
},
{
"default": "0",
"depends_on": "eval:doc.parenttype=='Salary Slip' && doc.parentfield=='earnings' && doc.additional_salary",
"fieldname": "is_recurring_additional_salary",
"fieldtype": "Check",
"label": "Is Recurring Additional Salary",
"read_only": 1
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-01-14 13:39:15.847158", "modified": "2021-03-14 13:39:15.847158",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Salary Detail", "name": "Salary Detail",

View File

@ -7,18 +7,19 @@ import datetime, math
from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate, get_first_day from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate, get_first_day
from frappe.model.naming import make_autoname from frappe.model.naming import make_autoname
from frappe.utils.background_jobs import enqueue
from frappe import msgprint, _ from frappe import msgprint, _
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.utilities.transaction_base import TransactionBase from erpnext.utilities.transaction_base import TransactionBase
from frappe.utils.background_jobs import enqueue
from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries
from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount
from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from erpnext.hr.utils import validate_active_employee
from six import iteritems from six import iteritems
class SalarySlip(TransactionBase): class SalarySlip(TransactionBase):
@ -39,6 +40,7 @@ class SalarySlip(TransactionBase):
def validate(self): def validate(self):
self.status = self.get_status() self.status = self.get_status()
validate_active_employee(self.employee)
self.validate_dates() self.validate_dates()
self.check_existing() self.check_existing()
if not self.salary_slip_based_on_timesheet: if not self.salary_slip_based_on_timesheet:
@ -616,7 +618,8 @@ class SalarySlip(TransactionBase):
get_salary_component_data(additional_salary.component), get_salary_component_data(additional_salary.component),
additional_salary.amount, additional_salary.amount,
component_type, component_type,
additional_salary additional_salary,
is_recurring = additional_salary.is_recurring
) )
def add_tax_components(self, payroll_period): def add_tax_components(self, payroll_period):
@ -637,7 +640,7 @@ class SalarySlip(TransactionBase):
tax_row = get_salary_component_data(d) tax_row = get_salary_component_data(d)
self.update_component_row(tax_row, tax_amount, "deductions") self.update_component_row(tax_row, tax_amount, "deductions")
def update_component_row(self, component_data, amount, component_type, additional_salary=None): def update_component_row(self, component_data, amount, component_type, additional_salary=None, is_recurring = 0):
component_row = None component_row = None
for d in self.get(component_type): for d in self.get(component_type):
if d.salary_component != component_data.salary_component: if d.salary_component != component_data.salary_component:
@ -675,6 +678,7 @@ class SalarySlip(TransactionBase):
component_row.set(attr, component_data.get(attr)) component_row.set(attr, component_data.get(attr))
if additional_salary: if additional_salary:
component_row.is_recurring_additional_salary = is_recurring
component_row.default_amount = 0 component_row.default_amount = 0
component_row.additional_amount = amount component_row.additional_amount = amount
component_row.additional_salary = additional_salary.name component_row.additional_salary = additional_salary.name
@ -708,6 +712,7 @@ class SalarySlip(TransactionBase):
# get remaining numbers of sub-period (period for which one salary is processed) # get remaining numbers of sub-period (period for which one salary is processed)
remaining_sub_periods = get_period_factor(self.employee, remaining_sub_periods = get_period_factor(self.employee,
self.start_date, self.end_date, self.payroll_frequency, payroll_period)[1] self.start_date, self.end_date, self.payroll_frequency, payroll_period)[1]
# get taxable_earnings, paid_taxes for previous period # get taxable_earnings, paid_taxes for previous period
previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date, previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date,
self.start_date, tax_slab.allow_tax_exemption) self.start_date, tax_slab.allow_tax_exemption)
@ -867,8 +872,16 @@ class SalarySlip(TransactionBase):
if earning.is_tax_applicable: if earning.is_tax_applicable:
if additional_amount: if additional_amount:
taxable_earnings += (amount - additional_amount) if not earning.is_recurring_additional_salary:
additional_income += additional_amount taxable_earnings += (amount - additional_amount)
additional_income += additional_amount
else:
to_date = frappe.db.get_value("Additional Salary", earning.additional_salary, 'to_date')
period = (getdate(to_date).month - getdate(self.start_date).month) + 1
if period > 0:
taxable_earnings += (amount - additional_amount) * period
additional_income += additional_amount * period
if earning.deduct_full_tax_on_selected_payroll_date: if earning.deduct_full_tax_on_selected_payroll_date:
additional_income_with_full_tax += additional_amount additional_income_with_full_tax += additional_amount
continue continue

View File

@ -129,7 +129,7 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None,
"earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), "earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
"deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), "deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
"payroll_frequency": payroll_frequency, "payroll_frequency": payroll_frequency,
"payment_account": get_random("Account", filters={"account_currency": currency}), "payment_account": get_random("Account", filters={'account_currency': currency}),
"currency": currency "currency": currency
} }
if other_details and isinstance(other_details, dict): if other_details and isinstance(other_details, dict):

View File

@ -14,6 +14,7 @@ from erpnext.hr.doctype.daily_work_summary.daily_work_summary import get_users_e
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list
from erpnext.controllers.employee_boarding_controller import update_employee_boarding_status
class Project(Document): class Project(Document):
def get_feed(self): def get_feed(self):
@ -37,6 +38,7 @@ class Project(Document):
self.send_welcome_email() self.send_welcome_email()
self.update_costing() self.update_costing()
self.update_percent_complete() self.update_percent_complete()
update_employee_boarding_status(self)
def copy_from_template(self): def copy_from_template(self):
''' '''
@ -132,6 +134,7 @@ class Project(Document):
def update_project(self): def update_project(self):
'''Called externally by Task''' '''Called externally by Task'''
self.update_percent_complete() self.update_percent_complete()
update_employee_boarding_status(self)
self.update_costing() self.update_costing()
self.db_update() self.db_update()

View File

@ -15,12 +15,15 @@ from erpnext.manufacturing.doctype.workstation.workstation import (check_if_with
WorkstationHolidayError) WorkstationHolidayError)
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
from erpnext.setup.utils import get_exchange_rate from erpnext.setup.utils import get_exchange_rate
from erpnext.hr.utils import validate_active_employee
class OverlapError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError): pass
class OverWorkLoggedError(frappe.ValidationError): pass class OverWorkLoggedError(frappe.ValidationError): pass
class Timesheet(Document): class Timesheet(Document):
def validate(self): def validate(self):
if self.employee:
validate_active_employee(self.employee)
self.set_employee_name() self.set_employee_name()
self.set_status() self.set_status()
self.validate_dates() self.validate_dates()

View File

@ -16,7 +16,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
doctype: "Bank Transaction", doctype: "Bank Transaction",
filters: { name: this.bank_transaction_name }, filters: { name: this.bank_transaction_name },
fieldname: [ fieldname: [
"date", "date as reference_date",
"deposit", "deposit",
"withdrawal", "withdrawal",
"currency", "currency",

View File

@ -3,7 +3,7 @@
frappe.ui.form.on('E Invoice Settings', { frappe.ui.form.on('E Invoice Settings', {
refresh(frm) { refresh(frm) {
const docs_link = 'https://docs.erpnext.com/docs/user/manual/en/regional/india/setup-e-invoicing'; const docs_link = 'https://docs.erpnext.com/docs/v13/user/manual/en/regional/india/setup-e-invoicing';
frm.dashboard.set_headline( frm.dashboard.set_headline(
__("Read {0} for more information on E Invoicing features.", [`<a href='${docs_link}'>documentation</a>`]) __("Read {0} for more information on E Invoicing features.", [`<a href='${docs_link}'>documentation</a>`])
); );

View File

@ -280,9 +280,15 @@ class GSTR3BReport(Document):
if self.get('invoice_items'): if self.get('invoice_items'):
# Build itemised tax for export invoices, nil and exempted where tax table is blank # Build itemised tax for export invoices, nil and exempted where tax table is blank
for invoice, items in iteritems(self.invoice_items): for invoice, items in iteritems(self.invoice_items):
if invoice not in self.items_based_on_tax_rate and (self.invoice_detail_map.get(invoice, {}).get('export_type') if invoice not in self.items_based_on_tax_rate and self.invoice_detail_map.get(invoice, {}).get('export_type') \
== "Without Payment of Tax"): == "Without Payment of Tax" and self.invoice_detail_map.get(invoice, {}).get('gst_category') == "Overseas":
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
else:
for item in items.keys():
if item in self.is_nil_exempt + self.is_non_gst and \
item not in self.items_based_on_tax_rate.get(invoice, {}).get(0, []):
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, [])
self.items_based_on_tax_rate[invoice][0].append(item)
def set_outward_taxable_supplies(self): def set_outward_taxable_supplies(self):
inter_state_supply_details = {} inter_state_supply_details = {}

View File

@ -641,7 +641,6 @@ def make_custom_fields(update=True):
'label': 'Export Type', 'label': 'Export Type',
'fieldtype': 'Select', 'fieldtype': 'Select',
'insert_after': 'gst_category', 'insert_after': 'gst_category',
'default': 'Without Payment of Tax',
'depends_on':'eval:in_list(["SEZ", "Overseas"], doc.gst_category)', 'depends_on':'eval:in_list(["SEZ", "Overseas"], doc.gst_category)',
'options': '\nWith Payment of Tax\nWithout Payment of Tax' 'options': '\nWith Payment of Tax\nWithout Payment of Tax'
} }
@ -660,7 +659,6 @@ def make_custom_fields(update=True):
'label': 'Export Type', 'label': 'Export Type',
'fieldtype': 'Select', 'fieldtype': 'Select',
'insert_after': 'gst_category', 'insert_after': 'gst_category',
'default': 'Without Payment of Tax',
'depends_on':'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', 'depends_on':'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)',
'options': '\nWith Payment of Tax\nWithout Payment of Tax' 'options': '\nWith Payment of Tax\nWithout Payment of Tax'
} }

View File

@ -286,7 +286,8 @@ class Gstr1Report(object):
# Build itemised tax for export invoices where tax table is blank # Build itemised tax for export invoices where tax table is blank
for invoice, items in iteritems(self.invoice_items): for invoice, items in iteritems(self.invoice_items):
if invoice not in self.items_based_on_tax_rate and invoice not in unidentified_gst_accounts_invoice \ if invoice not in self.items_based_on_tax_rate and invoice not in unidentified_gst_accounts_invoice \
and frappe.db.get_value(self.doctype, invoice, "export_type") == "Without Payment of Tax": and self.invoices.get(invoice, {}).get('export_type') == "Without Payment of Tax" \
and self.invoices.get(invoice, {}).get('gst_category') == "Overseas":
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
def get_columns(self): def get_columns(self):

View File

@ -1 +0,0 @@
Sales campaign / promotion, like special discount, exhibition, newsletter etc.

View File

@ -1 +0,0 @@
from __future__ import unicode_literals

View File

@ -1,15 +0,0 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Campaign", "refresh", function(frm) {
erpnext.toggle_naming_series();
if(frm.doc.__islocal) {
frm.toggle_display("naming_series", frappe.boot.sysdefaults.campaign_naming_by=="Naming Series");
}
else{
cur_frm.add_custom_button(__("View Leads"), function() {
frappe.route_options = {"source": "Campaign","campaign_name": frm.doc.name}
frappe.set_route("List", "Lead");
}, "fa fa-list", true);
}
})

View File

@ -1,17 +0,0 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'fieldname': 'campaign_name',
'transactions': [
{
'label': _('Email Campaigns'),
'items': ['Email Campaign']
},
{
'label': _('Social Media Campaigns'),
'items': ['Social Media Post']
}
]
}

View File

@ -1,7 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
test_records = frappe.get_test_records('Campaign')

View File

@ -1,10 +0,0 @@
[
{
"campaign_name": "_Test Campaign",
"doctype": "Campaign"
},
{
"campaign_name": "_Test Campaign 1",
"doctype": "Campaign"
}
]

View File

@ -62,12 +62,12 @@ class TestCurrencyExchange(unittest.TestCase):
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30", "for_selling") exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30", "for_selling")
self.assertEqual(exchange_rate, 62.9) self.assertEqual(exchange_rate, 62.9)
# Exchange rate as on 15th Dec, 2015, should be fetched from fixer.io # Exchange rate as on 15th Dec, 2015
self.clear_cache() self.clear_cache()
exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_selling") exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_selling")
self.assertFalse(exchange_rate == 60) self.assertFalse(exchange_rate == 60)
self.assertEqual(flt(exchange_rate, 3), 66.894) self.assertEqual(flt(exchange_rate, 3), 66.999)
def test_exchange_rate_strict(self): def test_exchange_rate_strict(self):
# strict currency settings # strict currency settings
@ -77,28 +77,17 @@ class TestCurrencyExchange(unittest.TestCase):
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-01", "for_buying") exchange_rate = get_exchange_rate("USD", "INR", "2016-01-01", "for_buying")
self.assertEqual(exchange_rate, 60.0) self.assertEqual(exchange_rate, 60.0)
# Will fetch from fixer.io
self.clear_cache() self.clear_cache()
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying") exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying")
self.assertEqual(flt(exchange_rate, 3), 67.79) self.assertEqual(flt(exchange_rate, 3), 67.235)
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30", "for_selling") exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30", "for_selling")
self.assertEqual(exchange_rate, 62.9) self.assertEqual(exchange_rate, 62.9)
# Exchange rate as on 15th Dec, 2015, should be fetched from fixer.io # Exchange rate as on 15th Dec, 2015
self.clear_cache() self.clear_cache()
exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_buying") exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_buying")
self.assertEqual(flt(exchange_rate, 3), 66.894) self.assertEqual(flt(exchange_rate, 3), 66.999)
exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-10", "for_selling")
self.assertEqual(exchange_rate, 65.1)
# NGN is not available on fixer.io so these should return 0
exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-09", "for_selling")
self.assertEqual(exchange_rate, 0)
exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-11", "for_selling")
self.assertEqual(exchange_rate, 0)
def test_exchange_rate_strict_switched(self): def test_exchange_rate_strict_switched(self):
# Start with allow_stale is True # Start with allow_stale is True
@ -111,4 +100,4 @@ class TestCurrencyExchange(unittest.TestCase):
# Will fetch from fixer.io # Will fetch from fixer.io
self.clear_cache() self.clear_cache()
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying") exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying")
self.assertEqual(flt(exchange_rate, 3), 67.79) self.assertEqual(flt(exchange_rate, 3), 67.235)

View File

@ -18,7 +18,7 @@ class TransactionDeletionRecord(Document):
doctypes_to_be_ignored_list = get_doctypes_to_be_ignored() doctypes_to_be_ignored_list = get_doctypes_to_be_ignored()
for doctype in self.doctypes_to_be_ignored: for doctype in self.doctypes_to_be_ignored:
if doctype.doctype_name not in doctypes_to_be_ignored_list: if doctype.doctype_name not in doctypes_to_be_ignored_list:
frappe.throw(_("DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it. "), frappe.throw(_("DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it."),
title=_("Not Allowed")) title=_("Not Allowed"))
def before_submit(self): def before_submit(self):
@ -31,7 +31,7 @@ class TransactionDeletionRecord(Document):
clear_notifications() clear_notifications()
self.delete_company_transactions() self.delete_company_transactions()
def populate_doctypes_to_be_ignored_table(self): def populate_doctypes_to_be_ignored_table(self):
doctypes_to_be_ignored_list = get_doctypes_to_be_ignored() doctypes_to_be_ignored_list = get_doctypes_to_be_ignored()
for doctype in doctypes_to_be_ignored_list: for doctype in doctypes_to_be_ignored_list:
self.append('doctypes_to_be_ignored', { self.append('doctypes_to_be_ignored', {
@ -74,7 +74,7 @@ class TransactionDeletionRecord(Document):
doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list() doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list()
docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list) docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list)
tables = self.get_all_child_doctypes() tables = self.get_all_child_doctypes()
for docfield in docfields: for docfield in docfields:
if docfield['parent'] != self.doctype: if docfield['parent'] != self.doctype:
no_of_docs = self.get_number_of_docs_linked_with_specified_company(docfield['parent'], docfield['fieldname']) no_of_docs = self.get_number_of_docs_linked_with_specified_company(docfield['parent'], docfield['fieldname'])
@ -90,7 +90,7 @@ class TransactionDeletionRecord(Document):
naming_series = frappe.db.get_value('DocType', docfield['parent'], 'autoname') naming_series = frappe.db.get_value('DocType', docfield['parent'], 'autoname')
if naming_series: if naming_series:
if '#' in naming_series: if '#' in naming_series:
self.update_naming_series(naming_series, docfield['parent']) self.update_naming_series(naming_series, docfield['parent'])
def get_doctypes_to_be_ignored_list(self): def get_doctypes_to_be_ignored_list(self):
singles = frappe.get_all('DocType', filters = {'issingle': 1}, pluck = 'name') singles = frappe.get_all('DocType', filters = {'issingle': 1}, pluck = 'name')
@ -101,9 +101,9 @@ class TransactionDeletionRecord(Document):
return doctypes_to_be_ignored_list return doctypes_to_be_ignored_list
def get_doctypes_with_company_field(self, doctypes_to_be_ignored_list): def get_doctypes_with_company_field(self, doctypes_to_be_ignored_list):
docfields = frappe.get_all('DocField', docfields = frappe.get_all('DocField',
filters = { filters = {
'fieldtype': 'Link', 'fieldtype': 'Link',
'options': 'Company', 'options': 'Company',
'parent': ['not in', doctypes_to_be_ignored_list]}, 'parent': ['not in', doctypes_to_be_ignored_list]},
fields=['parent', 'fieldname']) fields=['parent', 'fieldname'])
@ -121,7 +121,7 @@ class TransactionDeletionRecord(Document):
self.append('doctypes', { self.append('doctypes', {
'doctype_name' : doctype, 'doctype_name' : doctype,
'no_of_docs' : no_of_docs 'no_of_docs' : no_of_docs
}) })
def delete_child_tables(self, doctype, company_fieldname): def delete_child_tables(self, doctype, company_fieldname):
parent_docs_to_be_deleted = frappe.get_all(doctype, { parent_docs_to_be_deleted = frappe.get_all(doctype, {
@ -129,7 +129,7 @@ class TransactionDeletionRecord(Document):
}, pluck = 'name') }, pluck = 'name')
child_tables = frappe.get_all('DocField', filters = { child_tables = frappe.get_all('DocField', filters = {
'fieldtype': 'Table', 'fieldtype': 'Table',
'parent': doctype 'parent': doctype
}, pluck = 'options') }, pluck = 'options')

View File

@ -93,21 +93,21 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
try: try:
cache = frappe.cache() cache = frappe.cache()
key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date,from_currency, to_currency) key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date, from_currency, to_currency)
value = cache.get(key) value = cache.get(key)
if not value: if not value:
import requests import requests
api_url = "https://frankfurter.app/{0}".format(transaction_date) api_url = "https://api.exchangerate.host/convert"
response = requests.get(api_url, params={ response = requests.get(api_url, params={
"base": from_currency, "date": transaction_date,
"symbols": to_currency "from": from_currency,
"to": to_currency
}) })
# expire in 6 hours # expire in 6 hours
response.raise_for_status() response.raise_for_status()
value = response.json()["rates"][to_currency] value = response.json()["result"]
cache.setex(name=key, time=21600, value=flt(value))
cache.set_value(key, value, expires_in_sec=6 * 60 * 60)
return flt(value) return flt(value)
except: except:
frappe.log_error(title="Get Exchange Rate") frappe.log_error(title="Get Exchange Rate")

View File

@ -162,19 +162,19 @@ def get_batch_qty(batch_no=None, warehouse=None, item_code=None, posting_date=No
out = float(frappe.db.sql("""select sum(actual_qty) out = float(frappe.db.sql("""select sum(actual_qty)
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where warehouse=%s and batch_no=%s {0}""".format(cond), where is_cancelled = 0 and warehouse=%s and batch_no=%s {0}""".format(cond),
(warehouse, batch_no))[0][0] or 0) (warehouse, batch_no))[0][0] or 0)
if batch_no and not warehouse: if batch_no and not warehouse:
out = frappe.db.sql('''select warehouse, sum(actual_qty) as qty out = frappe.db.sql('''select warehouse, sum(actual_qty) as qty
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where batch_no=%s where is_cancelled = 0 and batch_no=%s
group by warehouse''', batch_no, as_dict=1) group by warehouse''', batch_no, as_dict=1)
if not batch_no and item_code and warehouse: if not batch_no and item_code and warehouse:
out = frappe.db.sql('''select batch_no, sum(actual_qty) as qty out = frappe.db.sql('''select batch_no, sum(actual_qty) as qty
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where item_code = %s and warehouse=%s where is_cancelled = 0 and item_code = %s and warehouse=%s
group by batch_no''', (item_code, warehouse), as_dict=1) group by batch_no''', (item_code, warehouse), as_dict=1)
return out return out

View File

@ -162,8 +162,15 @@ class MaterialRequest(BuyingController):
from `tabStock Entry Detail` where material_request = %s from `tabStock Entry Detail` where material_request = %s
and material_request_item = %s and docstatus = 1""", and material_request_item = %s and docstatus = 1""",
(self.name, d.name))[0][0]) (self.name, d.name))[0][0])
mr_qty_allowance = frappe.db.get_single_value('Stock Settings', 'mr_qty_allowance')
if d.ordered_qty and d.ordered_qty > d.stock_qty: if mr_qty_allowance:
allowed_qty = d.qty + (d.qty * (mr_qty_allowance/100))
if d.ordered_qty and d.ordered_qty > allowed_qty:
frappe.throw(_("The total Issue / Transfer quantity {0} in Material Request {1} \
cannot be greater than allowed requested quantity {2} for Item {3}").format(d.ordered_qty, d.parent, allowed_qty, d.item_code))
elif d.ordered_qty and d.ordered_qty > d.stock_qty:
frappe.throw(_("The total Issue / Transfer quantity {0} in Material Request {1} \ frappe.throw(_("The total Issue / Transfer quantity {0} in Material Request {1} \
cannot be greater than requested quantity {2} for Item {3}").format(d.ordered_qty, d.parent, d.qty, d.item_code)) cannot be greater than requested quantity {2} for Item {3}").format(d.ordered_qty, d.parent, d.qty, d.item_code))

View File

@ -329,6 +329,58 @@ class TestMaterialRequest(unittest.TestCase):
self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0)
self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0)
def test_over_transfer_qty_allowance(self):
mr = frappe.new_doc('Material Request')
mr.company = "_Test Company"
mr.scheduled_date = today()
mr.append('items',{
"item_code": "_Test FG Item",
"item_name": "_Test FG Item",
"qty": 10,
"schedule_date": today(),
"uom": "_Test UOM 1",
"warehouse": "_Test Warehouse - _TC"
})
mr.material_request_type = "Material Transfer"
mr.insert()
mr.submit()
frappe.db.set_value('Stock Settings', None, 'mr_qty_allowance', 20)
# map a stock entry
se_doc = make_stock_entry(mr.name)
se_doc.update({
"posting_date": today(),
"posting_time": "00:00",
})
se_doc.get("items")[0].update({
"qty": 13,
"transfer_qty": 12.0,
"s_warehouse": "_Test Warehouse - _TC",
"t_warehouse": "_Test Warehouse 1 - _TC",
"basic_rate": 1.0
})
# make available the qty in _Test Warehouse 1 before transfer
sr = frappe.new_doc("Stock Reconciliation")
sr.company = "_Test Company"
sr.purpose = "Opening Stock"
sr.append('items', {
"item_code": "_Test FG Item",
"warehouse": "_Test Warehouse - _TC",
"qty": 20,
"valuation_rate": 0.01
})
sr.insert()
sr.submit()
se = frappe.copy_doc(se_doc)
se.insert()
self.assertRaises(frappe.ValidationError)
se.items[0].qty = 12
se.submit()
def test_completed_qty_for_over_transfer(self): def test_completed_qty_for_over_transfer(self):
existing_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") existing_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
existing_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") existing_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")

View File

@ -239,6 +239,7 @@ def get_available_item_locations_for_batched_item(item_code, from_warehouses, re
and sle.`item_code`=%(item_code)s and sle.`item_code`=%(item_code)s
and sle.`company` = %(company)s and sle.`company` = %(company)s
and batch.disabled = 0 and batch.disabled = 0
and sle.is_cancelled=0
and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s
{warehouse_condition} {warehouse_condition}
GROUP BY GROUP BY

View File

@ -352,7 +352,7 @@ class PurchaseReceipt(BuyingController):
if self.is_return or flt(d.item_tax_amount): if self.is_return or flt(d.item_tax_amount):
loss_account = expenses_included_in_valuation loss_account = expenses_included_in_valuation
else: else:
loss_account = self.get_company_default("default_expense_account") loss_account = self.get_company_default("default_expense_account", ignore_validation=True) or stock_rbnb
cost_center = d.cost_center or frappe.get_cached_value("Company", self.company, "cost_center") cost_center = d.cost_center or frappe.get_cached_value("Company", self.company, "cost_center")
@ -436,7 +436,7 @@ class PurchaseReceipt(BuyingController):
"cost_center": cost_center, "cost_center": cost_center,
"debit": debit, "debit": debit,
"credit": credit, "credit": credit,
"against_account": against_account, "against": against_account,
"remarks": remarks, "remarks": remarks,
} }

View File

@ -336,10 +336,13 @@ class TestPurchaseReceipt(unittest.TestCase):
se3.cancel() se3.cancel()
po.reload() po.reload()
pr2.load_from_db() pr2.load_from_db()
pr2.cancel()
po.load_from_db() if pr2.docstatus == 1 and frappe.db.get_value('Stock Ledger Entry',
po.cancel() {'voucher_no': pr2.name, 'is_cancelled': 0}, 'name'):
pr2.cancel()
po.load_from_db()
po.cancel()
def test_serial_no_supplier(self): def test_serial_no_supplier(self):
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1)
@ -1044,7 +1047,7 @@ class TestPurchaseReceipt(unittest.TestCase):
'account': srbnb_account, 'account': srbnb_account,
'voucher_detail_no': pr.items[1].name 'voucher_detail_no': pr.items[1].name
}, pluck="name") }, pluck="name")
# check if the entries are not merged into one # check if the entries are not merged into one
# seperate entries should be made since voucher_detail_no is different # seperate entries should be made since voucher_detail_no is different
self.assertEqual(len(item_one_gl_entry), 1) self.assertEqual(len(item_one_gl_entry), 1)
@ -1055,13 +1058,13 @@ class TestPurchaseReceipt(unittest.TestCase):
def test_purchase_receipt_with_exchange_rate_difference(self): def test_purchase_receipt_with_exchange_rate_difference(self):
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice as create_purchase_invoice from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice as create_purchase_invoice
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_purchase_receipt as create_purchase_receipt from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_purchase_receipt as create_purchase_receipt
pi = create_purchase_invoice(company="_Test Company with perpetual inventory", pi = create_purchase_invoice(company="_Test Company with perpetual inventory",
cost_center = "Main - TCP1", cost_center = "Main - TCP1",
warehouse = "Stores - TCP1", warehouse = "Stores - TCP1",
expense_account ="_Test Account Cost for Goods Sold - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1",
currency = "USD", conversion_rate = 70) currency = "USD", conversion_rate = 70)
pr = create_purchase_receipt(pi.name) pr = create_purchase_receipt(pi.name)
pr.conversion_rate = 80 pr.conversion_rate = 80
pr.items[0].purchase_invoice = pi.name pr.items[0].purchase_invoice = pi.name

Some files were not shown because too many files have changed in this diff Show More